Notice
Recent Posts
Recent Comments
Link
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

che01 님의 블로그

Self-invocation Problem 본문

Spring

Self-invocation Problem

che01 2025. 6. 24. 19:18

Self-invocation problem은 스프링에서 같은 클래스 내부의 메서드를 호출할 때 발생하는 문제다. AOP나 프록시가 적용된 메서드를 같은 클래스 안에서 직접 호출하면, 프록시를 거치지 않아서 의도한 기능이 동작하지 않는다.

왜 발생하나?

스프링은 객체를 프록시로 감싸서 AOP 기능을 제공한다. 하지만 같은 클래스 내부에서 메서드를 호출할 때는 this.method()로 호출하게 되는데, 이때 this는 프록시 객체가 아닌 실제 객체를 가리킨다. 그래서 프록시를 거치지 않아 AOP가 동작하지 않는다.

실제 예제

문제가 되는 코드

@Service
public class UserService {
    
    @Transactional
    public void updateUser(Long userId) {
        // 일부 로직
        this.updateUserDetail(userId); // 문제 발생!
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateUserDetail(Long userId) {
        // 새로운 트랜잭션에서 실행되어야 하는데...
        // self-invocation으로 인해 트랜잭션이 새로 생성되지 않음
    }
}

위 코드에서 updateUserDetail은 새로운 트랜잭션에서 실행되어야 하지만, self-invocation으로 인해 기존 트랜잭션을 그대로 사용한다.

해결 방법들

1. 별도 클래스로 분리

@Service
public class UserService {
    
    private final UserDetailService userDetailService;
    
    @Transactional
    public void updateUser(Long userId) {
        // 일부 로직
        userDetailService.updateUserDetail(userId); // 프록시를 통해 호출됨
    }
}

@Service
public class UserDetailService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateUserDetail(Long userId) {
        // 새로운 트랜잭션에서 정상 실행됨
    }
}

2. ApplicationContext에서 프록시 객체 가져오기

이 방법은 스프링 컨테이너에서 프록시로 감싸진 자기 자신을 직접 가져오는 방식이다.

@Service
public class UserService implements ApplicationContextAware {
    
    private ApplicationContext applicationContext;
    
    @Transactional
    public void updateUser(Long userId) {
        // 일부 로직
        
        // 중요: 스프링 컨테이너에서 프록시 객체를 가져옴
        UserService self = applicationContext.getBean(UserService.class);
        self.updateUserDetail(userId); // 이제 프록시를 통해 호출됨
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateUserDetail(Long userId) {
        // 새로운 트랜잭션에서 정상 실행됨
    }
    
    // ApplicationContextAware 인터페이스 구현
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
}

왜 이렇게 동작하나?

  • this.updateUserDetail()로 호출하면 → 현재 객체(프록시가 아닌 실제 객체)에서 호출
  • applicationContext.getBean(UserService.class)로 가져오면 → 스프링이 관리하는 프록시 객체를 가져옴
  • 프록시 객체로 호출하면 → AOP가 정상 동작함

3. @Async 문제 상황

@Async도 동일한 self-invocation 문제가 발생한다.

@Service
@EnableAsync  // 비동기 기능 활성화
public class EmailService {
    
    public void sendWelcomeEmail(String email) {
        System.out.println("메인 스레드: " + Thread.currentThread().getName());
        
        // 문제: this로 호출하면 프록시를 거치지 않음
        this.sendEmailAsync(email); // 비동기로 동작하지 않음!
        
        System.out.println("메인 메서드 종료");
    }
    
    @Async
    public void sendEmailAsync(String email) {
        System.out.println("이메일 발송 스레드: " + Thread.currentThread().getName());
        // 실제로는 같은 스레드에서 실행됨 (비동기 X)
        
        try {
            Thread.sleep(3000); // 3초 대기
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        System.out.println("이메일 발송 완료");
    }
}

실행 결과 (문제 상황):

메인 스레드: main
이메일 발송 스레드: main  // 같은 스레드!
이메일 발송 완료
메인 메서드 종료

해결된 코드:

@Service
public class EmailService {
    
    @Autowired
    private EmailService self; // 자기 자신을 주입받음 (프록시 객체)
    
    public void sendWelcomeEmail(String email) {
        System.out.println("메인 스레드: " + Thread.currentThread().getName());
        
        // 프록시 객체로 호출
        self.sendEmailAsync(email); // 이제 비동기로 동작함!
        
        System.out.println("메인 메서드 종료");
    }
    
    @Async
    public void sendEmailAsync(String email) {
        System.out.println("이메일 발송 스레드: " + Thread.currentThread().getName());
        // 이제 다른 스레드에서 실행됨 (비동기 O)
        
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        System.out.println("이메일 발송 완료");
    }
}

실행 결과 (해결 후):

메인 스레드: main
메인 메서드 종료  // 먼저 끝남
이메일 발송 스레드: task-1  // 다른 스레드!
이메일 발송 완료

언제 주의해야 하나?

  • @Transactional 메서드 내부 호출
  • @Async 메서드 내부 호출
  • @Cacheable 메서드 내부 호출
  • @PreAuthorize/@PostAuthorize 내부 호출
  • 커스텀 AOP 어노테이션 내부 호출

핵심 포인트

같은 클래스 내부에서 메서드를 호출할 때는 프록시를 거치지 않는다는 점을 항상 기억해야 한다. AOP 기능이 필요한 메서드는 가능하면 별도 클래스로 분리하는 것이 가장 깔끔한 해결책이다.