[Spring Boot] Spring AOP Self Invocation과 @Transactional

2025. 4. 7. 14:53·Spring/기초 개념

발생한 문제

우선 아래 코드에서는 트랜잭션이 정상적으로 작동하지 않는다.

public class UserService {
    @Autowired
    private UserRepository userRepository;

    /* 사용자 토큰 저장 재귀 호출*/
    public void saveUserInfo(UserDto userDto){
        saveUserToken(userDto.getEmail());
    }

    /* 토큰 저장 */
    @Transactional
    public void saveUserToken(String email){
        String token = email + "_token";
        userRepository.saveToken(email, token);
    }
}

 

이 코드에서는 AOP의 self invocation으로 인해 saveUserToken에 트랜잭션이 전파되지 않고 있다.

 

그렇다면 이러한 문제는 왜 발생하는 것이며 어떻게 해결할 수 있을지 알아보려고 한다.

 

Spring의 AOP와 Self Invocation

Spring AOP

우선 Spring AOP에 대해서 알아보자. 우선 아래는 Spring 공식문서에서 제공하는 프록시를 생성 및 호출하는 코드이다.

public class Main {

	public static void main(String[] args) {
		ProxyFactory factory = new ProxyFactory(new SimplePojo());
		factory.addInterface(Pojo.class);
		factory.addAdvice(new RetryAdvice());

		Pojo pojo = (Pojo) factory.getProxy();
		// this is a method call on the proxy!
		pojo.foo();
	}
}

 

여기서 프록시의 advice 실행 및 target 오브젝트의 메서드(foo)가 어떻게 진행되는지 알아보자.

  1. 클라이언트가 프록시 객체의 메서드 호출
  2. proxy는 advice 실행 후 target( plain object )의 메서드 호출
  3. target의 메서드가 실행됨

즉 정리하자면, 아래와 같다.

  • 프록시 객체의 메서드를 호출하면, advice는 프록시 객체에서 실행되지만 대상 메서드는 원본 객체에서 실행된다.
  • advice는 프록시 객체를 통해 메서드를 호출할 때만 실행된다.
  • 원본 객체는 프록시 객체가 아니다.

Spring AOP Self Invocation

이번에는 Spring AOP의 Self Invocation( 자기 호출 )에 대해서 알아본다.

 

아래 코드는 프록시 객체의 원본 객체 코드이다. 

결론부터 말하자면, 프록시 객체에서 foo를 호출한다면 advice는 foo에만 적용되고 bar에는 적용되지 않는다.

public class SimplePojo implements Pojo {

	public void foo() {
		// this next method invocation is a direct call on the 'this' reference
		this.bar();
	}

	public void bar() {
		// some logic...
	}
}

 

foo는 bar를 호출하고 있고, bar를 호출하는 것은 자기 자신을 참조하여 호출하는 것을 볼 수 있다.

보다 자세히 설명하자면, foo에서 bar를 호출하는 것은 프록시가 아닌 원본 객체를 통해 호출되는 것이다.

 

만약 SimplePojo가 Bean이어도 결과는 같다.

프록시가 적용된 빈이 생성되었더라도, advice는 프록시 객체가 호출하고 foo는 원본 객채의 메서드가 호출된다.

 

우리는 Spring AOP를 알아볼 때, advice가 실행되기 위해서는 프록시 객체를 통해야 하는 것을 배웠다.

따라서 위 코드에서 foo에는 advice가 적용되지만, bar에는 advice가 적용되지 않음을 알 수 있다.

 

이처럼 Self Invocation은 객체가 자신의 메서드를 직접 호출했을 때, 해당 메서드에는 advice가 적용되지 않는 것을 말한다.

 

@Transcational에서의 Self Invocation

Spring AOP 기반으로 동작하는 @Transactional

우선 @Transactional은 AOP 기반으로 동작하는 대표적인 어노테이션이다.

 

실제로 이를 확인하기 위해 아래와 같이 코드를 작성했다.

@Service
public class CompilationService {
    @Transactional
    public CompilationResponseDto compileCppCode(String code, String userInput) throws IOException {
        // command 생성
        String[] command = generateCommand(code, userInput);
        // 프로세스 시작
        ProcessBuilder pb = new ProcessBuilder(command);
        pb.redirectErrorStream(true);
        Process process = pb.start();
        // 실행 결과 확인
        return getOutputDto(process);
    }
}

 

만약, @Transactional이 Spring AOP 기반으로 동작한다면, 해당 빈을 DI 받았을 때 원본 객체가 아닌 프록시 객체를 주입받아야 한다.

 

