[TS] 민감 정보 관리를 위한 @SecureKey 커스텀 어노테이션 도입기

정찬's avatar
Apr 21, 2025
[TS] 민감 정보 관리를 위한 @SecureKey 커스텀 어노테이션 도입기

개요

 
Nhn24Store 서비스를 개발하면서 DB 접속 정보, 이메일 인증 키, 외부 API 키 등 다양한 민감 정보를 다뤄야 했습니다.
이런 정보들을 안전하게 관리하기 위해 NHN Cloud의 SKM(Secure Key Manager) 서비스를 도입했지만,
그 과정에서 비즈니스 로직과 설정 로직 간의 결합이라는 구조적 문제가 발생했습니다.
이 글에서는 해당 문제를 어떻게 해결했는지, 그리고 어떻게 어노테이션을 통해 개선했는지를 공유합니다.
 

 

📌 문제 배경

 
처음에는 SecureKeyManagerService를 직접 주입받고, 애플리케이션이 시작될 때 민감 정보를 로드하는 구조였습니다.
@Service @RequiredArgsConstructor public class EmailSendServiceImpl implements EmailSendService { private final SecureKeyManagerService secureKeyManagerService; @Value("${secret.keys.email.account}") private String accountKey; @Value("${secret.keys.email.password}") private String passwordKey; private String USERNAME; private String PASSWORD; @PostConstruct private void initEmailCredentials() { this.USERNAME = secureKeyManagerService.fetchSecretFromKeyManager(accountKey); this.PASSWORD = secureKeyManagerService.fetchSecretFromKeyManager(passwordKey); } }
 
이 접근 방식은 민감 정보를 노출하지 않으면서도 안전하게 로드할 수 있다는 장점이 있었지만, 아래와 같은 문제점이 존재했습니다.

❗ 구조적 문제

  • 설정 클래스가 외부 서비스(SKM)의 세부 구현에 직접 의존
  • SecureKeyManagerService를 사용하는 곳이 늘어나면서 결합도 증가
  • 비즈니스 로직과 설정 로직이 뒤섞이며 유지보수 어려움
예를 들어, 이메일 발송 서비스에서도 민감 정보(SMTP 계정 정보)를 사용해야 했고, 이 역시 매번 SecureKeyManagerService를 직접 사용해야 했습니다. 이로 인해 설정 변경이 있을 때마다 비즈니스 로직에 영향을 주는 구조가 되었고, 이는 분명 개선이 필요한 지점이었습니다.

🛠️ 해결 방안: @SecureKey 커스텀 어노테이션 도입

이 문제를 해결하기 위해 Spring의 DI(Dependency Injection) 철학에서 아이디어를 얻었습니다.
"객체는 자신이 필요한 의존성을 직접 생성하거나 탐색하지 않고, 외부에서 주입받는다."
이 철학을 민감 정보에도 그대로 적용해보기로 했습니다.
@Service @RequiredArgsConstructor public class EmailSendServiceImpl implements EmailSendService { @SecureKey("secret.keys.email.account") private String USERNAME; @SecureKey("secret.keys.email.password") private String PASSWORD; private final Session EMAIL_SESSION = createEmailSession(); }
이제 SecureKeyManagerService는 내부 구현으로 완전히 은닉되었고, 필요한 클래스에서는 어노테이션만 붙이면 해당 민감 정보를 주입받을 수 있게 되었습니다.

🔧 어떻게 동작하는가?

1. @SecureKey 어노테이션 정의

@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface SecureKey { String value();

2. BeanPostProcessor를 이용해 어노테이션 처리

@Component public class SecureKeyInjector implements BeanPostProcessor { @Autowired private SecureKeyManagerService secureKeyManagerService; @Override public Object postProcessBeforeInitialization(Object bean, String beanName) { Field[] fields = bean.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(SecureKey.class)) { SecureKey annotation = field.getAnnotation(SecureKey.class); String key = annotation.value(); String value = secureKeyManagerService.fetchSecretFromKeyManager(key); field.setAccessible(true); try { field.set(bean, value); } catch (IllegalAccessException e) { throw new RuntimeException("Failed to inject secure key: " + key, e); } } } return bean; } }
이 방식은 Spring Bean이 초기화될 때 필드에 해당 키 값을 자동으로 주입해주며, 개발자는 더 이상 SKM을 직접 다룰 필요가 없습니다.

✅ 적용 결과

  • 민감 정보 로딩 로직 제거 → 비즈니스 로직이 간결해짐
  • SecureKeyManagerService 은닉 → 클래스 간 결합도 감소
  • 설정과 비즈니스 로직의 완전한 분리 → 응집도 향상, 유지보수성 개선
  • 어디서든 동일한 방식으로 민감 정보 사용 가능 → 일관성과 확장성 확보

💡 회고

이 경험을 통해 느낀 점은 다음과 같습니다:
  • 설정 로직과 비즈니스 로직은 반드시 분리해야 하며, 이는 코드의 생명력을 좌우합니다.
  • Spring의 철학은 단순한 프레임워크 문법이 아닌, 아키텍처 설계의 방향성을 제시합니다.
  • 반복되는 설정 로직은 어노테이션 기반 추상화로 간결하게 만들 수 있습니다.
 
 
 
Share article

lushlife99