본문 바로가기
OOP/<헤드 퍼스트 디자인 패턴>, 에릭 프리먼 외

Proxy

by 민휘 2023. 2. 24.

핵심 의도

특정 객체로의 접근을 제어하는 대리인을 제공한다. 클라이언트가 실제 객체의 메소드를 호출하면 그 호출을 중간에 가로챈다.

 

적용 상황

사용되는 상황이 많은데, 공통점은 클라이언트가 진짜 객체에 직접 메시지 요청을 보내지 않도록 한다는 점이다. 진짜 객체에 바로 메시지가 가기 전에 접근을 제어해야 할 때 사용한다.

  1. 원격 프록시 - 원격 객체로의 접근을 제어한다. 서로 다른 JVM에 존재하는 객체는 직접 데이터를 주고받을 수 없으므로 프록시 객체를 통해 접근을 제어한다. 원격 프록시는 접근을 제어해 네트워크 관련 사항을 처리한다.
  2. 가상 프록시 - 생성하는데 많은 비용이 드는 객체를 대신한다. 진짜 객체가 필요한 상황이 오기 전까지 객체의 생성을 미루거나 객체를 대신한다. 객체 생성이 끝나면 진짜 객체에 직접 요청을 전달한다.
  3. 보호 프록시 - 접근 권한이 필요한 자원으로의 접근을 제어한다. 접근 권한마다 프록시를 두어 클라이언트가 진짜 객체에 접근하기 전에 접근 권한을 검사한다. 메시지는 요청을 보낸 클라이언트, 메소드, 인자 등으로 구분할 수 있다.
  4. 방화벽 프록시 - 일련의 네트워크 자원으로의 접근을 제어하여 진짜 객체를 나쁜 클라이언트로부터 보호한다.
  5. 스마트 레퍼런스 프록시 - 진짜 객체가 참조될 때마다 추가 행동을 제공한다. 객체의 레퍼런스 개수를 센다든가..
  6. 캐싱 프록시 - 비용이 많이 드는 작업의 결과를 임시로 저장해 준다. 여러 클라이언트에서 결과를 공유하게 해줌으로써 계산 시간과 네트워크 지연을 줄여준다.
  7. 동기화 프록시 - 여러 스레드에서 주제에 접근할 때 안전하게 작업을 처리할 수 있다.
  8. 복잡도 숨김 프록시 - 복잡한 클래스의 집합으로의 접근을 제어하고 그 복잡도를 숨겨준다. 퍼사드 프록시라고 부르기도 한다. 이 프록시와 퍼사드 패턴의 차이점은 프록시는 접근을 제어하지만 퍼사드 패턴은 대체 인터페이스만 제공한다.
  9. 지연 복사 프록시 - 클라이언트에서 필요로 할 때까지 객체가 복사되는 것을 지연시킴으로써 객체의 복사를 제어한다. 변형된 가상 프록시로 볼 수 있다.

정말 많은 프록시 패턴의 종류가 있는데, 결국 공통점은 클라이언트가 실제 객체의 메소드를 호출할 때 그 호출을 중간에 가로채어 부가 작업을 하는 것에 있다.

 

솔루션의 구조와 각 요소의 역할

객체에게 책임을 분할하기

진짜 객체인 RealSubject와 클라이언트가 존재한다. 클라이언트는 진짜 객체에 접근하려고 한다. 접근 제어를 하는 책임을 담당하는 객체는 Proxy이다. 클라이언트는 Proxy를 통해 RealSubject에 메시지를 보내므로 동일한 인터페이스를 사용해야 한다.

 

구현 포인트

RMI

분산 객체 간의 통신을 구현하는 모든 프로토콜을 의미한다. 분산되어 존재하는 객체 간의 메시지 전송을 가능하게 하는 프로토콜이다. Java가 제공하는 RMI를 사용하면 통상 네트워크 프로그래밍할 때 사용하는 소켓 통신을 하부에 숨기고 상위 레벨에서 수행할 수 있다. 분산 객체 간의 데이터 전송을 애플리케이션 함수 호출 형태로 구현할 수 있기 때문에 훨씬 쉽다. RMI를 사용하면 서로 다른 JVM의 힙에 존재하는 객체 간에 메시지를 전송할 수 있다. 자바가 제공하는 라이브러리기 때문에 통신에 대한 신뢰성을 확보할 수 있다. 자바의 RMI에 원격 프록시 패턴이 적용되어 있다.

 

