Skip to content

[CHORE] 요청·응답 바디 로깅 및 민감정보 마스킹 적용 - #606

Merged
ljy1348 merged 7 commits into
devfrom
chore/#603
Aug 14, 2026
Merged

[CHORE] 요청·응답 바디 로깅 및 민감정보 마스킹 적용#606
ljy1348 merged 7 commits into
devfrom
chore/#603

Conversation

@ljy1348

@ljy1348 ljy1348 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Related Issue

Key Changes

요청·응답 본문을 로그에 남기되, 개인정보처리방침 제1조 수집 항목에 해당하는 민감정보는 마스킹 처리합니다. 기존 RequestBodyLoggingAdvice가 요청 본문을 평문으로 기록해 refreshToken, authorizationCode, idToken, fcmToken, 이메일 등이 그대로 남던 문제를 해결합니다.

0. 로그 포맷 JSON 구조화 — 1139596

dev에 아직 반영되지 않은 선행 커밋으로, 이 PR에 함께 포함됩니다. 요청·응답 로그를 CloudWatch에서 검색 가능한 JSON 필드로 구조화하고 traceId로 묶습니다.

1. 마스킹 기반 구성 — 3f03da3

@SensitiveData 어노테이션과 마스킹 정책(EMAIL, NAME, CREDENTIAL, FULL)을 정의했습니다.

마스킹 직렬화기는 SensitiveDataMasker가 직접 만든 로그 전용 ObjectMapper에만 등록합니다. ObjectMapper 타입 빈을 노출하면 Spring Boot 기본 ObjectMapper 자동 구성을 밀어내 실제 API 응답 직렬화까지 바뀌기 때문입니다. 실제 응답 본문은 변경되지 않습니다.

어노테이션 누락에 대비해 필드명 기반 안전망(SensitiveFieldNames)을 함께 뒀습니다. 닉네임처럼 다른 도메인 용어와 겹치는 이름은 작품명까지 가려버리므로 안전망에서 제외하고 어노테이션으로만 처리합니다.

2. DTO 어노테이션 부착 — efa669f

전수조사한 25개 DTO에 어노테이션을 부착했습니다.

분류 정책 대상
이메일 EMAIL UserInfoGetResponse, AppleAuthResult, KakaoUserInfo.KakaoAccount
닉네임·이름 NAME RegisterUserInfoRequest, UpdateMyProfileRequest, MyProfileResponse, ProfileGetResponse, UserBasicInfo, UserIdAndNicknameResponse, BlockGetResponse, PopularNovelGetResponse, CollectionOwnerGetResponse, CommentGetResponse, FeedGetResponse, FeedInfo, KakaoUserInfo.Properties
성별·출생연도 FULL EditMyInfoRequest, RegisterUserInfoRequest, UserInfoGetResponse, UserIdAndNicknameResponse
인증 크리덴셜 CREDENTIAL AuthResponse, ReissueRequest, ReissueResponse, LoginResponse, LogoutRequest, AppleLoginRequest, AppleIdUpdateRequest, AppleTokenResponse, AppleAuthResult
기기 식별자 CREDENTIAL FCMTokenRequest, LogoutRequest

토큰·인증코드·기기 식별자는 개인정보는 아니지만 유출 시 계정 탈취로 직결되어 함께 포함했습니다. 방침 제1조 5항에 따라 인종·사상·건강 등 인권침해 우려 정보는 수집하지 않으며, 코드상에도 해당 필드가 없음을 확인했습니다.

3. 요청 본문 마스킹 적용 — 55dbec1

RequestBodyLoggingAdvice가 마스킹된 본문을 기록하도록 바꾸고, 로그 한 건이 커지지 않도록 크기 상한(4KB)과 truncated 플래그를 남깁니다.

어노테이션 기반 마스킹은 BeanSerializerModifier가 record·POJO 프로퍼티에만 적용해서 Map 본문이 그대로 노출되는 문제가 있었습니다. 그래서 필드명 안전망은 직렬화 결과 트리를 순회하며 적용해 Map·중첩 구조까지 덮습니다. 이미 가려진 값은 길이 정보가 어긋나므로 다시 마스킹하지 않습니다.

