오늘보다 발전된 내일의 나를 위해…












Design Pattern-Wrapper Pattern


해당 내용은 POCU 아카데미 COMP_2500에서 배운 내용을 공부하기 위해 작성된 글입니다


:pushpin: Proxy Pattern


웹 브라우저 설정 등을 뒤자다 보면 프록시 서버라는 걸 볼 수 있다. Proxy Server는 실제 웹사이트와 사용자 사이에 위치하는 중간 서버이다. 인터넷상의 캐시 메모리처럼 작동한다. 사용자가 프록시 서버를 통해 원하는 문서를 읽으려 할 때 프록시 서버에 이미 그 문서가 저장되어 있다면 그걸 반환한다. 없다면 실제 웹서버에서 문서를 읽어와 프록시 서버에 저장한다.


프록시 패턴이 이루려는 목적도 비슷하다. 클래스 안에서 어떤 상태를 유지하는 게 여의치 않은 경우가 있다. 데이터가 너무 커서 미리 읽어 두면 메모리가 부족하고 개체 생성 시 데이터를 로딩하면 시간이 꽤 걸린다. 그리고 개체는 만들었으나 그 속의 데이터를 사용하지 않을 수도 있다. 이럴 경우 다음과 같은 방법을 통해 불필요한 데이터 로딩을 방지한다.

  • 개체 생성 시에는 데이터 로딩에 필요한 정보만(예: 파일 위치) 기억해 둠
  • 클라이언트가 실제로 데이터를 요청할 때 메모리에 로딩함


예를 들어보자. 이미지는 프록시 패턴을 적용하기 적합한 데이터이다. 용량이 크고 저장장치에서 읽어와야 하기 때문이다.

프록시 패턴을 적용하기 전 코드를 살펴보자


Image Class

public final class Image{
  private ImageData image;

  public Image(String filePath){
    this.image=ImageLoader.getInstance().load(filePath);
  }

  public void draw(Canvas canvas, float x, float y){
    canvas.draw(this.image, x, y);
  }
}


위 코드에서 문제점

  • 생성자에서 무조건 이미지를 읽어 옴
  • 메모리를 많이 사용
  • 이미지를 읽어오는 데 시간도 걸림(디스크 읽는 속도는 좀 느리니까)
  • 모든 image에 대해 draw()가 호출될지도 의심스러움


이제 프록시 패턴을 적용한 결과를 보자.



Image Class

public final class Image{
  private String filePath;
  private ImageData image;

  public Image(String filePath){
    this.filePath=filePath;
  }

  public void draw(Canvas canvas, float x, float y){
    if(this.image==null){
      this.image=ImageLoader.getInstance().load(this.filePath);
    }

    canvas.draw(this.image, x, y);
  }
}


위 코드를 보면 생성자에서 개체 생성 시에 아무 데이터도 읽지 않는다. 처음 사용할 때 draw() 메소드를 통해 이미지를 로딩한다. draw() 메서드를 보면 이미 메모리에 로딩을 해 놨다면 그대로 가져다 쓴다. 이렇게 늦게 읽어오는 방식을 지연 로딩(lazy loading)이라고 하고 반대 방식은 즉시 로딩(eager loading)이라고 한다.


언제 지연 로딩을 사용하고 언제 즉시 로딩을 사용해야 할까? 각각의 장단점을 살펴보자.



요즘 컴퓨터에는 메모리를 많이 장착한다. 미리 다 로딩해놔도 큰 문제가 아닌 경우가 많다. 한 번에 그리는 이미지 수가 많지 않다면 필요할 때마다 디스크에서 읽을 수 있다.(SSD면 더 빠르다) 하지만 인터넷에서 그 이미지들을 로딩한다면 디스크에서 읽을 때보다 시간이 더 오래 걸린다. 그 동안에 프로그램이 멈춰 있다면 사용자에게 나쁜 경험을 줄 수 있다.


요즘은 클라이언트가 내부 동작방법을 분명히 알고 그에 적합한 UI를 보여주는 방법이 더 사랑받는다. 따라서 요즘 세상에는 클래스가 남몰래 프록시 패턴을 사용하는 것보다 클라이언트에게 조작 권한을 주는 게 좋을 수 있다. 어떻게 하는 지 살펴보자.


Image Class

public final class Image{
  private String filePath;
  private ImageData image;

