@Transactional 사용 시 Self-invocation 해결
Spring JPA를 사용한 회사 프로젝트 기능 개발 중 마주했던,
@Transactional 어노테이션 사용 시 Self-invocation(자기 호출) 이슈 해결을 위한 고민과 그 해결 방법에 대한 기록입니다.
@Transactional ?
@Transacional 어노테이션에 대해 알아보기 전에, Transaction(트랜젝션)에 대해 알아보겠습니다.
Transaction이란 한 문장으로 정의해 보면 데이터베이스의 상태를 변화시키기 위해서 수행하는 작업의 단위를 의미합니다.
일반적인 SELECT, INSERT, UPDATE, DELETE를 사용해서 DB에 한번 접근했을 때 수행하는 작업의 단위라고 할 수 있습니다.
트랜잭션의 특징으로는 안전성을 보장하기 위해 필요한 4가지 성질이 있습니다. (ACID 성질)
1. 원자성(Atomicity) : 트랜잭션이 한번 실행될 때, 데이터베이스에 모두 반영되던가, 모두 반영되지 않아야 합니다.
2. 일관성(Consistency) : 트랜잭션의 작업 처리 결과는 항상 일관성이 있어야 합니다.
3. 독립성(Isolation) : 둘 이상의 트랜잭션이 동시에 실행될 때, 다른 트랜잭션의 연산에 끼어들 수 없습니다.
4. 지속성(Durability) : 트랜잭션이 성공적으로 완료되었을 경우, 결과는 영구적으로 반영이 되어야만 합니다.
@Transactional 어노테이션은, 해당 어노테이션이 적용되는 해당 타겟을 하나의 트랜잭션으로 묶고 관리하는 역할을 담당합니다.
이 어노테이션에서 주목해야할 점은 바로 'RollBack (롤백, 되돌리기)' 입니다.
예를 들어, 어떤 항목을 수정하는 중에 예외가 발생하였을때, 수정사항을 DB에 반영(커밋) 하지 않고 되돌리는(롤백) 하는 기능입니다.
Java에는 체크 예외(Checked Exception)와 언체크 예외(Unchecked Exception)가 있습니다. 체크 예외란 Exception 클래스 하위 클래스이며, 언체크 예외란 Exception 하위의 RuntimException 하위의 예외입니다.
스프링의 선언적 트랜잭션(@Transactional) 안에서 예외가 발생했을 때, 해당 예외가 언체크 예외(런타임 예외)라면 자동적으로 롤백이 발생합니다. 하지만 체크 예외라면 롤백이 되지 않는데, 체크 예외를 롤백시키기 위해서는 @Transactional의 rollbackFor 속성으로 해당 체크 예외를 적어주어야 합니다.
self-invocation ?
@Component
@RequiredArgsConstructor
@Slf4j
public class ItemBizService {
/**
* checkbox 선택 item 일괄 변경
* @param list
* @return
*/
public List<ItemDTO> changeSelectItem(List<ItemDTO> list) {
if (ListTools.isNullOrEmpty(list)) {
throw new Exception("선택된 아이템이 없습니다.");
}
//Self-Invocation 을 하면 @Transactional 어노테이션이 동작 하지 않아요.
return checkAndChangeItem(list);
}
/**
* validation 검증 후 item 변경
* @param list
* @return
*/
@Transactional(rollbackFor = Exception.class)
public List<ItemDTO> checkAndChangeItem(List<ItemDTO> list) {
.... 생략 ....
}
해당 코드로 제가 구현하고 싶었던 기능은, 다음과 같습니다.
1. checkbox로 선택한 항목의 validation을 검증
2. validation의 검증을 통과하지 못한 항목은 rollback + 에러메시지 출력
3. 통과한 경우에는 변경 사항 commit
제가 커밋한 코드에 'Self-Invocation' 이슈가 있다는 코멘트를 보고, 검색 또 검색 끝에 해결방법을 찾게 되었습니다.👓
@Transacional 관련 Spring 공식 문서에 따르면,
'target object, 즉 트랜잭션이 적용되는 메서드가 동일한 클래스의 다른 메서드를 호출하는 것을 self-invocation이라 한다.
그리고 이 경우에는 @Transactional 어노테이션이 적용되어 있더라도 실제로 트랜잭션이 적용되진 않는다.' 고 합니다.
@Transactional 트랜잭션의 기본 모드인 proxy가 프록시를 통한 접근만 잡아서 처리할 수 있는데,
스프링의 트랜잭션은 기본적으로 AOP를 활용하여 트랜잭션 대상 객체를 프록시 객체로 생성하고 외부 클라이언트, 메서드를 호출하는 쪽의 접근을 제어하는 방식으로 동작합니다.
그렇지만 제가 구현한 checkAndChangeItem 메서드처럼 같은 클래스 내의 메서드를 호출(self-invocation)한다면 클라이언트가 프록시 객체를 통해서 호출한 것이 아니라 이를 제어할 수 없기 때문에 트랜잭션의 전파가 제대로 반영되지 않는 것!
+) 이 밖에도 @Transactional 이 동작하지 않는 경우가 궁금할 경우, 하기 링크를 참조해주세요!
https://devgoat.tistory.com/28
해결방법
어노테이션이 있는데도 동작을 하지 않는다면, 어떻게 해결하면 좋을까...🙃
그 해결방법은 생각보다 간단했습니다. 바로 다른 클래스로 분리하는 것!
@Component
@RequiredArgsConstructor
@Slf4j
public class ItemBizService {
/**
* checkbox 선택 item 일괄 변경
* @param list
* @return
*/
public List<ItemDTO> changeSelectItem(List<ItemDTO> list) {
if (ListTools.isNullOrEmpty(list)) {
throw new Exception("선택된 아이템이 없습니다.");
}
return checkAndChangeItem(list);
}
/**
* validation 검증 후 item 변경
* @param list
* @return
*/
@Transactional(rollbackFor = Exception.class)
public List<ItemDTO> checkAndChangeItem(List<ItemDTO> list) {
for (ItemDTO itemDTO : list) {
ItemChangeService.checkAndChangeItem(itemDTO);
... 생략 ...
}
@Component
@RequiredArgsConstructor
public class ItemChangeService {
@Transactional(rollbackFor = Exception.class)
public void checkAndChangeItem(ItemDTO dto) {
... 생략 ...
}
여러건을 수정하지만 실패 시 전체를 Rollback하지 않고 실패한 건에 대해서만 Rollback이 되도록 메소드를 분리했고,
해당 코드를 실행해본 결과, 처음 의도한 대로 잘 동작하는 것을 확인할 수 있었습니다! 😊👌
@Transactional 으로 편하고 디테일하게 트랜잭션을 처리할 수 있지만, 동작 방식이나 신경써야할 예외 사항이 있기 때문에 주의해서 사용해야겠습니다. 무심코 써왔던 기능에 대해 다시 한번 살펴보고 모르는 점은 새롭게 알아가는 계기가 되었습니다! 😉