포스트

2026-09-18-TIL: 아웃박스는 한 번 이상 배달한다

이번 주는 사이드 프로젝트 여러 곳에서 실험을 돌려 결함을 찾고 고친 기록이 가장 많았다. 새 프로젝트인 proofu도 시작했다. Claude Code로 진행한 저장소는 커밋마다 Co-Authored-By 트레일러가 남아 있어서, 어느 세션에서 무엇을 했는지 로그만으로 거의 복원된다.

이번 주에 한 작업

monticker와 parity-pay: 실험이 찾은 결함들

주식 관찰 플랫폼인 monticker에서는 장애 실험과 정리가 섞였다. Kafka 브로커를 멈추는 실험, 실시간 파이프라인의 파티션과 컨슈머 순서 실험, 주문 폭주 상황에서 돈의 불변식을 검사하는 실험을 차례로 붙였다. 정리 쪽으로는 트래픽을 받은 적 없는 서비스 두 개를 걷어 내고, 페이퍼 계좌의 체결 경로를 매칭 엔진 하나로 합쳤다. 주요 결정은 아키텍처 결정 기록으로 남겼다.

결제·원장 시스템인 parity-pay에서도 실험이 이어졌다. Kafka 컨슈머가 처리하지 못한 레코드를 재시도 끝에 로그 한 줄만 남기고 건너뛰고 있어서, dead letter 테이블과 토픽으로 보내도록 고쳤다. 느린 외부 기관 하나가 서버 전체를 끌어내리던 문제는 bulkhead와 circuit breaker로 격리했다.

code-drill: 100문제와 프로젝트형 채점

알고리즘 훈련 플랫폼인 code-drill은 100문제를 채웠고, 커뮤니티와 대회 기능을 붙였다. 사용자 코드 컴파일도 샌드박스 안으로 옮겼다. 컴파일러가 사용자 코드를 처음 읽는 프로그램이니, 실행만 격리하면 반쪽이라는 이유였다. 여러 파일로 된 프로젝트를 채점하는 프로젝트형 문제도 열었다.

neon-diary와 proofu

Unity로 진행하는 neon-diary는 Codex 중심으로 작업하는 저장소다. 원본 플레이 영상을 기준으로 오프닝 장면을 연결하면서, 근거가 확인된 상태와 구현된 상태, 런타임에서 검증된 상태를 따로 기록하기로 했다.

proofu는 경력 사실과 증빙을 쌓아 두고, 채용공고 요구사항에 맞춰 근거를 추적할 수 있는 이력서를 만드는 도구다. 첫날에 API와 화면의 뼈대를 세웠다. 매칭 점수는 결정적 코드로 계산하고, 모델은 이유 문장만 쓰게 했다. 공고에서 뽑은 요구사항의 원문 인용도 서버에서 다시 찾아 위치를 확정한다. 처음에는 API의 인용 기능을 쓰려 했지만 구조화 출력과 함께 쓸 수 없어서 서버 측 검증으로 바꿨다.

배운 점: 아웃박스는 한 번 이상 배달한다

이번 주에 같은 교훈이 두 번 나왔다. 둘 다 Spring Modulith 아웃박스를 둘러싼 문제였다.

첫째는 브로커 장애 실험에서 나왔다. 브로커를 멈춰 보기도 전에, @Externalized 이벤트가 처음부터 전부 직렬화 오류로 실패하고 있었다. Modulith는 byte[] JSON을 넘기는데 프로듀서는 StringSerializer로 설정되어 있었다. 아웃박스 테이블에는 이벤트가 꼬박꼬박 쌓였지만 브로커로는 하나도 나가지 않았다.

