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

Chain Of Responsibility Pattern 본문

Design Pattern/Behavioral Pattern

Chain Of Responsibility Pattern

defacto standard 2018. 2. 16. 23:49

1. 가정


 - 여러 종류의 작업을 처리하는 프로그램을 작성한다.

 - 처리가능한 작업의 종류는 A~D까지 있다. (종류일 뿐, A~D등급은 아니다.)

 - 작업의 처리는 해당 작업의 종류를 출력하는 것으로 구현한다.

 - 위 4가지 종류의 작업이 아니라 다른 종류의 작업이 있으면, 해당 작업에 대해 처리를 할 수 없다는 표시를 한다.



2. Naive Code

 - Job

public class Job {
private char type;

public Job(char type) {
this.type = type;
}

public char getType(){
return this.type;
}
}

 작업을 의미하는 Job 클래스이다. 


 - JobExecutor

public class JobExecutor {
private List<Job> jobList;

public void setJobList(List<Job> jobList) {
this.jobList = jobList;
}

public void executeJobs() {

for (Job job : jobList) {
char type = job.getType();
if (type == 'A')
System.out.println("Job A is executed By JobExecutor");
else if (type == 'B')
System.out.println("Job B is executed By JobExecutor");
else if (type == 'C')
System.out.println("Job C is executed By JobExecutor");
else if (type == 'D')
System.out.println("Job D is executed By JobExecutor");
else
System.out.println("JobExecutor can't execute '" + job.getType() + "' Job");

}

}
}

Job에대한 처리를 수행하는 JobExecutor이다. Job에 대한 List를 세팅받은 후, 이를 처리하는 메서드가 존재한다.


 - NaiveClient

public class NaiveClient {
public static void main(String args[]) {
List<Job> jobList = new ArrayList<Job>();
jobList.add(new Job('A'));
jobList.add(new Job('E'));
jobList.add(new Job('C'));
jobList.add(new Job('D'));
jobList.add(new Job('B'));
jobList.add(new Job('F'));
jobList.add(new Job('A'));

JobExecutor jobExecutor = new JobExecutor();
jobExecutor.setJobList(jobList);
jobExecutor.executeJobs();

}
}


실행 결과는 다음과 같다.



3. 문제점


현재 JobExecutor는 executeJobs()라는 메서드 안에서 if-else if-else를 사용하여, 현재 루프 안에서 처리되는 Job의 Type에 따라 처리를 결정한다.


이는 switch문을 써서 처리하는 것과 비슷한 플로우를 가진다.


만약 다음과 같은 요구사항이 추가되었다고 가정해보자.


 - JobExecutor는 작업 'E' 역시 처리할 수 있다.

 - JobExecutor는 특정 조건에서 작업 'E'를 처리할 수 있다.


첫 번째 요구사항을 적용하기 위해서는, JobExecutor의 executeJobs()메서드의 내용이 변경되어야 한다.


if else문을 추가함으로써 E를 처리할 수 있다.


하지만 이는 작업할 수 있는 종류가 변경된다면 계속해서 바뀌어야 한다.


만약 종류가 서로 다른 작업이 1억개가 있다면, if-else문은 1억개가 된다.


이를 유지보수하기란 결코 쉽지 않다.


그렇게 적용했다고 가정하더라도, 문제는 끝나지 않는다.


두 번째 요구사항을 적용하기 위해서는, 역시 JobExecutor의 executeJobs()메서드의 내용이 변경된다.


if else문 내에 if else 문을 추가함으로써 특정 조건에 따라 적용시킬 수 있다.


결국 조건을 판별하는 if-else문 내에 또 다른 조건을 판별하는 if-else문을 추가하게 되고, 이는 유지보수에 상당한 시간과 노력을 소비하게 한다.



4. 해결방안


위 문제는, JobExecutor내부에서 if-else 문을 사용하여 해결가능한 작업을 추출해내는 로직을 구성하기 때문이다.


