Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-a05-f007-transactionprofileregistry.md
T
DongHyeonkaandClaude Fable 5.1 b25357c48a docs(clean-architecture-backend-template): fold analysis into final and re-select one topic
- analysis/·source-index·state.json 을 final/document.md 제2부·제3부로 접었다. SSOT 는 하나다
- 파일럿 — commit-ambiguity-as-a-result 를 새 기준으로 재선별. 후보 14 → 글감 5
  (PROMOTE 5 · MERGE_INTO 3 · KEEP_IN_SSOT 4 · 보류 2). 기록 5건을 다시 썼고 그림 1개를
  techviz 로 만들었다
- 재선별이 잡은 것: 제1부 §6.2·§11.1 이 자기 §13.2 와 어긋나 있었다(레인을 안 돌렸다 vs
  돌렸다) — 정정. 이미 답이 나와 있던 Question 을 HEAD 재실행 질문으로 다시 세웠다.
  Concept 이 인용한 코드가 SSOT 에 없어 뺐다
- candidateScope·sourceRepository 기록. 나머지 43개 주제는 재선별 대기(PENDING 905)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:39:20 +09:00

164 lines
13 KiB
Markdown

---
kind: CASE
slug: a05-f007-transactionprofileregistry
title: 오타를 막는 근거만 남기고, 오타가 들어올 자리를 지웠다
topic: runtime-reachability-and-composition
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:a05-f007-transactionprofileregistry
evidenceCapturedOn: 2026-09-02
assets:
- key: a05-f007-transactionprofileregistry-adr
file: ../../../final/evidence/rendered/a05-f007-transactionprofileregistry-adr.svg
- key: a05-f007-transactionprofileregistry
file: ../../../final/evidence/rendered/a05-f007-transactionprofileregistry.svg
evidence:
- ../../../final/evidence/raw/a05-f007-transactionprofileregistry-adr.txt
- ../../../final/evidence/raw/a05-f007-transactionprofileregistry.txt
source:
- 원본 분석 절은 `final/document.md#a05` §25 다. 프로덕션 참조 0, 전용 단위 테스트만 존재, 비공개 API 패키지, 루트 빈 배선 없음이 그 절의 판정이고 등급은 P3 다. ADR 대조와 형제 레지스트리 비교는 이 기록에서 새로 확인했다. 바로 앞 §24 는 같은 리프에서 트랜잭션 스택이 둘로 갈린 문제를 다룬다.
---
# 오타를 막는 근거만 남기고, 오타가 들어올 자리를 지웠다
프로파일을 이름으로 찾고 등록되지 않은 이름을 거절하는 레지스트리가 남았다. 이름을 건네던 유일한 호출부는 한 커밋에서 지워졌고, 같은 커밋이 그 레지스트리의 javadoc 에서 지워진 애너테이션 이름만 빼고 오타 예시와 논거는 남겼다.
## 관계
- **재시도 구현이 둘이고 정교한 쪽을 아무도 호출하지 않는다**
같은 스택의 다른 마디가 같은 형태로 남은 사례다.
- **legacy compatibility surface의 제거 조건을 세 가지로 고정한다**
이 후보를 그 세 조건에 대 봤다.
- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다**
같은 개념을 가리키는 어휘가 둘 남았을 때 어느 쪽이 죽었는지 표시하라는 규칙이다.
## 문제
이 레지스트리는 등록되지 않은 이름에 기본값을 주지 않고 던진다. javadoc 이 그 이유로 프로파일 이름 오타 하나가 남의 격리 수준과 타임아웃과 재시도 예산을 물려받는 상황을 든다.
그런 방어가 값을 하려면 이름을 건네는 쪽이 있어야 한다.
## 결론
이름을 건네던 곳은 하나였다. 삭제된 인터셉터의 66행이 애너테이션에 적힌 프로파일 이름을 레지스트리에 넘기고, 69행이 그렇게 얻은 프로파일로 코디네이터를 불렀다.
애너테이션 44줄, 인터셉터 109줄, 그 테스트 211줄이 한 커밋에서 함께 지워졌다. 파일 909개를 옮긴 기능 커밋 하나에 이 삭제가 섞여 들어갔다.
같은 커밋이 레지스트리 javadoc 도 한 줄 고쳤다. 오타 예시에서 애너테이션 표기를 빼고 이름만 남겼다. 방어의 근거는 손질해서 남기고, 방어할 대상은 같은 커밋으로 지운 것이다.
지금 세면 자기 파일을 뺀 프로덕션 참조 0, 자동설정 등록 0, 설정 키 0 이다. 패키지는 스캔 범위에 들어가는데 이 클래스에는 스테레오타입 애너테이션이 없다. 남은 참조는 전용 단위 테스트 한 파일의 셋이다.
레지스트리가 담는 타입도 사정이 다르지 않다. TransactionProfile 은 네 파일에 나타나고 공개 포트가 그것을 두 번째 인자로 받는다. 그런데 그 포트를 참조하는 main 파일은 포트 자신과 구현체뿐이고, 자기 파일 밖에서 프로파일을 만드는 main 코드도 없다. 참조가 네 파일 안에서만 돌고 바깥에서 들어오는 길이 없다.
프로덕션 트랜잭션은 다른 타입으로 열린다. 같은 패키지의 SpringTransactionPort 는 애플리케이션 계층 요청 타입을 받는데, 그 파일에서 TransactionProfile 을 부르는 곳이 없다.
그래서 이름 조회가 객체 전달로 대체된 것으로 읽으면 틀린다. 이름을 받던 입력이 사라졌고, 객체를 받는 쪽에도 프로덕션 호출자가 붙지 않았다. 레지스트리는 그 스택에서 가장 먼저 잘려 나간 마디다.
제거 조건 셋 중 둘은 확인된다. 외부 프로덕션 참조가 0 이고 설정 경로가 없다. 세 번째인 대체 경로의 특성화는 확인되지 않는다. 대체 경로가 같은 동작을 다르게 하는 것이 아니라, 이름 조회를 수행하는 프로덕션 경로가 없다.
이런 모양 자체가 버려진 것은 아니다. 문자열 이름을 받아 등록되지 않았으면 던지는 레지스트리를 정렬 필드 매퍼가 지금도 호출한다. 형제로 셀 수 있는 것은 결국 하나다. gRPC 채널 레지스트리는 검증된 값 객체를 키로 쓰고, Mongo 일관성 레지스트리는 enum 을 키로 써서 조회가 실패할 수 없다고 javadoc 이 적는다.
ADR 은 아직 지워진 것을 전제한다. 강제 절이 삭제된 클래스의 상수를 근거로 지목하고, 결정 절은 재시도 어드바이스가 스프링 트랜잭션 어드바이스 바깥에 정렬된다고 적는다. 그 상수 이름은 소스 전체에 없고, JPA 리프 main 에 어드바이저도 없다.
## 검증 환경
OpenJDK : 21.0.12
확인 방식 : 단어 경계 참조 계수, 삭제 커밋의 부모와 diff 열람, 형제 레지스트리의 키 타입 대조, ADR 대조
소스 수정 : x
## 재현 조건
1. 레지스트리의 패키지와 이 리프의 공개 API 패키지를 비교한다.
2. 자기 파일을 뺀 프로덕션 참조와 자동설정 등록과 설정 키를 센다. 스캔 대상 패키지인지, 스테레오타입이 있는지도 본다.
3. 레지스트리가 담는 타입의 참조를 세고, 그 타입을 만드는 코드와 포트를 부르는 코드를 따로 센다.
4. 프로덕션 트랜잭션이 실제로 지나는 포트를 찾아 그 타입을 확인한다.
5. 삭제 커밋의 부모에서 인터셉터를 열고, 같은 커밋의 레지스트리 diff 를 본다.
6. 형제 레지스트리들의 require 키 타입을 읽는다.
7. ADR 의 결정 절과 강제 절이 지목한 것을 소스에서 찾는다.
## 본문
<!-- body:start -->
`TransactionProfileRegistry` 는 이름을 받아 트랜잭션 프로파일을 돌려주고, 등록되지 않은 이름이면 기본값으로 넘어가는 대신 예외를 던진다.
클래스의 javadoc 이 그 완고함의 근거를 적는다. 기본값이 있으면 `"order-wrtie"` 같은 오타가 남의 격리 수준과 타임아웃과 재시도 예산으로 조용히 실행된다는 것이다.
그런 방어는 이름을 건네는 쪽이 있을 때만 값을 한다.
## 이름을 건네던 코드는 한 번 있었고, 지워졌다
:::evidence key="a05-f007-transactionprofileregistry-adr" alt="삭제 커밋의 규모와 지워진 세 파일, 그 커밋의 부모에서 열어 본 인터셉터의 해당 줄, 같은 커밋이 레지스트리 javadoc 에 낸 diff, 문자열 키를 쓰는 형제 레지스트리와 다른 키 타입을 쓰는 둘, 그리고 ADR 의 결정 절과 강제 절 원문을 차례로 출력한 터미널 기록." caption="삭제 규모와 지워진 세 파일 · 삭제 직전의 호출부 두 줄 · 같은 커밋의 javadoc diff · 문자열 키 형제는 하나 · ADR 두 절이 지목한 것 — 43줄 · exit 0" zoom="true"
:::
한 커밋이 애너테이션 44줄과 인터셉터 109줄과 그 테스트 211줄을 지웠다. 909개 파일을 손댄 큰 기능 커밋이고, 이 삭제는 그 안에 있다.
지운 파일을 그 커밋의 부모에서 열면 레지스트리를 어떻게 썼는지 보인다.
```java
41: private final TransactionProfileRegistry profiles;
66: TransactionProfile profile = profiles.require(policy.profile());
69: return coordinator.execute(operation, profile, () -> proceed(invocation), transactionKey);
```
`policy.profile()` 은 애너테이션에 적힌 문자열이다. 오타가 들어올 수 있는 자리가 거기였다.
## 같은 커밋이 근거만 손질해서 남겼다
```text
- * default would mean a typo in {@code @RetryableJpaTransaction(profile = "order-wrtie")} silently
+ * default would mean a typo in a profile name of {@code "order-wrtie"} silently runs with someone
```
지워지는 애너테이션의 이름만 문장에서 빼고, 오타 예시와 논거는 그대로 뒀다. 방어할 대상을 지우는 커밋이 방어의 근거는 문법만 고쳐 남긴 것이다.
## 이름이 사라진 자리를 객체가 넘겨받지도 않았다
:::evidence key="a05-f007-transactionprofileregistry" alt="레지스트리의 패키지 위치와 공개 API 패키지 파일 수, 자기 파일을 제외한 프로덕션 참조와 자동설정 등록과 설정 키의 계수, 컴포넌트 스캔 대상 여부와 스테레오타입 유무, 레지스트리가 담는 타입의 참조 계수와 그 타입을 만드는 코드 수, 포트를 참조하는 파일, 그리고 프로덕션 트랜잭션이 실제로 지나는 포트와 그 타입을 출력한 터미널 기록." caption="구현 패키지 · 프로덕션 참조 0 과 설정 키 0 · 스캔 대상이나 스테레오타입 없음 · 담긴 타입은 네 파일에 있으나 만드는 코드 0 · 실제 경로는 다른 포트 — 37줄 · exit 0" zoom="true"
:::
레지스트리 쪽 수는 전부 0 이다. 자기 파일을 뺀 프로덕션 참조 0, 자동설정 등록 0, 출하 설정의 프로파일 키 0. 리프의 공개 API 패키지에는 파일이 마흔아홉 개 있지만 이것은 그중에 없다. 패키지 자체는 컴포넌트 스캔 대상인데, 이 클래스에 스테레오타입 애너테이션이 없어 후보가 되지 않는다.
레지스트리가 담는 타입 쪽은 수가 다르다. `TransactionProfile` 은 네 파일에서 열한 줄에 나타나고, import 와 javadoc 을 빼면 일곱 줄이 타입 사용이다. 코디네이터, 정의 매퍼, 실행기 구현, 그리고 공개 포트다. 포트는 이름 대신 객체를 받는다.
```java
<T> T execute(PersistenceOperationName operation, TransactionProfile profile, Supplier<T> work);
```
두 번째 인자가 프로파일 객체다. 다만 이 포트를 참조하는 main 파일은 포트 자신과 그 구현체뿐이고, 자기 파일 밖에서 `TransactionProfile` 을 만드는 main 코드도 없다. 네 파일은 서로를 참조할 뿐이고 바깥에서 들어오는 진입점이 없다.
프로덕션 트랜잭션은 같은 패키지의 다른 클래스가 연다. `SpringTransactionPort` 가 애플리케이션 계층의 요청 타입을 받고, 그 파일 안의 `TransactionProfile` 참조는 0 이다.
그러니 이름 조회가 객체 전달로 대체됐다고는 말할 수 없다. 이름을 받던 입력이 사라진 뒤 객체를 받는 쪽에도 호출자가 붙지 않았고, 레지스트리는 그 스택에서 가장 먼저 잘려 나간 마디다.
## 제거 조건 세 가지에 대 보면
두 조건은 확인된다. 외부 프로덕션 참조가 0 이고, 운영자가 켤 수 있는 설정 경로가 없다.
세 번째 조건인 대체 경로의 특성화는 확인되지 않는데, 이유가 규칙이 상정한 것과 다르다. 규칙은 대체 경로가 같은 동작을 증명하지 못하면 지우는 순간 동작이 바뀐다고 말한다. 여기서는 이름 조회를 수행하는 프로덕션 경로가 없다. 지워도 바뀔 동작이 없는 대신, 무엇이 대체했는지도 말할 수 없다.
이 모양이 폐기된 것도 아니다. `SafeSortRegistry` 는 지금도 문자열 이름을 받아 등록되지 않았으면 던지고, 정렬 파라미터를 파싱하는 매퍼가 그것을 호출한다.
형제라 부를 만한 것은 그 하나다. gRPC 채널 레지스트리는 검증된 값 객체를 키로 쓰고, 던지는 이유도 오타가 아니라 아직 설치되지 않았다는 것이다. Mongo 일관성 레지스트리는 enum 을 키로 쓰고, 그 javadoc 이 모든 enum 상수가 언제나 있다고 적는다. 컴파일러가 키를 강제하므로 오타가 들어올 자리가 없다.
## ADR 이 지워진 것을 아직 전제한다
전체 트랜잭션 재시도 ADR 의 Enforcement 절이 두 가지를 근거로 지목한다.
```text
`FullTransactionRetryCoordinatorTest`; `RetryableJpaTransactionInterceptor.DEFAULT_ORDER`.
```
앞의 것은 있다. 뒤의 것은 삭제된 클래스의 상수이고, `DEFAULT_ORDER` 라는 이름은 소스 전체에 한 줄도 없다.
Decision 절도 마찬가지다. 재시도 어드바이스가 스프링 트랜잭션 어드바이스 바깥에 정렬되어 각 시도가 새 트랜잭션을 연다고 적는데, JPA 리프 main 에 어드바이저는 없다. 코드는 정리됐고 결정 기록은 그대로 남았다.
## 확인하지 못한 것
저장소 밖의 리플렉션이나 외부 직접 생성 여부는 소스 검색으로 알 수 없다.
포크가 이름 기반 조회를 다시 붙일 의도인지도 알 수 없다. 테스트 머리글이 계획 문서의 과제 번호와 설계 절을 가리키지만, 그 계획이 지금도 유효한지는 이 저장소가 말하지 않는다.
<!-- body:end -->