[JPA] ApplicationEventPublisher (회원등록 → 트랜잭션구현)
Categories: Spring
Tags: JPA, Spring, Transaction, 실습
📌 개인적인 공간으로 공부를 기록하고 복습하기 위해 사용하는 블로그입니다.
정확하지 않은 정보가 있을 수 있으니 참고바랍니다 :😸
[틀린 내용은 댓글로 남겨주시면 복받으실거에요]
회원등록 이메일에 대한 트랜잭션 구현
회원가입 로직
-
회원 등록이라는 작업과 회원 가입 이메일 전송이라는 작업은 서비스의 정책에 따라서 하나의 작업으로 트랜잭션 처리될 수도 있고, 그렇지 않을 수도 있다.
-
회원 가입 시 회원 정보는 DB에 이미 저장이 된 상태에서 이메일 전송은 실패할 경우, 도입할 수 있는 비즈니스적인 정책은 대략 다음 중 하나일 가능성이 높다
(1) 이메일 전송에 실패하더라도 사용자가 회원 가입 절차를 다시 하는 번거로움을 없애기 위해 저장된 회원 정보를 삭제(rollback)하지 않는다.(2) 회원 가입 완료 이메일에 회원이 확인해야 하는 중요한 정보(회원 정보를 최종 완료하는 링크 등)가 있기 때문에 번거롭더라도 이메일 전송에 실패하면 등록된 회원 정보를 삭제(rollback)하든가 회원 가입 상태를 ‘미완료’ 상태로 유지한다.
(3) 이메일 전송에 실패하더라도 회원 정보를 삭제(rollback)하지 않고, 대신에 카카오톡 같은 메신저 또는 휴대폰 문자 서비스로 회원 가입 메시지를 추가 전송한다.
구현 조건
- 회원 등록 시 예외가 발생할 경우, 데이터베이스에 저장된 회원 정보가 삭제(rollback)되도록 구현
- @Transactional 사용 없이 구현하기
- 등록된 회원 정보가 정상적으로 삭제가 되었는지 H2 웹 콘솔로 확인
- 이메일 전송 기능은 MemberService의 createMember() 코드 내에서 비동기적으로 동작
- 이메일 전송 기능은 회원 등록과 별개의 스레드에서 비동기적으로 실행되기 때문에 이메일 전송 기능에서 예외가 발생하더라도 이미 DB에 등록된 회원 정보가 rollback 처리되지 않는다. (H2 웹 콘솔에서 직접 확인해보기)
- 예외 발생 직전에 저장된 회원 정보 이외에 다른 회원 정보가 삭제되어서는 안됨
첫번째 시도 → Member Service에서 구현
-
일단 구현이 먼저라 생각해서 예외발생시 delete Member로 구현했다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
@Slf4j @Service public class MemberService { private final MemberRepository memberRepository; private final EmailSender emailSender; public MemberService(MemberRepository memberRepository, EmailSender emailSender) { this.memberRepository = memberRepository; this.emailSender = emailSender; } public Member createMember(Member member) { verifyExistsEmail(member.getEmail()); Member savedMember = memberRepository.save(member); log.info("# Saved member"); ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.submit(() -> { try { emailSender.sendEmail("any email message"); } catch (Exception e) { **deleteMember(savedMember.getMemberId());** log.error("MailSendException happened: ", e); throw new RuntimeException(e); } }); return savedMember; }
개선 Point
- 어쨌든 구현한 거지만 , CreateMember에서 예외 처리까지 하고 있고
- 이메일 발송에 문제가 생기면 롤백해야되는 것이기 때문에
- Spring 에 이미 있는 Event를 Publish(발행)하는 기능을 활용하여
- MemberService에서 회원 등록 이벤트를 비동기적으로 먼저 보내고 이 이벤트를 리스닝하는 곳에서 이메일을 보내면 됨
- 이벤트 리스너(Event Listener)가 이메일을 보내고 실패할 경우 이미 저장된 회원 정보를 삭제할 수 있다.
Event와 EventListener 클래스 생성
Event 기능은 아직 스프링에서 배우지는 못해서 (JS에서 비슷한걸 하긴 했지만 ,!) 검색을 활용하였다
[TIP]
https://www.baeldung.com/spring-events
[https://reflectoring.io/spring-boot-application-events-explained] (https://reflectoring.io/spring-boot-application-events-explained)
이 기능을 사용하기 위해서는 ApplicationEventPublisher 와 Event 그리고 EventListner가 필요하다.
ApplicationEventPublisher
- **ApplicationEventPublisher는 publishEvent() 메서드**로 event를 발행하는 역할을 하고, publisherEvent의 Event와 매칭되는 listener에게 알리는 역할을 함
- EventListener는 Event 발생 시 수신하고 처리하는 역할을 담당함, 해당 클래스에는 @Component를 사용하여 빈으로 등록해서 이벤트를 관리하고 발생 시 적절히 호출되게 해야 하며 , 메서드에는 @EventListener를 붙여야 Event를 수신할 수 있음!
- Event 클래스는 이벤트에 대한 데이터라고 생각하면 된다.
Event 클래스와 Evnetlister 클래스를 생성
이 기능들은 전역에서 사용하는게 맞고 이미 helper라는 패키지에 Email 관련 클래스들이 구현되어 있어 helper에 event 패키지를 생성 후 RegistrationEvent와 RegistrationEventhandler 클래스 생성
-
RegistrationEvent
- source는 이벤트가 처음 발생한 객체이거나 이벤트와 연결된 객체여야함!
- sprign 4.2 이후 부터는
ApplicationEvent
상속받지 않아도 된다고 해서 상속받지 않음
1 2 3 4 5 6 7 8 9 10 11 12 13 14
package com.springboot.helper.event; import com.springboot.member.entity.Member; public class RegistrationEvent { private final Member member; public RegistrationEvent(Member member){ this.member = member; } public Member getMember() { return member; } }
-
RegistrationEventhandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
package com.springboot.helper.event; import com.springboot.exception.BusinessLogicException; import com.springboot.exception.ExceptionCode; import com.springboot.helper.EmailSender; import com.springboot.member.entity.Member; import com.springboot.member.service.MemberService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.mail.MailSendException; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; @Slf4j **@Component** @RequiredArgsConstructor public class RegistrationEventHandler { private final EmailSender emailSender; private final MemberService memberService; **@Async @EventListener** public void handle(RegistrationEvent event) throws Exception{ Member member = event.getMember(); try { emailSender.sendEmail("Hi there"); } catch (MailSendException e) { log.error("이메일 발송 되지 않아 rollback을 실행합니다."); memberService.deleteMember(member.getMemberId()); } } }
- @Component는 빈으로 등록해야 스프링이 해당 클래스를 관리하게 되고 Event 발생시 적절히 호출할 수 있다. 즉 Event를 수신하고 처리할 수 있어야 하기 때문에 빈으로 등록해야 한다!
- @EventListener 어노테이션을 붙이면 메서드의 시그니처에 맞는 ApplicationListener를 자동으로 등록할 수 있음 ,
EventListener
어노테이션을 통해 Handler를 구현하고, 정해진 이벤트가 발생하면 이 어노테이션이 달린 메소드가 실행 됨 but event가 발행되는 시점에 Listening을 바로 진행하게 됨. - 트랜잭션과 독립적 - 참고 ) @TransactionalEventListener은 트랜잭션에 의존적이며 커밋되거나 롤백되는 시점에 이벤트를 처리하도록 설정할 수 있음,
phase
속성을 사용하면 되고 기본값은 AFTER_COMMIT(트랜잭션 커밋 후에 이벤트를 처리) -
@Async : 이벤트 리스너는 default로 동기적 실행을 하기 때문에 이벤트 리스너를 비동기 모드로 실행하려면 해당 리스너에 사용하면 된다.
→ 해당 annotation이 작동하려면 @StringBootApplication 클래스에 @EnableAsync를 사용해야 함.
- catch문에서 throw로 기존에 구현되어 있는 BusinessExeption을 작성할 경우 예외가 발생하면 호출한 곳으로가게되고 memberService로 갔는데 예외처리 하는 곳이없기 때문에 서비스를 호출한 곳으로 가고 컨트롤러로 가게 되고 여기서 예외처리 하게 됨. → 나는 이미 MailSendException으로 터지게 구현해서 두번 예외처 리하게는 하지 않았음!
ApplicationEventPublisher
MemberService에 ApplicationEventPublisher 주입시키기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
@Service
public class MemberService {
private final MemberRepository memberRepository;
private final ApplicationEventPublisher eventPublisher;
public MemberService(MemberRepository memberRepository,
ApplicationEventPublisher eventPublisher) {
this.memberRepository = memberRepository;
this.eventPublisher=eventPublisher;
}
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
Member savedMember = memberRepository.save(member);
//DB에 저장해놓고 기다림, 검증이 완료되면 실행
log.info("# Saved member");
eventPublisher.publishEvent(new RegistrationEvent(savedMember));
return savedMember;
}
- publisherEvent로 여러 Event를 발행할 수 있으며, 기존에 EmailSender를 주입받을 필요가 없어졌다(의존성 사라짐), 추후에 다른 이벤트( 가입완료 알림톡 전송, 할인쿠폰 등 )가 발생하더라도 Event에서 해결 가능.
- 즉, 관심사 분리로 member service에서 createmember의 기능을 수행하고, 나머지는 다른 곳에서 이루어지게 됨.
- 롤백하는 이유, 이메일주소 잘못입력시 오타 발생하면 발송에 실패하게 됨. 그럼 롤백해야함.→ 이메일 도착 여부로 검증 가능.
Comment
이번 과제는 분명 Transactional을 배우고 실습한 건데,, 왜 Event 에 대해서 배운 것 같은 느낌이 드는지..! 그래도 몰랐던 것을 구현하는 것은 검색을 많이 해봐야하는데 검색하면서 버전때문에 달라진 것도 알 수 있었고 나중에 다시 해보라고 하면 더 생각이 잘 날 것 같긴 하다 비록 한번 밖에 해보진 않았지만…!
아직 원문은 낯설어서 잘 못보긴 하는데 보는 연습도 되고,, 자동번역도 열심히 해서 보는 중이다
근데 블로그 피해서 영어 게시글 보는데 자꾸 블로그로 들어가는 것 같은 느낌이었는데 baeldung이라는 사이트가 자꾸 나오길래 여기 외국블로그인가 했는데 교육사이트 인 듯하다..! 생각보다 자세하고 명확하게 설명해주서 잘 이용하게 될듯..👍👍
Leave a comment