프록시 내부 호출 해결

jylee·2024년 2월 27일
0

그냥생각나는거

목록 보기
41/48

Log 출력 Aspect

@Slf4j
@Aspect
public class CallLogAspect {

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

문제 상황 CallServiceV0

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

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

CallServiceV0Test

class CallServiceV0Test {

    @Autowired
    CallServiceV0 callServiceV0;
    @Test
    void external() {
        callServiceV0.external();
    }

    @Test
    void internal() {
        callServiceV0.internal();
    }
}
  • external() 실행 결과
1. //프록시 호출
2. CallLogAspect : aop=void hello.aop.internalcall.CallServiceV0.external()
3. CallServiceV0 : call external
4. CallServiceV0 : call internal
  • internal() 실행 결과
CallLogAspect : aop=void hello.aop.internalcall.CallServiceV0.internal()
CallServiceV0 : call internal

callServiceV0.external() 안에서 internal() 을 호출할 때 문제가 발생한다. 이때는
CallLogAspect 어드바이스가 호출되지 않는다.


해결방안 1 - 자기 자신 주입

/**
 * 참고 : 생성자 주입은 순환 사이클을 만들기 때문에 실패한다.
 */
@Slf4j
@Component
public class CallServiceV1 {
    private CallServiceV1 callServiceV1;

    @Autowired
    public void setCallServiceV1(CallServiceV1 callServiceV1) {
        this.callServiceV1 = callServiceV1;
    }

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

    public void internal() {
        log.info("call internal");
    }
}
  • external() 실행 결과
CallLogAspect : aop=void hello.aop.internalcall.CallServiceV1.external()
CallServiceV2 : call external
CallLogAspect : aop=void hello.aop.internalcall.CallServiceV1.internal()
CallServiceV2 : call internal

callServiceV1 를 수정자를 통해서 주입 받는 것을 확인할 수 있다. 스프링에서 AOP가 적용된 대상을 의존관계 주입 받으면 주입 받은 대상은 실제 자신이 아니라 프록시 객체이다.

external() 을 호출하면 callServiceV1.internal() 를 호출하게 된다. 주입받은 callServiceV1 은 프록시이다. 따라서 프록시를 통해서 AOP를 적용할 수 있다.


해결방안 2 - 지연 조회

/**
 * ObjectProvider(Provider), ApplicationContext를 사용해서 지연(LAZY) 조회
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class CallServiceV2 {

//    private final ApplicationContext applicationContext;
    private final ObjectProvider<CallServiceV2> 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");
    }
}
  • external() 실행 결과
CallLogAspect : aop=void hello.aop.internalcall.CallServiceV2.external()
CallServiceV2 : call external
CallLogAspect : aop=void hello.aop.internalcall.CallServiceV2.internal()
CallServiceV2 : call internal

ObjectProvider 는 객체를 스프링 컨테이너에서 조회하는 것을 스프링 빈 생성 시점이 아니라 실제 객체를 사용하는 시점으로 지연할 수 있다.

callServiceProvider.getObject() 를 호출하는 시점에 스프링 컨테이너에서 빈을 조회한다.
여기서는 자기 자신을 주입 받는 것이 아니기 때문에 순환 사이클이 발생하지 않는다.


해결방안 3 - 구조 변경

/**
 * 구조를 변경(분리)
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class CallServiceV3 {
    private final InternalService internalService;
    public void external() {
        log.info("call external");
        internalService.internal(); // 외부 메서드 호출
    }
}

InternalService

@Slf4j
@Component
public class InternalService {
    public void internal() {
        log.info("call internal");
    }
}
  • external() 실행 결과
CallLogAspect : aop=void hello.aop.internalcall.CallServiceV2.external()
CallServiceV2 : call external
CallLogAspect : aop=void hello.aop.internalcall.CallServiceV2.internal()
CallServiceV2 : call internal

앞선 방법들은 자기 자신을 주입하거나 또는 Provider 를 사용해야 하는 것 처럼 조금 어색한 모습을 만들었다.

가장 나은 대안은 내부 호출이 발생하지 않도록 구조를 변경하는 것이다. 실제 이 방법을 가장 권장한다.

내부 호출 자체가 사라지고, callService internalService 를 호출하는 구조로 변경되었다. 덕분에 자연스럽게 AOP가 적용된다.

여기서 구조를 변경한다는 것은 이렇게 단순하게 분리하는 것 뿐만 아니라 다양한 방법들이 있을 수 있다.

예를 들어서 다음과 같이 클라이언트에서 둘다 호출하는 것이다.

  • 클라이언트 -> external()
  • 클라이언트 -> internal()

물론 이 경우 external() 에서 internal() 을 내부 호출하지 않도록 코드를 변경해야 한다. 그리고 클라이언트가 external() , internal() 을 모두 호출하도록 구조를 변경하면 된다. (물론 가능한 경우에 한해서)

참고
AOP는 주로 트랜잭션 적용이나 주요 컴포넌트의 로그 출력 기능에 사용된다.
쉽게 이야기해서 인터페이스에 메서드가 나올 정도의 규모에 AOP를 적용하는 것이 적당하다.
더 풀어서 이야기하면 AOP는 public 메서드에만 적용한다.
private 메서드처럼 작은 단위에는 AOP를 적용하지 않는다.
AOP 적용을 위해 private 메서드를 외부 클래스로 변경하고 public 으로 변경하는 일은 거의 없다. 그러나 위 예제와 같이 public 메서드에서 public 메서드를 내부 호출하는 경우에는 문제가 발생한다.
AOP가 잘 적용되지 않으면 내부 호출을 의심해보자

profile
ㅎㅇ

0개의 댓글