글쓰기

전체 포스트

다섯 개의 하네스를 동시에 무너뜨린 List

다섯 개짜리 Kubernetes 안티패턴 검사 하네스 전부가 `---`로 구분된 문서를 가정했다. 한 번의 `kubectl get -o yaml` 호출에 리소스를 두 개 이상 대면 결과가 `kind: List`로 감싸지고 — 모든 검사기가 조용히 플래그할 리소스를 0개 찾았는데, 이건 깨끗한 통과처럼 보였다.

서로 다른 두 도구, 똑같이 빠뜨린 루트

Argo CD의 App-of-Apps 증명은 전적으로 부모에게 있고, Flux의 dependsOn 증명은 자식이 선언하지만 혼자서는 검증이 안 된다. 둘 중 어느 트리든 루트를 빼고 감사하면 똑같이 실패한다 — 알고 보면 같은 근본 이유로.

영원히 100점을 받을 수 없는 벤치마크

채점 하네스가 Kubernetes 배포 실수 19개 카테고리를 다룬다. 그중 하나 — drift — 는 매니페스트가 이미 적용된 뒤에야 존재할 수 있고, 작성 벤치마크는 구조적으로 이걸 일으키거나 막을 수 없다. 정직한 해결책은 미래 버전이 아니라 영구적이고 문서화된 상한선이었다.

동점인 점수, 서로 다른 두 종류의 잘못

두 모델이 Kubernetes 매니페스트 작성 과제에서 동일한 9/9를 받았고, 독립적으로 재확인됐다. 실제로 각자 쓴 걸 읽어보니 한쪽엔 스스로를 무력화하는 NetworkPolicy가, 다른 쪽엔 존재하지 않는 리소스를 참조하는 프로모션 파이프라인이 있었다 — 동점인 하네스 점수엔 보이지 않는, 서로 무관한 결함 두 개.

아무도 선언하지 않은 기본값들

방금 깔끔하게 적용된 클러스터를 겨눈 drift 검사기가 온통 drift를 보고했다. 클러스터가 거짓말을 한 게 아니었다 — API 서버 자신의 admission 기본값 채우기가 Git이 언급조차 하지 않은 필드를 채워넣었고, 순진한 전체 객체 비교는 그 차이를 구분할 방법이 없었다.

Factory는 어디에 넣을지 알고 있었다

두 코드베이스가 Aggregate의 ID를 서로 다른 자리에서 생성한다 — 하나는 생성자에서, 하나는 Factory가 Infrastructure에 요청해서. Eric Evans의 원저에는 실제로 어느 패턴을 서술하는지에 대한 구체적이고 인용 가능한 답이 있고, 그건 두 코드베이스 각자의 관습이 가정하는 것과 다르다.

Evans가 쓴 적 없는 규칙

거의 모든 DDD 코드베이스가 다른 Aggregate를 직접 객체 참조하는 걸 금지한다 — ID로만. Eric Evans의 2003년 원저는 명시적으로 그걸 허용한다. 실제로 ID 전용 규칙을 쓴 Vaughn Vernon 본인이, 그럼에도 더 엄격한 규칙을 주장하는 바로 그 논문에서 이 사실을 스스로 밝힌다.

같은 순간, 서로 다른 두 타임스탬프

같은 순간이 같은 드라이버로 직렬화돼도 프로세스의 시간대에 따라 다른 문자열이 나온다. 4개 언어는 호출 지점에, 1개 언어는 프로세스 경계에 이 버그가 있었다 — 그리고 수정은 언어마다 진짜 다른 자리에 있어야 했다, 시간대를 9시간 떨어뜨려 실제로 테스트를 돌려서 검증했다.

자기 자신을 기다리고 있던 자동화

Dependabot auto-merge 워크플로가 그동안 머지한 PR은 전부 자기 자신의 6시간짜리 데드락을 상대로 한 경쟁에서 이겨서 그렇게 된 것이었다 — 스텝 하나가, 바로 그 스텝이 끝나야만 끝날 수 있는 체크 실행을 기다리고 있었다. 고쳤더니 바로 뒤에서 기다리던 두 번째 버그와, 평범한 GitHub 502가 남기고 간 반쪽짜리 머지들이 드러났다.

