포스트

2026-09-11-TIL: 잠금 전략보다 잠금을 쥐는 시간

최근 2주 가까이 쌓인 작업을 정리한다. 이 기간에 온라인 저지 code-drill과 결제·원장 시스템 parity-pay를 새로 열었고, 기존 저장소 중에서는 monticker가 가장 많이 움직였다. 새 저장소 둘 다 CLAUDE.md에 “작업은 커밋되어야 끝난다”는 규칙을 두고 Claude Code로 진행했다. 기록할 만한 판단이 남은 줄기 위주로 적는다.

요즘 한 작업

parity-pay: 장애가 나도 돈의 기록이 틀리지 않는 결제 시스템

README의 한 줄 소개가 “장애가 나도 돈의 기록이 틀리지 않는 결제·원장 시스템”이다. 골격은 처음부터 크게 잡았다. DB가 불변식을 강제하는 복식부기 원장, 멱등한 충전과 결제, Transactional Outbox, 결과를 모르는 충전을 외부 조회로 수렴시키는 복구, 정산과 대사까지다. jqwik로 무작위 연산 순서에서도 불변식이 깨지지 않는지 보는 속성 테스트도 붙였다.

그 뒤로는 대부분 측정과 수정의 반복이었다. 프로세스를 죽여서 장애 시나리오를 재현하고, 모의 은행과 모의 PG를 각자 프로세스와 DB로 분리했다. 프론트엔드에서는 refresh token을 스크립트가 닿지 않는 쿠키로 옮기고, access token은 메모리에만 두게 바꿨다.

monticker: MVP에서 상용화로, 그리고 장애 내성

범위를 MVP에서 상용화로 넓히면서 원칙을 하나 적었다. 실제 주문은 사용자가 연결한 증권 계좌로만 나가고, AI는 주문을 제안만 하며 리스크 검증과 사용자 확인 없이는 실행하지 않는다.

장애 내성 작업도 많았다. Redis가 죽으면 레이트리밋 필터가 예외를 그대로 던져 API 전체가 500이 되던 문제를 한 곳에서 정리했다. 레이트리밋과 캐시는 Redis가 없어도 통과시키고(fail-open), 실제 돈이 움직이는 멱등성 검사는 503으로 막는다(fail-closed). 이어서 Redis를 실제로 멈추는 장애 실험을 돌렸는데, 단위 테스트가 못 잡은 결함이 두 개 나왔다. 503 응답 본문의 한글이 깨졌고, 회원가입의 인증 토큰 저장이 이 보호 로직 밖에 있었다. 헬스 체크도 readiness에는 DB를 넣고 liveness는 프로세스 생존만 보게 나눴다.

code-drill과 그 밖의 저장소

code-drill은 Kotlin 멀티모듈로 시작해 제출, 채점, 실행 트레이스 리플레이까지 빠르게 이어졌다. 문제를 공개하기 전에 대표 오답을 돌려서 테스트가 그것을 잡아내는지 보는 검증 단계도 넣었다. neon-diary는 세이브·복원 경로의 결함을 집중적으로 고쳤고, sys-drill에는 팀 모델과 Google 로그인이 들어갔다.

잠금 전략이 아니라 잠금을 쥐는 시간

이번에 가장 많이 배운 건 parity-pay의 한 성능 수정이었다. 같은 지갑에 결제가 몰릴 때 처리량이 떨어지는 문제다.

앞선 부하 실험은 응답 시간만 보고 “잔액 행이 병목”이라고 추론했었다. 이번에는 PostgreSQL에서 직접 확인했다. pg_stat_statements를 켜고 대기 이벤트를 짧은 간격으로 샘플링하니, 같은 지갑 조건에서 대기의 대부분이 그 잔액 행의 잠금이었다. 커넥션 풀 대기는 거의 없었다.

새로 보인 건 트랜잭션 안의 문장 순서였다. 잔액 차감 UPDATE가 트랜잭션 중간에 있어서, 그 뒤의 원장 기록, 결제 INSERT, Outbox 기록, 커밋까지가 전부 행 잠금을 쥔 채로 일어났다. 잠금을 쥔 시간 중 DB가 실제로 일한 시간은 일부였고, 나머지는 애플리케이션을 기다린 시간이었다.

