ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Boot 고급 시리즈 7화 – 도메인 이벤트 기반 아키텍처 설계와 적용 전략
    기술과 산업/언어 및 프레임워크 2025. 6. 22. 13:23
    728x90

    애플리케이션이 점차 복잡해질수록 하나의 행위가 여러 결과를 유발하게 됩니다. 예를 들어, 회원 가입을 처리하면서 이메일 인증을 보내고, 추천인 포인트를 지급하고, 가입 로그를 저장하는 등 다양한 후속 로직이 따라붙습니다. 이 모든 것을 하나의 서비스 메서드 안에 처리한다면, 유지보수와 테스트는 점점 더 어려워집니다.

     

    이럴 때 가장 효과적인 해법은 도메인 이벤트(Domain Event) 기반으로 부수 효과를 분리하는 설계입니다.

     

     

    도메인 이벤트란 무엇인가?

     

    도메인 이벤트는 말 그대로, 도메인 내에서 발생한 의미 있는 사건을 나타냅니다. 예를 들어 “회원이 가입되었다”는 사실 자체가 하나의 이벤트입니다.

    이벤트는 명령(Command)이 아닌, 발생한 사실을 기술하는 객체입니다.

    그에 반응하는 로직은 이벤트 리스너(Event Listener)에서 처리됩니다.

     

    이 개념은 DDD(Domain Driven Design)에서 널리 사용되며, 이벤트 주도 아키텍처의 기초가 됩니다.

     


     

    Spring에서 도메인 이벤트 구현 방식

     

    Spring은 도메인 이벤트를 매우 간결하게 구현할 수 있는 메커니즘을 제공합니다.

     

     

    1. 이벤트 클래스 정의

    public class MemberJoinedEvent {
        private final Long memberId;
        private final LocalDateTime joinedAt;
    
        public MemberJoinedEvent(Long memberId, LocalDateTime joinedAt) {
            this.memberId = memberId;
            this.joinedAt = joinedAt;
        }
    
        // getter 생략
    }

     

    2. 이벤트 퍼블리싱

    @Service
    @RequiredArgsConstructor
    public class MemberService {
    
        private final ApplicationEventPublisher publisher;
    
        @Transactional
        public void join(MemberJoinRequest request) {
            Member member = memberRepository.save(request.toEntity());
            publisher.publishEvent(new MemberJoinedEvent(member.getId(), LocalDateTime.now()));
        }
    }

     

    3. 이벤트 리스너 정의

    @Component
    public class MemberEventHandler {
    
        @EventListener
        public void handle(MemberJoinedEvent event) {
            emailService.sendWelcomeMail(event.getMemberId());
            pointService.giveSignupBonus(event.getMemberId());
        }
    }

    이렇게 구성하면 핵심 도메인 로직(회원 가입)과 부수 로직(이메일 발송, 포인트 지급 등)이 분리됩니다.

     


     

    동기 vs 비동기 처리 전략

     

    이벤트 리스너는 기본적으로 동기적으로 실행됩니다. 즉, 퍼블리싱한 서비스 메서드 내에서 이벤트 리스너가 모두 실행될 때까지 대기합니다.

    필요에 따라 다음과 같이 비동기로 분리할 수 있습니다.

    @Async
    @EventListener
    public void handle(MemberJoinedEvent event) {
        // 비동기 실행
    }

    주의할 점은 @Async가 제대로 작동하려면 아래 조건이 충족되어야 한다는 것입니다:

     

    • @EnableAsync 선언 필요
    • 비동기 메서드는 반드시 별도 빈에서 호출되어야 함 (자기 자신 호출 불가)
    • 트랜잭션 완료 이후 이벤트 실행을 원한다면 @TransactionalEventListener(phase = AFTER_COMMIT) 사용

     


     

    트랜잭션 이후 이벤트 처리

     

    도메인 이벤트가 트랜잭션 안에서 퍼블리싱되면, 롤백 시 이벤트 리스너도 실행되었다는 문제가 생깁니다.

    이때는 반드시 다음처럼 명시적으로 트랜잭션 종료 후 이벤트 리스너를 실행하도록 설정해야 합니다.

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void afterCommit(MemberJoinedEvent event) {
        // 트랜잭션 커밋 이후 실행
    }

    이를 통해 예외 상황에서도 이벤트 발송으로 인한 부작용을 완전히 방지할 수 있습니다.

     


     

    도메인 이벤트의 장점과 한계

    장점설명

    결합도 감소 서비스 로직과 후속 로직이 느슨하게 연결됨
    테스트 용이성 핵심 로직만 테스트 가능, 부수 로직 분리
    확장성 향상 이벤트 리스너만 추가하면 새로운 기능 확장 가능

    한계설명

    흐름 파악 어려움 이벤트가 많아지면 호출 관계 추적 어려움
    예외 처리 어려움 리스너 내 예외가 퍼블리셔로 전파되지 않음
    디버깅 복잡성 순차 실행 보장이 없어 문제 추적이 복잡할 수 있음

     


     

    언제 도메인 이벤트를 쓰는 것이 좋은가?

     

    • 후속 로직이 필수가 아닌 선택 사항일 때 (ex. 알림, 이메일 등)
    • 하나의 도메인 이벤트에 여러 부수 기능이 붙을 가능성이 있을 때
    • 도메인 로직은 단순히 ‘사건’을 발생시키고, 구현은 관심 없어야 할 때

     

    예를 들어, 회원 가입 이후에 다양한 서비스가 확장될 가능성이 있는 경우라면 도메인 이벤트 기반으로 아키텍처를 구성해두는 것이 탁월한 전략이 됩니다.

     

    Spring의 도메인 이벤트 기능은 단순한 설계 편의를 넘어서, 유지보수성과 확장성을 근본적으로 개선해주는 설계 도구입니다.

    비즈니스의 복잡도가 점차 높아질수록 이러한 이벤트 기반 설계는 코드의 응집도를 높이고, 의도를 분리하는 데 중요한 역할을 하게 됩니다.

    728x90
Designed by Tistory.