에러 이벤트 기반 Discord 알림 시스템 설계 문서

임동혁 Ldhbenecia·2025년 8월 9일

SpringBoot

목록 보기
22/28
post-thumbnail

개요

멀티모듈(Spring) 환경에서 순환 의존성 없이, 시스템 예외 발생 시 Discord 웹훅으로 실시간 알림을 전송할 수 있도록 설계한다.

배경 및 문제점

  • common 모듈: 예외 핸들러, DTO, 상수 등 순수 공통 코드 담당
  • support 모듈: Discord 알림 등 부가 기능 담당
  • 예외 발생 시 Discord로 알림을 보내고 싶어 GlobalExceptionHandler 내부에서 DiscordWebhookService를 직접 호출하려고 했음
    • DiscordWebhookServicesupport 모듈에 있으므로, commonsupport 의존성을 추가해야 했음.
    • 그런데 이미 support 모듈은 순수 코드(common)에 의존하고 있어
      commonsupport 순환 의존성이 생김
  • 순환 의존성은 빌드, 테스트, DI 초기화에 문제(Circular reference)를 야기하며, 모듈 경계를 흐려 장기적으로 유지보수가 어려워질 것임

해결 전략: 이벤트 기반 설계

구분이전(직접 호출)이후(이벤트 방식)
호출 위치‎⁠GlobalExceptionHandler⁠ → ‎⁠DiscordWebhookService⁠‎⁠GlobalExceptionHandler⁠ → ErrorOccuredEvent 발행
의존성‎⁠common⁠ → ‎⁠support⁠ (순환)‎⁠support⁠ → ‎⁠common⁠ (단방향)
결합도강결합느슨한 결합(옵저버 패턴)

1. 이벤트 클래스 정의

  • common/event/ErrorOccuredEvent.kt
  • 예외 정보를 담는 순수 데이터 클래스
  • 어느 모듈에서도 부담 없이 의존 가능

2. 예외 핸들러에서 이벤트 발행

  • common/exception/GlobalExceptionHandler.kt
  • 예외 발생 시 ErrorOccuredEvent를 발행 (ApplicationEventPublisher.publishEvent)
  • DiscordWebhookService 등 외부 연동 코드를 직접 호출하지 않음

3. 알림 리스너에서 이벤트 구독/처리

  • support/discord/ErrorEventListener.kt
  • ErrorOccuredEvent를 구독하여 DiscordWebhookService를 통해 알림 전송
  • DiscordWebhookService는 Map 기반 직렬화를 사용하여 안전하게 Embed 메시지 전송

4. 의존성 구조

  • supportcommon (단방향)
  • commonsupport를 전혀 알지 못함
  • 순환 의존성 없이 각 모듈의 역할이 명확하게 분리됨

추가 이점

  • 확장성: Slack, 이메일, SMS 등 다른 알림 채널도 리스너만 추가하면 됨
  • 안정성: 알림 전송 실패가 예외 처리 로직에 영향을 주지 않음
  • 성능: 알림 전송을 비동기 처리하여 웹 요청 응답 속도 저하 방지

전체 흐름

  1. 예외 발생
  2. GlobalExceptionHandler에서 ErrorOccuredEvent 발행
  3. Spring Event Bus를 통해 ErrorEventListener에서 이벤트 수신
  4. DiscordWebhookService에서 Embed 메시지 생성 및 웹훅 전송

예시

예외 발생
   │
GlobalExceptionHandler (common)
   │  ErrorOccuredEvent 발행
   ▼
Spring Event Bus
   ▼
ErrorEventListener (support)
   │  └─ Embed JSON 생성
   └─ DiscordWebhookService 호출
         └─ Discord 알림

핵심 아이디어

Publisher

  • GlobalExceptionHandler가 예외를 잡으면 ErrorOccuredEvent라는 순수 POJO 이벤트 객체를 발행
  • 이벤트 클래스는 common 내부에 두어 어느 모듈에서도 의존 부담 없이 사용할 수 있게 함

Listener

  • support 모듈 안의 ErrorEventListener가 이벤트를 구독
  • 실제 외부 연동(Discord 웹훅 호출)은 이 리스너가 담당
  • 따라서 support는 여전히 common에만 의존하고, commonsupport를 전혀 알지 못함.

결과적으로 순환 의존성이 사라지고, 각 모듈의 역할도 명확해졌다.

순수 POJO 이벤트란?

단순 데이터 객체이다.

  • POJO(Plain Old Java Object)는 특별한 상속이나 어노테이션 없이, 단순히 필드와 getter/setter만 가진 일반적인 객체를 의미한다.
  • 스프링 이벤트 시스템에서 사용할 때 ApplicationEvent 를 상속하지 않고, 단순 데이터만 담는 객체를 말한다.
package com.benecia.lifetracker.common.event

data class ErrorOccuredEvent(
    val exception: Throwable,
)

ApplicationEventPublisher란?

ApplicationEventPublisher는 스프링에서 이벤트를 발행하는 역할을 하는 인터페이스이다.
스프링 컨텍스트가 자동으로 빈으로 등록해주며, 원하는 곳에서 주입받아 사용할 수 있다.

publishEvent(event) 메서드를 사용해서 이벤트 객체를 발행하면 스프링 컨텍스트 내의 모든 @EventListener가 해당 이벤트를 구독하여 처리한다.

@Component
class SomeHandler(
    private val publisher: ApplicationEventPublisher
) {
    fun handleError(e: Exception) {
        publisher.publishEvent(ErrorOccuredEvent(e))
    }
}
  • EventOccuredEvent가 발행되면, 저 타입을 구독하는 모든 이벤트 리스너들이 자동으로 호출된다.

그렇게 해서 오류를 보낼 곳에서 이벤트를 구독해두고, 이벤트 리스너 함수에서 호출될 때 어떤 작업을 할 지 처리하면 된다.

0개의 댓글