
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 라는 속성을 부여하여 해결 한 것이다.

'개발 이슈 해결' 카테고리의 다른 글
[ 개발 이슈 해결 ] Redis 이상한 문자가 key로 저장되는 문제 해결 (0) | 2023.11.22 |
---|---|
[ 개발 이슈 해결 ] 멀티스레드 테스트 중 오류 발생 (0) | 2023.11.21 |
[ 개발 이슈 해결 ] application.properties 작성하는 민감 정보가 github에 올라가지 않게 하는 방법 (0) | 2023.11.21 |