# 두 소비자가 같은 FailOpenDependencyLogger 를 쓴다
  adapter/outbound/messaging · main · OutboundMessagePublisher.java:19  private final FailOpenDependencyLogger dependencyLogger;
  adapter/outbound/messaging · test · OutboundMessagePublisherTest.java:28  private FailOpenDependencyLogger dependencyLogger;
  adapter/outbound/messaging · test · OutboundMessagePublisherTest.java:37  dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
  adapter/outbound/messaging · test · OutboundMessagePublisherTest.java:138  new OutboundMessagePublisher(broker, new FailOpenDependencyLogger(throwingLogger));
  adapter/outbound/notification · main · FailOpenNotificationProvider.java:17  private final FailOpenDependencyLogger dependencyLogger;
  adapter/outbound/notification · test · NotificationAdapterTest.java:47  private FailOpenDependencyLogger dependencyLogger;
  adapter/outbound/notification · test · NotificationAdapterTest.java:56  dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
  adapter/outbound/notification · test · RoutingNotifierTest.java:55  private FailOpenDependencyLogger dependencyLogger;
  adapter/outbound/notification · test · RoutingNotifierTest.java:64  dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
  adapter/outbound/support · main · FailOpenDependencyLogger.java:17  public FailOpenDependencyLogger() {
  adapter/outbound/support · main · FailOpenDependencyLogger.java:22  public FailOpenDependencyLogger(Logger log) {
  adapter/outbound/support · main · OutboundSupportConfig.java:18  return new FailOpenDependencyLogger();
  adapter/outbound/support · test · FailOpenDependencyLoggerTest.java:18  private FailOpenDependencyLogger dependencyLogger;
  adapter/outbound/support · test · FailOpenDependencyLoggerTest.java:28  dependencyLogger = new FailOpenDependencyLogger(logbackLogger);

# 두 파일의 이력
  FailOpenNotificationProvider.java
    821fe00c 2026-07-24 init: 클린 아키텍처 백엔드
  OutboundMessagePublisher.java
    2f5d2fc2 2026-08-15 feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
    821fe00c 2026-07-24 init: 클린 아키텍처 백엔드

# 처음 출하된 메시징 publish (821fe00c) — 지금 알림 쪽과 같은 모양
  OutboundMessagePublisher.java@821fe00c:26   @Override
  OutboundMessagePublisher.java@821fe00c:27   public void publish(OutboundMessage message) {
  OutboundMessagePublisher.java@821fe00c:28     try {
  OutboundMessagePublisher.java@821fe00c:29       broker.send(message);
  OutboundMessagePublisher.java@821fe00c:30       dependencyLogger.logSuccess(broker.brokerId(), DEPENDENCY_TYPE, "publish");
  OutboundMessagePublisher.java@821fe00c:31     } catch (Exception ex) {
  OutboundMessagePublisher.java@821fe00c:32       // fail-open: observe with correlationId, delegate durability to outbox/retry,
  OutboundMessagePublisher.java@821fe00c:33       // do NOT propagate — the core use case must still succeed.
  OutboundMessagePublisher.java@821fe00c:34       dependencyLogger.logFailure(broker.brokerId(), DEPENDENCY_TYPE, "publish", ex);
  OutboundMessagePublisher.java@821fe00c:35     }
  OutboundMessagePublisher.java@821fe00c:36   }
  OutboundMessagePublisher.java@821fe00c:37 }

# 지금의 메시징 publish (:26~:60)
  OutboundMessagePublisher.java:26   @Override
  OutboundMessagePublisher.java:27   public void publish(OutboundMessage message) {
  OutboundMessagePublisher.java:28     // The send and the observation are separate steps because they used to share a try block: a
  OutboundMessagePublisher.java:29     // logger that threw after a successful send was caught by the same catch and reported as a
  OutboundMessagePublisher.java:30     // publish failure. The broker had accepted the message; the only thing that failed was the
  OutboundMessagePublisher.java:31     // record of it, and the two must not be confusable.
  OutboundMessagePublisher.java:32     boolean sent = false;
  OutboundMessagePublisher.java:33     try {
  OutboundMessagePublisher.java:34       broker.send(message);
  OutboundMessagePublisher.java:35       sent = true;
  OutboundMessagePublisher.java:36     } catch (Exception ex) {
  OutboundMessagePublisher.java:37       // fail-open: observe with correlationId, delegate durability to outbox/retry,
  OutboundMessagePublisher.java:38       // do NOT propagate — the core use case must still succeed.
  OutboundMessagePublisher.java:39       observeQuietly(
  OutboundMessagePublisher.java:40           () -> dependencyLogger.logFailure(broker.brokerId(), DEPENDENCY_TYPE, "publish", ex));
  OutboundMessagePublisher.java:41     }
  OutboundMessagePublisher.java:42     if (sent) {
  OutboundMessagePublisher.java:43       observeQuietly(
  OutboundMessagePublisher.java:44           () -> dependencyLogger.logSuccess(broker.brokerId(), DEPENDENCY_TYPE, "publish"));
  OutboundMessagePublisher.java:45     }
  OutboundMessagePublisher.java:46   }
  OutboundMessagePublisher.java:47 
  OutboundMessagePublisher.java:48   /**
  OutboundMessagePublisher.java:49    * Runs an observation, absorbing whatever it throws.
  OutboundMessagePublisher.java:50    *
  OutboundMessagePublisher.java:51    * <p>Diagnostics are non-authoritative. An appender that is out of disk must not change what the
  OutboundMessagePublisher.java:52    * caller believes about the broker.
  OutboundMessagePublisher.java:53    */
  OutboundMessagePublisher.java:54   private static void observeQuietly(Runnable observation) {
  OutboundMessagePublisher.java:55     try {
  OutboundMessagePublisher.java:56       observation.run();
  OutboundMessagePublisher.java:57     } catch (RuntimeException ignored) {
  OutboundMessagePublisher.java:58       // Nothing to report it to: the reporter is what failed.
  OutboundMessagePublisher.java:59     }
  OutboundMessagePublisher.java:60   }

# 지금의 알림 send 와 그 클래스가 스스로 적은 계약
  FailOpenNotificationProvider.java:7 /**
  FailOpenNotificationProvider.java:8  * Fail-open decorator: a provider failure is logged (no payload/PII) and swallowed so a
  FailOpenNotificationProvider.java:9  * notification — a side-effect — never fails the core use case. Applied centrally by
  FailOpenNotificationProvider.java:10  * NotificationConfig.
  FailOpenNotificationProvider.java:11  */
  FailOpenNotificationProvider.java:35   @Override
  FailOpenNotificationProvider.java:36   public void send(Notification notification) {
  FailOpenNotificationProvider.java:37     try {
  FailOpenNotificationProvider.java:38       delegate.send(notification);
  FailOpenNotificationProvider.java:39       dependencyLogger.logSuccess(delegate.providerId(), DEPENDENCY_TYPE, "send");
  FailOpenNotificationProvider.java:40     } catch (Exception ex) {
  FailOpenNotificationProvider.java:41       // fail-open: observe (no payload/PII), do not fail the core use case.
  FailOpenNotificationProvider.java:42       dependencyLogger.logFailure(delegate.providerId(), DEPENDENCY_TYPE, "send", ex);
  FailOpenNotificationProvider.java:43     }
  FailOpenNotificationProvider.java:44   }
  FailOpenNotificationProvider.java:45 }

