Spring AOP 실무 주의 사항

hoyong.eom·2023년 10월 1일
0

스프링

목록 보기
50/59
post-thumbnail

Spring

프록시와 내부 호출 - 문제

스프링은 프록시 방식의 AOP를 사용한다.
따라서 AOP를 적용하려면 항상 프록시를 통해서 대상 객체(target)를 호출해야한다.
이렇게 해야 프록시에서 먼저 어드바이스를 호출하고, 이후에 대상 객체를 호출한다.
만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고 어드바이스도 호출되지 않는다.(트랜잭션에서 AOP를 사용하는 경우에도 동일한 문제가 발생했었다.)

AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링 빈으로 등록한다. 따라서 스프링은 의존관계 주입시에 항상프록시 객체를 주입한다. 프록시 객체가 주입되기 떄문에 대상 객체를 직접 호출하는 문제는 일반적으로 발생하지 않는다. 하지만 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다.
아래의 코드는 내부 메서드 호출로 인해서 프록시가 적용되지 않는 문제의 예시 코드이다.

@Slf4j
@Aspect
public class CallLogAspect {

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

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

    public void internal() {
        log.info("call internal");
    }
}
  • CallService0.external()을 호출하면 내부에서 internal()이라는 자기 자신의 메서드를 호출한다.
    자바 언어에서는 메서드를 호출할때 대상을 지정하지 않으면 앞에 자기 자신의 인스턴스를 뜻하는 this가 붙게 된다. 따라서 this.internal()이 호출된다.
  • Aspect에 정의된 AspectJ 표현식을 보면 internalcall 하위에 존재하는 모든 객체의 메서드에 대해서 어드바이저를 적용하도록 정의되어 있다.

아래는 테스트 코드이다.

@Slf4j
@Import(CallLogAspect.class)
@SpringBootTest
class CallServiceV0Test {

    @Autowired CallServiceV0 callServiceV0;

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

    @Test
    void internal() {
        callServiceV0.internal();
    }
}
  • @Import(CallLogAspect.class) : 앞서 만든 간단한 Aspect를 스프링빈으로 등록한다. spring-boot-start-aop 라이브러리에 의해서 자동으로 스프링빈에 등록된 빈후처리기에 의해서 스프링빈에 등록된 Aspect를 찾아 Advisor(어드바이스 + 포인트컷)를 생성하고, 이렇게 생성되어 스프링빈에 등록된 어드바이저를 찾아 프록시를 자동으로 생성해주기 떄문에 스프링빈으로 등록해줘야한다.
  • @SpringbootTest : 내부에 컴포넌트 스캔으르 포함하고 있다. CallService0@Component가 붙어있으므로 스프링 빈 등록 대상이 된다.

실행 결과 - external()

1. //프록시 호출
2. CallLogAspect : aop=void hello.aop.internalcall.CallServiceV0.external()
3. CallServiceV0 : call external
4. CallServiceV0 : call internal

실행 결과를 보면 callServiceV0.external()을 실행할때는 프록시가 적용된다. 따라서 CallLogAspect어드바이스가 호출된것을 확인할 수 있다.
그리고 AOP Proxy는 target.external()을 호출한다.
그런데 여기서 문제는 callServiceV0.external()안에서 internal()을 호출할때 발생한다.
이때는 CallLogAspect어드바이스가 호출되지 않는다.

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

다만 외부에서 internal()을 호출하는 경우 정상적으로 프록시가 적용된다.

외부에서 호출하는 경우 프록시를 거치기 떄문에 internal()CallLogAspect어드바이스가 적용된것을 확인할 수 있다.

프록시 방식의 AOP 한계
스프링은 프록시 방식의 AOP를 사용한다. 프록시 방식의 AOP는 메서드 내부 호출에 프록시를 적용할 수 없다.

