본문 바로가기
프로그래밍

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

by 탁구치는 개발자 2013. 3. 5.

간혹 스프링 트랜잭션을 적용하였는데 예외 발생 시 롤백이 되지 않을 때가 있다.

안되는 이유야 여러 가지가 있겠지만 난 그 중 한 가지 문제에 대해서 작성하려고 한다.


일단 테스트하는 스프링 애플리케이션 컨텍스트의 트랜잭션 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에 대해서만 롤백을 해준다.

즉, <tx:method name="insert*" propagation="REQUIRED" /> 설정이 다음과 같이 rollback-for 옵션이 지정된 것 같다. 

<tx:method name="insert*" propagation="REQUIRED" rollback-for="RuntimeException" />

결과적으로 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에 대한 예외 처리를 하지 않아도 되기 때문에 코드의 가독성이 좋아진다.