4. 프록시 패턴 - midoBanDev/design-pattern-java GitHub Wiki
프록시란(Proxy)
- 우선 서버와 클라이언트의 개념을 먼저 알아보자.
클라이언트는 어떤 서비스를 받는 고객 또는 의뢰인을 의미한다.
서버는 서비스나 상품을 제공하는 사람 또는 물건을 의미한다.
이 개념을 컴퓨터 네트워크에 적용하면 클라이언트는 필요한 무언가를 서버에 요청하고, 서버는 받은 요청을 처리하여 클라이언트에서 응답해 주는 역할을 한다.
쉬운 예로 클라이언트를 웹 브라우저이고 서버는 웹 서버가 된다.
이 개념을 객체에 도입하면 요청하는 객체는 클라이언트가 되고, 요청을 처리하는 객체는 서버가 된다.
1. 직접 호출
- 보통 클라이언트가 요청을 하고, 서버가 요청을 처리한 후 응답을 해주는 직접 호출이 일반적이다.
- 그런데 상황에 따라 간접 호출을 해야 하는 경우가 있다.
- 간접 호출이란 클라이언트의 요청을 중간에서 대리자가 받고, 대리자가 받은 요청을 서버에 전달해준다.
- 서버가 요청을 처리한 후 대리자에게 전달하면 대리자가 클라이언트에서 결과를 응답한다.
2. 간접 호출
이때 대리자를 프록시(Proxy)라고 한다. 이런 간접 호출(이하 프록시)을 통해 얻을 수 있는 이점이 있다.
- 접근 제어
- 부가 기능 추가
- GOF 디자인 패턴에는 프록시를 사용하는 프록시 패턴과 데코레이터 패턴이 있다. 실제 두 패턴의 모양은 거의 비슷하다.
- 그래서 해당 패턴을 구별하기 위해서는 패턴 사용의 의도(Intent)를 파악해야 한다.
- 접근 제어를 목적을 하는 패턴을 프록시 패턴이라 하고, 부가 기능을 목적으로 하는 패턴을 데코레이터 패턴이라고 한다.
프록시 패턴
1. 인터페이스 기반 프록시 패턴
▶ 프록시 패턴을 적용 전 ( 인터페이스 기반 )
- 클라이언트 객체가 요청을 하면 해당 요청을 받은 서버 객체의 구현 클래스가 응답을 해준다.
코드
- 인터페이스
public interface Subject {
String operation();
}
- 구현 클래스 클랙스
@Slf4j
public class RealSubject implements Subject{
@Override
public String operation() {
log.info("실제 객체 호출");
sleep(1000);
return "data";
}
public void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 클라이언트 클래스
public class ProxyPatternClient {
private Subject subject;
public ProxyPatternClient(Subject subject) {
this.subject = subject;
}
public void execute() {
subject.operation();
}
}
- 일반 요청 실행 코드
일반적인 클라이언트가 인터페이스에 의존한 후 구현 클래스의 처리 결과값을 받는 방법이다.
public class ProxyPatternTest {
@Test
void noProxyTest() {
Subject subject = new RealSubject();
ProxyPatternClient client = new ProxyPatternClient(subject);
client.execute();
client.execute();
client.execute();
}
}
▶ 프록시 패턴을 적용 후( 인터페이스 기반 )
- 프록시 패턴의 중요한 포인트는 프록시 클래스도 Subject 인터페이스에 의존한다.
- 그리고 프록시 내부에서 실제 요청을 처리하는 RealSubject 클래스를 호출한다.
- 클라이언트는 요청을 Subject 인터페이스를 통해서 하면 된다.
- 클라이언트는 요청을 프록시 클래스가 받는지 RealSubject 클래스가 받는지 전혀 몰라도 상관없다.
- 하지만 프록시 클래스는 실제 요청을 처리하는 RealSubject 클래스(target class)를 알아야 한다. 따라서 RealSubject 클래스를 주입해 줘야한다.
코드
- 프록시 클래스
@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;
}
}
- 프록시 패턴 실행
클라이언트는 여전히 Subject 인터페이스에 의존하고 있다.
Subject를 구현한 cacheProxy 클래스를 만들고 Subject에 RealSubject 대신 cacheProxy를 주입해주면 된다.
public class ProxyPatternTest {
@Test
void cacheProxyTest() {
Subject realSubject = new RealSubject();
Subject cacheProxy = new CacheProxy(realSubject);
ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
client.execute();
client.execute();
client.execute();
}
}
2. 구체 클래스 기반 프록시 패턴
- 인터페이스가 아니어도 프록시 패턴이 가능하다.
- 자바의 다형성은 인터페이스를 구현하든, 클래스를 상속받든 상위 타입만 맞으면 다형성이 적용된다.
코드
- 구체 클래스
@Slf4j
public class ConcreteLogic {
public String operation() {
log.info("ConcreteLogic 실행");
return "data";
}
}
- 클라이언트 클래스
public class ConcreteClient {
private ConcreteLogic concreteLogic;
public ConcreteClient(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
public void execute() {
concreteLogic.operation();
}
}
- ConcreteLogic을 상속 받은 구체 클래스
@Slf4j
public class TimeProxy extends ConcreteLogic{
private ConcreteLogic concreteLogic;
public TimeProxy(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
/**
* super.operation() 와 concreteLogic.operation() 의 차이
* 즉, 직접 호출하는 경우와 의존성을 주입받는 경우의 장,단점
* 1. 직접 호출하는 경우
* 장점
* - 단순함 : super를 사용하는 경우 별도의 멤버 변수나 생성자가 필요하지 않는다.
* - 직관성 : 해당 프록시 클래스가 부모 클래스의 특정 메서드를 오버라이드하는 것이 명확하게 들어난다.
* 단점
* - 강한결합 : TimeProxy가 ConcreteLogic에 강하게 결합되어 TimeProxy의 활용 범위가 ConcreteLogic에 국한된다.
* - 확장의 제한 : 다른 로직의 프록시로 사용하려면 새로운 프록시 클래스를 만들어야 한다.
*
* 2. 주입받는 경우
* - 유연성 : 동일한 TimeProxy 클래스를 여러 ConcreteLogic의 하위 클래스에 대해 사용할 수 있다.
* 즉, 다양한 ConcreteLogic의 서브 클래스들을 주입 받아 동일한 프록시 로직을 적용할 수 있다.
* - 테스트 용이성 : 테스트 시 mock 객체나 다른 구현체를 주입하여 테스트하기 용이하다
*
* 결국 인터페이스 아니라 상속 관계에서도 주입을 통해 접근하는 방식은 프록시 클래스의 활용 범위와 재사용성을 높이기 때문에 더 우수한 방식인것 같다.
*/
@Override
public String operation() {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
String result = concreteLogic.operation();
long endTime = System.currentTimeMillis();
long resultTime = startTime - endTime;
log.info("TimeProxy 종료 resultTime={}ms", resultTime);
return result;
}
}
- 구체 클래스 기반 실행 코드
클라이언트는 여전히 ConcreteLogic에 의존하고 있다.
ConcreteLogic를 상속받은 TimeProxy 구체 클래스를 만들어 클라이언트에게 주입해 주기만 하면 된다.
public class ConcreteProxyTest {
// @Test
void noProxy() {
ConcreteLogic concreteLogic = new ConcreteLogic();
ConcreteClient client = new ConcreteClient(concreteLogic);
client.execute();
}
@Test
void addProxy() {
ConcreteLogic concreteLogic = new ConcreteLogic();
TimeProxy proxy = new TimeProxy(concreteLogic);
ConcreteClient client = new ConcreteClient(proxy);
client.execute();
}
}