기록

Spring/이벤트 처리 예제 - ApplicationEvent, 모듈화와 확장성을 향상시키는 방법 본문

Web/Spring

Spring/이벤트 처리 예제 - ApplicationEvent, 모듈화와 확장성을 향상시키는 방법

youngyin 2024. 12. 16. 00:00

스프링 이벤트 처리 예제 및 코드 구현

시작하면서

이 프로젝트는 오픈소스를 분석하다가 각 모듈들이 이벤트를 통해 통신하는 것을 발견한 것이 계기가 되었습니다. 처음에는 이벤트 기반 통신이 어떻게 이루어지는지 이해하기 어려웠지만, 이를 제대로 익히기 위해 직접 샘플 프로젝트를 만들어보기로 했습니다. 스프링의 이벤트(Event) 기능은 애플리케이션의 모듈화와 확장성을 높이는 데 큰 도움이 되며, 이번 글에서는 간단한 이벤트 처리 프로젝트를 통해 이벤트 구현, 발행(Publish), 리스너(Listener) 설정 방법을 단계별로 익혀보겠습니다. 또한 이러한 이벤트 기반 아키텍처의 장점과 활용 방법에 대해서도 함께 논의해 보겠습니다.

1. 프로젝트 개요 및 목표

이번 프로젝트에서는 두 가지 사용자 정의 이벤트(FirstEvent, SecondEvent)를 생성하고, 이를 발행하는 CustomEventPublisher와 처리하는 CustomEventListener를 구현해 보겠습니다. 또한, 이벤트 발행을 자동화하기 위해 스케줄러(EventScheduler)를 추가하여 스프링의 이벤트 시스템이 어떻게 동작하는지 살펴보고, 이벤트 기반 모듈 분리와 비동기 처리의 장점을 체험해 보겠습니다.

 

프로젝트의 구성은 아래와 같습니다. (프로젝트 전체 소스 : GitHub 링크)

2. 코드 구성 및 설명

이제 코드 구현을 단계별로 설명하겠습니다. 각 부분은 독립적이면서도 서로 연관되어 이벤트 기반 아키텍처를 완성합니다.

(1) 이벤트 정의하기

먼저 FirstEventSecondEvent라는 두 가지 사용자 정의 이벤트를 만들어 보았습니다. 이들은 ApplicationEvent 클래스를 상속받아 구현되었습니다. 각 이벤트는 메시지와 생성 시간을 속성으로 가집니다.

FirstEvent.java

@Getter
public class FirstEvent extends ApplicationEvent {
    private final String message;
    private final LocalDateTime createdAt;

    public FirstEvent(Object source, String message) {
        super(source);
        this.message = message;
        this.createdAt = LocalDateTime.now();
    }
}

SecondEvent.java

@Getter
public class SecondEvent extends ApplicationEvent {
    private final String message;
    private final LocalDateTime createdAt;

    public SecondEvent(Object source, String message) {
        super(source);
        this.message = message;
        this.createdAt = LocalDateTime.now();
    }
}

(2) 이벤트 발행 로직 구현하기

이벤트를 발행하기 위한 CustomEventPublisher 클래스를 구현해 보았습니다. 이 클래스는 ApplicationEventPublisher를 사용해 정의된 이벤트들을 발행합니다.

CustomEventPublisher.java

@Component
@AllArgsConstructor
public class CustomEventPublisher {
    private final ApplicationEventPublisher eventPublisher;

    public void publishEvent(ApplicationEvent event) {
        if (event instanceof FirstEvent) {
            System.out.println("Publishing first event with message: " + ((FirstEvent) event).getMessage());
            eventPublisher.publishEvent(event);
        } else if (event instanceof SecondEvent) {
            System.out.println("Publishing second event with message: " + ((SecondEvent) event).getMessage());
            eventPublisher.publishEvent(event);
        } else {
            throw new RuntimeException("지원하지 않는 이벤트입니다.");
        }
    }
}

(3) 이벤트 리스너 설정하기

이제 발행된 이벤트를 처리하는 리스너를 구현합니다. CustomEventListener 클래스는 @EventListener 어노테이션을 통해 각 이벤트에 대한 처리를 정의합니다.

CustomEventListener.java

@Component
@Getter
public class CustomEventListener {
    private String message;

    @EventListener(FirstEvent.class)
    public void handleFirstEvent(ApplicationEvent event) {
        message = ((FirstEvent) event).getMessage();
        System.out.println("Received first event with message: " + message);
    }

    @EventListener(SecondEvent.class)
    public void handleSecondEvent(ApplicationEvent event) {
        message = ((SecondEvent) event).getMessage();
        System.out.println("Received second event with message: " + message);
    }
}

(4) 자동 이벤트 발행을 위한 스케줄러 구현하기

정기적으로 이벤트를 발행하기 위해 EventScheduler 클래스를 구현했습니다. 스케줄링을 통해 각각 1초와 2초마다 이벤트를 발행합니다.

EventScheduler.java

