SpringEvent / EDA

2025. 6. 24. 21:58·SpringBoot

이벤트 기반 아키텍처(Event Driven Architecture) 를 IT 쪽을 학습하는 사람이라면 한번쯤은 들어보셨을 겁니다.

그런데 이를 어떠한 상황에서 사용하는 것이 좋고, 도입할 때 고려해야할 부분은 무엇인지, 장단점은 무엇인지와 실제 사용방법을 코드를 통해 알아보도록 하겠습니다.

 

1. EDA란 ?

우선 위에 작성한 것처럼 번역은 이벤트 기반 아키텍처를 의미합니다. 이벤트를 중심으로 시스템이 동작하는 아키텍처를 의미합니다. ( 이벤트란 상태가 변하는 것을 의미합니다. )

 

이 아키텍처를 서비스에 실적용을 많이 합니다. 그이유는 이벤트를 기반으로 동작하기 때문에 도메인간 결합도 혹은 생산자와 소비자의 결합도가 낮아지기 때문입니다. 

 

분리된 아키텍처란 왜 중요할까요?

➡️ 분리하여 결합도 / 의존도를 떨어뜨림으로써 확장성 / 유지보수성 / 생산성이 좋아집니다.

 

물론 소수의 인원으로 개발한다면 오히려 떨어질 수도 있겠지만 체계적인 환경의 개발팀이라면 더 올라갈 가능성이 높습니다.

 

EDA 아키텍처 자체는 MSA 환경과 같이 독립적으로 분리된 서비스를 느슨하게 결합함으로써 확장성 / 생산성 / 유지보수성을 잡는 것에 더 많은 의미가 있다고 생각합니다.

 

물론 제가 오늘 작성하는 코드예시와 추가적인 내용은 EDA 아키텍처 자체보다는, EDA 아키텍처에 포함되어 있는 SpringEvent를 활용하고, 왜 적용하는지 등등에 관한 이야기를 작성할 예정입니다.

 

하지만 상태변화를 나타내는 "이벤트" 자체는 Spring 내부에서도 사용할 수 있습니다. 물론 범위가 완전히 다릅니다. EDA 아키텍처 자체는 서비스 간 적용하는 아키텍처이며 SpringEvent는 스프링 내부에서 사용됩니다. 

 


2. SpringEvent

Spring이 제공하는 이벤트 시스템입니다. A클래스에서 이벤트를 발행하고, B클래스에서 이벤트를 리스닝할 수 있게 합니다.

이 과정에서 SpringEvent 또한 느슨한 결합을 가질 수 있게 합니다. 어떻게 처리되길래 느슨한 결합을 가짐으로써 확장성을 상승시킬 수 있는지 알아보겠습니다.

 

우선 이벤트를 생성합니다.

@Getter
@AllArgsConstructor
public class OrderCreateEvent {

    private Long userId;
    private int totalPrice;
}

 

사실 이벤트라 부르기도 뭐한 기본적인 클래스입니다.

 

클래스명을 보시면 알겠지만 주문할 때 부가적인 요소들을 처리할 건데 그중 하나의 이벤트입니다.

글을 쓰며 생각해보니 명칭을 바꾸고 여러 이벤트를 나누거나(아직 하나만 추가했습니다), 명칭을 그대로 하고 내부에 들어가는 속성을 추가하거나 둘 중 하나의 방법으로 변경해야 할 것 같네요

 

이벤트가 있으니 이벤트를 핸들링할 수 있는 리스너가 필요할 겁니다.

@Component
@RequiredArgsConstructor
public class PointHistoryListener {

    private final PointHistoryRepository repository;

    @EventListener
    @Transactional
    public void handleOrderCreateEvent(OrderCreateEvent event) {

        int changeAmount = (int) (event.getTotalPrice() * 0.1);

        PointHistory pointHistory = PointHistory.builder()
            .userId(event.getUserId())
            .changeAmount(changeAmount)
            .reason("물품 구매")
            .build();

        repository.save(pointHistory);
    }
}

 

