# 주제: messaging-admin-runtime 의 공개 API·의존 선언에서 확인된 것들
# revision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916

# ---- (1) public 인터페이스가 package-private 타입을 반환한다 ----
# DefaultMessagingAdminService.java:242-256
#   /** How many messages a redrive would move, and how many already failed one. */
#   record RedriveEstimate(int candidates, int alreadyRedriven) {}     <-- 수식어 없음 = package-private
#
#   /** Estimates a redrive's candidates. */
#   @FunctionalInterface
#   public interface RedriveEstimator {                                <-- public
#     RedriveEstimate estimate(RedriveRequest request);                <-- package-private 반환 타입
#   }
#
# DefaultMessagingAdminService 는 public final class 이고 그 생성자는
#   public DefaultMessagingAdminService(…, ReplayEstimator, RedriveEstimator, …)  (:78-88)
# 로 두 estimator 를 외부에서 받는다. 그런데 RedriveEstimator 를 구현하려면
# 반환 타입 RedriveEstimate 를 이름으로 써야 하고, 그 타입은 패키지 밖에서 접근할 수 없다.
# => public 생성자가 요구하는 public 인터페이스를 패키지 밖에서 구현할 수 없다.
#    (컴파일은 된다. Java 는 이 조합을 막지 않는다.)
# 대조: 같은 파일의 ReplayEstimator 는 long 을 반환하므로 외부 구현이 가능하다(:229-240).
# command: git grep -n "RedriveEstimate" -- src
#   DefaultMessagingAdminService.java:163, 243, 255  — 전부 같은 파일. 외부 구현 시도 자체가 없다.

# ---- (2) 감사 싱크 인터페이스가 중복이다 ----
# RedriveService.java:199-209
#   /** Records privileged operations. */
#   @FunctionalInterface
#   public interface AuditSink {
#     void record(dev.caskeleton.messaging.observation.MessagingAuditEvent event);
#   }
# messaging-observability/.../MessagingAuditSink.java:18-25
#   public interface MessagingAuditSink {
#     void record(MessagingAuditEvent event);
#   }
# => 시그니처가 동일하고 이벤트 타입도 같다. admin-runtime 은 이미 messaging-observability 를
#    의존하며 MessagingAuditEvent 를 그 모듈에서 import 한다(RedriveService.java:126).
#    즉 플랫폼 표준 싱크를 쓸 수 있는데 중첩 인터페이스를 새로 선언했다.
#
# 부수 효과 2개:
#  (a) ReplayService 가 형제 서비스의 중첩 타입에 의존한다.
#      ReplayService.java:28  private final RedriveService.AuditSink audit;
#      ReplayService.java:38  public ReplayService(…, RedriveService.AuditSink audit)
#  (b) MessagingAuditSink 는 InMemory 구현을 이미 제공한다(:33-55). RedriveResumptionTest 는
#      RecordingAudit 를 다시 만든다(:203-210).
#
# 그리고 계약이 하나 유실된다: MessagingAuditSink javadoc:15-16
#   "Every record has already passed {@link MessagingRedactor}, so an audit trail proves who did
#    what without becoming a second copy of the payload."
# RedriveService.AuditSink 에는 그런 서술이 없고, RedriveService:125-136 은
# request.source().value() 와 details 를 레닥션 없이 그대로 넣는다.

# ---- (3) 선언된 의존 6개 중 3개가 import 0건 ----
# command: git grep -h "^import dev.caskeleton.messaging.<pkg>" -- src/messaging/messaging-admin-runtime/src/main | wc -l
  messaging-core-api        import dev.caskeleton.messaging.api          = 6건   O
  messaging-admin-api       import dev.caskeleton.messaging.admin        = 45건  O
  messaging-observability   import dev.caskeleton.messaging.observation  = 1건   O
  messaging-policy          import dev.caskeleton.messaging.policy       = 0건   X
  messaging-transport-spi   import dev.caskeleton.messaging.transport    = 0건   X
  messaging-security        import dev.caskeleton.messaging.security     = 0건   X
