@Transactional public Member saveMember(Member member){ Member dbMember = memberRepository.save(member); if (1==1) { thrownew RuntimeException("테스트를 위해 강제로 발생시킨 예외"); } return dbMember; } }
InitRunner에 다음과 같이 HelloService.saveMember()를 호출하고 예외를 잡아 처리하는 코드를 추가한다.
// Member member = new Member("Homo Efficio", "homo.efficio@gmail.com"); // try { // Member dbMember = helloService.saveMember(member); // log.info("TTT 회원 [{}] 추가됨", dbMember); // } catch (Exception e) { // log.error("TTT 회원 추가 중 예외 발생. 메시지: {}",e.getMessage()); // } }
실행해보면 롤백이 적용되지 않으므로 다음과 같이 저장 후 예외가 발생하더라도 레코드가 추가되는 것을 확인할 수 있다.
RemoteSimpleJob에서 @Transactional 사용
이제 다음과 같이 작업 클래스의 execute() 메서드에 @Transactional을 붙여서 실행해보자.
try { Member dbMember = memberRepository.save(new Member("Homo Efficio", "homo.efficio@gmail.com")); log.info("TTT 회원 [{}] 추가됨", dbMember); if (1==1) { thrownew RuntimeException("테스트를 위해 강제로 발생시킨 예외"); } } catch (Exception e) { log.error("TTT 회원 추가 중 예외 발생. 메시지: {}",e.getMessage()); } }
이번에는 안타깝게도 다음과 같은 에러를 만나게 된다. 지면을 많이 차지하니 지스트(Gist) 링크로 대신하고, 주요 부분만 살펴보면 다음과 같다.
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'io.homo_efficio.quartz.job.RemoteSimpleJob': Initialization of bean failed; nested exception is org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class io.homo_efficio.quartz.job.RemoteSimpleJob: Common causes of this problem include using a final class or a non-visible class; nested exception is org.springframework.cglib.core.CodeGenerationException: java.lang.NoClassDefFoundError-->io/homo_efficio/quartz/job/RemoteSimpleJob
Caused by: org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class io.homo_efficio.quartz.job.RemoteSimpleJob: Common causes of this problem include using a final class or a non-visible class; nested exception is org.springframework.cglib.core.CodeGenerationException: java.lang.NoClassDefFoundError-->io/homo_efficio/quartz/job/RemoteSimpleJob
긴 내용이지만 요약하면 @Transactional 기능을 추가하기 위해서 RemoteSimpleJob의 프록시 객체를 CGLib 라이브러리를 이용해서 생성해야 하는데, 이때 RemoteSimpleJob 클래스를 찾을 수 없다는 얘기다.
@Transactional이 없을 때는 프록시 객체를 만들 필요가 없으므로 RemoteSimpleJob은 우리가 만든 커스텀 클래스로더를 통해 정상적으로 로딩되어 실행된다. 하지만 @Transactional이 붙어서 CGLib을 통해 프록시 객체를 생성할 때는 우리가 만든 커스텀 클래스로더가 사용되지 못하므로 RemoteSimpleJob을 찾지 못하고 위와 같은 에러가 발생하게 된다. 그림으로 보면 대략 다음과 같다.
그럼 CGLib이 사용하는 클래스로더가 RemoteSimpleJob을 로딩할 수 있게 만들면 이 문제도 해결될 것 같다. CGLib는 애플리케이션 구동 환경에서 정해진 클래스로더를 사용하는데 대략 다음과 같다.
Standalone Tomcat 환경이라면 context.xml 파일에 <Loader> 엘리먼트를 통해 커스텀 클래스로더를 지정할 수 있고, 스프링부트 환경이라면 PropertiesLauncher 클래스와 loader.path 속성으로 클래스로더를 지정할 수 있고, 가장 범용적으로는 manifest 파일에 Class-Path로 로딩할 클래스가 포함된 클래스패스를 지정해주면 된다.
이론적으로는 그런데 실제로는 애플리케이션 구동 환경 자체도 로컬 개발 환경, 서버 환경 모두 다르고, 그에 따라 스프링 내부에서 구동되는 CGLib이 RemoteSimpleJob을 로딩할 수 있게 하려면 스프링 내부에 대한 심도있는 지식이 필요하다. 그걸로 끝나는 게 아니라 RemoteSimpleJob이 참조하는 다른 클래스, 그 클래스가 참조하는 다른 클래스, 그 클래스가 참조하는 다른 클래스… 를 모두 로딩할 수 있어야 한다.
이것저것 시도해보다가 CGLib이 RemoteSimpleJob에 대한 프록시 객체를 생성하게 만드는 데 간신히 성공했다. TRACE 로그로 확인할 수 있었다. (이건 예전에 했던 내용이라 클래스 이름 등은 현재 예제와 좀 다르다 ;;)
그런데 로그를 자세히 보면 'o.s.aop.framework.CglibAopProxy : Unable to apply any optimizations to advised method: [[[@Transactional_붙어있는_메서드]]]' 대략 이런 내용이 찍히고, 실제로도 트랜잭션 롤백이 동작하지 않았다.
더 결정적인 문제도 있는데 이렇게 스프링이 제공해주는 프록시 생성 로직을 통해 프록시로 등록되면 해당 프록시는 캐시된다는 점이다. 그래서 나중에 RemoteSimpleJob의 내용을 바꿔서 작업 클래스 모듈의 jar 를 새로 생성한 후에 RemoteSimpleJob을 다시 수행해도 새 프록시가 생성되지 않고 기존 내용을 기준으로 생성되어 캐시된 프록시가 사용될 수 있다. 이러면 작업 클래스 모듈을 분리한 의도가 퇴색되어 버리는 결과가 된다.
이런 모든 문제를 해결하려면 할 수도 있겠지만 나는 이 정도에서 멈추기로 한다. 왜냐하면 스프링에서는 @Transactional이 아니라도 PlatformTransactionManager를 사용해서 트랜잭션 기능을 추가할 수 있기 때문이다.
PlatformTransactionManager 사용
다음과 같이 @Transactional을 제거하고 PlatformTransactionManager를 사용하도록 RemoteSimpleJob을 개선한다.