4. 프록시 패턴 - midoBanDev/design-pattern-java GitHub Wiki

프록시란(Proxy)

  • 우선 서버와 클라이언트의 개념을 먼저 알아보자.

클라이언트는 어떤 서비스를 받는 고객 또는 의뢰인을 의미한다.
서버는 서비스나 상품을 제공하는 사람 또는 물건을 의미한다.
이 개념을 컴퓨터 네트워크에 적용하면 클라이언트는 필요한 무언가를 서버에 요청하고, 서버는 받은 요청을 처리하여 클라이언트에서 응답해 주는 역할을 한다.
쉬운 예로 클라이언트를 웹 브라우저이고 서버는 웹 서버가 된다.

이 개념을 객체에 도입하면 요청하는 객체는 클라이언트가 되고, 요청을 처리하는 객체는 서버가 된다.

1. 직접 호출

image

  • 보통 클라이언트가 요청을 하고, 서버가 요청을 처리한 후 응답을 해주는 직접 호출이 일반적이다.
  • 그런데 상황에 따라 간접 호출을 해야 하는 경우가 있다.
  • 간접 호출이란 클라이언트의 요청을 중간에서 대리자가 받고, 대리자가 받은 요청을 서버에 전달해준다.
  • 서버가 요청을 처리한 후 대리자에게 전달하면 대리자가 클라이언트에서 결과를 응답한다.

2. 간접 호출

image

이때 대리자를 프록시(Proxy)라고 한다. 이런 간접 호출(이하 프록시)을 통해 얻을 수 있는 이점이 있다.

  1. 접근 제어
  2. 부가 기능 추가
  • GOF 디자인 패턴에는 프록시를 사용하는 프록시 패턴데코레이터 패턴이 있다. 실제 두 패턴의 모양은 거의 비슷하다.
  • 그래서 해당 패턴을 구별하기 위해서는 패턴 사용의 의도(Intent)를 파악해야 한다.
  • 접근 제어를 목적을 하는 패턴을 프록시 패턴이라 하고, 부가 기능을 목적으로 하는 패턴을 데코레이터 패턴이라고 한다.

프록시 패턴

1. 인터페이스 기반 프록시 패턴

▶ 프록시 패턴을 적용 전 ( 인터페이스 기반 )

  • 클라이언트 객체가 요청을 하면 해당 요청을 받은 서버 객체의 구현 클래스가 응답을 해준다.

image

코드

  • 인터페이스
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 클래스를 주입해 줘야한다.

image

image

코드

  • 프록시 클래스
@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();
	}
}