ApplicationEventPublisher를 활용하여 트랜잭션 강결합 문제를 해결하는 방법을 알아보겠습니다.
프로젝트를 진행하다 보면 의도치 않게 서비스끼리 강결합되는 문제가 발생합니다. 아래 예시가 있습니다.
UserService
@Service
@RequiredArgsConstructor
@Transactional
public class UserService {
private final UserRepository userRepository;
private final MailService mailService;
public Long create(UserReqDto dto) {
User user = userRepository.save(User.builder()
.email(dto.getEmail())
.password(dto.getPassword())
.build());
mailService.sendMockMail(user.getId());
return user.getId();
}
}
MailService
@Service
@Transactional
public class MailService {
public String sendMockMail(User user) {
System.out.println("메일 전송에 실패하였습니다.")
throw new CustomException();
}
}
위의 코드는 간단한 회원가입 시나리오입니다. 시나리오는 1) 회원가입이 성공하면 2) 문자 메시지를 발송하는 순서로 진행됩니다.
위 서비스에는 문제가 있습니다. 회원 가입이 성공해도 문자 메시지 발송이 실패하면 회원 가입이 취소되는 현상이 발생합니다. UserService 내 create 메서드가 트랜잭션의 영향을 받기 때문에, create 메서드 내에서 Unchecked Exception이 발생하면 create 메서드 호출 후 실행되었던 작업들이 모두 롤백됩니다. 이렇게 한 트랜잭션 내에서 특정 서비스의 행위가 다른 서비스에 영향을 주는 것을 서비스가 강결합 되어있다고 합니다.
강결합 문제를 해결하기 앞서 우선 정해야 할 것은 해당 메서드 내 서비스들이 핵심 기능인지 부가 기능인지 파악해야 합니다. 예를 들어 1) 회원 가입을 한다. 2) 웰컴 쿠폰을 발급한다. 3) 회원 가입 축하 문자 메시지를 발송한다 와 같은 순서를 가지는 회원 가입 시나리오가 있다고 가정해보겠습니다.
제 생각에는 위 시나리오에서 회원 가입을 하는 것과 웰컴 쿠폰을 발급하는 것이 핵심 기능에 해당할 것입니다. 그리고 회원 가입 축하 문자 메시지를 발송하는 것이 부가 기능일 것입니다.
핵심 기능은 서비스에 문제가 생기면 서비스 전체가 롤백 되어야 합니다. 회원 가입에 성공했어도 웰컴 쿠폰 발급이 실패한다면 회원 가입 또한 취소되어야 합니다.
부가 기능은 서비스에 문제가 생겨도 서비스 전체가 롤백 되어서는 안됩니다. 회원 가입 축하 문자 메시지가 발송 실패해도 회원 가입과 웰컴 쿠폰 발급이 롤백되어서는 안됩니다.
위와 같이 핵심 기능과 부가 기능을 나누고 난 뒤, 부가 기능을 ApplicationEventPublisher를 활용하여 처리해주면 됩니다.
ApplicationEventPublisher
본격적으로 ApplicationEventPublisher를 사용하기에 앞서 Event와 EventHandler에 대해 알아보겠습니다.
MailEvent
@Getter
@NoArgsConstructor
public class MailEvent {
private User user;
public MailEvent(User user) {
this.user = user;
}
public static MailEvent of(User user) {
System.out.println("Pass Event");
return new MailEvent(user);
}
}
Event는 이벤트를 처리하는 데이터를 포함하는 클래스 입니다.
MailEventHandler
@Component
@RequiredArgsConstructor
public class MailEventHandler {
private final MailService mailService;
@TransactionalEventListener(classes = MailEvent.class)
public void sendMail(MailEvent mailEvent) {
System.out.println("Pass EventHandler");
mailService.sendMockMail(mailEvent.getUser());
}
}
EventHandler는 생성된 Event에 따라 어떤 EventListener가 수신하여 처리할지 정해주는 클래스입니다. EventListener는 @EventListener와 @TransactionalEventListener 두 종류가 있습니다.
@EventListener
@EventListener는 이후 결과와 상관없이 호출 시점에 바로 실행됩니다. 이 방법은 트랜잭션의 처리 상태와 상관없이 동작하기 때문에 이벤트 처리후 트랜잭션 rollback 이 발생해도 이벤트는 rollback 되지 않는 문제를 가지고 있습니다.
@TransactionalEventListener
위의 문제를 해결하기 위해 등장한 것이 @TransactionalEventListener입니다. @TransactionalEventListener는 트랜잭션의 특정 시점에 실행됩니다. 기본값으로 트랜잭션이 commit 되면 이벤트가 실행됩니다. 다른 @TransactionalEventListener 옵션은 다음과 같습니다.
- AFTER_COMMIT - default 값으로 트랜잭션이 commit 되었을 때 이벤트를 실행합니다.
- AFTER_ROLLBACK – 트랜잭션이 rollback 되었을 때 이벤트를 실행합니다.
- AFTER_COMPLETION – 트랜잭션이 commit 또는 rollback 되었을 때 이벤트를 실행합니다.
- BEFORE_COMMIT - 트랜잭션이 commit 되기 전에 이벤트를 실행합니다.
UserService
@Service
@RequiredArgsConstructor
@Transactional
public class UserService {
private final UserRepository userRepository;
private final ApplicationEventPublisher applicationEventPublisher;
public Long create(UserReqDto dto) {
User user = userRepository.save(User.builder()
.email(dto.getEmail())
.password(dto.getPassword())
.build());
applicationEventPublisher.publishEvent(MailEvent.of(user));
return user.getId();
}
}
위와 같이 설정 후 mailEvent를 호출하면 mailEventHandler내 해당하는 eventListner가 이를 감지하여 mailService를 실행시킵니다.
실행 결과는 이전과 다르게 메일 전송 실패와 상관없이 회원 가입이 정상적으로 된 것을 확인할 수 있습니다.
@Aysnc
AsyncConfig
@Configuration
@EnableAsync
public class AsyncConfig {
}
MailEventHandler
@Component
@RequiredArgsConstructor
public class MailEventHandler {
private final MailService mailService;
@Async
@TransactionalEventListener(classes = MailEvent.class)
public void sendMail(MailEvent mailEvent) {
System.out.println("Pass EventHandler");
mailService.sendMockMail(mailEvent.getUser());
}
}
ApplicationEvent는 이벤트 드리븐 방식이기 떄문에 비동기로 처리될 것 같지만 사실 동기식으로 처리됩니다. 이에 위와 같이 리스너가 비동기로 동작하게 만들어 성능을 향상시킬 수 있습니다. 자세한 것은 정 아마추어님의 "Spring ApplicationEvent 비동기로 처리될 것만 같지?" (https://jeong-pro.tistory.com/238) 글을 읽어 보시면 좋을 것 같습니다.
참고
- https://velog.io/@znftm97/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EA%B8%B0%EB%B0%98-%EC%84%9C%EB%B9%84%EC%8A%A4%EA%B0%84-%EA%B0%95%EA%B2%B0%ED%95%A9-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-ApplicationEventPublisher
- https://backtony.github.io/spring/2022-06-06-spring-event/#%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%84-%EA%B0%95%EA%B2%B0%ED%95%A9-%EB%AC%B8%EC%A0%9C
- https://cheese10yun.github.io/event-transaction/
- https://jeong-pro.tistory.com/238
'BE > Spring' 카테고리의 다른 글
Spring에서 AWS RDS MySQL Replication 적용하기 (0) | 2022.11.29 |
---|---|
스프링 S3 연동 오류: No valid instance id defined (0) | 2022.07.27 |
본인 확인은 어떤 layer에서 이루어져야 할까? (0) | 2022.07.22 |
yaml 파일을 그룹으로 관리하기 (0) | 2022.07.09 |
Springboot에서 Redis Cache 적용하기 (0) | 2022.06.27 |
댓글