스프링 트래잭션 롤백이 되지 않을 때 확인 사항

간혹 스프링 트랜잭션을 적용하였는데 예외 발생 시 롤백이 되지 않을 때가 있다.
안되는 이유야 여러 가지가 있겠지만 난 그 중 한 가지 문제에 대해서 작성하려고 한다.

일단 테스트하는 스프링 애플리케이션 컨텍스트의 트랜잭션 AOP 설정은 다음과 같이 선언적 트랜잭션을 사용하였다.
service 패키지 하위에 있는 모든 클래스 중 insert*, delete*, update* 이름에 매칭되는 메소드에 트랜잭션 설정

<tx:advice id=”txAdvice” transaction-manager=”transactionManager”>
    <tx:attributes>
        <tx:method name=”get*” read-only=”true” />
        <tx:method name=”find*” read-only=”true” />
        <tx:method name=”insert*” propagation=”REQUIRED” />
        <tx:method name=”delete*” propagation=”REQUIRED” />
        <tx:method name=”update*” propagation=”REQUIRED” />
    </tx:attributes>

</tx:advice><aop:config>
    <aop:pointcut id=”servicePublicMethod” expression=”execution(public * com.incross.svc.component..service..*.*(..))” />
    <aop:advisor advice-ref=”txAdvice” pointcut-ref=”servicePublicMethod” />
</aop:config>

테스트 코드는 다음과 같다.
문제가 발생되는 원인에 대해서 보여주려고 실패 case에 대한 메소드를 생성하였다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {“/test-application-context.xml”})
@ActiveProfiles(“dev”)
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    public void 트랜잭션롤백테스트_실패case() throws Exception {
        User user = new User();
        user.setUserId(“abc1111”);
        user.setPassword(“1111”);
        user.setUserName(“kyu”);

        User user1 = new User();
        user1.setUserId(“abc2222”);
        user1.setPassword(“2222”);
        user1.setUserName(“kyu”);

        userService.insertUser(user, user1);
    }
}

서비스 클래스의 insertUser 메소드 내부 코드이다.
다음과 같이 insert를 두 번 한 후 FileNotFoundException을 강제 발생하였다.

public void insertUser(User user, User user1) throws FileNotFoundException {
    userDAO.insertUser(user);userDAO.insertUser(user1);// checked Exception 강제로 발생
    throw new FileNotFoundException();
}

위와 같이 코드를 작성한 후 테스트를 돌리면 어떻게 될까?
아마 두 번 실행된 insertUser 트랜잭션에 대해 정상적으로 롤백이 되어야 한다고 생각한다.

하지만, 스프링 트랜잭션 AOP는 default 옵션으로 unchecked Exception인 RuntimeException에 대해서만 롤백을 해준다.
즉, 설정이 다음과 같이 rollback-for 옵션이 지정된 것 같다.

결과적으로 insert* 메소드에서 RuntimeException 발생 시에만 자동 롤백을 해준다는 것이다.

만약 FileNotFoundException 발생 시에도 롤백을 지원하고 싶다면 rollback-for=”Exception” 와 같이 설정하면 된다.
하지만 난 이 방법은 추천하고 싶지 않다.

Checked Exception인 FileNotFoundException 발생 시 try catch 블록을 이용하여 RuntimeException 으로 포장하는 편이
더 깔끔한 코드를 유지할 수 있기 때문이다.

public void insertUser(User user, User user1) {
    userDAO.insertUser(user);
    userDAO.insertUser(user1);

    try {
        // checked Exception 강제로 발생
        throw new FileNotFoundException();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    }
}

위의 코드를 보면 throws FileNotFoundException이 사라졌다.
결국 insertUser 메소드를 호출하는 쪽에서 FileNotFoundException에 대한 예외 처리를 하지 않아도 되기 때문에 코드의 가독성이 좋아진다.