어떻게 하면 마스킹 부분을 확장하여 개발할 수 있을까?

반응형

이전에 API 요청/응답 로깅 과정에서 민감 정보를 마스킹하는 로직을 적용한 경험이 있습니다.

String maskedRequestBody = SensitiveDataMasker.maskSensitiveData(
   new String(requestBodyBytes, StandardCharsets.UTF_8));

내부 API의 경우, 요청 스펙과 데이터 구조에 대한 제어 권한이 전적으로 서비스 내부에 있었기 때문에
해당 마스킹 로직을 수정하더라도 영향 범위가 제한적이었고, 유지보수 측면에서도 큰 문제가 되지 않았습니다.

하지만 동일한 마스킹 로직을 외부 API 로깅에도 그대로 적용하면서 구조적인 한계가 드러났습니다.
외부 API는 요청 필드의 명명 규칙이 통일되어 있지 않으며, 민감 정보가 항상 동일한 키 이름으로 전달된다는 보장이 없습니다.
예를 들어 비밀번호에 해당하는 값이 password, pwd, secret 등 다양한 형태로 전달될 수 있습니다.

이로 인해 마스킹 대상 필드를 다음과 같은 상수 집합으로 관리하는 방식은 근본적인 문제를 내포하게 됩니다.

private static final Set<String> SENSITIVE_FIELDS = Set.of(
    "password", "email", "cvc", "cardNumber"
);

이 구조는 시간이 지날수록 다음과 같은 문제를 야기합니다.

  • 외부 API가 추가되거나 스펙이 변경될수록 SENSITIVE_FIELDS는 지속적으로 확장되며, 관리 비용이 기하급수적으로 증가합니다.
  • 단순한 필드 추가에도 마스킹 로직 자체를 수정해야 하므로, 변경 범위가 불필요하게 넓어집니다.
  • 마스킹 누락이 발생하더라도 컴파일 타임이나 런타임에서 즉시 감지되지 않아, 민감 정보가 로그에 그대로 노출될 위험이 있습니다.
  • 로깅·보안·도메인 정책이 하나의 클래스에 결합되어 있어, 버그 발생 시 원인 추적이 어렵습니다.

즉, 이 방식은 단순 구현은 쉽지만
외부 API 환경에서는 확장성과 안정성, 그리고 보안 측면에서 지속 가능하지 않은 설계라는 결론에 도달했습니다.
(이 글은 작업 내용을 작성한 내용이 아닙니다.)

그렇다면 어떻게 해야 할까요?

위 SENSITIVE_FIELDS를 제외하고, 총 2가지의 문제점이 존재하여였습니다.

1. 마스킹 방법이 같지 않을 수 있다. 
모든 민감 정보가 동일한 방식으로 마스킹되어야 할 이유는 없습니다.
예를 들어,

  • 비밀번호, 토큰과 같은 인증 정보는 전체 마스킹이 적절할 수 있지만
  • 카드 번호와 같은 결제 정보는 부분 마스킹(앞·뒤 일부 노출)이 필요할 수 있고
  • 이메일이나 사용자 식별자는 상황에 따라 도메인만 노출하는 방식이 요구될 수도 있습니다.

즉, "민감하다"라는 속성만으로는 충분하지 않으며, 데이터의 성격에 따라 서로 다른 마스킹 전략이 필요합니다.
하지만 기존 구조에서는 모든 필드가 동일한 마스킹 로직을 강제받고 있어, 이러한 요구사항을 수용하기 어렵습니다.

2. 유연성이 부족하다.
현재 방식은 마스킹 대상과 방식이 코드에 강하게 결합되어 있습니다.
그 결과,

  • 새로운 외부 API가 추가되거나
  • 동일한 필드라도 API별로 다른 마스킹 정책이 필요해지거나
  • 특정 API에서는 아예 다른 마스킹 규칙을 적용해야 하는 경우

모두 기존 코드 수정으로 이어지게 됩니다.

