내부적으로 @Transactional
이 걸려있는 메소드를 호출해서 사용한다면 트랜잭션이 걸리길 기대하지 말자.
@Component
@RequiredArgsConstructor
class 서비스_이용_클래스 {
// 임시서비스를 주입받아서 사용한다고 생각하자.
void 상황1() {
// 트랜잭션이 걸려있는 메소드를 직접 호출한다.
임시서비스.트랜잭션메소드();
// 결론 : 당연하게도 트랜잭션이 걸린다.
}
void 상황2() {
// 임시서비스 클래스에서 내부적으로 트랜잭션메소드를 호출한다.
임시서비스.트랜잭션_없는_메소드();
// 결론 : 트랜잭션이 걸리지 않는다.
}
}
@Service
class 임시서비스 {
@Transactional
트랜잭션메소드() { Repository를 활용한 복잡한 처리 로직; }
트랜잭션_없는_메소드() { this.트랜잭션메소드(); }
}
나의 프로젝트 팀원이 DB상에 삭제 처리된 데이터들을 한 번에 제거하는 코드를 구현했다.
구현할 때 스프링 스케줄러를 이용해서 사용자가 없는 새벽 3시에 실행하도록 만들었다.
다음 코드는 정상적으로 동작하는 코드이다.
다음과 같이 스케줄러가 호출하는 메소드(execute
)에 트랜잭션이 걸려있다.
@Component
@RequiredArgsConstructor
public class DeletionTask {
...
/**
* 회원탈퇴, 그룹 삭제, 클라이언트 삭제를 처리하기 위한 스케줄링
*/
@Scheduled(cron = "0 0 3 * * *", zone = "Asia/Seoul") // 새벽 3시에 작업 수행
@Transactional
public void execute() {
...
int deletedImageCount = deleteClientImages(); //이미지 삭제
if(isS3Error(deletedImageCount)){
return;
}
...
}
private int deleteClientImages() {
// S3와 JPA를 이용해서 연관된 데이터 제거
...
}
}
나의 팀원은 JPA 관련 처리를 하는 중에 오류가 발생해도 앞서 실행한 코드들이 롤백되지 않았으면 좋겠다는 생각을 했다.
그래서 다음과 같이 스케줄러가 호출하는 메소드(execute
)에 트랜잭션을 걸지 않고
JPA로 복잡한 처리를 하는 코드(deleteClientImages
)에만 트랜잭션을 걸었다.
@Component
@RequiredArgsConstructor
public class DeletionTask {
...
/**
* 회원탈퇴, 그룹 삭제, 클라이언트 삭제를 처리하기 위한 스케줄링
*/
@Scheduled(cron = "0 0 3 * * *", zone = "Asia/Seoul") // 새벽 3시에 작업 수행
// @Transactional 이 코드는 필요없겠다!
public void execute() {
...
int deletedImageCount = deleteClientImages(); //이미지 삭제
if(isS3Error(deletedImageCount)){
return;
}
...
}
@Transactional // 트랜잭션을 여기만 걸어보자.
private int deleteClientImages() {
// S3와 JPA를 이용해서 연관된 데이터 제거
...
}
}
하지만 이 코드는 다음 오류를 발생시킨다.
org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException: Executing an update/delete query
...
Caused by: javax.persistence.TransactionRequiredException: Executing an update/delete query
...
단순히 트랜잭션을 이동했는데 왜 오류가 발생했을까?
내가 이전에 정리했던 Spring Jpa - save vs saveAll에 관한 글을 통해 원인을 알 수 있었다.
메소드에 트랜잭션을 건다면 스프링 컨테이너에 트랜잭션 AOP가 적용된 객체가 프록시 객체로 등록된다.
아마 나의 팀원은 다음과 같은 흐름을 기대했을 것이다.
팀원이 생각한 코드의 기대 흐름
1. 스케줄러execute()
메소드 실행!
2.deleteClientImages()
에@Transactional
이 걸려있다! - 트랜잭션 시작!
3.deleteClientImages()
메소드 실행!
4. 트랜잭션 종료!
5. 정상 종료!
하지만 실제 트랜잭션 AOP가 적용된 프록시 객체는 다음과 같이 동작할 것이다.
실제 흐름
1.execute()
메소드에@Transactional
이 안걸려있다. 트랜잭션 시작 안해!
2. 스케줄러execute()
메소드 실행!
3.deleteClientImages()
메소드 실행!
4. 정상 종료!
우리는 이 문제를 deleteClientImages
메소드의 동작을 다른 컴포넌트로 등록하여 스프링 컨테이너로부터 주입받아 사용하도록 만들어서 해결했다.
@Component
@RequiredArgsConstructor
public class DeletionTask {
private final ImageDeletionTask imageDeletionTask; // 주입 받음
...
/**
* 회원탈퇴, 그룹 삭제, 클라이언트 삭제를 처리하기 위한 스케줄링
*/
@Scheduled(cron = "0 0 3 * * *", zone = "Asia/Seoul")
public void execute() {
...
int deletedImageCount = imageDeletionTask.deleteClientImages(); // 주입받은 프록시 객체로 실행
if(isS3Error(deletedImageCount)){
return;
}
...
}
}
@Component
@RequiredArgsConstructor
@Slf4j
public class ImageDeletionTask {
@Transactional
public int deleteClientImages() {
// S3와 JPA를 이용해서 연관된 데이터 제거
...
}
}