PeekCart 학습 기록 18: Phase 4로 넘어가기 전에 - 글을 쓰다 발견한 22개의 부채

연재 첫 글부터 동시 주문 부하 테스트 글까지, 이 연재는 “구현한 것을 복기한다”는 목적으로 시작했습니다. 그런데 글을 쓰는 동안 예상하지 못한 일이 반복됐습니다.

설명하려고 코드를 다시 열 때마다, “어 이거 왜 이렇게 했지?”가 튀어나왔다.

상품 캐시 글에서 캐시를 설명하다 hit rate를 볼 메트릭이 없다는 걸 알았고 분산 락 글에서 분산 락을 설명하다 주문 경로에서 그 락이 사실상 무력화돼 있다는 걸 알았습니다. 동시 주문 부하 테스트 글에서 부하 결과를 검증하다 검증 SQL 자체에 결함이 있다는 걸 알았습니다. 글쓰기가 곧 정적 코드 리뷰였다.

그렇게 쌓인 결함이 스물두 개입니다. 이 글은 이 22개를 정리합니다. 동시에 Phase 3에서 부하와 리뷰가 찾아 대부분 해소한 부채와 비교하고 Phase 4 MSA 진입 전에 무엇을 가져가고 무엇을 바꾸는지 체크리스트로 만듭니다.

이 연재는 PeekCart를 네 단계로 진행했습니다. Phase 1은 단일 Spring Boot 모놀리스로 도메인을 구현한 단계, Phase 2는 캐시, 분산 락, Kafka와 Outbox로 성능과 정합성을 보강한 단계, Phase 3은 CI, Kubernetes, 관측성, 부하 테스트로 운영 환경을 검증한 단계, Phase 4는 서비스별로 나눈 MSA 단계입니다.

이 글의 결론을 먼저 적으면 이렇습니다. 부채는 발견 경로에 따라 성격이 갈립니다.

부채를 찾은 것성격Phase 3 시점 상태
부하 테스트, 리뷰, CI 분석운영과 측정에서 터진 것 (시끄러움)코드 결함은 대부분 해소, 일부는 추적과 운영으로 이관
학습 글 작성정독에서만 보인 것 (조용함)22개 목록화, Phase 4 착수 시 처리 판정

앞의 것은 운영 과정에서, 뒤의 것은 글쓰기 과정에서 발견했습니다. 이 둘의 성격 차이가 이 글에서 다룰 내용의 기준입니다.


용어

용어이 글에서의 의미
운영이 찾은 부채부하 테스트와 리뷰에서 드러난 결함. 런타임 증상(실패율, No-data 패널)으로 시끄럽게 보인다
글쓰기가 찾은 부채코드를 설명하려 정독하다 보인 결함. 런타임 증상이 없어 조용히 잠복한다
선해소정식 부채 등록 절차 없이 즉시 고치는 것. 명백한 문서와 쿼리 오류에 해당
옵션 가치형 부채지금은 비용이 안 보이고, 특정 변경(키 회전과 서비스 분리 등)이 필요해진 시점에야 비용이 드러나는 부채
Choreography Saga중앙 오케스트레이터 없이 각 서비스가 이벤트를 주고받으며 보상하는 분산 트랜잭션 패턴
eventual consistency즉시가 아니라 일정 시간 뒤 정합성이 맞춰지는 모델. 서비스별 DB 분리의 필연적 결과
공유(common) 모듈Phase 4 멀티모듈에서 여러 서비스가 함께 쓰는 코드 모듈
신뢰 경계그 안쪽이면 “검증된 요청”이라고 믿어도 되는 경계. Phase 4에선 Gateway가 그 입구가 된다

이번 글의 질문

  1. 운영이 찾은 부채와 글쓰기가 찾은 부채는 무엇이 다를까? 왜 따로 기록했을까?
  2. 학습 글을 쓰면서 부채가 발견된다는 게 무슨 의미일까?
  3. 22개는 어떻게 묶이고 언제 처리될까?
  4. Phase 4에서 구조적으로 바뀌는 것은 무엇이고 그대로 가져가는 것은 무엇일까?
  5. 서비스별 DB 분리는 어떤 정합성 문제를 새로 만들까?
  6. 공유 모듈에는 무엇을 둘 수 있고 무엇을 두면 안 될까?
  7. Gateway 인증과 내부 서비스 신뢰 경계는 어떻게 설계해야 할까?
  8. 부하, 테스트가 아직 검증하지 못한 갭(chaos, 상호배제)은 어떻게 들고 갈까?

