--- kind: CASE slug: a20-f011-grpccompletionreconciler-arraylist title: GrpcCompletionReconciler 의 pending 만 잠금 없는 ArrayList 다 topic: grpc-and-streaming project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: case:a20-f011-grpccompletionreconciler-arraylist evidenceCapturedOn: 2026-09-04 body: case-a20-f011-grpccompletionreconciler-arraylist.body.md assets: - key: a20-f011-grpccompletionreconciler-arraylist file: ../../../final/evidence/rendered/a20-f011-grpccompletionreconciler-arraylist.svg evidence: - ../../../final/evidence/raw/a20-f011-grpccompletionreconciler-arraylist.txt source: - 원본 분석 절은 analysis/20-grpc-platform.md#L678 이다. --- # GrpcCompletionReconciler 의 pending 만 잠금 없는 ArrayList 다 `GrpcCompletionReconciler.pending` 은 `ArrayList` 이고, `reconcile` 과 `clearPending` 이 잠금 없이 구조를 바꾸는 동안 `pendingCases()` 가 잠금 없이 그것을 순회한다. 같은 리프의 `GrpcCancellationCoordinator` 는 같은 모양의 맵을 들고 공개 메서드 다섯을 모두 `synchronized` 로 닫았다. 다만 이 타입은 오늘 어디에서도 만들어지지 않는다. ## 관계 - **queued 를 줄이는 유일한 연산이 상한을 읽지 않는 승격 안에 있다** 그 기록은 원자 타입 위의 검사 후 실행이고 여기는 잠금 없는 컬렉션이다. 둘 다 타입이 주는 보증과 코드가 필요로 하는 보증이 다른 자리다. - **의도적으로 스레드 안전하지 않은 타입이 하나 있다** 그 기록의 `BoundedByteSink` 는 안전하지 않다는 것을 자바독에 적고 호출마다 새로 만들어 쓴다. 여기에는 그런 표기도 그런 사용 규약도 없다. - **실행 코드가 없어서 동시성 계약이 전부 문서다** 그 리프는 실행 코드가 없어 계약을 문서로만 표현한다. 여기는 실행 코드가 있고 그것을 부르는 코드가 없으며, 운영 문서가 그 동작을 미리 적어 두었다. ## 문제 GrpcCompletionReconciler 가 원장이 답을 주지 못한 연산을 목록에 모은다. 그 목록이 어떤 자료구조이고 어느 메서드가 그것을 바꾸거나 읽는지, 무엇이 언제 넣는지, 같은 리프가 같은 모양을 어떻게 다루는지 확인했다. ## 결론 pending 은 new ArrayList<>()(:25)다. 닿는 자리는 셋인데 하는 일이 다르다 — reconcile:79 의 add 와 clearPending:91 의 remove 는 구조를 바꾸고, pendingCases:86 의 List.copyOf 는 순회해 복사한다. 어느 쪽에도 잠금이 없고 동기화 마커 일곱 가지가 모두 0 이다. 들어가는 조건은 좁다. :64~:77 이 COMMITTED·FAILED_TERMINAL·업무 조회가 있는 NOT_FOUND 를 모두 먼저 반환하므로, :78 에 닿는 것은 GrpcCompletionResolution:50 이 정의한 UNKNOWN 과 IN_PROGRESS 뿐이다. 앞쪽은 원장 장애 시점이다. 같은 리프에 같은 모양이 하나 더 있다. GrpcCancellationCoordinator:27 도 동시 자료구조가 아닌 LinkedHashMap 을 오래 들고 있는데, 공개 메서드 register·cancel·markCommitBoundaryCrossed·businessEffectAborted·reason 이 모두 synchronized 다. grpc-policy 안에서 그런 파일은 여덟이다. 위험은 리스트 구조에 갇혀 있다. pending 은 밖으로 나가지 않고 원소인 PendingCase 는 record 다. 프로덕션 호출자는 없다. 자기 파일 밖 main 참조가 0 개이고, 같은 검색이 짝 클래스에서는 0 이 아니다. 운영 런북이 이 클래스의 동작과 에스컬레이션 조건을 적어 두었지만, 같은 문서 :3~:6 이 이 가족 전부가 build-only 라 프로덕션에서 도는 것이 없다고 먼저 밝힌다. 요구하는 스위치는 코드에 정의가 0 개다. 판정은 P2 이고 원문과 같다. 코드 쪽 사실은 확인됐고, 요청 경로라는 근거는 그것을 부르는 자리가 없어 오늘 서지 않는다. 대신 그 상황을 다룰 절차가 이미 문서에 있고 리스트는 지금 모양 그대로라는 사실이 남는다. ## 검증 환경 OpenJDK : 21.0.12 확인 방식 : 클래스 자바독과 목록 선언 확인, pending 이 나오는 줄 전수와 구조 변경·순회 지점 구분, 동기화 마커 일곱을 이 파일·짝 파일·리프 전체 세 열로 계수, 같은 모양의 필드를 가진 클래스 검색과 그 공개 메서드 확인, reconcile 의 분기 전수와 requiresReconciliation 정의 확인, 경로 제한 없는 참조 검색과 대조 타입 계수, 운영 런북의 앞머리와 해당 절차 확인, 그 런북이 요구하는 프로퍼티의 정의 검색 소스 수정 : x ## 재현 조건 1. 목록 필드의 선언과 클래스 자바독을 읽는다. 2. 그 이름이 나오는 줄을 전부 찾고 구조를 바꾸는 것과 아닌 것을 가른다. 3. 동기화 마커를 여러 이름으로 세되, 같은 검색을 같은 리프의 다른 파일에도 걸어 검색이 도는지 확인한다. 4. 그 검색을 리프 전체에 걸어 마커를 가진 파일을 센다. 5. 같은 모양의 필드를 가진 클래스를 찾고 그 공개 메서드의 잠금 여부를 읽는다. 6. 목록에 넣는 줄까지 오는 분기를 처음부터 따라가고, 그 조건 메서드의 정의를 읽는다. 7. 이 타입의 참조를 경로 제한 없이 검색하고, 같은 방식으로 대조 타입도 센다. 8. 문서 참조가 있으면 그 문서의 앞머리부터 읽어 절차에 조건이 걸려 있는지 본다. 9. 그 문서가 요구하는 설정 프로퍼티가 코드에 정의돼 있는지 찾는다. ## 본문 `GrpcCompletionReconciler` 는 원장이 답을 주지 못한 gRPC 호출을 목록에 모아 두는 93 줄짜리 클래스다. 그 목록은 `new ArrayList<>()`(`:25`)이고 감싸는 것이 없다. ## pending 을 바꾸는 두 자리와 순회하는 한 자리 :::evidence key="a20-f011-grpccompletionreconciler-arraylist" alt="저장소 루트에서 돌린 정적 검색 출력 137줄. GrpcCompletionReconciler 의 클래스 자바독과 ArrayList 선언이 8번부터 26번 줄까지 실리고, pending 이라는 이름이 나오는 줄이 전부 나오는데 선언과 add 와 copyOf 와 remove 넷에 예외 메시지 둘이다. 동기화 마커 일곱 가지를 이 파일과 같은 리프의 GrpcCancellationCoordinator 와 리프 전체에 각각 걸어 세 열로 비교한 표가 나오는데 이 파일은 전부 0 이고 짝 파일은 synchronized 다섯이다. 이어서 같은 검색을 리프 62 파일에 걸어 표시자를 가진 여덟 파일이 나열된다. pending.add 가 실행되는 조건이 reconcile 본문 전체와 함께 실리고, requiresReconciliation 이 UNKNOWN 이나 IN_PROGRESS 일 때만 참이라는 정의가 따라온다. 같은 모양의 필드를 가진 클래스 다섯이 나오고 그중 GrpcCancellationCoordinator 의 공개 메서드가 전부 synchronized 인 것이 보인다. 참조 검색은 자기 파일 밖 main 이 0 개이고 대조 타입은 0 이 아니다. 마지막으로 운영 런북의 첫 아홉 줄이 실려 이 가족 전부가 build-only 라 여기 있는 것 중 프로덕션에서 도는 것이 없다고 문서가 스스로 밝히는 대목이 나오고, 그 런북이 요구하는 스위치가 코드에 정의된 파일 수와 지원 매트릭스의 같은 서술, 그리고 이 클래스에 기대하는 절차가 이어진다." caption="ArrayList 선언과 pending 이 나오는 줄 전부 · 표시자 일곱을 이 파일·짝·리프에 건 세 열 비교 · 리프 62 중 여덟 · add 의 실행 조건과 requiresReconciliation 정의 · 같은 모양 필드 다섯과 짝의 synchronized · 자기 파일 밖 main 0 · 런북의 build-only 자기 규정 — 137줄 · exit 0" zoom="true" ::: `pending` 이라는 이름은 여섯 줄에 나온다. 선언(`:25`), `reconcile` 안의 `add`(`:79`), `pendingCases()` 의 `List.copyOf`(`:86`), `clearPending` 의 `remove`(`:91`), 그리고 `PendingCase` 생성자의 예외 메시지 둘(`:34`, `:37`)이다. 셋이 잠금 없이 같은 리스트에 닿는데 하는 일이 다르다. `add`(`:79`)와 `remove`(`:91`)는 구조를 바꾸고, `List.copyOf(pending)`(`:86`)은 그 목록을 순회해 복사한다. 원문은 두 실패 모드를 따로 적었다. 동시 `add` 는 원소 유실이나 `ArrayIndexOutOfBoundsException` 이고, `add` 가 도는 중의 `List.copyOf` 는 `ConcurrentModificationException` 이나 널 원소로 인한 `NullPointerException` 이다. `pending` 이 담는 것은 사람이 조정해야 하는 연산이므로 유실은 조정되지 않은 채 잊히는 연산이 된다. 이 기록은 그 실행을 재현하지 않았다. 위험이 리스트 구조에 갇혀 있기는 하다. `pending` 은 `private final` 이고 밖으로 나가는 것은 `List.copyOf` 로 만든 불변 복사뿐이라 가변 참조가 새지 않는다. `PendingCase` 는 record 라 원소 자체도 불변이다. ## 항목이 들어가는 조건 `reconcile:62`\~`:82` 를 따라가면 `add` 에 닿는 길이 좁다. `:64`\~`:67` 이 `COMMITTED` 와 `FAILED_TERMINAL` 을 먼저 돌려보낸다. `:68`\~`:77` 은 `NOT_FOUND` 이고 업무 조회가 주어진 경우인데, 조회가 결과를 보이면 `:71` 에서, 아니면 `:76` 에서 반환한다. 어느 쪽이든 `:78` 에 닿지 않는다. `:78` 의 `requiresReconciliation()` 은 `GrpcCompletionResolution:50` 에서 `status == UNKNOWN || status == IN_PROGRESS` 다. 즉 목록에 쌓이는 것은 원장을 조회하지 못했거나 청구가 아직 진행 중인 경우다. 앞쪽은 원장 장애 시점이고, 그때가 호출이 가장 몰리는 때다. ## 같은 리프의 짝은 같은 모양을 잠근다 동기화 마커 일곱 가지를 이 파일과 `GrpcCancellationCoordinator` 와 리프 전체에 각각 걸었다. 이 파일은 전부 0 이고, 짝 파일은 `synchronized` 다섯이다. 같은 검색이 한쪽에서 0 이 아닌 값을 내므로 검색이 헛돌지 않는다. 두 클래스는 모양이 같다. `GrpcCancellationCoordinator:27` 이 `Map operations = new LinkedHashMap<>()` 를 들고, 이 클래스가 `List pending = new ArrayList<>()` 를 든다. 둘 다 동시 자료구조가 아니고 둘 다 오래 사는 객체다. 갈리는 것은 그다음이다. 짝의 공개 메서드는 `register`(`:45`), `cancel`(`:69`), `markCommitBoundaryCrossed`(`:90`), `businessEffectAborted`(`:100`), `reason`(`:110`)이 모두 `synchronized` 다. 이 클래스에는 하나도 없다. 이 짝만 그런 것도 아니다. 리프를 전부 훑으면 여덟 파일이 잠금이나 동시 자료구조를 쓴다. 이 리프가 동시성을 다루지 않는 것이 아니다. ## 런북은 자기가 아직 안 도는 것을 알고 쓰였다 운영 런북이 이 클래스를 안내한다는 것은 원문에 없다. `docs/runbooks/grpc-platform-operations.md:36`\~`:37` 은 `UNKNOWN` 이 원장을 조회할 수 없었다는 뜻이고 아무것도 결론지을 수 없으며 그 사례는 이 클래스가 큐에 넣고 나중에 재시도한다고 적는다. `:41`\~`:42` 는 그 목록이 여러 차례에 걸쳐 자라면 에스컬레이션하라고 적는다. 그러나 같은 문서 `:3`\~`:6` 이 먼저 조건을 건다. 범위가 `:grpc:*` 가족이고 오늘 전부 build-only 이며 모든 leaf 의 `runtime_memberships` 가 비어 있어 여기 있는 것 중 프로덕션에서 도는 것이 없다는 것이다. 그렇게 미리 쓰는 이유도 적는다 — 이 문서가 다루는 상태들은 당직자가 새벽 세 시에 원리부터 풀어낼 수 있는 것이 아니고, 동작을 런북보다 먼저 출하하면 그것을 처음 만나는 사람이 그 일을 하게 된다는 것이다. `:9` 는 `ca-skeleton.grpc.platform.enabled` 가 `true` 여야 플랫폼이 시작된다고 적는데, 그 프로퍼티를 정의하는 파일이 저장소에 0 개다. `docs/compatibility/grpc-support-matrix.md:66`\~`:67` 도 배포 아티팩트가 이 가족을 싣지 않는다고 따로 적는다. 그러므로 오늘 이 절차를 밟는 운영자는 없다. 남는 것은 그 절차가 이미 적혀 있고, 배선되는 순간 그것이 읽는 대상이 이 잠금 없는 리스트라는 것이다. ## 이 타입을 만드는 프로덕션 코드가 없다 자기 파일 밖에서 이 타입을 쓰는 main 파일은 0 개다. 참조는 자기 파일 둘, `GrpcCompletionReconcilerTest` 다섯 줄, 그리고 문서 셋이다. 같은 검색을 `GrpcCancellationCoordinator` 에 걸면 0 이 아니다. ## 원문과 갈리는 자리 원문은 이 리프에서 스레드 안전성을 명시적으로 다루는 유일한 클래스가 `GrpcSerializedStreamWriter` 라고 적었다. 62 파일 중 여덟이 동시성 구성을 쓴다. 원문은 `reconcile` 이 완료 결과가 불확실한 호출마다 불린다고 적었다. 목록에 쌓이는 조건은 그보다 좁아서 `UNKNOWN` 과 `IN_PROGRESS` 둘뿐이고, `NOT_FOUND` 는 업무 조회가 있는 경로에서 `:78` 에 닿지도 않는다. `pending` 이 `ArrayList` 라는 것, 두 자리가 잠금 없이 구조를 바꾸고 한 자리가 잠금 없이 순회한다는 것, 마커가 0 이라는 것은 원문대로다. ## 등급에 대해 원본 분석의 등급은 P2 다. 이 기록은 새로 매기지 않는다. 상류가 P2 로 둔 근거는 요청 경로에서 동기화 없는 리스트가 변경된다는 것이다. 자료구조와 표시자 부재는 확인했고, 요청 경로라는 부분은 부르는 코드가 없어 오늘 성립하지 않는다. 같은 무게로 반대편에 놓이지 않는 사실이 하나 있다. 이 코드가 오늘 도는 것은 아니지만, 그것이 다룰 상황과 그때의 절차는 이미 문서에 적혀 있다. 배선은 남은 작업이고 리스트는 지금 모양 그대로다. ## 확인하지 못한 것 스레드를 여럿 띄워 동시 호출로 원소가 사라지는 것을 재현하지 않았다. 리스트 타입과 마커 부재와 세 자리가 하는 일을 읽은 데까지다. `UNKNOWN` 상태를 만들어 넣지 않았다. 분기와 조건 정의를 코드로 따라갔다. 다른 브랜치에 이 클래스를 물리는 코드가 있는지 확인하지 않았다. 이 리비전의 main 에 없다는 것까지다. 다른 브랜치에 이 클래스를 물리는 코드가 있는지 확인하지 않았다. 이 리비전의 main 에 없다는 것까지다.