스프링 핵심 원리(고급편)

Ch12. 스프링 AOP(실전 예제) - 프록시와 내부 호출(대안)

webmaster 2022. 4. 22. 11:08
728x90

자기 자신 주입

내부 호출을 해결하는 가장 간단한 방법은 자기 자신을 의존관계 주입받는 것이다.(프록시를 의존관계 주입)

@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();
    }
}

this를 호출하는 것이 아닌 주입 받은 프록시의 internal 메소드를 호출하여 aop 적용된다.

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가 잘 적용되지 않으면 내부 호출을 의심해보자

728x90