기록하며 성장한다 - 개발, 회고

개발 이슈 해결

[ 개발 이슈 해결 ] @Transactional 어노테이션이 있는데, 정상적으로 롤백이 이루어지지 않았던 문제점 해결

전대홍 2024. 4. 16. 21:13

 

1.1. 문제점

필자의 회사는 빅데이터 회사이고, 빅데이터 솔루션을 개발할 때에 Java 와 Spring 을 통해 개발을 진행한다. 이 때 Spring 을 사용 할 때 회사 자체내에서 유지보수를 용이하게 하기 위한 자체 프레임워크를 개발해 두었는데, 해당 프레임워크를 활용하여 개발하는 것이 방침이다.

@Override
@Transactional
public Object mainService(String serviceId, String serviceDtlOpt, Map<String, Object> inputParamMap) throws CustomCommonException { 
      switch (serviceDtlOpt) {
          case "A":
              return aMethod(serviceId, inputParamMap);
          case "B":
              return bMethod(serviceId, inputParamMap);
          default:
              throw new CustomCommonException(". . . Error");
      }
  }


private void aMethod(String mapperId, Map<String, Object> inputParamMap) throws CustomCommonException {
    List<Map<String, Object>> paramList = (List<Map<String, Object>>) inputParamMap.get("params");
    try {
        // ... List를 받아와서, for 문으로 각각 update
    } catch (Exception e) {
        e.printStackTrace();
        throw new CustomCommonException(". . . Error");
    }
}


...

기본적으로 이런 양식을 사용하는데, ( 물론 회사의 코드이기 때문에 전체적으로 많이 수정하였다. )

aMethod 에서 insert 를 할 때, 예를들어 10개의 데이터가 들어가다가 6번 째 데이터를 insert 하는 도중에 에러가 발생하면, CustomCommonException 에러가 발생하고, 문제가 발생하게 된다.

@Transactional 이 있기 때문에, 1 ~ 5번째 insert 된 데이터도 모두 rollback 되어야 하지만, 어째서인지 rollback 이 되지 않는 문제가 발생한 것이다.
( 참고로, JPA 의 saveAll( ) 을 사용하면 편하지만, 필자의 회사는 Mybatis 를 사용하기에 그렇게 해결 할 수는 없다. )

이 문제를 해결하기 위해 여러 방법으로 디버깅도 해보았고, 트랜잭셕의 propergation 도 수정해보는 등 많은 시도를 해보게 되었고, 1시간 정도 씨름한 끝에 문제점을 알 수 있게 되었다.

윽! 이 오류는 뭐지!?



 

1.2. 해결

처음에는 트랜잭션 전파에서 문제가 발생한 것이 아닐까 라는 생각이 들었다.

그래서 사내 프레임워크 사용 규칙에는 위배되지만, 문제를 해결 할 수 없다면 잘못된 프레임워크인 것이다라는 마인드로, 새로운 Service 를 만들어서 Transaction 을 옮겨주었다. 그러나 self-invocation 문제도 아니고, 문제는 해결되지 않았다.

그러던 도중 필자의 눈에 띈 것은, 사내 프레임워크에서 사용하는 CustomException 이었다.

CustomCommonException ( 실제 클래스 명은 이게 아님 ) 을 들어가보니, Exception 을 그대로 상속 받고 있었다.

드디어 문제점을 발견한 것이다.

그래서 그 문제를 해결하기 위해, @Transactional 어노테이션에 아래와 같은 코드를 추가하였고, 문제를 해결 할 수 있었다.

@Transactional(rollbackFor={CustomCommonException.class})

 

 

 

1.3. 해결 방법 및 근거

어떤 근거로 문제를 해결 할 수 있었던 것일까?

과거 공부했던 내용과, 인터넷으로 문제점을 찾아본 내용을 바탕으로 문제를 해결 할 수 있었는데, Transaction 과 Exception 에 대한 내용이 근거가 되었다.

스프링은 기본적으로 Checked Exception은 비즈니스 의미가 있을 때 사용하고, Runtime Exception은 복구가 불가능한 예외라고 가정한다.

그래서 Checked Exception 의 경우에는 Transactional 어노테이션이 있어도, rollback 을 해주지 않는다. 그렇기 때문에 rollback 을 원하는 경우 RuntimeException 을 상속 받아야 한다. ( 그냥 Exception 을 받으면, Checked 와 UnChecked (Runtime) 을 포함하기 때문에 ) 

여기서, 비즈니스 의미가 있는 예외는 뭘까? 고객이 결제를 하는데, 잔고가 부족해서 예외가 발생하거나 하는 오류가 있을 수 있다. 결제 잔고가 부족한 것은 시스템에 문제가 있어서 발생하는 예외가 아니다. 오히려 시스템은 비즈니스의 규칙(잔고가 부족하면 결제를 못한다)에 충실하게 처리한 것이다. 이 비즈니스 예외는 매우 중요하고, 반드시 처리해야 하는 경우가 많으므로 Checked Exception을 고려할 수 있다.

그럼, 복구 불가능한 예외는 뭘까? 데이터베이스 접근이 안되거나, SQL문에 오류가 있거나, 네트워크 통신이 안되는 등의 경우에는, 예외를 잡았다고 하더라도 처리를 할 수가 없기 때문에 Runtime Exception으로 만들어서, 상위 계층으로 올려 고객님께 죄송하다는 말을 전달하는 편이 좋다.

위와 같은 이유로, 필자가 개발하려 하는 기능에서, 사내 프레임워크의 구조를 망가뜨리지 않고 정상적으로 rollback 을 진행하려면, 강제적으로 rollback 기능을 부여해야 하고, 그 때문에 @Trancsactional 어노테이션에 rollbackFor 라는 속성을 부여하여 해결 한 것이다.

 

하~ 편안하다 !!!