참고
실제 코드에 AOP를 직접 적용하는 AspecJ를 사용하면 이런 문제가 발생하지 않는다. 프록시를 통하는것이 아니라 해당 코드에 직접 AOP 적용 코드가 붙어 있기 때문에 내부 호출과 무관하게 AOP를 적용할 수 있다.
하지만 로드 타임 위빙 등을 사용해야하는데 설정이 복잡하고 JVM 옵션을 주어야하는 부담이 있다.
따라서 AspectJ를 직접 사용하는 방법은 실무에서는 거의 사용되지 않는다.
이 문제를 해결하려면 application.properties에 다음을 추가하자.

spring.main.allow-circular-references=true

프록시와 내부 호출 - 대안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");
    }
}
  • callServiceV를 Setter를 통해서 주입 받는것을 확인할 수 있다. 생성자를 이용할 순 없다. 왜냐하면 자기 자신을 주입받는데 자기 자신이 생성되기 전에 생성자에서 주입받을 수 없기 떄문이다. 그리고 Setter를 이용해서 주입받는다. 스프링에서 AOP가 적용된 대상을 의존관계 주입 받으면 주입 받은 대상은 실제 자신이 아니라 프록시 객체이다.
    external()을 호추랗면 callServiceV1.internal()을 호출하게 된다. 주입 받은 callServiceV은 프록시이다. 따라서 프록시를 통해서 AOP를 적용할 수 있다.

주의
스프리 부트 2.6부터는 순환참조를 기본적으로 금지하도록 정책이 변경되었다. 따라서 위 코드를 스프링 부트 2.6이상의 버전에서실행하면 다음과 같은 오류 메시지가 나오면서 정상적으로 실행이 불가능하다.

Error creating bean with name 'callServiceV1': Requested bean is currently in 
creation: Is there an unresolvable circular reference?

프록시와 내부 호출 - 대안2 지연 조회

앞선 대안에서는 생성자 주입이 실패한다고 했었다. (그래서 Setter를 이용한 의존성 주입을 사용했다.) 그 이유는 자기 자신을 생성하면서 주입해야 하기 떄문이다. 이 경우 수정자 주입를 사용하거나 지연 조회를 사용하면 된다.
스프링 빈을 지연해서 조회하면 되는데, ObjectProvider(Provider), ApplicationContext를 사용하면 된다.

@Slf4j
@Component
public class CallServiceV2 {

//    private final 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");
    }
}
  • ObjectProviderapplicationContext에서 스프링빈 조회 기능만 수행한다고 보면 된다. ObjectProvider는 객체를 스프링컨테이너에서 조회하는것을 스프링빈 생성시점이 아니라 실제 객체를 사용하는 시점으로 지연할 수 있다.
    callServiceProvider.getObect()를 호출하는 시점에 스프링 컨테이너 빈을 조회한다. 여기서는 자기 자신을 주입 받는것이 아니기 떄문에 순환 사이클이 발생하지 않는다.(ObjectProvider 또는 ApplicationContext를 주입 받기 때문에!)

프록시와 내부 호출 - 대안3 구조 변경

트랜잭션 AOP를 사용하는 경우도 구조 변경하는 방법을 사용했었다.
앞선 방법들은 자기 자신을 주입하거나 Provider를 사용해야하는것 처럼 조금 어색한 모습을 보이기 떄문이다.
마찬가지로 가장 나은 대안은 내부 호출이 발생하지 않도록 구조를 변경하는 것이다. 실제로 이 방법을 가장 권장한다고 한다.

@Slf4j
@Component
@RequiredArgsConstructor
public class CallServiceV3 {

    private final InternalService internalService;

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

}

@Slf4j
@Component
public class InternalService {

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

}

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

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

예를 들어, 내부 호출 자체를 없앤다.
클라이언트 -> external()
클라이언트 -> internal()

물론 이 경우 external()에서 internal()을 내부 호출하지 않도록 코드를 변경해야한다.

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

프록시 기술과 한계 - 타입 캐스팅

JDK 동적 프록시와 CGLIB를 사용해서 AOP 프록시를 만드는 방법은 각각 장단점이 있다.
JDK 동적 프록시는 인터페이스가 필수이고, 인터페이스를 기반으로 프록시를 생성한다.(상속을 이용하지 않는다.)
CGLIB는 구체 클래스를 상속받아 프록시를 생성한다.

