원본 본문으로 이동하기

Spring @Transactional Method 적용범위 : rollback 주의

박용서 - 서론 항상 SQL 이나 jdbc의 트랜잭션을 사용해오다가 이번에 서블릿에서 스프링으로 갈아타면서 Transactional 을 사용해 보았습니다. Transactional에 대해서 찾아보던 중 자동생성된 객체의 처음으로 불린 메서드의 구문을 그대로 딸아간다는 강의를 보고 테스트해보았습니다. 예제 : Spring Boot, PostgreSQL, MyBatis 테이블 구조 -- 유저 (예제로 급하게만들다보니 전체적인 네이밍이 망한...;;) CREATE TABLE users ( no bigint NOT NULL, name character varying(64) NOT NULL ); CREATE SEQUENCE users_no_seq; -- 유저 데이터 CREATE TABLE user_data ( no bigint NOT NULL, name character varying(12) NOT NULL, value character varying(12) NOT NULL ); CREATE UNIQUE INDEX user_data_uni ON user_data USING btree (no, name) 매퍼 @Mapper public interface UsersMapper { @Insert("INSERT INTO users (no, name) VALUES (#{no}, #{name})") long addUser(@Param("no") long no, @Param("name") String name); @Select("SELECT nextval('users_no_seq')") long getKey(); } @Mapper public interface UserDataMapper { @Insert("INSERT INTO user_data (no, name, value) VALUES (#{no}, #{name}, #{value})") long addData(@Param("no") long no, @Param("name") String name, @Param("value") String value); } 서비스 @Component public class UserDemoService { SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd"); @Autowired UsersMapper usersMapper; @Autowired UserDataMapper userDataMapper; @Transactional public long addUser(String name) { long no = usersMapper.getKey(); String join_date = SDF.format(new Date()); usersMapper.addUser(no, name); userDataMapper.addData(no, "join_date", join_date); return no; } } 실행 @SpringBootApplication public class App implements CommandLineRunner { Logger logger = LoggerFactory.getLogger(App.class); @Autowired UserDemoService userService; @Override public void run(String... args) throws Exception { logger.info("유저추가 : " + userService.addUser("abc")); } public static void main(String[] args) { SpringApplication.run(App.class, args); } } 실행결과 1;"abc" 1;"join_date";"2016-07-18" 오류를 내보자!! SQL 오류 // UserDemoService 클래스 @Transactional public long addUser(String name) { long no = usersMapper.getKey(); String join_date = SDF.format(new Date()); usersMapper.addUser(no, name); userDataMapper.addData(no, "NULL 을 넣어도 되겠지만 (NOT NULL 이라..) 그냥 글자수를 많이 써보자.!!", join_date); return no; } // App 클래스 run logger.info("유저추가 : " + userService.addUser("SQL 오류!")); 결과 : 양 테이블 모두 롤백 되었고 키는 롤백되지 않았습니다. (하지만 pg-sql의 키는 트랜잭션과 무관하게 지나간다.. : 그럼으로 이후 생략.) 자바 오류 // UserDemoService 클래스 @Transactional public long addUser(String name) { long no = usersMapper.getKey(); String join_date = SDF.format(new Date()); usersMapper.addUser(no, name); userDataMapper.addData(no, "join_date", join_date); int error = 0 / 0; // 자바오류를 내보았다.!! return no; } // App 클래스 run logger.info("유저추가 : " + userService.addUser("자바 오류!")); 결과 : 동일 (신기하다.. 제어역전을 매퍼가 이벤트만 날리는걸 받을 줄 알았는데 throws 도없는 자바 런타임익셉션을!!) 하지만 문제는 다음!! (트랜잭션 메서드를 직접 호출하지 않았다!) // UserDemoService 클래스 public long addUser(String name) { return proxyAddUser(name); } @Transactional public long proxyAddUser(String name) { long no = usersMapper.getKey(); String join_date = SDF.format(new Date()); usersMapper.addUser(no, name); userDataMapper.addData(no, "join_date", join_date); int err = 0 / 0; // 오류!! return no; } // App 클래스 run logger.info("유저추가 : " + userService.addUser("트랜잭션 테스트")); 결과 4;"트랜잭션 테스트" 4;"join_date";"2016-07-18" 해설 테스트가 아닌 서비스 였다면, 불안정한 데이터를 넣어 잠재적인 버그를 만들어낸 것입니다.!! 그냥 볼때는 문제 없어보이는 코드이지만.. 스프링에서 Transactional 은 인스턴스에서 처음으로 부르는 메서드의 속성(클래스의 속성 포함)을 따라가게 되어 있습니다. - 호출될때의 메서드/클래스의 트랜잭셔널 어노테이션을 긁어서 보관하면서 다른 메서드(해당 객체 내 메서드)로 넘어가는 것을 확인하지 않고 그상태를 그대로 유지하는 것 같습니다. 그래서 아래와 같이 적용해 보았습니다. // UserDemoService 클래스 @Transactional public long addUser(String name) { return proxyAddUser(name); } // @Transactional 있으나 없으나 이 함수를 부른 addUser 의 속성을 따라가게된다. private long proxyAddUser(String name) { long no = usersMapper.getKey(); String join_date = SDF.format(new Date()); usersMapper.addUser(no, name); userDataMapper.addData(no, "join_date", join_date); int err = 0 / 0; // 오류!! return no; } // App 클래스 run logger.info("유저추가 : " + userService.addUser("트랜잭션 테스트2")); 결과 롤백되었다!! - 스프링 자바직접호출로는 적용안되는걸 보니 AOP 활용하나보네요 박용서 아마 기본 AOP의 프록시는 처음 리플렉션시 어노테이션만 기억하는 것 같습니다. 물론 AspectJ AOP를 이용하면 처음불리는 곳에 트랜잭션이 없더라도 적용시킬 수 있습니다. http://docs.spring.io/autorepo/docs/spring/4.2.x/spring-framework-reference/html/transaction.html하긴 그거말고 방법이 없긴함