여기어때 유저혜택개발팀에서 운영 환경 NPE를 원인 추적하다 코드 구조적 결함을 발견하고, 개발자 실수가 아닌 타입과 도메인 설계 수준에서 원천 차단하는 방법으로 전환한 과정을 다룬다. 나아가 Claude Code 기반 AI 에이전트 파이프라인에 장애 교훈을 자동화 체크리스트로 녹이는 방법까지 이어진다.
핵심 포인트- 리팩토링 중 예산 제한 쿠폰 검증 if문을 호출부로 이동하면서 null 게이트가 사라져 일반 쿠폰(issueEndDate=null)에서 운영 NPE 발생
- 컴파일러·테스트·코드 리뷰 모두 이 버그를 잡지 못한 이유는 Java 타입 시스템이 null 가능성을 컴파일 시점에 강제하지 않기 때문
- 해법은 null 체크 추가가 아니라 "깜빡하는 것 자체가 불가능한 구조" — Optional, VO 응집, 경계 null 정리, @Nullable 어노테이션
- Claude Code SSOT 브랜치 + AI 에이전트 파이프라인을 통해 장애 패턴을 implementer.md 체크리스트로 자동화해 재발 차단
상세 정리- 사건: 쿠폰 복원(주문 취소 시 Redis 예산 집계 되돌리기) 로직 리팩토링 중 일반 쿠폰에서 NPE 발생, 쿠폰 복원 자체가 멈춤
- NPE 경로: CommonUseService에서 BudgetVerifyVO.of(policy, issue) 직접 호출 → getTtlExpireDate → DateUtil.getLaterDate(a=null, b) → a.isBefore() NPE
- 원래 구조: UseRedisService 내부에 if (policy.isBudgetLimitPolicy()) 게이트가 null 유입을 차단하고 있었음
- 리팩토링 실수: of() 메서드를 서비스 밖 호출부로 이동하면서 게이트(if문)를 함께 옮기지 않음 — 조건문이 내부 코드의 null 전제조건을 지키던 울타리였다
- 왜 못 잡았나: Java 타입으로 null 강제 불가, 비예산×복원 엣지 케이스 테스트 미비, 다중 레이어로 코드 리뷰 시 직관적 발견 어려움
- 설계 1 — Optional/VO 승격: Policy.budget()을 Optional<Budget> 반환, .map().ifPresent() 체이닝으로 게이트 누락 자체가 구조적으로 불가능하게
- 설계 2 — null 판단 소유자 이관: 범용 DateUtil이 아닌 IssuePeriod 값 객체 내부에서 endDate null 여부를 laterOf() 메서드로 처리 — 도메인 지식의 도메인 객체 응집
- 설계 3 — 경계에서 null 즉시 정리: Mapper/Adapter/팩토리에서 requireNonNull 또는 기본값으로 차단, 도메인 내부는 null 안전 구역으로 유지
- 설계 4 — @Nullable 명시: JPA 엔티티 등 Optional 불가 지점에 어노테이션으로 정적 분석 도구(IntelliJ·SonarQube) 경고 유발
- 피드백 루프: 장애 분석 후 /improve-issue 명령으로 implementer.md에 "신규 nullable 필드 매핑 시 Objects.isNull 가드 필수" 체크리스트 자동 커밋
- 사이클: /issue 지라 티켓 → planner→test-writer→implementer→code-reviewer→ac-checker 5단계 에이전트 파이프라인
- 핵심 교훈: 장애 교훈이 회고 문서에만 머물면 잊힌다 — 에이전트 체크리스트로, 매 작업마다 로드되는 룰로, 사이클 안의 리뷰 기준으로 남겨야 한다
왜 읽나운영 NPE를 구조 설계로 예방하고 장애 교훈을 AI 에이전트 체크리스트로 자동화하려는 Java 백엔드 엔지니어에게.