프록시 패턴은 실제 객체와 같은 인터페이스를 구현한 프록시를 실제 객체 앞단에 두고 접근 제어를 실행하는 패턴이다.
데이터가 한번 조회하면 변하지 않는 데이터라면 어딘가에 보관해두고 이미 조회한 데이터를 사용하는 것이 성능상 좋다. 이런 것을 캐시라고 한다.
@Slf4j
public class CacheProxy implements Subject {
// 실제 객체
private Subject target;
// 캐시할 데이터
private String cacheValue;
public CacheProxy(Subject target) {
this.target = target;
}
@Override
public String operation() {
log.info("프록시 호출");
// 캐시된 데이터가 없다면 실제 객체에서 캐시 값을 받아옴
if (cacheValue == null) {
cacheValue = target.operation();
}
return cacheValue;
}
}
데코레이터 패턴
데코레이터 패턴은 부가 기능 추가에 초점을 맞춘 디자인 패턴이다.
실제 객체와 같은 인터페이스를 구현한 데코레이터들을 등록하고, 내부에서 부가 기능들을 누적해서 추가하는 방식이다.
@Slf4j
public class TimeDecorator implements Component {
// 실제 객체 or 중복으로 꾸며줄 다른 데코레이터
private Component component;
public TimeDecorator(Component component) {
this.component = component;
}
@Override
public String operation() {
log.info("TimeDecorator 실행");
long startTime = System.currentTimeMillis();
String result = component.operation();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeDecorator 종료 resultTime={}ms", resultTime);
return result;
} }
프록시 패턴과 데코레이터 패턴의 의미
둘은 프록시라는 개념을 동일하게 사용하지만 사용 목적이 다르다.
프록시 패턴: 접근 제어가 목적
데코레이터 패턴: 새로운 기능 추가가 목적
실제 스프링에 프록시를 적용 시
@Configuration
public class InterfaceProxyConfig {
@Bean
public OrderControllerV1 orderController(LogTrace logTrace) {
OrderControllerV1Impl controllerImpl = new OrderControllerV1Impl(orderService(logTrace));
// 실제 객체 대신 프록시 객체를 스프링 빈으로 등록
return new OrderControllerInterfaceProxy(controllerImpl, logTrace);
}
}
기존에는 실제 클래스가 스프링 컨테이너에 올라갔지만 프록시를 생성하고 프록시를 스프링 빈에 등록하면, 프록시는 스프링 컨테이너에 등록되고 자바 힙에도 등록된다.
하지만 일반 실제 객체는 자바 힙 메모리에만 적재된다. 참조를 통해서 실제 객체에 접근 가능
다음 강의들에서 배울 프록시들도 모두 실제 객체 대신 프록시 객체가 스프링 컨테이너에 등록된다.
JDK 동적 프록시와 CGLIB
이전 강의의 프록시 방식의 문제는 대상 클래스 수 만큼 로그 추적을 위한 프록시 클래스를 만들어야 한다는 점이다.
자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있다.
즉, 프록시 클래스를 지금처럼 계속 만들지 않아도 된다는 것이다. 프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용해서 프록시 객체를 찍어내면 된다.
리플렉션
JDK 동적 프록시는 리플렉션을 사용한다. 리플렉션은 클래스나 메서드의 메타정보를 사용해서 동적으로 호출하는 메서드를 변경할 수 있다.
public class Singleton {
private Singleton() {}
public static final Singleton INSTANCE = new Singleton();
public Singleton getInstance()
{
return INSTANCE;
}
}
-> private 생성자를 호출하는 코드
public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException {
// 클래스 메타 데이터 가져오기
Class<?> singleton = Class.forName("com.example.Singleton");
// 생성자 정보 가져오기
Constructor<?> declaredConstructor = singleton.getDeclaredConstructor();
// private 메서드에 접근 허용
declaredConstructor.setAccessible(true);
// 싱글톤 객체이지만 새로운 객체 생성 가능
Singleton newSingleton = (Singleton) declaredConstructor.newInstance();
}
리플렉션 주의 사항
리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.
예를 들어서 지금까지 살펴본 코드에서 getMethod("callA") 안에 들어가는 문자를 실수로 getMethod("callZ") 로 작성해도 컴파일 오류가 발생하지 않는다. 그러나 해당 코드를 직접 실행하는 시점에 발생하는 오류인 런타임 오류가 발생한다.
리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 한다.
JDK 동적 프록시
JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가
필수이다. JDK 동적 프록시는 리플렉션 기반으로 동작하고 java.lang.Reflection 패키지의 InvocationHandler를 제공해준다.
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
// 실제 객체
private final Object target;
public TimeInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
// 실제 객체의 메서드 호출
Object result = method.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime; log.info("TimeProxy 종료 resultTime={}", resultTime); return result;
}
}
테스트 사례
@Test
void dynamicA() {
// 인터페이스만 사용 가능
AInterface target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
// 리플렉션 기반의 프록시 생성
AInterface proxy = (AInterface)Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[],{AInterface.class}, handler);
// 프록시 호출
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
JDK 동적 프록시 기술 덕분에 적용 대상 만큼 프록시 객체를 만들지 않아도 된다. 그리고 같은 부가 기능 로직을 한번만 개발해서 공통으로 적용할 수 있다. 만약 적용 대상이 100개여도 동적 프록시를 통해서 생성하고, 각각 필요한 InvocationHandler 만 만들어서 넣어주면 된다.
결과적으로 프록시 클래스를 수 없이 만들어야 하는 문제도 해결하고, 부가 기능 로직도 하나의 클래스에 모아서 단일 책임 원칙(SRP)도 지킬 수 있게 되었다.
<img width="843" alt="스크린샷 2023-01-04 오후 9 40 43" src="https://user-images.githubusercontent.com/82302520/210562342-580caff1-990b-4778-bc9d-e076eea914d8.png">
### CGLIB
---
CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.
CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다. CGLIB는 원래는 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스 코드에 포함했다.
따라서 스프링을 사용한다면 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있다.
JDK 동적 프록시에서 실행 로직을 위해 InvocationHandler 를 제공했듯이, CGLIB는 MethodInterceptor 를 제공한다.
@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {
// 실제 객체
private final Object target;
public TimeMethodInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object obj, Method method, Object[] args,MethodProxy proxy) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
// 실제 객체의 메서드를 호출
Object result = proxy.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime; log.info("TimeProxy 종료 resultTime={}", resultTime); return result;
}
}
- 테스트 사례
@Slf4j
public class CglibTest {
@Test
void cglib() {
ConcreteService target = new ConcreteService();
// 프록시를 만들어냄
Enhancer enhancer = new Enhancer();
// 프록시가 상속할 부모 클래스를 설정함
enhancer.setSuperclass(ConcreteService.class);
// 실행할 로직을 등록
enhancer.setCallback(new TimeMethodInterceptor(target));
// 실제 프록시 생성
ConcreteService proxy = (ConcreteService)enhancer.create();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
// 프록시 호출
proxy.call();
}
}
- 의존관계 구성도
<img width="822" alt="스크린샷 2023-01-04 오후 9 54 23" src="https://user-images.githubusercontent.com/82302520/210562318-cf5db192-92b2-49d0-bfa6-581be021ffa7.png">
### CGLIB 제약
----
- 클래스 기반 프록시는 상속을 사용하기 때문에 제약 존재
- 부모 클래스의 생성자를 체크해야 한다. CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
- 클래스에 final 키워드가 붙으면 상속이 불가능하다. CGLIB에서는 예외가 발생한다. 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다. CGLIB에서는 프록시 로직이 동작하지 않는다.
프록시
클라이언트/서버 개념을 사용할때 클라이언트가 서버를 바로 호출하는걸 직접 호출이라고 한다. 반면 클라이언트가 대리자를 통해서 간접적으로 호출할때, 그 대리자를 프록시라고 한다.
클라이언트는 요청을 보내는 대상이 실제 서버인지 프록시인지 상관하지 않고 호출할 수 있어야 한다. 즉, 서버와 프록시는 다형성으로 구현이 되어 있어야 한다.
프록시의 주요 기능
프록시를 통해서 할 수 있는 일은 크게 2가지로 구분할 수 있다.
접근 제어
부가 기능 추가
넓은 개념에서의 프록시의 예시
AWS 네트워크 상에서 찾을 수 있는 프록시의 구현 (WAF VPC 프록시)
보안 설정을 하지 않으면 DDOS 공격, 여러 웹 취약점 공격(SQL Injection, XSS) 그리고 특정 IP로부터 악성 봇이 유입될 수도 있다.
이때 실제 WAS와 ELB 앞단에 WAF를 배치하면 보안성을 강화할 수 있고, 특정 IP에 대한 접근 차단도 가능함
위의 예시는 VPC내에 WAF를 직접 설치하는 경우, AWS WAF의 경우 ELB나 CloudFront에 직접 붙여서 사용 가능한 서비스
혹시 aws 보안에 대해 더 궁금하다면
프록시 패턴
프록시 패턴은 실제 객체와 같은 인터페이스를 구현한 프록시를 실제 객체 앞단에 두고 접근 제어를 실행하는 패턴이다.
데이터가 한번 조회하면 변하지 않는 데이터라면 어딘가에 보관해두고 이미 조회한 데이터를 사용하는 것이 성능상 좋다. 이런 것을 캐시라고 한다.
데코레이터 패턴
데코레이터 패턴은 부가 기능 추가에 초점을 맞춘 디자인 패턴이다.
실제 객체와 같은 인터페이스를 구현한 데코레이터들을 등록하고, 내부에서 부가 기능들을 누적해서 추가하는 방식이다.
프록시 패턴과 데코레이터 패턴의 의미
둘은 프록시라는 개념을 동일하게 사용하지만 사용 목적이 다르다.
실제 스프링에 프록시를 적용 시
기존에는 실제 클래스가 스프링 컨테이너에 올라갔지만 프록시를 생성하고 프록시를 스프링 빈에 등록하면, 프록시는 스프링 컨테이너에 등록되고 자바 힙에도 등록된다. 하지만 일반 실제 객체는 자바 힙 메모리에만 적재된다. 참조를 통해서 실제 객체에 접근 가능
다음 강의들에서 배울 프록시들도 모두 실제 객체 대신 프록시 객체가 스프링 컨테이너에 등록된다.
JDK 동적 프록시와 CGLIB
이전 강의의 프록시 방식의 문제는 대상 클래스 수 만큼 로그 추적을 위한 프록시 클래스를 만들어야 한다는 점이다.
자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있다.
즉, 프록시 클래스를 지금처럼 계속 만들지 않아도 된다는 것이다. 프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용해서 프록시 객체를 찍어내면 된다.
리플렉션
JDK 동적 프록시는 리플렉션을 사용한다. 리플렉션은 클래스나 메서드의 메타정보를 사용해서 동적으로 호출하는 메서드를 변경할 수 있다.
참고 사항 (이펙티브 자바 item 3)
-> private 생성자를 호출하는 코드
리플렉션 주의 사항
리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.
예를 들어서 지금까지 살펴본 코드에서 getMethod("callA") 안에 들어가는 문자를 실수로 getMethod("callZ") 로 작성해도 컴파일 오류가 발생하지 않는다. 그러나 해당 코드를 직접 실행하는 시점에 발생하는 오류인 런타임 오류가 발생한다.
리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 한다.
JDK 동적 프록시
JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가 필수이다. JDK 동적 프록시는 리플렉션 기반으로 동작하고 java.lang.Reflection 패키지의 InvocationHandler를 제공해준다.
테스트 사례
}
@Slf4j public class TimeMethodInterceptor implements MethodInterceptor {
} }
@Slf4j public class CglibTest {
} }