관리 메뉴

나구리의 개발공부기록

스프링 AOP - 실무 주의사항, 프록시와 내부 호출(문제/대안), 프록시 기술과 한계(타입캐스팅/의존관계 주입/CGLIB/스프링의 해결책) 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 핵심원리 - 고급편

스프링 AOP - 실무 주의사항, 프록시와 내부 호출(문제/대안), 프록시 기술과 한계(타입캐스팅/의존관계 주입/CGLIB/스프링의 해결책)

소소한나구리 2024. 11. 16. 17:25

출처 : 인프런 - 스프링 핵심 원리 - 고급편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 프록시와 내부 호출 - 문제

1) 문제점

(1) 문제 설명

  • 스프링은 프록시 방식의 AOP를 사용하므로 AOP를 적용하려면 항상 프록시를 통해서 대상 객체(target)을 호출해야 프록시에서 먼저 어드바이스를 호출하고 이후에 대상 객체를 호출함
  • 만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고 어드바이스도 호출되지 않음
  • AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링빈으로 등록하여 의존관계 주입시에 항상 프록시 객체를 주입함
  • 프록시 객체가 주입되기 때문에 대상 객체를 직접 호출하는 문제는 일반적으로 발생하지 않음
  • 그러나 대상 객체의 내부에서 메서드 호출이 발생하며 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생함
  • 해당 문제는 실무에서 반드시 한번은 만나서 고생하는 문제이기때문에 꼭 이해하고 넘어가야함

2) 예제

(1) CallServiceV0

  • internalcall 패키지를 생성하여 작성
  • 외부에서 external()을 호출하면 내부에서 internal()이라는 자기 자신의 메서드를 호출
  • 자바 언어에서 메서드를 호출할 때 대상을 지정하지 않으면 앞에 자기 자신의 인스턴스를 뜻하는 this가 자동으로 붙어서 실행됨
package hello.aop.internalcall;

@Slf4j
@Component
public class CallServiceV0 {

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

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

 

(2) CallLogAspect

  • internalcall 패키지 하위에 aop패키지를 생성하여 작성
  • CallServiceV0에 적용할 AOP
package hello.aop.internalcall.aop;

@Slf4j
@Aspect
public class CallLogAspect {

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

 

(3) CallServiceV0Test

  • @Import로 CallLogAspect를 스프링빈으로 등록하여 CallServiceV0에 AOP프록시를 적용
  • @SpringBootTest: 내부에 컴포넌트 스캔을 포함하고 있어 CallServiceV0에 적용한 @Component를 스프링 빈으로 적용하여 테스트를 수행함
package hello.aop.internalcall;

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

    @Autowired CallServiceV0 callServiceV0;

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

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

}

 

(4) external() 실행 결과 및 동작 설명

external() 실행 결과
external() 메서드 동작 설명

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

(5) internal() 실행 결과 및 동작 설명

internal() 실행 결과
internal() 메서드 동작 설명

  • 외부에서 internal()을 호출하는 테스트의 결과를 보면 이 경우에는 프록시를 거치기 때문에 CallLogAspect 어드바이스가 적용된 모습을 확인할 수 있음

(6) 프록시 방식의 AOP 한계

  • 스프링은 프록시 방식의 AOP를 사용하며 프록시 방식의 AOP는 메서드 내부 호출에 프록시를 적용할 수 없음

** 참고

  • 실제 코드에 AOP를 직접 적용하는 AspectJ 프레임워크를 사용하면 이런 문제가 발생하지 않음
  • 프록시를 통하는 것이 아니라 해당 코드에 직접 AOP 적용 코드가 붙어있기 때문에 내부 호출과 무관하게 AOP를 적용할 수 있음
  • 하지만 로드 타임 위빙 등을 사용해야하므로 설정의 복잡함과 JVM 옵션을 주어야하는 부담이 있으며, 이후에 설명할 프록시 방식의 AOP에서 내부 호출의 문제점을 대응할 수 있는 대안들이 있으므로 AspectJ를 직접 사용하는 방법은 실무에서 거의 사용하지 않음
  • 스프링 애플리케이션과 함께 직접 AspectJ를 사용하는 방법은 스프링 공식 메뉴얼을 참고
  • 공식메뉴얼

2. 대안1 - 자기 자신 주입

** 주의! - 스프링 부트 2.6 이상 사용시 설정 변경필요

