Spring Framework

Spring MVC Controller에서 transaction이 적용되지 않는다. (Self-Invocation)

binarycastle 2019. 10. 11. 15:32

Spring 2.5 버전을 사용중인 프로젝트를 3.2 버전으로 마이그레이션하고 있었다.

모든 XML 기반 설정들을 Java 기반으로 변경하는 작업도 진행했는데, 이 XML 설정은 선언적 트랜잭션 설정도 포함하고 있었다. (CGLIB 사용)

 

다음과 같이 말이다.

<tx:advice id="txAdvice" transaction-manager="transactionManager">
  <tx:attributes>
  	<tx:method name="*" propagation="REQUIRED" />
  </tx:attributes>
</tx:advice>

<aop:config>
  <aop:pointcut id="txMethod" expression="execution(* kr.co.iquest.admin..*.*(..))" />
  <aop:advisor advice-ref="txAdvice" pointcut-ref="txMethod"/>
</aop:config>

 

아래는 Java config로 옮긴 모습이다.

@Bean
public TransactionInterceptor transactionInterceptor(DataSourceTransactionManager transactionManager) {
	return new TransactionInterceptor(transactionManager, transactionAttributeSource());
}

@Bean
public TransactionAttributeSource transactionAttributeSource() {
	NameMatchTransactionAttributeSource transactionAttributeSource = new NameMatchTransactionAttributeSource();
	transactionAttributeSource.addTransactionalMethod("*", new RuleBasedTransactionAttribute());

	return transactionAttributeSource;
}

@Bean
public AspectJExpressionPointcutAdvisor transactionAdvisor(TransactionInterceptor advice) {
	AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
	advisor.setAdvice(advice);
	advisor.setExpression("execution(* kr.co.iquest.admin..*.*(..))");
  
	return advisor;
}

 

이렇게 pointcut과 그 외 속성 모두를 분명 똑같이 옮겼는데 며칠간 운영해보니 트랜잭션 관련 에러 로그가 곳곳에서 찍히고, 심지어 새벽에 배치 작업이 진행되다가 해당 스케줄러에서 사용하는 일부 DB 테이블이 blocking되는 현상까지 발생했다.

 

트랜잭션 설정이 뭔가 잘못된 것 같았다.

 

blocking 현상이 일어나던 배치 작업 코드를 먼저 살펴봤다.

요청 -> DB stored procedure 실행 -> 사내 서비스 API 호출 -> 응답

간단하게 설명하면 위와 같은 프로세스였다.

 

stored procedure와 API에서 같은 테이블을 update하고 있었다.

그렇다면 서로 다른 커넥션에서 동일한 테이블을 update하고 있고, 이 작업은 한 트랜잭션에 묶여있으므로 blocking되는 것이 맞다.

그런데 왜 마이그레이션 이전 코드에서는 에러가 발생하지 않았을까.

트랜잭션 설정은 두 코드 모두 동일하지만 내가 모르는 뭔가가 다르게 동작하는 것이 분명하다.

 

결론부터 말하면, 기존 코드에서는 Controller에 트랜잭션이 적용되지 않고 있었다.

Controller mapping도 annotation 기반이 아닌 XML 기반이었기 때문에 Spring Framework에서 제공하는 Controller 인터페이스의 구현체들을 상속받아 사용했다.

사용했던 구현체 중에는 AbstractController, SimpleFormController, MultiActionController 등이 있는데 이 중 AbstractController로 확인해보겠다.

 

아래는 AbstractController의 코드이다.

public abstract class AbstractController extends WebContentGenerator implements Controller {

	private boolean synchronizeOnSession = false;

	public final void setSynchronizeOnSession(boolean synchronizeOnSession) {
		this.synchronizeOnSession = synchronizeOnSession;
	}

	public final boolean isSynchronizeOnSession() {
		return this.synchronizeOnSession;
	}

	public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
			throws Exception {

		// Delegate to WebContentGenerator for checking and preparing.
		checkAndPrepare(request, response, this instanceof LastModified);

		// Execute handleRequestInternal in synchronized block if required.
		if (this.synchronizeOnSession) {
			HttpSession session = request.getSession(false);
			if (session != null) {
				Object mutex = WebUtils.getSessionMutex(session);
				synchronized (mutex) {
					return handleRequestInternal(request, response);
				}
			}
		}

		return handleRequestInternal(request, response);
	}

	protected abstract ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
	    throws Exception;

 

아래와 같이 AbstractController 클래스를 상속받아서 handleRequestInternal 메서드를 구현한다.

public class FooController extends AbstractController {
	
	@Override
	protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
		// Do something...
	}
}

 

그리고 FooController의 proxy는 아래와 같이 생성된다.

class Cglib$FooController extends FooController {

	@Override
	public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		super.handleRequest(request, response);
	}

	@Override
	protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		// Do something for transaction...
		super.handleRequestInternal(request, response);
	}
}

위의 프록시 클래스 코드에서 handleRequest 메서드는 결과적으로 부모 클래스인 FooController의 handleRequest 메서드를 호출하는 것을 확인할 수 있다.

여기서 중요한 점은, Request가 들어올 때 Spring Framework에서는 우리가 구현한 handleRequestInternal 메서드가 아닌 handleRequest 메서드를 호출한다는 것이다.

handleRequestInternal 메서드는 handleRequest 메서드 내부에서 호출된다.

결국 트랜잭션을 적용하려는 handleRequestInternal 메서드는 프록시 클래스가 아닌 FooController 클래스에서 호출된다.

 

이처럼 어떤 객체의 메서드 내부에서 해당 객체의 다른 메서드를 호출했을 때는 AOP가 적용되지 않는다(Proxy 기반 AOP의 경우). 이를 self-invocation이라고 한다.

 

아래는 Stack Overflow에서 발견한 답변 내용 중 일부이다.


In proxy mode (which is the default), only 'external' method calls coming in through the proxy will be intercepted. This means that 'self-invocation', i.e. a method within the target object calling some other method of the target object, won't lead to an actual transaction at runtime even if the invoked method is marked with @Transactional!

 

Reference

https://stackoverflow.com/questions/34197964/why-doesnt-springs-transactional-work-on-protected-methods