EventLister를 활용한 느슨한 결합 및 이벤트 처리.
서론
사내에서 새로운 도메인 작업을 담당하면서 특정 로직을 실행한 뒤
후처리를 해야되는 상황에서 사용했던 방법에 대해서 공유하고자합니다.
가상 시나리오
회원 가입 -> 포인트 적립 및 이메일 발송
예제를 명시적으로 보이기위해 SignUp~, Update~Service 기능을 나타내는 네이밍을 사용했습니다.
- Before: 결합도가 강하고 좋지 않은 설계
@Getter public class User { private Long id; private String email; public User(final Long id, final String email) { this.id = id; this.email = email; } }
@Repository public class MemoryUserRepository implements UserRepository { private static final Map<Long, User> store = new ConcurrentHashMap<>(); private static final AtomicLong SEQUENCE = new AtomicLong(); @Override public User save(final User user) { if (user.getId() == null) { user.setId(SEQUENCE.incrementAndGet()); } store.put(user.getId(), user); return user; } }
다음 두 인터페이스는 밑에 SignUpUserService에서 회원가입을 진행 후 실행하는 후처리 인터페이스입니다
실제로 호출하는 행위를 검증만 할 것이기 때문에 인터페이스만 만들고 구현체는 만들지 않았습니다.
// 이메일 발송 public interface SendEmailService { void send(final String email); }
// 포인트 업데이트 public interface UpdatePointService { void rewardPoint(final Long userId); // 적립 void spendPoint(final Long userId); // 소비 }
회원가입을 처리하는 SingUpUserService

SignUpService는 회원 가입을 하고, 이메일 발송 등 후처리를 진행하는 역할을 하고있습니다.
이상한 부분이 느껴지시나요?
SignUpService 객체는 다음과 같은 문제가 있습니다.
- 결합도가 높다
-> SignUpService는 후처리 로직들을 모두 직접적으로 알고있습니다.
-> 후처리 로직들이 변경, 추가 되면 SignUpService에게도 직접적인 영향을 끼칠 수 있습니다. OCP 위반
-> 변경에 용이하지 않다. - 객체 책임이 너무 크다.
-> 회원 가입을 하는 객체인데, 후처리 (메일 발송, 포인트 적립 등)도 책임지고 있습니다.
-> SRP 위반
-> 후처리에 대한 기능들을 추가 될수록 SignUpService의 책임은 계속 커진다.
그림으로 나타내면 다음과 같습니다. (Producer가 Consumer에게 직접적으로 메세지를 보낸다)

-After: EventLister를 통한 느슨한 결합
@Service @RequiredArgsConstructor public class SignUpUserService { private final UserRepository userRepository; private final ApplicationEventPublisher eventPublisher; public void register(final String email) { final User user = userRepository.save(new User(email)); eventPublisher.publishEvent(new SignUpUserEvent(user)); // 해당하는 이벤트를 등록한 리스너들에게 알림 } }
@Getter @RequiredArgsConstructor public class SignUpUserEvent { private final User user; }
@Component @RequiredArgsConstructor public class SignUpUserEventHandler { private final SendEmailService sendEmailService; private final UpdatePointService updatePointService; @EventListener public void sendMail(final SignUpUserEvent signUpUserEvent) { final String email = signUpUserEvent.getUser().getEmail(); sendEmailService.send(email); } @EventListener public void rewardPoint(final SignUpUserEvent signUpUserEvent) { final Long userId = signUpUserEvent.getUser().getId(); updatePointService.rewardPoint(userId); } }
@Async를 이용해서 비동기 처리를 하거나
@Order를 이용해서 처리 순서를 지정하는 것도 가능합니다.
물론 실제로 이메일 발송 등 제어할 수 없는 대상을 다루거나, 후행 작업에서 오류가 발생할 어떻게 대응할지 잘 고려해봐야합니다.
위 방식은 동기적으로 로직을 수행하고 트랜잭션이 하나로 묶이기 때문에 후행 작업을 처리하다 exception이 발생하면 모두 롤백을 진행하기 때문에 주의해야합니다.
애플리케이션 구성 다이어그램은 다음과 같습니다.

이제 SignUpService는 회원 가입만하고, 그 후엔 어떤 일이 일어나는지 전혀 모릅니다. 카카오 알림톡 발송 기능이 추가되던, 이메일 발송 메서드 이름이 변경되건 SignUpService는 아무 변경이 없게되었습니다.
실제로 동작하는 테스트코드로 검증을 해보겠습니다.
@SpringBootTest class SignUpUserServiceTest { @Autowired SignUpUserService signUpUserService; @MockBean SendEmailService sendEmailService; @MockBean UpdatePointService updatePointService; @Test @DisplayName("유저를 저장하고 등록한 EventLister가 호출되는지 확인한다.") void verify_event() { final String email = "abc@naver.com"; signUpUserService.register(email); verify(sendEmailService, times(1)).send(email); verify(updatePointService, times(1)).rewardPoint(anyLong()); } }

테스트가 정상적으로 성공하는 것을 확인할 수 있습니다.
코드는 깃허브에서 확인할 수 있습니다.
brick0123/event-handler
Contribute to brick0123/event-handler development by creating an account on GitHub.
github.com
'Spring' 카테고리의 다른 글
@Async는 어떤식으로 실행될까? (0) | 2021.08.23 |
---|---|
Spring AOP self-invocation이 발생하는 이유와 @Transaction 사용시 주의사항 (0) | 2021.08.11 |
ResponseEntity는 왜 사용하는 것이며 @RestControllerAdvice는 무엇일까. (2) | 2021.05.02 |
@ConfigurationProperties를 immutable하게 설계하기 (0) | 2021.04.15 |
[Spring, OOP] 생성자 주입이 좋은 이유와 스프링을 이용한 다양한 DI (2) | 2021.02.10 |
댓글을 사용할 수 없습니다.