물론 인터페이스가 없고 구체 클래스만 있는 경우에는 CGLIB를 사용해야한다. 그런데 인터페이스가 있는 경우에는 JDK 동적 프록시나 CGLIB 둘중에 하나를 선택할 수 있다.

스프링이 프록시를 만들때 제공하는 proxyFactoryproxyTargetClass옵션에 따라 둘중 하나를 선택해서 프록시를 만들 수 있다.

  • proxyTargetClass=falseJDK동적 프록시를 사용해서 인터페이스 기반 프록시 생성
  • proxyTargetClass=trueCGLIB를 사용해서 구체 클래스 기반 프록시 생성
  • 참고로 옵션과 무관하게 인터페이스가 없으면 JDK 동적 프록시를 적용할 수 없으므로 CGLIB를 사용한다.

JDK 동적 프록시 한계
인터페이스 기반으로 프록시를 생성하는 JDK 동적 프록시는 구체 클래스로 타입 캐스팅이 불가능한 한계가 있다. (당연하다. 왜냐하면 인터페이스를 구현해서 만들었기 떄문에 구체 클래스의 존재 자체를 모른다!!!)


@Slf4j
public class ProxyCastingTest {

    @Test
    void jdkProxy() {
        MemberServiceImpl target = new MemberServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.setProxyTargetClass(false); //JDK 동적 프록시

        //프록시를 인터페이스로 캐스팅 성공
        MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();

        //JDK 동적 프록시를 구현 클래스로 캐스팅 시도 실패, ClassCastException 예외 발생
        assertThrows(ClassCastException.class, () -> {
            MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
        });
    }

    @Test
    void cglibProxy() {
        MemberServiceImpl target = new MemberServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.setProxyTargetClass(true); //CGLIB 프록시

        //프록시를 인터페이스로 캐스팅 성공
        MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();

        log.info("proxy class={}", memberServiceProxy.getClass());

        //CGLIB 프록시를 구현 클래스로 캐스팅 시도 성공
        MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
    }
}

JDK 동적 프록시

jdkProxy()테스트
여기서는 MemberServiceImpl타입을 기반으로 JDK 동적 프록시를 생성한다. MemeberServieImpl타입은 MemberService인터페이스를 구현한다. 따라서 JDK 동적 프록시는 MeberService인터페이스를
기반으로 프록시를 생성한다. 이 프록시를 JDK Proxy라고 하자. 여기서는 memberServiceProxy가 바로 JDK Proxy이다.

JDK 동적 프록시 캐스팅

그런데 여기에서 JDK Proxy를 대상 클래스인 MemberServiceImpl 타입으로 캐스팅하려고 하니 예외가 발생한다.
왜냐하면 JDK 동적 프록시는 인터페이스를 기반으로 프록시를 생성하기 떄문이다. JDK Proxy는 MembeerService 인터페이스를 기반으로 생성된 프록시이다. 따라서 JDK Proxy는 MemberService로 캐스팅은 가능하지만 MemberServiceImpl이 어떤것인지는 전혀 알지 못한다. 따라서 MemberServiceImpl타입으로는 캐스팅이 불가능하다. 캐스팅을 시도하면 ClassCastException.class예외가 발생한다.

cglib 프록시

    @Test
    void cglibProxy() {
        MemberServiceImpl target = new MemberServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.setProxyTargetClass(true); //CGLIB 프록시

        //프록시를 인터페이스로 캐스팅 성공
        MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();

        log.info("proxy class={}", memberServiceProxy.getClass());

        //CGLIB 프록시를 구현 클래스로 캐스팅 시도 성공
        MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
    }

cglibProxy() 테스트
MemberServiceImpl타입 기반으로 CGLIB 프록시를 생성했다. MemberServiceImpl타입은 MemberService인터페이스를 구현했다. CGLIB는 구현 클래스를 기반으로 프록시를 생성한다. 정확히 말하면 상속을 사용한다. 따라서 CGLIB는 MemberServiceImpl 구체 클래스를 상속받아 프록시를 생성한다. 이 프록시를 CGLIB Proxy라고 하자. 여기서 memberServiceProxy가 바로 CGLIB Proxy이다.