{"event":"API_REQUEST_BODY","traceId":"4dd1b284","userId":"10034","body":{"authorizationCode":"***(len=18)","idToken":"***(len=100)"},"truncated":false}
{"event":"API_REQUEST_BODY","traceId":"4dd1b284","userId":"10034","body":{"novelTitle":"전지적 독자 시점","rating":4.5,"email":"re***@we***"},"truncated":false}

4. 응답 본문 구조 요약 로깅 — e0e6053

응답은 값을 남기지 않고 타입·필드명·길이만 기록합니다. 값을 직렬화하지 않아 개인정보가 흘러갈 수 없고, 목록은 원소를 순회하지 않아 크기와 무관하게 비용이 일정합니다.

{"event":"API_RESPONSE_BODY","body":{"type":"UserInfoGetResponse","fields":{"email":"String(len=17)","gender":"String(len=4)","birth":"Integer"}}}
{"event":"API_RESPONSE_BODY","body":{"type":"List","size":20,"elementType":"FeedGetResponse"}}

피드 20건 응답 기준 전량 직렬화는 10.122μs, 구조 요약은 0.074μs로 137배 저렴합니다.

5. 슬라이스 테스트 호환 — d463787

@WebMvcTest@ControllerAdvice만 등록하고 일반 컴포넌트는 제외합니다. 마스커와 요약기를 빈으로 두면 어드바이스가 의존성을 찾지 못해 컨트롤러 테스트 컨텍스트가 전부 깨졌습니다(148건 실패). 상태 없는 두 협력 객체를 빈에서 제외하고 어드바이스가 직접 생성하도록 했습니다.

6. 로그 출력 비동기 전환 — e4bc2f3

기존에는 Console·File 두 appender가 모두 동기이고 immediateFlush=true라 요청 스레드가 직접 인코딩·write·flush를 수행했습니다. 16스레드 부하 실측 결과입니다.

구성 평균 p99 처리량
로깅 OFF (기준선) 22μs 11μs 504,169 req/s
기존 (동기 Console+File) 538μs 7,209μs 28,515 req/s
기존 + 응답 메타 로깅 574μs 4,918μs 26,622 req/s
Async 적용 + 응답 메타 37μs 8.8μs 350,713 req/s

부하 증가분은 바디 로깅 추가(+7%)가 아니라 기존 동기 appender 구성에 있었습니다. stdout 소비자(awslogs 드라이버)가 정체된 조건에서는 파이프 버퍼 포화로 요청 스레드가 최대 501ms까지 블로킹됐고, Async 적용 시 37ms로 줄었습니다.

File appender는 Docker가 stdout을 수집하므로 중복이라 제거했습니다.

To Reviewers

  • 실제 API 응답은 바뀌지 않습니다. 마스킹은 로그 전용 ObjectMapper에만 걸리며, 이를 검증하는 테스트(doesNotAffectApiResponseSerialization)를 넣어뒀습니다.
  • neverBlock=true는 큐가 가득 차면 로그를 버립니다. 요청 스레드 블로킹보다 낫다고 판단했지만, 이 트레이드오프가 괜찮은지 봐주세요. 관련해서 deploy.sh에 --log-opt mode=non-blocking --log-opt max-buffer-size=4m를 함께 넣는 것을 권장합니다. awslogs 드라이버 기본값이 blocking이라, CloudWatch 전송이 밀리면 컨테이너 stdout이 막히고 그 상태에서 로그가 조용히 버려질 수 있습니다.
  • 닉네임을 NAME(첫 글자만 노출)으로 잡았습니다. 처리방침상 수집 항목이라 가렸지만 로그에서 사용자 추적이 어려워지는 면이 있어, 정책 조정이 필요하면 알려주세요.
  • 요청 본문 크기 상한은 4KB로 잡았습니다.
  • #603의 deploy.sh awslogs 드라이버 설정은 이 PR에 포함되지 않았습니다. 그래서 자동 close 링크를 걸지 않았습니다.

