Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f002-pool-route-exceeds-total.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

13 KiB

kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
kind slug title topic project status sourceRevision rootTreeNode evidenceCapturedOn body assets evidence source
CASE a11-f002-pool-route-exceeds-total 설정 참고 문서가 약속한 코드를 운영자는 받지 못한다 http-client-and-resilience clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:a11-f002-pool-route-exceeds-total 2026-09-02 case-a11-f002-pool-route-exceeds-total.body.md
key file
a11-f002-pool-route-exceeds-total ../../../final/evidence/rendered/a11-f002-pool-route-exceeds-total.svg
key file
a11-f002-pool-route-exceeds-total-shape ../../../final/evidence/rendered/a11-f002-pool-route-exceeds-total-shape.svg
../../../final/evidence/raw/a11-f002-pool-route-exceeds-total.txt
../../../final/evidence/raw/a11-f002-pool-route-exceeds-total-shape.txt
원본 분석 절은 final/document.md#a11#L146 이다. 등급은 P3 이다. 레코드 정규 생성자와 검증기 분기가 같은 조건을 본다는 대조, 프로파일이 이미 구성된 풀 설정을 들고 있어 그 상태로 존재할 수 없다는 판정, 프로덕션의 생성 지점이 같은 생성자를 지난다는 확인, 이 코드의 test 참조가 0 이라는 사실이 그 절에 있다. 결정적 진단 형식을 이 한 조합만 받지 못한다는 지적도 원본의 것이다.
이 기록이 더한 것은 넷이다. 두 실패 모양을 프로덕션 판정으로 나란히 실행해, 정렬까지 포함한 약속이 지켜지는 쪽과 문장 하나만 오는 쪽을 보였다. 문법과 정책의 분업이 이 계층 전반의 방식이고 이 규칙만 양쪽에 적혀 있다는 것을 확인했다. 같은 목록의 `DUPLICATE_CLIENT_NAME` 이 같은 성질이라는 것을 찾았다. 그리고 검증기의 코드가 서른다섯 종이며 원본의 서른넷은 여러 줄에 걸친 호출 하나를 지나친 수라는 것을 확인했다.

설정 참고 문서가 약속한 코드를 운영자는 받지 못한다

풀 상한 규칙이 레코드 정규 생성자와 프로파일 검증기 양쪽에 적혀 있다. 앞의 검증에서 먼저 예외가 발생하므로 뒤의 위반 코드는 실행되지 않는다. 설정 참고 문서는 그 코드를 발화하는 코드들과 같은 줄에 적어 둔다. 같은 목록에 같은 성질의 코드가 하나 더 있다.

관계

  • 지워도 test가 초록인 검사가 셋이다 두 사례 모두 앞의 검증에서 먼저 예외가 발생해 뒤의 검증 코드에 도달하지 못한다.
  • 위반 코드 34종 중 22종이 어떤 test에서도 이름으로 확인되지 않는다 이 코드의 test 참조가 0 인 이유가 그 표에 있다. 그 표의 34 는 한 줄 grep 이 만든 수다.
  • 문서가 지목하는 조정 레코드를 쓰는 코드가 없다 두 사례 모두 문서가 약속한 것을 만드는 코드가 없다.

문제

풀 설정 레코드의 정규 생성자가 경로당 상한이 전체 상한을 넘으면 던진다.

프로파일 검증기가 같은 조건을 검사하고 위반 코드를 목록에 넣는다.

두 검사가 겹치는지, 겹친다면 운영자가 받는 것이 무엇인지 확인했다.

결론

겹치고, 뒤가 도달하지 않는다.

프로파일은 이미 구성된 풀 설정을 들고 있다. 그 레코드는 경로당 상한이 전체를 넘는 상태로 존재할 수 없다. 정규 생성자를 지나지 않고 레코드를 만들 방법이 없으므로 생성 지점 수와 무관하게 그렇다. src/main 의 생성 지점이 부트스트랩 팩토리 한 군데이고 설정에서 읽은 값도 같은 생성자를 지난다는 것은 프로덕션 값도 예외가 아니라는 확인이다.

여기까지는 이 계층의 일반적인 분업이다. 레코드가 막는 것은 문법이 틀린 값이고, 검증기가 잡는 것은 문법은 맞는데 정책이 금하는 값이다. 리다이렉트 홉 수와 재시도 백오프도 같은 식으로 나뉘어 있다.

이 규칙만 양쪽에 적혀 있고, 실제로 걸리는 것은 앞쪽이다.

그래서 운영자가 받는 것이 달라진다. 프로덕션에서 전송을 잘못 잡으면 코드와 프로파일 이름과 설정 경로를 담은 위반이 돌아오고, 여럿이면 정렬된 목록으로 온다. 풀 상한을 잘못 잡으면 코드도 이름도 경로도 없는 인자 예외 하나가 온다. 부트스트랩을 거치면 빈 생성 실패가 그 인자 예외를 감싼다.

