![[TS] 민감 정보 관리를 위한 @SecureKey 커스텀 어노테이션 도입기](https://image.inblog.dev?url=https%3A%2F%2Finblog.ai%2Fapi%2Fog%3Ftitle%3D%255BTS%255D%2520%25EB%25AF%25BC%25EA%25B0%2590%2520%25EC%25A0%2595%25EB%25B3%25B4%2520%25EA%25B4%2580%25EB%25A6%25AC%25EB%25A5%25BC%2520%25EC%259C%2584%25ED%2595%259C%2520%2540SecureKey%2520%25EC%25BB%25A4%25EC%258A%25A4%25ED%2585%2580%2520%25EC%2596%25B4%25EB%2585%25B8%25ED%2585%258C%25EC%259D%25B4%25EC%2585%2598%2520%25EB%258F%2584%25EC%259E%2585%25EA%25B8%25B0%26logoUrl%3Dhttps%253A%252F%252Finblog.ai%252Finblog_logo.png%26blogTitle%3Dlushlife99&w=2048&q=75)
개요
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