728x90
@Value란?
DB 접속 정보나 비밀번호와 같이 민감한 정보를 별도의 파일로 분리해서 환경 정보에 맞는 값을 불러오도록 하는 애너테이션이다. application.properties 또는 application.yml에 값을 설정하면 필드나 메서드에 값을 주입해준다.
배경
Refresh Token을 암호화해서 클라이언트에 전달하기 위해 AES128 암호화 클래스를 구현했다. 구현 후 암호화가 잘 이루어지는지 확인하기 위해 테스트를 진행했는데 예외 처리했던 ENCRYPTION_FAILED가 발생했다.
확인해보니 @Value 애너테이션을 통해 application.yml로부터 전달받아야 할 secretKey가 제대로 전달 받지 못하고 null을 반환하기 때문에 발생했다.
AES128Config
@Component
public class AES128Config {
private static final Charset ENCODING_TYPE = StandardCharsets.UTF_8;
private static final String INSTANCE_TYPE = "AES/CBC/PKCS5Padding";
@Value("${aes.secret-key}")
private String secretKey; // @Value가 null을 반환
private IvParameterSpec ivParameterSpec;
private SecretKeySpec secretKeySpec;
private Cipher cipher;
@PostConstruct
public void init() throws NoSuchPaddingException, NoSuchAlgorithmException {
validation(secretKey);
SecureRandom secureRandom = new SecureRandom();
byte[] keyBytes = secretKey.getBytes(ENCODING_TYPE);
secureRandom.nextBytes(keyBytes);
secretKeySpec = new SecretKeySpec(keyBytes, "AES");
ivParameterSpec = new IvParameterSpec(keyBytes);
cipher = Cipher.getInstance(INSTANCE_TYPE);
}
// AES 암호화
public String encryptAes(String plaintext) {
try {
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] encryted = cipher.doFinal(plaintext.getBytes(ENCODING_TYPE));
return new String(Base64.getEncoder().encode(encryted), ENCODING_TYPE);
} catch (Exception e) {
throw new BusinessLogicException(ExceptionCode.ENCRYPTION_FAILED);
}
}
// AES 복호화
public String decryptAes(String plaintext) {
try {
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] decoded = Base64.getDecoder().decode(plaintext.getBytes(ENCODING_TYPE));
return new String(cipher.doFinal(decoded), ENCODING_TYPE);
} catch (Exception e) {
throw new BusinessLogicException(ExceptionCode.DECRYPTION_FAILED);
}
}
// SecretKey가 16자리가 맞는지 검증
public void validation(String secretKey) {
Optional.ofNullable(secretKey)
.filter(Predicate.not(String::isBlank))
.filter(Predicate.not(key -> key.length() != 16))
.orElseThrow(() -> new BusinessLogicException(ExceptionCode.SECRET_KEY_INVALID));
}
}
AES128ConfigTest
@Slf4j
@SpringBootTest
class AES128ConfigTest {
@Test
@DisplayName("Aes128 암호화가 잘 이루어지는지 테스트")
void aes128Test() {
String text = "this is test";
AES128Config aes128Config = new AES128Config();
String enc = aes128Config.encryptAes(text);
String dec = aes128Config.decryptAes(enc);
log.info("enc = {}", enc);
log.info("dec = {}", dec);
assertThat(dec).isEqualTo(text);
}
}
원인
@Value 애너테이션이 property 값을 받아오지 못하는 경우는 크게 네 가지가 있다.
- 프로퍼티 이름을 잘못 입력했을 경우
- 내 코드에서는 문제는 따로 문제가 없었다.
- Bean으로 등록되지 않은 경우
- 클래스를 Bean으로 등록하지 않으면 Spring이 의존성 주입을 할 수 없다.
- AES128Config 클래스는 @Component 애너테이션으로 Bean 등록을 했기 때문에 이 부분도 문제가 없었다.
- static 변수로 받은 경우
- @Value 값은 static 변수로 받을 수 없다.
- 외부에서 해당 클래스를 new로 생성했을 경우
- @Value 애너테이션은 Spring Context에 의존하기 때문에 해당 클래스가 Spring Bean으로 등록되어 있지 않으면 @Value 값은 null을 반환한다.
- 따라서 new로 클래스 인스턴스를 생성하면 Spring Bean으로 등록되어 있지 않았기 때문에 @Value 값이 null을 반환했던 것이다.
- 등록된 Bean을 사용하기 위해서는 @Autowired 애너테이션을 사용해야 한다. @Autowired 애너테이션은 해당 타입의 Bean을 찾아서 주입해준다.
- 내가 겪은 문제도 Test하는 과정에서 AES128Config 클래스 인스턴스를 new로 생성했기 때문에 발생했다. AES128Config 클래스를 @Autowired 애너테이션으로 주입해주자
@Slf4j
@SpringBootTest
class AES128ConfigTest {
@Autowired
private AES128Config aes128Config;
@Test
@DisplayName("Aes128 암호화가 잘 이루어지는지 테스트")
void aes128Test() {
String text = "this is test";
String enc = aes128Config.encryptAes(text);
String dec = aes128Config.decryptAes(enc);
log.info("enc = {}", enc);
log.info("dec = {}", dec);
assertThat(dec).isEqualTo(text);
}
}
테스트가 문제없이 성공했다!
마무리
테스트 코드를 작성할 때 @Autowired 애너테이션으로 주입받는다고 배워와서 정확한 이유도 모른채 사용했었는데 이번 일을 계기로 @Autowired의 역할과 @Value가 어떤식으로 작동하는지 알게 되었다. 다시 코드를 작성하러 떠나자…
728x90