본문 바로가기
프레임워크/스프링

[Spring] AOP 프록시와 내부 호출 문제 해결 방안 3가지

by so5663 2023. 1. 22.

프록시와 내부 호출 - 문제

 

스프링은 프록시 방식의 AOP를 사용한다. 따라서 AOP를 적용하려면 항상 프록시를 통해서 대상 객체(Target)을 호출해야 한다. 이렇게 해야 프록시에서 먼저 어드바이스를 호출하고, 이후에 대상 객체를 호출한다.

만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고,

어드바이스도 호출되지 않는 문제가 발생한다.

 

AOP를 적용하면 스프링은 프록시를 스프링 빈으로 등록하고 의존관계 주입시 프록시 객체를 주입한다.

그래서 대상 객체를 직접 호출하는 문제는 잘 발생하지 않는다. 하지만 대상 객체에서 프록시를 거치치 않고 

직접 대상 객체를 호출하는 경우가 있는데 그럴 경우 문제가 발생한다고 한다.

 

실무에서 반드시 한번은 만나서 고생하는 문제라고 한다.

 

김영한의 강의에서는 3가지를 예시로 보여주는데 마지막만 봐도 될 것 같다.

 

내부 호출이 발생하는 예제

CallServiceV0.java

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

 

CallLogAspect.java

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

 

CallServiceV0Test

@Import(CallLogAspect.class)
@SpringBootTest
class CallServiceV0Test {
    @Autowired
    CallServiceV0 callServiceV0;
    
    @Test
    void external() {
        callServiceV0.external();
    }
    
    @Test
    void internal() {
        callServiceV0.internal();
    }
}

 

여기서 문제는 callServiceV0.external() 안에서 internal() 을 호출할 때 발생한다. 이때는 CallLogAspect 어드바이스가 호출되지 않는다. 자바 언어에서 메서드 앞에 별도의 참조가 없으면 this 라는 뜻으로 자기 자신의 인스턴스를 가리킨다. 결과적으로 자기 자신의 내부 메서드를 호출하는 this.internal() 이 되는데, 여기서 this 는 실제 대상 객체(target)의 인스턴스를 뜻한다. 결과적으로 이러한 내부 호출은 프록시를 거치지 않는다. 따라서 어드바이스도 적용할 수 없다.

 

해결방안

첫번째는 자기 자신을 주입하는 것이다.

/**
 * 참고: 생성자 주입은 순환 사이클을 만들기 때문에 실패한다.
 */
@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");
    }
}

참고로 이 경우 생성자 주입시 오류가 발생한다. 본인을 생성하면서 주입해야 하기 때문에 순환 사이클이 만들어진다.

반면에 수정자 주입은 스프링이 생성된 이후에 주입할 수 있기 때문에 오류가 발생하지 않는다.

 

두번째는 지연 조회이다.

앞서 생성자 주입은 자시 자신을 생성하면서 주입하기 때문에 실패하는 것이다.

이경우 수정자 주입, 지연 조회를 사용하면 된다고 한다.

/**
 * 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");
    }
}

 

마지막 셋번째는 구조 변경이다.

CallServiceV3.java

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

 

InternalService.java

@Slf4j
@Component
public class InternalService {
    public void internal() {
    	log.info("call internal");
    }
}

 

내부 호출 자체가 사라지고, callService -> internalService 를 호출하는 구조로 변경되었다.

덕분에 자연스럽게 AOP가 적용된다.

 

 

출처 https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8/dashboard

 

스프링 핵심 원리 - 고급편 - 인프런 | 강의

스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

'프레임워크 > 스프링' 카테고리의 다른 글

[Spring] actuator  (0) 2024.02.15
[SpringBoot] 라이브러리 관리  (0) 2023.03.11
스프링 xml 설정 정리  (0) 2023.01.17
Spring @Aspect 범위 애노테이션으로 적용하기  (0) 2023.01.06
@RequestParam vs @PathVariable  (0) 2022.12.29