이는 곧, 변경 범위가 불필요하게 커지고 테스트 부담이 증가하며 로깅 정책 변경이 서비스 전체에 영향을 미치는 구조로 이어집니다.

결과적으로 이 방식은 확장에 열려 있지 않고(OCP 위반), 외부 API가 증가할수록 유지보수 비용과 리스크가 함께 커지는 구조입니다.

내부 API에서 사용할 때는 지금과 같은 방법 SensitiveDataMasker클래스에서만 사용해도 크게 무리는 없습니다. 왜냐하면 어찌 되었든 필드는 개발자인 제가 관리하기 때문에 role에 의거하여 작성하면 됩니다. 하지만 외부 API를 사용하는 경우에는 상황이 달라집니다.

내부 API에서만 사용할 경우라면, 현재와 같은 SensitiveDataMasker 단일 클래스 기반의 방식도 크게 문제 되지 않을 수 있습니다.
요청 필드와 스펙을 개발자가 직접 통제할 수 있고, 역할(Role)과 도메인 규칙에 따라 일관성 있게 관리할 수 있기 때문입니다.
하지만 외부 API를 사용하는 경우 상황은 완전히 달라집니다.

앞서 언급했듯이, password라는 의미 하나만 보더라도 password, pwd, secret 등 API마다 표현 방식이 다를 수 있습니다.
이러한 상황에서 SENSITIVE_FIELDS만 지속적으로 수정하며 대응하는 방식은 규모가 커질수록 현실적인 선택지가 되기 어렵습니다.
물론 외부 API의 수가 적고, 스펙이 안정적이라면 관리 가능할 수도 있습니다.
하지만 새로운 API가 지속적으로 추가되거나, 연동해야 할 외부 API의 수가 많아지는 경우라면 어떨까요?
모든 API를 일일이 테스트하며 마스킹 필드를 추가하는 방식은 시간과 비용 측면에서 과도한 공수를 요구하게 됩니다.

그래서 어떻게 하라고?

가장 먼저 떠올릴 수 있는 방법은 마스킹 방식을 정책화하여 사용하는 것입니다.
형태만 보면 전략 패턴과 유사해 보일 수 있습니다. 하지만 이 문제는 일반적인 전략 패턴을 그대로 적용하기에는 적절하지 않습니다.
전략 패턴은 보통 전략 선택의 책임이 호출부 또는 생성 시점에 명확히 존재합니다.
그러나 로깅 마스킹의 경우, 호출부는 어떤 마스킹 전략이 적용될지 알 필요도 없고, 알아서도 안 됩니다.

로깅은 항상 동일한 진입점을 가지며,
마스킹 전략은 요청의 맥락(API 유형, 외부/내부 여부 등)에 따라 내부에서 동적으로 결정되어야 합니다.
(그렇다고 if문을 남발해서 개발하는 건 기존 방식과 다르지 않을 거 같습니다.)

즉, 이 문제는 단순히 전략을 교체하는 문제가 아니라,
하나의 진입점에서 정책을 해석하고 적절한 전략을 선택·조합하는 구조가 필요한 문제입니다.

여러 대안을 검토한 끝에, 마스킹 로직을 플러그인 형태로 분리하는 아키텍처가 가장 적합하다고 판단했습니다.
AI를 통해 여러 설계 대안을 검토하는 과정에서 플러그인 아키텍처가 제안되었고,
이를 다시 문제의 성질에 대입해 보니 다음 조건들을 가장 잘 만족했습니다.

이 문제에 대해 다시 한번 정의해 보면 진입점은 하나입니다.

로깅 -> 마스킹 -> 출력 

하지만 마스킹 방식은 전체가 될 수도 있고, 부분이 될 수 있고, 조건부가 노출이 될 수 있습니다. 어쩌면 마스킹이 미적용이 될 수 있습니다.
그리고 어떤 방식을 쓸지는 사전에 고정할 수 없습니다. 예를 들어, 각 API는 성격이 다를 수 있고, 보안 요구 수준도 전부 다를 겁니다.
제일 중요한 사실은 기존 로깅 코드는 가급적 수정하지 않아야 합니다. 어찌 되었든 로깅은 횡단 관심사이며 잦은 변경은 위험하기 때문입니다. 즉,  확장 가능해야 하지만, 중심 로직은 건드려면 안 되는 구조라고 할 수 있습니다.