실제로 빈을 주입받으면, CompilationService가 아닌 CompilationService@370a935f를 주입받았음을 확인할 수 있다.

이는 JPA가 @Transactional이 적용된 원본 객체를 프록시 객체로 생성하여 빈으로 등록했다는 것을 나타낸다

@Transcational에서 Self Invocation

이제 다시 기존 코드를 확인해보자.

public class UserService {
    @Autowired
    private UserRepository userRepository;

    /* 사용자 토큰 저장 재귀 호출*/
    public void saveUserInfo(UserDto userDto){
        saveUserToken(userDto.getEmail());
    }

    /* 토큰 저장 */
    @Transactional
    public void saveUserToken(String email){
        String token = email + "_token";
        userRepository.saveToken(email, token);
    }
}

 

우선 우리는 기존에 학습한 내용을 바탕으로 아래 내용들을 확인할 수 있다.

  • UserService는 @Transactional이 적용되어 있으므로 DI받으면 프록시 객체로 주입받는다.
  • saveUserInfo에서 saveUserToken을 호출하고 있지만, 실제로는 saveUserInfo에서 트랜잭션이 시작되지 않기에 saveUserToken으로 전파되지 않는다.

따라서 결론적으로 위 코드에서 saveUser를 실행해도 트랜잭션이 적용되지 않음을 확인할 수 있다.

 

@Transactional의 self invocation 해결하기

전체 개요

우리는 이번에는 위 코드에서 발생한 self invocation 문제를 해결할 수 있는 4가지 방법에 대해서 알아본다.

  1. 부모 트랜잭션 전파
  2. Avoid self invocation
  3. Inject a self reference
  4. AspectJ 사용

해결방법 1 : 부모 트랜잭션 전파

우선 첫 번째 방법은 부모 트랜잭션을 자식 트랜잭션으로 전파하는 것이다.

public class UserService {
    @Autowired
    private UserRepository userRepository;

    /* 사용자 토큰 저장 재귀 호출*/
    @Transcational
    public void saveUserInfo(UserDto userDto){
        saveUserToken(userDto.getEmail());
    }

    /* 토큰 저장 */
    @Transactional
    public void saveUserToken(String email){
        String token = email + "_token";
        userRepository.saveToken(email, token);
    }
}

 

기존에 saveUserToken에 트랜잭션이 적용되지 않았던 이유는 saveUserInfo에서 트랜잭션이 시작되지 않았기 때문이었다.

 

따라서 이처럼 saveUserInfo에서 트랜잭션이 시작되도록 한다면, 해당 트랜잭션이 자식 트랜잭션으로 전파되므로 트랜잭션이 적용될 것이다.

해결방법 2 : Avoid Self Invocation

기존에 saveUserInfo를 호출했을 때, saveUserToken에 트랜잭션이 적용되지 않았던 이유는 saveUserInfo가 원본 객체의 메서드를 호출했기에 트랜잭션 AOP가 작동하지 않았기 때문이다.

 

따라서 saveUserInfo와 saveUserToken을 서로 다른 객체에 두고, saveUserToken이 있는 프록시 객체를 주입받아 사용하면 정상적으로 트랜잭션이 적용된다.

@Service
public class UserTxService {
    @Autowired
    private UserRepository userRepository;

    /* 토큰 저장 */
    @Transactional
    public void saveUserToken(String email){
        String token = email + "_token";
        userRepository.saveToken(email, token);
    }
}

@Service
public class UserService {
    @Autowired
    private UserTxService userTxService;

    /* 사용자 토큰 저장 재귀 호출*/
    public void saveUserInfo(UserDto userDto){
        userTxService.saveUserToken(userDto.getEmail());
    }
}

 

이번에는 saveUserInfo에서 원본 객체의 메서드가 아닌 프록시 객체인 userTxService의 메서드를 호출하도록 했다.

 

따라서 정상적으로 saveUserToken에서 트랜잭션이 시작될 것이다.

 

이 방법은 Spring AOP 공식 문서에서 가장 추천하는 방법이다.

해결방법 3 : Inject a self references

만약 같은 클래스 내의 메서드를 원본 객체가 아닌 프록시 객체를 통해서 호출할 수 있다면, 기존 문제가 해결될 것임을 알 수 있다.

 

따라서 아래와 같이 자기 자신을 프록시 객체로 DI 받고, 해당 객체를 통해서 자기 자신의 메서드를 호출하면 문제가 해결된다.

@Service
public class UserService {
    @Autowired
    private UserService self;

    /* 사용자 토큰 저장 재귀 호출*/
    public void saveUserInfo(UserDto userDto){
        self.saveUserToken(userDto.getEmail());
    }

