[응용] 13. 스프링 AOP - 실무 주의사항

kiwonkim·2021년 12월 3일
0

[ 이전 포스팅 ]

@Aspect 를 사용하는 스프링의 AOP 를 활용하여 어노테이션을 추가하는 방식으로 부가기능을 적용해보았다. 스프링의 AOP 는 실제객체를 상속받는 프록시객체를 생성하고, 메서드를 오버라이딩하여 구현한다. 오버라이딩 메서드에서는 부가기능을 추가한 뒤 실제객체의 메서드를 호출하는 방식으로 구현된다.

즉 프록시객체라는 별도 객체를 만들어 AOP 를 수행하는데, 이러한 프록시 방식 AOP 의 한계에 대해 알아보자.


[ 프록시 내부 호출 문제 ]

CallService 빈 등록

위와같이 두개의 메서드를 갖고 있는 객체를 스프링 빈으로 등록하였다. external 메서드는 내부의 internal 메서드를 호출한다.

@Aspect 생성

@Aspect 를 생성하여 위의 빈에 AOP 를 적용하였다. 이제 각 메서드 호출 전에 로그를 출력한다.

실행결과

Aspect 를 스프링빈으로 등록하고, CallService 를 주입받아 external 메서드를 호출하였다.

실행결과이다. 외부의 external 메서드에는 AOP 가 적용되었지만 external 이 호출한 internal 메서드에는 AOP 가 적용되지 않았다. 프록시는 해당 패키지의 모든 메서드에 적용되도록 하였는데 왜 내부 메서드는 AOP가 적용되지 않은 것일까?

내부 호출 문제

  1. 요청객체가 스프링빈의 external 을 호출했다. 스프링빈은 프록시객체이므로 프록시객체의 external 이 호출된다.
  2. 프록시객체의 external 은 어드바이스를 호출하여 부가기능을 수행한다.
  3. 부가기능 수행 완료후 실제객체의 external 이 호출된다.
  4. 실제객체의 external 내에서 this.internal 이 호출된다. 즉 AOP 가 적용되지 않은 실제객체의 internal 메서드가 호출된다.

참고로 자바 언어에서 별도의 참조가 없으면 자기 자신의 인스턴스를 가리킨다. 그래서 this.internal 이 되어 실제객체의 내부메서드가 호출된 것이다.


[ 프록시 내부 호출 문제 - 해결 ]

해결을 위해서는 실제객체 내부에 프록시객체를 주입하여 해결하는 방법 (1,2)과.
내부 호출을 없애도록 변경하는 방법(3) 이 있다.

1. 실제객체에 프록시객체 주입

실제객체에 자기자신을 필드로 두고, 프록시객체를 주입받는 것이다. 프록시객체는 실제객체의 자식이기에 주입 가능하다. 이 때 생성자 주입은 무한참조가 발생한다. 스프링은 빈 객체 생성단계와 주입단계가 나뉘므로, setter 주입을 사용하면 생성해놓은 프록시객체를 주입받을 수 있다.

그 후 external 에서 주입받은 프록시객체의 Internal 을 호출하는 것이다. 그러면 AOP 가 적용된 프록시객체의 Internal 메서드가 호출된다.

2. 지연 주입

ObjectProvider 를 필드로 두고, 주입받은 뒤에. external 메서드를 호출하는 시점에 getObject 로 빈으로 등록된 프록시객체를 가져오는 것이다. 그 후 프록시객체의 Internal 메서드를 호출하면 AOP가 적용된 Internal 메서드가 호출된다.

3. 구조 변경

1, 2 번은 실제객체가 자신의 프록시 객체를 주입받는 등 어색한 모습을 보인다. 가장 나은 대안은 내부 호출이 일어나지 않게 구조를 변경하는 것이다.

내부 메서드를 따로 빼서 별도의 빈으로 등록하고.

외부 메서드의 클래스에서는 내부 메서드의 빈을 주입받아 사용한다. AOP 가 적용된 스프링빈은 반드시 프록시객체이므로 내부 호출 문제가 발생하지 않는다.


[ 프록시 기술 한계 ]

동적 프록시 생성 방식은 JDK 동적 프록시와 CGLIB 가 있었다. 각각의 단점은 무엇이며 스프링은 왜 CGLIB 를 채택했을까?

JDK 동적 프록시 한계

JDK 동적 프록시에서 실제객체와 프록시객체는 같은 인터페이스의 자식이다. 그러나 프록시객체와 실제객체는 부모자식 관계가 아니다.

즉 프록시객체를 실제객체 타입으로 형변환이 불가능하다는 소리다.

이는 의존성 주입때 문제가된다. 실제객체인 MemberServiceImpl 을 참조변수로 의존성 주입을 받으려하면 에러가 발생한다. 스프링 빈인 JDK 프록시객체는 실제객체인 MemberServiceImpl 로 형변환 될 수 없기 때문이다. 하지만 둘의 공통 인터페이스인 MemberService 로는 형변환이 가능하다.

CGLIB 의 한계

CGLIB은 실제객체가 프록시객체의 부모클래스이므로 형변환이 가능하며, 실제객체 타입의 참조변수에도 의존성 주입이 가능하다. 대신 상속을 사용하기에 다음과 같은 한계가 존재한다.

1. 실제객체 클래스에 기본 생성자 필수

자바에서 자식클래스의 생성자를 호출할 때 반드시 부모클래스의 생성자를 호출해야한다. CGLIB 의 자식클래스는 자동으로 만들어지는데, 이때 부모의 기본생성자를 호출한다. 따라서 실제객체에 기본 생성자가 반드시 존재해야한다.

2. 실제객체 생성자 2번 호출

실제객체의 생성자가 부모인 실제객체를 생성할 때와. 프록시객체인 자식객체를 생성할 때 두번 호출되는 문제가 있다.

3. final 클래스 프록시생성 불가

final 클래스는 상속이 불가능하며 final 메서드는 오버라이딩이 불가능하다. 상속을 활용하는 CGLIB 프록시 특성상 final 클래스와 final 메서드에는 AOP 적용이 불가능하다.

스프링의 선택

objenesis 라는 라이브러리는 생성자 호출 없이 객체를 생성할 수 있게 해준다. 스프링은 objenesis 의 도입으로 CGLIB 의 한계 중 1,2 번을 해결하였다. 그래서 대부분의 문제를 해결하고, 구체클래스에도 의존성주입이 가능한 CGLIB 을 기본으로 선택하였다. 대신 설정을 통해 인터페이스가 존재하는 경우 JDK 동적 프록시를 사용하도록 변경할 수도 있다.


[ 정리 ]

내부 메서드를 호출할 경우 실제객체의 메서드가 호출되어 AOP 가 적용되지 않는 문제가 발생한다. 이는 내부 메서드를 별도의 스프링 빈으로 분리하여 해결한다.
JDK 는 인터페이스 구현 방식이기에, 구체 타입에 의존관계 주입이 불가능하다. CGLIB 은 구체 클래스를 상속해서 프록시객체를 구현하기에 구체 타입에 의존관계 주입이 가능하나, 실제객체의 기본생성자가 필수이고 생성자가 두번 호출되는 문제가 있다.
스프링은 objenesis 의 도입으로 문제를 해결한 CGLIB 을 기본으로 사용한다.

0개의 댓글