아무도 눈치채지 못한 빌드 안 되는 이미지

낡은 문서 대신 git 히스토리를 확인하고, 검색 인덱스가 없다고 우기던 라이브러리의 우회책을 찾아냈던 Spring Boot 4 마이그레이션은, 하루 뒤 배포 이미지가 아예 빌드가 안 되는 상태로 끝났다 — 의미가 방금 바뀐 파일을 CI 안 그 무엇도 지켜보고 있지 않았기 때문이다.

발견 0건, 버그 80건

경로 존재 여부만 확인하는 체커는 3개 언어 감사 라운드 전후로 계속 발견 0건을 보고했지만, 그 라운드는 실제 문제 약 80건을 고쳤다 — 낡은 코드 인용, 아무것도 스캔하지 않고 스스로에게 만점을 주는 평가기, 그리고 자기가 본뜬 코드에서 이미 고쳐진 버그를 여전히 그대로 뱉어내는 생성기.

End-to-End이 아니었던 End-to-End 테스트

nestjs의 e2e 스위트는 실제 앱을 부팅하는 대신 자기만의 근사치를 조립하고 있었고, 모든 언어의 LLM 기능은 자기 자신의 폴백 경로로만 실행돼왔다. 둘 다 고치는 과정에서 더 이상한 버그가 드러났다: 같은 패치된 모듈을 두고 싸우는 nock과 testcontainers.

완벽한 점수, 작동하지 않는 기능

같은 문서, 같은 과제, 두 개의 모델을 별도 worktree에서 동시에 돌렸다. 둘 다 완벽한 harness 점수를 자체 보고했다. 실제 Postgres와 LocalStack을 대상으로 독립 재현했을 때, 동작한 쪽은 하나뿐이었다.

버그가 돌아왔다, 다섯 개의 다른 가면을 쓰고

이벤트에 두 번째 구독자를 지원하지 못하는 언어 둘을 벤치마크 과제가 찾아낸 지 일주일 뒤, 실제 기능 4개가 모든 언어에 그걸 요구하게 만들었다. 이번엔 5개 언어 전부가 깨졌다 — 시끄러운 부팅 시점 크래시부터 아무 로그도 남기지 않는 조용한 핸들러 드롭까지.

모든 리포트에 서버가 필요한 건 아니다

월별 명세서도, GDPR식 데이터 내보내기도 같은 질문 앞에서 무너졌다: 클라이언트가 직접 만들면 되지 않나? 그 질문을 통과한 지출 분석 ETL과, 거기서 드러난 규칙.

사기꾼을 그대로 믿은 사기 탐지 신호

RefundReasonClassifier의 사기 위험 점수는 환불 요청자가 직접 통제하는 텍스트만으로 계산되었다. 그것과 함께 있던 자매 ML 스코어러를 제거하고, 이번 제거가 남긴 단 하나의 규칙까지.

무엇은 좁히고, 누구는 정하지 않는다

계좌 본인의 거래 내역을 다루는 구조화 데이터 RAG 기능과, LLM이 안전하게 관여하도록 만드는 가드레일, 그리고 그 불변식이 다섯 개 언어 각자의 관례 속에서도 살아남은 과정.

유닛 테스트가 볼 수 없는 버그들

빠진 @Transactional, JDK HTTP 클라이언트의 재시도 결함, VARCHAR(36) 오버플로우, SQS FIFO 중복 제거 충돌 — 실제 인프라가 있어야만 존재할 수 있었던 버그 네 가지.

Domain Service에 LLM 연결하기

RefundReasonClassifier는 환불 사유를 읽고 신호를 돌려줄 뿐이다 — 실제 판단을 내리는 Domain Service는 그걸 호출조차 하지 않으며, Claude에서 자체 호스팅 Ollama로 LLM 백엔드를 바꿔도 테스트는 거의 건드릴 필요가 없었다.