같은 목록에 같은 성질의 코드가 하나 더 있다. 이름 중복을 세는 자리는 프로파일 맵을 순회하는데, 맵의 키가 프로파일 이름이라 같은 이름이 두 번 들어갈 수 없다. 이름이 겹치면 설정 레코드가 먼저 그 이름을 두 번 선언했다고 던진다. 여기서도 코드가 비어 나간다.

경로당 상한과 전체 상한이 같은 값이면 양쪽 다 통과한다. 검사가 초과만 보기 때문이다.

주석이 적은 실제 위험은 막혀 있다. 어떤 반응형 전송에서는 경로당 손잡이가 유일하게 존재하는 것이라 그것이 조용히 실효 상한이 된다는 것인데, 그 조합은 만들어지지 않는다.

검증기가 내는 코드는 서른다섯 종이다. 원본이 적은 서른넷은 한 줄 grep 이 만든 수다. 한 코드가 세 줄에 걸쳐 있어 그 검색에 잡히지 않는다.

판정은 P3 다. 잘못된 설정은 어느 쪽이든 기동을 세운다.

검증 환경

OpenJDK : 21.0.12 확인 방식 : 레코드 생성자와 검증기 분기 대조, 생성 지점 전수 확인, 실행 탐침 소스 수정 : x

재현 조건

  1. 풀 설정 레코드의 정규 생성자를 읽는다.
  2. 프로파일 검증기의 같은 조건 분기를 읽는다.
  3. 검증기 클래스 javadoc 이 약속하는 보고 형태를 읽는다.
  4. 검증기가 내는 코드 수를 여러 줄에 걸친 호출까지 세어 확인한다.
  5. src/main 전체에서 그 레코드를 만드는 곳과 값의 출처를 본다.
  6. 이름 중복을 세는 자리가 무엇을 순회하는지, 그 앞에 무엇이 있는지 읽는다.
  7. 두 코드의 이름을 참조하는 곳을 저장소 전체에서 센다.
  8. 프로덕션 판정으로 정상 프로파일과 위반 하나짜리와 둘짜리를 검증기에 넣어 결과 모양을 본다.
  9. 경로당 상한이 전체를 넘는 풀 설정을 만들어 보고, 같은 이름을 두 번 넣은 맵의 크기를 본다.

본문

풀 상한을 잘못 잡는 실수를 두 곳이 본다.

같은 규칙이 두 곳에 있다

:::evidence key="a11-f002-pool-route-exceeds-total" alt="풀 설정 레코드의 정규 생성자가 막는 두 조건, 프로파일 검증기가 같은 조건을 다시 보는 분기와 그 주석, 검증기 클래스 javadoc 이 약속하는 보고 형태, 검증기가 내는 코드 수를 여러 줄에 걸친 호출까지 세어 한 줄 검색과 대조한 결과와 그 걸리지 않는 호출, src/main 전체에서 그 레코드를 만드는 곳, 이름 중복을 세는 자리와 그 앞에서 먼저 던지는 설정 레코드, 그리고 두 코드의 이름을 참조하는 곳을 저장소 전체에서 확장자 제한 없이 센 터미널 기록." caption="레코드 생성자가 경로당 초과를 먼저 막고 검증기가 같은 조건을 다시 본다 · javadoc 은 코드마다 정렬된 위반을 약속한다 · 코드는 35종이고 한 줄 검색은 34종만 본다 · 이름 중복도 맵 앞에서 설정 레코드가 먼저 던진다 · 두 코드를 이름으로 참조하는 곳은 그 분기들과 설정 참고 문서뿐 — 54줄 · exit 0" zoom="true" :::

if (maxConnectionsPerRoute > maxTotalConnections) {
  throw new IllegalArgumentException("per-route pool must not exceed the total pool");
}

레코드의 정규 생성자다. 검증기에는 같은 조건이 이렇게 있다.

if (profile.pool().maxConnectionsPerRoute() > profile.pool().maxTotalConnections()) {
  // A per-route ceiling above the total is incoherent, and on Reactor — where the per-route
  // knob is the only one that exists — it silently becomes the effective limit.
  out.add(violation("POOL_ROUTE_EXCEEDS_TOTAL", profile, "pool.max-connections-per-route"));
}