자바 RMI에 적용된 프록시 패턴 찾기

클라이언트 객체는 원격에 있는 서비스 객체에 메소드 호출을 통해 메시지를 보낸다. 네트워크 통신을 처리해야하므로 클라이언트와 서버 측에서 네트워크 통신을 처리하는 객체가 필요하다. 클라이언트 측은 스텁, 서버 측은 스켈레톤이라고 부른다.

클라이언트는 서비스 객체에게 요청을 보내고 응답을 받는다고 생각하지만, 실제로 클라이언트는 보조 객체인 스텁에게 요청을 보낸다. 그러면 스텁이 그 요청을 원격 객체에게 전달한다. 스텁은 서버에 연락을 취하고, 메소드 호출에 관한 정보를 전달하고, 서버로부터 리턴되는 정보를 기다리고 클라이언트 객체에게 전달한다.

 

서비스 보조 객체인 스켈레톤은 클라이언트 보조 객체로부터 요청을 받아 오고, 호출 정보를 해석해 진짜 서비스 객체에 있는 진짜 메소드를 호출한다. 그러면 서비스 객체는 그 메소드 호출이 원격 클라이언트가 아닌 로컬 객체로부터 들어온다고 생각한다. 스켈레톤은 서비스로부터 리턴값을 받아서 Socket의 출력 스트림으로 클라이언트 보조 객체에게 전송한다.

 

스텁과 스켈레톤은 클라이언트 객체와 서비스 객체 앞에서 접근을 제어하는 프록시 객체이다. 스텁은 클라이언트 객체로 들어오는 응답을 직렬화하여 전달하고, 클라이언트 객체에서 나가는 요청은 통신 처리하여 보낸다. 스켈레톤은 서비스 객체로 들어오는 외부 요청을 받아들여 로컬에서 보낸 요청처럼 처리한다.

 

RMI 구현

  1. 원격 인터페이스를 만든다.
  2. 실제 서비스 객체에 인터페이스를 구현하고 서비스를 RMI 레지스트리에 등록한다. RMI 시스템이 레지스트리에 스텁만 등록해준다.
  3. RMI 레지스트리를 실행한다.
  4. 원격 서비스를 실행한다.

 

RMI 작동 방식

  1. 클라이언트에서 RMI 레지스트리를 룩업한다.
  2. RMI 레지스트리에서 스텁 객체를 리턴한다.
  3. 클라이언트는 스텁의 메소드를 호출한다. 스텁이 진짜 서비스 객체라고 생각한다.

 

동적 프록시

자바의 java.lang.reflect 패키지 안에 프록시 기능이 내장되어 있다. 이 패키지를 사용하면 즉석에서 하나 이상의 인터페이스를 구현하고, 지정한 클래스에 메소드 호출을 전달하는 클래스를 만들 수 있다. 진짜 프록시 클래스는 실행 중에 생성된다.

자바에서 Proxy 클래스를 생성해주므로, Proxy 클래스에게 무슨 일을 해야 하는지 알려주는 책임을 InvocationHandler가 가진다. InvocationHandler는 프록시에 호출되는 모든 메소드에 응답한다.

 

다음과 같이 InvocationHandler에 접근 제어를 구현한다.

import java.lang.reflect.*;
 
public class OwnerInvocationHandler implements InvocationHandler { 
	Person person;
 
	public OwnerInvocationHandler(Person person) {
		this.person = person;
	}
 
	public Object invoke(Object proxy, Method method, Object[] args) 
			throws IllegalAccessException {
  
		try {
			if (method.getName().startsWith("get")) {
				return method.invoke(person, args);
   			} else if (method.getName().equals("setGeekRating")) {
				throw new IllegalAccessException();
			} else if (method.getName().startsWith("set")) {
				return method.invoke(person, args);
			} 
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } 
		return null;
	}
}

