Files
DongHyeonkaandClaude Opus 5 ef947e5bb0 refactor(build,ci): 현재 상태 검증을 걷어내고 불변조건만 남기는 검증 표면 축소
외부 리뷰("현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를
다시 검증하는 구조까지 생겼다")를 설계 문서로 정리하고 코드로 반영한다.
설계·판단 근거는 docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md.

삭제
- .github/ci-gate-matrix.yml(1,025줄) + verify-gate-matrix.sh(568줄):
  Gradle task graph와 workflow graph에 이미 있는 정보의 3중 복제
- verify-gradle-wrapper.sh(799줄): workflow 바이트 해시 잠금.
  wrapper 검증은 gradle/actions/wrapper-validation(full SHA 핀)에 위임
- DeveloperExperienceContractTest 등의 CI YAML mutation 테스트:
  애플리케이션 test suite가 GitHub Actions YAML 파서를 검증하던 계층 역전
- 문서 drift 파서: verifyReadmeCommands, verifyRunbookReferences,
  verifyDocumentedLeafCount, verifyTestSourceSetRegistry
- 빈 레지스트리를 지키던 커스텀 YAML 파서: verifyTrivyignore,
  verifyQuarantineSunset, flaky-quarantine.yaml
- verifyConfigurationPropertiesProcessor, verifyOneTypePerFile:
  각각 ca.spring-config convention과 Checkstyle OneTopLevelClass가 대체
- 정상 입력으로도 성공할 수 없던 messaging always-fail task
- ModuleRegistry의 JSON 필드 집합 정확 일치, sample-portfolio negative guard

이동
- java/quality/spring 공통 설정을 configure(subprojects) 블록에서
  ca.java-conventions / ca.quality-conventions / ca.java-library /
  ca.spring-library convention plugin으로
- 아키텍처 검증을 ca.architecture로, JPA·messaging qualification을
  gradle/qualification/ 아래로, verifyEnvKeys를 :app-bootstrap 소유로

완화
- Git revision은 releaseCheck·아카이브 생성에서만 요구. 일반 빌드는 SNAPSHOT
- SpotBugs/FindSecBugs는 로컬 check에서 빼고 qualityCheck 레인으로

task 계층
- leaf check는 그 leaf만. architectureCheck / qualityCheck /
  configContractCheck / integrationCheck / ci / releaseCheck로 이름 분리

CI
- _reusable-gradle.yml 신규. checkout + wrapper validation + JDK/캐시 공통화
- fileserver-release.yml -> fileserver-certification.yml (CD가 아니라 certification)
- GitHub Actions = CI + artifact, Argo CD = CD 경계를 docs/ci-cd/boundary.md로 고정

순증감 +3,274 / -7,483.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 20:33:19 +09:00

114 lines
4.0 KiB
Markdown

# 운영 Runbook
## 배포 전 체크
```bash
./gradlew verifyCleanArchitectureDependencies --console=plain
./gradlew verifyRuntimeModuleMembership --console=plain
./gradlew checkstyleMain --console=plain
```
destination profile은 startup에서 검증된다. 아래는 **부팅 실패**다.
- ordered destination + reorder 가능 retry
- `ordering=KEY` + key resolver 없음
- payload 상한 > 8,388,608 bytes
- DLQ 자기 참조 / retry 자기 참조
- retry·DLQ 그래프 cycle
- 미등록 retry·DLQ destination
- M1 destination + manual settlement
- `AT_LEAST_ONCE` + confirmation `NONE`
- production profile + topology auto-create
- broker topology가 manifest와 불일치
## 증상별 대응
### publish가 AMBIGUOUS로 쏟아진다
broker confirm 경로 문제다. 실패가 아니다.
1. `PublishEvidence.transmission``MAY_HAVE_BEEN_TRANSMITTED`인지 확인
2. Kafka: `delivery.timeout.ms`, ISR 상태, leader election 확인
3. Rabbit: confirm timeout, channel 상태 확인
4. Outbox를 쓰고 있다면 `status='AMBIGUOUS'` row가 같은 messageId로 재시도 중이다. **정상이다.**
5. consumer 쪽 Inbox가 중복을 흡수하는지 확인
`AMBIGUOUS`를 실패로 취급해 새 messageId로 재발행하지 말 것. 중복이 복구 불가능해진다.
### DLQ가 비어 있는데 메시지가 사라졌다
DLQ publish 실패 시 source는 settlement되지 않는다. 메시지는 source에 남아 재전달된다.
1. `msg.failure-code``DEAD_LETTER_*`인 로그 확인
2. DLQ destination이 실제로 존재하는지 (topology validation)
3. DLQ credential에 publish 권한이 있는지
### consumer lag이 한 partition에서만 증가한다
`ContiguousPartitionOffsetTracker`가 gap에서 멈춘 것이다. 설계된 동작이다.
commit은 **연속** 완료 offset까지만 전진한다. offset 11이 아직 실행 중이면
10과 12가 끝나도 watermark는 10에 머문다. 12를 commit하면 consumer가 죽었을 때 11을 잃는다.
1. 해당 partition의 in-flight를 확인
2. 느린 handler를 찾는다 (`handlerTimeout` 초과 여부)
3. 필요하면 `PAUSE_PARTITION` retry가 걸려 있는지 확인
### 재시도 폭풍
`RetryPolicy.jitter=false`인지 확인한다. jitter 없이는 같은 초에 실패한 모든 consumer가
같은 초에 재시도한다.
### shutdown이 오래 걸린다
`GracefulShutdownCoordinator`가 in-flight를 기다리는 중이다.
- `inFlight()`가 0이 되면 즉시 종료
- drain deadline(기본 30초) 초과 시 남은 작업을 **unsettled로 포기**한다 → broker가 재전달
- draining 시작 후 새 retry attempt는 만들지 않는다
## Destructive 작업
전부 `DestructiveOperationGuard`를 통과해야 한다.
| 조건 | 요구 |
|---|---|
| admin credential | application runtime은 보유하지 않음 |
| `AdminApproval` | 유효기간 내 |
| dry-run | 항상 허용 |
### Replay
```text
기본: 격리된 consumer group (replay-<requestId>)
기존 group 대상: 승인 티켓 필수
```
기존 production group으로 replay하는 것은 "다시 읽기"가 아니라 **live consumer를 되감는 것**이다.
그 사이의 모든 것이 재처리된다.
### Redrive
```text
dry-run으로 후보 수 확인
→ 승인 획득
→ batch 100건 이하로 실행
→ republish CONFIRMED 인 것만 DLQ에서 settlement
```
`redriveId`로 재구동 루프를 추적한다. 같은 메시지가 반복해서 redrive되면
근본 원인이 해결되지 않은 것이다.
### Offset reset
`KafkaOffsetResetExecutor`는 승인 predicate를 **생성자 인자**로 받는다.
승인 소스 없이 조립된 runtime은 물리적으로 reset을 수행할 수 없다.
## Topology
production topology는 IaC가 만들고 애플리케이션은 **검증만** 한다.
`TopologyValidationRuntime`은 모든 불일치를 한 번에 보고하고 startup을 실패시킨다.
partition 수가 다르면 destination이 광고하는 ordering 보장이 달라지고,
`min.insync.replicas`가 없으면 `acks=all`의 의미가 달라진다.