Notice
Recent Posts
Recent Comments
«   2024/05   »
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 31
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

Proxy Pattern 본문

Design Pattern/Structural Pattern

Proxy Pattern

defacto standard 2018. 2. 16. 19:48

1. 가정

 - 사용자의 요청에 따라 처리를 하는 서버 시스템을 구현한다.

 - 서버는 사용자 인증을 수행한다. 여기서는 User1이라는 ID와 1234라는 Password를 가지면 인증에 성공한다고 가정한다.

 - 서버는 인증에 성공한 유저의 이름만 출력한다.


2. Naive Code


 - User

public class User {
private String ID;
private String PW;

public User(String ID, String PW) {
this.ID = ID;
this.PW = PW;
}

public String getID() {
return ID;
}

public String getPW() {
return PW;
}
}


 - Security

public class Security {
private final String authID = "User1";
private final String authPW = "1234";

public boolean authenticate(User user) {
return (authID.equals(user.getID()) && authPW.equals(user.getPW()));
}

}

인증을 담당하는 Security 클래스. User객체를 받아서 특정 ID와 PW가 매치되면 true를 반환한다.


 - Server

public class Server {
Security security;

public Server() {
security = new Security();
}

public void printName(User user) {
if(security.authenticate(user))
System.out.println(user.getID() + " Authenticate Success");
else
System.out.println(user.getID() + " Authenticate Failed");
}
}

사용자의 요청을 받아서 처리하는 Server이다.

내부에 Security 클래스를 선언하고, 생성자에서 인스턴스화 하여 이를 사용하는 Client에서는 Security의 존재를 알지 못한다.


 - NaiveClient

public class NaiveClient {
public static void main(String args[]) {
User user1 = new User("User1", "1234");
User user2 = new User("User1", "123");
User user3 = new User("User2", "1234");
Server server = new Server();

server.printName(user1);
server.printName(user2);
server.printName(user3);
}
}



3. 문제점


위 소스코드는 많은 문제점을 가지고 있다. 만약 다음과 같은 요구사항이 추가되었다고 가정하자.

 - 서버는 사용자의 ID와 Request Time 대한 로그를 남긴다.

 - 더 이상 인증을 진행하지 않는다.


첫 번째 요구사항을 충족시키기 위해서는, Server에 기능을 추가해야한다.

Server에서 로그를 남기는 것은 맞지만, Server자체에서 로그 기능을 제공하는 것을 적절하지 않다.


새로운 Logger 클래스를 파서, 여기에 기능을 구현하고 Server에서는 이를 가져다 쓰면 적절할 것이다.


하지만 여기서 또 다른 기능을 서버에서 수행하려면, Server의 소스코드는 계속해서 변경되어야 한다.


어쨋든 변경을 감수한다고 쳐보자.


두 번째 요구사항을 충족시키기 위해서는, Server에서 Security를 사용하지 않도록 변경해야한다.


이 역시도 Server가 계속해서 변경이 일어나게 되는 원인이 된다.



4. 해결방안


위와 같은 문제의 근본적인 이유는, Server에서 구체적인 클래스와 강한 결합도를 보이기 때문이다.


잘 생각을 해보면 인증, 로깅 등은 분명 서버에 요청하면 처리되지만 이는 추가적 기능이다.


즉, 비즈니스 로직과는 상관이 없다.


여기에서 비즈니스 로직은 '접속한 사용자의 ID를 출력하는 것'이다.


인증을 수행하는 것과 로그를 찍는 것은 옵션사항이다.


따라서, Server에서는 이에 대한 정보가 없는 것이 좋다.



또한, 이러한 작업들은 특정 기능의 앞뒤 혹은 중간에 수행한다.


이럴 때 유용하게 쓰이는 것이 Delegation이다.


자신의 할일을 수행하고, Delegation을 통해 다른 객체의 역할을 수행하면된다.


Delegation을 활용한다면 Server클래스는 다른 클래스의 존재를 알지 못하더라도, 추가적인 기능을 수행할 수 있다.


외부에서는 내부에서 사용하는 존재를 알지 못해도 구현이 가능하다.


즉, 디커플링을 구현할 수 있다.


한 가지 더 고려를 하자면, 단순히 내부에서 사용한다고 끝난다면 그다지 좋은 설계가 아니다.


단순히 Delegation, Delegation, Delegation.. 을 하게 된다면


인증->로깅에서 로깅->인증 등의 기능순서가 바뀌었을 때, 혹은 기능이 추가되거나 제거된다면 이에 관련된 소스코드를 수정해야한다.


만약 A->B->C->Server 라는 호출구조라고 생각해보자. B기능이 사라진다고 가정하자.


A는 현재 책임을 B에게 위임(Delegate)한다.

B는 더이상 쓰이지 않기 때문에 바로 C를 호출해야할 필요가 있다.

따라서 A의 소스코드가 변경된다.


이렇게 계속해서 기능이 변화한다면 계속해서 호출하는 클래스를 수정해야 한다.




기능이 변하면 기존의 소스코드를 수정해야하는 것이 당연하다면, 이를 최소화하는 방법으로 생각해야 한다.



이에대한 해결방법은, Proxy기능을 하는 것들을 Server와 같은 관점에서 다룰 필요가 있다.


이렇게 한다면 로깅을 먼저하든, 인증을 먼저하든, 2개의 순서만 바꾸면 된다.


단지, 이를 사용하는 Client에서 비즈니스 로직에 따라 이를 결정하여 바인딩하면된다.



5. Solution Code


 - Subject.java

public interface Subject {
public void operation();
}

어떠한 동작을 의미하는 operation()메서드를 가지는 Subject Interface이다.

Client에서는 구체적인 클래스와의 디커플링을 위해 추상화된 객체를 다루는데, 그 추상화된 객체가 Subject객체가 해당된다.


 - RealSubject1


// on System A
// Client로부터 명령받은 ProxySubject에 의해 조작되는 진짜 Subject
public class RealSubject implements Subject {

private String filename;

public RealSubject(String filename) {
this.filename = filename;
}

@Override
public void operation() {
System.out.println("RealSubject# operation(), filename is " + filename);
}
}

사용자가 실질적으로 원하는 로직을 가지고 있는 RealSubject이다. 이 로직은 Subject Interface를 상속받아 구현해야하는 operation()메서드에 구현한다. filename을 출력하는 기능이다.


 - ProxySubject1

// on System B
// Client에서 조작하는 Subject, RealSubject에 명령을 내리는 것 같지만 이쪽으로 명령함
// RealSubject를 수행하기 전 Pre/Post Execution 가능
public class ProxySubject1 implements Subject {

private Subject subject = null;

public ProxySubject1(Subject subject) {
this.subject = subject;
}

@Override
public void operation() {
preOperation();
subject.operation(); // Delegation
postOperation();

}

private void preOperation() {
System.out.println("ProxySubject1# preOperation()");
}

private void postOperation() {
System.out.println("ProxySubject1# postOperation()");
}
}

operation()에 대한 Request(Java에서 Message)를 받으면 내부적으로 전처리( preOperation() )와 후처리( postOperation() )를 하는 ProxySubject1이다.


전처리 메서드인 preOperation()의 내용은 현재 객체가 할당되어 있지 않다면, Client로부터 넘어온 filename을 가지는 RealSubject를 생성하여 저장해놓고, preOperation()이 동작하였다는 것을 print하는 것이다.


후처리 메서드인 postOperation()의 내용은, 단순히 후처리 메서드가 동작하였다는 것을 print하는 것이다.


예제에서는 로깅 기능을 수행하는 요소에 해당한다.



 - ProxySubject2

// on System B
// Client에서 조작하는 Subject, RealSubject에 명령을 내리는 것 같지만 이쪽으로 명령함
// RealSubject를 수행하기 전 Pre/Post Execution 가능
public class ProxySubject2 implements Subject {

private Subject subject = null;

public ProxySubject2(Subject subject) {
this.subject = subject;
}

@Override
public void operation() {
preOperation();
subject.operation(); // Delegation

}

private void preOperation() {
System.out.println("ProxySubject2# preOperation()");
}
}

전처리 메서드 ( preOperation )만 존재한다.

예제에서는 인증 기능을 수행하는 요소에 해당한다.


 - Client

public class Client {
public static void main(String args[]){
RealSubject realSubject = new RealSubject("filename"); // 실제 서버
Subject subject1 = new ProxySubject1(realSubject); // 로깅
Subject subject2 = new ProxySubject2(subject1); // 인증
subject2.operation();

System.out.println();

subject2 = new ProxySubject2(realSubject);
subject2.operation();

}
}

Client는 ProxySubject객체를 2개 만들되


결과는 다음과 같다.





6. Proxy Pattern


Proxy란 중간에 가로채는것을 말한다.


프록시 서버란 Request를 중간에 가로채서 어떠한 연산을 한 후 최종 Destination Server에 다시 보내고, Client에 다시 Response를 하는 서버를 말한다.


프록시 패턴 역시도 비슷한 플로우를 가진다.


Client는 Subject(ProxySubject)에 메시지를 보내면(이것을 '가로챈다'라고 표현한다)

Subject는 추상화된 개념으로서(Interface로 구현), Client입장에서 보면 메시지를 전송할 목적지이다.


인터페이스를 구현하는 ConcreteSubject로는 2가지가 있다.

첫 번째는 프록시의 역할을 하는 ProxySubject,

두 번째는 실제 필요한 연산을 하는 RealSubject가 있다.


Client는 ProxySubject에 요청을 하지만, ProxySubject는 상황에따라 추가적인 전처리/후처리 혹은 RealSubject의 내용을 바로 수행하거나 수행하지 않을 수도 있다.


프록시 패턴은 스프링의 AOP와 느낌이 비슷하다. 

상황에 따라 트랜잭션, 시큐리티, 로그 등의 추가적인 처리를 구현할 수 있으며, Client와 RealSubject간의 디커플링을 도모하여 유연성있는 소스코드를 제공한다.


예제에서는 외부에서 주입받아 사용하는 경우를 다루었다. 하지만, 아예 RealSubejct를 ProxySubject 안에서 생성하여, 

Client가 완전히 RealSubject의 개념을 모르도록 할 수도 있다.


Proxy Pattern은 아주 많은 방식이 존재하므로, Proxy Pattern이 특정 소스코드의 형태를 취하지는 않는다.


상황에 따라 변경하여 사용하면 되므로, 반드시 예제나 위키피디아에 적혀있는 예제대로 할 필요는 없다.


- UML


ProxySubject (위 그림에서 Proxy)와 RealSubject는 Subject Interface를 상속받으므로 동일한 이름의 메서드를 구현한다.

ProxySubject는 요청을 받아도 그 기능을 RealSubject에 위임(Delegate)하여 실제 동작은 RealSubject에게 맡기고, 자신은 추가적인 처리나 상황에 맞는 처리를 하게 된다.



7. 장단점


장점 : 

단점 : 

'Design Pattern > Structural Pattern' 카테고리의 다른 글

Facade Pattern (기본)  (4) 2018.02.16
Bridge Pattern (기본)  (3) 2018.02.16
Adapter Pattern  (0) 2018.02.16
Composite Pattern  (0) 2017.09.26
Decorator Pattern  (0) 2017.09.26
Comments