2 분 소요

 인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.

지난 시간마지막에 JDK 동적 프록시를 사용하면서 의존관계에 문제가 있다고 말했다.

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

JDK 동적 프록시를 사용하면서 의존관계 주입을 할 때 어떤 문제가 발생하는지 코드로 알아보자.

hello.aop.proxyvs.code.ProxyDIAspect

1
2
3
4
5
6
7
8
9
@Slf4j  
@Aspect  
public class ProxyDIAspect {  
  
    @Before("execution(* hello.aop..*.*(..))")  
    public void doTrace(JoinPoint joinPoint) {  
        log.info("[proxyDIAdvice] {} ", joinPoint.getSignature());  
    }  
}

AOP 프록시 생성을 위해 간단한 Aspect를 만들었다.

ProxyDITest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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 = {}", memberService.getClass());  
        log.info("memberServiceImpl = {}", memberServiceImpl.getClass());  
  
        memberServiceImpl.hello("hello");  
    }  
}
  • properties = {"spring.aop.proxy-target-class=false"} : application.properties에 설정하는 대신에 해당 테스트에만 설정을 임시로 적용한다. 이렇게 하면 각 테스트마다 다른 설정을 손쉽게 적용할 수 있다.
  • spring.aop.proxy-target-class=false : 스프링이 AOP 프록시를 생성할 때 JDK 동적 프록시를 우선 생성한다. 물론 인터페이스가 없다면 CGLIB를 사용한다.
  • @Import(ProxyDIAspect.class) : 앞서 만든 Aspect를 스프링 빈으로 등록한다.

JDK 동적 프록시를 구체 클래스 타입에 주입

JDK 동적 프록시를 구체 클래스 타입에 주입할 때 어떤 문제가 발생하는지 지금부터 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
@Autowired  
MemberService memberService;  

@Autowired  
MemberServiceImpl memberServiceImpl;


@Test  
void go() {  
	..
	memberServiceImpl.hello("hello");  
} 

다음과 같이 memberService, memberServiceImpl을 따로 의존성 주입 받았고

구체 클래스인 memberServiceImpl을 실행했다. 이러면 JDK 동적 프록시로 프록시가 적용될 것이다.

타입과 관련된 예외가 발생한다. 내용을 보면 'hello.aop.member.MemberServiceImpl'가 들어와야 하는데 jdk.proxy2.$Proxy52가 들어왔다고 한다.

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

CGLIB 프록시를 구체 클래스 타입에 주입

이번에는 JDK 동적 프록시 대신에 CGLIB를 사용해서 프록시를 적용해보자. 다음과 같이 주석을 반대로 걸어보자.

1
2
//@SpringBootTest(properties = {"spring.aop.proxy-target-class=false"}) // JDK 동적 프록시 적용  
@SpringBootTest(properties = {"spring.aop.proxy-target-class=true"}) // CGLIB 프록시 적용

실행하면 정상적으로 동작한다.

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

정리

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

지금까지 JDK 동적 프록시가 가지는 한계점을 알아보았다. 실제로 개발할 때는 인터페이스가 있으면 인터페이스를 기반으로 의존관계를 주입 받는 것이 맞다.

DI의 장점이 무엇인가? DI 받는 클라이언트 코드의 변경 없이 구현 클래스를 변경할 수 있는 것이다. 이렇게 하려면 인터페이스를 기반으로 의존관계를 주입 받아야 한다. MemberServiceImpl 타입으로 의존관계를 주입받는 것처럼 구현 클래스에 의존관계를 주입하면 향후 구현 클래스를 변경할 때 의존관계를 주입받는 클라이언트 코드도 함께 변경해야 한다.

따라서 올바르게 잘 설계된 애플리케이션이라면 이런 문제가 자주 발생하지 않는다.

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

여기까지 들어보면 CGLIB를 사용하는 것이 좋아 보인다.

하지만 CGLIB도 한계가 있다.

댓글남기기