
멀티모듈(Spring) 환경에서 순환 의존성 없이, 시스템 예외 발생 시 Discord 웹훅으로 실시간 알림을 전송할 수 있도록 설계한다.
common 모듈: 예외 핸들러, DTO, 상수 등 순수 공통 코드 담당support 모듈: Discord 알림 등 부가 기능 담당GlobalExceptionHandler 내부에서 DiscordWebhookService를 직접 호출하려고 했음DiscordWebhookService는 support 모듈에 있으므로, common → support 의존성을 추가해야 했음.support 모듈은 순수 코드(common)에 의존하고 있어common ↔ support 순환 의존성이 생김| 구분 | 이전(직접 호출) | 이후(이벤트 방식) |
|---|---|---|
| 호출 위치 | GlobalExceptionHandler → DiscordWebhookService | GlobalExceptionHandler → ErrorOccuredEvent 발행 |
| 의존성 | common → support (순환) | support → common (단방향) |
| 결합도 | 강결합 | 느슨한 결합(옵저버 패턴) |
common/event/ErrorOccuredEvent.ktcommon/exception/GlobalExceptionHandler.ktApplicationEventPublisher.publishEvent)support/discord/ErrorEventListener.ktsupport → common (단방향)common은 support를 전혀 알지 못함예외 발생
│
GlobalExceptionHandler (common)
│ ErrorOccuredEvent 발행
▼
Spring Event Bus
▼
ErrorEventListener (support)
│ └─ Embed JSON 생성
└─ DiscordWebhookService 호출
└─ Discord 알림
Publisher
GlobalExceptionHandler가 예외를 잡으면 ErrorOccuredEvent라는 순수 POJO 이벤트 객체를 발행common 내부에 두어 어느 모듈에서도 의존 부담 없이 사용할 수 있게 함Listener
support 모듈 안의 ErrorEventListener가 이벤트를 구독support는 여전히 common에만 의존하고, common은 support를 전혀 알지 못함.결과적으로 순환 의존성이 사라지고, 각 모듈의 역할도 명확해졌다.
단순 데이터 객체이다.
- POJO(Plain Old Java Object)는 특별한 상속이나 어노테이션 없이, 단순히 필드와 getter/setter만 가진 일반적인 객체를 의미한다.
- 스프링 이벤트 시스템에서 사용할 때
ApplicationEvent를 상속하지 않고, 단순 데이터만 담는 객체를 말한다.
package com.benecia.lifetracker.common.event
data class ErrorOccuredEvent(
val exception: Throwable,
)
ApplicationEventPublisher는 스프링에서 이벤트를 발행하는 역할을 하는 인터페이스이다.
스프링 컨텍스트가 자동으로 빈으로 등록해주며, 원하는 곳에서 주입받아 사용할 수 있다.
publishEvent(event) 메서드를 사용해서 이벤트 객체를 발행하면 스프링 컨텍스트 내의 모든 @EventListener가 해당 이벤트를 구독하여 처리한다.
@Component
class SomeHandler(
private val publisher: ApplicationEventPublisher
) {
fun handleError(e: Exception) {
publisher.publishEvent(ErrorOccuredEvent(e))
}
}
그렇게 해서 오류를 보낼 곳에서 이벤트를 구독해두고, 이벤트 리스너 함수에서 호출될 때 어떤 작업을 할 지 처리하면 된다.