# 그 send 를 부르는 자리
  RoutingNotifier.java:115     Map<String, FailOpenNotificationProvider> channelRegistry =
  RoutingNotifier.java:116         registry.getOrDefault(channel, Map.of());
  RoutingNotifier.java:117     // FailOpenNotificationProvider.send declares no throws — no try/catch needed.
  RoutingNotifier.java:118     // Individual provider failures are observed (logged) inside the decorator
  RoutingNotifier.java:119     // and never propagated, so one failure does not block remaining fan-out sends.
  RoutingNotifier.java:120     for (String providerId : providerIds) {
  RoutingNotifier.java:121       channelRegistry.get(providerId).send(notification);
  RoutingNotifier.java:122     }
  RoutingNotifier.java:123   }
  NotificationPort 를 참조하는 main 파일 :
    Notification.java
    NotificationConfig.java
    NotificationPort.java
    RoutingNotifier.java

# 메시징 쪽 회귀 시험 전문 (:111~:145)
  OutboundMessagePublisherTest.java:111   @org.junit.jupiter.api.DisplayName(
  OutboundMessagePublisherTest.java:112       "a logger failure after a confirmed send is not a publish failure")
  OutboundMessagePublisherTest.java:113   @Test
  OutboundMessagePublisherTest.java:114   void aLoggerFailureAfterAConfirmedSendIsNotAPublishFailure() {
  OutboundMessagePublisherTest.java:115     // The send and the success log used to share one try block, so a logger that threw after the
  OutboundMessagePublisherTest.java:116     // broker had accepted the message was caught by the failure branch and recorded as a publish
  OutboundMessagePublisherTest.java:117     // failure. The broker's outcome and the record of it are different facts.
  OutboundMessagePublisherTest.java:118     FakeBroker broker = new FakeBroker();
  OutboundMessagePublisherTest.java:119     org.slf4j.Logger throwingLogger =
  OutboundMessagePublisherTest.java:120         (org.slf4j.Logger)
  OutboundMessagePublisherTest.java:121             java.lang.reflect.Proxy.newProxyInstance(
  OutboundMessagePublisherTest.java:122                 getClass().getClassLoader(),
  OutboundMessagePublisherTest.java:123                 new Class<?>[] {org.slf4j.Logger.class},
  OutboundMessagePublisherTest.java:124                 (proxy, method, args) -> {
  OutboundMessagePublisherTest.java:125                   if (method.getName().equals("debug")) {
  OutboundMessagePublisherTest.java:126                     throw new IllegalStateException("the appender is out of disk");
  OutboundMessagePublisherTest.java:127                   }
  OutboundMessagePublisherTest.java:128                   if (method.getReturnType() == boolean.class) {
  OutboundMessagePublisherTest.java:129                     return true;
  OutboundMessagePublisherTest.java:130                   }
  OutboundMessagePublisherTest.java:131                   if (method.getReturnType() == String.class) {
  OutboundMessagePublisherTest.java:132                     return "test.messaging";
  OutboundMessagePublisherTest.java:133                   }
  OutboundMessagePublisherTest.java:134                   return null;
  OutboundMessagePublisherTest.java:135                 });
  OutboundMessagePublisherTest.java:136 
  OutboundMessagePublisherTest.java:137     OutboundMessagePublisher publisher =
  OutboundMessagePublisherTest.java:138         new OutboundMessagePublisher(broker, new FailOpenDependencyLogger(throwingLogger));
  OutboundMessagePublisherTest.java:139     OutboundMessage message = new OutboundMessage("worklog-events", "wl-1", "{}");
  OutboundMessagePublisherTest.java:140 
  OutboundMessagePublisherTest.java:141     assertThatCode(() -> publisher.publish(message)).doesNotThrowAnyException();
  OutboundMessagePublisherTest.java:142     assertThat(broker.sent)
  OutboundMessagePublisherTest.java:143         .as("the message reached the broker; only the record of it failed")
  OutboundMessagePublisherTest.java:144         .containsExactly(message);
  OutboundMessagePublisherTest.java:145   }