  public Image(String filePath){
    this.filePath=filePath;
  }

  public boolean isLoaded(){
    return this.image!=null;
  }

  public void load(){
    if(this.image==null){
      this.image=ImageLoader.getInstance().load(this.filePath);
    }
  }
  public void unload(){
    this.image=null;
  }
  public void draw(Canvas canvas, float x, float y){
    canvas.draw(this.image, x, y);
  }
}


위의 코드에서 isLoaded() / load() / unload()이미지의 로딩 상태를 클라이언트가 명확히 알 수 있게 해준다. 클라이언트가 로딩과 언로딩 시점을 직접 제어할 수 있게 해준다. 이를 이용해 게임이나 앱에서 봤던 로딩 스크린을 보여줄 수 있다.(모든 이미지를 다 읽어올 때까지)


프록시 패턴의 흔한 다른 예시 살펴보기


:pushpin: 프록시 패턴이란?


프록시대리인이라는 뜻으로 무엇인가를 대신 처리하는 의미이다.

일종의 비서라고 생각하면 된다.

어떤 객체를 사용하고자 할때, 객체를 직접적으로 참조 하는 것이 아니라, 해당 객체를 대행(대리, proxy)하는 객체를 통해 대상객체에 접근하는 방식을 사용하면 해당 객체가 메모리에 존재하지 않아도 기본적인 정보를 참조하거나 설정할 수 있고 또한 실제 객체의 기능이 반드시 필요한 시점까지 객체의 생성을 미룰 수 있다.



예를 들어 용량이 큰 이미지와 글이 같이 있는 문서를 모니터 화면에 띄운다고 가정하였을 때 이미지 파일은 용량이 크고 텍스트는 용량이 작아서 텍스트는 빠르게 나타나지만 그림은 조금 느리게 로딩되는 것을 본 적이 있다.

만약 이렇게 처리가 안되고 이미지와 텍스트가 모두 로딩이 된 후에야 화면이 나온다면 사용자는 페이지가 로딩될 때까지 의미없이 기다려야 한다. 그러므로 먼저 로딩이 되는 텍스트라도 먼저 나오는게 좋다.

이런 방식을 취하려면 텍스트 처리용 프로세서, 그림 처리용 프로세스를 별도로 운영하면 된다.

이런 구조를 갖도록 설계하는 것이 바로 프록시 패턴이다.

일반적으로 다른 무언가와 이어지는 인터페이스의 역할을 하는 클래스를 의미한다.


:pushpin: 프록시 패턴 장점


  • 사이즈가 큰 객체(ex: 이미지)가 로딩되기 전에도 프록시를 통해 참조를 할 수 있다.
  • 실제 객체의 public, protected 메소드들을 숨기고 인터페이스를 통해 노출시킬 수 있다.
  • 로컬에 있지 않고 떨어져 객체를 사용할 수 있다.
  • 원래 객체의 접근에 대해서 사전처리를 할 수 있다.


:pushpin: 프록시 패턴 단점


  • 객체를 생성할 때 한 단계를 거치게 되므로, 빈번한 객체 생성이 필요한 경우 성능이 저하될 수 있다.
  • 프록시 내부에서 객체 생성을 위해 스레드가 생성, 동기화가 구현되야 하는 경우 성능이 저하될 수 있다.
  • 로직이 난해해져 가독성이 떨어질 수 있다.


:pushpin: 프록시 패턴 예제


Image Interface

public interface Image {
   void displayImage();
}


RealImage.java

public class RealImage implements Image {

    private String fileName;

    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk(fileName);
    }

    private void loadFromDisk(String fileName) {
        System.out.println("Loading " + fileName);
    }

    @Override
    public void displayImage() {
        System.out.println("Displaying " + fileName);
    }
}


ProxyImage.java

public class ProxyImage implements Image {
    private RealImage realImage;
    private String fileName;

    public ProxyImage(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public void displayImage() {
        if (realImage == null) {
            realImage = new Real_Image(fileName);
        }
        realImage.displayImage();
    }
}


Main

public class Main {
    public static void main(String[] args) {
        Image image1 = new Proxy_Image("test1.png");
        Image image2 = new Proxy_Image("test2.png");

        image1.displayImage();
        System.out.println();
        image2.displayImage();
    }
}



YoungKyonYou

Integration of Knowledge