  • 스프링 부트 2.6 부터는 순환 참조를 기본적으로 금지하도록 정책이 변경되어 생성자 주입이 아닌 수정자 주입을 하여도 순환참조 에러가 발생함
  • 해당 문제를 해결하려면 application.properties에 spring.main.allow-circular-references=true를 추가해야함
  • 이후의 다른 테스트에도 영향을 주기 때문에 스프링 2.6 이상이라면 해당 설정을 추가하고 실습을 진행해야함

1) 내부호출을 해결하는 가장 간단한 방법

  • 자기 자신을 의존관계 주입받는 것

(1) CallServiceV1

  • 수정자를 통해서 자기자신을 주입
  • 스프링 AOP가 적용된 대상을 의존관계 주입을 받으면 주입 받은 대상은 실제 자신이 아니라 프록시 객체임
  • external()을 내부에서 callServiceV1.internal()를 호출하도록 작성하여 주입받은 프록시를 통해 internal()을 호출이 되어 AOP가 적용됨

** 참고

  • 이 경우 생성자 주입하면 오류가 발생하는데 본인을 생성하면서 주입해야하기때문에 순환사이클이 만들어짐
  • 반면 수정자 주입은 스프링이 생성된 이후에 주입할 수 있기 때문에 오류가 발생하지 않음 - 위 주의! 확인필요
package hello.aop.internalcall;

/**
 * 참고: 자기 자신을 의존관계 주입받을 때 생성자 주입을 사용하면 순환 사이클을 만들기 때문에 실패함
 */
@Slf4j
@Component
public class CallServiceV1 {

    private CallServiceV1 callServiceV1;

    // setter 주입
    @Autowired
    public void setCallServiceV1(CallServiceV1 callServiceV1) {
        this.callServiceV1 = callServiceV1;
    }

    public void external() {
        log.info("call external");
        callServiceV1.internal(); // 자기 자신을 참조한 변수로 internal()을 외부호출
    }

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

 

(2) CallServiceV1Test 실행결과

  • CallServiceV1을 의존관계 주입하여 external()을 테스트 실행해보면 internal()에 AOP가 적용된 것을 확인할 수 있음
@Slf4j
@Import(CallLogAspect.class)
@SpringBootTest
class CallServiceV1Test {

    @Autowired CallServiceV1 callServiceV1;

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

 

callServiceV1.external() 실행 결과
callserviceV1.external() 실행 구조


3. 대안2 - 지연 조회

1) 스프링 빈을 지연 조회

  • ObjectProvider(Provider), ApplicationContext를 사용

(1) CallServiceV2

  • ApplicationContext나 ObjectProvider를 사용하면 객체를 스프링 컨테이너에서 조회하는 것을 스프링 빈 생성 시점이 아니라 실제 객체를 사용하는 시점으로 지연시킬 수 있음
  • ApplicationContext는 너무 많은 기능을 제공하므로 직접 ApplicationContext를 사용하는 것보다 기본편에서 학습한 ObjectProvider를 활용하는 것이 더 나음
  • callServiceProvider.getObject()를 호출하는 시점에 스프링 컨테이너에서 빈을 조회함
  • 자기 자신을 주입 받는 것이 아니기 때문에 순환 사이클이 발생하지 않는다
package hello.aop.internalcall;

@Slf4j
@Component
public class CallServiceV2 {

    // ApplicationContext 활용, 기능이 너무 많기 때문에 권장하지 않음
//    private final ApplicationContext applicationContext;

//    public CallServiceV2(ApplicationContext applicationContext) {
//        this.applicationContext = applicationContext;
//    }

