적용 예시 (회원가입)
회원가입 후 DB 회원정보 저장
회원정보로 받은 Email에 회원가입 메일 전송
여기서 제가 진행 중인 MSA 프로젝트에서는 회원 도메인과 이메일 Notificaion 도메인이 분리가 되어 있어 회원 서비스에서는 회원 가입만 수행하고
이메일 전송은 notification 서비스에서 수행합니다.
이 과정을 통해 간단한 코드를 작성하자면
@Slf4j
@RequiredArgsConstructor
@Transactional
@Service
public class AuthService {
private final AccountRepository accountRepository;
private final NotificationSNSPublisher notificationSNSPublisher;
public void join(JoinRequest request) {
log.info("join start");
Account account = Account.create(request);
accountRepository.save(account);
log.info("account save");
// send email
notificationSNSPublisher.sendEmail(request.getEmail());
log.info("join end");
}
}
해당 Service 는 request 로 회원 정보를 입력받고 DB에 회원정보 저장 후 가입회원에게 email 전송하는 간단한 비즈니스 로직입니다.
email을 전송하는 notificaion 서비스와 통신은 비동기 message queue 를 통해 진행하고 있습니다.
해당 join 메소드에 대해 테스트 코드를 작성하고 진행하면 다음과 같은 log 가 출력됩니다.
[nio-8002-exec-3] com.authserver.service.AuthService : join start
Hibernate:
/* insert for
com.authserver.entity.Account */insert
into
account (name,password,user_id)
values
(?,?,?)
[nio-8002-exec-3] com.authserver.service.AuthService : account save
[nio-8002-exec-3] com.authserver.service.AuthService : email listener start
[nio-8002-exec-3] com.authserver.service.AuthService : email send
[nio-8002-exec-3] com.authserver.service.AuthService : email listener end
[nio-8002-exec-3] com.authserver.service.AuthService : join end
여기서 join 메소드는 문제점이 있습니다.
1. 해당 프로젝트는 MSA 구조로 작성되어 Notification 서비스는 회원 서비스와 분리되어있습니다.
회원 서비스는 도메인에 맞게 회원가입 로직만 수행하고 회원가입 이메일 전송은 Notificaion 서비스에서 회원가입한 회원의 이메일 정보만
message queue AWS SNS, SQS 를 통해 전송합니다
따라서 join 메소드를 실행하면 Transaction이 적용되는 범위에는 실제 이메일이 전송되는 비즈니스 로직이 포함되어 있지않습니다.
만약에라도 Account Entity를 생성하는 코드나 DB에 저장하는 도중 Exception이 발생한다면 이메일 전송도 Transaction에 적용되야 합니다.
이 문제를 해결하기 위해 Spring Event에서 제공하는 TransactionalEventListener 를 적용하려고 합니다.
@Slf4j
@RequiredArgsConstructor
@Transactional
@Service
public class AuthService {
private final AccountRepository accountRepository;
private final ApplicationEventPublisher applicationEventPublisher;
public void join(JoinRequest request) {
log.info("join start");
Account account = Account.create(request);
accountRepository.save(account);
log.info("account save");
// send join Email
applicationEventPublisher.publishEvent(request);
log.info("join end");
}
}
ApplicationEventPublisher 를 통해 해당 이벤트를 발행시킵니다.
이벤트를 발생하는 방법은 간단하게 publishEvent 메소드만 호출하면됩니다.
publishEvent에 파라미터로 전달된 객체를 받는 Listener를 구현해야 합니다.
@Slf4j
@Component
public class EventHandler {
private final NotificaionSNSPublisher notificaionSNSPublisher;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendEmail(JoinRequest request){
log.info("email listener start");
log.info("email send");
notificaionSNSPublisher.send(request.getEmail());
log.info("email listener end");
}
}
EventHandler 를 @Component 적용하여 Bean으로 등록하고
실제 Listener에는 @TransactionalEventListener 작성합니다. @TransactionalEventListener 옵션으로 phase를 설정해야 하는데
4가지 BEFORE_COMMIT, AFTER_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION 가 존재합니다.
저는 여기서 AFTER_COMMIT 을 적용하겠습니다. 위 phase 작동원리는 따로 찾아보시길 바랍니다.
AFTER_COMMIT 의 작동원리는 이벤트를 발생한 트렌젝션이 commit이 된 후 해당 이벤트 리스너가 실행되는 원리입니다.
Event를 적용시킨 후 service 테스트 코드를 실행해보겠습니다.
[nio-8002-exec-3] com.authserver.service.AuthService : join start
Hibernate:
/* insert for
com.authserver.entity.Account */insert
into
account (name,password,user_id)
values
(?,?,?)
[nio-8002-exec-3] com.authserver.service.AuthService : account save
[nio-8002-exec-3] com.authserver.service.AuthService : join end
[nio-8002-exec-3] com.authserver.service.AuthService : email listener start
[nio-8002-exec-3] com.authserver.service.AuthService : email send
[nio-8002-exec-3] com.authserver.service.AuthService : email listener end
아까와는 다른 log가 출력됩니다.
join 메소드 commit이 이루어진 후에 listener log가 출력됩니다.
join 메소드가 정상적으로 수행되지 않고 exception이 발생된다면 listener 또한 실행되지 않습니다.
이로서 문제되었던 Transaction이 적용되어 join 메소드와 listener 가 하나의 Transaction으로 묶여 작동하게됩니다.
!!!!! 하지만 또하나의 문제점이 있습니다.
join 메소드가 정상적으로 commit이 되면 DB에는 회원정보가 정상적으로 저장되고 listener가 수행된다고 했습니다.
만약 listener에서 exception이 발생된다면 어떻게 될까요...?
앞서 말씀드렸겠지만 회원 서비스와 알림 서비스는 분리가 되어있습니다. 따라서 알림 서비스에서 exception이 발생하거나 로직상 문제가 있어
이메일 전송이 실패했다면..
우리는 message queue 를 사용하여 서비스간 통신을 하기 때문에 SQS subscription 이 실패하는 상황에 대비해야 합니다.
다음 포스트에서는 message subscription 이 실패하는 상황에 대비하는 방법을 작성하겠습니다.
작성된 글에서 잘못된 부분이 있을경우 많은 지적바랍니다