따라서, 해당 if-else문에 따라 처리되는 로직 자체를 클래스화 해야한다.


단순히 클래스를 만들고 그 안에 로직을 넣은 후 클래스를 사용한다면 현재 if-else문의 구조는 바뀌지 않는다.


오히려 클래스만 많아져 복잡도만 야기할 뿐이다.


이는 해결하기 생각보다 간단한데, 로직을 클래스로 분리하면서도, 클래스 내부에서 해당 if-esle문을 가져가면 된다.


이렇게 한다면 외부에서도 if-else로 인한 복잡한 분기가 필요가 없어지고, 로직이 해당 작업을 처리할 수 있는 클래스 내에서만 구성되기 때문에, 클래스의 소스코드가 줄어들면서도 원하는대로 수행시킬 수 있다.



여기서 분리한 클래스를 ConcreteA/B/CJobExcecutor 등으로 분리했다고 가정한다.


그렇다면 외부에서는 어떠한 방식으로 ConcreteA/B/CJobExcecutor를 사용하여 처리하여야 할지를 고민해야한다.


이제 if-else문은 ConcreteA/B/CJobExcecutor 내부에 들어가있으므로, 해당 클래스를 우선 불러서 해당 작업을 수행할 수 있다면 처리를 하게끔 하면된다.


이렇게 하지 않고서는 외부에서 if-else를 선언하지 않고는 해결할 수 없다.


이를 해결하는 방법은, 현재 클래스에서 처리를 못하는 경우 다른 클래스에게 다시 요청을 하게 만들면 된다.




예를 들면, Client가 ConcreteAJobExcecutor에게 B 작업을 처리하는 요청을 한다.


ConcreteAJobExcecutor는 A종류 작업만 지원하므로, 이를 처리할 수 없다.


따라서 ConcreteBJobExcecutor 에게 해당 요청을 전달한다.


ConcreteBJobExcecutor는 B종류 작업만 지원하는데, 이를 처리할 수 있다.


만약 Client가 'Z'라는 종류의 작업의 처리를 요청한다면, ConcreteA/B/CJobExcecutor 모두 이 작업을 처리할 수 없으므로 마지막에 연결된 클래스까지 전달될 것이다. 


각 ConcreteJobExecutor는 다음 ConcreteJobExecutor의 레퍼런스를 가지고 있다. 


이를 일관성있게 처리할 수 있도록 추상적인 요소를 둔다.




이러한 구조는, 마치 각 역할을 수행하는 클래스가 체인처럼 이어져있어 '역할 체인', 'Chain Of Responsibility'라고 한다.


역할 체인 패턴에서 요청을 처리하는 추상적인 요소를 Handler라고 한다.


그리고, ConcreteA/B/CJobExcecutor 등과 같이 요청을 처리하는 구체적인 요소를 Receiver라고 한다.


5. Solution Code


 - Request

public class Request {
private String requestName;
private RequestType requestType;

public Request(RequestType requestType, String requestName) {
this.requestType = requestType;
this.requestName = requestName;
}

public RequestType getRequestType() {
return requestType;
}

public String getRequestName() {
return requestName;
}
}

특정 종류의 작업 하나 의미하는 Request이다. 


 - RequestType

public enum RequestType {
A, B, C, D, E, F
}

작업의 종류에 대한 리스트이다.


 - Handler

public abstract class Handler {
protected Handler handler;

protected abstract boolean getExecutable(RequestType requestType);
protected abstract void execute(Request request);

public Handler setHandler(Handler handler) {
this.handler = handler;
return this;
}

public void handleRequest(Request request) {

if (this.getExecutable(request.getRequestType()))
execute(request);

else if (handler != null)
handler.handleRequest(request);

}
}

특정 종류의 작업을 수행하는 Receiver들의 추상적인 개념이다.

getExecutable()은 현재 바인딩된 구체적 요소인 Receiver가 해당 RequestType의 요청을 처리할 수 있는지의 여부를 리턴한다.