문제 상황: 부채는 두 군데서 나온다

Phase 3을 종결할 때 개발 부채 표는 거의 정리돼 있었습니다. 코드와 설계 결함이었던 것들은 전부 해소됐습니다. 메트릭 histogram이 설정 병합으로 가려진 것(설정을 Java Config로 옮김), 관측성 계약이 5개 파일에 흩어진 것(한 표로 명시하고 lint로 강제), Kafka Consumer 로그에 traceId가 안 붙던 것(인터셉터로 헤더 전파), Outbox 발행 시 trace context가 끊기던 것(이벤트에 컬럼으로 영속화)이 그 예이고, 그 외 설정 공통화와 테스트 인프라 표준화 등도 해결했습니다.

남은 셋은 즉시 고칠 코드 결함은 아니라 추적, 운영으로 이관됐습니다(완전히 손 뗀 건 아닙니다. 후속 측정, 튜닝과 자동화 권장이 별도로 남아 있습니다). 캐시 TPS가 목표 3배에 미달한 것은 인프라 용량 한계로 보고 추적과 후속 튜닝에 넣었고, Grafana pod selector 편의 문제는 Won’t Fix로, 부하 도구의 JDK 버전 미지원은 운영 지식으로 수용하되 자동화를 권장합니다.

이것들의 공통점은 발견 경로입니다. 전부 부하 테스트(캐시 효과 측정, 동시 주문 측정), 종합 리뷰, CI 분석에서 나왔습니다. 즉 “돌려보니 터진 것”, “리뷰어가 짚은 것”입니다.

그런데 학습 글을 쓰기 시작하자 다른 종류의 부채가 나오기 시작했습니다. 부하를 준 것도 아니고 리뷰를 받은 것도 아닌데, 남에게 설명하려고 코드를 정독하는 행위 자체가 결함을 드러냈습니다.

flowchart LR
    LT["부하 테스트"] --> DD["메트릭/정합성<br/>운영 이슈"]
    RV["리뷰/CI 분석"] --> DR["관측성/CI<br/>구조 이슈"]
    WR["설명하려고<br/>코드 정독"] --> LL["22개<br/>코드 레벨 결함"]

    DD --> FIX["대부분 Phase 3에서 해소<br/>일부 추적/운영 이관"]
    DR --> FIX
    LL --> HOLD["Phase 4 진입 시<br/>처리 판정"]

이 둘을 굳이 나눠 기록한 이유가 있습니다. 처리 시점이 다르다. 운영이 찾은 부채는 “지금 운영을 막으니 지금 고친다”. 글쓰기가 찾은 부채 대부분은 “지금은 무해하고, Phase 4의 특정 작업과 묶일 때 비로소 고치는 게 자연스럽다”. 섞어 버리면 급하지 않은 22개가 급한 항목이 있는 표에 섞입니다.


무엇을 부채로 적고 무엇을 뺐나

22개를 무작정 모은 게 아닙니다. 등재에 한 가지 규율을 뒀습니다.

사용자 경험을 바꾸지 않고, 서버 측 하드닝만으로 해소할 수 있는 보안, 운영, 관측성 결함만 적는다.

이 기준으로 명시적으로 뺀 것들이 있습니다.

제외한 것이유
MFA, 디바이스별 토큰 격리, OIDC 이행기능 미구현: 사용자 동작 변화/별도 흐름 필요. 부채가 아니라 미구현 기능
메시지 포맷 추출 같은 순수 리팩토링섣부른 추상화. 실제 변경 driver(다국어 등) 생길 때 부채화
Outbox 발행 처리량 한계 (순차 동기 발행)driver(부하 실측) 없이 손대면 섣부른 최적화. 발행 지연이 실측되면 그때

