2026. 9. 10. 01:12ㆍ스프링
self-invocation이란
self-invocation은 한 객체 안에서 자기 자신의 다른 메서드를 호출하는 걸 의미합니다.
평범한 자바코드에서는 아무 문제 없지만 Spring AOP 기반 기능(@Transactional, @Async, @Cacheable 등)을 쓸 때 내부에서 호출한 메서드에는 어노테이션이 동작하지 않기 때문에 문제가 발생합니다.
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void outer() {
inner();
}
@Transactional
public void inner() {
userRepository.save(new User("kim"));
throw new RuntimeException("일부러 던지는 예외");
}
}
생각대로라면 inner()에 @Transactional이 있으니 예외가 터지면 롤백되어서 저장된 유저가 없어야 합니다.
@SpringBootTest
class UserServiceTest {
@Autowired
UserService userService;
@Autowired
UserRepository userRepository;
@AfterEach
void tearDown() {
userRepository.deleteAll();
}
@Test
@DisplayName("outer 호출은 self-invocation이므로 트랜잭션이 적용되지 않는다.")
void outer() {
assertThatThrownBy(() -> userService.outer()).isInstanceOf(RuntimeException.class);
assertThat(userRepository.count()).isEqualTo(1); // 저장되어 있으므로 1이 나옴
}
@Test
@DisplayName("inner를 직접 호출하면 트랜잭션이 적용된다.")
void inner() {
assertThatThrownBy(() -> userService.inner()).isInstanceOf(RuntimeException.class);
assertThat(userRepository.count()).isEqualTo(0); // 정상적으로 롤백
}
}
하지만 테스트코드를 작성해서 실행해 보면 두 테스트 모두 통과합니다.
따라서 내부에서 호출하면 롤백되지 않고, 외부에서 직접 호출하면 롤백이 된다는 걸 알 수 있습니다.
왜 이런 일이 발생할까
Spring AOP가 프록시 패턴으로 구현되어 있기 때문입니다.
알다시피 Spring은 원본 객체를 직접 사용하지 않고 프록시 객체를 만든 후 해당 프록시 객체를 통해서 원본 메서드 실행, 전/후 공통 관심사처리 등과 같은 역할을 수행합니다.
따라서, 의존성 주입을 받은 UserService는 원본이 아니라 프록시 객체입니다.
그래서 외부에서 UserService.inner()를 호출하면 Spring AOP를 통해 트랜잭션이 걸립니다.
문제는 내부 호출입니다. outer()에서 호출하는 inner()는 프록시를 거치지 않고 원본 객체의 메서드를 직접 실행합니다.
원본 객체를 직접 호출하니 트랜잭션 어드바이스가 붙을 기회가 사라져 에러가 발생해도 롤백이 되지 않는 것입니다.
당연하게 @Async, @Cacheable 등 프록시 기반으로 동작하는 모든 어노테이션이 같은 이유로 self-invocation에서 동작하지 않습니다.
해결방법
가장 깔끔한 해결책은 메서드를 다른 클래스로 분리하는 것입니다.
서로 다른 클래스끼리는 호출하면 자연스럽게 프록시를 거치게 되고 AOP 어노테이션이 적용됩니다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRegistrationService registrationService;
public void outer() {
registrationService.inner(); // 다른 클래스 → 프록시 경유
}
}
@Service
public class UserRegistrationService {
@Transactional
public void inner() { ... }
}
'스프링' 카테고리의 다른 글
| @Async로 UX 개선하기 (0) | 2026.09.09 |
|---|---|
| Service Interface를 반드시 생성해야 하는가 (0) | 2026.09.07 |
| 트랜잭션 전파 레벨(REQUIRES_NEW)의 함정과 이벤트 기반 해결 (0) | 2026.08.13 |
| [JPA] Hibernate @SoftDelete (0) | 2025.09.05 |
| [JPA] N+1 문제와 Fetch Join (1) | 2025.08.29 |