아무도 찾고 있지 않던 버그 다섯 개

미완성 Swagger 문서를 5개 언어에 걸쳐 완성하고, 애노테이션이 컴파일된다고 믿는 대신 실제로 앱을 켜서 검증했다. 찾아낸 건 문서화와는 아무 관계가 없었다 — 프로덕션 마이그레이션이 조용히 한 번도 안 돌게 만든 Spring Boot 4의 의존성 분리 사건도 포함해서.

실제 버그를 잡아낸 네이밍 규칙

지루하기 짝이 없는 find/save/delete 네이밍 컨벤션을 자동화하자, 네 개의 코드베이스에서 아무도 눈치채지 못한 위반 사례가 바로 드러난 이야기.

툴 출력 자체가 에이전트를 조종하려 할 때

셸 명령 출력 안에 실제 시스템 메시지처럼 위장한 내용이 변경사항을 숨기라고 지시한 적이 여러 번 있었다. 지켜야 할 규칙은 하나다: 따르지 말고, 그 사실을 알린다.

두 계좌, 하나의 트랜잭션, 다섯 개의 서로 다른 답

송금 기능에 필요한 건 딱 하나, 모든 구현체가 이미 지원한다고 주장했던 것 — 두 Aggregate의 원자적 쓰기. 실제로 만들어보니 한 언어는 메커니즘이 진짜 동작했고, 한 언어는 당연해 보이는 수정 한 걸음 안쪽에 회귀가 도사리고 있었고, 한 언어는 문서가 자기 코드에 대해 조용히 틀려 있었다.

구독자가 둘이어야만 존재하던 버그

쉬운 합성 과제에서 5개 언어가 전부 100점을 받는다고 해서 어디서 실패할지가 드러나는 건 아니다. 아직 건드려본 적 없는 코드 경로를 정확히 겨냥해 만든 4단계 난이도 사다리가 그 천장을 찾아냈고, 마지막 단에서 같은 이벤트에 둘이 구독해본 적이 한 번도 없어서 보이지 않던 팬아웃 버그가 드러났다.

문서는 "끝났다"고 했다. 절반만 끝나 있었다.

쓰기 쪽 인터페이스에만 반영된 Repository 네이밍 수정, 그 갭을 영구적인 하네스 규칙으로 바꾼 네 라운드, 그리고 라운드당 3~4건이던 진짜 버그가 2건, 결국 0건으로 떨어진 수확 곡선 — 그 하락 자체가 가장 쓸모 있는 결과였다.

하네스는 두 번째 도메인을 만나본 적이 없었다

하네스 규칙 두 개가 몇 달째 깨끗했던 이유는 지금까지 입력된 도메인이 전부 Account와 Card뿐이었기 때문이다. 완전히 무관한 세 번째 도메인을 만들어보니 오탐 두 건이 드러났고, 진짜 실수를 잡을 규칙은 여전히 그걸 잡는다는 것도 확인됐다.

비밀번호 없이 로그인하기

보안 감사에서 /auth/sign-in이 userId만 받고 아무것도 검증하지 않는다는 사실이 드러났다 — 다섯 언어에서 같은 버그가 어떻게 다르게 나타났는지, 그리고 새로 만든 401 테스트가 드러낸 JDK 재시도 버그까지.

문서와 코드가 사이좋게 함께 틀렸을 때

다섯 언어에 걸친 위반 세 가지 — 쓰기용 Repository를 읽는 Query, JPA를 그대로 단 도메인 클래스, 잘못된 레이어의 notification 모듈. 진짜 버그는 하나뿐이었고, 나머지 둘은 그 많은 이전 감사가 왜 이걸 하나도 못 잡았는지를 드러낸다.

Bounded Context 간의 소통

동기 Adapter와 비동기 Integration Event 중 무엇을 선택할 것인가, 실제 보상 트랜잭션(compensating transaction) 사례와 함께.