그래서 선택한 것이 플러그인 아키텍처입니다.

 

Plug-in Architecture

and the story of the data pipeline framework

medium.com

전략 패턴과의 결정적인 차이

전략 패턴은 일반적으로 다음과 같은 전제를 가집니다. 전략의 종류는 상대적으로 제한적이며 전략 선택 기준이 호출부 또는 객체 생성 시점에 명확히 존재하고 한 번의 실행 흐름에서 하나의 전략을 선택해 실행합니다. 즉, 전략 패턴은 어떤 전략을 사용할지 호출부가 알고 있는 상황에 적합한 패턴입니다.

하지만 마스킹 문제는 이 전제와 맞지 않습니다.
마스킹 전략은 외부 API가 늘어날수록 지속적으로 추가될 수 있고 어떤 마스킹을 적용할지는 호출부가 아니라 요청 콘텍스트 내부 정보에 의해 결정되며, 하나의 요청에 대해 여러 마스킹 규칙이 동시에 적용될 수 있어야 합니다.
이 문제는 전략을 교체하는 문제가 아니라, 요청 맥락을 해석해 적용 가능한 규칙들을 조합하는 문제에 가깝습니다.

플러그인 아키텍처를 사용하면 이점은?

플러그인 아키텍처의 본질은 명확합니다. 핵심 로직은 고정하고, 행위는 외부에서 계속 추가할 수 있도록 만든다.

이를 마스킹 문제에 적용하면 다음과 같은 구조가 됩니다.

  • 로깅 흐름(진입점)은 변경하지 않고 유지
  • 마스킹 규칙은 플러그인 단위로 분리
  • 새로운 외부 API가 추가되면 플러그인만 추가
  • 기존 로깅·마스킹 코드는 수정하지 않음

그 결과, 마스킹 규칙 확장이 기존 코드에 영향을 주지 않고 외부 API 증가에 따른 유지보수 비용이 선형적으로 증가하지 않으며
로깅이라는 횡단 관심사를 안정적으로 보호할 수 있습니다.

결론

이번 글에서는 배경 → 방안 1 → 방안 2 → 선택의 흐름으로 로깅 마스킹 문제를 정리해 보았습니다. 결론적으로 저는 플러그인 아키텍처를 이용해 마스킹 로직을 확장하는 방식을 선택했습니다. 이 글에서는 플러그인 아키텍처 자체의 장점이나 단점을 깊게 다루지는 않았으며, 이 방법이 항상 최선의 해답이라고 주장하고자 한 것도 아닙니다. 실제로 상황에 따라서는 AOP와 어노테이션을 조합하는 방식이 더 효율적인 선택이 될 수도 있습니다. 하지만 제가 플러그인 아키텍처를 선택한 가장 큰 이유는 안전성이었습니다. 마스킹은 겉보기에는 단순한 기능처럼 보이지만, 이를 세분화해 보면 적용 대상, 방식, 범위, 정책 등 여러 갈래의 판단이 필요합니다. 그리고 이러한 판단 지점들이 늘어날수록, 확장을 전제로 한 구조가 아니면 오히려 리스크가 커진다고 판단했습니다. 플러그인 아키텍처는 마스킹이라는 단일한 책임을 여러 개의 확장 가능한 단위로 분리하면서도, 핵심 로깅 흐름은 건드리지 않도록 만들어 주었습니다. 그 결과, 기능 추가보다 안정적인 확장을 우선하는 선택을 할 수 있었습니다. 이번 주 안으로는 이 구조를 실제 코드에 어떻게 적용했는지, 그리고 어떤 기준으로 플러그인을 분리했는지를 중심으로 한 번 더 정리해 볼 예정입니다.

반응형

댓글

Designed by JB FACTORY