@Transactional과 Spring AOP self-invocation

dominic·2022년 8월 31일
0

SpringAOP

목록 보기
1/1

멘토링 과정에서 질문으로 받았던 @Transactional의 사용과 관련된 내용에 대해 정리하려고 한다. 질문에 내용은 Spring AOP와 self-invocation에 관련된 내용이었으며 해당 상황을 코드로 표현하면 다음과 같다.

@Service
public class UserService {
	@Transactional
	public User findUserByEmail(String email) {
		return userMapper.findUserByEmail(email);    
    }

	public void loginUser(LoginUser loginUser) {
    	User user = findUserByEmail(loginUser.getEmail());
        .. 생략 ..
    }
}

위 코드는 현재 진행 중인 프로젝트의 과거 버전에 해당하는데 질문 내용에 부합되어 이를 통해 내용을 살펴보려 한다.

코드를 보면 loginUser에서 호출하는 findUserByEmail 메서드에 @Transactional annotation을 확인할 수 있다. 하지만 loginUser에는 아무런 annotation이 없는 상태다. 이때 findUserByEmail에 트랜잭션이 적용될 수 있는지가 질문이었고 대답을 하지 못했다.

결론을 먼저 이야기하자면 프록시 방식의 Spring AOP에서 findUserByEmail에는 트랜잭션이 적용되지 않는다.

# 원인

Spring 공식 문서(https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-declarative-annotations)에서 @Transactional을 살펴보면 다음 내용을 확인할 수 있다.

💡 In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation (in effect, a method within the target object calling another method of the target object) does not lead to an actual transaction at runtime even if the invoked method is marked with . Also, the proxy must be fully initialized to provide the expected behavior, so you should not rely on this feature in your initialization code — for example, in a method.@Transactional @PostConstruct

프록시 모드에서는 외부 메서드 호출 시에만 프록시가 동작하며 self-invocation(프록시 대상 객체 내에서 대상 객체의 다른 메서드를 호출하는 것) 과정에서는 실제 트랜잭션으로 이어지지 않는다. self-invocation을 예제에서 살펴보면 loginUser 메서드 내에서 findUserByEmail 메서드를 호출하는 것을 의미한다. 즉, loginUser에서 findUserByEmail을 호출하는 경우에는 @Transactional이 적용되지 않는 것이다.

그럼 loginUser 메서드의 @Transactional 적용 여부에 따라 프록시를 통한 호출이 어떤 차이를 보이는지 콜스택을 통해 살펴보자.

## loginUser 메서드에 @Transactional 적용 O

  • loginUser 메서드가 호출되는 과정에서 TransactionInterceptor를 통해 트랜잭션 처리가 적용된다.

## loginUser 메서드에 @Transactional 적용 X

  • findUserByEmail이 호출되기까지 콜스택을 살펴봐도 트랜잭션과 관련된 프록시 처리가 적용되지 않았음을 확인할 수 있다.

# 해결 방법

## AopContext 사용

public class UserService {
	@Transactional
	public User findUserByEmail(String email) {
		return userMapper.findUserByEmail(email);    
    }

	public void loginUser(LoginUser loginUser) {
		User user = ((UserService)AopContext.currentProxy()).findUserByEmail(loginUser.getEmail());
        .. 생략 ..
    }
}

## self injection 객체 사용

public class UserService {
	@Autowired ApplicationContext applicationContext;
	private UserService self;
    
    @PostConstructor
    private void init() {
    	self = applicationContext.getBean(UserService.class);
    }
    
	@Transactional
	public User findUserByEmail(String email) {
		return userMapper.findUserByEmail(email);    
    }

	public void loginUser(LoginUser loginUser) {
		User user = self.findUserByEmail(loginUser.getEmail());
        .. 생략 ..
    }
}

위 두 가지 방식을 사용하면 findUserByEmail 메서드 호출 과정에서 TransactionInterceptor를 거치는 것을 확인할 수 있다.

## AspectJ
마지막으로 Spring AOP의 Weaving 방식을 AspectJ Weaving 방식으로 바꾸는 방법이 있는데 공식 문서를 보면 이 방법을 권장한다. 간단히 설명하면 Spring AOP 방식과 다르게 바이트 코드를 직접 조작하기 때문에 self-invocation이 발생하지 않는다.

# 마무리

지금까지 프록시 방식의 AOP에서 발생하는 self-invocation과 이로 인해 발생하는 @Transaction이 적용되지 않는 문제에 대해 살펴보았다. 해결 방법 중 AspectJ를 간략하게 설명했는데 추후 포스팅에서 Spring의 AOP 기능 제공 방식에 대해 좀 더 깊이 공부하고 정리하는 시간을 갖도록 하겠다.

0개의 댓글