    // ObjectProvider 활용
    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");
    }
}

 

(2) CallserviceV2Test 작성 및 실행

  • CallServiceV2를 의존관계 주입 받아서 위의 테스트케이스와 동일하게 external()을 호출하는 테스트를 실행하면 external()메서드 내부에서 호출되는 internal()메서드에도 AOP가 적용되는 모습을 확인할 수 있음

4. 대안3 - 구조 변경

1) 가장 나은 대안

  • 앞선 방법들은 자기 자신을 주입하거나 Provider를 사용하여 조금 억지로 문제를 해결하였음(어색한 해결 방법)
  • 가장 나은 대안으로는 내부 호출이 발생하지 않도록 구조를 변경하는 것이며 스프링에서도 이 방법을 가장 권장함

(1) CallServiceV3

  • CallService에 있던 internal() 메서드를 InternalService라는 별도의 클래스로 분리하고 해당 클래스를 의존관계를 주입받아서 외부에서 internal()메서드를 호출
  • 즉, external()메서드가 호출되면 internalService의 internal()메서드가 호출되어 내부호출 구조자체가 분리 되었음
package hello.aop.internalcall;

/**
 * 구조를 변경(분리)
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class CallServiceV3 {

    private final InternalService internalService;

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

 

(2) InternalService

  • internal()을 가지고 있는 InternalService클래스
@Slf4j
@Component
public class InternalService {

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

 

(3) CallServiceV3Test  작성 및 실행

동작 설명

  • 테스트클래스에서 의존관계를 CallServiceV3로 변경하고 external()메서드를 호출하는 테스트 케이스를 실행하면 정상적으로 AOP가 모두 적용되는 로그를 확인할 수 있음
  • 내부 호출 자체가 사라지고 callService -> internalService를 호출하는 구조로 변경되어 자연스럽게 AOP가 적용됨
  • 여기서 구조를 변경한다는 것은 단순하게 분리하는 것뿐만아니라 클라이언트에서 두 메서드를 호출하는 등의 다양한 방법을 적용하여 문제를 해결할 수 있음
  • 물론 클라이언트에서 두 메서드를 호출하여 문제를 해결할 때에는 external()에서 internal()을 내부 호출하지 않도록 코드를 변경하야 가능하므로 비즈니스와 현재 애플리케이션의 구조를 보고 적절하게 적용하면 됨

** 참고

  • AOP는 주로 트랜잭션 적용이나 주요 컴포넌트의 로그 출력 기능에 사용됨
  • 즉, 인터페이스에 메서드가 나올 정도의 규모에 AOP를 적용하는 것이 적당하며 public 메서드에만 적용함
  • 애초에 private 메서드처럼 작은단위에는 AOP를 적용하지도 않고 할 수도 없으며 AOP 적용을 위해 private 메서드를 외부 클래스로 변경하고 public으로 변경하는 일은 거의 없음
  • public 메서드에서 public 메서드를 내부 호출 하는 경우에 이런 문제가 발생하며 실무에서 꼭 한번은 만나는 문제이기 때문에 AOP가 잘 적용되지 않으면 내부 호출을 의심해봐야 함

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

1) JDK 동적 프록시 CGLIB 프록시

(1) 리마인드

  • JDK 동적 프록시는 인터페이스가 필수이고 인터페이스를 기반으로 프록시를 생성
  • CGLIB는 구체 클래스를 기반으로 프록시를 생성
  • 인터페이스가 없고 구체클래스만 있는 경우에는 CGLIB를 사용해야만 하며 인터페이스가 있는 경우에는 둘중에 하나를 선택할 수 있음
  • 스프링이 프록시를 만들 때 제공하는 ProxyFactory에 proxyTargetClass 옵션에 따라서 둘중 하나를 선택해서 만들 수 있음
  • proxyTargetClass=false로 하면 JDK 동적 프록시를 사용하고 true로 하면 CGLIB를 사용하여 프록시를 생성하며 기본값은 false임
  • 옵션과 무관하게 인터페이스가 없으면 JDK 동적 프록시를 적용할 수 없으므로 CGLIB를 사용함

(2) JDK 동적 프록시 한계

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

2) 예제

(1) ProxyCastingTest

  • test하위에 proxyvs 패키지를 생성 후 작성
  • JDK 동적 프록시를 사용하도록 설정하여 ProxyFactory로 MemberServiceImpl을 target으로하여 프록시를 생성
  • proxyFactory.getProxy()로 프록시를 조회할 때 MemberService 인터페이스 타입으로 캐스팅하면 JDK 동적 프록시는 인터페이스를 기반으로 프록시를 생성하기 때문에 당연히 성공함
  • 그러나, 다시 MemberServiceImpl 구체클래스 타입으로 형변환을 하고자하면 형변환 에러가 발생함
package hello.aop.proxyvs;

@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();

        // memberServiceProxy -> MemberServiceImpl 타입캐스팅 - 실패 (ClassCastException 에러발생)
        assertThatThrownBy(() -> {
            MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
        }).isInstanceOf(ClassCastException.class);
    }
}

 

(2) jdkProxy() 테스트 설명

JDK 동적 프록시 동작 설명

  • MemberServiceImpl 타입을 기반으로 JDK 동적 프록시를 생성하도록 코드를 작성하였지만 MemberServiceImpl은 MemberService인터페이스를 구현하므로 JDK 동적 프록시는 MemberService 인터페이스를 기반으로 프록시를 생성함
  • JDK 동적 프록시는 인터페이스를 기반으로 프록시를 생성하기 때문에 이렇게 생성된 인터페이스 기반의 프록시는 MemberService 인터페이스타입으로는 캐스팅이 가능함
  • 그러나 구현체인 MemberServiceImpl을 전혀 알지 못하기 때문에 구체 클래스 타입으로 형변환을 시도하면 ClassCastException.class(형변환 예외)가 발생함

(3) ProxyCastingTest - cglibProxy()추가

  • CGLIB를 사용하는 테스트 케이스 추가
  • setProxyTargetClass(true)를 적용하여 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());

    // 다시 구체 클래스로 형변환 - 성공
    MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
}

 

(4) cglibProxy() 테스트 설명

CGLIB 동작 설명

  • MemberServiceImpl 타입을 기반으로 CGLIB 프록시를 생성
  • CGLIB는 구체 클래스를 기반으로 프록시를 생성하였기 때문에 생성된 프록시는 MemberServiceImpl은 물론이고, MemberServiceImpl이 구현한 인터페이스인 MemberService로도 캐스팅할 수 있음

(5) 정리

  • JDK 동적 프록시는 대상 객체인 MemberServiceImpl로 캐스팅 할 수 없지만 CGLIB 프록시는 캐스팅 할 수 있음

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

1) JDK 동적 프록시의 의존관계 주입 문제

(1) ProxyDIAspect

  • proxyvs패키지 하위에 code패키지를 만든 후 작성
  • hello.aop와 그 하위의 모든 대상에 간단히 로그를 출력하는 애스펙트
package hello.aop.proxyvs.code;

@Slf4j
@Aspect
public class ProxyDIAspect {

    @Before("execution(* hello.aop..*.*(..))")
    public void doTrace(JoinPoint joinPoint) {
        log.info("[proxyDIAdvice] {}", joinPoint.getSignature());
    }
}

 

(2) ProxyDITest

  • application.properties에 설정을 적용하는 대신 @SpringBootTest의 애노테이션으로 옵션을 적용하여 해당 테스트에만 임시로 스프링이 AOP를 생성할 때 JDK 동적 프록시를 우선 생성하도록 설정, 물론 인터페이스가 없으면 CGLIB를 사용함
  • @Import로 ProxyDIAspect를 스프링 빈으로 등록
package hello.aop.proxyvs;

@Slf4j
@SpringBootTest(properties = {"spring.aop.proxy-target-class=false"})   // JDK 동적 프록시 사용 - 테스트 실패(에러발생)
@Import(ProxyDIAspect.class)
public class ProxyDITest {

    @Autowired
    MemberService memberService;

    @Autowired
    MemberServiceImpl memberServiceImpl;

    @Test
    void go() {
        log.info("memberService class={}", memberService.getClass());
        log.info("memberServiceImpl class={}", memberServiceImpl.getClass());
        memberServiceImpl.hello("hello");
    }
}

 

(3) 테스트 실패

에러메세지 일부
JDK 동적 프록시의 의존관계 주입

  • 테스트코드를 실행해보면 에러메세지와 함께 테스트를 실패하는데, memberServiceImpl에 주입되길 기대하는 타입이 hello.aop.member.MemberServiceImpl이지만 실제 넘어온 타입이 com.sun.proxy.$Proxy57이여서 타입 예외가 발생하였음
  • JDK 동적 프록시로 생성된 프록시는 인터페이스를 기반으로 만들어졌기 때문에 MemberService 인터페이스의 의존관계 주입이 가능하지만 MemberServiceImpl타입은 전혀 알 수 없기 때문에 의존관계 주입도 할 수 없음

(4) CGLIB사용 - 테스트 통과

//@SpringBootTest(properties = {"spring.aop.proxy-target-class=false"})   // JDK 동적 프록시 사용 - 테스트 실패(에러발생)
@SpringBootTest(properties = {"spring.aop.proxy-target-class=true"})   // CGLIB 사용 - 성공

테스트 성공 로그
CGLIB 프록시의 의존관계 주입

  • 설정을 CGLIB를 사용하도록 변경한 후 테스트를 실행하면 테스트 통과와 함께, CGLIB 프록시로 AOP가 적용되는 로그를 확인할 수 있음
  • CGLIB로 생성된 프록시는 구체 클래스를 상속받아서 생성되기 때문에 구체클래스는 물론 구현한 인터페이스까지도 모두 의존관계 주입을 할 수 있음

(5) 정리

  • JDK 동적 프록시는 대상 객체인 MemberServiceImpl 타입에 의존관계를 주입할 수 없고 CGLIB프록시는 의존관계 주입이 가능함
  • JDK 동적 프록시가 가지는 한계점을 알아 보았는데, 실제로 개발할 때는 인터페이스가 있으면 인터페이스를 기반으로 의존관계 주입을 받는 것이 맞음
  • MemberServiceImpl 타입으로 의존관계를 주입받는 것처럼 구현 클래스에 의존관계를 주입하면 향후 구현 클래스를 변경할 때 클라이언트의 코드도 함께 변경해야하는 문제가 있음
  • DI의 장점이 바로 의존관계 주입을 받는 클라이언트 코드의 변경 없이 구현 클래스를 변경할 수 있는 것인데, 이렇게 하려면 인터페이스를 기반으로 의존관계를 주입받아야하므로 올바르게 잘 설계된 애플리케이션이라면 이런 문제가 자주 발생하지는 않음
  • 그럼에도 불구하고 테스트 또는 여러가지 이유로 AOP 프록시가 적용된 구체 클래스를 직접 의존관계 주입을 받아야하는 경우가 있을 수 있는데 이때는 CGLIB를 통해 구체 클래스 기반으로 AOP 프록시를 적용하면 됨
  • 사실 CGLIB를 사용하면 이런 고민 자체를 하지 않아도 되지만 CGLIB도 단점이 존재함

 


7. 프록시 기술과 한계 - CGLIB

1) CGLIB 구체 클래스 기반 프록시 문제점

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

  • 자바 언어에서 상속을 받으면 자식 클래스의 생성자를 호출할 때 자식 클래스의 생성자에서 부모 클래스의 생성자도 호출해야 함
  • 이 부분이 생략되어 있다면 자식 클래스의 생성자 첫줄에 부모 클래스의 기본 생성자를 호출하는 super()가 자동으로 들어가며 이러한 동작방식은 자바 문법 규약임
  • CGLIB를 사용할 때 CGLIB가 만드는 프록시의 생성하는 개발자가 직접 호출하지 않고 CGLIB프록시가 대상 클래스를 상속받고 생성자에서 대상 클래스의 기본생성자를 호출하기 때문에 대상 클래스에 기본 생성자를 만들어 주어야 함
  • 자바 문법상 생성자가 하나도 없으면 자동으로 만들어지며 파라미터가 있는 생성자가 있다면 직접 기본생성자를 만들어 주어야 함

(2) 생성자 2번 호출 문제

생성자가 2번 호출됨

  • CGLIB 프록시를 사용하면 실제 target의 객체를 생성할 때 생성자를 1번 호출하고, 프록시 객체를 생성할 때 부모 클래스의 생성자를 1번 더 호출하여 총 2번 호출됨

(3) final 키워드 클래스, 메서드 사용 불가

  • final 키워드가 클래스에 있으면 상속이 불가능하고 메서드에 있으면 오버라이딩이 불가능하기 때문에 상속을 기반으로 하는 CGLIB가 프록시를 생성되지 않거나 정상 동작하지 않음
  • 프레임워크 같은 개발이 아니라 일반적인 웹 애플리케이션을 개발할 때는 final 키워드를 잘 사용하지 않기 때문에 해당 문제는 특별히 문제가 되지는 않음

(4) 정리

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

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

1) 스프링의 기술 선택 변화

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

  • CGLIB를 사용하려면 CGLIB 라이브러리가 별도로 필요했었으나 스프링 3.2 부터는 스프링 내부에 함께 패키징하여 별도의 라이브러리 추가 없이 CGLIB를 사용할 수 있게 되었음
  • CGLIB spring-core org.springframework

(2) objenesis 라이브러리 사용으로 문제 해결

  • 스프링 4.0 부터 objenesis라는 특별한 라이브러리를 사용하여 기본 생성자 없이 객체 생성이 가능하게 되었음
  • 해당 라이브러리가 생성자 호출 없이 객체를 생성할 수 있게 해줌으로써 프록시를 생성할 때 부모클래스의 생성자를 호출할 필요가 없게됨
  • 즉, 기본 생성자가 필수인 문제와 생성자가 2번 호출되는 문제가 모두 한방에 해결되었음

(3) 스프링 부트 2.0 - CGLIB 기본사용

  • 스프링 부트 2.0 버전부터 CGLIB를 기본으로 사용하도록 하였기 때문에 스프링 부트로 프로젝트를 생성하고 별도의 설정을 하지 않는다면 AOP를 적용할 때 기본적으로 proxyTargetClass=true 설정이 적용되어 인터페이스가 있어도 항상 CGLIB를 사용하여 구체클래스 기반으로 프록시를 생성함
  • 물론 스프링은 우리에게 선택권을 열어주기 때문에 여러 예제를 통해 설정을 적용했던 것처럼 JDK 동적 프록시도 사용할 수 있음

(4) 정리

  • 스프링은 최종적으로 CGLIB를 기본으로 사용하도록 결정하여 JDK 동적 프록시에서 동작하지 않는 구체 클래스 주입이 가능하도록 문제를 해결하였으며 추가적인 CGLIB의 단점들도 해결하였음
  • CGLIB의 남은 문제라면 final클래스나 메서드가 있는데, AOP를 적용할 대상에는 final 클래스나 메서드를 잘 사용하지 않기 때문에 이부분은 크게 문제가 되지 않음
  • 개발자 입장에서보면 어떤 프록시 기술을 사용하든 크게 상관이 없으며 심지어 새로운 프록시 기술을 사용해도 문제없고 개발하기에 편리하면 됨
  • 심지어 클라이언트 입장에서는 어떤 프록시 기술을 사용하는지 모르고 잘 동작하는 것이 가장 좋음