지속적으로 재수정하고 있는 글입니다 ! 🙋🏻♀️
Proxy 는 영단어에서 '대리(행위)' 의 뜻을 지닌다.
프록시라는 개념은 객체안에서 사용할 수 도 있고, 웹 서버에서도 사용할 수 있다.
객체 형태인로 구현되어있는가, 웹 서버로 구현되어 있는가 처럼 규모의 차이가 있을 뿐 근본적인 역할은 같다.
이제 우리는 크게 두 갈래에서 proxy 객체와 proxy 서버에 대해서 살펴보겠다.
Proxy 객체는 원본 객체를 대신해서 호출되는 객체이다.
원본 객체와 클라이언트 요청 중간 단계에 위치하여 원본 객체를 감싸서 클라이언트에게 반환한다. (ex. 마치 선물을 보자기로 감쌀때 보자기라는 객체처럼)
[목적] 그렇다면 프록시 객체를 언제, 왜 사용하는가?
프록시 객체는 '감싼다' 는 역할에 맞게 사용 목적을 크게 2가지로 볼 수 있다.
- '원본 객체에 대한 접근을 제어하기 위해 ' 프록시 객체로 감싼다
ex) 권한에 따른 접근 검사(보안), 캐싱(성능 개선), 지연 로딩- '부가적인 기능을 제공하기 위해 ' 프록시 객체로 감싼다
ex) 트랜잭션, 요청에 대한 추가 로그
[장점] 그렇다면 이를 통해 얻는 장점은 무엇인가 ?
- OCP : 기존 코드(객체)를 변경하지 않고도 새로운 기능을 추가할 수 있다
- SRP : 기존 코드(객체)가 해야하는 일만 유지할 수 있다
[단점] 단, 코드의 복잡도가 증가하고 코드의 중복이 발생하게 된다.
public interface Subject {
void order();
}
public class SubjectImpl implements Subject {
private subjectDao;
@Override
public void order() {
// 실제 비즈니스 로직
subjectDao.decreaseItemCount();
}
}
public class SubjectProxy implements Subject {
private Subject target;
public CacheProxy(Subject target) {
this.target = target;
}
@Override
public void order() {
// log.info("Proxy 호출");
// 부가 기능 로직 (ex. 캐싱, 트랜잭션..)
target.order(); //실제 객체 호출
}
}
하나의 인터페이스를 두고 실제 객체와 프록시 객체는 오버라이딩(@Override
)을 통해 서로 다른 구현체를 구현하고 있다.
이때, 클라이언트가 프록시를 호출하면 프록시가 최종적으로 실제 객체를 호출해야하므로 프록시는 내부에 실제 객체의 참조(호출 대상 : target
)를 가지고 실제 객체를 호출한다.
결국, 프록시 객체가 원본 객체와 같은 인터페이스를 구현하고 있기 때문에
클라이언트는 프록시 객체를 호출하는 것으로 인해 원본 객체의 메소드를 호출하는 것과 같은 효과를 얻을 수 있다.
프록시 패턴
위와 같은 프록시 구현 형태는 프록시 패턴 / 데코레이터 패턴의 디자인 패턴 형태이다. 자세한 사항은 이전에 정리하였던 디자인 패턴 포스팅을 참고하자.
위에서 Proxy 객체의 단점은 코드의 복잡도가 증가한다고 하였다.
이를 위해 Java에서는 개발자가 직접 프록시 클래스를 만들지 않아도,
JDK 동적 프록시와 CGLIB과 같은 라이브러리를 사용하여 프록시 객체를 동적으로(런타임에) 생성할 수 있다.
이때, 프록시 생성에 있어 인터페이스 유무에 따라 적용되는 기술이 다르다.
JDK 다이나믹 프록시는 JDK 에서 지원하는 프록시 생성 방법으로,
Interface를 기반으로 Proxy를 생성해주는 방식이다. 때문에 인터페이스의 존재가 필수적이다.
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
Main.class.getClassLoader(), // 클래스로더
new Class[]{MyInterface.class}, // 타깃의 인터페이스
new MyInvocationHandler(realObject) // 타깃의 정보가 포함된 Handler
);
만약 JDK Dynamic Proxy의 원리를 모른다면, JDK Dynamic Proxy는 인터페이스 기반으로 DI가 이뤄지는데 만약 우리가 구현체를 선언해줄 경우 Exception이 발생할 수 있다.
@Controller
public class PayController{
@Autowired
private KakaoPayService kakaoPayService; // ← JDK 동적 프록시 X, Exception 발생
@Autowired
private PayService payService; //JDK 동적 프록시 OK, CGLIB OK
// ...
}
@Service
public class KakaoPayService implements PayService{
@Override
public void pay(Long id){
// 비즈니스 로직
}
}
InvocationHandler
라는 인터페이스를 구현(implement)해서 프록시를 생성하며,Reflection API
: 자바에서는 JVM이 실행되면 작성된 자바 코드가 static 영역에 저장된다.
Reflection API는 이 정보를 활용하여 구체적인 클래스 타입을 알지 못해도 클래스 이름을 통해 static 영역에서 그 클래스의 정보(메서드, 타입, 변수 등등)에 접근할 수 있게 해준다. 이를 통해 우리는 유연하게 런타임 시에 클래스의 정보를 확인하고, 메서드를 호출하거나 필드에 접근할 수 있지만, 생성자의 인자 정보는 가져올 수 없고 컴파일 타임에 정적인 정보를 알 수 없기 때문에 사용 시 주의가 필요하다.
CGLIB(Code Generator Library)는 상속을 통한 프록시 구현 방법으로,
때문에 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 생성할 수 있다.
(+) 외부 라이브러리여서 의존성 추가가 필요했지만, Spring 3.2 버전부터 ProxyFactory에서 CGLIB를 사용할 수 있도록 도와주기 때문에 직접 사용하거나 라이브러리를 추가할 필요 없다.
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class); // 타깃 클래스
enhancer.setCallback(new MyMethodInterceptor()); // Handler
MyService proxy = (MyService) enhancer.create(); // Proxy 생성
MethodInterceptor
라는 클래스를 상속(extends)해서 프록시를 생성하며,자바가 동적 프록시를 지원한다는 것 알았다. 그렇다면, 스프링은 어떻게 프록시를 지원할까 ?
스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리 기능을 제공한다.
프록시 팩토리(Proxy Factory)란, 프록시 객체를 생성하는 역할을 담당하는 클래스이다.
인터페이스 유무에따라 ProxyFactory는 자동으로 JDK 동적 프록시 혹은 CGLIB를 사용하여 proxy 객체를 동적으로 생성한다.
또한 스프링에서는 프록시의 부가적인 로직도 기존의 핸들러, 인터셉터 대신 Advice 하나만을 정의해주기만 하면 된다. (그러면 스프링이 내부적으로 Advice를 호출하는 AdviceInvocationHandler , AdviceMethodInterceptor를 사용한다.)
Spring Boot에서는 AOP를 사용할때 명시적으로 설정하지 않는 경우에는 CGLIB이 기본(default) 프록시 생성 방식으로 사용한다.
이유는 성능적인 측면뿐만 아니라 인터페이스 기반 프록시는 때때로 ClassCast Exceptions를 추적하기 어렵기 때문이다.
만약 Jdk DynamicProxy를 적용하고 싶다면 yml 설정에서 proxy-target-class
: true가 아닌 false로 변경해주면 된다.
AOP proxy 에 대한 더 자세한 내용은 다음 포스팅을 참조.
JPA Proxy 에 대한 더 자세한 내용은 다음 포스팅을 참조.
우리가 많이 쓰는 상황에서 들어본 Proxy의 경우들이다.
AOP에서의 Proxy와 Cache에서의 Proxy, JPA에서의 Proxy는 각기 다른 상황과 요구 사항을 해결하기 위해 사용되지만,
모두 앞서 학습했던 프록시 패턴을 활용하여 대상 객체에 부가적인 동작을 추가하거나 접근을 제어하고 있다는 것을 알 수 있다.
먼저, 프록시 객체는 실 객체를 감싸고, 실 객체를 대신 호출해주는 역할이기 때문에 객체 호출과 관련하여 유의해야한다.
위에서 보았듯 부가적인 기능을 적용하려면 항상 프록시를 통해서 대상 객체를 호출해야한다. (순서가 프록시 호출 → (부가기능) → 실 객체 호출 → 비즈니스 로직 → (부가기능) → 종료/반환 이렇게 되는 것이다.) 따라서 스프링은 의존관계 주입시에 항상 실제 객체 대신 프록시 객체를 대신 주입한다.
하지만 만약 우리가 직접 코드를 작성하는 과정에서 대상 객체를 직접 호출하였다면 프록시를 거치지 않았기 때문에 부가 기능은 적용되지 않는다. 예를 들어, 우리가 대상 객체 내에서 내부 메서드를 호출하는 경우 프록시를 거치지 않고 메서드를 호출하였기 때문에 프록시의 부가기능은 적용되지 않는다.
(ex. 대표적으로 @Transaction이 적용된 프록시에서 내부 호출을 하는 경우 트랜잭션이 적용되지 않는 문제가 있을 수 있다.)
또한 프록시는 구현
혹은 상속
을 통해 프록시를 생성한다고 설명했다.
때문에, 자바의 기본적인 상속과 관련된 제약사항을 유의해야한다.
1) private 메서드 제약
2) final 클래스 및 메소드 제약
위와 같은 문제들은 Proxy 객체가 생성되지 않거나 정상 동작하지 않는다.
때문에 런타임 Exception이 발생하거나, @Transactional
@Cacheable
같은 AOP 적용이 실패할 수 있다.
Proxy 서버란, 두 PC가 통신을 할 때 직접 하지 않고 중간에서 중계 역할을 하는 서버이다.
위에서 살펴봤던 것과 마찬가지로 프록시 서버는 'Proxy' 라는 개념에 맞게 중계 서버 역할로써 보안 목적이나 캐싱 등의 기능을 제공한다.
프록시 서버는 서버가 어디에 위치하느냐에 따라 포워드 프록시와 백워드 프록시로 나뉜다.
클라이언트에서 서버로 리소스를 요청할 때 직접 요청하지 않고 프록시 서버를 거쳐서 요청하는데, 이때 포워드 프록시는 서버에게 클라이언트가 누구인지 감춰주는 역할을 한다.
이러한 특징 때문에 기업 사내망에서 주로 사용된다.
(일반적으로 프록시 서버라고 말하면 포워드 프록시를 말한다.)
리버스 프록시는 애플리케이션 서버의 앞에 위치하여 클라이언트로부터의 요청을 받아서 적절한 웹 서버로 요청을 전송하는 역할을 한다.
웹 서버는 요청을 받아서 평소처럼 처리를 하지만, 응답을 클라이언트로 보내지 않고 Reverse Proxy로 반환한다. Reverse Proxy는 서버로부터 응답을 전달받아 그 응답을 클라이언트로 전송하는 역할을 한다.
이 경우, 클라이언트는 애플리케이션 서버를 직접 호출하는 것이 아니라 프록시 서버를 통해 호출하기 때문에 리버스 프록시는 애플리케이션 서버를 감추는 역할을 하게 된다.
요약하자면,
(ex. Nginx, Apache Web Server)
[참고]
https://youtu.be/MFckVKrJLRQ?feature=shared / 테코톡
https://gymdev.tistory.com/68
https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html
https://tecoble.techcourse.co.kr/post/2022-10-17-jpa-hibernate-proxy/ / JPA proxy 테코블