@Component
public class EventScheduler {
    private final CustomEventPublisher customEventPublisher;
    private int executionCountFirstEvent = 0;
    private int executionCountSecondEvent = 0;

    @Scheduled(fixedRate = 1000) // 1초마다 실행
    public void publishEvents01() {
        if (executionCountFirstEvent < 3) {
            customEventPublisher.publishEvent(new FirstEvent(this, "Hello, My First Event!"));
            executionCountFirstEvent++;
        }
    }

    @Scheduled(fixedRate = 2000) // 2초마다 실행
    public void publishEvents02() {
        if (executionCountSecondEvent < 2) {
            customEventPublisher.publishEvent(new SecondEvent(this, "Hello, Second Event!"));
            executionCountSecondEvent++;
        }
    }
}

실행 결과 및 이벤트 기반 아키텍처의 활용

3. 프로젝트 실행 결과

애플리케이션을 실행하면 다음과 같은 로그가 출력됩니다:

  • FirstEventSecondEventCustomEventPublisher를 통해 발행되고, CustomEventListener에서 처리됩니다.
  • 또한, EventScheduler에 의해 주기적으로 이벤트가 발행되어 리스너에서 처리됩니다.

 

이렇게 하면 이벤트가 발행되고 리스너에서 처리되는 과정을 쉽게 확인할 수 있습니다. 이를 통해 모듈 간 강한 의존성을 줄이고, 비동기적으로 처리하는 구조의 장점을 경험할 수 있습니다.

4. 이벤트 처리 방법

이벤트 기반 처리는 여러 가지 방법으로 구현할 수 있으며, 각각의 방법은 다양한 장점을 제공합니다. 여기서는 스프링의 이벤트 처리 외에 일반적으로 사용되는 다른 이벤트 기반 처리 방법들도 함께 소개합니다:

  1. 스프링 이벤트 시스템: 스프링에서 제공하는 ApplicationEvent@EventListener를 이용한 이벤트 처리 방식으로, 애플리케이션 내부의 모듈 간 통신을 간편하게 만들어 줍니다. 주로 모듈 간의 결합도를 낮추고 비동기적인 처리를 위해 사용됩니다.
  2. 메시지 브로커 사용 (예: RabbitMQ, Kafka): 메시지 브로커를 통해 이벤트를 발행하고 구독하는 방식으로, 주로 마이크로서비스 환경에서 사용됩니다. 이 방식은 서비스 간의 결합도를 줄이고, 대규모 시스템에서 이벤트를 확장 가능하게 만듭니다. 예를 들어, Apache Kafka는 대용량의 실시간 데이터 스트리밍을 처리하는 데 자주 사용됩니다.
  3. 옵저버 패턴: 옵저버 패턴은 객체 지향 디자인 패턴 중 하나로, 한 객체의 상태 변화가 다른 객체들에 의해 관찰되고 반응하도록 하는 방식입니다. 이는 주로 UI 개발이나 간단한 애플리케이션 내의 이벤트 처리를 위해 사용됩니다.

5. 이벤트 기반 처리의 장점

이벤트 기반 처리는 다음과 같은 장점을 제공합니다:

  1. 모듈화: 이벤트를 발행하는 클래스와 이벤트를 처리하는 클래스 사이의 의존성을 낮출 수 있습니다. 이는 유지보수를 쉽게 하고 애플리케이션의 구조를 좀 더 유연하게 만들어 줍니다.
  2. 확장성: 새로운 이벤트를 추가하거나 기존 이벤트 처리 로직을 수정할 때 기존 코드에 최소한의 변경만으로 쉽게 확장할 수 있습니다. 새로운 리스너를 추가하는 방식으로 기능을 확장할 수 있기 때문에, 코드의 변경에 따른 리스크를 줄일 수 있습니다.
  3. 비동기 처리: 스프링의 이벤트 시스템은 비동기 처리를 지원하여 주요 비즈니스 로직의 흐름을 방해하지 않으면서도 별도의 작업을 처리할 수 있습니다. 이는 시스템의 반응 속도를 높여 사용자 경험을 개선할 수 있습니다.
  4. 재사용성 및 책임 분리: 특정 이벤트가 발생했을 때 여러 리스너가 다양한 작업을 수행하도록 함으로써, 각 리스너는 자신만의 역할을 책임지고 독립적으로 동작할 수 있습니다. 예를 들어, 사용자 등록 이벤트가 발생하면 이메일 알림, 데이터 로깅, 분석 서비스 통지 등을 각각의 리스너가 담당할 수 있습니다.

이벤트 기반 아키텍처는 마이크로서비스 환경에서도 많이 사용됩니다. 서로 다른 서비스들이 이벤트를 통해 통신하게 되면 서비스 간의 결합도를 줄이고, 독립적인 배포와 확장이 가능합니다. 또한 도메인 주도 설계(DDD)에서 도메인 이벤트를 활용해 비즈니스 로직과 애플리케이션 로직을 분리할 수 있어 더욱 깔끔한 구조를 유지할 수 있습니다.

Comments