# 저장소에 FailureCategory 라는 이름의 열거형이 몇 개인가
  src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/api/operation/FailureCategory.java
  src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/FailureCategory.java
  src/application-core/src/main/java/dev/caskeleton/application/notification/platform/api/error/FailureCategory.java
  src/messaging/messaging-core-api/src/main/java/dev/caskeleton/messaging/api/error/FailureCategory.java

# messaging-core-api 의 FailureCategory 전문 (40 줄)
  FailureCategory.java:1 package dev.caskeleton.messaging.api.error;
  FailureCategory.java:2 
  FailureCategory.java:3 /**
  FailureCategory.java:4  * The stable classification a retry engine, DLQ router, and dashboard all agree on.
  FailureCategory.java:5  *
  FailureCategory.java:6  * <p>Categories exist so that retry decisions are made from a declared class of failure rather than
  FailureCategory.java:7  * from exception type matching, which drifts every time a client library is upgraded.
  FailureCategory.java:8  */
  FailureCategory.java:9 public enum FailureCategory {
  FailureCategory.java:10 
  FailureCategory.java:11   /** Broker or network fault expected to clear on its own. */
  FailureCategory.java:12   TRANSIENT_INFRASTRUCTURE,
  FailureCategory.java:13 
  FailureCategory.java:14   /** The broker or a downstream applied backpressure or a quota. */
  FailureCategory.java:15   THROTTLED,
  FailureCategory.java:16 
  FailureCategory.java:17   /** The handler failed in a way that may succeed on redelivery. */
  FailureCategory.java:18   PROCESSING_TRANSIENT,
  FailureCategory.java:19 
  FailureCategory.java:20   /** The business rejected the message; redelivery cannot help. */
  FailureCategory.java:21   PERMANENT_BUSINESS,
  FailureCategory.java:22 
  FailureCategory.java:23   /** The message repeatedly destroys its consumer and must be parked. */
  FailureCategory.java:24   POISON_MESSAGE,
  FailureCategory.java:25 
  FailureCategory.java:26   /** The payload could not be decoded against its schema. */
  FailureCategory.java:27   DESERIALIZATION,
  FailureCategory.java:28 
  FailureCategory.java:29   /** Broker authentication failed. */
  FailureCategory.java:30   AUTHENTICATION,
  FailureCategory.java:31 
  FailureCategory.java:32   /** Broker authorization denied the operation. */
  FailureCategory.java:33   AUTHORIZATION,
  FailureCategory.java:34 
  FailureCategory.java:35   /** The outcome could not be determined. */
  FailureCategory.java:36   AMBIGUOUS,
  FailureCategory.java:37 
  FailureCategory.java:38   /** The platform or destination is misconfigured. */
  FailureCategory.java:39   CONFIGURATION
  FailureCategory.java:40 }
  상수 개수 : 10 개

# 그 범주가 무엇을 가르는가
  FailureDescriptor.java:67   public static boolean defaultRetryable(FailureCategory category) {
  FailureDescriptor.java:68     return switch (category) {
  FailureDescriptor.java:69       case TRANSIENT_INFRASTRUCTURE, THROTTLED, PROCESSING_TRANSIENT -> true;
  FailureDescriptor.java:70       case PERMANENT_BUSINESS,
  FailureDescriptor.java:71           POISON_MESSAGE,
  FailureDescriptor.java:72           DESERIALIZATION,
  FailureDescriptor.java:73           AUTHENTICATION,
  FailureDescriptor.java:74           AUTHORIZATION,
  FailureDescriptor.java:75           AMBIGUOUS,
  FailureDescriptor.java:76           CONFIGURATION ->
  FailureDescriptor.java:77           false;
  FailureDescriptor.java:78     };
  FailureDescriptor.java:79   }
  범주 이름을 DLQ 헤더로 쓰는 자리 :
    messaging/messaging-kafka · KafkaRetryMetadataMapper.java:53  new HeaderValue(failure.category().name()));
    messaging/messaging-policy · DeadLetterEnvelopeFactory.java:40  new HeaderValue(metadata.category().name()));