그리고 동적 프록시를 생성하는 코드를 작성한다.

Person getOwnerProxy(Person person) {
	return (Person) Proxy.newProxyInstance( 
		person.getClass().getClassLoader(),
		person.getClass().getInterfaces(),
		new OwnerInvocationHandler(person));
}

이렇게 사용한다.

public static void main(String[] args) {
		MatchMakingTestDrive test = new MatchMakingTestDrive();
		test.drive();
	}
 
	public MatchMakingTestDrive() {
		initializeDatabase();
	}

	public void drive() {
		Person joe = getPersonFromDatabase("Joe Javabean"); 
		Person ownerProxy = getOwnerProxy(joe);
		System.out.println("Name is " + ownerProxy.getName());
		ownerProxy.setInterests("bowling, Go");
		System.out.println("Interests set from owner proxy");
		try {
			ownerProxy.setGeekRating(10);
		} catch (Exception e) {
			System.out.println("Can't set rating from owner proxy");
		}
		System.out.println("Rating is " + ownerProxy.getGeekRating());

		Person nonOwnerProxy = getNonOwnerProxy(joe);
		System.out.println("Name is " + nonOwnerProxy.getName());
		try {
			nonOwnerProxy.setInterests("bowling, Go");
		} catch (Exception e) {
			System.out.println("Can't set interests from non owner proxy");
		}
		nonOwnerProxy.setGeekRating(3);
		System.out.println("Rating set from non owner proxy");
		System.out.println("Rating is " + nonOwnerProxy.getGeekRating());
	}

 

적용 예시

 

요구사항

어떤 문자열을 출력하는데 준비 비용이 크다고 가정하자. 문자열을 하나씩 출력하면 시간이 오래 걸리므로 버퍼를 두어 문자열을 빠르게 출력하려고 한다. 버퍼를 두는 부분은 프록시 객체에 할당한다.

설계

ScreenDisplay에는 준비 시간이 긴 출력 메소드가 포함되어있다. BufferDisplay는 클라이언트가 ScreenDisplay에 보내는 요청을 받아서 버퍼에 저장하고 ScreenDisplay에 요청을 보내는 책임을 가진다. 클라이언트는 BufferDisplay를 통해 ScreenDisplay에 메시지를 보내므로 동일한 인터페이스를 구현해야 한다.

 

코드

Display

public interface Display {
    void print(String content);
}

ScreenDisplay

public class ScreenDisplay implements Display {

    @Override
    public void print(String content) {
        try {
            Thread.sleep(500); // 출력을 위한 준비 작업이 느리다고 가정
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(content);
    }
}

BufferDisplay

public class BufferDisplay implements Display {

    private ArrayList<String> buffer = new ArrayList<>();
    private ScreenDisplay screen;
    private int bufferSize;

    public BufferDisplay(int bufferSize) {
        this.bufferSize = bufferSize;
    }

    @Override
    public void print(String content) {
        buffer.add(content);
        if(buffer.size() == bufferSize) {
            flush();
        }
    }

    public void flush() {
        if(screen == null) screen = new ScreenDisplay();
        String lines = String.join("\\n", buffer);
        screen.print(lines);
        buffer.clear();
    }
}

MainEntry

public class MainEntry {
    public static void main(String[] args) {
//        Display display = new ScreenDisplay();
        Display display = new BufferDisplay(5);

        display.print("안녕하세요.");
        display.print("소프트웨어 설계를 위한 디자인패턴");
        display.print("프록시~~");
        display.print("안녕하세요.");
        display.print("소프트웨어 설계를 위한 디자인패턴");
        display.print("프록시~~");

        ((BufferDisplay) display).flush();
    }
}

'OOP > <헤드 퍼스트 디자인 패턴>, 에릭 프리먼 외' 카테고리의 다른 글

헤드 퍼스트 디자인 패턴 서평  (0) 2023.02.28
MVC (아키텍처 패턴)  (1) 2023.02.24
State  (0) 2023.02.24
Composite  (0) 2023.02.24
Iterator  (0) 2023.02.24