티스토리 뷰

‘개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴' 책을 보고 정리한 글입니다.

 

제품 목록을 보여주는 GUI 프로그램은 다음 사진처럼 목록 중 일부를 화면에 보여주고, 스크롤을 할 때 나머지 목록을 화면에 표시할 수 있다.

제품 목록을 구성할 때 관련된 모든 이미지를 로딩하도록 구현할 수 있는데, 이 경우 불필요하게 메모리를 사용하는 문제가 발생할 수 있다.

예를 들어, 목록 하단에 위치한 이미지는 실제로 스크롤을 하기 전까지는 화면에 보이지 않음에도 불구하고 목록을 구성할 때 메모리에 이미지 정보를 로딩하게 된다.

특히 이미지를 로컬 파일 시스템이 아닌 웹에서 읽어 온다면 이미지 로딩으로 인해 제품 목록을 보여주기까지 대기 시간이 길어지게 된다.

 

위같은 문제를 해결하는 방법은 이미지가 실제로 화면에 보여질 때 이미지 데이터를 로딩하는 것이다.

필요 시점에 Image 클래스를 이용해서 이미지를 로딩하는 DynamicLoadingImage 클래스를 추가하고 다음 그림과 같이 목록을 보여주는 클래스에서 Image 클래스 대신 DynamicLoadingImage를 사용하게 만드는 것이다.

하지만 위 그림처럼 구현하게 되면, 이미지 로딩 방식을 변경해야 할 때 ListUI 코드를 변경해야 하는 문제가 발생한다.

 

이런 상황에서 ListUI 변경 없이 이미지 로딩 방식을 교체할 수 있도록 해주는 패턴이 프록시 패턴이다.

프록시 패턴은 실제 객체를 대신하는 프록시 객체를 사용해서 실제 객체의 생성이나 접근 등을 제어할 수 있도록 해 주는 패턴이다.

  • Image 인터페이스 : 이미지를 표현
  • ListUI : Image 타입을 이용해서 화면에 이미지를 표시
  • RealImage 클래스 : 실제로 이미지 데이터를 로딩해서 메모리에 보관하는 콘크리트 클래스

ProxyImage 클래스가 프록시 패턴에서 프록시 역할을 한다.

public class ProxyImage implements Image {
    private String path;
    private RealImage image;

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

    public void draw() {
        if(image == null) {
            image = new RealImage(path); // 최초 접근 시 객체 생성
        }
        
        image.draw(); // RealImage 객체에 위임

    }
}

ProxyImage 클래스는 draw() 메서드가 호출되기 전까지 RealImage 객체를 생성하지 않는다.

ProxyImage 클래스의 draw() 메서드는 최초로 draw() 메서드를 실행할 때 RealImage 객체를 생성하고, 그 뒤에 생성된 RealImage 객체의 draw() 메서드를 호출한다.

ListUI 클래스는 Image 타입을 사용하기 때문에 실제 타입이 RealImage인지 ProxyImage인지 여부는 모른다.

단지, Image 타입의 draw() 메서드를 이용해서 이미지를 그려 달라고 할 뿐이다.

ListUI 클래스가 다음과 같이 Image 타입의 목록을 가지고 있고, 스크롤이 되는 시점에 해당 Image 객체의 draw()를 호출한다고 하자.

import java.util.List;

public class ListUI {

    private List<Image> images;

    public ListUI(List<Image> images) {
        this.images = images;
    }

    public void onScroll(int start, int end) {
        // 스크롤 시, 화면에 표시되는 이미지를 표시
        for (int i = start; i <= end; i++) {
            Image image = images.get(i);
            image.draw();
        }
    }
}

ListUI 클래스의 images 필드에 보관된 Image 객체에 실제 타입이 ProxyImage인 경우, 위 코드의 onScroll() 메서드에서 이미지를 그리는 과정은 다음과 같이 동작한다.

위 그림에서 ProxyImage 객체는 최초에 draw() 메서드가 실행될 때 RealImage 객체를 생성하기 때문에, ProxyImage 객체의 draw() 메서드가 호출되기 전에는 RealImage 객체가 생성되지 않으므로 메모리에 이미지 데이터를 로딩하지 않는다.