    /* 토큰 저장 */
    @Transactional
    public void saveUserToken(String email){
        String token = email + "_token";
        userRepository.saveToken(email, token);
    }
}

 

여기서 self는 원본 객체가 아닌, 스프링 컨텍스트를 통해 주입받은 프록시 객체이다.

 

따라서 위 코드에서 saveUserInfo를 호출한다면 saveUserToken에 트랜잭션이 정상적으로 적용된다.

 

하지만, 이 방식은 가독성을 떨어뜨린다는 단점이 있다. 리뷰어 입장에서는 자기 참조를 하는 이유를 추론해야하기 때문이다.

해결방법 4 : AspectJ 사용하기

마지막 방법은 AspectJ를 사용하여 saveUserToken을 프록시를 통해 호출하는 방법이다.

@Service
public class UserService {
    /* 사용자 토큰 저장 재귀 호출*/
    public void saveUserInfo(UserDto userDto){
        ((UserService) AopContext.currentProxy())
            .saveUserToken(userDto.getEmail());
    }

    /* 토큰 저장 */
    @Transactional
    public void saveUserToken(String email){
        String token = email + "_token";
        userRepository.saveToken(email, token);
    }
}

 

이처럼 원본 객체를 통해 메서드를 호출하는 것이 아닌, 클래스 내부 로직을 Spring AOP에 연결해버린다.

 

하지만 이 방법은 매우 추천되지 않는다. 코드가 Spring AOP에 강하게 결합되어버리고 AOP를 사용한다는 것이 노출되어 AOP의 장점이 일부 사라진다.

 

마무리

이번에는 AOP에 대해서 간단하게 알아보았고, 이를 기반으로 self invocation 문제를 알아보았다.

또한 더 나아가 @Transcational에서 self invocation이 야기하는 문제에 대해서 알아보고 이에 대한 해결방법을 알아보았다.

 

간단하게 정리하면 아래와 같다.

  • Spring AOP
    • 프록시 객체를 통해 메서드를 호출할 때만 advice가 적용된다.
    • 원본 객체를 통해 메서드를 호출한다면, 해당 메서드에는 advice가 적용되지 않는다.
  • @Transactional
    • Spring AOP 기반으로 동작하는 어노테이션이다.
  • @Transactional self invocation
    • @Transcational이 적용된 메서드를 호출하는 @Transcational이 적용되지 않은 메서드를 호출하면 정상적으로 트랜잭션이 시작되지 않는다.
  • @Transcational self invocation 해결
    • 부모 트랜잭션 전파
    • 트랜잭션이 필요한 기능이 프록시 객체를 통해서 호출되도록 하기
    • 자기 참조를 통해 자기 자신을 프록시 객체로 주입받아서 사용하기
    • AspectJ 사용하기

'Spring > 기초 개념' 카테고리의 다른 글

[Spring Boot] MSA에서 git action과 docker를 활용한 Docker CI/CD 구축  (1) 2025.04.21
[Spring Boot] Spring MVC + WebClient 환경에서 JPA 사용하기  (0) 2025.04.09
Spring Data JPA의 영속성 컨텍스트  (0) 2025.04.02
[Spring Boot] 실시간 비동기 작업 처리기 만들기 - 실습  (0) 2025.03.26
[Spring Boot] 실시간 비동기 작업 처리기 만들기 - 기초 개념  (0) 2025.03.19
'Spring/기초 개념' 카테고리의 다른 글
  • [Spring Boot] MSA에서 git action과 docker를 활용한 Docker CI/CD 구축
  • [Spring Boot] Spring MVC + WebClient 환경에서 JPA 사용하기
  • Spring Data JPA의 영속성 컨텍스트
  • [Spring Boot] 실시간 비동기 작업 처리기 만들기 - 실습
코드래곤
코드래곤
코드래곤 님의 블로그 입니다.
  • 코드래곤
    코드래곤 님의 블로그
    코드래곤
  • 전체
    오늘
    어제
    • 분류 전체보기 (61)
      • 알고리즘 (3)
        • 그리디 (1)
        • 그래프 (2)
      • 시스템 설계 (6)
      • CS 및 기본 개념 (17)
      • Docker (5)
      • Spring (23)
        • 백준 서비스 구현하기 (1)
        • 기초 개념 (14)
        • MSA (2)
        • JPA (1)
      • Dart (3)
      • Flutter (1)
      • Kubernetes (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
코드래곤
[Spring Boot] Spring AOP Self Invocation과 @Transactional
상단으로

티스토리툴바