모든 기술이 그러하듯 스프링 AOP 또한 많은 부분에 있어서 도움을 받을 수 있지만, 문제점 또한 존재한다.
이번 글에서는 그 중에서도 내부 호출 이슈에 대해서 다뤄보도록 하겠다.
내부 호출 문제라는 것은 같은 클래스 안에서 어떤 메서드를 또 다른 메서드 안에서 호출할 때 발생하는 문제를 말한다.
코드를 보며 자세히 알아보자.
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class SomeService {
public void external() {
log.info("SomeService.external()");
internal();
}
public void internal() {
log.info("SomeService.internal()");
}
}
위와 같은 형태의 클래스가 있다고 가정해보자.
주요 특징은 external메서드에서 internal 메서드가 호출되고 있는 것이다.
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class LogAspect {
@Before("execution(* hello.springaop.aopInternalCall..*.*(..))")
public void doLog(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
log.info("signature={}", signature);
}
}
그리고, Spring AOP를 통해서 프록시 대상을 기록하는 기능을 갖고 있는 프록시를 적용해보도록 하겠다.
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@Slf4j
@SpringBootTest
class SomeServiceTest {
@Autowired
SomeService someService;
@Test
public void externalCallTest() throws Exception {
someService.external();
}
}
그리고 위와 같은 테스트 코드를 실행해보자.

그러면, 이렇게 로그가 출력되는 것을 확인할 수 있다.
이미지 속 글씨가 작아서 잘 안보일 수 있긴하지만, external()메서드에는 프록시가 적용되고, external()메서드 안에서 호출한 internal()메서드에는 프록시가 적용이 안된 것을 확인할 수 있다.
즉, 나는 SomeService 클래스에 프록시를 적용했는데, 메서드가 다른 메서드 내부에서 호출되는 경우 프록시가 적용이 되지 않는 다는 것을 확인할 수 있다.
원인은 사실 간단하다.
external()에서 internal()을 호출한다는 것은 this.internal()을 호출하는 것과 같다.
여기서 this는 프록시 객체가 아닌 실제 객체를 의미하기 때문에 당연하게도 내부 호출된 internal()메서드에는 프록시 기능이 적용되지 않게 되는 것이다.
그렇다면, 이렇게 내부 호출되는 메서드에 프록시가 적용이 되지 않는 이슈는 어떻게 해결할 수 있을까?
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class SomeService {
private final ObjectProvider<SomeService> serviceProvider;
public void external() {
log.info("SomeService.external()");
SomeService service = serviceProvider.getObject();
serviceV2.internal();
}
public void internal() {
log.info("SomeService.internal()");
}
}
internal() 메서드를 직접 호출하는 것이 아니라 스프링 컨테이너에서 컴포넌트를 꺼내서 실행되도록 하는 방법이다.
이러한 방법을 지연 로딩이라고 부른다.
이렇게 하면, 기능적으로는 내부 호출되는 메서드에도 프록시가 적용되게 할 수는 있지만, 사실 깔끔해보이는 방법은 아닌 것 같다.
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class SomeService {
private final InternalService internalService;
public void external() {
log.info("SomeService.external()");
internalService.internal();
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class InternalService {
public void internal() {
log.info("InternalService.internal()");
}
}
이 방법은 사실 가장 무식하면서도(?) 부작용없는 방법이기도 하다. ㅎㅎ
그냥 두 메서드를 서로 다른 클래스에 정의하는 방법이다.
이 방법은 기능적으로는 당연히 아무 문제 없게 되지만, 하나로 묶여 있는 게 자연스러운 두 메서드가 기술적 한계로 인해 분리되어 관리되어야만 한다는 문제도 동반할 수 있게 된다.
김영한님이 강의 중에 언급한 내용에 따르면, 이 내부 호출 이슈는 100% 완벽한 해결 방법은 없는 것 같다.
스프링에서는 권장하는 방법은 2번 해결방법과 같이 구조 변경을 하는 것이다.
하지만, 보는 것처럼 기술적 한계로 인해서 그 한계에 시스템의 구조를 맞춰야하는 한계점이 분명히 존재한다.
즉, 프록시를 사용하는 경우 내부 호출 이슈가 발생할 수 있는 것을 인지하고 구조를 설계하고, 부득이하게 구조 변경이 불가능한 경우엔 지연 로딩 방식을 사용해서 원하는 대로 프록시를 적용해야할 것으로 생각이 된다.