--- kind: CASE slug: a05-f005-jparetrypolicy title: 애플리케이션이 넣은 재시도 정책이 한 번도 불리지 않고 작업만 두 번 돈다 topic: runtime-contract-correctness project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: case:a05-f005-jparetrypolicy evidenceCapturedOn: 2026-09-02 body: case-a05-f005-jparetrypolicy.body.md assets: - key: a05-f005-jparetrypolicy file: ../../../final/evidence/rendered/a05-f005-jparetrypolicy.svg - key: a05-f005-jparetrypolicy-probe file: ../../../final/evidence/rendered/a05-f005-jparetrypolicy-probe.svg evidence: - ../../../final/evidence/raw/a05-f005-jparetrypolicy.txt - ../../../final/evidence/raw/a05-f005-jparetrypolicy-probe.txt source: - 분석 문서는 persistence-jpa 편 §20 이다. 이것을 단순한 죽은 필드가 아니라 공개 조립 팩토리가 제공하는 기능이 동작하지 않는 계약 결함으로 규정하고 P2 로 둔다. §7.6 이 API 스코프에서 먼저 도달 불가 후보로 올리고 트랜잭션 스코프로 넘긴 흔적이 남아 있다. --- # 애플리케이션이 넣은 재시도 정책이 한 번도 불리지 않고 작업만 두 번 돈다 조립 팩토리가 애플리케이션이 제공한 재시도 정책을 받는 오버로드를 공개한다. 그 정책은 대체 정책 필드에 담기고, 그 필드를 읽는 분기는 호출자 프로파일의 재시도 프로파일이 널일 때만 열린다. 프로파일 타입이 그것을 널로 두지 못하게 한다. ## 관계 - **재시도 구현이 둘이고 정교한 쪽을 아무도 호출하지 않는다** 같은 코디네이터의 다른 공백이다. - **재시도 단위는 statement가 아니라 유스케이스 전체다** 이 정책이 참여하는 결정이다. - **@Bean이 있다는 것은 조립 증거가 아니다** 빈으로 등록된 확장점이라도 실제로 불리는지는 따로 세어 봐야 한다. ## 문제 트랜잭션 자동설정 팩토리는 오버로드를 둘 갖는다. 정책을 스스로 만드는 쪽과 밖에서 받는 쪽이다. ## 결론 정책 오버로드는 실제로 매번 불린다. 프로파일 오버로드가 하는 일이 그 호출 한 줄뿐이라서다. 그 자리를 채우는 호출자가 저장소 어디에도 없다. 넣으면 어떻게 되는지 실행으로 확인했다. 언제나 재시도 금지를 돌려주는 정책과 호출 카운터를 넣고, 두 번까지 허용하는 유효한 프로파일로 직렬화 실패를 던지는 작업을 돌렸다. 정책은 0 번, 작업은 2 번 불렸다. 정책은 말할 기회를 얻지 못했고 작업은 두 번 실행됐다. 그 정책이 담기는 필드의 javadoc 이 사연을 적는다. 예전 구현은 시도 횟수만 호출자에게서 받고 적격성과 백오프는 이 정책으로 덮었다. 재시도 하나를 프로파일 둘이 나눠 결정하게 되면서, 어느 쪽을 읽어도 실제 동작을 못 맞히게 됐다. 현재 형태가 그 수정의 결과다. 같은 javadoc 이 이 필드를 호출이 자기 재시도 프로파일을 주지 않을 때만 쓰인다고 적는다. 프로파일 타입의 컴팩트 생성자가 그 필드에 널 아님을 요구하므로, 문서에 남은 발동 조건이 코드에서는 성립하지 않는다. 앞쪽 오버로드의 javadoc 에는 두 값을 따로 두면 서로 어긋난 조합이 만들어지고 운영자 눈에는 그것이 설정 실수가 아니라 고장으로 보이기 때문에, 정책과 예산을 같은 프로파일에서 만든다고 적혀 있다. 그 문단 바로 아래에 정책만 따로 받는 오버로드가 놓여 있다. 플랫폼 자신도 같은 문에 걸린다. 런타임 자동설정이 시도 3회짜리 기본 프로파일로 정책을 만들어 넘기고, 그 정책은 대체 정책 필드에 담긴 뒤 읽히지 않는다. 예외도 경고도 없다. 코디네이터는 정책을 받았는지 널 검사만 하고, 그 정책이 쓰이지 않는다는 것은 어디서도 드러나지 않는다. ## 검증 환경 OpenJDK : 21.0.12 확인 방식 : 두 오버로드와 분기 열람, 애플리케이션 정책을 넣은 코디네이터 실행, 호출 계수 소스 수정 : x ## 재현 조건 1. 자동설정의 두 오버로드를 읽는다. 앞의 것이 뒤의 것을 부르는지 확인한다. 2. 뒤의 오버로드가 넘긴 정책이 담기는 필드와, 그 필드를 읽는 분기를 찾는다. 3. 프로파일 타입의 컴팩트 생성자가 그 분기의 조건을 허용하는지 확인한다. 4. 호출 카운터를 단 정책과 유효한 프로파일로 코디네이터를 세워 실패하는 작업을 실행한다. 5. 정책 호출과 작업 호출을 센다. ## 본문 트랜잭션 자동설정 팩토리는 코디네이터를 만드는 오버로드를 둘 갖는다. 하나는 재시도 프로파일을 받고, 다른 하나는 애플리케이션이 준 정책을 받는다. ## 그 정책은 대체 정책 필드로 간다 :::evidence key="a05-f005-jparetrypolicy" alt="자동설정의 두 조립 팩토리와 앞쪽의 설계 근거 javadoc, 뒤쪽 오버로드가 넘긴 정책이 담기는 필드와 그 필드의 javadoc, 그 필드를 읽는 유일한 분기, 프로파일 타입의 컴팩트 생성자, 그리고 조립 팩토리 호출 전수와 코디네이터를 직접 생성하는 곳이 넘기는 정책을 출력한 터미널 기록." caption="두 오버로드와 앞쪽의 설계 근거 · 정책이 담기는 대체 정책 필드와 그 사연 · 분기는 널일 때만 열림 · 프로파일 생성자가 널을 금지 · 밖에서 넣은 정책 0 — 55줄 · exit 0" zoom="true" ::: 앞쪽 오버로드의 javadoc 이 이유를 적는다. 정책과 예산은 같은 프로파일에서 만든다, 일부러 그렇게 한다. 따로 설정하면 재시도하라는 정책과 한 번만 허용하는 예산이 만나고, 그것은 오설정이 아니라 재시도가 고장 난 것처럼 보인다는 것이다. 바로 그 아래에 정책만 따로 받는 오버로드가 있다. 그 오버로드가 넘긴 정책은 `fallbackRetryPolicy` 필드에 담긴다. 그 필드를 읽는 자리는 하나다. ```java JpaRetryPolicy retryPolicy = profile.retryProfile() == null ? fallbackRetryPolicy : DefaultJpaRetryPolicy.forProfile(profile.retryProfile()); ``` 그리고 프로파일 타입의 컴팩트 생성자가 `retryProfile` 에 널 아님을 요구한다. 왼쪽 가지가 열리는 조건을 타입이 금지한다. ## 그 필드는 잊힌 것이 아니라 강등된 것이다 필드 javadoc 이 사연을 적는다. 코디네이터는 예전에 이 정책의 적격성과 백오프를 모든 호출에 적용하면서 시도 횟수만 호출자 프로파일에서 가져왔다. 프로파일 둘이 재시도 하나를 나눠 결정했고, 어느 쪽을 읽든 실제 동작을 잘못 예측하게 됐다. 지금 형태는 그 수정의 결과다. 같은 javadoc 이 이 필드를 호출이 자기 재시도 프로파일을 주지 않을 때만 쓰인다고 적는데, 그 조건이 곧 타입이 금지하는 것이다. 발동 조건이 문서에 남고 코드에서 사라졌다. ## 그 자리에 애플리케이션 정책이 들어온 적이 없다 정책 오버로드 자체는 매번 불린다. 프로파일 오버로드의 몸통이 그것을 부르는 한 줄이다. 밖에서 자기 정책을 넣는 호출은 프로덕션에도 테스트에도 없다. 유일한 프로덕션 진입은 프로파일 쪽이고, 테스트는 코디네이터를 직접 세우면서도 프로파일에서 만든 기본 정책만 넘긴다. ## 넣으면 어떻게 되는지 실행으로 확인했다 :::evidence key="a05-f005-jparetrypolicy-probe" alt="애플리케이션이 준 정책과 호출 카운터, 두 번까지 허용하는 유효한 프로파일을 코디네이터에 넣고 직렬화 실패를 던지는 작업을 실행한 결과와, 플랫폼 런타임 자동설정이 자기 빈에 넣는 기본 프로파일을 출력한 터미널 기록." caption="언제나 재시도 금지를 돌려주는 정책과 카운터 투입 · 정책 호출 0 · 작업 호출 2 · 플랫폼 기본 프로파일도 같은 자리로 — 30줄 · exit 0" zoom="true" ::: 언제나 재시도 금지를 돌려주는 정책에 호출 카운터를 달았다. 프로파일은 두 번까지 허용하는 유효한 것이고, 작업은 직렬화 실패를 던진다. 정책 호출 0, 작업 호출 2 다. 정책은 재시도하지 말라고 말할 기회를 얻지 못했고, 작업은 호출자 프로파일에 따라 두 번 실행됐다. 예외도 경고도 없었다. 코디네이터가 하는 검증은 정책을 받았다는 널 아님 확인뿐이고, 그것이 쓰이지 않는다는 신호는 아니다. ## 플랫폼 자신도 같은 문에 걸린다 런타임 자동설정이 `jpa-platform-default` 라는 이름과 시도 3회짜리 프로파일로 정책을 만들어 넘긴다. 그 정책도 대체 정책 필드에 담긴 뒤 읽히지 않는다. 실행마다 쓰이는 것은 호출자가 준 프로파일에서 그 자리에서 만든 정책이다. 도달하지 못하는 것은 공개된 확장점만이 아니라 플랫폼이 자기 빈에 넣은 기본값이기도 하다. ## 고칠 방향은 셋이다 생성자가 받은 정책을 권위로 삼고 예산만 프로파일에서 계산하거나, 오버로드를 없애고 재시도 프로파일이 유일한 출처임을 API 로 못박거나, 정책과 프로파일을 하나의 객체로 합치는 것이다. 분석 문서는 지금 형태가 가장 나쁘다고 적는다. 설정 출처를 둘 받아 놓고 한쪽을 조용히 버리기 때문이다. ## 확인하지 못한 것 실제 데이터베이스를 붙인 통합 실행에서 같은 결과가 나오는지는 확인하지 않았다. 탐침은 트랜잭션 관리자를 대역으로 세운 단위 실행이다.