따라서 화면에 표시되지 않는 이미지를 로딩하기 위해 불필요하게 메모리를 낭비하는 상황을 방지할 수 있게 된다.

 

또한, ListUI 클래스는 이미지가 언제 로딩되는지 알 필요가 없기 때문에, 이미지 로딩 정책을 변경하더라도 ListUI 클래스의 코드는 영향을 받지 않는다.

예시로, 상위 4개는 바로 이미지를 로딩하고 나머지는 화면에 보여지는 순간에 로딩하도록 구현해야 할 경우 다음 코드처럼 RealImage 객체와 ProxyImage 객체를 섞어서 ListUI에 전달해주면 된다.

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> paths = new ArrayList<>(); // 이미지 경로 목록
        List<Image> images = new ArrayList<>(paths.size());

        for (int i = 0; i < paths.size(); i++) {
            if (i <= 4) {
                images.add(new RealImage(paths.get(i)));
            } else {
                images.add(new ProxyImage(paths.get(i)));
            }
        }
        // 이미지 로딩 정책의 변경이 ListUI에 영향을 주지 않는다.
        ListUI listUI = new ListUI(images);
    }

}

ProxyImage처럼 필요한 순간에 실제 객체를 생성해 주는 프록시를 가상 프록시라고 부른다.

가상 프록시 외에는 보호 프록시나 원격 프록시 등이 존재한다.

  • 보호 프록시 : 실제 객체에 대한 접근을 제어하는 프록시
    • 접근 권한이 있는 경우에만 실제 객체의 메서드를 실행하는 방식으로 구현한다.
  • 원격 프록시 : 자바의 RMI처럼 다른 프로세스에 존재하는 객체에 접근할 때 사용되는 프록시
    • IPC나 TCP 통신을 이용해서 다른 프로세스의 객체를 실행하게 된다.

프록시 패턴을 적용할 때 고려할 점

실제 객체를 누가 생성할 것이냐에 대한 것이다.

ProxyImage와 같은 가상 프록시는 필요한 순간에 실제 객체를 생성하는 경우가 많기 때문에, ProxyImage 클래스에서 직접 RealImage 타입을 사용한 것처럼 가상 프록시에서 실제 생성할 객체의 타입을 사용하게 된다.

반면에 접근 제어를 위한 목적으로 사용되는 보호 프록시는 보호 프록시 객체를 생성할 때 실제 객체를 전달하면 되므로, 실제 객체의 타입을 알 필요 없이 추상 타입을 사용하면 된다.

 

위임 방식이 아닌 상속을 사용해서 프록시를 구현할 수도 있다.

특정 기능은 관리자만 실행할 수 있어야 한다고 할 경우 보호 프록시를 사용할 수 있을 것이다.

이 때 보호 프록시는 다음과 같이 상위 클래스의 메서드를 재정의하는 방법으로 구현할 수 있다.

public class ProtectedService extends Service {
    @Override
    public void someMethod() {
        if (! CurrentContext.getAuth().isAdmin()) {
            throw new AccessDeniedException();
        }
        
        super.someMethod();
    }
}

위임 기반의 프록시 패턴 vs 데코레이터 패턴

프록시 패턴의 경우 실제 객체에 대한 접근을 제어하는데 초점이 맞춰져 있다.

데코레이터 패턴의 경우 기존 객체의 기능을 확장하는데 초점을 맞추고 있다.

따라서 클래스의 이름을 부여할 때에는 의도에 맞는 단어를 선택해야 한다.

 

'JAVA > 디자인 패턴' 카테고리의 다른 글

옵저버(Observer) 패턴  (0) 2022.03.03
어댑터(Adapter) 패턴  (0) 2022.03.01
데코레이터(Decorator) 패턴  (0) 2022.03.01
상태(State) 패턴  (0) 2022.03.01
템플릿 메서드 패턴  (0) 2022.02.23
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함