# 저장소 전체 (62 줄) GrpcOutcomeReplay.java:1 package dev.caskeleton.grpc.idempotency; GrpcOutcomeReplay.java:2 GrpcOutcomeReplay.java:3 import java.util.Optional; GrpcOutcomeReplay.java:4 import java.util.concurrent.ConcurrentHashMap; GrpcOutcomeReplay.java:5 import java.util.concurrent.ConcurrentMap; GrpcOutcomeReplay.java:6 GrpcOutcomeReplay.java:7 /** GrpcOutcomeReplay.java:8 * Returns the stored answer for an operation that already committed. GrpcOutcomeReplay.java:9 * GrpcOutcomeReplay.java:10 *

Separated from the ledger because the two have different size problems. The ledger row must GrpcOutcomeReplay.java:11 * stay small and is written inside the business transaction; a response may be large and is only GrpcOutcomeReplay.java:12 * needed if somebody actually retries. So the ledger stores a reference and this resolves it, GrpcOutcomeReplay.java:13 * against whatever the deployment chose — a small inline store, an object store, or a re-read of GrpcOutcomeReplay.java:14 * the committed resource. GrpcOutcomeReplay.java:15 */ GrpcOutcomeReplay.java:16 public final class GrpcOutcomeReplay { GrpcOutcomeReplay.java:17 GrpcOutcomeReplay.java:18 private final ConcurrentMap storedOutcomes = new ConcurrentHashMap<>(); GrpcOutcomeReplay.java:19 private final int maxInlineBytes; GrpcOutcomeReplay.java:20 GrpcOutcomeReplay.java:21 /** GrpcOutcomeReplay.java:22 * @param maxInlineBytes the largest response stored inline; anything larger must be kept behind a GrpcOutcomeReplay.java:23 * reference in an object store instead, which is why {@link #store} refuses it rather than GrpcOutcomeReplay.java:24 * silently truncating GrpcOutcomeReplay.java:25 */ GrpcOutcomeReplay.java:26 public GrpcOutcomeReplay(int maxInlineBytes) { GrpcOutcomeReplay.java:27 if (maxInlineBytes < 1) { GrpcOutcomeReplay.java:28 throw new IllegalArgumentException("an inline outcome store needs a positive size limit"); GrpcOutcomeReplay.java:29 } GrpcOutcomeReplay.java:30 this.maxInlineBytes = maxInlineBytes; GrpcOutcomeReplay.java:31 } GrpcOutcomeReplay.java:32 GrpcOutcomeReplay.java:33 /** Stores a response under {@code outcomeReference}. */ GrpcOutcomeReplay.java:34 public void store(String outcomeReference, byte[] serializedResponse) { GrpcOutcomeReplay.java:35 if (outcomeReference == null || outcomeReference.isBlank()) { GrpcOutcomeReplay.java:36 throw new IllegalArgumentException("an outcome needs a reference to be stored under"); GrpcOutcomeReplay.java:37 } GrpcOutcomeReplay.java:38 if (serializedResponse == null) { GrpcOutcomeReplay.java:39 throw new IllegalArgumentException("an outcome needs a serialized response"); GrpcOutcomeReplay.java:40 } GrpcOutcomeReplay.java:41 if (serializedResponse.length > maxInlineBytes) { GrpcOutcomeReplay.java:42 throw new IllegalArgumentException( GrpcOutcomeReplay.java:43 "response of " GrpcOutcomeReplay.java:44 + serializedResponse.length GrpcOutcomeReplay.java:45 + " bytes exceeds the inline outcome limit of " GrpcOutcomeReplay.java:46 + maxInlineBytes GrpcOutcomeReplay.java:47 + "; store it behind an object reference instead"); GrpcOutcomeReplay.java:48 } GrpcOutcomeReplay.java:49 storedOutcomes.put(outcomeReference, serializedResponse.clone()); GrpcOutcomeReplay.java:50 } GrpcOutcomeReplay.java:51 GrpcOutcomeReplay.java:52 /** The stored response, if there is one. */ GrpcOutcomeReplay.java:53 public Optional replay(String outcomeReference) { GrpcOutcomeReplay.java:54 byte[] stored = storedOutcomes.get(outcomeReference); GrpcOutcomeReplay.java:55 return Optional.ofNullable(stored).map(byte[]::clone); GrpcOutcomeReplay.java:56 } GrpcOutcomeReplay.java:57 GrpcOutcomeReplay.java:58 /** How many outcomes are held. */ GrpcOutcomeReplay.java:59 public int size() { GrpcOutcomeReplay.java:60 return storedOutcomes.size(); GrpcOutcomeReplay.java:61 } GrpcOutcomeReplay.java:62 } # 그 맵에 가해지는 연산 전부 GrpcOutcomeReplay.java:18 private final ConcurrentMap storedOutcomes = new ConcurrentHashMap<>(); GrpcOutcomeReplay.java:49 storedOutcomes.put(outcomeReference, serializedResponse.clone()); GrpcOutcomeReplay.java:54 byte[] stored = storedOutcomes.get(outcomeReference); GrpcOutcomeReplay.java:60 return storedOutcomes.size(); 클래스와 필드의 접근 한정 : GrpcOutcomeReplay.java:16 public final class GrpcOutcomeReplay { GrpcOutcomeReplay.java:18 private final ConcurrentMap storedOutcomes = new ConcurrentHashMap<>(); 줄이는 연산 이름 열여덟을 통틀어 : 0 건 같은 검색을 대조 파일에 걸면 : 5 건 (검색이 헛돌지 않음을 보인다) # 크기 상한이 실제로 거부하는가 16 바이트 저장 후 size : 1 17 바이트 저장 : response of 17 bytes exceeds the inline outcome limit of 16; store it behind an object reference instead 거부 후 size : 1 서로 다른 키 1000 개 저장 후 size : 1001 그 1000 개를 모두 replay 한 뒤 : 1001 같은 키로 1000 번 덮어쓴 뒤 : 1001 # 이 타입이 나오는 파일 전부 — 경로 제한 없이, 절단 없이 docs/2026-08-13-grpc-type-safe-rpc-platform-implementation-plan.md src/grpc/grpc-policy/src/main/java/dev/caskeleton/grpc/idempotency/GrpcOutcomeReplay.java src/grpc/grpc-policy/src/test/java/dev/caskeleton/grpc/idempotency/GrpcIdempotencyInterceptorTest.java 자기 파일 밖 main 참조 : 0 개 [대조] 같은 패키지의 GrpcIdempotencyDecision : 1 개 (0 이 아니어야 정상) git 이 추적하지 않는 변경 : 0 건 .java 밖에서 이 이름을 쓰는 파일 : 1 개 # 같은 패키지의 인터셉터는 재생을 어떻게 판정하는가 GrpcIdempotencyInterceptor.java:95 if (!record.sameRequestAs(requestFingerprint)) { GrpcIdempotencyInterceptor.java:96 return GrpcIdempotencyDecision.fingerprintMismatch(); GrpcIdempotencyInterceptor.java:97 } GrpcIdempotencyInterceptor.java:98 if (record.state() == GrpcOperationLedgerState.COMMITTED) { GrpcIdempotencyInterceptor.java:99 return GrpcIdempotencyDecision.replay( GrpcIdempotencyInterceptor.java:100 record GrpcIdempotencyInterceptor.java:101 .outcomeReference() GrpcIdempotencyInterceptor.java:102 .orElseThrow( GrpcIdempotencyInterceptor.java:103 () -> GrpcIdempotencyInterceptor.java:104 new IllegalStateException( GrpcIdempotencyInterceptor.java:105 "COMMITTED ledger record for '" GrpcIdempotencyInterceptor.java:106 + identity.storageKey() GrpcIdempotencyInterceptor.java:107 + "' has no outcome reference to replay"))); GrpcIdempotencyInterceptor.java:108 } GrpcIdempotencyInterceptor.java:109 if (record.state() == GrpcOperationLedgerState.FAILED_TERMINAL) { GrpcIdempotencyInterceptor.java:110 // A terminal failure is not replayed: the caller asked for the operation, it did not happen, 그 파일이 GrpcOutcomeReplay 를 부르는가 : 0 건 # 자바독이 이 타입을 무엇이라고 적는가 GrpcOutcomeReplay.java:7 /** GrpcOutcomeReplay.java:8 * Returns the stored answer for an operation that already committed. GrpcOutcomeReplay.java:9 * GrpcOutcomeReplay.java:10 *