둘째는 주문 폭주 실험에서 나왔다. 커넥션 풀이 고갈된 상황에서 정합성 검사가 일부 계좌의 원장 드리프트를 잡았다. 흐름은 이랬다.

  1. 체결의 현금 변경은 주문 트랜잭션에서 동기로 커밋된다.
  2. 원장 기록은 @ApplicationModuleListener가 별도 트랜잭션에서 비동기로 쓴다.
  3. 풀 고갈로 리스너의 완료 표시가 실패하면, 이벤트가 미완료로 남는다.
  4. 재전송 설정이 몇 분 뒤 같은 이벤트를 다시 배달한다.
  5. 원장 기록 메서드가 멱등하지 않아서 같은 체결의 원장 행이 한 번 더 들어간다.

현금은 맞고 원장만 많았으니 돈이 사라진 건 아니었지만, 대사 기준인 원장이 부하 중에 어긋난 것이다. 수정은 두 겹으로 했다. 같은 체결과 이벤트 종류의 행이 이미 있으면 아무것도 하지 않게 했고, 동시에 들어오는 경우를 막으려고 부분 유니크 인덱스를 백스톱으로 걸었다. 검사 스크립트도 아웃박스가 다 비워질 때까지 기다린 뒤 검사하게 바꿨다.

정리하면 아웃박스가 보장하는 것은 “최소 한 번”이다. 기록한다고 발행되는 것도 아니고, 한 번만 처리된다는 보장도 없다. 발행은 실제 브로커 오프셋으로 확인하고, 처리는 받는 쪽이 멱등해야 한다. parity-pay에서 dead letter를 다시 재생해도 안전했던 것도 컨슈머가 멱등했기 때문이다.

같이 남길 것: 판정이 JIT 경주에 걸리면

code-drill에서는 같은 n² 풀이가 같은 머신에서 1초에 끝나기도 하고 4초에 끝나기도 했다. 원인은 앞 테스트 케이스가 데운 메서드의 C2 컴파일이 다음 케이스 시작 전에 끝나느냐였다. 백그라운드 컴파일 스레드와 경주하고 있었던 것이다. Kotlin, Java 채점을 동기 JIT(-Xbatch)로 바꾸고 기존 측정 결과를 다시 쟀다.

OpenAI Agents API

얼마 전 OpenAI가 Agents API를 공개 베타로 열었다는 소식이 피드에 자주 보였다. 한 줄로 줄이면 Codex와 ChatGPT의 업무용 에이전트가 쓰는 하네스를 API로 빌려 쓰게 한 것이다.

여기서 하네스는 모델 바깥에서 에이전트 루프를 돌리는 부분을 말한다. 세션 상태를 유지하고, 컨텍스트가 차면 앞부분을 압축하고, 도구를 찾아 호출하고, 하위 에이전트에 일을 나누고, 중간에 죽으면 복구하는 일이다. 지금까지는 이걸 각자 직접 만들어야 했다. Agents API는 이를 에이전트, 실행 환경, 세션, 이벤트라는 네 가지 개념으로 묶는다. 실행 환경은 OpenAI가 호스팅하는 샌드박스, 자체 인프라, 파트너 샌드박스 중에서 고를 수 있고, MCP 서버도 도구로 붙는다.

OpenAI는 별도 요금 없이 토큰과 도구 사용량만 받는다고 설명한다. 실제로 장시간 세션에서 압축이 얼마나 쓸 만한지는 써 봐야 알 일이다. 일단은 “에이전트 루프를 직접 짜지 않고 관리형으로 맡기는 선택지가 생겼다” 정도로 이해해 두었다.

참고:

다음에 확인할 것

  • monticker의 나머지 원장 기록 경로도 같은 방식으로 멱등하게 만들기
  • 주문 경로의 커넥션 풀 고갈 대응
  • proofu 문서 생성과 제출 스냅샷
  • code-drill 프로젝트 문제마다 있는 대표 오답을 채점 자체에 더 쓸 수 있는지
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.
고쳐 쓴 기록 1번 수정 ·
  1. docs(til): drop dates and internal codes and add ai news to the 2026-09-18 til

댓글

아직 댓글이 없습니다