
클라이언트의 요구 사항을 비즈니스 로직으로 풀어내다 보면 하나의 요청이 여러 작업으로 연쇄적으로 이어지는 경우가 많다. 대표적인 예가 삭제 시나리오다. 폴더 삭제 요청이 들어오면 하위 파일 삭제, 연관 데이터 정리, 통계 반영 등 여러 로직이 함께 수행된다. 이러한 흐름을 모두 직접 호출로 처리할 경우 서비스 간 의존성이 빠르게 증가하게 된다.
Spring-Event 의존성 문제
이벤트를 사용하지 않는 구조에서는 상위 서비스가 하위 서비스들을 직접 알고 호출해야 한다. A-Service가 삭제 요청을 받으면 B-Service, C-Service를 직접 호출하여 각각의 삭제 로직을 수행한다. 이때 A-Service는 어떤 서비스들이 존재하는지, 어떤 순서로 호출해야 하는지 모두 알고 있어야 한다.
문제는 요구사항이 추가될 때 발생한다. 기존 삭제 흐름에 D-Service가 추가되면 A-Service는 다시 수정된다. 이처럼 하나의 변경이 여러 서비스 코드 수정으로 이어지는 구조는 확장성과 유지보수성 측면에서 취약하다. 특히 서비스가 많아질수록 A-Service는 점점 비대해지고 변경에 민감한 구조가 된다.
이 구조의 핵심 문제는 강한 결합이다. 삭제라는 하나의 행위가 구체적인 서비스 구현과 강하게 묶여 있으며, 새로운 요구사항이 들어올 때마다 상위 로직이 함께 변경된다. 이러한 문제를 해결하기 위해 이벤트 기반 구조가 사용된다.
Spring-Event 이벤트 발행
이벤트 기반 구조에서는 A-Service가 더 이상 B-Service, C-Service를 직접 호출하지 않는다. 대신 삭제 이벤트를 발행한다. 이 이벤트는 “삭제가 발생했다”라는 사실만을 전달하며, 어떤 서비스가 이를 처리할지는 알지 못한다. A-Service는 오직 이벤트 발행 책임만 가진다.
Spring에서는 ApplicationEvent를 통해 이벤트 모델을 제공한다. 이벤트는 하나의 메시지 상자 역할을 하며, 발행자와 수신자는 이 상자를 통해 간접적으로 연결된다. 이벤트 객체에는 발생 시점, 출처 객체, 고유 식별 정보 등이 포함된다.
Spring Application에서 이벤트를 사용하려면 ApplicationEvent를 상속한 클래스를 정의한다. 아래는 간단한 커스텀 이벤트 예시다.
public class MyCustomEvent extends ApplicationEvent {
private final String message;
public MyCustomEvent(Object source, String message) {
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
}
이벤트 발행은 ApplicationEventPublisher를 통해 이루어진다. 스프링 컨텍스트는 기본 구현체를 제공하므로 일반적으로 직접 구현하지 않고 주입받아 사용하는 방식이 권장된다.
Spring-Event 이벤트 수신
이벤트 수신자는 발행된 이벤트를 받아 자신의 책임에 맞는 작업을 수행한다. 중요한 점은 수신자는 발행자를 알 필요가 없다는 것이다. 오직 이벤트 타입만을 기준으로 동작한다.
Spring에서는 @EventListener 어노테이션을 통해 간단하게 이벤트 수신자를 구현할 수 있다. 특정 이벤트 타입을 지정하거나, 조건을 걸어 선택적으로 처리할 수도 있다. ApplicationListener를 구현하는 방식도 존재하지만, 가독성과 단순함 측면에서 어노테이션 방식이 일반적으로 선호된다.
아래는 이벤트 수신자의 예시 코드다.
@Component
public class MyCustomEventListener {
@EventListener(MyCustomEvent.class)
public void handleMyCustomEvent(MyCustomEvent event) {
System.out.println("Received event: " + event.getMessage());
}
@EventListener(condition = "#event.message == 'SpecialMessage'")
public void handleEventWithCondition(MyCustomEvent event) {
System.out.println("Special event received");
}
}
이 구조의 장점은 명확하다. 새로운 요구사항이 추가되어도 기존 발행자 코드는 변경되지 않는다. 새로운 수신자만 추가하면 된다. 이벤트 기반 구조는 서비스 간 결합도를 낮추고, 확장에 유연한 아키텍처를 구성하는 데 중요한 역할을 한다.