# 플랫폼이 스스로 막는 경로 : mayPublish 에서 CONFIGURATION 까지
  DefaultMessagePublisher.java:166     MessageEnvelope<EncodedMessage> encoded;
  DefaultMessagePublisher.java:167     try {
  DefaultMessagePublisher.java:168       profile = destinations.require(destination.name());
  DefaultMessagePublisher.java:169       requireSupportedOptions(profile, options);
  DefaultMessagePublisher.java:170       if (!access.mayPublish(destination.name())) {
  DefaultMessagePublisher.java:171         // Before encoding: an unauthorized publish must not serialise the payload, because the
  DefaultMessagePublisher.java:172         // encoded bytes are what a claim-check or a log would then be holding.
  DefaultMessagePublisher.java:173         return rejected(
  DefaultMessagePublisher.java:174             "PUBLISH_FORBIDDEN",
  DefaultMessagePublisher.java:175             "this application may not publish to '" + destination.name().value() + '\'',
  DefaultMessagePublisher.java:176             startedAt);
  DefaultMessagePublisher.java:177       }
  DefaultMessagePublisher.java:178       encoded = encode(message);

  DefaultMessagePublisher.java:278   private CompletionStage<PublishResult> rejected(String code, String reason, long startedAt) {
  DefaultMessagePublisher.java:279     return CompletableFuture.completedFuture(
  DefaultMessagePublisher.java:280         new PublishResult(
  DefaultMessagePublisher.java:281             PublishCompletion.REJECTED,
  DefaultMessagePublisher.java:282             PublishEvidence.notTransmitted(),
  DefaultMessagePublisher.java:283             RoutingOutcome.UNKNOWN,
  DefaultMessagePublisher.java:284             Optional.empty(),
  DefaultMessagePublisher.java:285             1,
  DefaultMessagePublisher.java:286             elapsedSince(startedAt),
  DefaultMessagePublisher.java:287             Optional.of(
  DefaultMessagePublisher.java:288                 new FailureDescriptor(
  DefaultMessagePublisher.java:289                     FailureCategory.CONFIGURATION, code, false, reason, Optional.empty()))));
  DefaultMessagePublisher.java:290   }
  DefaultMessagePublisher.java:291 
  그 헬퍼를 부르는 자리 전부 (호출마다 코드까지) :
    :173         return rejected(
    :174             "PUBLISH_FORBIDDEN",
    :175             "this application may not publish to '" + destination.name().value() + '\'',
    :176             startedAt);
    ---
    :182       return rejected("PUBLISH_PREPARATION_FAILED", sanitized(beforeTheWire), startedAt);
    :183     }
    :184 
    :185     Duration remaining = remainingBudget(options.timeout(), startedAt);
    ---
    :190       return rejected(
    :191           "PUBLISH_DEADLINE_EXCEEDED",
    :192           "the "
    :193               + options.timeout().toMillis()
    ---
    :235       return rejected("PUBLISH_RUNTIME_UNAVAILABLE", sanitized(beforeTheSend), startedAt);
    :236     }
    :237   }
    :238 
    ---
  이 파일이 쓰는 FailureCategory 상수 :
          1 FailureCategory.AMBIGUOUS
          1 FailureCategory.CONFIGURATION
  import :
    :5  import dev.caskeleton.messaging.api.error.FailureCategory;

# 브로커가 막는 경로 : AUTHORIZATION 을 붙이는 messaging main 코드
  messaging/messaging-core-api · MessageAuthorizationException.java:11  private static final FailureCategory CATEGORY = FailureCategory.AUTHORIZATION;
  messaging/messaging-kafka · KafkaPublishFailureClassifier.java:83  return FailureCategory.AUTHORIZATION;
  messaging/messaging-rabbit · RabbitPublishFailureClassifier.java:102  return FailureCategory.AUTHORIZATION;
  그 두 분류기가 빈으로 만들어지는 자리 :
    messaging/messaging-kafka · KafkaMessagingTransport.java:112  this.failures = new KafkaPublishFailureClassifier();
    messaging/messaging-rabbit · RabbitMessagingTransport.java:130  sequence, new RabbitPublishFailureClassifier().classify(sendFailed, true, Duration.ZERO));
    messaging/messaging-spring-boot-starter · KafkaMessagingAutoConfiguration.java:86  return new KafkaPublishFailureClassifier();
    messaging/messaging-spring-boot-starter · RabbitMessagingAutoConfiguration.java:71  return new RabbitPublishFailureClassifier();