프로파일은 이미 만들어진 풀 설정을 들고 있고, 레코드는 정규 생성자를 지나지 않고 만들 수 없다. 이 조건이 참인 프로파일은 존재하지 않는다.

  bootstrap/autoconfigure/httpclient/HttpClientProfileFactory.java:83:    return new PoolSettings(

src/main 의 생성 지점은 여기 하나이고, 설정에서 읽은 값을 같은 생성자에 넘긴다. 프로덕션 값도 예외가 아니라는 확인이다.

문법과 정책은 원래 나뉘어 있다

문법이 틀린 값은 레코드가 막고, 문법은 맞는데 정책이 금하는 값은 검증기가 잡는다. 음수 홉 수는 리다이렉트 설정 레코드가 던지고, 홉 수 0 에 리다이렉트를 켠 조합은 검증기가 코드로 보고한다. 재시도 백오프도 같은 식이다.

풀 상한 규칙만 양쪽에 적혀 있다. 그리고 걸리는 쪽은 앞이다.

그래서 운영자가 무엇을 받는가

:::evidence key="a11-f002-pool-route-exceeds-total-shape" alt="프로덕션 판정으로 정상 프로파일과, 경로당 상한이 전체와 같은 프로파일과, 위반이 하나인 프로파일과 둘인 프로파일을 각각 검증기에 넣어 돌아온 위반 목록. 경로당 상한이 전체를 넘는 풀 설정을 만들어 봤을 때의 결과. 그리고 같은 이름을 두 번 넣은 맵의 크기를 출력한 터미널 기록. 픽스처는 고정 리비전 소스에서 직접 컴파일한다." caption="전송 오설정은 코드와 프로파일 이름과 설정 경로를 담은 위반으로 돌아오고 둘이면 정렬되어 온다 · 경로당과 전체가 같으면 통과 · 경로당이 전체를 넘는 풀 설정은 인자 예외로 끝난다 · 같은 이름을 두 번 넣은 맵의 크기는 1 — 20줄 · exit 0" zoom="true" :::

검증기의 클래스 javadoc 이 보고 형태를 약속한다.

 * <p>Every guard in the design has exactly one stable violation code here. The result is sorted so
 * a configuration error reports deterministically across runs and machines.

프로덕션 판정에서 전송을 잘못 잡으면 그 약속대로 온다.

  전송만 SIMPLE 로 바꾼 프로파일
      ClientProfileViolation[code=PRODUCTION_SIMPLE_FACTORY_FORBIDDEN, detail=profile=payment setting=transport]
  전송 JDK + 경로 풀 요구 + HTTP/3 미승인
      ClientProfileViolation[code=HTTP3_STABLE_FORBIDDEN, detail=profile=payment setting=protocols]
      ClientProfileViolation[code=JDK_FINE_GRAINED_POOL_UNSUPPORTED, detail=profile=payment setting=transport]

둘이면 코드 순으로 정렬되어 온다. 풀 상한은 다르다.

  전체 10 / 경로당 20 -> IllegalArgumentException: per-route pool must not exceed the total pool

코드도, 프로파일 이름도, 설정 경로도 없다. 부트스트랩에서는 이것이 빈 생성 실패에 감싸여 나온다.

경계값은 양쪽 다 통과한다.

  전체 20 / 경로당 20 (같음)
      위반 없음

검사가 보는 것이 초과뿐이라 같은 값은 걸리지 않는다.

같은 목록의 다른 코드

    profiles.forEach(
        (name, profile) -> {
          if (!seenNames.add(name.value())) {
            violations.add("DUPLICATE_CLIENT_NAME profile=" + name.value());
          }

순회 대상이 프로파일 맵이고 키가 프로파일 이름이다. 같은 이름이 두 번 들어갈 수 없다.

  같은 이름을 두 번 넣은 맵의 크기 : 1

이름이 겹치는 설정은 그 앞에서 이미 죽는다.

      if (seen.contains(name)) {
        throw new IllegalStateException(where + " declares '" + name + "' more than once");
      }

여기도 코드가 붙지 않는다.

문서는 두 코드를 다른 코드와 같이 적어 둔다

docs/httpclient/configuration-reference.md:191:`RETRY_BACKOFF_REQUIRED`, `MISSING_PRODUCTION_SETTING`, `DUPLICATE_CLIENT_NAME`,
docs/httpclient/configuration-reference.md:194:`HTTP2_REQUIRED_TRANSPORT_UNSUPPORTED`, `POOL_ROUTE_EXCEEDS_TOTAL`,
docs/httpclient/configuration-reference.md:209:- `POOL_ROUTE_EXCEEDS_TOTAL` — a per-route ceiling above the total is incoherent, and on Reactor,

두 코드가 저장소에 나오는 곳은 만드는 분기 각각 한 줄과 이 세 줄뿐이다.

코드는 몇 개인가

    violation( 호출 40개, 서로 다른 코드 35개
    한 줄 grep 이 보는 코드 34개, 놓치는 것 ['PROXY_AMBIENT_NO_PROXY_UNSUPPORTED']

한 코드가 세 줄에 걸쳐 있다.

    if (profile.proxy().importAmbientNoProxy()) {
      out.add(
          violation(
              "PROXY_AMBIENT_NO_PROXY_UNSUPPORTED", profile, "proxy.import-ambient-no-proxy"));

원본이 적은 서른넷은 이 호출을 지나친 수다.

확인하지 못한 것

두 위반 코드가 과거에 도달 가능했던 시점이 있었는지 이력에서 확인하지 않았다.

부트스트랩을 실제로 띄워 잘못된 풀 설정이 어떤 예외로 감싸여 보고되는지 관측하지 않았다. 확인한 것은 레코드 생성자가 던지는 예외 자체까지다.