그래서 차감을 트랜잭션 맨 끝으로 옮겼다. 결과는 바뀌지 않는다. 잔액이 부족하면 차감이 실패하고 트랜잭션 전체가 롤백되므로, 앞에서 쓴 원장과 결제 기록도 함께 사라진다. 이 변경으로 잠금 보유 시간이 크게 줄었고, 같은 지갑 처리량은 1.7배 남짓 올랐다.

후속도 있었다. 순서를 바꿔도 Hibernate는 INSERT를 모아 두었다가 나중에 내보내기 때문에, 그 INSERT들이 차감 뒤, 즉 잠금 안으로 다시 들어갈 수 있었다. 차감 직전에 명시적으로 flush를 넣어 이걸 앞으로 뺐다. 대가는 코드에 적어 두었다. 원장과 결제 쓰기의 제약 위반이 커밋이 아니라 flush 시점에 터진다.

정리하면 이건 “어떤 잠금을 쓰는가”가 아니라 “잠금을 얼마나 오래 쥐는가”의 문제였다. 조건부 UPDATE든 비관적 잠금이든 똑같이 겪었을 문제라, 잠금 전략을 비교하는 실험만으로는 보이지 않았다.

느낀 점

이번에는 측정이 머신 상태에 휘둘린 기록이 여러 저장소에 남았다. parity-pay에서는 같은 코드끼리 비교했는데도 큰 차이가 나와서, 실험 도구가 두 빌드를 번갈아 돌리게 바꿨다. code-drill에서는 시간 초과로 걸려야 할 느린 오답이 한가한 머신에서는 통과했다. 판정을 가른 게 알고리즘이 아니라 머신 속도였다.

두 경우 모두 같은 값을 더 정밀하게 재는 대신, 머신 부하에 흔들리지 않는 값으로 측정 대상을 바꿨다. 처리량 대신 트랜잭션 안의 문장 순서를, 실행 시간 대신 연산 횟수의 자릿수를 봤다. 숫자가 깔끔하게 나와도 그 숫자가 무엇에 좌우되는지를 먼저 의심해야 한다.

monticker의 Redis 장애 실험도 비슷했다. 단위 테스트를 다 통과했는데 실제로 Redis를 멈추자 결함이 두 군데 나왔다. 실제로 죽여 보는 실험은 테스트와 다른 층을 본다.

Claude Fable 5.1과 캐시 읽기 가격

얼마 전 Anthropic이 Claude Fable 5.1과 Mythos 5.1을 냈다. 둘은 같은 모델이고 안전장치 수준만 다르다. Fable 5.1은 일반 공개이고, Mythos 5.1은 사이버보안과 생명과학 쪽 검증된 조직에만 열려 있다.

개발자 입장에서 눈에 띈 건 가격 구조였다. 기본 단가는 그대로인데 캐시 읽기 가격을 75% 내렸다. 에이전트는 매 턴마다 시스템 프롬프트, 도구 정의, 지금까지의 대화를 다시 보낸다. 이 앞부분이 캐시에 걸리면 캐시 읽기 가격으로 계산되니, 턴이 길게 이어지는 에이전트 작업일수록 효과가 크다. Anthropic은 일반 작업에서 약 25%, 에이전트 작업에서 최대 45%까지 싸진다고 주장한다.

Claude Code 사용자에게는 사이버보안 안전장치가 잘못 개입하는 경우가 세션당 평균 60% 정도 줄었다고 한다. 이것도 회사가 낸 수치다. Claude Code로 하루 종일 세션을 돌리는 입장에서는 성능보다 이런 가격과 개입 빈도 쪽이 체감에 더 가깝다.

참고:

다음에 확인할 것

  • parity-pay에서 취소 경로의 잠금 보유 시간도 같은 방법(문장 순서)으로 계속 추적할 수 있게 만들기
  • 같은 코드끼리 비교했는데 처리량이 움직인 원인
  • monticker에서 나눈 readiness와 liveness 설정이 실제 K8s 배포에서도 의도대로 동작하는지
  • 내 Claude Code 세션에서 캐시 읽기 비중이 실제로 얼마나 되는지
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.
고쳐 쓴 기록 1번 수정 ·
  1. docs(til): drop dates and internal codes and add ai news to the 2026-09-11 til

댓글

아직 댓글이 없습니다