--- kind: PROJECT_DECISION slug: retry-unit-is-the-use-case title: 재시도 단위는 statement가 아니라 유스케이스 전체다 topic: commit-ambiguity-as-a-result project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: decision:retry-unit-is-the-use-case decisionStatus: ADOPTED decidedOn: 2026-08-11 source: - docs/adr/ADR-JPA-002-full-transaction-retry.md — Status Accepted, Date 2026-08-11, Design §19.2 - src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java - src/application-core/src/main/java/dev/caskeleton/application/transaction/IrreversibleSideEffectContext.java - analysis/05-adapter-outbound-persistence-jpa.md --- # 재시도 단위는 statement가 아니라 유스케이스 전체다 ## 결정문 재시도는 실패한 statement 나 트랜잭션이 아니라 유스케이스 전체를 새 트랜잭션과 새 Persistence Context 에서 다시 실행한다. ## 판단 이유 statement 수준 재시도는 지금 재시도하려는 실패들에 대해 정확히 틀린 선택이다. 낙관적 충돌은 시도가 계산의 기준으로 삼은 상태가 더 이상 커밋된 상태가 아니라는 뜻이므로, 같은 statement 를 다시 쏘면 이미 움직인 버전에 대해 같은 틀린 답을 계산한다. 도메인 규칙이 다시 읽은 데이터 위에서 다시 돌아야 하고, 그것은 곧 유스케이스 전체다. Persistence Context 재사용도 같은 이유로 틀리다. 두 번째 시도가 1차 캐시에서 첫 시도의 낡은 엔티티를 읽게 된다. advice 순서도 결정의 일부다. 재시도 advice 가 Spring 의 트랜잭션 advice 바깥에 놓여야 각 시도가 새 트랜잭션을 시작한다. 순서가 뒤집히면 재시도 루프가 이미 rollback-only 로 표시된 하나의 트랜잭션 안에서 돌고, 두 번째 시도는 아무것도 실행하지 못한 채 즉시 실패한다. 예산은 시도 횟수와 경과 시간 두 상한을 함께 갖는다. 코디네이터가 매 시도마다 경과를 계산해 두 상한을 함께 확인한다. 두 가지 실패는 예산과 무관하게 재시도하지 않는다. 완료를 알 수 없는 실패는 조정으로 가고, 정책이 실패로 분류한 것은 그대로 실패다. ## 영향 감수하는 것 재시도 가능한 유스케이스는 처음부터 다시 실행해도 안전해야 한다. 커밋 전에 되돌릴 수 없는 외부 효과가 있으면 안 된다. 그 조건이 성립하지 않는 유스케이스는 스스로 선언해야 한다. IrreversibleSideEffectContext 가 그 선언을 받고, 정책은 남은 예산과 무관하게 재시도를 거부한다. 재시도 한 번의 비용이 statement 재시도보다 크다. 유스케이스 전체가 다시 돈다. 얻는 것 낙관적 충돌과 직렬화 실패가 실제로 해소된다. 다시 읽은 데이터 위에서 도메인 규칙이 다시 판단하기 때문이다. 두 프로파일이 하나의 재시도를 나눠 결정하는 상황이 없다. 호출자가 넘긴 프로파일 하나가 적격성과 백오프와 예산을 모두 정한다. 2026-09-01 재검증 재검증 결과다. 결정은 채택되어 있고 코디네이터는 조건부 빈으로 생성된다. 그러나 그 빈을 주입받아 호출하는 프로덕션 코드가 없다. ADR 의 Enforcement 절은 두 가지를 지목한다. 첫 번째인 FullTransactionRetryCoordinatorTest 는 실재하며 코디네이터 자체를 검증한다. 두 번째인 RetryableJpaTransactionInterceptor 는 이 저장소에 존재하지 않고 구현 계획과 코드 리뷰와 이 ADR 세 문서에만 이름으로 남아 있다. app-bootstrap 과 persistence-jpa 의 main 에는 Advisor 도 MethodInterceptor 도 Pointcut 도 Aspect 도 없다. 따라서 현재 상태는 이렇다. 코디네이터의 동작은 테스트로 고정되어 있고, 그것이 어떤 유스케이스에 적용되는지는 고정되어 있지 않다. 이 절의 근거는 EVD-336 이다. ## 근거 - **번역되지 않은 경합 예외가 재시도 코디네이터의 catch를 통째로 비껴갔다** 이 재시도 경로가 실제로는 돌지 않았던 사례다. - **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다** 예산과 무관하게 재시도하지 않는 두 실패 중 하나가 이 규칙에서 나온다.