개요

프록시란?

  • ‘대리’라는 의미로 무엇인가를 대신 처리함
  • 보안 상의 이유로 서버를 외부에 노출시키지 않기 위해 서버와 클라이언트단 중간에 위치한 서버를 프록시 서버라 함

 

정의

  • 실제 기능을 수행하는 객체(Real Object)를 바로 접근해서 제어하지 않고, 가상의 객체(Proxy Object)를 통해 우회하여 실제 기능에 접근함
  • 대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 디자인 패턴
  • 클라이언트 쪽에서는 실제 객체를 통해 메서드를 호출하고 반환 값을 받는지, 대리자 객체를 통해 받는지 전혀 모름

 

클래스 다이어그램

  • 클라이언트가 요청을 보냄(RealSubject의 request()메서드 호출)
  • Proxy가 대신 RealSubject의 request()메서드를 호출
  • 그 반환 값을 클라이언트에게 전달
  • Real Object와 Proxy Object는 동일한 인터페이스를 구현
  • Proxy Object는 메소드 수행 시 Real Object의 메소드에 위임함

 

특징

  • 대리자는 실제 서비스와 같은 이름의 메서드를 구현함(이때 인터페이스를 사용)
  • 대리자는 실제 서비스에 대한 참조 변수를 가짐
  • 대리자는 실제 서비스의 메서드 호출 전후에도 별도의 로직 수행 가능
  • 흐름 제어만 할 뿐 결과값을 조작하거나 변경시키면 안됨
  • 일반적으로 프록시는 서비스 객체들의 전체 수명 주기를 관리함
흐름 제어가 필요한 이유

⇒ 동기적인 처리를 최대한 비동기적으로 처리하기 위함

1. 프록시 객체를 사용하지 않는 경우

  • 많은 양의 리소스를 필요로 하는 상황에서 DB 쿼리가 많이 느려질 수 있음
  • 지연 초기화를 위한 코드 작성이 필요하는데, 이를 모든 클래스마다 직접 넣을 경우 엄청 많은 코드 중복이 발생함

2. 프록시 객체를 사용하는 경우

  • 프록시 객체가 요청을 먼저 받은 뒤 흐름을 제어하여 DB에 쿼리를 날릴 수 있음
  • 서비스를 유연하게 제공 가능

 

장점

  • 참조를 통한 메모리 공간 확보
    • 해당 객체가 메모리에 존재하지 않아도 기본적인 정보를 참조하거나 설정할 수 있음
    • 사이즈가 큰 객체가 로딩되기 전에도 프록시를 통해서 참조할 수 있음
    • 객체의 기능이 필요한 시점까지 객체의 생성을 미룰 수 있음
    • 클라이언트들의 활성 상태 여부를 확인하여, 비어있을 경우 해당 서비스 객체를 닫고 그에 해당하는 시스템 자원을 확보할 수 있음
  • 흐름 제어
    • 클라이언트의 모든 요청을 받음 → 서비스를 유연하게 제공
    • 실제 메소드가 호출되기 이전에 전처리 등을 구현 객체의 변경 없이 추가할 수 있음
  • 캐시 사용 가능
    • 프록시가 내부 캐시를 통해 데이터가 캐시에 존재하지 않는 경우에만 주체 클래스에서 작업이 실행되도록 할 수 있음
  • 인터페이스를 두기 때문에 개발 코드에서 구현체에 영향을 받지 않아 코드 변경이 최소화
  • 실제 객체의 메소드를 숨기고 인터페이스를 통해 노출시킬 수 있어 방패(보안) 역할
  • 사용자 입장에서는 프록시 객체나 실제 객체나 사용법은 유사하므로 사용성이 좋음

 

활용

  • 기존의 작업 수행하며 부가적인 작업(초기화 지연, 로깅, 액세스 제어, 캐싱 등)을 수행하기 좋음
  • 비용이 많이 드는 연산(DB 쿼리, 대용량 텍스트 파일 등)을 실제로 필요한 시점에 수행할 수 있음
  • 페이지를 로드할 때 이미지는 텍스트보다 용량이 커서 로딩 시간이 오래 걸릴 수 있기 때문에, 프록시의 흐름 제어를 통해 현재까지 완료된 내용들을 우선적으로 보여주도록 함

 

단점

  • 객체를 생성할 때 한 단계를 거치게 되므로 빈번한 객체 생성은 성능을 저하시킬 수 있음
  • 객체 생성을 위해 스레드가 생성, 동기화가 구현되어야 하는 경우 성능을 저하시킬 수 있음
  • 로직이 난해해져 가독성이 떨어짐

 


프록시 패턴의 종류

가상 프록시와 보호 프록시의 사용 예시는 아래 예제 코드의 시나리오를 참고하면 도움이 됩니다.
해당 예제는 이 블로그를 참고했습니다. 

 

