Requires_new is king of side effects.
c.c) spring-framework 이슈
“REQUIRES_NEW는 부작용 대장이다.”라는 말에는 그 이유가 있다.
REQUIRES_NEW
전파 속성은 새로운 트랜잭션을 시작하는 기능을 가지며, 그로 인해 데드락 발생의 위험성이 증가한다.
이미 실행 중인 트랜잭션 내에서 이 전파속성을 가진 메서드를 호출하면 새로운 트랜잭션이 시작된다.
이 과정에서 커넥션 풀의 리소스가 부족해지면 데드락의 위험이 있다.
데드락의 원리
REQUIRES_NEW
는 이미 실행 중인 트랜잭션이 있을 때 새로운 트랜잭션을 시작하는 전파 속성이다.
만약 동시에 많은 요청이 여러 트랜잭션을 시작하려 할 때, 사용 가능한 커넥션 리소스가 부족하게 되면 데드락이 발생한다.
예시 상황
사용자가 ‘좋아요’를 누르면 알림을 전송하는 서비스가 있다고 가정하자.
LikeService
는 좋아요 정보를 DB에 저장하고, NotificationService
에 알림 전송을 요청한다.
NotificationService
는 알림을 전송하고, 알림 정보를 DB에 저장한다.
알림 전송에 실패해도 좋아요 정보는 롤백되어서는 안 된다.
NotificationService
의 sendNotification()
의 전파 속성을 REQUIRES_NEW
로 설정함으로써 두 작업을 독립적인 트랜잭션으로 분리 가능하다.
@Service
@RequiredArgsConstructor
@Slf4j
public class LikeService {
private final PostRepository postRepository;
private final LikeRepository likeRepository;
private final NotificationService notificationService;
@Transactional
public void like(Long postId) {
Post post = postRepository.findById(postId).orElseThrow();
Like like = likeRepository.save(new Like(post));
try {
notificationService.sendNotification(like);
} catch (Exception e) {
log.error("알림 전송 실패", e);
}
}
}
@Service
@RequiredArgsConstructor
public class NotificationService {
private final NotificationRepository notificationRepository;
private final NotificationClient notificationClient;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendNotification(Like like) {
Notification notification = new Notification(like);
notificationClient.send(notification);
notificationRepository.save(notification);
}
}
문제점
문제는 하나의 요청이 2개의 데이터베이스 커넥션을 점유한다는 것이다.
(외부 트랜잭션과 내부 트랜잭션에서 각각 1개의 커넥션을 필요로 한다.)
많은 요청이 동시에 들어오면, 커넥션 풀이 고갈되어 모든 커넥션들이 새로운 커넥션을 대기하는 데드락이 발생한다.
해결 방안
이러한 데드락 위험성 때문에, 동시 요청이 많은 상황에서는 REQUIRES_NEW
를 최대한 지양하는 것이 좋다.
그러면 두 요청을 독립적인 트랜잭션으로 분리하고 싶을 땐 어떻게 할 수 있을까?
1. @Async 활용
@Async
어노테이션을 사용해 메서드를 비동기로 처리를 하면, 트랜잭션을 분리할 수 있다.
비동기 메서드는 새로운 스레드에서 작업을 수행하는데, 트랜잭션은 스레드 로컬이기 때문에 두 요청에서 서로 다른 트랜잭션을 사용하게 된다.
커넥션 풀이 가득 찬 상황이라도, 내부 트랜잭션은 외부 트랜잭션이 끝날 때까지 대기하다가 해당 커넥션을 가져간다.
@Async
@Transactional
public void sendNotification(Like like) {
/***/
}
비동기로 호출 시, 외부 메서드가 내부 비동기 메서드의 결과를 기다리지 않고 반환한다.
즉, 내부 메서드의 결과를 외부 메서드에서 활용해야 하는 경우에는 적합하지 않다.
2. FacadeService 활용
두 서비스 메서드 앞단에 FacadeService를 사용하면 데드락 문제를 해결할 수 있다.
이 서비스는 트랜잭션을 적용하지 않고, 각 트랜잭션 메서드를 순차적으로 호출한다.
이렇게 하면 각 메서드가 종료될 때마다 사용했던 커넥션을 반환하므로 데드락의 위험이 크게 줄어든다.
@Service
@RequiredArgsConstructor
public class LikeFacadeService {
private final LikeService likeService;
private final NotificationService notificationService;
public void like(Long postId) {
Like like = likeService.like(postId);
notificationService.sendNotification(like);
}
}
@Service
@RequiredArgsConstructor
public class LikeService {
/***/
@Transactional
public Like like(Long postId) {
Post post = postRepository.findById(postId).orElseThrow();
return likeRepository.save(new Like(post));
}
}
마무리
이번 포스트에서 동시 요청 상황에서 REQUIRES_NEW의 데드락 위험성에 대해 알아보았다.
해결 방법으로 @Async 어노테이션과 FacadeService 활용 방법을 중심적으로 알아보았지만, 이외에도 다양한 방법들이 존재한다.
언제나 본인의 환경과 요구사항에 가장 적합한 전략을 선정하는 것이 가장 중요하다!!
참고 자료
https://github.com/spring-projects/spring-framework/issues/26250
'프로그래밍' 카테고리의 다른 글
CATS: MySQL은 락 점유 순서를 어떻게 결정할까? (3) | 2023.11.17 |
---|---|
AOP 및 @Retryable를 활용한 낙관적 락 재시도 (6) | 2023.11.16 |
[Spring] Mockito 테스트의 중복 given절 줄이기: lenient를 활용한 유틸리티 클래스 (1) | 2023.10.08 |
[Spring] TaskScheduler를 활용해 런타임에 동적으로 작업 예약하기 (7) | 2023.10.08 |
Spring Event란? (0) | 2023.09.17 |