리스너 클래스에 @Component가 적용되어 빈으로 등록되어있습니다. 그 이유는 SpringEvent는 SpringContainer에 등록된 빈만 관리하기 때문입니다.

 

내부 메서드를 확인해보면 @EventListener가 있습니다. 이는 이벤트 리스너를 등록하는 실제 메서드임을 나타내고, 빈으로 등록된 클래스들 중 ApplicationEventPublisher가 이벤트를 발행할 때 검사하여 사용할 수 있게 합니다.

 

이제 리스너까지 구현했으니 이벤트 발행을 시켜보겠습니다.

 

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final UserRepository userRepository;
    private final ApplicationEventPublisher applicationEventPublisher;

    @Transactional
    public OrderResponse createOrder(OrderRequest orderRequest, Long userId) {

        Users user = userRepository.findById(userId)
            .orElseThrow(() -> new RuntimeException("User not found"));

        int totalPrice = orderRequest.getOrderDetails().stream()
            .mapToInt(OrderDetailRequest::getPrice)
            .sum();

        Orders order = Orders.builder()
            .user(user)
            .totalPrice(totalPrice)
            .usedPoint(orderRequest.getUsedPoint())
            .build();

        Orders savedOrder = orderRepository.save(order);
        user.usePoint(orderRequest.getUsedPoint());

        applicationEventPublisher.publishEvent(new OrderCreateEvent(
            userId, savedOrder.getTotalPrice()
        ));

        return OrderResponse.of(savedOrder);
    }
}

 

내부적으로 User와 Orders 는 이벤트를 사용하지 않습니다. Order을 중심으로 그에 따른 아래에 있는 행위들을 이벤트로 등록할 예정입니다. 포인트를 등록하는 이벤트만 등록이 되어있으며 전체 개수에서 빼는것 등을 추가하여 연습할 예정입니다.

 

중요 부분은 저장을 전부 하고 하단에 있는

applicationEventPublisher.publishEvent(new OrderCreateEvent(
            userId, savedOrder.getTotalPrice()
));

 

이벤트를 발행하는 코드입니다.

 

확인해보면 맨처음 위에서 Event를 생성했던 OrderCreateEvent class만 존재하고 리스너는 따로 명시되어 있지 않습니다. 이전에 작성한 것처럼 @EventListener을 통해 스프링이 자동으로 검사하기 때문입니다. 물론 @TransactionalEventListener 또한 검사합니다.

 

이런 로직을 거치면 간단하게 이벤트 등록을 하여 정상적으로 비즈니스 로직이 실행됩니다.

 

SpringEvent를 쓸 때의 코드 또한 중요한 부분이지만 어째서 사용하고, 장단점은 무엇일까요?

코드는 알아봤으니 나머지를 한번 생각해보겠습니다.

 

제가 Order class를 만든 이유는 로직이 추가될 수 있는 부분이 많기 때문입니다. 실제로는 결제, 포인트, 리뷰 등 수많은 로직이 "주문" 이라는 하나의 로직 안에 들어올 수 있을 겁니다.

 

처음 작성할 때는 문제가 없습니다만 추후 유지보수하는 상황이 발생했을 때 연관되어 있는 로직들이 의존성을 가지고 있기 때문에 유지보수성이 많이 떨어지게 됩니다. 또한 만약 도메인을 담당하는 팀이 있고 나는 오더 팀에 속해 있는데 타 부분에서 문제 상황이 발생해서 롤백이 계속 되는 상황이 발생했을 때, 이는 수정 요청을 하고 또 응답을 받는 등 부가적인 절차가 추가될 것입니다.

 

하지만 이런 SpringEvent를 사용했을 때 이벤트 / 리스너를 미리 만들고 서비스에서 그저 사용만 하는 것이기 때문에 유지보수성이 많이 향상될 수 있을 겁니다.

