[Spring] @Transactional의 rollback과 주의점

jayk·2024년 5월 29일

Spring

목록 보기
1/1
post-thumbnail

📖 Transaction이란??

트랜잭션이란 간단히 말해,

데이터베이스의 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위 또는 한꺼번에 모두 수행되어야 할 일련의 연산

을 뜻한다.

예를 들자면, 100만원을 계좌이체를 할 때 아래와 같이 2가지 작업으로 나눌 수 있는데,
1. A계좌에서 돈을 빼내는 작업 (-100만원)
2. B계좌에 돈을 넣는 작업 (+100만원)

만약 1번 작업은 실행이 되었지만 2번 작업에서 오류가 발생했을 때, 1번 작업을 다시 되돌리지 않으면, 그냥 100만원이 사라지는 결과를 낳게된다.

이와 같이 반드시 한꺼번에 수행되어야 하는 연산을 뜻하는 것이 트랜잭션이다.


❗️ @Transactional 사용 시 주의점

Spring에서는 @Transactional 어노테이션을 사용하여 특정 메소드에 트랜잭션을 걸 수 있다.
@Transactional을 사용할 때, 몇가지 주의해야할 점이 있는데,

📍 @Transactional의 rollbackFor 속성

@Trasactional 어노테이션은 기본 적으로 RuntimeException과 Error에 대하여 Rollback처리를 하도록 되어 있다.
때문에, @Transactional의 rollbackFor 속성의 기본값은 RuntimeException.class, Error.class 로 되어 있다.

@Transactional
public void publicMethod() {}
@Transactional(rollbackFor = {RuntimeException.class, Error.class})
public void publicMethod() {}

그렇기 때문에 @Transactional만 적용하게 되면, 동작 중 발생되는 오류에 대해서는 Rollback처리가 되지 않는다.
왜냐하냐...!
동작 중 발생되는 오류는 대부분 Exception.class를 상속받는 예외 클래스인데, RuntimeException.class, Error.class는 Exception.class를 상속받는 클래스가 아니기 때문이다.
때문에 동작 중 발생되는 오류를 잡아서 Rollback을 시키고 싶다면 아래와 같이 rollbackFor 속성을 적용해야한다.

@Transactional(rollbackFor = {Exception.class})
public void publicMethod() {}

❓ 만약 try/catch 구문을 사용하면 Rollback처리가 될까?

어떻게 쓰냐에 따라 다르다

Case 1

/*
 * (Rollback 안되는 case)
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ExServiceClass {
	private final ExRepo exRepo;
    
    @Transactional(rollbackFor = {Exception.class})
    public void publicMethod() {
    	try {
    		exRepo.doSomething();
            
        } catch (Exception e) {
        	log.error(e.getMessage());
            
        }
    }
}

위의 경우, doSomething()에서 에러 발생 시에도 Rollback 되지 않는다!!
위의 코드는 catch절에서 예외처리를 모두 끝냈으니까 신경쓰지마~ 라는 말과 동일하다.
때문에 try/catch 절 바깥 부분에서는 예외에 대해 신경쓰지 않기 때문에 Rollback이 동작되지 않는다.

Case 2

/*
 * (Rollback 되는 case)
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ExServiceClass {
	private final ExRepo exRepo;
    
    @Transactional(rollbackFor = {Exception.class})
    public void publicMethod() {
    	try {
    		exRepo.doSomething();
            
        } catch (Exception e) {
        	log.error(e.getMessage());
            throw e;
            
        }
    }
}

위의 경우에는 doSomething()에서 에러 발생 시 Rollback이 된다!!!
catch 구문에서 예외 처리를 try/catch 구문 바깥으로 다시 넘겼기 때문이다.

📍 @Transactional 메소드 셀프 호출

다음은 아는 사람은 안다는 바로 그 @Trasactional 메소드 셀프 호출 이다.
사실,,,'메소드 셀프 호출' 이라는 말은 그냥 방금 내가 만든 말이다...(이걸 뭐라고 설명해야될지 모르겠어서..🤦‍♂️)
풀어서 말하자면, @Transactional가 적용된 메소드를 같은 클래스에서 호출하는걸 뜻한다.
결론부터 말하면,

@Transactional가 적용된 메소드를 같은 클래스에서 호출하면 안된다!!

/*
 * (Rollback 안되는 case)
 */
@Service
@RequiredArgsConstructor
public class ExService {
	private final ExRepo exRepo;
	
	public void publicMethod() {
		this.privateMethod();
        
	}
	
    @Transactional(rollbackFor = {Exception.class})
	private void privateMethod() {
		exRepo.doSomething();
        
	}

이 경우에는 의도와는 다르게 privateMethod()에 트랜잭션 적용이 되지 않고, 무시한다.
(IntelliJ에서는 'private' 부분에 빨간줄을 그어주기 때문에, 아마 IntelliJ를 사용하는 사람이라면 알거다)

❓ 그러면 메소드를 public으로만 바꾸면??

/*
 * (Rollback 안되는 case)
 * public으로 바꿔도 실행 주체가 같은 객체라면 안된다.
 */
@Service
@RequiredArgsConstructor
public class ExService {
	private final ExRepo exRepo;
	
	public void publicMethod() {
		this.privateMethod();
        
	}
	
    @Transactional(rollbackFor = {Exception.class})
	public void privateMethod() {
		exRepo.doSomething();
        
	}

안된다.(단호)
이제 이유를 설명해보겠다.

Spring은 AOP를 사용해서 @Transactional 어노테이션을 처리한다.
Spring은 기본적으로 AOP할 때, proxy 패턴을 사용하기 때문에, 이 과정에서 동적으로 해당 클래스를 프록시 객체로 생성하고, 생성된 프로시 객체를 원래의 Bean 객체를 대신해서 호출하게된다.
때문에 클래스 내부에서 호출하게되면 외부에서 접근하여 프록시 객체를 생성할 수 없게 되는 것이다.
이 경우를 많이 놓치는 이유가 Spring에서 에러를 발생시키지 않고, 그냥 무시하고 실행하기 때문이다.

❓ 그러면 @Transactional이 걸려있는 public 메소드에서 private 메소드를 실행하면??

/*
 * (Rollback 되는 case)
 */
@Service
@RequiredArgsConstructor
public class ExService {
	private final ExRepo exRepo;
	
    @Transactional(rollbackFor = {Exception.class})
	public void publicMethod() {
		this.privateMethod();
        
	}
	
	public void privateMethod() {
		exRepo.doSomething();
        
	}

된다!
위의 설명과 동일하다. publicMethod()를 실행할 때, 이미 프록시 객체를 생성했기 때문이다.
때문에, exRepo.doSomething()에서 오류가 발생하더라도 publicMethod()의 트랜잭션에 걸리게 된다.

0개의 댓글