# 같은 검사를 AUTHORIZATION 으로 올리는 클래스는 아무도 만들지 않는다
  DestinationAccessValidator.java:28    * Requires publish permission.
  DestinationAccessValidator.java:29    *
  DestinationAccessValidator.java:30    * @param destination the logical destination
  DestinationAccessValidator.java:31    */
  DestinationAccessValidator.java:32   public void requirePublish(DestinationName destination) {
  DestinationAccessValidator.java:33     if (!policy.mayPublish(destination)) {
  DestinationAccessValidator.java:34       throw new MessageAuthorizationException(
  DestinationAccessValidator.java:35           "DESTINATION_PUBLISH_DENIED",
  DestinationAccessValidator.java:36           "the producer credential may not publish to " + destination.value());
  DestinationAccessValidator.java:37     }
  DestinationAccessValidator.java:38   }
  DestinationAccessValidator.java:39 
  DestinationAccessValidator.java:40   /**
  이름이 나오는 파일 전부 (pathspec 없이) :
    docs/messaging/security.md
    src/messaging/messaging-security/src/main/java/dev/caskeleton/messaging/security/DestinationAccessValidator.java
  [대조] DestinationAccessPolicy 를 참조하는 main 파일 :
    BrokerSecurityProfile.java
    DeclaredDestinationAccess.java
    DefaultMessagePublisher.java
    DestinationAccessValidator.java
    MessagingConfigurationCompiler.java
    MessagingCoreAutoConfiguration.java
  [자기시험] 없는 이름 DestinationAccessValidatorZZZ 로 같은 검색 : 0 개 (0 이어야 정상)

# 보안 문서는 그 클래스가 지금 검사한다고 적는다
  docs/messaging/security.md:65 logical identity가 호출자 제어가 된다.
  docs/messaging/security.md:66 
  docs/messaging/security.md:67 ## ACL
  docs/messaging/security.md:68 
  docs/messaging/security.md:69 `DestinationAccessValidator`가 broker ACL **이전에** 검사한다.
  docs/messaging/security.md:70 broker ACL 거부는 애플리케이션 컨텍스트가 없는 연결 수준 오류로 도착하므로
  docs/messaging/security.md:71 "어느 모듈이 어디에 publish하려 했는가"가 조사 대상이 된다.
  docs/messaging/security.md:72 
  docs/messaging/security.md:73 ## 관측성 누출

# 기본 배선이 넣는 정책과 그 정책이 그렇게 정해진 이유
  messaging/messaging-spring-boot-starter · MessagingCoreAutoConfiguration.java:378  return dev.caskeleton.messaging.runtime.DestinationProfileRegistry.of(destinations.all());
  messaging/messaging-spring-boot-starter · MessagingCoreAutoConfiguration.java:396  return dev.caskeleton.messaging.runtime.DeclaredDestinationAccess.of(destinations.all());
  DeclaredDestinationAccess.java:25  * <p>So the default is neither: <b>a deployment may publish to the destinations it declared.</b>
  DeclaredDestinationAccess.java:26  * Declaring a destination profile is already an act of configuration — it states the ordering
  DeclaredDestinationAccess.java:27  * guarantee, the retry policy and the dead-letter target — and a message to a destination nobody
  DeclaredDestinationAccess.java:28  * declared is not an access-control edge case, it is a typo or a module reaching past its own
  DeclaredDestinationAccess.java:29  * contract. That is exactly what this check is for, and it is now the thing it catches.
  DeclaredDestinationAccess.java:30  *