Separated from the ledger because the two have different size problems. The ledger row must GrpcOutcomeReplay.java:11 * stay small and is written inside the business transaction; a response may be large and is only GrpcOutcomeReplay.java:12 * needed if somebody actually retries. So the ledger stores a reference and this resolves it, GrpcOutcomeReplay.java:13 * against whatever the deployment chose — a small inline store, an object store, or a re-read of GrpcOutcomeReplay.java:14 * the committed resource. GrpcOutcomeReplay.java:15 */ GrpcOutcomeReplay.java:16 public final class GrpcOutcomeReplay { # 대조 클래스에 같은 잣대를 댄다 (123 줄) 이 파일 : src/grpc/grpc-policy/src/main/java/dev/caskeleton/grpc/idempotency/GrpcOutcomeReplay.java 대조 파일 : src/grpc-advanced/grpc-advanced-streaming/src/main/java/dev/caskeleton/grpc/advanced/streaming/GrpcClientMessageDeduplicator.java GrpcClientMessageDeduplicator.java:11 *

Checkpoint-based rather than a set of seen keys. A set grows without bound for the life of a GrpcClientMessageDeduplicator.java:12 * session and answers "have I seen this" — which is not quite the question. The question is "has GrpcClientMessageDeduplicator.java:13 * this been applied", and a monotonic applied-sequence answers it in constant space and survives GrpcClientMessageDeduplicator.java:14 * the process restart that a set does not. GrpcClientMessageDeduplicator.java:16 *

Replayed outcomes are kept for the small window after the checkpoint, so a duplicate that GrpcClientMessageDeduplicator.java:17 * arrives before the checkpoint advances gets the original answer rather than being reapplied. GrpcClientMessageDeduplicator.java:18 */ GrpcClientMessageDeduplicator.java:19 public final class GrpcClientMessageDeduplicator { GrpcClientMessageDeduplicator.java:20 GrpcClientMessageDeduplicator.java:21 private final ConcurrentMap checkpoints = GrpcClientMessageDeduplicator.java:22 new ConcurrentHashMap<>(); GrpcClientMessageDeduplicator.java:23 private final ConcurrentMap replayableOutcomes = new ConcurrentHashMap<>(); 적용된 메시지마다 무엇을 넣는가 : GrpcClientMessageDeduplicator.java:70 public void recordApplied( GrpcClientMessageDeduplicator.java:71 GrpcClientStreamMessage message, String outcomeReference, Instant at) { GrpcClientMessageDeduplicator.java:72 GrpcClientStreamCheckpoint checkpoint = requireCheckpoint(message.sessionId()); GrpcClientMessageDeduplicator.java:73 checkpoints.put(message.sessionId().value(), checkpoint.advancedTo(message.sequence(), at)); GrpcClientMessageDeduplicator.java:74 if (outcomeReference != null && !outcomeReference.isBlank()) { GrpcClientMessageDeduplicator.java:75 replayableOutcomes.put(message.dedupKey(), outcomeReference); GrpcClientMessageDeduplicator.java:76 } GrpcClientMessageDeduplicator.java:77 } 정리 경로와 그 호출자 : main · GrpcClientMessageDeduplicator.java:110 public void endSession(GrpcClientStreamSessionId sessionId) { test · GrpcClientMessageDeduplicatorTest.java:117 deduplicator.endSession(session); 그 대조 클래스의 자기 파일 밖 main 참조 : 0 개 # 두 클래스가 같은 family 인가 modules.json 의 family 키 : 0 건 src/grpc/CLAUDE.md:1 # grpc — local authority for the gRPC platform family src/grpc/CLAUDE.md:2 src/grpc/CLAUDE.md:3 이 문서는 `grpc:*` family의 **local authority**다. leaf 목록·gradle path·허용 의존성은 src/grpc-advanced/CLAUDE.md:3 이 문서는 `grpc-advanced:*` family의 **local authority**다. leaf 목록·gradle path·허용 의존성은 src/grpc-advanced/CLAUDE.md:4 `src/config/architecture/modules.json`이 SSOT다. Root 정책(`CLAUDE.md` / `AGENTS.md`)과 충돌하면 src/grpc-advanced/CLAUDE.md:5 root가 이긴다. src/grpc-advanced/CLAUDE.md:6 src/grpc-advanced/CLAUDE.md:7 이 family는 Stable gRPC 플랫폼(`grpc:*`)이 **의도적으로 제외한** 능력들을 담는다. 별도 디렉터리와 src/grpc-advanced/CLAUDE.md:8 별도 Gradle prefix인 이유는 하나다: "Stable starter가 advanced module을 참조하면 build가 실패한다"는 src/grpc-advanced/CLAUDE.md:9 불변 조건을 registry의 `allowed_dependencies`만으로 기계 검증할 수 있게 하기 위해서다. src/grpc-advanced/CLAUDE.md:15 grpc:* → grpc-advanced:* (금지 — registry가 거부한다) src/grpc-advanced/CLAUDE.md:74 - Stable leaf(`grpc:*`)가 이 family를 참조하는 것.