--- kind: CASE slug: grpc-policy-f03 title: 직렬 스트림 기록기의 가장 오래된 것 버리기가 잘못된 메시지의 바이트를 뺀다 topic: schema-and-data-contracts project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: case:grpc-policy-f03 evidenceCapturedOn: 2026-09-01 assets: - key: grpc-policy-f03 file: ../../../final/evidence/rendered/grpc-policy-f03.svg - key: grpc-policy-f03-diagram file: ../../../final/assets/diagrams/grpc-policy-f03.svg evidence: - ../../../final/evidence/raw/grpc-policy-f03.txt source: - 원본 분석 절은 final/document.md#a20-grpc-policy#L249 이다. module: grpc-policy priority: P2 --- # 직렬 스트림 기록기의 가장 오래된 것 버리기가 잘못된 메시지의 바이트를 뺀다 버려지는 것은 꺼낸 봉투인데 빼는 값은 새 메시지의 크기다. 봉투는 크기를 성분으로 담지 않으므로 이 지점에서 버려지는 크기를 알 방법이 없다. ## 문제 버려지는 것은 꺼낸 봉투인데 빼는 값은 새 메시지의 크기다. 봉투는 크기를 성분으로 담지 않으므로 이 지점에서 버려지는 크기를 알 방법이 없다. ## 결론 계산을 따라가면 이렇다. 한 번의 DROP_OLDEST 마다 queuedBytes 는 nextBytes 만큼 빠졌다가 enqueue 에서 같은 값만큼 다시 더해진다 — 순변화 0. 그런데 큐의 실제 내용은 nextBytes - droppedBytes 만큼 바뀐다. 그 차이가 매 낙차마다 쌓인다. 방향은 둘 다 틀렸다. 들어오는 메시지가 버려지는 것보다 크면 추적값이 실제보다 낮아져 바이트 경계가 늦게 발화한다(메모리). 반대면 실제보다 높아져 경계가 이르게 발화한다(불필요한 종료·낙차). 누적 바이트는 흐름 제어 정책의 판정 입력이고, 바이트 경계의 존재 이유가 javadoc 에 있다 — 개수 경계만 있으면 메모리 한도를 가장 큰 메시지가 정한다. ## 검증 환경 OpenJDK : 21.0.12 java -version 으로 확인 Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 확인 방식 : 큐에서 빼는 대상과 계수기에서 차감하는 값의 대조, 봉투 성분 확인 소스 수정 : x ## 재현 조건 원문은 final/document.md#a20-grpc-policy#L249 에 있다. ## 본문 버려지는 것은 꺼낸 봉투인데 빼는 값은 새 메시지의 크기다. 봉투는 크기를 성분으로 담지 않으므로 이 지점에서 버려지는 크기를 알 방법이 없다. ## 두 자리가 가리키는 대상 :::evidence key="grpc-policy-f03-diagram" alt="큐에서 빠지는 것 쪽에 가장 오래된 봉투와 그 봉투의 바이트가 놓이고 차감되는 값 쪽에 새 메시지와 그 바이트가 놓인다" caption="두 자리가 가리키는 대상" zoom="false" ::: ## 계산을 따라가면 한 번의 DROP_OLDEST 마다 `queuedBytes` 는 `nextBytes` 만큼 빠졌다가 `enqueue` 에서 같은 값만큼 다시 더해진다 — **순변화 0**. 그런데 큐의 실제 내용은 `nextBytes - droppedBytes` 만큼 바뀐다. 그 차이가 매 낙차마다 쌓인다. ## 빼는 값과 버리는 대상 :::evidence key="grpc-policy-f03" alt="분석 문서 final/document.md#a20-grpc-policy 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-policy 발췌 — 15줄" zoom="true" ::: ## 방향은 둘 다 틀렸다 들어오는 메시지가 버려지는 것보다 크면 추적값이 실제보다 **낮아져** 바이트 경계가 늦게 발화한다(메모리). 반대면 실제보다 **높아져** 경계가 이르게 발화한다(불필요한 종료·낙차). 누적 바이트는 흐름 제어 정책의 판정 입력이고, 바이트 경계의 존재 이유가 javadoc 에 있다 — 개수 경계만 있으면 메모리 한도를 가장 큰 메시지가 정한다. ## flush 가 오차를 끊는다 `flush()` 가 큐를 비우면서 `queuedBytes = 0L` 로 되돌리므로 오차가 flush 를 건너 누적되지는 않는다. 그래서 이것은 영구 드리프트가 아니라 한 flush 주기 안의 폭주 구간에서 바이트 경계를 잘못 판정하는 결함이다. 낙차가 일어나는 상황이 곧 소비자가 못 따라가는 상황이고, 그때 flush 간격이 가장 길어진다. ## 확인하지 못한 것 실제 스트림으로 버리기를 유발해 바이트 오차를 관측하지 않았다. 봉투가 크기를 성분으로 담지 않는다는 것으로 판정했다.