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());
}
}
테스트가 정상적으로 성공하는 것을 확인할 수 있습니다.
코드는 깃허브에서 확인할 수 있습니다.
'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 |