늘
[프록시] 스프링에서 사용되는 proxy전략 본문
스프링과 JPA로 애플리케이션을 개발하다 보면 프록시에 대해서 많이 만나게 된다. 이와 관련되어 구글링을 해보면 게시글들마다 다른 말을 해서 직접 정리해보려고 한다. - 총정리
스프링 AOP는 [런타임에 프록시 인스턴스가 동적으로 변경되는] 다이나믹 프록시 기법으로 구현되어있다
인터페이스의 유무에 따라 다음과 같이 나뉜다.
1. JDK 다이나믹 프록시
2. CGLIB
JDK Dynamic Proxy
- JDK에서 지원하는 프록시 생성 방법
- Invocation Handler를 재정의한 invoke를 구현하여 부가기능 수행
- Reflection API 사용
- 인터페이스를 통해서만 프록시 생성 가능
CodeGenratorLibrary(CGLIB)
- 상속을 기반으로 프록시 생성
- final이 붙으면 오버라이딩이 불가능하므로 CGLIB프록시가 작동하지 않는다.
- Enhancer의존성을 추가해야 한다.
- 3.2 version Spring core 패키지에 포함되어서 의존성 추가하지 않아도 된다.
- Default 생성자가 필요하다.
- 4.0 version Objensis 라이브러리를 포함하면서 필요 없어졌다.
- 타겟의 생성자를 두 번 호출한다.
- 4.0 version Objensis 라이브러리를 포함하면서 한 번만 호출된다.
- Spring 4.3 & spring boot 1.4부터 default로 CGLIB을 사용하게 되었다.
따라서 실제 우리가 스프링부트에서 인터페이스를 만들고 Proxy를 적용하려고 해도 CGLIB이 적용된다. 인터페이스를 적용했을 때, JDK 동적 프록시를 사용하려면 properties혹은 yml파일에서 proxy-target-class를 false로 바꿔주면 된다.
성능
reflection api는 C코드로 실행한다. 그래서 느리다.
CGLIB을 사용할 때의 FastClass는 JVM 내에서 직접 메서드를 호출하는 바이트 코드를 만든다.
또한 JDK dynamic proxy는 reflection api를 사용하기 때문에 성능상 느리다.(JVM에 optimization이 작동하지 않으므로)
하지만 CGLIB은 바이트 코드를 사용하므로 성능이 빠르다.
굵은 글씨가 유의미한 지표라고 한다. 보면은 CGLIB이 JDK Dynamic 프록시보다 3배 가까이 성능이 좋다는 것을 알 수 있다.
왜 스프링 부트 2.0부터는 default로 CGLIB proxying을 사용할까?
인터페이스를 사용하여 DI를 적극 활용하는 방식을 두고 왜 CGLIB을 채택했을 까?
phil의 말에 따르면 아래와 같다고 한다.
인터페이스 기반 프록시는 때때로 ClassCast Exceptions를 추적하기 어렵게 한다. 특히, @Bean이 JDK 프록시로 대체되어 원래 클래스 형식으로 주입되지 않을 수 있었다.
스프링 프레임워크는 요즘 CGLIB의 음영 복사가 있기 때문에, 즉시 사용하지 않을 이유가 거의 없었다.
음영 복사에 대해서는 더 공부해봐야겠다.
프록시 팩토리
- 다이나믹 프록시를 생성한다
- 사용자가 코드를 쳐서 수동으로 생성한다.
- 인터페이스 유무에 따라 다이나믹 프록시를 생성한다.
스프링에선 팩토리 빈의 구현체인 프록시 팩토리 빈 사용한다. 팩토리 빈의 구현체이다.
- 부가기능을 invocationHandler가 아닌 addAdvice()를 통해 구현한다.
- Proxy Factory Bean은 인터페이스가 없어도 클래스를 받아서 인터페이스를 검출하여 프록시를 생성할 수 있다.
- 따라서 프록시가 타깃 오브젝트에 의존하지 않아도 된다.
프록시 팩토리 빈의 MethodInterceptor의 invoke메소드는 프록시 팩토리 빈으로부터 타겟에 대한 정보도 받는다.
그래서 MethodInterceptor가 타겟에 의존하지 않는다.
따라서 methodInterceptor 오브젝트는 타겟이 다른 여러 프록시에서 함께 사용할수 있고 싱글톤으로 빈 등록이 가능하다.
프록시 팩토리 빈 → 자동 프록시 생성기
프록시 자체는 aop가 아니다.
프록시를 만들면 빈 이름이 같은 게 만들어지는데 Quailfier를 안쓰고 이를 해결하는 방식이 자동 프록시 생성기가 컨테이너 초기화 때 만들어진 빈을 바꿔치기해서 프록시 빈을 자동 등록해준다. 이때 빈의 의존관계를 바꿔치기 하는 것은 빈 후처리기를 사용한다.
자동 프록시 생성기
- 자동 프록시 생성기 방식은 타겟을 빈으로 직접 노출되지 않는다!!
- aop적용 때문에 @Autowired를 사용하지 못하는 불상사를 막는다.
- proxy를 빈으로 등록하는 게 아닌 기존의 빈을 프록시 빈으로 후처리기를 통해 변경하여 저장하여 Qualifier를 적용할 필요가 없다.
스프링 프록시 기반 aop는 자동 프록시 생성기를 통해 동작한다.
스프링에선 팩토리 빈의 구현체인 프록시 팩토리 빈 사용 -
팩토리 빈의 구현체이다.
- 프록시가 타깃 오브젝트에 의존하지 않아도 된다.
- 부가기능은 addAdvice()로 구현한다.
프록시 팩토리 빈 → 자동 프록시 생성기
스프링 프록시 기반 aop는 자동 프록시 생성기를 통해 동작한다.
스프링과 프록시
JDK 다이내믹 프록시는 일반적인 방법으로 Spring의 Bean으로 등록할 수 없다
-> 왜냐하면 동적으로 프록시 객체를 알게 되므로 그 전에는 빈으로 등록 불가능
-> 팩토리 빈 등장
팩토리 빈
FactoryBean 인터페이스의 getObject() 메소드는 Bean 객체를 생성하는 목적을 가지고 있다.
따라서 FactoryBean의 구현 클래스를 구성하고 기존에 작성했던 Proxy.newInstance()의 로직(JDK Dynamic Proxy 구현 메서드)을 getObject() 메소드에 작성해주면 FactoryBean에 의해 프록시 객체가 Bean으로 생성된다.
public interface FactoryBean<T> {
T getObject() throws Exception; → Bean 객체를 생성하고 반환
Class<?> getObjectType(); → FactoryBean에 의해 생성된 객체의 Type
default boolean isSingleton() {return true;} → getObject()의 반환된 객체의 싱글톤 여부
}
Factory Bean 구현
public class MonitorFactoryBean implements FactoryBean<Object>{
private Class<?> interfaces;
private Object target;
@Override
public Object getObject() throws Exception {
return Proxy.newProxyInstance(getClass().getClassLoader()
, new Class[] {interfaces}
, new MonitorHandler(target));
}
}
프록시 패턴 - 접근방법 제어
데코레이터 패턴 - 부가기능 효과
프록시 방식의 AOP는 객체지향 디자인패턴의 데코레이터 패턴 또는 프록시 패턴을 응용해서 기존 코드에 영향 주지않는채로 부가기능을 제공할수 있는 oop에서 출발한다.
여기에 포인트 컷이라는 적용 대상 선택과 자동 프록시 생성이라는 적용까지 접목하면 aop가 된다.
그렇다면 엔티티에서는 왜 기본 생성자가 필요할까?
하이버네이트 공식 문서를 보면 "Hibernate는 Java Reflection을 사용하여 객체를 생성해야 합니다."라고 되어 있다. 즉 AOP는 CGLIB를 통해 상속을 받아 프록시 객체가 만들어지고 Hibernate의 Entity에서 프록시가 만들어질 때는 reflection ApI를 이용한다. 따라서 엔티티의 프록시에서 리플렉션을 사용해야 하므로 기본 생성자는 필요하다.
Reference
https://developer.jboss.org/docs/DOC-15785
https://www.inflearn.com/questions/105043
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-pfb-proxy-types
https://tecoble.techcourse.co.kr/post/2020-07-16-reflection-api/https://docs.jboss.org/hibernate/orm/6.1/quickstart/html_single/
'백앤드 개발일지 > 스프링부트' 카테고리의 다른 글
[Transaction] commit된 트랜잭션에 롤백된 트랜잭션이 참여하면 어떻게 될까? TranscationalEventListener와 함께 알아보자 (0) | 2022.12.02 |
---|---|
[RestDocs]API 문서화 (2) | 2022.06.03 |
@Mock vs @MockBean vs @InjectMocks (1) | 2022.05.31 |
[Spring bean lifecycle, hook]빈 생명주기 (0) | 2022.05.23 |
@RestController와 @ResponseBody없이 json으로 통신하는 방법 (2) | 2022.04.30 |