Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f023.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

131 lines
9.4 KiB
Markdown

---
kind: CASE
slug: analysis-finding-a05-f023
title: 쿼터 원장은 사용량을 적기만 하고 승인은 그것을 읽지 않는다
topic: multitenancy-isolation
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:analysis-finding-a05-f023
evidenceCapturedOn: 2026-09-04
body: case-analysis-finding-a05-f023.body.md
assets:
- key: analysis-finding-a05-f023
file: ../../../final/evidence/rendered/analysis-finding-a05-f023.svg
evidence:
- ../../../final/evidence/raw/analysis-finding-a05-f023.txt
source:
- 원본 분석 절은 final/document.md#a05 §23 이다.
---
# 쿼터 원장은 사용량을 적기만 하고 승인은 그것을 읽지 않는다
`DefaultTransferAdmissionController.acquireUpload:45`~`:62` 가 확인하는 것은 파일 크기와 저장소 고수위와 세마포어 둘이다. `JpaFileQuotaService:84``:89` 가 범위별 예약·확정 바이트를 계산하지만 그 둘을 부르는 main 코드가 정의 파일 밖에 0 줄이다.
## 관계
- **선언은 통과하는데 강제하는 주체가 없음 계열**
이 사례가 속한 구조다. 값을 계산하는 코드는 있고 그것으로 무언가를 거절하는 코드가 없다.
- **tenant별 풀이 개별적으로 합리적이고 그 합이 서버 상한을 넘긴다**
같은 계열의 자원 격리 문제다.
- **회수가 페이지 하나를 다 쓰면 남은 바이트를 들고 그대로 끝난다**
같은 원장의 다른 결함이다. 그쪽은 기록된 값이 실제보다 커지는 것이다.
## 문제
파일서버 설계 문서는 쿼터를 범위별 바이트 강제로 정의한다. 승인 컨트롤러도 클래스 설명에서 같은 어휘를 쓴다.
업로드 요청이 그 상한을 실제로 지나는지 확인했다.
## 결론
acquireUpload 는 네 가지를 지난다. :46 이 단일 파일 크기 정책, :47 이 저장소 하드 고수위, :49 가 범위별 업로드 세마포어, :54 가 인스턴스 업로드 세마포어다.
넷 중 어느 것도 바이트 집계가 아니다. 인자로 받은 바이트 수는 :46 한 곳에서만 쓰이고, 그 범위에 이미 쌓인 양은 아무 검사도 조회하지 않는다.
JpaFileQuotaService.reserve:38~:52 도 마찬가지다. 음수만 거른 뒤 예약 엔티티를 만들어 조건 없이 저장한다. 상한 조회도 집계도 없다.
집계를 계산하는 코드는 있다. :84 의 reservedBytes 와 :89 의 committedBytes 가 각각 합계 질의를 부른다. 두 메서드가 불리는 자리를 저장소 전역에서 훑으면 정의한 파일 자신 말고는 통합 시험과 가짜 구현뿐이다. 검색이 도는지 보려고 acquireUpload 를 같은 방식으로 세면 main 2 줄이 나온다.
설정 쪽에도 없다. 승인 컨트롤러가 읽는 프로퍼티 다섯은 허가 수 셋과 고수위 둘이고, 테넌트나 네임스페이스의 용량을 담은 값이 없다.
QuotaExceededException 이 던져지는 자리는 :50 하나인데, 그 이유는 바이트 초과가 아니라 범위 업로드 동시성 소진이다.
## 검증 환경
OpenJDK : 21.0.12
확인 방식 : 설계 문서의 쿼터 서술과 승인 컨트롤러 클래스 자바독 인용, acquireUpload 본문 전문 인용, 예약 저장 자리 인용, 범위별 바이트 집계를 읽는 자리 전수와 정의 파일 밖 main 호출자 계수를 대조 이름과 함께 확인, 설정의 fileserver 절 인용, 승인 컨트롤러가 읽는 프로퍼티 전수
소스 수정 : x
## 재현 조건
1. 설계 문서에서 쿼터를 바이트 강제로 정의한 줄과 승인 컨트롤러의 클래스 자바독을 인용한다.
2. acquireUpload 본문을 전문으로 싣고 무엇을 검사하는지 센다.
3. 예약을 저장하는 자리를 인용하고 조건절이 있는지 본다.
4. 범위별 예약·확정 바이트를 읽는 자리를 저장소 전역에서 찾고, 정의 파일을 뺀 main 호출자를 센다.
5. 같은 방식으로 대조 이름을 세어 검색이 도는지 확인한다.
6. 설정의 fileserver 절을 싣고 승인 컨트롤러가 읽는 프로퍼티를 전부 나열한다.
## 본문
<!-- body:start -->
파일서버 설계 문서는 쿼터를 단순 회계가 아니라 범위별 바이트 강제로 설명한다. `DefaultTransferAdmissionController` 의 클래스 자바독도 범위가 상한을 넘는 것을 쿼터 초과라고 부른다.
## acquireUpload 가 실제로 지나는 네 검사
:::evidence key="analysis-finding-a05-f023" alt="저장소 루트에서 돌린 정적 검색 출력 132줄. 먼저 docs/fileserver/design-deviations.md 에서 쿼터를 다루는 줄들이 실리고, DefaultTransferAdmissionController 1~30번 줄의 클래스 자바독이 나온다. 이어서 44~62번 줄의 acquireUpload 본문이 실리는데, 파일 크기 정책과 고수위를 확인한 뒤 범위 세마포어와 인스턴스 세마포어를 얻고 둘 다 얻으면 허가를 돌려준다. 바이트 집계를 읽는 줄이 없다. 그다음 JpaFileQuotaService 36~53번 줄의 reserve 가 나오는데 음수만 거른 뒤 예약 엔티티를 만들어 조건 없이 저장한다. 범위별 바이트 집계를 읽는 자리를 저장소 전역에서 찾으면 JpaFileQuotaService 84번과 89번 줄의 정의 둘과 postgresqlIntegrationTest 와 시험용 가짜 구현들이 나오고, 정의 파일 밖의 main 호출자는 0 줄이다. 대조로 센 acquireUpload 를 부르는 main 줄은 2 줄이다. 그 아래에 application.yml 810~832번 줄의 fileserver 설정이 실리는데 목적지의 최대 행 수와 최대 인코딩 바이트, 제공자의 루트 디렉터리와 마운트 검사만 있고 범위별 바이트 상한이 없다. 범위별 바이트 상한 이름을 fileserver 안에서 찾은 결과가 나오고, 마지막으로 승인 컨트롤러가 읽는 프로퍼티 다섯이 나오는데 인스턴스 업로드 허가 수, 직접 다운로드 허가 수, 소프트 고수위, 하드 고수위, 범위 업로드 허가 수다." caption="설계 문서의 쿼터 정의와 승인 컨트롤러의 자바독 · acquireUpload 가 실제로 검사하는 넷 · 예약이 조건 없이 저장되는 자리 · 바이트 집계를 읽는 main 호출자 0 과 대조 · 설정의 fileserver 절과 승인이 읽는 프로퍼티 다섯 — 132줄 · exit 0" zoom="true"
:::
`:46` 이 단일 파일 크기 정책을 확인하고 `:47` 이 저장소 하드 고수위를 확인한다.
`:48`\~`:53` 이 범위별 업로드 세마포어를 얻는다. 실패하면 `:50``QuotaExceededException` 을 던지는데 메시지는 `scope upload concurrency is exhausted` 다. 동시성이지 바이트가 아니다.
`:54`\~`:59` 가 인스턴스 업로드 세마포어를 얻는다. 실패하면 다른 예외다.
`requestedBytes``:46` 에만 쓰인다. 그 범위가 이미 얼마를 예약하고 확정했는지는 어느 검사도 묻지 않는다.
## 예약은 조건 없이 저장된다
`JpaFileQuotaService.reserve:38` 은 음수 바이트만 거른다.
`:43`\~`:51` 이 예약 엔티티를 만들고 `:52``reservations.save(entity)` 를 부른다. 조회도 조건절도 없다.
## 집계는 계산되지만 아무도 읽지 않는다
`:84``reservedBytes` 가 만료되지 않은 예약 바이트 합계를 부르고 `:89``committedBytes` 가 확정 바이트 합계를 부른다.
그 둘을 부르는 자리를 저장소 전역에서 찾으면 정의 파일 밖의 main 호출자가 0 줄이다. 나머지는 `PostgreSqlFileserverMetadataStoreIntegrationTest``PostgreSqlFileserverReclamationIntegrationTest` 의 단언, 그리고 `FakeFileQuotaService` 같은 시험용 구현이다.
같은 방식으로 `acquireUpload` 호출을 세면 main 2 줄이 나온다. 검색이 main 을 보고 있다.
즉 통합 시험은 그 합계가 맞는지 확인한다. 프로덕션 코드는 그 합계를 근거로 무엇도 거절하지 않는다.
## 설정에도 그 상한이 없다
`application.yml:810`\~`:832` 의 fileserver 절에는 목적지의 최대 행 수와 최대 인코딩 바이트, 제공자의 루트 디렉터리와 마운트 검사가 있다. 범위별 바이트 상한이 없다.
승인 컨트롤러가 읽는 프로퍼티는 다섯이다 — `:40` 인스턴스 업로드 허가 수, `:41` 직접 다운로드 허가 수, `:80` 소프트 고수위, `:93` 하드 고수위, `:102` 범위 업로드 허가 수.
앞의 둘과 마지막은 동시성이고 가운데 둘은 디스크 사용률이다. 어느 것도 테넌트나 네임스페이스의 용량이 아니다.
## 원문과 갈리는 자리
원문은 프로덕션 호출 그래프에 그 상한이 없다고 적었고 그대로다.
원문이 적지 않은 것은 `QuotaExceededException` 이 실제로 던져지는 자리다. 그 예외는 `:50` 에서 한 번 던져지는데 사유가 동시성 소진이다. 로그나 대시보드에서 이 예외를 바이트 초과로 읽으면 실제와 다르다.
## 확인하지 못한 것
한 범위가 실제로 큰 바이트를 쌓을 수 있는지 업로드를 돌려 확인하지 않았다.
이 상한을 애초에 넣었다가 뺀 이력이 있는지 추적하지 않았다.
디스크 사용률 상한이 범위별 상한의 대체가 되는지는 판단하지 않았다.
## 등급에 대해
원본은 이 항목을 P1 로 매겼다. 원장이 쓰이지 않는다는 사실은 이 리비전에서도 그대로이므로 등급을 새로 매기지 않는다.
<!-- body:end -->