본문 바로가기
공부/Spring

[Spring] @Value 동작 방식 및 주의 사항

by 웅대 2023. 5. 18.
728x90

프로젝트를 진행하다보면 시크릿 키와 같이 외부에 노출해서는 안되는 값들을 사용할 때가 있다.

 

만약 코드 내부에 시크릿 키를 저장하고 사용할 경우 깃 허브에 올리면 외부에 노출되게 된다.

 

이럴 경우 application.properties와 같은 파일에 보안이 필요한 값들을 넣어두고 .gitignore로 등록하여 외부에 노출되지 않도록 하곤 한다.

 

그리고 이 application.properties에 보관된 값을 꺼내오는 어노테이션이 바로 @Value이다.

 

@Value를 단순하게 "application.properties에서 값을 꺼내온다"라고만 이해하고 사용하다가 오류가 발생하였다.

 

그래서 한번 @Value에 대해 알아보고 내가 실수했던 내용을 정리해보려고 한다.

 

@Value

@Value 어노테이션은 properties에 보관되어있는 값을 가져오는 역할을 한다.

 

예를 들어서 application.properties에 다음과 같은 값들이 보관되어 있다고 하자.

 

<application.properties>

jwt.secretKey=secretKey
jwt.expiredMs=600000

이를 외부에서 꺼내려면 다음과 같이 @Value 어노테이션을 사용한다.

@Value("${jwt.secretKey}")
private String secretKey;
@Value("${jwt.expiredMs}")
private Long expiredMs;

 

 

그런데 프로젝트를 진행하던 도중 해당 값들이 주입을 받지 못하고 null인 상태로 남아있는 경우가 있었다.

 

우선 다음 코드를 보자.

 

<ValueService>

@Getter
public class ValueService {
    @Value("${jwt.secretKey}")
    private String secretKey;
    @Value("${jwt.expiredMs}")
    private Long expiredMs;
}

@Value 어노테이션을 통해 값을 주입받는 필드 두 개가 존재하는 서비스이다.

 

보다싶이 @Service 어노테이션이 붙어있지 않기 때문에 컴포넌트 스캔의 대상이 되지 않는다.

 

그리고 다음 서비스 코드를 돌려보자.

@SpringBootTest
class ValueServiceTest {
    @Test
    void valueTest() {
        ValueService valueService = new ValueService();
        System.out.println("secretKey = "+ valueService.getSecretKey());
        System.out.println("expiredMs = "+ valueService.getExpiredMs());
        Assertions.assertThat(valueService.getSecretKey()).isEqualTo("secretKey");
        Assertions.assertThat(valueService.getExpiredMs()).isEqualTo(600000L);
    }

만약 서비스가 properties 파일로부터 secretKey와 expiredMs 값을 잘 불러왔다면 테스트가 통과해야 할 것이다.

 

그런데 아래와 같이 null 값이 저장되어 있는 모습을 확인할 수 있다.

이 이유는 @Value의 동작 시점이 의존 관계 주입 시점이기 때문이다.

 

우리는 @Component 어노테이션을 통해서 객체를 스프링 빈으로 등록할 수 있다.

 

컨트롤러, 서비스, 레포지토리 등은 보통 스프링 빈으로 등록해서 의존 관계를 주입한다.

 

@Controller, @Service, @Repository 모두 @Component 어노테이션을 포함하고 있기 때문에 스프링 빈으로 등록할 수 있는 것이다.

 

@Value 어노테이션은 스프링 빈으로 등록을 하고 의존 관계를 주입할 때 동작한다.

 

근데 위 코드는 @Service 어노테이션이 붙어 있지 않기 때문에 컴포넌트 스캔의 대상이 되지 않고 스프링 빈으로 등록되지 않기 때문에 @Value 어노테이션이 동작하지 않는 것이었다.

 

그러면 @Value 어노테이션이 동작하도록 만들기 위해서는 우리가 만든 ValueService 객체를 스프링 빈으로 등록하면 된다.

 

<ValueService>

@Service
@Getter
public class ValueService {
    @Value("${jwt.secretKey}")
    private String secretKey;
    @Value("${jwt.expiredMs}")
    private Long expiredMs;
}

@Service 어노테이션을 붙여서 컴포넌트 스캔의 대상이 되도록 하였다.

 

만약 컨트롤러, 서비스, 레포지토리가 아니라면 그냥 @Component만 붙여도 된다.

 

그리고 Test 코드를 의존 관계를 주입 받도록 변경한다.

@SpringBootTest
class ValueServiceTest {
    @Autowired
    private ValueService valueService;
    @Test
    void valueTest() {
        System.out.println("secretKey = "+ valueService.getSecretKey());
        System.out.println("expiredMs = "+ valueService.getExpiredMs());
        Assertions.assertThat(valueService.getSecretKey()).isEqualTo("secretKey");
        Assertions.assertThat(valueService.getExpiredMs()).isEqualTo(600000L);
    }

}

다시 테스트 코드를 돌려보면 통과하는 모습을 확인할 수 있다.

 

나의 경우 jwt를 생성하는 JwtUtil 클래스에서 오류가 발생했었다.

 

아래 링크는 내가 옛날에 작성했던 JwtUtil 관련 포스팅이다.

https://growth-coder.tistory.com/166

 

[Spring] 스프링 jwt 생성, 검증, 값 가져오기 구현

로그인한 회원임을 인증하기 위해서 토큰 방식이 주로 쓰이곤한다. 로그인에 성공하면 서버에서 토큰을 클라이언트에게 넘겨준다. 그리고 로그인이 필요한 요청을 할 때마다 요청 헤더에 토큰

growth-coder.tistory.com

위 포스팅에서 만들었던 JwtUtil 클래스는 아래와 같다.

<JwtUtil>

public class JwtUtil {
    public static String getMemberEmail(String token, String secretKey) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
                .getBody().get("email", String.class);
    }
    public static boolean isExpired(String token, String secretKey) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
                .getBody().getExpiration().before(new Date());
    }
    public static String createJwt(String secretKey, Long expiredMs, String memberEmail) {
        Claims claims = Jwts.claims();
        claims.put("email", memberEmail);

        String token = Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expiredMs))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
        return token;
    }
}

