[Spring][AOP] 프록시 기술 한계

donghyeok·2023년 3월 19일
0

Spring

목록 보기
8/9

프록시 내부 호출 문제

스프링은 프록시 방식의 AOP를 사용한다.
프록시 사용으로 인해 나타나는 대표적인 문제 중 하나는 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제이다.

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

위 예시의 모든 메서드에 스프링AOP를 적용했다고 가정해보자.
우리가 원하는 결과는 external(), internal() 메서드 모두 AOP가 동작하는 것이다.
하지만 위 예시에서 internal() 메서드에는 AOP가 적용되지 않는다.

해당 코드는 위 코드처럼 동작하는데 이유는, 자바에서 메서드 앞에 별도 참조가 없으면 this라는 뜻으로 자기 자신의 인스턴스를 가리킨다. 여기서 this는 프록시가 아닌 실제 대상 객체를 뜻하므로 이러한 내부 호출은 프록시를 거치지 않는다. 따라서 어드바이스도 적용되지 않는다.

대안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");
	}
}

위 코드처럼 세터주입을 받으면(생성자 주입은 순환 사이클이 발생해 오류 발생) 주입 받은 대상은 실제 자신이 아니라 프록시 객체이다. 따라서 callServiceV1.internal()을 호출시 프록시를 통해 AOP가 적용된다.

대안2 지연 조회

앞선 예제는 세터주입을 하더라도 스프링 부트 2.6부터는 순환 참조 에러가 발생하며 정상 수행되지 않는다.
또 다른 대안으로는 ObjectProvider, ApplicationContext 등을 사용하여 빈을 지연 조회하는 것이다. (ApplicationContext는 너무 많은 내용은 포괄하므로 ObjectProvider 추천)

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

위 예제 역시 실제 대상 객체(this)가 아닌 프록시 객체를 조회해서 정상적으로 스프링 AOP가 적용된다.

대안3 구조 변경

앞서 방법들은 자기 자신을 주입하거나 특정 객체를 사용하는 것처럼 조금 어색하다. 가장 나은 대안은 내부 호출이 발생하지 않도록 내부 호출 메서드를 다른 클래스로 분리하는 등 구조를 변경하거나 클라이언트를 따로 두어 해당 메서드들을 로직에 문제 없이 호출시키는 방법이 있다. 실제 이 방법이 가장 권장된다.

JDK 동적 프록시 문제 - 타입 캐스팅

인터페이스 기반으로 프록시를 생성하는 JDK 동적 프록시는 구체 클래스로 타입 캐스팅이 불가능한 한계가 있다.

위와 같이 JDK 동적 프록시는 인터페이스를 구현하고 대상 클래스(구체 클래스)를 구성(Composition)하는 방식을 취하고 있는데 이 때문에 실제 대상 클래스 타입으로 캐스팅이 불가능하다.

반면, CGLIB 프록시의 경우 대상 클래스(구체 클래스)를 상속하며 동시에 구성하는 방식을 취하고 있기 때문에 실제 대상 클래스 타입으로 캐스팅이 가능하다.

이러한 캐스팅 문제의 가장 큰 발생 상황은 의존 관계 주입에서 발생한다. 컴포넌트에서 대상 클래스 타입으로 주입하고 싶은 경우 JDK 동적 프록시는 주입할 수 없지만 CGLIB 프록시는 가능하다. (사실 인터페이스가 있으면 인터페이스를 기반으로 의존관계 주입을 받는 것이 맞다.)

CGLIB 프록시 문제

CGLIB는 구체 클래스를 상속받기 때문에 다음과 같은 문제가 발생한다.

  • 대상 클래스에 기본 생성자가 필수
    - 프록시 객체를 만들 때 대상 클래스 생성자 호출
  • 생성자 2번 호출 문제
    - 실제 대상 클래스를 생성할 때, CGLIB 프록시 객체를 생성할 때 (부모 생성자)
  • final 키워드 클래스, 메서드 사용 불가
    - 일반적으로 잘 사용하지 않아서 크게 문제X

위와 같은 문제들은 현재 스프링 버전들에서 대부분 개선되었다.

  • CGLIB 기본 생성자 필수 문제 해결
    - 스프링 4.0부터 objenesis라는 라이브러리를 사용해서 기본 생성자 없이 객체 생성 가능
  • 생성자 2번 호출 문제
    - 역시 스프링 4.0부터 objenesis 라이브러리 덕분에 생성자 1번 호출

위와 같은 이유들로 스프링 부트 2.0 버전부터는 CGLIB를 기본으로 사용하도록 하는 등 CGLIB 사용이 권장되고 있다.

0개의 댓글