이것이 중요한 이유는, 부채 목록이 “개선하고 싶은 것 전부”가 되면 의미가 없기 때문입니다. “지금 안 했지만 언젠가 좋을 것”은 무한합니다. 목록은 그중 드러난 결함이면서 + 서버 측만으로 해결할 수 있는 것으로 범위를 좁혔습니다. 나머지는 근거만 따로 남겨 두고 목록엔 올리지 않았습니다.


스물두 개를 여섯 묶음으로

하나씩 보는 건 의미가 없습니다. 처리 시점과 작업 단위로 묶으면 여섯 그룹이 됩니다. 묶음의 기준은 “Phase 4의 어떤 작업과 같이 처리되는가” 입니다.

묶음 1: 보안/인증 (Gateway 분리에 종속)

결함처리 트리거
비밀번호 인코더가 알고리즘 식별자(prefix) 없이 해시를 저장: 향후 cost 상향/알고리즘 전환 시 점진 마이그레이션 경로가 끊김비밀번호 모델 변경 시
시크릿 저장소(KMS/Vault) 부재 + JWT 키 회전 경로 없음JWT를 비대칭 키(RS256/ES256)로 전환 시
Refresh Token 재사용 탐지 미구현 (토큰 패밀리 추적 없음). 탈취를 공격 신호로 해석 못 함분산 환경 진입 시
인증 실패 경로의 무로그와 무알림: 유효하지 않은 토큰을 기록 없이 거부하고, 401 폭주에 알람이 없음Gateway 인증 경계 재설계 시

이 넷은 모두 Phase 4에서 인증이 앱 내부 필터에서 API Gateway 필터로 이동하는 순간 자연스럽게 한 작업 단위가 됩니다. 지금 단독으로 고치는 건 옵션 가치형 투자라 우선순위가 낮습니다.

묶음 2: 관측성 (관측성 계약 갱신에 연동)

결함비고
운영 알림이 Slack 단일 채널에 의존 + 발송 실패 자체가 메트릭으로 안 보임메트릭만 부분 적용 가능
캐시 hit rate 메트릭 부재 (캐시가 메트릭 바인더에 연결 안 됨)캐시 fallback(묶음 6)의 선결 표면
Outbox 발행 파이프라인 메트릭 부재 (backlog 크기, 발행 지연, 실패율)발행 처리량 측정의 선결 표면
Kafka lag의 consumer group 식별 불가 + 대시보드 범례 공백범례 정정은 즉시 가능

여기서 확인한 점은 다음과 같습니다: Phase 3 막바지에 관측성 계약이 어느 파일과 레이어에 사는지, 그리고 각 항목이 Phase 4에서 어느 모듈로 갈지를 미리 한 표에 명시해 뒀다. 그런데 그 표에 캐시와 Outbox 항목은 행 자체가 없습니다. Phase 4에서 멀티모듈로 표를 갱신할 때 이 빠진 행을 추가하며 함께 처리하는 게 자연스럽습니다.

묶음 3: 발행 경로 한 줄 하드닝

결함수정 규모
Outbox 발행의 동기 대기에 타임아웃이 없어, 브로커 무응답 시 폴링 스레드가 잠식됨타임아웃 인자 추가
DLQ 발행 결과를 확정하지 않아, DLQ 발행이 실패하면 메시지가 원본에서도 DLQ에서도 사라질 수 있음플래그 한 줄

둘 다 한 줄짜리입니다. 정상 경로는 무해하고 드문 장애 경로(브로커 무응답과 DLQ 발행 실패)에서만 발현합니다. 타임아웃 건은 ShedLock 글에서 다룬 분산 스케줄러 락의 보유 시간 상한과 “한 손잡이의 양면”입니다. 발행 사이클 상한을 코드로 한정해야 락 보유 시간과 정합되어 중복 발행 경로가 막힙니다.

묶음 4: Order 동시성 제어 (Phase 4 분리로 일부 형태 소멸)

결함Phase 4에서
주문 생성 경로에서 “락이 트랜잭션을 감싼다”는 불변식이 무력화 (동시 주문 부하 테스트에서 실측 확인)모놀리스 형태는 소멸: 단, 재고 차감 경로 동시성 계약은 재검증
주문 상태 전이의 동시성 무방비 (Order에 낙관적 락 없음, 4개 이상 트리거가 같은 주문을 건드림)사실상 필수화: 트리거가 네트워크 경계를 넘음

이 묶음이 가장 흥미롭습니다. 같은 도메인의 두 결함인데 Phase 4가 정반대로 작용합니다.

  • 생성 경로 결함은 “락이 트랜잭션을 감싼다”는 불변식이 깨져 “락 해제 후 커밋” 역순이 되는 것입니다. 동시 주문 부하 테스트에서 오버셀링을 분산 락 대신 낙관적 락이 막았다는 것으로 실측됐습니다. 주문과 재고가 별도 서비스와 별도 DB가 되면 바깥 주문 트랜잭션이 차감 트랜잭션을 감쌀 수 없게 되므로 폐기 후보입니다. 단, 여기서 사라지는 건 “바깥 주문 트랜잭션이 차감을 감싼다”는 모놀리스 특유의 구조뿐입니다. 재고 서비스 내부에서 “락 획득, 로컬 트랜잭션 커밋, 락 해제” 순서가 실제로 보장되는지는 Phase 4에서 별도 재검증 대상입니다. “자연 해소”는 바깥 트랜잭션 문제에 한정된 표현이지, 재고 차감 경로의 동시성 계약 전체가 공짜로 옳아진다는 뜻이 아닙니다.
  • 상태 전이 결함은 반대입니다. 모놀리스에선 트리거 간 경합이 드물어 잠복했지만 주문/결제가 서비스로 쪼개지면 상태 전이 트리거가 네트워크 경계를 넘어 경합이 더 흔해지므로 Phase 4에선 사실상 필수.

묶음 5: retention / CI, 배포 게이트

결함묶음
발행 완료(Outbox) 행의 청소(retention) 정책 부재, 무한 누적retention
처리 완료(멱등성) 행의 청소(retention) 정책 부재, 무한 누적retention
CI가 빨간불이어도 머지를 기계적으로 막지 못함 (branch protection 미설정)CI 게이트
PR에서 Docker 이미지를 빌드, 검증하지 않음 (smoke test 부재). Dockerfile 깨짐이 머지 후에야 드러남CI 게이트
배포 매니페스트가 :latest에 고정: 떠 있는 버전이 비결정적, 롤백 모호이미지 불변 참조
매니페스트의 namespace 누락을 막는 자동 lint 부재. 엉뚱한 NS로 누출 가능CI 게이트

retention(두 누적 테이블)은 같은 작업 단위로 묶입니다. CI, 배포 게이트는 “CI가 검증, 강제, 산출물 보존을 온전히 못 한다”는 한 묶음이라, Phase 4로 서비스가 N개 늘기 전에 함께 해소하는 게 자연스럽습니다.

묶음 6: 가벼운 선해소와 하드닝 후보

결함성격
비용 정리(cleanup) 명령이 문서와 스크립트에서 불일치. 문서대로 실행하면 삭제가 실패해 크레딧 누수명백한 오류: 즉시 문서 정정
부하 검증 SQL이 상태 필터를 LEFT JOIN ON 절에만 둬, 취소/실패 주문을 합산할 수 있음명백한 오류: 한 줄 수정
부하 리포트의 CPU 사용률 해석 오류 (노드 스펙과 Pod request 기준 혼동)명백한 오류: 문서 문구 정정 (동시 주문 부하 테스트 글에는 이미 정정 반영)
Redis 장애 시 조회 캐시 fallback 부재. 상품 조회가 5xx로 노출오류 아님: 복원력 하드닝

이 묶음은 성격이 둘로 갈립니다. 앞의 세 가지는 명백한 오류라 트레이드오프로 미룰 이유가 없고, 정식 부채 등록 없이 즉시 고치는 게 원칙입니다. 특히 cleanup 불일치는 방치 시 직접적 금전 손실(orphan 클러스터와 VM 과금)이 걸려 있어 실효 우선순위가 낮지 않습니다.

반면 캐시 fallback은 복원력 하드닝입니다. 1차 fallback(캐시 예외를 전파하지 않고 DB 조회로 전환하기) 자체는 가벼워 같은 묶음에 넣었지만 결이 다릅니다. 캐시 hit rate 메트릭(묶음 2)이 먼저 있어야 fallback 발동 빈도를 추적할 수 있고, Redis가 서비스 간 공유 인프라가 되거나 장애 시뮬레이션 시나리오가 추가될 때 비로소 값어치가 커집니다. 즉 조건부 하드닝 후보이고, 즉시 정정 대상에서는 뺐습니다.

flowchart TB  
    A["글쓰기가 찾은 22개"] --> G1["묶음1 보안/인증<br/>4건"]  
    A --> G2["묶음2 관측성<br/>4건"]  
    A --> G3["묶음3 발행 하드닝<br/>2건"]  
    A --> G4["묶음4 Order 동시성<br/>2건"]  
    A --> G5["묶음5 retention·CI/배포<br/>6건"]  
    A --> G6["묶음6 선해소·하드닝<br/>4건"]  
    G1 -.->|Gateway 분리 시| P4["Phase 4 작업 단위"]  
    G2 -.->|관측성 계약 갱신 시| P4  
    G4 -.->|형태소멸·재검증 / 필수화| P4  

Phase 4에서 가져갈 것과 바꿀 것

부채를 정리하면서 “Phase 4에 무엇을 들고 가는가”를 정합니다.

그대로 가져갈 것 (이미 다음 Phase를 보고 설계해 둠)

자산Phase 4에서
Outbox 패턴도메인별 구현에서 공유 모듈로. 단 테이블 스키마는 서비스별
trace context 전파표준 헤더(traceparent) 한 단계만 추가하면 분산 추적과 호환
멱등성 처리공유 메커니즘 + 서비스별 테이블
관측성 계약을 한 표에 명시한 것각 항목의 Phase 4 위치가 이미 표에 적혀 있음
Kustomize base/overlaysservices/ 아래 서비스 디렉토리를 형제로 추가, 기존 수정 0
설정 분리 원칙동작 규약은 공통, 환경마다 다른 자격증명만 프로파일: 서비스별로 동일 적용

Phase 3에서 해소한 부채들은 Phase 4의 발판이 되도록 설계됐습니다. trace context는 “표준 헤더만 추가하면 분산 추적으로 넘어가도록”, 관측성 계약은 “각 항목이 어느 모듈로 갈지 미리 정해 두도록” 해소했습니다. 부채를 해소할 때 이미 다음 Phase를 본 것입니다.

구조적으로 바뀌는 것

항목Phase 1~3Phase 4
프로젝트 구조단일 모듈멀티모듈
인증앱 내부 JWT 필터API Gateway 필터
이벤트 DTO도메인 내부공유 모듈
DB단일 통합 DB서비스별 분리
정합성 보장DB 트랜잭션 + 낙관적 락Choreography Saga
외래 키(FK)DB 레벨 FKFK 없음 (이벤트 스냅샷 참조)

서비스별 DB 분리가 새로 만드는 정합성 문제

이것이 Phase 4에서 가장 주의할 부분입니다. 모놀리스에서 단일 DB의 트랜잭션과 낙관적 락이 자동으로 보장하던 정합성이, DB가 쪼개지는 순간 사라진다.

flowchart LR  
    subgraph P3["Phase 3 (단일 DB)"]
        T["하나의 트랜잭션<br/>주문+재고차감+낙관적 락"]  
    end

    subgraph P4["Phase 4 (서비스별 DB)"]
        OS["Order DB"] -.->|"order.created (Kafka)"| IS["Inventory DB"]
        IS -.->|"reserve.failed 등 보상 이벤트"| OS  
    end

    P3 -->|"FK 소멸 + 트랜잭션 경계 분리"| P4  

구체적으로 다음 문제가 새로 생깁니다.

  1. 오버셀링 방어 위치가 바뀐다. 동시 주문 부하 테스트에서 오버셀링을 실제로 막은 건 단일 DB의 낙관적 락이었습니다. 별도 DB가 되면 낙관적 락은 한 서비스 안에서만 유효합니다. 서비스 간 정합성(주문과 재고)은 이제 Saga 보상 흐름이 책임지는데 여기엔 서로 다른 두 가지가 있습니다: (a) 주문 생성 시 재고 예약 실패: order.created → inventory.reserve.failed → order.cancelled, (b) 결제 실패 후 재고 복구: payment.failed → order.cancelled → inventory.restore. 오버셀링을 직접 막는 건 (a)의 재고 차감 단계이고 (b)는 이미 차감된 재고를 되돌리는 별개 보상입니다. 둘을 한 흐름으로 섞으면 안 됩니다.

  2. 즉시 정합성이 eventual consistency로 바뀐다. FK 조인 대신 이벤트 수신 시점에 데이터를 스냅샷으로 저장합니다(서비스 간엔 FK 제약이 없으므로). 스냅샷과 원본 사이에 지연 창이 생깁니다.

  3. 생성 경로 결함의 형태가 소멸하고 상태 전이 결함이 부상한다. 묶음 4에서 본 “락 해제 후 커밋” 역순은 모놀리스 특유 전제라서 트랜잭션 경계가 분리되면 사라집니다(단 재고 서비스 내부 락 순서는 재검증 대상). 반대로 상태 전이 동시성 결함은 트리거가 네트워크 경계를 넘으면서 필수가 됩니다. 같은 변화가 한 결함은 (형태를) 죽이고 한 결함은 살린다.

모놀리스의 한 줄이 Phase 4의 여러 단계가 된다

가장 구체적으로 이해하기 쉬운 건 위 가지 (a) “재고 예약 실패 후 주문 취소”입니다. 모놀리스에서 이것은 한 트랜잭션의 자동 rollback이었습니다. 서비스가 쪼개지면 같은 일이 여러 네트워크 홉으로 흩어집니다.

[Phase 3: 단일 트랜잭션]  
BEGIN → 주문 생성 → 재고 차감(낙관적 락) → COMMIT  
        (어디서 실패하든 한 번에 rollback)  
  
[Phase 4: Choreography Saga, 가지 (a) 재고 예약 실패]  
Order      주문 PENDING 저장 + outbox(order.created)          (Order DB 트랜잭션)  
Inventory  order.created 수신 → 재고 예약(차감) 시도  
           실패 → outbox(inventory.reserve.failed)            (Inventory DB 트랜잭션)  
Order      inventory.reserve.failed 수신 → 주문 CANCELLED     (Order DB 트랜잭션)  

가지 (b) 결제 실패 후 복구는 이와 대칭입니다. payment.failed를 수신한 Order가 주문을 CANCELLED로 바꾸고 order.cancelled를 발행하면, Inventory가 그것을 받아 이미 차감한 재고를 inventory.restore로 되돌립니다.

여기서 모놀리스엔 없던 실패 모드가 생깁니다. 각 단계가 부분 성공할 수 있고(주문은 됐는데 재고 이벤트가 안 감), 보상 이벤트 자체가 유실될 수도 있습니다. 그래서 Phase 3에서 마련해 둔 Outbox, 멱등성과 DLQ가 Phase 4에선 Saga의 척추가 됩니다. 보류해 둔 발행 타임아웃과 DLQ 유실 방지(묶음 3)가 여기서 “있으면 좋은 것”에서 “없으면 보상이 새는 것”으로 격상되는 이유입니다. 단일 DB rollback이 받쳐줄 때는 드문 장애 경로였지만 Saga에선 그 한 줄이 정합성을 지키는 최종 안전장치입니다.

그래서 Phase 4 진입 전 검증해야 할 것이 명확합니다: 단일 DB 낙관적 락을 대체하는 Saga 보상 흐름의 정합성을, 낙관적 락이 받쳐주던 것과 동등하게 증명할 수 있는지입니다.


공유 모듈: 둘 수 있는 것과 두면 안 되는 것

멀티모듈에서 공유 모듈은 장점과 단점이 있습니다. 공유가 과하면 서비스 독립성이 깨지고 부족하면 중복이 생깁니다.

둘 수 있는 것

common/  
├── event/         OrderCreatedEvent, PaymentCompletedEvent ...  (서비스 간 계약)  
├── outbox/        발행 공통 로직 (인터페이스·추상 클래스)  
├── idempotency/   중복 소비 방지 공통 로직  
├── exception/     공통 예외 / 에러 코드 포맷  
└── response/      표준 응답 포맷  

기준은 “서비스 간 계약이거나 순수 메커니즘인가” 입니다. 이벤트 DTO는 발행자와 소비자가 합의해야 하는 계약이고 Outbox/멱등성은 비즈니스와 무관한 메커니즘입니다. 이것은 공유가 맞습니다.