가상 프록시(Virtual Proxy)

  • 생성하기 힘든 자원에 대한 접근을 제어
  • 꼭 필요로 하는 시점까지 객체의 생성을 연기하고, 해당 객체가 생성된 것처럼 동작하도록 만들고 싶을 때 사용
  • 프록시 클래스에서 작은 단위의 작업을 처리하고 리소스가 많이 요구되는 작업들이 필요할 경우만 주체 클래스를 사용하도록 구현
  • 무거운 서비스 객체가 항상 가동되며 시스템 자원을 낭비하는 초기화 지연 경우에 사용

 

보호 프록시(Protection Proxy)

  • 접근 권한이 필요한 자원에 대한 접근을 제어
  • 특정 클라이언트에게만 서비스 객체를 사용할 수 있도록 하는 경우에 사용
  • 프록시 클래스에서 클라이언트가 주체 클래스에 대한 접근을 허용할지 말지 결정하도록 함
  • 예를 들어 운영 체제의 중요한 부분에 악의적인 응용 프로그램들이 접근하지 못하도록 할 수 있음

 

원격 프록시(Remote Proxy)

  • 원격 객체에 대한 접근을 제어
  • 서비스 객체가 원격 서버에 존재하는 경우, 네트워크를 통해 클라이언트의 요청을 전달하여, 네트워크와의 작업의 모든 복잡한 세부 사항을 처리함
  • 서로 다른 주소 공간에 있는 객체에 대해 마치 같은 주소 공간에 있는 것처럼 동작하게 하는 패턴
  • Ex: Google Docs

 

기타

  • 로깅 프록시(요청들의 로깅)
    • 서비스 객체에 대한 요청들의 기록을 유지하고 싶은 경우
    • 프록시는 각 요청을 서비스에 전달하기 전에 로깅할 수 있음
  • 캐싱 프록시(요청 결과들의 캐싱)
    • 요청 결과들을 캐싱하고, 이 캐시들의 수명 주기를 관리할 때(특히 결과들이 상당히 큰 경우)에 사용
    • 항상 같은 결과를 생성하는 반복 요청들에 대해 캐싱을 구현
    • 요청들의 매개변수들을 캐시 키로 사용할 수 있음

 


예제 코드

일반

public interface Subject {
    void request();
}
public class RealSubject implements Subject {
 
    @Override
    public void request() {
        System.out.println("Hello World!");
    }
}
public class Proxy implements Subject {
 
    private Subject subject;
 
    @Override
    public void request() {
        // 객체의 초기화를 지연시켜 실제로 사용될 때 생성함으로써 메모리 절약 가능
        if (this.subject == null) {
            this.subject = new RealSubject();
        }
 
        subject.request(); // 프록시가 RealSubject()의 메소드를 호출
    }
}
public class Main {
    public static void main(String[] args) {
        // RealSubject 클래스의 메소드를 호출하는것이 아닌 Proxy 클래스의 메소드를 호출
        Subject subject = new Proxy();
        subject.request(); // 내부적으로 RealSubject의 메소드를 호출
    }
}

 

가상 프록시

콘솔 프로그램으로 20개씩 난독화된 전자 서류의 본문을 복호화해서 보여주는 프로그램 작성한다고 해보자. 아래처럼 텍스트 파일을 읽는 인터페이스가 있다. 메서드가 하나밖에 없는 아주 간단한 인터페이스로, 앞으로 이 인터페이스를 구현하는 클래스는 반드시 fetch() 메서드를 구현해야 한다.

 

interface TextFile {
    String fetch();
}

실제 업무를 수행하기에 앞서 협업 조직에 SecretTextFile 이라는 클래스를 인수 받았다. 이 클래스는 난독화되어 있는 텍스트 파일을 복호화해서 평문으로 바꿔주는 클래스로, 협업 조직에서 라이브러리로 제공한 클래스라고 가정하자. 즉, 나는 이 클래스를 수정할 권한이 없다.

 

class SecretTextFile implements TextFile {
	private String plainText;

	public SecretTextFile(String fileName) {
		// 특별한 복호화 기법을 이용해 데이터를 복원해서 내용을 반환
		this.plainText = SecretFileHolder.decodeByFileName(fileName);
	}

	@Override
	public String fetch() {
		return plainText;
	}
}

이 클래스로 콘솔 프로그램을 구성했으나, 실행 후 첫 결과가 나오기까지 6초라는 시간이 걸렸다. 이유를 확인해보니 SecretTextFile 클래스에서 사용 중인 SecretFileHolder.decodeByFileName() 메서드의 수행 속도가 0.3초였던 것이다. 그리고 목록에 20개의 파일 내용을 노출해야 하는 상태였기 때문에 문제가 된 것이다. 화면을 구성할 때 이 파일들을 전부 객체로 만들다 보니 6초 정도의 로딩 시간을 갖게 된 것이다. 이제 프록시 클래스를 구현하자.

 

class ProxyTextFile implements TextFile {
    private String fileName;
    private TextFile textFile;

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

    @Override
    public String fetch() {
        if (textFile == null) {
            textFile = new SecretTextFile(fileName);
        }
				// 프록시 객체를 사용하는 경우를 확인하기 위해 [proxy] 문구를 넣음
        return "[proxy] " + textFile.fetch();
    }
}

