← 글 목록

PR 머지 기준 정리 — CI 통과와 "머지해도 되는가"는 다른 질문이다 (연재 3/3)

/ 6분 분량

깃허브 PR 하나를 붙잡고 "이거 머지해도 되나?"를 판단하는 기준을 제대로 세워보려고 실제 PR을 하나 끝까지 파봤습니다. 결론부터 말하면 코드 품질, 테스트 커버리지, 절차적 게이트(리뷰 승인)는 완전히 별개의 축이라는 걸 몸으로 확인한 경험이었습니다.

학습 주제

  • 주제: PR 머지 가능 여부를 판단하는 체계적인 방법 (이슈 완료기준 대조, 테스트 커버리지 매트릭스, 리뷰 대응 추적)
  • 학습 날짜: 2026년 7월 21일

탐구 과정

처음 질문은 단순했습니다. "이 PR, 머지해도 될까?" 그런데 이 질문에 제대로 답하려면 생각보다 여러 층위를 훑어야 한다는 걸 깨달았습니다. 코드가 잘 돌아가는지(CI), 기능이 요구사항대로 동작하는지(이슈 완료기준), 팀이 정한 절차를 지켰는지(리뷰 승인)를 각각 따로 확인해야 하더라고요.

처음엔 CI만 초록불이면 머지해도 될 거라고 막연히 생각했습니다. 그런데 이슈 문서를 다시 열어보니 "선우님 또는 시현님의 리뷰 승인 없이는 머지하지 않는다"는 조건이 명시돼 있었습니다. 실제 리뷰 기록을 보니 리뷰는 딱 한 명이 남겼는데, 그마저도 APPROVED가 아니라 COMMENTED였습니다. 여기서 막혔습니다 — CI는 통과했는데 이 조건은 CI로 대체가 안 되는구나, 라는 걸요.

그다음엔 리뷰에서 지적된 사항들이 실제로 반영됐는지 궁금해졌습니다. 리뷰 코멘트만 읽고 "반영했겠지"라고 넘기지 않고, 각 지적사항을 실제 커밋 해시와 하나씩 대조해봤습니다. 이 과정에서 지적 4개 중 3개가 실제로 커밋으로 반영됐고, 1개는 의도적으로 범위에서 제외했다는 걸(그리고 그 이유가 PR 설명에 명시돼 있다는 걸) 확인할 수 있었습니다.

핵심 학습 내용

1. 이슈 완료기준(DoD) 대조표 만들기

기능 요구사항을 표로 정리해서 각 항목이 실제 구현·테스트로 커버되는지 하나씩 체크하는 방식이 유용했습니다.

| 완료기준 | 상태 |
|---|---|
| 관리자가 모집 상태를 켜고 끌 수 있다 | ✅ open()/close() |
| 켜면 구독자 전원 발송 + 로그 기록 | ✅ E2E로 실제 검증 |
| 중복 발송 안 됨(멱등성) | ✅ 순차·동시성 양쪽 테스트 |
| 리뷰 승인 없이 머지 금지 | ❌ 미충족 |

기능적으로는 다 채워졌어도 절차적 조건 하나가 빠지면 전체가 "머지 불가" 상태가 된다는 게 인상적이었습니다.

2. 테스트 커버리지 매트릭스

단위 테스트, 통합 테스트(MockMvc), E2E 테스트(실제 SMTP)를 시나리오별로 매트릭스로 그려보니 어디가 비어있는지 한눈에 보였습니다.

| 시나리오 | 단위 | 통합 | E2E |
|---|---|---|---|
| 닫힘→열림, 전원 발송 | | ✅(mock) | ✅ 실수신+로그 |
| 동시 트리거, 1회만 | | ✅(mock, 10스레드) | ✅(실SMTP, 6스레드×15명) |
| 권한 부족(403) | | ✅ | |

빈 셀이 있다고 무조건 문제가 아니라, "다른 레이어에서 이미 커버되는가"를 같이 봐야 한다는 점도 배웠습니다.

3. 동시성·비동기 처리 패턴

리뷰에서 지적된 문제 중 하나가 "동시 클릭 시 중복 발송" 위험이었는데, synchronized 락으로 선제 방어한 커밋을 확인했습니다. 또 다른 지적인 "동기 발송으로 인한 타임아웃 위험"은 @Async + 이벤트 리스너 방식으로 비동기 전환한 걸 봤는데, 이게 @TransactionalEventListener(AFTER_COMPLETION, fallbackExecution=true) 조합이라는 게 눈에 띄었습니다. 트랜잭션이 완전히 커밋된 다음에만 이벤트를 발행해서, DB 반영과 이메일 발송 사이의 정합성을 지키는 패턴이었습니다.

이해한 내용

가장 크게 이해한 건 "CI 통과 = 머지 가능"이 아니라는 것입니다. CI는 코드가 깨지지 않았다는 것만 보장하지, 팀이 합의한 프로세스(리뷰 승인 같은)를 지켰는지는 전혀 판단해주지 않습니다. 이 둘을 분리해서 봐야 성급한 머지를 막을 수 있다는 걸 실감했습니다.

또 하나는 리뷰 지적사항을 "말로만" 확인하지 않고 실제 커밋과 대조하는 습관의 중요성입니다. 지적받은 게 정말 고쳐졌는지, 아니면 의도적으로 남겨둔 건지 구분해야 리뷰가 형식적으로 끝나지 않습니다. 잔여 리스크를 별도 이슈로 분리해서 판단 근거까지 문서화해두면, "왜 이걸 그냥 뒀지?"라는 나중의 의문에 답할 수 있다는 것도 배웠습니다.

CI 완료를 기다리는 백그라운드 모니터링을 20초마다 폴링하는 방식으로 짰다가, 알림이 너무 잦아서 불편했던 것도 소소하지만 실용적인 교훈이었습니다. 상태가 바뀔 때만, 혹은 완료 시점에만 딱 한 번 알려주는 방식이 훨씬 낫다는 걸 몸으로 느꼈습니다.

실전 적용

앞으로 PR을 리뷰하거나 머지 여부를 판단할 때 다음 순서를 체크리스트처럼 쓰려고 합니다.

  1. 이슈/스펙 문서에서 완료기준을 표로 뽑아 각 항목 충족 여부 확인
  2. 테스트를 단위/통합/E2E로 나눠 매트릭스로 시각화, 빈 셀은 이유를 명시
  3. 리뷰 지적사항은 실제 커밋 해시와 1:1 대조
  4. CI 통과와 절차적 게이트(승인, 정책 등)를 완전히 분리해서 판단
  5. 반영하지 않은 지적사항은 별도 이슈로 분리하고 판단 근거를 남기기

이 방식은 개인 프로젝트든 팀 프로젝트든, PR이 여러 개 쌓이는 상황에서 머지 순서와 우선순위를 정할 때 그대로 써먹을 수 있을 것 같습니다.

추가 학습 계획

  • @TransactionalEventListener의 다른 phase(BEFORE_COMMIT, AFTER_ROLLBACK 등)별 동작 차이를 더 파보고 싶습니다.
  • synchronized 락 말고 분산 환경에서도 통하는 동시성 제어 방식(분산 락, 낙관적 락)도 비교해보고 싶습니다.
  • 폴링 대신 이벤트 기반으로 상태 변화를 감지하는 패턴을 백그라운드 작업 모니터링에 어떻게 더 깔끔하게 적용할 수 있을지 고민해볼 계획입니다.