단, 여기서 경계를 한 번 더 좁혀야 합니다. 계획안을 그대로 옮기면 발행 이벤트와 처리 기록 같은 저장 엔티티까지 공유 모듈에 두기 쉬운데, 서비스별 DB, 서비스별 마이그레이션으로 가면 테이블 스키마와 엔티티를 공유 모듈에 강하게 묶는 순간 서비스별 진화가 막힌다, 한 서비스가 컬럼 하나를 바꾸려 해도 공유 엔티티를 건드려 전 서비스에 영향이 번지기 때문입니다. 그래서 더 안전한 경계는 이렇습니다.

계약 DTO + 순수 메커니즘(추상 발행/소비 로직)은 공유 모듈, 서비스별 저장소, 마이그레이션, 테이블 소유권은 각 서비스.

공유 모듈은 “어떻게 발행하고 멱등 처리하는가”(인터페이스와 추상 클래스)를 주고 “어디에 어떤 스키마로 저장하는가”(엔티티, 마이그레이션, 구현체)는 각 서비스가 가집니다. 같은 발행 로직이라도 이 선을 어디에 긋느냐가 서비스 독립성을 좌우합니다.

두면 안 되는 것

두면 안 되는 것이유
도메인 비즈니스 로직서비스의 존재 이유. 공유하면 서비스 경계가 무너짐
서비스별 정책(가격과 재고 규칙)서비스마다 달라야 함
환경 자격증명환경 프로파일에 둔다
섣부른 공통 추상화driver 없이 만들면 그 자체가 부채

마지막 행의 섣부른 공통 추상화는 부채 목록에서 “메시지 포맷 추출”을 뺀 것과 같은 원칙입니다. “여러 서비스가 쓸 것 같으니 미리 공유 모듈에 넣자”가 가장 위험한 유혹입니다. 실제 두 번째 서비스가 그것을 요구할 때까지 미룹니다.


Gateway 인증과 내부 서비스 신뢰 경계

Phase 4에서 인증은 앱 내부 JWT 필터에서 API Gateway 필터로 이동합니다. 이때 내려야 할 설계 결정은 다음과 같습니다.

flowchart LR  
    C["Client"] -->|"JWT"| GW["API Gateway<br/>JWT 검증 + Rate Limit"]
    GW -->|"신뢰 경계 내부<br/>(검증 완료)"| US["User Service"]  
    GW --> OS["Order Service"]
    GW --> PS["Payment Service"]

    style GW fill:#F1EFE8,stroke:#888780  
  1. 인증을 Gateway에서 한 번만 검증한다. 모든 서비스가 각자 JWT를 검증하면 중복이고 키 회전 시 N곳을 바꿔야 합니다. Gateway가 검증하고 통과한 요청은 신뢰 경계 내부로 들어옵니다. 내부 서비스는 식별 정보(userId)를 헤더로 받되, 재검증 부담은 덜되 신뢰 경계가 곧 보안 경계가 되는 트레이드오프를 감수합니다.

    다만 “헤더로 userId를 받는다”는 모델은 그 자체로는 위험하다. 외부에서 내부 서비스에 직접 접근할 수 있으면 누구나 X-User-Id 헤더를 위조해 타인을 사칭할 수 있기 때문입니다. 이 모델은 아래 전제가 함께 충족될 때만 유효합니다.

    • 외부에서 내부 서비스로의 직접 접근 차단 (NetworkPolicy / 내부 전용 Service, Gateway가 유일한 입구)
    • Gateway가 인바운드의 기존 identity 헤더를 먼저 제거한 뒤 검증 결과로 재주입 (클라이언트 헤더 위조 무력화)
    • 서비스 간 호출 인증 검토: mTLS 또는 내부 서명 토큰(단명 JWT 등). “내부망이니 신뢰”는 zero-trust 원칙으로는 불충분

    이 전제 없이 헤더 신뢰만 도입하면 Gateway가 보안을 집중시킨 게 아니라 우회 가능하게 만든 셈이 됩니다.

  2. 여기서 묶음 1의 보안 부채가 전부 살아난다. 이 이동 작업이 비대칭 키 회전, 토큰 재사용 탐지, 인증 실패 관측성의 자연스러운 처리 시점입니다. 비대칭 키로 전환하면 Gateway가 공개키로 검증하고 발급 서비스가 개인키로 서명하는 분리가 가능해지는데 이때 시크릿 저장소가 없으면 키 배포와 회전 운영 절차가 빈약해집니다.

즉 Gateway 분리는 인증 모델 전체를 다시 그리는 작업이고 보류해 둔 보안 부채 네 가지가 그 안에서 함께 처리됩니다.


부하와 테스트가 아직 못 본 것: 검증 갭 체크리스트

지금까지의 22개는 “코드의 결함”입니다. 그런데 이 글이 추가로 떠안는 게 하나 더 있습니다: “아직 검증하지 못한 것”. 이것은 테스트 커버리지의 공백이라 코드 결함인 부채 목록과는 따로 적어 뒀습니다. 단독 항목으로 번호를 붙이기보다, Phase 4 진입 전 체크리스트로 들고 가는 게 맞기 때문입니다.

검증 갭무엇을 못 봤나왜 Phase 4 전 필요한가
Outbox chaos 테스트at-least-once 중복 창, 브로커 다운 후 재발행, dual-write 롤백 원자성을 장애 주입으로 미검증분산 환경에서 비로소 필수
스케줄러 상호배제통합 테스트가 단일 컨텍스트라 “두 인스턴스 경쟁 시 하나만 실행”을 검증 못 함Phase 4 서비스별 스케줄러 증가 시
Saga 보상 정합성단일 DB 낙관적 락을 대체할 보상 흐름의 정합성 미측정DB 분리의 핵심 안전망
다중 트리거 상태 모순결제 완료와 타임아웃이 같은 주문에 동시 적용되는 시나리오 미측정트리거가 네트워크 경계 넘으면 흔해짐

이것들의 공통점은 모놀리스에선 무해하거나 드물어 잠복하지만 분산에선 일상이 된다는 것입니다. 동시 주문 부하 테스트에서 정합성을 실측했듯, Phase 4에선 이 갭들을 실측 대상으로 삼아야 합니다.


한계: 이 글이 결론 내지 못한 것

항목왜 보류했나
22개 각각의 처리 여부Phase 4 착수 시점의 작업 결정에 종속. 지금 단정하면 추측
우선순위 절대값대부분 “묶음으로 처리”라 단독 순위가 의미 없음
전수 해소 일정일부는 Phase 4에서 형태 소멸, 일부는 필수화. 일정은 구조 결정 후
검증 갭의 측정 방법chaos/상호배제 측정은 Phase 4 분산 환경이 전제

이 글의 목적은 “무엇이 있는지 알고 들어가자”입니다. 지금 다 고치려는 것은 아닙니다. 22개를 인지한 상태로 Phase 4에 진입하는 것과, 모르고 진입해 다시 발견하는 것은 다릅니다.


더 읽을거리

질문같이 읽을 것연결 지점
왜 단계적 MSA인가모놀리스에서 MSA로 가는 흐름 글Phase 4 진입 명분
분산 락 vs 낙관적 락 실측분산 락 글, 동시 주문 부하 테스트 글어떤 결함은 사라지고 어떤 건 강해지나
Outbox, 멱등성, DLQOutbox 글, Consumer 멱등성 글Saga의 척추
Choreography Saga / eventual consistency(외부) 분산 트랜잭션과 Saga 패턴 자료서비스별 DB 정합성
내부 신뢰 경계 / 헤더 위조 방어(외부) zero-trust 네트워킹 자료Gateway 전제

Phase 4로 가져가는 한 문장

이 연재의 마지막 글에서 남기는 결론은 다음과 같습니다.

Phase 4는 이 22개 중 일부를 형태 소멸시키고(주문 생성 경로의 락 순서), 일부를 필수로 끌어올리며(상태 전이 동시성), 나머지를 Gateway, 공유 모듈, 서비스별 DB라는 새 구조 안에서 다시 묻는다.
코드를 남에게 설명할 수 있을 만큼 이해하면 그 코드의 결함도 보인다. 이 연재가 증명한 것이 이것이고, Phase 4에서도 같은 방식으로 부채를 발견하게 될 것이다.