프록시 패턴을 적용해 필요할 때만 파일 복호화를 하도록 수정하기로 했다. ProxyTextFile 클래스는 객체를 생성할 때에는 별다른 동작을 하지 않지만, 실제로 데이터를 가져와야 할 때는 실제 객체인 SecretTextFile 객체를 만들어내고 기능을 위임한다. 이제 프로그램 코드를 수정하자.

 

void main() {
	List<TextFile> textFileList = new ArrayList<>();

	textFileList.addAll(TextFileProvider.getSecretTextFile(0, 3));
	textFileList.addAll(TextFileProvider.getProxyTextFile(3, 20));

	textFileList.stream().map(TextFile::fetch).forEach(System.out::println);
}

다음의 코드는 처음 3개의 파일만 실제 객체를 사용하고 나머지는 프록시 객체를 사용해 프로그램에서 첫 결과가 나오는 것을 1초 내로 만들도록 한다. textFileList를 사용하는 입장에서는 별다른 조치없이 그대로 사용하면 된다. 콘솔에서 textFileList를 순회하면서 노출한다고 하면 처음 세개는 이미 로딩이 되어 있는 상태이므로 바로 노출하고 그다음 아이템부터는 차근차근 노출할 것이다.

따라서 ProxyTextFile 같은 프록시 클래스를 만들고 기존 SecretTextFile 클래스 대신 사용한것만으로도 초기 객체 생성 시간이 대폭 감소했다. 정말 필요한 시점에만 텍스트 복호화를 하게 된 것이다. 이렇게 초기 비용이 많이 드는 연산이 포함된 객체의 경우 가상 프록시를 사용했을 때 효과를 볼 수 있다.

 

보호 프록시

인사팀에서 인사정보에 대한 데이터 접근을 직책 단위로 세분화 하려고 한다. 기존에는 오로지 인사팀에서만 사용했던 부분이었으나 최근 인사정보를 직책별로 공개해줘야 하는 경우가 생겼다. 따라서 직책에 따라서 조직원의 인사정보 접근을 제어하는 업무를 수행해야 한다. 기존에 인사팀만 사용하던 코드는 아래와 같다.

 

// 직책 등급(차례대로 조직원, 조직장, 부사장)
enum GRADE {
    Staff, Manager, VicePresident
}

// 구성원
interface Employee {
    String getName(); // 구성원의 이름
    GRADE getGrade(); // 구성원의 직책
    String getInformation(Employee viewer); // 구성원의 인사정보(매개변수는 조회자)
}

// 일반 구성원
class NormalEmployee implements Employee {
    private String name;
    private GRADE grade;

    public NormalEmployee(String name, GRADE grade) {
        this.name = name;
        this.grade = grade;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public GRADE getGrade() {
        return grade;
    }

    // 기본적으로 자신의 인사정보는 누구나 열람할 수 있도록 되어있습니다.
    @Override
    public String getInformation(Employee viewer) {
        return "Display " + getGrade().name() + " '" + getName() + "' personnel information.";
    }
}

NormalEmployee 클래스는 단순히 Employee 인터페이스를 구현하기만 한 것으로, getInformation() 메서드는 조직원이 누구든 자신의 인사정보를 열람할 수 있도록 작성이 되어있다. 지금 이 상태로만 놔둔다면 누구든지 Employee 객체에서 getInformation() 메서드를 호출하면 누가 조회하든 정보를 보여줄 것이다. 이제부터 보호 프록시 클래스를 구성해보자. 이 클래스는 조회자의 직책을 확인하고 예외를 던지거나 인사 정보를 노출할 수 있도록 실제 객체에게 위임한다. 보호 프록시에서 메서드 호출시 조회자에게 권한이 없으면 NotAuthorizedException 예외를 던지도록 한다.

 

// 인사정보가 보호된 구성원(인사 정보 열람 권한 없으면 예외 발생)
class ProtectedEmployee implements Employee {
    private Employee employee;

    public ProtectedEmployee(Employee employee) {
        this.employee = employee;
    }

    @Override
    public String getInformation(Employee viewer) {
        // 본인 인사정보 조회
        if (this.employee.getGrade() == viewer.getGrade() && this.employee.getName().equals(viewer.getName())) {
            return this.employee.getInformation(viewer);
        }

        switch (viewer.getGrade()) {
            case VicePresident:
            	// 부사장은 조직장, 조직원들을 볼 수 있다.
                if (this.employee.getGrade() == GRADE.Manager || this.employee.getGrade() == GRADE.Staff) {
                    return this.employee.getInformation(viewer);
                }
            case Manager:
                if (this.employee.getGrade() == GRADE.Staff) { // 조직장은 조직원들을 볼 수 있다.
                    return this.employee.getInformation(viewer);
                }
            case Staff:
            default:
                throw new NotAuthorizedException(); // 조직원들은 다른 사람의 인사정보를 볼 수 없다.
        }
    }

    @Override
    public String getName() {
        return employee.getName();
    }

    @Override
    public GRADE getGrade() {
        return employee.getGrade();
    }
}

class NotAuthorizedException extends RuntimeException {
    private static final long serialVersionUID = -1714144282967712658L;
}

References

+ Recent posts