테스트

  • wss-service-support 신규 테스트 통과 (마스킹 8, 요청 본문 2, 응답 요약 4, logback 구성 3)
  • 루트 모듈 실제 DTO 마스킹 테스트 5건 통과
  • 전체 695건 중 13건 실패는 기존 실패입니다. 변경 전 커밋 11395964에서 동일한 13건이 같은 방식으로 실패함을 확인했습니다 (로컬 Redis 미구동에 따른 컨텍스트 로드 실패 + RecentSearchService:52 기존 NPE).

References

ljy1348 and others added 7 commits August 13, 2026 12:57
개인정보처리방침상 수집 항목과 인증 크리덴셜을 로그에서 가리기 위한
@sensitivedata 어노테이션과 마스킹 정책을 추가한다.

마스킹 직렬화기는 SensitiveDataMasker가 직접 만든 전용 ObjectMapper에만
등록한다. ObjectMapper 타입 빈을 노출하면 Spring Boot 기본 ObjectMapper
자동 구성을 밀어내 실제 API 응답 직렬화까지 바뀌기 때문이다.

어노테이션 누락에 대비해 필드명 기반 안전망을 함께 두되, 닉네임처럼 다른
도메인 용어와 겹치는 이름은 작품명까지 가려버리므로 제외한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
개인정보처리방침 제1조 수집 항목을 기준으로 전수조사한 25개 DTO에
@sensitivedata를 부착한다. 이메일, 닉네임·이름, 성별·출생연도와 함께
유출 시 계정 탈취로 이어지는 토큰·인증코드·기기 식별자를 포함한다.

방침 제1조 5항에 따라 인종·사상·건강 등 인권침해 우려 정보는 수집하지
않으며 코드상에도 해당 필드가 없음을 확인했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RequestBodyLoggingAdvice가 평문 대신 마스킹된 본문을 기록하도록 바꾸고,
로그 한 건이 지나치게 커지지 않도록 크기 상한과 truncated 플래그를 남긴다.

어노테이션 기반 마스킹은 record·POJO 프로퍼티에만 걸려 Map 본문이 그대로
노출되던 문제가 있어, 필드명 안전망은 직렬화 결과 트리를 순회하며 적용한다.
이미 가려진 값은 길이 정보가 어긋나므로 다시 마스킹하지 않는다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
응답 본문을 값 없이 타입·필드명·길이로만 기록한다. 값을 직렬화하지 않아
개인정보가 로그로 흘러갈 수 없고, 목록은 원소를 순회하지 않아 크기와
무관하게 비용이 일정하다.

실측상 피드 20건 응답 기준 전량 직렬화는 10.122us가 드는 반면 구조 요약은
0.074us로, 목록 API에서 로그량과 지연을 모두 줄인다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@WebMvcTest 슬라이스 테스트는 @ControllerAdvice만 등록하고 일반 컴포넌트는
제외한다. 마스커와 요약기를 빈으로 두면 어드바이스가 의존성을 찾지 못해
컨트롤러 테스트 컨텍스트가 모두 깨지므로, 상태 없는 두 협력 객체를 빈에서
제외하고 어드바이스가 직접 생성한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
동기 appender는 요청 스레드가 직접 인코딩·write·flush를 수행해 16스레드
부하 실측에서 요청당 평균 538us, p99 7,209us가 로깅에 소요됐다. 로그 전송이
정체되면 파이프 버퍼 포화로 요청 스레드가 최대 501ms까지 멈추기도 했다.

AsyncAppender를 적용해 평균 37us, p99 8.8us로 낮추고, 큐가 가득 차면 로그를
버리되 요청 처리는 막지 않도록 neverBlock을 켠다. INFO 로그까지 임의로
버리지 않도록 discardingThreshold는 0으로 둔다.

컨테이너 stdout은 Docker awslogs 드라이버가 수집하므로 파일 appender는
중복이라 제거한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ljy1348
ljy1348 marked this pull request as ready for review August 14, 2026 12:49
@ljy1348
ljy1348 requested a review from GiJungPark August 14, 2026 12:49
@github-actions
github-actions Bot requested a review from sansan20535 August 14, 2026 12:49

@GiJungPark GiJungPark left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다~

@ljy1348
ljy1348 merged commit bd7fb6c into dev Aug 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants