이력서에 넣을 수치 목록을 하나 받았다. “구축량 중심 문구”를 “검증 수치”로 바꾸라는 조언과 함께 8개 축이 붙어 있었다.

검증 분야별로 이력서에 넣을 수 있는 수치 8행 — 성능(TPS·동시 사용자 수·p95·p99 응답시간), 이벤트 정합성(발행 이벤트 수·유실 중복 반영 건수), 장애 복구(Kafka/DB 중단 후 복구 시간·미처리 이벤트 재처리율), 대사(1일 대사 건수·불일치 탐지율·처리시간), 회계(생성 전표 수·차대 불일치 건수), 아키텍처(ArchUnit 규칙 수·위반 건수), 테스트(단위 통합 테스트 수·핵심 도메인 커버리지), 배포(서비스 수·배포시간·롤백시간)

목록 자체는 맞는 말이다. “MSA 17개 구축”보다 “ArchUnit 규칙 59개, 위반 0건”이 세다. 그래서 내 정산 프로젝트를 이 8행으로 직접 채점해 봤다.

그런데 채점을 하다 보니 표가 묻지 않는 질문이 하나 남았다. 그 수치는 어디서 나오는가. 8행을 다 채운 이력서와, 8행 중 3행만 채웠지만 전부 재현 가능한 이력서 중에 뭐가 더 강한가. 나는 후자라고 본다. 면접에서 나올 다음 질문이 “몇이었어요?”가 아니라 “그거 어떻게 뽑으셨어요?” 이기 때문이다.

그 기준으로 다시 보니 내 수치들은 세 등급으로 갈렸다.


1등급 — 세면 나오는 수치

명령 한 줄이면 지금 이 자리에서 재현되는 수치다. 반박이 불가능하다.

아키텍처. 서비스 17개에 ArchUnit 테스트 파일이 하나씩 있고, 그 안의 ArchRule 선언은 전부 59개다.

for f in $(find . -path "*/src/test/*" -name "*Architecture*Test.java" -not -path "*/build/*"); do
  grep -c 'ArchRule ' $f
done

규칙 내용은 헥사고날 경계 3종이 뼈대다 — 도메인이 Spring 을 모를 것, 애플리케이션이 JPA 를 직접 만지지 않을 것, 어댑터가 다른 도메인을 넘보지 않을 것. ArchUnit 은 이 규칙을 테스트로 표현하므로 위반은 곧 빌드 실패다.1 “위반 0건”은 주장이 아니라 CI 의 초록불이다.

테스트 수. @Test 가 자바 6,701개, 코틀린 83개. 테스트 파일은 961개(java)와 15개(kt), 이름이 *IntegrationTest 인 것이 24개.

이 등급의 수치는 거짓말을 할 수 없다. 누가 물어보면 그 자리에서 grep -c 를 치면 된다. 대신 약하다. 규칙이 59개라는 사실은 그 59개가 의미 있는 규칙인지 아무것도 보장하지 않는다. 세는 것과 지키는 것은 다르다.

2등급 — 게이트가 지키는 수치

숫자가 기준 아래로 떨어지면 빌드가 깨지는 수치다. 이게 이력서에 넣기 가장 좋은 종류다.

정산 서비스 커버리지가 여기 속한다. 2026-07-12 실측으로 테스트 520 통과 / 실패 0, LINE 커버리지 94.17%(4,103/4,357), INSTRUCTION 93.12%. 재현 커맨드는 리포 문서에 그대로 박혀 있다.

./gradlew :settlement-service:test :settlement-service:jacocoTestCoverageVerification --console=plain

핵심은 뒤쪽 태스크다. jacocoTestCoverageVerification 은 정해 둔 최소치를 못 넘기면 빌드를 실패시킨다.2 즉 “94.17%”는 문서에 적어 둔 과거가 아니라 90% 아래로 내려가면 자동으로 무너지는 현재다. 문서의 초록불과 빌드 결과가 어긋날 수 없는 구조다.

다만 이 글이 쓴 94.17% 자체는 인용이다 — 리포 문서에 기록된 2026-07-12 값이고 이번에 재실행하지 않았다. 같은 날 올린 커버리지 95%가 보증하는 것과 보증하지 않는 것 에는 오늘 CI 아티팩트로 다시 집계한 값이 있다 — 정산 서비스 LINE 95.39%, 게이트 대상 전체 95.26%, 전체 소스 90.50%. 숫자가 어긋나 보이는 건 날짜와 범위 정의(게이트 대상이냐 전체 소스냐)가 다르기 때문이다. 이 글의 주장을 이 글에 그대로 적용하면 답은 분명하다 — 재측정한 쪽을 믿어라.

같은 등급에 회계와 이벤트 정합성이 들어간다.

  • 회계 — 원장·분개·시산표 테스트 27개. 그중 압권은 시산표가 균형인데도 분개가 틀린 경우를 잡는 검사다. 차변 합과 대변 합만 보면 통과하는 상태에서 분개 한 짝이 통째로 빠졌거나 반쪽만 기록된 걸 따로 검출한다. 균형은 정합성의 필요조건일 뿐이라는 걸 테스트로 표현한 셈이다.
  • 이벤트 정합성 — 멱등이 3층이다. 발신 측 outbox 의 event_id UNIQUE, 수신 측 processed_events PK, 마지막으로 업무 테이블의 DB UNIQUE 제약. Kafka 가 최소 한 번(at-least-once) 전달을 보장하는 이상 중복은 온다는 전제 위에 서 있는 설계다.3 트랜잭셔널 아웃박스는 DB 트랜잭션과 메시지 발행 사이의 원자성을 위한 표준 패턴이고,4 여기에 수신 측 멱등을 겹쳐야 비로소 “유실 0 · 중복 반영 0”을 말할 수 있다.

3등급 — 아무도 지키지 않는 수치

그리고 여기서 내가 내 발등을 찍었다.

README 에는 이미 성능 표가 있다. 결제 승인 200 VU 에 p95 412ms, p99 687ms — 이런 식으로 시나리오 4종의 RPS 와 백분위 지연이 적혀 있다. 표 아래에는 이렇게 쓰여 있었다.

CI 에서 k6 thresholds 로 회귀 자동 감지.

확인해 봤다.

grep -rn "k6 run\|load-test" .github/workflows/
# (출력 없음)

참조 0건. 워크플로 8개 어디에도 k6 를 실행하는 잡이 없다. 부하 테스트 결과 파일도 리포에 없다 — load-test/ 에 있는 건 시나리오 스크립트 4개와 README 뿐이다.

그러니까 저 표의 수치는 어느 시점에 로컬에서 재서 옮겨 적은 것이고, 지금은 아무것도 그 수치를 지키고 있지 않다. 코드가 두 배 느려져도 표는 그대로다. 그리고 표 아래 한 줄은 사실이 아니었다.

k6 의 thresholds 는 원래 이런 용도로 만들어진 기능이다 — 조건을 못 넘기면 종료 코드를 0 이 아닌 값으로 내서 파이프라인을 실패시킨다.5 기능이 없어서 못 한 게 아니라, 쓴다고 적어 놓고 안 쓴 것이다.

수치를 지운 게 아니라 이 사실을 적는 쪽을 택했다. 이력서에 저 p99 를 쓰려면 순서는 하나뿐이다. 돌린다 → 결과를 커밋한다 → thresholds 를 CI 에 건다. 그 전까지 저건 수치가 아니라 기억이다.


아직 못 채운 두 칸

장애 복구. DLQ 와 리플레이 콘솔, 회복 에스컬레이션 통합테스트, Resilience4j 서킷브레이커는 다 있다. 없는 건 실제로 Kafka 나 DB 를 죽였다가 살리는 시나리오와, 거기서 나오는 복구 시간이다. 지금 있는 건 “복구 로직이 올바른가”이지 “복구에 몇 초 걸리는가”가 아니다.

배포. 서비스 디렉터리 24개(그중 17개가 Gradle 빌드, 7개는 폴리글랏), CI 잡 10개, GHCR 이미지 빌드와 배포 후 스모크까지 있다. 없는 건 배포 소요시간과 롤백 소요시간이다.

이 두 칸은 DORA 가 이미 정의해 둔 지표와 정확히 겹친다. 다만 이름을 최신으로 맞춰 쓰는 게 좋다. DORA 는 2023년에 MTTR 을 failed deployment recovery time(실패한 배포로부터의 복구 시간)으로 개명·재정의했고, 2024년에 다섯 번째 지표인 deployment rework rate 를 추가해 지금은 4개가 아니라 5개 체계다.6 이력서에 “MTTR”이라고 쓰면 2018년에 멈춘 사람처럼 보인다. 사소하지만 공짜로 얻는 신호다.


채점표

지금 상태 등급
아키텍처 ArchRule 59개 / 위반 0 1 → 2등급(테스트가 강제)
테스트 @Test 6,784개 · 통합 24개 1등급
커버리지 정산 모듈 520통과 · LINE 94.17% 2등급
회계 원장·시산표 27개, 반쪽 분개 검출 2등급
이벤트 정합성 멱등 3층 + 정합성 스위트 2등급
대사 대사 테스트 25개+ · PG 차이 5종 분류 2등급 (탐지율은 미측정)
성능 표는 있으나 지키는 게이트 없음 3등급
장애 복구 로직 검증만 · 복구시간 없음 미측정
배포 파이프라인만 · 시간 없음 미측정

가장 싸게 올릴 수 있는 칸은 대사의 불일치 탐지율이다. 불일치를 N건 인위적으로 주입해 M건 잡히는지 재면 끝이다. 검사기가 실제로 무언가를 잡는지 확인하는 이 방식은 뮤테이션 테스팅의 논리 그대로다 — 게이트가 통과했다는 사실만으로는 그 게이트가 일하고 있는지 알 수 없다.


정리

표가 준 8행은 “무엇을 재라”까지만 말한다. 실제로 이력서를 지켜 주는 건 그다음 한 겹이다. 이 수치는 누가 지키고 있는가.

  • 세면 나오는 수치는 반박당하지 않지만 아무것도 증명하지 않는다.
  • 게이트가 지키는 수치는 떨어지는 순간 빌드가 깨지므로, 문서의 초록불과 현실이 어긋날 수 없다.
  • 지키는 사람이 없는 수치는 시간이 지날수록 조용히 거짓말이 된다. 내 README 의 성능 표가 그랬다.

8행을 다 채우는 것보다, 채운 칸마다 “어떻게 뽑았는지” 한 줄을 붙일 수 있는 쪽이 낫다. 그 한 줄이 없으면 면접에서 그 수치는 없는 것과 같다.


References

본문의 정산 프로젝트 수치는 2026-08-18 에 develop 브랜치(93fe6368)에서 grep·find 로 직접 집계했다. 커버리지·테스트 통과 수(520 / 94.17%)는 리포의 검증 문서에 기록된 2026-07-12 실측값을 인용한 것이며 이번에 재실행하지 않았다 — 인용한 수치와 재실행한 수치를 섞지 않기 위해 밝혀 둔다. 성능 표의 p95·p99 수치는 리포 README 에 적힌 값을 근거가 확인되지 않은 인용으로 명시해 옮긴 것이지, 내가 재현한 값이 아니다.

  1. ArchUnit User Guide 참조. 규칙은 JUnit 테스트로 실행되므로 위반은 테스트 실패로 나타난다. 

  2. JaCoCo 의 커버리지 검증 태스크는 설정한 최소치를 만족하지 못하면 빌드를 실패시킨다. 

  3. Apache Kafka 공식 문서의 Message Delivery Semantics 절. 기본 구성에서 재시도로 인한 중복 전달이 가능하며, 이를 전제로 수신 측 멱등이 필요하다. 

  4. microservices.io 의 Transactional outbox 패턴 정의. 

  5. Grafana Labs 의 k6 Thresholds 문서. 임계값을 만족하지 못하면 k6 가 0 이 아닌 종료 코드를 반환한다. 

  6. dora.dev 의 지표 가이드와 변천사 문서.