프록시 패턴(Proxy Pattern)이란?
프록시(Proxy)를 번역하면 대리자, 대변인의 의미를 갖고 있다. 무엇인가를 대신 처리하는 의미라고 할 수 있다.
일종의 비서라고 생각하면 된다. 사장님한테 사소한 질문을 하기보다는 비서한테 먼저 물어보는 개념이라고 생각할 수 있겠다.
이렇게 어떤 객체를 사용하고자 할때, 객체를 직접적으로 참조 하는것이 아니라, 해당 객체를 대행(대리, proxy)하는 객체를 통해 대상객체에 접근하는 방식을 사용하면 해당 객체가 메모리에 존재하지 않아도 기본적인 정보를 참조하거나 설정할 수 있고 또한 실제 객체의 기능이 반드시 필요한 시점까지 객체의 생성을 미룰 수 있다.
구조 패턴(Structural Pattern)이란?
구조 패턴이란 작은 클래스들을 상속과 합성을 이용하여 더 큰 클래스를 생성하는 방법을 제공하는 패턴이다.
이 패턴을 사용하면 서로 독립적으로 개발한 클래스 라이브러리를 마치 하나인 양 사용할 수 있다. 또, 여러 인터페이스를 합성(Composite)하여 서로 다른 인터페이스들의 통일된 추상을 제공한다.
구조 패턴의 중요한 포인트는 인터페이스나 구현을 복합하는 것이 아니라 객체를 합성하는 방법을 제공한다는 것이다. 이는 컴파일 단계에서가 아닌 런타임 단계에서 복합 방법이나 대상을 변경할 수 있다는 점에서 유연성을 갖는다.
예를 들어 용량이 큰 이미지와 글이 같이 있는 문서를 모니터 화면에 띄운다고 가정하였을 때 이미지 파일은 용량이 크고 텍스트는 용량이 작아서 텍스트는 빠르게 나타나지만 그림은 조금 느리게 로딩되는 것을 본적 있을 것이다.
만약 이렇게 처리가 안되고 이미지와 텍스트가 모두 로딩이 된 후에야 화면이 나온다면 사용자는 페이지가 로딩될때까지 의미없이 기다려야 한다. 그러므로 먼저 로딩이 되는 텍스트라도 먼저 나오는게 좋을 것이다. 이런 방식을 취하려면 텍스트 처리용 프로세서, 그림 처리용 프로세스를 별도로 운영하면 된다. 이런 구조를 갖도록 설계하는 것이 바로 프록시 패턴이다. 일반적으로 프록시는 다른 무언가와 이어지는 인터페이스의 역할을 하는 클래스를 의미한다.
즉, 한마디로 대상 객체(Subject)에 접근하기 전에 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 디자인 패턴이다.
이를 통해 객체의 속성, 변환 등을 보완하며 보안, 데이터 검증, 캐싱, 로깅에 사용된다. 대표적으로 프록시 서버를 예로 들 수 있다.
프록시 서버란?
서버와 클라이언트 사이에서 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 컴퓨터 시스템이나 응용 프로그램을 가리킨다.
프록시 예시
CORS와 프론트 엔드의 프록시 서버
CORS(Corss-Origin Resource Sharing)는 서버가 웹 브라우저에서 리소스를 로드할 때 다른 Origin을 통해 로드하지 못하게 하는 HTTP 헤더 기반 메커니즘이다.
프론트 엔드 개발 시 프론트 엔드 서버를 만들어서 벡엔드 서버와 통신할 때 주로 CORS 에러를 마주치는데, 이를 해결하기 위해 프론트 엔드에서 프록시 서버를 만들기도 한다.
Origin이란?
프로토콜 + 호스트 이름 + 포트의 조합을 말한다.
https://seyoung.com:12031/test 라는 주소에서 오리진은 https://seyoung.com:12031를 뜻한다.
예를 들어 프론트 엔드에서는 127.0.0.1:3000으로 테스트를 하는데 벡엔드 서버는 127.0.0.1:1201 이라면 포트 번호가 다르기 때문에 CORS 에러가 발생한다.
이때 프록시 서버를 둬서 프론트 엔드 서버에서 요청되는 Origin을 127.0.0.1:1201 으로 변경하는 것이다.
프록시가 사용되는 대표적인 5가지
가상프록시
꼭 필요로 하는 시점까지 객체의 생성을 연기하고, 해당 객체가 생성된 것처럼 동작하도록 만들고 싶을때 사용하는 패턴이다. 프록시 클래스에서 자잘한 작업들을 처리하고 리소스가 많이 요구되는 작업들이 필요할 때에만 주체 클래스를 사용하도록 구현하면 위의 예시처럼 해상도가 아주 높은 이미지를 처리해야 하는 경우 작업을 분산시킬 수 있.
원격프록시
원격 객체에 대한 로컬 대변자 역할을 한다. 프록시의 메소드를 호출하면 그 호출이 네트워크를 통하여 전달되어 결국 원격 객체의 메소드가 호출된다. 그리고 그 결과를 다시 프록시를 거쳐 클라이언에게 전달된다.
예시로 Google Docs를 들 수 있다. 브라우저는 브라우저대로 필요한 자원을 로컬에 가지고 있고 또 다른 자원은 Google 서버에 있는 형태다.
보호프록시
주체 클래스에 대한 접근을 제어하기 위한 경우에 객체에 대한 접근 권한을 제어하거나 객체마다 접근 권한을 달리하고 싶을때 사용하는 패턴으로 프록시 클래스에서 클라이언트가 주체 클래스에 대한 접근을 허용할지 말지 결정하도록 할 수가 있다.
로깅 프록시
서비스 객체에 대한 요청들의 기록을 유지하려는 경우. 프록시는 각 요청을 서비스에 전달하기 전에 로깅(기록)할 수 있다.
캐싱 프록시
클라이언트 요청들의 결과들을 캐시하고 이 캐시들의 수명 주기를 관리해야 할 때, 특히 결과들이 상당히 큰 경우에 사용된다. 프록시는 항상 같은 결과를 생성하는 반복 요청들에 대해 캐싱을 구현할 수 있다. 프록시는 요청들의 매개변수들을 캐시 키들로 사용할 수 있다.
프록시 예제
프록시 패턴은 어떤 다른 객체로 접근하는 것을 통제하기 위해서 그 객체의 대리자(surrogate)나 자리표시자(placeholder)의 역할을 하는 객체를 제공하는 패턴이라고 했다.
예를 들어, 개발자가 시스템 명령어를 실행하는 객체를 갖고 있을 때 개발자가 그 객체를 사용하는 것이라면 괜찮지만 만약 그 객체를 클라이언트에게 제공하려고 한다면 클라이언트 프로그램이 우리가 원치 않는 파일을 삭제하거나 설정을 변경하는 등의 명령을 내릴 수 있기 때문에 심각한 문제를 초래할 수 있다.
프록시 패턴은 클라이언트에게 접근에 대한 컨트롤을 제공하여 위와 같은 문제를 해결한다.
간단한 예시를 통해 살펴보자.
먼저 CommandExecutor 라는 명령을 실행하는 메소드를 제공하는 인터페이스를 정의한다.
CommandExecutor.java
1
2
3
4
|
public interface CommandExecutor {
public void runCommand(String cmd) throws Exception;
}
|
cs |
이번에는 만약 프록시 패턴을 적용하지 않고 앞서 말한 것처럼 그냥 클라이언트에게 명령에 대한 권한을 전부 넘겨주는 객체를 생성해보겠다.
CommandExecutorImpl.java
1
2
3
4
5
6
7
8
9
|
public class CommandExecutorImpl implements CommandExecutor {
@Override
public void runCommand(String cmd) throws IOException {
//some heavy implementation
Runtime.getRuntime().exec(cmd);
System.out.println("'" + cmd + "' command executed.");
}
}
|
cs |
보시다시피 CommandExecutorImpl 클래스에서는 runCommand()의 파라미터로 받은 cmd 명령어를 그대로 수행하고 있다. 이렇게 구현할 경우에는 앞서 말한 것처럼 원치 않는 파일 삭제나 설정 변경 등에 문제가 발생할 가능성이 높아진다.
그렇다면 이를 해결하기 위해 프록시 객체를 두어 관리자(Admin) 계정이 아닐 경우에는 rm 이라는 명령어에 대해 수행하지 못하도록 구현해보겠다.
CommandExecutorProxy.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public class CommandExecutorProxy implements CommandExecutor {
private boolean isAdmin;
private CommandExecutor executor;
public CommandExecutorProxy(String user, String pwd){
if("ReadyKim".equals(user) && "correct_pwd".equals(pwd))
isAdmin = true;
executor = new CommandExecutorImpl();
}
@Override
public void runCommand(String cmd) throws Exception {
if(isAdmin){
executor.runCommand(cmd);
}else{
if(cmd.trim().startsWith("rm")){
throw new Exception("rm command is not allowed for non-admin users.");
}else{
executor.runCommand(cmd);
}
}
}
}
|
cs |
이번에는 위에서 작성한 코드들을 테스트 해보겠다.
ProxyPatternTest.java
1
2
3
4
5
6
7
8
9
10
11
12
|
public class ProxyPatternTest {
public static void main(String[] args){
CommandExecutor executor = new CommandExecutorProxy("ReadyKim", "wrong_pwd");
try {
executor.runCommand("ls -ltr");
executor.runCommand("rm -rf abc.pdf");
} catch (Exception e) {
System.out.println("Exception Message::"+e.getMessage());
}
}
}
|
cs |
1
2
|
'ls -ltr' command executed.
Exception Message::rm command is not allowed for non-admin users.
|
cs |
결과를 보면 Admin 계정의 ID와 Password가 틀렸기 때문에 프록시 객체가 rm 명령어에 대한 수행을 거부하였고 그 결과로 Exception을 던진다.
프록시 패턴은 이렇듯 어떤 객체에 대하여 접근할 때에 Wrapper Class를 두어 접근에 대한 통제(Control access)를 위해 사용한다.
프록시 패턴의 장단점
프록시 패턴 장점
- 클라이언트들이 알지 못하는 상태에서 서비스 객체를 제어할 수 있다.
- 실제 객체의 public, protected 메소드들을 숨기고 인터페이스를 통해 노출시킬 수 있다.
- 클라이언트들이 신경 쓰지 않을 때 서비스 객체의 수명 주기를 관리할 수 있다.
- 프록시는 서비스 객체가 준비되지 않았거나 사용할 수 없는 경우 사전처리가 가능하다.
- 개방/폐쇄 원칙. 서비스나 클라이언트들을 변경하지 않고도 새 프록시들을 도입할 수 있다.
- 사이즈가 큰 객체(ex : 이미지)가 로딩되기 전에도 프록시를 통해 참조를 할 수 있다.
프록시 패턴 단점
- 객체를 생성할때 한단계를 거치게 되므로, 빈번한 객체 생성이 필요한 경우 성능이 저하될 수 있다.
- 프록시 내부에서 객체 생성을 위해 스레드가 생성, 동기화가 구현되야 하는 경우 성능이 저하될 수 있다.
- 로직이 난해해져 가독성이 떨어질 수 있다.
다른 패턴과의 관계
- 어댑터는 다른 인터페이스를, 프록시는 같은 인터페이스를, 데코레이터는 향상된 인터페이스를 래핑된 객체에 제공한다.
- 퍼사드 패턴은 복잡한 객체 또는 시스템을 보호하고 자체적으로 초기화한다는 점에서 프록시와 유사하다. 퍼사드 패턴과 달리 프록시는 자신의 서비스 객체와 같은 인터페이스를 가지므로 이들은 서로 상호 교환이 가능하다.
- 데코레이터와 프록시의 구조는 비슷하나 이들의 의도는 매우 다르다. 두 패턴 모두 한 객체가 일부 작업을 다른 객체에 위임해야 하는 합성 원칙을 기반으로 한다.
이 두 패턴의 차이점은 프록시는 일반적으로 자체적으로 자신의 서비스 객체의 수명 주기를 관리하는 반면 데코레이터의 합성은 항상 클라이언트에 의해 제어된다는 점이다.
참고
https://refactoring.guru/ko/design-patterns/proxy
https://coding-factory.tistory.com/711