CGIB 프록시 캐스팅

여기에서 CGLIB Proxy를 대상 클래스인 MemberServiceImpl 타입으로 캐스팅하면 성공한다.
왜냐하면 cGLIB는 구체 클래스를 기반으로 프록시를 생성하기 때문이다.(구현이 아닌 상속이다!!!)
CGLIB proxy는 MemberServiceImpl구체 클래스를 기반으로 생성된 프록시이다. 따라서 CGLIB Proxy는 MemberServiceImpl은 물론이고 MemberService인터페이스로도 캐스팅이 가능하다.

정리
JDK 동적 프록시는 대상 객체인 MemberServiceImpl로 캐스팅할 수 없다.
CGLIB 프록시는 대상 객체인 MemberServiceImpl로 캐스팅할 수 있다.
그런데, 프록시를 캐스팅할 일이 많지 않을것같은데? 진짜 문제는 의존관계 주입시에 발생한다.

참고
proxyFactory.setProxyTargetClass를 생략하면 jdk 동적 프록시를 사용함.

프록시 기술과 한계 - 의존관계 주입

JDK 동적 프록시를 사용하면서 의존관계 주입을 받는 경우 문제를 아래 코드를 통해 확인해보면 다음과 같다.

@Slf4j
@Aspect
public class ProxyDIAspect {

    @Before("execution(* hello.aop..*.*(..))")
    public void doTrace(JoinPoint joinPoint) {
        log.info("[proxyDIAdvice] {}", joinPoint.getSignature());
    }
}
@Slf4j
//@SpringBootTest(properties = {"spring.aop.proxy-target-class=false"}) //JDK 동적 프록시
//@SpringBootTest(properties = {"spring.aop.proxy-target-class=true"}) //CGLIB 프록시
@SpringBootTest
@Import(ProxyDIAspect.class)
public class ProxyDITest {

    @Autowired
    MemberService memberService;

    @AutowiredMemberServiceImpl
    MemberServiceImpl memberServiceImpl;

    @Test
    void go() {
        log.info("memberService class={}", memberService.getClass());
        log.info("memberServiceImpl class={}", memberServiceImpl.getClass());
        memberServiceImpl.hello("hello");
    }
}
  • @SpringBootTest : 내부에 컴포넌트 스캔을 포함하고 있다. MemberServiceImpl@Component가 붙어 있으므로 스프링 빈 등록 대상이 된다.
  • properties={"spring.aop.proxy-target-class=false"} : application.properties에 설정하는 대신에 해당 테스트에서만 설정을 임시로 적용하기로 한다. 이렇게 하면 각 테스트마다 다른 설정을 손쉽게 적요할 수 있다.
  • spring.aop.proxy-target-class=false: 스프링이 AOP 프록시를 생성할 때 JDK 동적 프록시를 우선 생성한다. 물론 인터페이스가 없다면 CGLIB를 사용한다.
  • @Import(ProxDIAspect.class) : 앞서 만든 Aspect를 스프링빈으로 등록한다.

JDK 동적 프록시를 구체 클래스 타입에 주입
JDK동적 프록시를 구체 클래스 타입에 주입할 때 어떤 문제가 발생하는지 아래 코드를 보면 확인할 수 있다.

JDK 동적 프록시를 우선 생성하도록 하기 위해서 spring.aop.proxy-target-class=false설정을 사용한다.
하지만 이렇게 해놓고 실행하면 아래와 같은 오류가 발생한다.

BeanNotOfRequiredTypeException: Bean named 'memberServiceImpl' is expected to 
be of type 'hello.aop.member.MemberServiceImpl' but was actually of type 
'com.sun.proxy.$Proxy54'

