- 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>
130 lines
6.7 KiB
Markdown
130 lines
6.7 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: analysis-finding-a15-f001
|
|
title: 원인 사슬 순회가 2-순환에서 무한 루프에 빠지고, 저장소는 이미 그 사례를 이름으로 적어 두었다
|
|
topic: grpc-and-streaming
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:analysis-finding-a15-f001
|
|
evidenceCapturedOn: 2026-09-01
|
|
body: case-analysis-finding-a15-f001.body.md
|
|
assets:
|
|
- key: analysis-finding-a15-f001
|
|
file: ../../../final/evidence/rendered/analysis-finding-a15-f001.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/analysis-finding-a15-f001.txt
|
|
source:
|
|
- 원본 분석 절은 final/document.md#a15#L171 이다.
|
|
---
|
|
|
|
# 원인 사슬 순회가 2-순환에서 무한 루프에 빠지고, 저장소는 이미 그 사례를 이름으로 적어 두었다
|
|
|
|
오류 코드 판정의 종료 조건은 자기참조 하나다. 서로를 원인으로 갖는 두 예외에서는 그 조건이 참이 되지 않는다. 같은 저장소의 다른 모듈이 정확히 그 경우를 이름으로 적고 깊이 제한을 채택했다.
|
|
|
|
## 관계
|
|
|
|
- **원인 사슬은 바깥부터 안쪽까지 걸어야 한다**
|
|
같은 계열의 순회 규칙이다.
|
|
- **자기참조 검사는 2-순환을 잡지 못한다**
|
|
이 사례가 그 규칙의 형태다.
|
|
- **같은 문제를 다른 모듈에서는 닫았다**
|
|
기록하는 이유다.
|
|
|
|
## 문제
|
|
|
|
원격 호출 오류를 코드로 바꾸는 메서드가 원인 사슬을 순회한다.
|
|
|
|
종료 조건이 무엇인지 확인했다.
|
|
|
|
## 결론
|
|
|
|
자기참조 하나뿐이다. 현재 예외의 원인이 자기 자신이면 멈춘다.
|
|
|
|
서로를 원인으로 갖는 두 예외에서는 이 조건이 참이 되지 않는다.
|
|
|
|
현재 지점이 두 예외 사이를 무한히 순환한다.
|
|
|
|
이 사슬은 평범한 자바로 구성 가능하다. 하나를 만들고 그것을 원인으로 삼는 둘째를 만든 뒤, 첫째의 원인을 둘째로 지정하면 된다.
|
|
|
|
같은 저장소가 이 정확한 위험을 다른 모듈에서 이름으로 서술하고 다른 관용구를 택했다.
|
|
|
|
깊이 제한이지 순환 탐지가 아니라는 것이다. 원인 사슬은 순환일 수 있고 두 예외가 서로를 원인으로 지정한 경우가 그것이며, 제한 없는 순회 하나가 발송 스레드를 멈추게 한다는 것이다. 열이면 실제 전송 감싸기보다 훨씬 깊다는 것이다.
|
|
|
|
즉 그 주석은 자기참조 검사가 놓치는 바로 그 경우를 지목하고 깊이 제한을 그 이유로 채택한다.
|
|
|
|
저장소의 아홉 순회 지점 중 다섯이 깊이 제한이고 넷이 자기참조 검사다.
|
|
|
|
실패 시나리오는 이렇다.
|
|
|
|
기능 서비스가 순환 원인 사슬을 가진 라이브러리 예외를 전파한다. 일부 연결 풀과 재시도 감싸개가 실패 원인을 상호 참조하는 형태로 만든다.
|
|
|
|
오류 종료 경로가 그 예외를 상태에 실어 정제 종료로 보내고, 오류 코드 판정이 진입해 돌아오지 않는다.
|
|
|
|
처리기 스레드 하나가 처리기를 태우며 멈추고, 클라이언트는 응답도 상태도 받지 못한 채 마감까지 기다린다.
|
|
|
|
같은 예외가 반복되면 서버 스레드가 하나씩 소진된다.
|
|
|
|
나머지 세 지점의 영향도 비슷하다.
|
|
|
|
두 연결 끊김 탐지기는 요청 처리 중 클라이언트 끊김을 판정하는 곳이고, 트랜잭션 재시도 분류기는 재시도 여부를 판정하는 곳이다. 셋 다 요청 스레드 위에서 실행된다.
|
|
|
|
권고는 네 지점을 깊이 제한으로 통일하는 것이다.
|
|
|
|
다른 모듈의 형태가 이미 정본이고 그 근거까지 코드에 있다.
|
|
|
|
이 리프에서는 순회 반복문을 깊이 상한이 있는 형태로 바꾸면 닫힌다.
|
|
|
|
판정은 P2 다.
|
|
|
|
## 검증 환경
|
|
|
|
OpenJDK : 21.0.12
|
|
확인 방식 : 종료 조건 분석과 저장소 전체 순회 지점 계수
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
원문은 final/evidence/raw/179 계열에 있다.
|
|
|
|
1. 오류 코드 판정의 순회 반복문을 읽는다.
|
|
2. 종료 조건을 확인한다.
|
|
3. 두 예외가 서로를 원인으로 갖는 사슬을 만든다.
|
|
4. 그 사슬에서 종료 조건이 참이 되는지 확인한다.
|
|
5. 저장소의 다른 순회 지점을 세고 관용구를 분류한다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
`errorCodeOf`의 종료 조건은 `current.getCause() == current` 하나다. 서로를 원인으로 갖는 두 예외(`a.cause = b`, `b.cause = a`)에서는 이 조건이 참이 되지 않고 `current`가 a→b→a→b로 무한히 순환한다. 이 사슬은 평범한 자바로 구성 가능하다 — `a = new RuntimeException(); b = new RuntimeException(a); a.initCause(b);`.
|
|
|
|
## MvcDisconnectDetector 참조 위치
|
|
|
|
:::evidence key="analysis-finding-a15-f001" alt="코드베이스에서 MvcDisconnectDetector 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MvcDisconnectDetector 코드베이스 검색 — 2줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
## 같은 저장소가 이 위험을 이름으로 적고 다른 관용구를 택했다
|
|
|
|
> `JdkNotificationHttpGateway:93-97` — "Depth-bounded rather than cycle-detecting: **a cause chain can be circular (two exceptions each `initCause`'d to the other)**, and an unbounded walk over one hangs the dispatch thread. Ten is far deeper than any real transport wrapping."
|
|
|
|
저장소의 아홉 개 순회 지점 중 다섯이 깊이 제한이고 넷이 자기참조 검사다(§3.2).
|
|
|
|
## 실패 시나리오
|
|
|
|
feature gRPC 서비스가 순환 원인 사슬을 가진 라이브러리 예외를 전파한다. `closeWithError`가 그 예외를 `Status.withCause`에 실어 sanitizing `close`로 보내고, `errorCodeOf`가 진입해 돌아오지 않는다. gRPC 핸들러 스레드 하나가 CPU를 태우며 멈추고, 클라이언트는 응답도 상태도 받지 못한 채 데드라인까지 기다린다. 같은 예외가 반복되면 서버 스레드가 하나씩 소진된다.
|
|
|
|
## 나머지 세 지점의 영향도
|
|
|
|
`MvcDisconnectDetector`와 `WebFluxDisconnectDetector`는 요청 처리 중 클라이언트 연결 끊김을 판정하는 곳이고, `TransactionRetryClassifier`는 트랜잭션 재시도 여부를 판정하는 곳이다. 셋 다 요청 스레드 위에서 실행된다. P2.
|
|
|
|
## 권고
|
|
|
|
네 지점을 깊이 제한으로 통일한다. `JdkNotificationHttpGateway`의 형태가 이미 정본이고 그 근거까지 코드에 있다. 이 leaf에서는 `errorCodeOf`의 `while`을 `for (int depth = 0; current != null && depth < 16; depth++, current = current.getCause())`로 바꾸면 닫힌다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
순환 사슬을 실제로 흘려 스레드가 멈추는 것을 재현하지 않았다. 종료 조건상 그 결과가 나온다.
|
|
|
|
<!-- body:end -->
|