보다시피 메소드들을 static으로 구현하였고 secretKey와 expiredMs를 외부에서 받도록 구현하였다.

 

그런데 생각해보니 외부에서 주입받는 것 보다는 @Value 어노테이션을 통해서 application.properties에서 받아오는 편이 안전하다는 생각이 들었다.

 

그래서 다음과 같은 코드를 작성하였다.

public class JwtUtil {
    @Value("${jwt.secretKey}")
    private static String secretKey;
    @Value("${jwt.expiredMs}")
    private static Long expiredMs;
    public static String getMemberEmail(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
                .getBody().get("email", String.class);
    }
    public static boolean isExpired(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
                .getBody().getExpiration().before(new Date());
    }
    public static String createJwt(String memberEmail) {
        Claims claims = Jwts.claims();
        claims.put("email", memberEmail);

        String token = Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expiredMs))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
        return token;
    }
}

static 메소드에서 클래스 내부 변수를 사용하려면 똑같이 static이어야 하기 때문에 static을 달아주었다.

 

당연히 제대로 작동하지 않는다. 스프링 빈으로 등록 자체가 되지 않기 때문이다.

 

그래서 @Component 어노테이션을 달아주었음에도 오류가 발생했는데 그 이유는 secretKey와 expiredMs가 static 변수이기 때문이다.

 

static 변수나 메소드는 객체 생성 이전에 메모리 영역에 모두 저장된다.

 

즉 의존성 주입을 받기도 전에 메모리에 저장되는 것이다.

 

해결 방안은 간단했다. 그냥 static 변수나 메소드를 사용하지 않고 JwtUtil 객체를 스프링 빈으로 등록하고 이를 사용할 때는 의존성 주입을 받아서 사용하면 된다.

 

그래서 변경된 JwtUtil 클래스 코드는 다음과 같다.

 

<JwtUtil>

@Component
public class JwtUtil {
    @Value("${jwt.secretKey}")
    private String secretKey;
    @Value("${jwt.expiredMs}")
    private  Long expiredMs;
    public String getMemberEmail(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
                .getBody().get("email", String.class);
    }
    public boolean isExpired(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
                .getBody().getExpiration().before(new Date());
    }
    public String createJwt(String memberEmail) {
        Claims claims = Jwts.claims();
        claims.put("email", memberEmail);

        String token = Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expiredMs))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
        return token;
    }
}

@Value 어노테이션의 동작 방식에 대해서 배웠고 잠시 잊고 있었던 static 변수, 메소드에 대해 복습할 수 있었던 시간이었다.

728x90
반응형

댓글