프록시의 사전적 정의는 '대리인
'으로, 간단하게 설명하면 내가 어떤 객체를 사용하려고 할 때 해당 객체에 직접 요청하는 것이 아닌 중간에 가짜 프록시 객체(대리인)를 두어서 프록시 객체가 대신해서 요청을 받아 실제 객체를 호출해 주도록 하는 것이다.
스프링에서 사용되는 프록시 기술은 크게 두 가지로 나눌 수 있습니다.
JDK 동적 프록시는 자바에서 제공하는 기본적인 프록시 기술입니다.
자바의 인터페이스를 기반
으로 동작하며, 인터페이스의 구현 클래스를 자동으로 생성하여 프록시 객체를 생성
합니다. 인터페이스의 메소드를 호출하면 프록시 객체는 InvocationHandler를 통해 해당 메소드를 호출하고 부가 기능을 추가합니다.
JDK 동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 된다. 이름 그대로 프록시 객체를 동적으로 런타임에 개발자 대신 만들어준다.
그리고 동적 프록시에 원하는 실행 로직을 지정할 수 있다.
JDK 동적 프록시 기술 덕분에 적용 대상 만큼 프록시 객체를 만들지 않아도 된다.
결과적으로 프록시 클래스를 수 없이 만들어야 하는 문제도 해결하고, 부가 기능 로직도 하나의 클래스에 모아서 단일 책임 원칙(SRP)도 지킬 수 있게 되었다.
적용후!
* JDK 동적 프록시는 인터페이스가 필수이다.
* CGLIB를 사용해야 한다.
참고로 우리가 CGLIB를 직접 사용하는 경우는 거의 없다. 이후에 설명할 스프링의 ProxyFactory 라는 것이 이 기술을 편리하게 사용하게 도와주기 때문에, 너무 깊이있게 파기 보다는 CGLIB가 무엇인지 대략 개념만 잡으면 된다.
CGLIB (Code Generation Library) Proxy는 JDK Dynamic Proxy와 달리 인터페이스가 아닌 클래스를 기반
으로 동작합니다.
런타임에 클래스의 서브클래스를 생성하여 프록시 객체를 생성
합니다.
이 때, 상속이 불가능한 final 클래스나 private 생성자를 가진 클래스는 프록시 객체를 생성할 수 없습니다.
메소드 호출 시 프록시 객체는 MethodInterceptor를 통해 해당 메소드를 호출하고 부가 기능을 추가합니다.
CGLIB
라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
- CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.
- CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
- CGLIB가 동적으로 생성하는 클래스 이름은 다음과 같은 규칙으로 생성된다.
대상클래스$$EnhancerByCGLIB$$임의코드
참고로 다음은 JDK Proxy가 생성한 클래스 이름이다.
proxyClass=class com.sun.proxy.$Proxy1
이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직
이 들어있다.
가짜 프록시 객체는 실제 request scope 와는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 있고, 싱글톤
처럼 동작한다.
스프링에서는 프록시 객체를 사용하기 위해 빈으로 프록시를 만들 수 있다.
그 프록시를 request scope 프록시
라고 한다.
request scope 프록시
란, HTTP 요청 단위의 스코프를 가지는 빈(Bean)에 대한 프록시 객체를 의미합니다. 이러한 프록시 객체는 실제 빈이 처음 요청될 때 생성되며, 그 후에는 요청에 대한 모든 빈의 참조가 이 프록시 객체로 전달된다.
이 프록시 객체는 해당 요청 범위 내에서 빈의 생명주기를 관리하고, 빈이 스코프에서 제거될 때 해당 빈의 destroy() 메서드를 호출하여 빈이 제대로 종료되도록 보장한다.
이를 통해, Spring 애플리케이션에서 Request scope 빈을 쉽게 사용할 수 있으며, 빈이 요청 범위 내에서만 존재하고 다른 요청에서는 재사용되지 않도록 할 수 있다.
CGLIB이라는 바이트 코드를 조작하는 라이브러리를 사용
해서 프록시 객체를 주입해준다.proxyMode
옵션을 사용하여 설정할 수 있다.@Component //클래스가 아닌 인터페이스라면 ScopedProxyMode.INTERFACE
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) //프록시 설정
public class HelloBean {
public void hello() { ... }
}
@Service
public class HelloService {
//실제 HelloBean 객체가 아닌 가짜 프록시 객체
@Autowired HelloBean helloBean;
public void method() {
//실제 요청이 일어날 때 프록시 객체는 실제 HelloBean의 메서드를 호출해준다.
helloBean.hello();
}
}
printProxyBean() 메서드를 호출해보면 자동 의존 주입을 통해 받은 HelloBean객체는 가짜 프록시 객체임을 알 수 있다.
//printProxyBean 메서드 호출 결과
ProxyHelloBean: HelloBean$$EnhancerBySpringCGLIB$$35c7aa62
RealHelloBean: HelloBean
프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다.
사실 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점
이다.
단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다.
꼭 웹스코프가 아니어도 프록시는 사용할 수 있다.
클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
생성자를 체크해야 한다.
CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본생성자가 필요하다.final 키워드
가 붙으면 상속이 불가능하다. CGLIB에서 예외가 발생한다.final 키워드
가 붙으면 해당 메서드를 오버라이딩 할 수 없다. => CGLIB에서는 프록시 로직이 동작하지 않는다.Jpa 조회기능 중 lazy loading 을 통해 proxy로 호출
Advice
는 프록시에 적용하는 부가 기능 로직이다.
이것은 JDK 동적 프록시가 제공하는 InvocationHandler
와
CGLIB가 제공하는 MethodInterceptor
의 개념과 유사한다.
둘을 개념적으로 추상화 한 것이다. 프록시 팩토리
를 사용하면 둘 대신에 Advice
를 사용하면 된다.
@Transactional
아노테이션이 대표적인 예자세한 건 이 블로그 참조
https://velog.io/@mooh2jj/스프링-트랜잭션-AOP-이해
@Transactional 을 사용하면 스프링의 트랜잭션 AOP가 적용된다.
트랜잭션 AOP는 기본적으로 프록시(Proxy) 방식
을 사용한다.
간단히 정리하자면,
@Transactional 을 적용하면 프록시 객체가 요청을 먼저 받아서(위임) 트랜잭션을 대신 처리하고, 실제 객체를 호출해주는 것이다.
이 밖에 @Cachable
, @Async
도 있다.