Transactional에서 Exception에 의한 Rollback

PEPPERMINT100·2022년 7월 27일
0

서론

Transactional 어노테이션으로 트랜잭션 단위를 묶은 메소드가 있었다. 해당 메소드 내부에는 여러 쿼리문이 있었고, 여러 쿼리문 이전에 어떤 조건이 맞지 않으면 Exception을 띄워서 트랜잭션이 커밋되지 않고 롤백 되기를 의도하여 메소드를 작성했다.

예시

@Transactional
public void process() {
	SomeCondition condition = someService.someProcess();
    
    if (condition.isNotOk()) {
    	throw new WrongConditionException();
    }
    
   someService.nextProcess(); // 이 프로세스 내에도 여러 예외가 존재한다.
   someService.theNextNextProcess(); // 여 메소드 안에도 예외가 있다.
}

대략 이런 식이다. condition이 isNotOk의 경우 예외를 던지고 또 그 아래 메소드도 각각 조건에 맞는 예외를 품고 있다고 가정하자.

당연히 각 Process에 해당하는 예외가 발생하면 @Transactional로 묶인 process라는 메소드의 모든 트랜잭션은 커밋되지 않고 롤백될 것으로 예상했다.

하지만 결과는 그렇지 않았다.

진짜 심각한건 몇몇 에러는 롤백이 되는데 몇몇 에러는 롤백이 안되고 있었다. 이러한 현상이 왜 일어난 걸까?

스프링의 Transactional은 사실

예외를 띄우면 롤백을 시켜준다. 단 Unchecked ExceptionError 에 한해서만 롤백을 시켜준다.

Error는 하드웨어 단 에러를 말하고 Unchecked Exception 은 체크되지 않는 예외, 즉 RunTimeException을 말한다.

Checked Exception은 그냥 Exception, Unchecked Exception은 RunTimeException이다. 각각 꼭 처리해서 체크해줘야 하는 예외, 처리를 안해줘도 되는 예외를 뜻한다.

실제로 Transaction 어노테이션의 옵션중 하나인 rollBackFor의 내부 구현을 보면 주석으로 자세히 설명이 되어 있다.

/**
 * Defines zero (0) or more exception {@linkClass classes}, which must be
 * subclasses of {@linkThrowable}, indicating which exception types must cause
 * a transaction rollback.
 *<p>By default, a transaction will be rolling back on {@linkRuntimeException}
 * and {@linkError} but not on checked exceptions (business exceptions). See
 * {@linkorg.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)}
 * for a detailed explanation.
 *<p>This is the preferred way to construct a rollback rule (in contrast to
 * {@link#rollbackForClassName}), matching the exception class and its subclasses.
 *<p>Similar to {@linkorg.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(Class clazz)}.
 *@see#rollbackForClassName
 *@seeorg.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)
 */
Class<? extends Throwable>[] rollbackFor() default {};

즉 process 메소드 내에서 Unchecked Exception에 해당하는 예외에서는 롤백을 시켜주고 Checked Exception의 경우에는 롤백을 시켜주지 않았던 것이다. (당연히 코드의 원래 의도는 모든 예외 경우에서 전부 롤백시켜주는 것이 맞았다.)

그럼 어떻게 처리해야 할까

당연히 전부 Unchecked Exception인 RunTimeException을 상속받는 예외로 교체를 함으로서 처리했다. 하지만 이런 경우가 있을 수 있다.

어떤 서비스에 회원가입을 하고 추천인 입력 보상을 받기 위해 추천인을 입력한다.

추천인이 존재하지 않아서 추천인 관련하여는 예외가 발생하여 입력이 되지 않지만 회원가입은 정상적으로 처리되어 db에 입력되어야 한다. 한 트랜잭션 내에서.

가장 먼저 든 깔끔한 방법은 새로운 예외 클래스를 만든다.

class NoRollbackException extends RunTimeException {}
class InvalidInvitationCode extends NoRollbackException {}

그리고 Transactional에서 noRollbackFor 옵션을 아래와 같이 적용한다.

@Transactional(noRollBackFor=NoRollbackException.class)
public void process() {
}

이렇게 하면 나름 일관적으로 작성되지 않을까 싶다. 더 좋은 방법이 분명히 있을 것도 같은데, 일단 가장 처음 생각난 방법을 적어본다.

profile
기억하기 위해 혹은 잊어버리기 위해 글을 씁니다.

0개의 댓글