타입과 관련된 예외가 발생한다. 자세히 읽어보면 memberServiceImpl에 주입되길 기대하는 타입은 hello.aop.member.MemberServiceImpl이지만 실제로 넘어온 타입은 com.sun.proxy.$Proxy54이다. 따라서 타입 예외가 발생한다고 한다.

  • @Autowired MemberService memberService :이 부분은 문제가 없다. JDK Proxy는 MemberService인터페이스를 기반으로 만들어진다. 따라서 해당 타입으로 캐스팅할 수 있다.
  • @Autowired MemberServiceImpl memberServiceImpl : 이 부분이 문제가 된다. JDK Proxy는 MemberService 인터페이스를 기반으로 만들어진다. 따라서 MemberServiceImpl타입이 뭔지 전혀 모른다. 그래서 해당 타입에 주입할 수 없다.

CGLIB 프록시를 구체 클래스 타입에 주입
JDK 동적 프록시 대신에 CGLIB 를 사용해서 프록시를 적용해보자.

//@SpringBootTest(properties = {"spring.aop.proxy-target-class=false"}) //JDK 
동적 프록시, DI 예외 발생
@SpringBootTest(properties = {"spring.aop.proxy-target-class=true"}) //CGLIB 
프록시, 성공

이후에 실행하면 정상적으로 구체 클래스에서 의존성 주입이 되는걸 확인할 수 있다.

  • @Autowired MemberService memberService : CGLIB Proxy는 MemberServiceImpl구체클래스를 상속받아 만들어진다. MemberServiceImplMemberService인터페이스를 구현했기 때문에 해당 타입으로 캐스팅할 수 있다.
  • @Autowired MemberServiceImpl memberServiceImpl : CGLIB Proxy는 MemberServiceImpl 구체 클래스를 상속받아 만들어진다. 따라서 해당 타입으로 캐스팅할 수 있다.

정리
JDK 동적 프록시는 대상 객체인 MemberServiceImpl 타입에 의존관계 주입을 할 수 없다.
CGLIB 프록시는 대상 객체인 MemberServiceImpl타입에 의존관계 주입을 할 수 있다.

지금까지 JDK 동적 프록시가 가지는 한계점에 대해서 학습했는데, 사실 실제로 개발 할때는 인터페이스가 있으면 인터페이스를 기반으로 의존관계를 주입하는게 맞다.
DI의 장점은 DI 받는 클라이언트 코드의 변경 없이 구현 클래스를 변경할 수 있는것이다. 이렇게 하려면 인터페이스를 기반으로 의존관계를 주입받아야하낟. MemberServiceImpl타입으로 의존관계를 주입받는것처럼 구현 클래스에 의존관계를 주입하면 향후 구현 클래스를 변경할댸 의존관계주입받는 클라이언트 코드도 함께 변경해야한다.
따라서 올바르게 잘 설계된 애플리케이션이라면 이런 문제가 자주 발생하지 않는다.

그럼에도 불구하고 테스트, 또는 여러가지 이유로 AOP 프록시가 적용된 구체 클래스를 직접 의존관계 주입을 받아야하는 경우가 있을 수 있다. 이떄는 CGLIB를 통해 구체 클래스 기반으로 AOP 프록시를 적용하면 된다.

사실여기까지정리된내용만 보면 JDK 동적 프록시보다 CGLIB를 사용하는게 훨씬 좋아보인다.
하지만 CGLIB에도 단점은 존재한다.

프록시 기술과 한계 - CGLIB

CGLIB는 구체 클래스를 상속 받기 때문에 아래와 같은 문제가 있다.
CGLIB 구체 클래스 기반 프록시 문제점

  • 대상 클래스에 기본 생성자 필수
  • 생성자 2번 호출 문제
  • fianl 키워드 클래스, 메서드 사용 불가

대상 클래스에 기본 생성자 필수

CGLIB는 구체 클래스를 상속받는다. 자바언어에서 상속받으면 자식 클래스의 생성자를 호출할때 자식 클래스의 생성자에서 부모 클래스의 생성자도 호출해야한다. (이 부분이 생략되어 있따면 자식 클래스의 생성자 첫줄에 부모 클래스의 기본 클래스를 호출하는 super()가 자동으로 들어간다.) 이 부분은 자바 문법 규약이다.
CGLIB를 사용할떄 CGLIB가 만드는 프록시의 생성자는 우리가 호출하는게 아니다. CGLIB 프록시는 대상 클래스를 상속받고, 생성자에서 대상 클래스의 기본 생성자를 호출한다. 따라서 대상 클래스에 기본 생성자를 만들어야한다.