execute()는 현재 바인딩된 구체적 요소인 Receiver가 실질적으로 Request를 처리하는 메서드이다.


setHandler의 경우, 현재 Receiver가 처리할 수 없는 경우, 다음에 Request를 건내줄 다음 Receiver를 세팅하는 함수이다.

즉, 현재 Receiver의 후계자(Successor)를 지정하는 메서드이다.


여기서 this를 리턴하는데, 그 이유는 여러개의 Handler가 있을 때, setHandler를 좀 더 쉽게 쓰게하기 위함이다.

만약 void setHandler()로 정의하게 된다면, setter를 쓰는데 소스코드가 불필요하게 길어지고 복잡해진다.

이는 Main 함수의 소스를 보면 쉽게 알 수 있다.


handleRequest는 Receiver가 실질적으로 이 Request를 수행할 수 있는지 판별하여, 수행할 수 있다면 처리하고,

수행할 수 없다면 다음 Receiver에게 작업을 요청한다.


 - Receiver 1~3

public class Receiver1 extends Handler {

@Override
protected boolean getExecutable(RequestType requestType) {
return requestType == RequestType.A;
}

@Override
protected void execute(Request request) {
System.out.println(request.getRequestName() + " is Executed by Receiver1");
}
}

public class Receiver2 extends Handler {

@Override
protected boolean getExecutable(RequestType requestType) {
return requestType == RequestType.B;
}

@Override
protected void execute(Request request) {
System.out.println(request.getRequestName() + " is Executed by Receiver2");
}
}

public class Receiver3 extends Handler {

@Override
protected boolean getExecutable(RequestType requestType) {
return requestType == RequestType.C;
}

@Override
protected void execute(Request request) {
System.out.println(request.getRequestName() + " is Executed by Receiver3");
}
}

작업리스트 A~F중, 각 Receiver는 A~C를 처리할 수 있다.


 - Receiver4

// Default Handler
public class Receiver4 extends Handler {
@Override
protected boolean getExecutable(RequestType requestType) {
return true;
}

@Override
protected void execute(Request request) {
System.out.println(request.getRequestName() + " is Executed by Default Receiver");
}
}

작업리스트 A~F중, A~C는 다른 핸들러에 의해 처리될 수 있으나, 처리하지 못하는 작업들은 마지막 Receiver인 Receiver4에서 전부 받아서 수행한다.


앞에서 거친 모든 핸들러가 처리하지 못한 것을 모두 받아서 수행해야 하기 때문에, getExecutable은 무조건 true를 리턴한다.

따라서, Main에서는 이러한 예외를 다루는 핸들러를 마지막에 바인딩해야한다.


 * 참고

Receiver4는 Chain Of Responsibility Pattern에 포함되는 요소(무조건 true를 리턴하여 전부 처리하는 Receiver)는 아니고,

예제를 위해서 이런식으로 구현한 것이다.


 - Client

// Sender
public class Client {
public static void main(String args[]) {

Handler handler1 = new Receiver1();
Handler handler2 = new Receiver2();
Handler handler3 = new Receiver3();
Handler defaultHandler = new Receiver4();

Handler requestsHandler
= handler1.setHandler(
handler2.setHandler(
handler3.setHandler(defaultHandler)
)
);

List<Request> requestList = new ArrayList<Request>();
requestList.add(new Request(RequestType.A, "A-1 Job"));
requestList.add(new Request(RequestType.C, "C-1 Job"));
requestList.add(new Request(RequestType.E, "E-1 Job"));
requestList.add(new Request(RequestType.F, "F-1 Job"));
requestList.add(new Request(RequestType.B, "B-1 Job"));
requestList.add(new Request(RequestType.A, "A-2 Job"));
requestList.add(new Request(RequestType.F, "F-2 Job"));

for(Request request : requestList)
requestsHandler.handleRequest(request);
}
}

실질적으로 Responsibility Chain에 여러 작업들(RequestList)을 처리하도록 요청하는 클래스이다.


실행 결과는 다음과 같다.




