자기 자신 주입
내부 호출을 해결하는 가장 간단한 방법은 자기 자신을 의존관계 주입받는 것이다.(프록시를 의존관계 주입)
@Slf4j
@Component
public class CallServiceV1 {
private CallServiceV1 callServiceV1;
@Autowired
public void setCallServiceV1(CallServiceV1 callServiceV1){
log.info("callServiceV1 setter={}", callServiceV1.getClass());
this.callServiceV1 = callServiceV1;
}
/*
@Autowired
public CallServiceV1(CallServiceV1 callServiceV1){
//빈등록 전에 나를 의존성을 주입하려고 하니 안된다.
this.callServiceV1 = callServiceV1;
}
*/
public void external(){
log.info("call external"); //외부 호출
callServiceV1.internal(); //외부 메서드 호출
}
public void internal(){
log.info("call internal");
}
}
- callServiceV1을 수정자를 통해서 주입받는 것을 확인할 수 있다. 스프링에서 AOP가 적용된 대상을 의존관계 주입받으면 주입받은 대상은 실제 자신이 아니라 프록시 객체이다.
- external()을 호출하면 callServiceV1.internal()를 호출하게 된다. 주입받은 callServiceV1 은 프록시이다. 따라서 프록시를 통해서 AOP를 적용할 수 있다
- 참고로 이 경우 생성자 주입 시 오류가 발생한다. 본인을 생성하면서 주입해야 하기 때문에 순환 사이클이 만들어진다. 반면에 수정자 주입은 스프링이 생성된 이후에 주입할 수 있기 때문에 오류가 발생하지 않는다.
Test
@Slf4j
@Import(CallLogAspect.class)
@SpringBootTest
class CallServiceV1Test {
@Autowired
CallServiceV1 callServiceV1;
@Test
public void external(){
callServiceV1.external();
}
@Test
public void internal(){
callServiceV1.internal();
}
}

internal()을 호출할 때 자기 자신의 인스턴스를 호출하는 것이 아니라 프록시 인스턴스를 통해서 호출하는 것을 확인할 수 있다. 당연히 AOP도 잘 적용된다
주의)
스프링 부트 2.6부터는 순환 참조를 기본적으로 금지하도록 정책이 변경되었다. 따라서 이번 예제를 스프링 부트 2.6 이상의 버전에서 실행하면 다음과 같은 오류 메시지가 나오면서 정상 실행되지 않는다
application.properties에 spring.main.allow-circular-references=true를 추가하여 해결한다.
지연 조회
앞서 생성자 주입이 실패하는 이유는 자기 자신을 생성하면서 주입해야 하기 때문이다. 이 경우 수정자 주입을 사용하거나 지금부터 설명하는 지연 조회를 사용하면 된다. 스프링 빈을 지연해서 조회하면 되는데, ObjectProvider(Provider) , ApplicationContext를 사용하면 된다.
@Slf4j
@Component
public class CallServiceV2 {
//private final ApplicationContext applicationContext;
/*
public CallServiceV2(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
*/
private final ObjectProvider<CallServiceV2> callServiceProvider;
public CallServiceV2(ObjectProvider<CallServiceV2> callServiceProvider) {
this.callServiceProvider = callServiceProvider;
}
public void external(){
log.info("call external");
//CallServiceV2 callServiceV2 = applicationContext.getBean(CallServiceV2.class); //지연 로딩
CallServiceV2 callServiceV2 = callServiceProvider.getObject();
callServiceV2.internal(); //외부 메서드 호출
}
public void internal(){
log.info("call internal");
}
}
- ApplicationContext는 너무 많은 기능을 제공한다. ObjectProvider는 객체를 스프링 컨테이너에서 조회하는 것을 스프링 빈 생성 시점이 아니라 실제 객체를 사용하는 시점으로 지연할 수 있다.
- callServiceProvider.getObject()를 호출하는 시점에 스프링 컨테이너에서 빈을 조회한다
- 여기서는 자기 자신을 주입받는 것이 아니기 때문에 순환 사이클이 발생하지 않는다.
Test
@Slf4j
@Import(CallLogAspect.class)
@SpringBootTest
class CallServiceV2Test {
@Autowired
CallServiceV2 callServiceV2;
@Test
public void external(){
callServiceV2.external();
}
@Test
public void internal(){
callServiceV2.internal();
}
}
구조 변경
앞선 방법들은 자기 자신을 주입하거나 또는 Provider를 사용해야 하는 것처럼 조금 어색한 모습을 만들었다. 가장 나은 대안은 내부 호출이 발생하지 않도록 구조를 변경하는 것이다. 실제 이 방법을 가장 권장한다
/**
* 구조 변경
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CallServiceV3 {
private final InternalService internalService;
public void external(){
log.info("call external");
internalService.internal(); //외부로 호출하도록 변굥
}
}
- 내부 호출을 InternalService라는 별도의 클래스로 분리했다.
InternalService
@Slf4j
@Component
public class InternalService {
public void internal(){
log.info("call internal");
}
}
Test
@Slf4j
@Import(CallLogAspect.class)
@SpringBootTest
class CallServiceV3Test {
@Autowired
CallServiceV3 callServiceV3;
@Test
public void external(){
callServiceV3.external();
}
}

- 내부 호출 자체가 사라지고, callService internalService를 호출하는 구조로 변경되었다. 덕분에 자연스럽게 AOP가 적용된다
- 여기서 구조를 변경한다는 것은 이렇게 단순하게 분리하는 것뿐만 아니라 다양한 방법들이 있을 수 있다.
- 클라이언트에서 둘 다 호출하는 식으로 변경 가능(client -> external(), internal())
참고
AOP는 주로 트랜잭션 적용이나 주요 컴포넌트의 로그 출력 기능에 사용된다. 쉽게 이야기해서 인터페이스에 메서드가 나올 정도의 규모에 AOP를 적용하는 것이 적당하다. 더 풀어서 이야기하면 AOP는 public 메서드에만 적용한다. private 메서드처럼 작은 단위에는 AOP를 적용하지 않는다.
AOP 적용을 위해 private 메서드를 외부 클래스로 변경하고 public으로 변경하는 일은 거의 없다. 그러나 위 예제와 같이 public 메서드에서 public 메서드를 내부 호출하는 경우에는 문제가 발생한다. 실무에서 꼭 한 번은 만나는 문제이기에 이번 강의에서 다루었다.
AOP가 잘 적용되지 않으면 내부 호출을 의심해보자
'스프링 핵심 원리(고급편)' 카테고리의 다른 글
| Ch12. 스프링 AOP(실전 예제) - 프록시 기술과 한계(CGLIB, 스프링 해결책) (0) | 2022.04.22 |
|---|---|
| Ch12. 스프링 AOP(실전 예제) - 프록시 기술과 한계(JDK 동적 프록시) (0) | 2022.04.22 |
| Ch12. 스프링 AOP(실전 예제) - 프록시와 내부 호출(문제) (0) | 2022.04.22 |
| Ch12. 스프링 AOP(실전 예제) - 재시도 AOP (0) | 2022.04.21 |
| Ch12. 스프링 AOP(실전 예제) - 예제 만들기 & 로그 출력 AOP (0) | 2022.04.21 |