# 알림 쪽에서 그 클래스를 실제로 조립하는 시험
  adapter/outbound/notification · test · NotificationAdapterTest.java:91  new FailOpenNotificationProvider(
  adapter/outbound/notification · test · NotificationAdapterTest.java:101  new FailOpenNotificationProvider(
  adapter/outbound/notification · test · NotificationAdapterTest.java:123  new FailOpenNotificationProvider(
  adapter/outbound/notification · test · NotificationAdapterTest.java:133  new FailOpenNotificationProvider(
  adapter/outbound/notification · test · RoutingNotifierTest.java:137  return new FailOpenNotificationProvider(provider, dependencyLogger);
  adapter/outbound/notification · test · RoutingNotifierTest.java:147  new FailOpenNotificationProvider(
  adapter/outbound/notification · test · RoutingNotifierTest.java:157  new FailOpenNotificationProvider(
  adapter/outbound/notification · test · RoutingNotifierTest.java:179  new FailOpenNotificationProvider(
  adapter/outbound/notification · test · RoutingNotifierTest.java:189  new FailOpenNotificationProvider(
  adapter/outbound/notification · test · RoutingNotifierTest.java:243  new FailOpenNotificationProvider(
  adapter/outbound/notification · test · RoutingNotifierTest.java:245  new FailOpenNotificationProvider(
  adapter/outbound/notification · test · RoutingNotifierTest.java:316  new FailOpenNotificationProvider(
  그 시험들 안에서 던지도록 만든 것 :
    NotificationAdapterTest.java
      :79  throw new RuntimeException("provider-failure");
    RoutingNotifierTest.java
      :130  throw new RuntimeException("provider-failure");
  로거 자체를 던지게 만드는 자리 (메시징 회귀 시험이 쓰는 방식) :
    src/adapter/outbound/notification  Proxy.newProxyInstance : 0 개
    src/adapter/outbound/messaging     Proxy.newProxyInstance : 1 개
  그 클래스 이름을 파일명에 가진 시험 : 0 개

# 프로브 : 같은 로거 프록시를 세 구조에 똑같이 물린다
    probe:24  if (n.equals("debug")) {
    probe:27  throw new IllegalStateException("the appender is out of disk");
    probe:30  if (n.equals("warn")) {
    probe:33  throw new IllegalStateException("the appender is out of disk");
    probe:36  if (method.getReturnType() == boolean.class) {
    probe:39  if (method.getReturnType() == String.class) {
    probe:60  sends.incrementAndGet();
    probe:65  static void reset() {
    probe:72  reset();
    probe:74  new FailOpenNotificationProvider(succeeding(), new FailOpenDependencyLogger(logger(debugThrows, warnThrows)));
    probe:78  } catch (RuntimeException ex) {
    probe:87  reset();
    probe:97  sends.incrementAndGet();
    probe:101  new dev.caskeleton.adapter.outbound.messaging.core.OutboundMessagePublisher(
    probe:102  broker, new FailOpenDependencyLogger(logger(debugThrows, warnThrows)));
    probe:106  } catch (RuntimeException ex) {
    probe:116  reset();
    probe:126  sends.incrementAndGet();
    probe:130  new PublisherAsShippedAtInit(
    probe:131  broker, new FailOpenDependencyLogger(logger(debugThrows, warnThrows)));
    probe:135  } catch (RuntimeException ex) {
  프로브가 로드한 클래스가 어디서 왔는가
    FailOpenNotificationProvider   file:/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/notification/build/classes/java/main/
    OutboundMessagePublisher       file:/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/messaging/build/classes/java/main/
    FailOpenDependencyLogger       file:/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/support/build/libs/support-0.0.1+21234e38cdb9.jar

  알림 쪽 FailOpenNotificationProvider.send (위임 전송은 항상 성공한다)
    로거가 던지지 않을 때                       전송 1 · debug 1 · warn 0 · 호출자에게 나간 예외 없음
    성공 로거만 던질 때                        전송 1 · debug 1 · warn 1 · 호출자에게 나간 예외 없음
    성공 로거와 실패 로거가 던질 때                 전송 1 · debug 1 · warn 1 · 호출자에게 나간 예외 IllegalStateException

  메시징 쪽 OutboundMessagePublisher.publish (브로커 전송은 항상 성공한다)
    로거가 던지지 않을 때                       전송 1 · debug 1 · warn 0 · 호출자에게 나간 예외 없음
    성공 로거만 던질 때                        전송 1 · debug 1 · warn 0 · 호출자에게 나간 예외 없음
    성공 로거와 실패 로거가 던질 때                 전송 1 · debug 1 · warn 0 · 호출자에게 나간 예외 없음

  저장소가 처음 출하한 메시징 구조 (821fe00c, 지금 알림 쪽과 같은 모양)
    성공 로거만 던질 때                        전송 1 · debug 1 · warn 1 · 호출자에게 나간 예외 없음
      OutboundMessagePublisherTest:141 의 첫 단언(예외 없음) : 통과
      OutboundMessagePublisherTest:142 의 둘째 단언(브로커가 받음) : 통과