6. Chain Of Responsibility Pattern


Chain Of Responsibility는 각 클래스가 특정 경우의 수에 대해서 처리하는 로직을 분리시키고, 이를 체인 처럼 엮어서 자신이 못하면 다른 클래스에게 위임시키는 패턴이다.


if-else를 줄일 수 있는 좋은 패턴이다.


실제 예로는 Spring Framework에서 Filter Chain이라는 개념이 있다.


if-else를 줄이는 패턴 중에서 State패턴이 있다.



State패턴과 Chain Of Responsibility 패턴 모두 if-else를 줄이는데 사용된다.

그렇다면 무엇이 다른지, 어떤 상황에서 써야하는지를 알아야 한다.



State패턴의 경우 '상태 전이'라는 개념이 존재한다.

이 말은, 어떤 상태로 변화해야하는지 ConcreteClass에서 알고 있다는 것을 의미한다.


예를 들면 위에서 A-C-E-F-B-A-F 라는 순서로 작업을 해야할 때 State와 Chain Of Responsibility의 차이를 알아보자.


State패턴을 여기에 적용하려면

현재 A라면 그 다음은 반드시 C여야한다.

현재 C라면 그 다음은 반드시 E여야한다.

현재 E라면 그 다음은 반드시 F여야한다.

현재 F라면 그 다음은 반드시 B여야한다.

현재 B라면 그 다음은 반드시 A여야 한다.

그러나, 마지막은 A 다음에 F가 온다.


이는 State A에 해당하는 클래스에 if문을 추가하게 되는 원인이 된다.

왜냐하면, 특정 상태에서 다음 상태로 넘어가는 경우의 수가 2가지(A->C, A->F)이기 때문이다.


만약 A다음 B D E 등도 올 수 있게 수정된다면 그만큼 if문이 늘어나게 된다. 


즉, 규칙이 없는 작업의 리스트가 제공되었을 때, 이를 일괄적으로 처리하려는 경우 그 순서를 알지 못할 확률이 높고, 이를 처리하기 위해서는 if-else가 동반되어 유지보수가 힘들어진다.


디자인 패턴을 적용하는 의미가 없다. 오히려 클래스가 많아져 복잡도만 야기한다.




Chain Of Responsibility Pattern은 특정 작업을 처리할 수 있는 클래스가 체인처럼 연결되어있기 때문에, 어떤 것이 오든 한번만 요청하면 체인을 따라가면서 처리할 수 있는 클래스가 알아서 처리하게 된다.


즉, 규칙이 없는 작업의 순서처리는 훨씬 유리하다. 



그렇다고해서 Chain Of Responsibility Pattern이 무조건 좋은 경우는 아니다.

체인 처럼 엮여있는 것 자체가 여러 번 비교하겠다는 의미가 되고, 이는 어떤 작업의 순서가 올지 모르기 때문이다.

즉, 이 패턴은 여러 번 비교하게 되는 오버헤드를 감수하면서, if-else문을 줄여 유지보수를 향상시키는 패턴이다.



- UML & Sequence Diagram




7. 장단점

장점 :

작업 순서에 상관없이 여러 개의 작업을 일괄적으로 처리할 수 있다.

로직을 클래스화하고, if-else문을 클래스 내부에 가져오기 때문에, 이를 사용하는 클래스에서의 복잡한 if-else문을 줄일 수 있다.



단점 :

체인으로 엮여있기 때문에, 많은 작업에 대해, 처리할 수 있는 클래스가 뒤쪽에 있을 수록 앞쪽 클래스에서 자신이 처리할 수 있는지 값을 비교하는 로직에 의해 오버헤드가 증가할 수 있다.

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

Visitor Pattern  (0) 2018.02.16
Mediator Pattern  (0) 2018.02.16
Iterator Pattern / Iterable Interface  (0) 2018.02.15
Template Method Pattern (기본)  (0) 2017.09.26
Observer Pattern  (0) 2017.09.26
Comments