# build.gradle 은 6개 전부 api 로 선언한다.

# ---- (4) 저널의 단조성(monotonic itemsCompleted)이 인터페이스 계약에 없다 ----
# DefaultMessagingAdminService.java:146, 200
#   journal.fail(lease, lease.resumeFrom(), failureCodeOf(failure), clock.get());
#   -> 이번 시도에서 checkpoint 로 올린 값이 아니라, 시도 **시작 시점**의 값을 넘긴다.
# AdminOperationJournal.fail javadoc (messaging-admin-api/AdminOperationJournal.java:65-73)
#   "Marks the operation failed at its current checkpoint, leaving it resumable."
#   "@param itemsCompleted how many items are durably done"
#   -> 파라미터를 문자 그대로 저장하라는 서술이다.
#
# 두 구현 모두 최댓값으로 clamp 한다:
#   InMemoryAdminOperationJournal.java:128,147,167   Math.max(current.itemsCompleted(), itemsCompleted)
#   JdbcAdminOperationJournal CHECKPOINT/SETTLE SQL   SET items_completed = GREATEST(items_completed, ?)
# => 진행 상황이 0으로 되돌아가지 않는 것은 두 구현이 각각 clamp 하기 때문이며,
#    인터페이스가 요구해서가 아니다. 파라미터를 문자 그대로 저장하는 세 번째 구현은
#    유일한 호출자와 결합했을 때 체크포인트를 잃는다.

# ---- (5) DestructiveMessagingAdmin.Approved 가 검증되지 않은 승인을 담는다 ----
# DestructiveMessagingAdmin.java:23-38
#   record Approved(
#       DestructiveOperation operation,
#       DestinationName destination,
#       AdminApproval approval,                 <-- public 생성자를 가진 평범한 record
#       long estimatedMessagesAffected) { … }
# 생성자는 null/음수만 검사한다. 다음을 검사하지 않는다:
#   - approval 이 이 operation 을 인가하는가
#   - approval 이 이 destination 을 인가하는가
#   - estimatedMessagesAffected 가 승인 상한 이하인가
#   - 계획 다이제스트 결합 (필드 자체가 없다)
#
# 대조군: messaging-admin-api 의 ApprovedReplayPlan/ApprovedRedrivePlan 은
#   VerifiedApproval 을 담고 생성자에서 4가지를 검사한다.
# 그리고 VerifiedApproval javadoc:9-13 이 정확히 이 형태를 과거 결함으로 기록한다:
#   "The approved-plan types used to hold a plain AdminApproval record with a public constructor,
#    so 'this plan was approved' was a claim the caller made about itself. Any code that could
#    reach the execute method could write new AdminApproval("TICKET-1", "someone", now, later)
#    and the platform believed it."
# => 그 수정이 REPLAY/REDRIVE(복구 가능한 작업)에는 적용되었고,
#    PURGE/DELETE_DESTINATION/OFFSET_RESET(복구 불가능한 작업)에는 적용되지 않았다.
#    현재 구현체가 0건이므로 실행되는 결함은 아니다(EVD-307). 그러나 운영자 도구를 쓰는 순간의 모양이다.

# ---- (6) guard 우회를 dryRun 파라미터로 표현한다 ----
# ReplayService.java:58-64
#   boolean needsApproval = !request.isolatedConsumerGroup();
#   guard.authorize(DestructiveOperation.REPLAY, request.destination(), approval,
#                   request.dryRun() || !needsApproval,      <-- dryRun 자리
#                   now);
# DestructiveOperationGuard.authorize 의 dryRun 파라미터는 :54-56 에서 무조건 return 한다.
# 격리 컨슈머그룹 리플레이가 승인 불필요라는 판단 자체는 ReplayService.java:16-19 에 근거가 있다.
# 다만 그 판단이 "dryRun=true 로 넘긴다" 로 구현되어, 감사 로그(:77)에는
#   approval.map(VerifiedApproval::ticket).orElse("isolated")
# 로 남고 guard 쪽에는 dry run 과 구별되지 않는다.
