2026-08-09-TIL: rollback-only 버그로 다시 본 트랜잭션 전파, 그리고 Muse Code
최근 2주는 spring-internals-lab에서 커밋이 가장 많이 나왔다. 트랜잭션 전파부터 DispatcherServlet, Spring Boot 자동 구성까지 로드맵의 남은 부분을 몰아서 진행했다. 그 사이 monticker 정산 도메인을 설계했고, Dudecide라는 새 프로젝트를 문서부터 시작했다.
요즘 한 작업
spring-internals-lab과 spring-lite
트랜잭션 파트에서는 실제 Spring의 전파 속성을 실험하고, 축소판 트랜잭션 매니저에 REQUIRED/REQUIRES_NEW와 rollback-only 전파를 직접 구현했다. 이어서 DispatcherServlet 흐름을 추적하는 MVC 실습과, SpringApplication 생명주기와 Condition 평가 시점을 보는 Boot 실습을 진행했다. 마지막에는 core/autoconfigure/starter 세 모듈짜리 작은 starter를 직접 조립했다. 흩어져 있던 버전 문자열은 Gradle version catalog로 모았다.
학습용 시각화 대시보드도 새로 얹기 시작했다. 기존 JDI 브레이크포인트 엔진은 그대로 두고, 출력만 콘솔 텍스트에서 NDJSON 스트림으로 바꿔 브라우저로 보내는 구조다. 이벤트 멀티캐스트나 Boot 조건 평가 같은 시나리오가 이제 화면에서 라이브로 보인다. spring-lite 쪽에서도 visual lab 프론트엔드를 별도 앱으로 분리하고 책 원고에 단계별 그림을 넣었다.
monticker와 Dudecide
monticker에는 Google, Kakao, Naver 소셜 로그인을 붙이고, 정산 시스템을 설계했다. 페이퍼트레이딩 정산, 제작자 수익 분배, 구독료 정산, 증권사 정산 mock 네 도메인이 서로의 Repository를 직접 부르지 않고 도메인 이벤트로만 통신하게 정했다.
Dudecide는 모임에서 나온 결정 사항을 근거와 함께 관리하고, 날짜와 장소는 투표로 정하는 서비스다. 코드보다 에이전트용 AGENTS.md와 결정 기록 디렉터리를 먼저 깔고, “긴 맥락은 채팅이 아니라 문서에 남긴다”는 원칙을 README에 적었다. 기술 스택은 처음에 TypeScript와 NestJS로 정했다가, 관계형 비즈니스 로직을 오래 다룰 기반이 필요하다는 이유로 Kotlin + Spring Boot 모듈러 모놀리스로 바꿨다. 결정의 상태 머신과 투표 점수 규칙을 문서로 먼저 고정하고, 스키마와 도메인 규칙을 거쳐 투표 흐름까지 구현했다.
참여 트랜잭션의 실패와 commit 위치
REQUIRED로 참여한 안쪽 메서드가 실패하면, 참여자는 커넥션을 직접 롤백하지 않는다. 트랜잭션의 주인이 아니기 때문이다. 대신 공유 홀더에 rollback-only 표시만 남긴다. 바깥 메서드가 예외를 잡아 삼키고 정상 종료해도, 주인이 commit()할 때 그 표시를 보고 롤백한 뒤 UnexpectedRollbackException을 던진다.
처음 만든 축소 구현은 참여자의 실패를 주인에게 알리지 않았다. 테스트가 통과했던 건 바깥이 예외를 그대로 던지는 경우만 봤기 때문이다. 그래서 rollback-only 전파를 넣었더니 이번에는 인터셉터가 터졌다.
1
2
3
4
5
6
7
8
try {
Object result = invocation.proceed();
txManager.commit(status); // 여기서 MiniUnexpectedRollbackException
return result;
} catch (Throwable ex) {
txManager.rollback(status); // 이미 닫힌 커넥션에 다시 rollback
throw ex;
}
commit()이 예외를 던지자 같은 catch가 그걸 실행 실패로 받아, 이미 정리된 트랜잭션에 rollback()을 다시 불렀다. Spring의 TransactionAspectSupport.invokeWithinTransaction()을 다시 보니 커밋 호출은 proceed()를 감싼 try/catch 바깥에 있었다. 실행 실패와 커밋 실패를 다른 경로로 다루려는 구조다. 같은 모양으로 commit()을 try 밖으로 옮겨서 해결했다.
NESTED와 REQUIRES_NEW의 차이도 확인했다. 둘 다 안쪽 실패가 바깥으로 번지지 않지만, REQUIRES_NEW는 기존 커넥션을 떼어 두고 새 커넥션을 쓰고, NESTED는 같은 커넥션 위에 savepoint를 만들 뿐이다.
느낀 점
얕은 축소 구현은 겉으로는 테스트를 통과한다. 정책을 하나씩 실제에 가깝게 추가할 때 숨어 있던 가정이 드러난다. Spring 소스에서 commit이 왜 그 위치에 있는지는 직접 틀리게 만들어 보고 나서야 이해가 됐다.
Dudecide를 문서부터 시작한 것도 비슷한 이유였다. 에이전트와 같이 작업하면 결정 근거가 대화에 묻히기 쉽다. 스택을 바꾼 이유도 문서에 남아 있어서 나중에 다시 볼 수 있다.
Muse Code
이번 주에 Meta가 Muse Code라는 터미널 코딩 에이전트를 베타로 공개했다. Claude Code나 Codex와 같은 자리를 노리는 도구로, Meta가 함께 공개한 코딩용 모델 Muse Spark 1.2 위에서 돈다.
눈에 띈 건 두 가지다. 하나는 큰 작업을 여러 하위 에이전트로 나눌 때 각자 별도 git worktree에서 작업하게 해서, 내 작업 디렉터리를 건드리지 않고 서로 파일이 충돌하지 않게 한다는 점이다. 다른 하나는 하위 에이전트 생성, 툴 호출, 판단을 전부 재생 가능한 이벤트 로그로 남긴다는 점이다. 에이전트가 무엇을 했는지 나중에 따라가 볼 수 있고, 세션이 죽어도 이어서 복구할 수 있다는 설명이다.
Meta는 비용 면에서 좋은 선택지라고 내세운다. 모델은 100만 토큰 컨텍스트를 지원하고 여러 코딩 벤치마크에서 성능이 좋다고 하지만, 벤치마크 수치는 회사 발표다. 정리하면 “병렬 하위 에이전트 + worktree 격리 + 감사 가능한 로그”를 기본값으로 묶은 코딩 에이전트 정도로 이해했다. Dudecide에서 결정 근거를 문서로 남기려는 이유와 같은 방향의 고민이다.
참고:
- Meet Muse Spark 1.2 and Muse Code - Meta for Developers
- Meta launches Muse Code, an AI agent for large code bases - TechCrunch
다음에 확인할 것
- 축소 트랜잭션 매니저에서 생략한
NESTED를 JDBC savepoint로 직접 구현해 보기 - 지금까지 만든 축소 구현과 실험을 하나의 샘플 애플리케이션으로 묶어 보기
- Dudecide 백엔드 API를 마무리하고 Next.js 프론트엔드 붙이기
- 학습 대시보드에서 트랜잭션 suspend/resume을 어떻게 보여줄지
댓글
아직 댓글이 없습니다