생성자 2번 호출 문제

CGLIB는 구체 클래스를 상속받는다. 자바언어에서 상속을 받으면 자식 클래스의 생성자를 호출할댸 부모 클래스의 생성자도 호출해야한다. 그런데 왜 2번일까?

  • 1.실제 target의 객체를 생성할때
  • 2.프록시 객체를 생성할때 부모의 생성자 호출

fina 키워드 클래스, 메서드 사용 불가

final 키워드가 클래스에 있으면 상속이 불가능하고, 메서드에 있으면 오버라이딩이 불가능하다. CGLIB는 상속을 기반으로 하기 때문에 두 경우 프록시가 생성되지 않거나 정상 동작하지 않을 수 있다.

사실 프레임워크 같은 개발이 아니라 일반적인 웹 애플리케이션을 개발할때는 final키워드를 잘 사용하지 않는다. 따라서 이 부분은 특별히 문제가 된진 않는다.

정리
JDK 동적 프록시는 대상 클래스 타입으로 주입할때 문제가 있고, CGLIB는 대상 클래스에 기본 생성자 필수, 생성자 2번 호출 문제가 있다.

그렇다면 스프링은 어떤 방법을 권장할까?

프록시 기술과 한계 - 스프링의 해결책

스프링은 AOP 프록시 생성을 편리하게 제공하기 위해 여러 문제를 해결했다.

스프링 3.2 , CGLIB를 스프링 내부에 함께 패키징

CGLIB를 사용하려면 CGLIB 라이브러리가 별도로 필요했다. 스프링은 CGLIB 라이브러리를 스프링 내부에 함께 패키징해서 별도의 라이브러리 추가 없이 CGLIB를 함께 사용할 수 있게 되었다.
CGLIB spring-core org.spring.framework

CGLIB 기본 생성자 필수 문제 해결

스프링 4.0 부터 CGLIB의 기본 생성자 필수인 문제가 해결되었다.
objenesis라는 특별한 라이브러리를 사용해서 기본 생성자 없이 객체 생성이 가능하다.
참고로 이 라이브러리는 생성자 호출 없이 객체를 생성할 수 있게 해준다.

생성자 2번 호출 문제

스프링 4.0 부터 CGLIB의 생성자 2번 호출 문제가 해결되었다.
이것도 역시 objenesis라는 특별한 라이브러리 덕분에 가능해졌다.
이제는 생성자가 1번만 호출된다.

스프링 부트 2.0 - CGLIB 기본 사용
스프링 부트 2.0버전 부터 CGLIB를 기본으로 사용하도록 했다.
이렇게 해서 구체 클래스 타입으로 의존관계를 주입하는 문제를 해결했다.
스프링 부트는 별도의 설정이 없다면 AOP를 적용할때 기본적으로 proxyTargetClass=true로 설정해서 사용한다.
따라서 인터페이스가 있어도 JDK 동적 프록시를 사용하는것이 아니라 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성한다.
물론 스프링은 우리에게 선택권을 주기 떄문에 아래와같이 설정하면 JDK 동적 프록시도 사용할 수 있다.

//application.propertie
spring.aop.proxy-target-class=false

정리
스프링은 최종적으로 스프링 부트 2.0에서 CGLIB를 기본으로 사용하도록 결정했다. CGLIB를 사용하면 JDK 동적 프록시에서 동작하지 않는 구체 클래스 주입이 가능하다. 여기에 추가로 CGLIB의 단점들이 이제는 많이 해결되었다. CGLIB의 남은 문제라면 fianl 클래스나 메서드가 있는데 AOP를 적용할 대상에 final 클래스나 final 메서드를 잘 사용하지 않으므로 이 부분은 크게 문제가 되지 않는다.

참고

해당 포스팅은 아래의 강의를 공부하여 정리한 내용입니다.
김영한님의 프록시 - 내부호출

0개의 댓글