또한 결합도가 낮아지기에 Join 또한 줄일 수 있습니다. 위 리스너 내부 PointHistory를 생성하는 빌더 코드를 다시 보겠습니다.

 

    @EventListener
    @Transactional
    public void handleOrderCreateEvent(OrderCreateEvent event) {

        int changeAmount = (int) (event.getTotalPrice() * 0.1);

        PointHistory pointHistory = PointHistory.builder()
            .userId(event.getUserId())
            .changeAmount(changeAmount)
            .reason("물품 구매")
            .build();

        repository.save(pointHistory);
    }

 

기존 JPA 코드와 한 부분이 다릅니다. 바로 userId가 직접적으로 들어간 부분입니다. 기본적으로 연관관계가 있다면 검증을 명확하게 하기 위해서 받아온 userId를 통해서 따로 User를 조회후 넣어주는 식으로 query문이 한번 더 들어가야 했을 것입니다. 

 

사실 이런 단순한 쿼리문 하나이지만 시스템이 커지고 MSA 환경에서는 분리되어 있는 타 서비스에 요청이 한번 줄어드는 것이라고 생각할 수도 있습니다. 분리되어 있다는 것은 사용자가 많다는 것을 암시할 수도 있으며 이는 한두번 요청을 줄이는 것이라고 해도 굉장히 많은 수를 실제로 줄이는 것이라고 생각할 수 있을 것 같습니다.

 

물론 장점만 있는 것은 아닙니다. 프로젝트 규모가 작은데 굳이 이벤트 리스너를 사용한다? 이는 오버엔지니어링이 될 수도 있습니다. 쿼리문을 줄인다는 것만 보고는 만약 팀에 이벤트를 모르는 인원이 있다면 그에 대한 시간이 소모되는 것이고 오히려 시간이 낭비될 수도 있을 겁니다. 

또한 @EventListener 뿐 아니라 @TransactionalEventListener 와 같이 리스너가 존재하고 특히나 @TransactionalEventListener에는 다양한 속성을 줄 수 있는데 이 부분이 잘못 코딩될 가능성 또한 있습니다. 그로 인해 오류가 발생한다면 기존 모놀리스 환경에서 에러를 눈으로 곧바로 확인할 수 있었지만 이벤트는 호출과 분리되어 있기 때문에 예외가 전파되지 않기 때문입니다.

따라서 어중간하게 썼다가는 굉장한 시간 소모가 들 수 있다는 생각이 듭니다. 테스트 코드를 필수적으로 많은 테스트케이스를 두고 진행해야겠다는 생각이 듭니다.

 

기존 개발 방식 혹은 이런 최근 많이 사용되는 아키텍처 방식을 사용한 SpringEvent 방식 또한 충분히 장점이 많지만 상황에 따라 잘 사용해야 한다는 생각입니다.

 

 

 

'SpringBoot' 카테고리의 다른 글

SpringBoot 대용량 데이터 조회 (Index)  (1) 2025.07.03
AOP 탐구  (0) 2025.07.01
JPA 영속성 파헤치기  (0) 2025.06.28
Spring 예외처리 (@ExceptionHandler, @ControllerAdvice)  (0) 2025.05.12
Spring Security 없이 로그인을 하는 방법 (Session)  (0) 2025.05.07
'SpringBoot' 카테고리의 다른 글
  • AOP 탐구
  • JPA 영속성 파헤치기
  • Spring 예외처리 (@ExceptionHandler, @ControllerAdvice)
  • Spring Security 없이 로그인을 하는 방법 (Session)
vanc
vanc
공부 기록 블로그입니다.
  • vanc
    Vanc
    vanc
  • 전체
    오늘
    어제
    • 분류 전체보기 (29) N
      • Project (0)
      • SpringBoot (8)
      • Java (0)
      • 백준 (0)
      • 부트캠프 (0)
  • 블로그 메뉴

    • 홈
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    @ControllerAdvice
    jdbc
    @Enumerated
    REQUIRES_NEW
    @ExceptionHandler
    spring data elasticsearch 8
    @ResponseStatus
    N+1
    정합성
    db
    @entitygraph
    Transactional
    fetch join
    @converter
    @BatchSize
    일정관리
    springboot
    동시성이슈
    JPA
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
vanc
SpringEvent / EDA
상단으로

티스토리툴바