[Spring] 스프링 AOP 주의사항 - 프록시 내부호출

스프링은 프록시 방식의 AOP를 사용합니다.

스프링 AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링 빈으로 등록하고, 스프링은 의존관계 주입시에 항상 프록시 객체를 주입합니다.

프록시 객체가 주입되기 때문에 대상 객체를 직접 호출하는 문제는 일반적으로 발생하지 않지만

대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생하게 됩니다.

 

어떤 문제가 발생하는지 간단한 예시를 통해 알아보겠습니다.

 

CallService에서 external() 메서드를 호출하는데 그 안에서 CallService 자기 자신의 메서드인 internal()을 호출하는 상황
package hello.aop.internalcall.aop;
...

@Slf4j
@Component
public class CallService {

    public void external() {
        log.info("call external");
        internal(); // 내부 메서드 호출 (this.internal())
    }

    public void internal() {
        log.info("call internal");
    }
}

 

스프링 AOP로 간단한 로그를 적용할 CallLogAspect
@Slf4j
@Aspect
public class CallLogAspect {

    @Before("execution(* hello.aop.internalcall..*.*(..))")
    public void doLog(JoinPoint joinPoint) {
        log.info("aop = {}", joinPoint.getSignature());
    }
}

 

테스트 코드
@Slf4j
@Import(CallLogAspect.class)
@SpringBootTest
class CallServiceTest {

    @Autowired
    CallService callService;

    @Test
    void external() {
        callService.external();
    }
}

 

CallService에서 external() 메서드를 호출한 결과를 보면 다음과 같습니다.

callService.external()을 실행할 때는 프록시를 호출합니다.

하지만 callService.external()안에서 호출한 internal()에는 CallLogAspect가 적용되지 않았습니다.

 

internal()에 AOP가 적용되지 않은 이유

자바 언어에서 메서드 앞에 별도의 참조가 없으면 this 라는 뜻으로 자기 자신의 인스턴스를 가리키므로

자기 자신의 내부 메서드를 호출하는 this.internal() 이 되는데, 여기서 this 는 프록시가 아닌 실제 대상 객체(target)의 인스턴스를 뜻합니다.

그래서 프록시가 아닌 실제 대상 객체CallService의 internal()을 호출하게 되어,  결과적으로 이러한 내부 호출은 프록시를 거치지 않게 됩니다.

 


해결 방법 - 별도의 클래스로 분리

이러한 프록시 내부 호출 문제를 해결 하기 위해서는

바로, "내부 호출이 발생하지 않도록 구조를 변경"하는 것입니다.

 

앞의 CallService에서 내부호출 문제가 발생하는 internal() 메서드를 별도의 클래스로 분리해 보겠습니다.

 

InternalService : internal() 메서드를 분리한 클래스
@Slf4j
@Component
public class InternalService {
    public void internal() {
        log.info("call internal");
    }
}

 

CallService
@Slf4j
@Component
@RequiredArgsConstructor
public class CallService {

    private final InternalService internalService;

    public void external() {
        log.info("call external");
        internalService.internal(); // 외부 메서드 호출 (this.internal())
    }
}

 

테스트 코드
@SpringBootTest
@Import(CallLogAspect.class)
class CallServiceTest {

    @Autowired
    CallService callService;

    @Test
    void external() {
        callService.external();
    }
}

 

로그를 확인해 보면 external()과 internal() 둘 다 AOP가 적용된 것을 확인 할 수 있습니다.

 

이 방식으로 문제가 해결된 이유를 보면 다음과 같습니다.

별도의 클래스로 분리로 분리하여 callService 가 internalService를 호출하는 구조로 변경되었습니다.

그래서 내부 호출 문제가 사라졌기 때문에 둘 다 AOP가 적용되었습니다.

 

이 방법외에도 프록시 내부 호출을 해결하는 여러 방법이 있지만

별도의 클래스로 분리 하는 방법이 스프링에서 권장하는 방법이기 때문에 이 방법에 대해서만 설명하였습니다.


참고자료 : 김영한의 스프링 핵심 원리 - 고급편