최근 Spring Transaction 전파에 대해 깊이있게 학습하며 예전 알림 문제를 어떻게하면 해결할 수 있을지 계속 고민이 들었다.
과연 에전에 내가 한 방법이 정말 올바른 방법이였을까?
2024.04.20 - [대외활동] - 알림기능, 문제의 시작
알림기능, 문제의 시작
기존 서비스저희 게시판 서비스는 HoneyTip(꿀팁공유해요 게시판)과 HoneyTipComment(꿀팁공유해요 댓글)이 연관관계 매핑이 되어있습니다. HoneyTipComment는 Comment를 단일테이블 전략으로 상속받고 있습
toychip.tistory.com
예제를 아주 간단하게 세팅해서 문제를 명확히 파악해보자.
Entity
/* Entity */
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
private String writer;
private Comment(final String content, final String writer) {
this.content = content;
this.writer = writer;
}
public static Comment of(final String content, final String writer) {
return new Comment(content, writer);
}
}
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Log {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Comment comment;
private String content;
private Log(final Comment comment, final String content) {
this.comment = comment;
this.content = content;
}
public static Log of(final Comment comment, final String content) {
return new Log(comment, content);
}
}
Service
/* CommentService */
@Service
@RequiredArgsConstructor
public class CommentService {
private final CommentRepository commentRepository;
private final LogService logService;
@Transactional
public void registerComment(Comment comment) {
commentRepository.save(comment);
logService.registerLog(comment);
}
}
@Service
@RequiredArgsConstructor
public class LogService {
private final LogRepository logRepository;
@Transactional
public void registerLog(Comment comment) {
String logUuid = "log + " + UUID.randomUUID();
String logUuidSubString = logUuid.substring(0, 15);
Log log = Log.of(comment, logUuidSubString);
logRepository.save(log);
}
}
1. 같은 논리트랜잭션 참여
현재, 아래와 같은 상황이다.
LogService가 CommentService의 트랜잭션에 참여한다.
자 이제 이상황에서 LogService에서 예외가 발생한다면 모두 롤백이 될 것이다.
/* CommentService */
@Transactional
public void registerCommentLogException(Comment comment) {
commentRepository.save(comment);
logService.registerLogException(comment);
}
/* LogService */
@Transactional
public void registerLogException(Comment comment) {
String logUuid = "log + " + UUID.randomUUID();
String logUuidSubString = logUuid.substring(0, 15);
Log log = Log.of(comment, logUuidSubString);
logRepository.save(log);
if (true) {
throw new RuntimeException("예외 발생! 롤백 !");
}
}
테스트가 통과하는 것을 볼 수 있다.
2. REQUIRES_NEW, 트랜잭션 분리
LogService에서 예외가 발생한다. 과연 어떻게 될까?
/* CommentService */
@Transactional
public void registerCommentLogExceptionRequiresNew(Comment comment) {
commentRepository.save(comment);
logService.registerLogCatchError(comment);
}
/* LogService */
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registerLogExRequiresNew(Comment comment) {
String logUuid = "log + " + UUID.randomUUID();
String logUuidSubString = logUuid.substring(0, 15);
Log log = Log.of(comment, logUuidSubString);
logRepository.save(log);
if (true) {
throw new RuntimeException("예외 발생! 롤백 !");
}
}
현재 아래와 같은 상황이다. 어떻게 될까?
테스트 코드로 돌려보자.
우리는 트랜잭션을 분리했기 때문에 댓글은 생성되고, Log는 생성되지 않았을 거라 생각한다.
하지만 테스트를 실패함을 볼 수 있다.
결과적으로 Comment까지 함께 롤백이 됨을 알 수 있다.
그 이유는 뭘까?
전체 롤백이 발생한 이유
- LogService.registerLog() 메서드가 새로운 트랜잭션으로 시작되었지만, 이 트랜잭션 내에서 예외가 발생하면 해당 예외가 상위 메서드(CommentService.registerComment())로 전파된다.
- 상위 메서드인 registerComment()는 새로운 트랜잭션을 생성했기 때문에, 이 트랜잭션 내에서도 예외 발생 시 롤백이 일어난다.
- 즉, @Transactional(propagation = Propagation.REQUIRES_NEW)로 별도의 트랜잭션을 만들었더라도, 그 트랜잭션 내에서 발생한 예외가 상위 트랜잭션으로 전파되며, 상위 트랜잭션도 롤백된다.
- 그렇기 때문에 예외를 잡아서 없애줘야한다. 예외를 먹어야한다!
3. REQUIRES_NEW, 트랜잭션 분리, 예외 Catch
/* CommentService */
@Transactional
public void registerCommentLogExRequiresNewCatchEx(Comment comment) {
commentRepository.save(comment);
logService.registerLogCatchError(comment);
}
/* LogService */
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registerLogCatchError(Comment comment) {
try {
String logUuid = "log + " + UUID.randomUUID();
String logUuidSubString = logUuid.substring(0, 15);
Log log = Log.of(comment, logUuidSubString);
if (true) {
throw new RuntimeException("예외 발생! 롤백 !");
}
logRepository.save(log);
} catch (Exception e) {
System.out.println("에러 잡음! 정상 흐름 변환");
}
}
테스트를 성공함을 알 수 있다. 그를 저장하다 예외가 발생하더라도 현재 댓글은 정상적으로 생성된다.
하지만 필자는 이것을 몰랐을 때 예외를 캐치하지 않으면서 예외를 터뜨렸다.
왜 안될까 이것저것 시도해보다가 구글링을 해보니 @Async로 해결할 수 있다고 했다.
그래서 @Async와 @TransationalEventListener를 알게 되었고 이를 적용했다.
/* CommentService */
@Transactional
public void registerCommentAsyncTxEventListener(Comment comment) {
commentRepository.save(comment);
// logService.registerLogCatchError(comment);
eventPublisher.publishEvent(comment);
}
/* LogService */
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void registerLogTxEventListener(Comment comment) {
log.info("registerCommentAsyncTxEventListener 호출");
String logUuid = "log + " + UUID.randomUUID();
String logUuidSubString = logUuid.substring(0, 15);
Log log = Log.of(comment, logUuidSubString);
if (true) {
throw new RuntimeException("예외 발생! 롤백 !");
}
logRepository.save(log);
}
테스트가 성공함을 알 수 있다.
실행 동작은 위와 같다. 댓글이 정상적으로 생성되었을 때 이벤트를 발행하고, LogService는 그 이벤트를 기다리고 있다가 본인만의 별도의 물리 트랜잭션을 시작한다.
나는 이전에 이렇게 분리를 해서 알림 기능을 완성했었다.
위와 같은 작업을 했기 때문에, 댓글이 생성되면 클라이언트에게 즉시적으로 응답이 갔고, 비동기로 알림을 처리했었다.
그렇기에 응답 시간을 줄일 수 있었고 전체 롤백되는 현상 또한 해결했다.
이방법 저방법 삽질하다가 실행됐지만, REQUIRES_NEW와 트랜잭션 전파에 대해서 제대로 짚고 나가지 않은 것이였다.
우연에 일치이지만, REQUIRES_NEW는 데드락 때문에 자주 사용하지 않는다고 한다. 아래 참고 자료 확인
이전 프로젝트에서의 응답 시간을 현재 테스트에서 똑같이 구현할 수는 없지만, 댓글 생성과 로그 생성 및 롤백 부분에서 비동기로 처리할 때의 시간차이를 알아보자.
위와 같이 응답처리 속도가 동기처리 + REQUIRES_NEW는 64ms, 비동기처리 + TransactionalEventListener는 6ms가 걸린 것을 알 수 있다.
우연에 일치로 REQUIRES_NEW를 피하고, Async + TransactionalEventListener를 사용하여 응답시간도 줄였으며 해결했다.
현재 프로젝트에서 비동기 처리 후 예외를 잡고 다시 예외를 터뜨리는 로직이 있는데, 이 로직을 비동기 에러처리에서 핸들링하던 예외를 삼키고 로그를 찍던 수정해야겠다.
이번 계기로 트랜잭션 에러 전파, REQURIES_NEW, TransactionalEventLister를 사용한 트랜잭션 분리, Async를 사용한 핵심 기능에 즉각적인 응답 처리 등 많이 알게 되었다!
참고 자료
https://xxeol.tistory.com/55
'프로젝트 > 똑립' 카테고리의 다른 글
멀티모듈에서 AOP 사용시 주의점 (0) | 2024.11.15 |
---|---|
똑립 3. 멀티쓰레드 환경에서 동시성 제어. 분산락 적용 (0) | 2024.09.08 |
똑립 1. 알림기능, 문제의 시작 (0) | 2024.04.20 |