오늘은 트랜잭션 구현 실습을 진행했다.
사실 스프링에서 트랜잭션은 @Transactional을 사용하면 쉽게 구현할 수 있다.
그래서 실습에서 몇가지 제약 조건을 걸고 트랜잭션을 구현했다.
(Case)
회원가입을 하면 가입한 메일로 가입 완료 메일이 전송된다.
만약 가입 완료 메일이 정상적으로 전송되지 않으면 가입 과정에서 오류가 생긴 것으로 보고 회원가입 정보를 삭제하는 코드를 구현하였다.
(실제 상황이라면 메일 전송에 오류가 생기면 카톡이나 SMS를 보내거나, 혹은 회원가입을 진행 중 상태로 두는 등 여러 가지 조치방법이 있을 수 있다.)
회원 데이터 저장과 메일 전송을 *비동기적으로 처리하여 실행 시간을 단축시키는 방법을 사용해야한다.
- "동기(Synchronous) 방식"은 진행 과정이 순차적으로 수행되는 것을 말한다.
한 동작이 끝나고 응답을 받으면 다음 동작이 수행된다. - "비동기(Asynchronous) 방식"은 동작이 요청되면 그 응답과는 관계없이 다음 동작이 수행되는 방법을 말한다.
(멀티 스레드로 동시에 수행되는 방법을 생각할 수 있다.)
❯ @Async 적용
스프링에서는 @Async를 사용해 비동기 처리를 할 수 있다.
먼저, 비동기로 수행할 작업이 있는 클래스에 @EnableAsync 애너테이션을 사용해 비동기 활성화를 해주어야 한다.
(혹은 설정파일로 @EnableAsync를 추가해 주거나 애플리케이션 자체에 적용할 수도 있다.)
그리고 비동기 처리가 필요한 메서드에 @Async 애너테이션을 붙여준다.
@Component
@EnableAsync
public class EmailListener {
(...)
@Async
public void sendEmail(MemberEvent memberEvent) throws Exception{
(...)
}
}
여기에서 주의할 점은, 비동기 처리를 하게 되면 별도 스레드에서 실행되므로 기존에 진행 중인 트랜잭션이 있다면 해당 트랜잭션과는 별개로 작동한다는 것이다.
실제로 관련 코드를 실행시켜 보면 아래와 같이 다른 스레드에서 메서드가 실행되는 것을 확인할 수 있다.
❯ Spring Event 적용
스프링에서는 Event라는 기능이 있다. 이벤트 기능 쉽게 말해서 어떤 이벤트가 발생할 경우 해당 이벤트를 발행(Publish)하면 이 이벤트를 리스닝(Listening)하는 곳에서 지정된 동작을 수행하게 되는 방식을 말한다.
예를 들어, 회원 정보가 등록되었다면 회원 등록 이벤트를 Publish 한다. 그러면 이벤트 리스너가 이를 받아서 메일을 보내게 된다.
Event 기능은 Event Object, Event Listener, Event Publisher를 통해 구현된다.
Event Object
이벤트 객체가 생성되는 클래스이다. 이벤트를 발생시킬 인자를 이벤트 객체로 생성하여 이벤트 리스너에게 전달해 준다.
위 케이스에서는 Member 생성에 따른 이벤트이므로 "MemberEvent"라고 정의하였다.
@Value(staticConstructor = "of")
public class MemberEvent {
Member member;
}
Event Listener
이벤트가 발행되면 수행될 작업을 구현하는 메서드이다.
이벤트 발행을 받아서(Listen) 작업을 수행하게 된다. 메서드에 @EventListener 애너테이션을 추가한다.
아래 코드에서는 @Async를 통해 비동기적으로 수행되므로 리스너 메서드에서 예외가 발생해도 발행 메서드는 정상적으로 수행된다.
하지만 실습 내용이 리스너 메서드에서 예외 발생 시 회원정보가 저장되지 않도록 하는 것이므로 catch문에서 해당 내용을 구현했다.
@Slf4j
@Component
@EnableAsync
public class EmailListener {
private final EmailSender emailSender;
private final MemberService memberService;
public EmailListener(EmailSender emailSender, MemberService memberService) {
this.emailSender = emailSender;
this.memberService = memberService;
}
@EventListener // 이벤트가 발행되면 실행된다.
@Async
public void sendEmail(MemberEvent memberEvent) throws Exception{
try {
emailSender.sendEmail("any email message");
} catch (Exception e) {
log.error("MailSendException happened: ", e);
Member member = memberEvent.getMember();
memberService.deleteMember(member.getMemberId());
} // 이 메서드 실행중 예외가 발생하면 등록된 멤버를 삭제(rollback)하도록 구현함
}
}
EventListener 중 @TransactionalEventListener 애너테이션도 있는데, 이는 Publish해준 메서드의 트랜잭션이 커밋된 후 동작하게 되는 애너테이션이다. publish 하는 메서드에서 예외가 발생한다면 listener 메서드를 호출하지 않는다.
Event Publisher
스프링에서 ApplicationEventPublisher를 통해 이벤트를 발행(Publish)할 수 있다.
이벤트를 발행(리스너를 호출)해야 하는 시점에서 ApplicationEventPublisher의 publishEvent() 메서드를 통해 이벤트를 발행해 준다.
@Slf4j
@Transactional
@Service
public class MemberService {
private final MemberRepository memberRepository;
// publisher를 필드로 갖는다.
private final ApplicationEventPublisher publisher;
public MemberService(MemberRepository memberRepository,
ApplicationEventPublisher publisher) {
this.memberRepository = memberRepository;
this.publisher = publisher;
}
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
Member savedMember = memberRepository.save(member);
log.info("# Saved member");
// 이벤트 발행 시점에서 해당되는 이벤트를 발행해준다.(of() 메서드를 사용해 MemverEvent 생성해줌)
publisher.publishEvent(MemberEvent.of(savedMember));
return savedMember;
}
(...)
}
참고자료
Async를 이용하여 EventListener 사용하기
최대한 여러 자료들을 참고하면서 정확한 내용을 담으려고 노력했다.
그렇지만 공부를 시작한 지 두 달 조금 넘은 사람으로서.. 이게 정확한 내용인가 하는 의문이 들 때도 있다.
코드가 의도한 대로 동작해도 "이게 왜 되지?" 하고 고민에 빠지기도 한다.
언젠가는 스스로를 믿고 작업할 수 있는 때가 오기를 기대해 본다.
* 개인 공부기록입니다. 혹시 잘못된 부분이 있다면 편하게 말씀해 주세요. :)
'부트캠프 개발일기 > Spring MVC' 카테고리의 다른 글
55일차: 슬라이스 테스트 (0) | 2023.05.01 |
---|---|
54일차: 테스팅(단위테스트, JUnit) (0) | 2023.04.28 |
52일차: Transaction(트랜잭션) (0) | 2023.04.26 |
51일차: Spring Data JPA(데이터 액세스 계층 구현) (0) | 2023.04.25 |
50일차: 엔티티간 연관관계 매핑 (0) | 2023.04.24 |