반응형
Chain of Responsibility Pattern
- 책임 연쇄 패턴 (chain of responsibility) 이란?
- 클라이언트의 요청에 대한 세세한 처리를 한 객체가 전부 하는 것이 아닌, 여러 개의 처리 객체들로 나누고 이들을 사슬(chain) 처럼 연결해 연쇄적으로 처리하는 행동 패턴
- Handler (핸들러) : 처리 객체를 지칭, 요청 받으면 각 핸들러는 요청을 처리할 수 있는지 판단하고, 없으면 다음 핸들러로 책임을 전가
- 클라이언트의 요청에 대한 세세한 처리를 한 객체가 전부 하는 것이 아닌, 여러 개의 처리 객체들로 나누고 이들을 사슬(chain) 처럼 연결해 연쇄적으로 처리하는 행동 패턴
- 책임 연쇄 패턴이 필요한 상황
- 온라인 주문 시스템을 개발하려고 하는 예시에서,
- 인증된 사용자들만 주문을 생성할 수 있도록 시스템에 대한 접근을 제한
- 관리 권한이 있는 사용자들에게는 모든 주문에 대한 전체 접근 권한을 부여
- 이러한 검사들을 차례대로 수행하도록 구성했다고 가정
- 검사 조건을 추가하는 상황
- 당신은 요청 내의 데이터를 정제(sanitize) 하는 추가 유효성 검사 단계를 추가
- 시스템이 무차별 대입 공격을 방어하기 위해 같은 IP 주소에서 오는 반복적으로 실패한 요청을 걸러내는 검사를 즉시 추가
- 데이터가 포함된 반복 요청에 대해 캐시된 결과를 반환하여 시스템 속도를 개선
- 온라인 주문 시스템을 개발하려고 하는 예시에서,
- 책임 연쇄 패턴의 아이디어
- 특정 행동들을 핸들러라는 독립 실행형 객체로 변환
- 핸들러를 체인으로 연결해서 체인을 따라 요청을 처리할 수 있도록 함
- 경우에 따라서는 핸들러가 요청을 체인 뒤로 더이상 전달 하지 않고 추가 처리를 중지하는 결정을 할 수도 있음
- 책임 연쇄 패턴의 구조
- 책임 연쇄 패턴 예시 코드
class Client{ public static void main(String[] args){ String url1 = "http://www.youtube.com:80"; System.out.println("INPUT: " + url1); URLParser.run(url1); String url2 = "https://www.inpa.tistory.com:443"; System.out.println("INPUT: " + url2); URLParser.run(url1); String url3 = "http://localhost:8080"; System.out.println("INPUT: " + url3); URLParser.run(url1); } }
class URLParser {
public static void run(String url) {
// Protocol 처리
int index = url.indexOf("://");
if (index != -1) System.out.println("PROTOCOL: " + url.substring(0, index));
else System.out.println("NO PROTOCOL");
// Domain 처리
int startIndex= url.indexOf("://");
int lastIndex= url.lastIndexOf(":");
System.out.print("DOMAIN : ");
if (startIndex== -1) {
if (lastIndex== -1) System.out.println(url);
else System.out.println(url.substring(0, lastIndex));
} else if (startIndex!= lastIndex) {
System.out.println(url.substring(startIndex+ 3, lastIndex));
} else {
System.out.println(url.substring(startIndex+ 3));
}
// Port 처리
int index2 = url.lastIndexOf(":");
if (index2 != -1) {
String strPort= url.substring(index2 + 1);
try {
int port = Integer.parseInt((strPort));
System.out.println("PORT : " + port);
} catch (NumberFormatExceptione) {
e.printStackTrace();
}
}
}
}
abstract class Handler {
protected Handler nextHandler = null;
public Handler setNext(Handler handler) {
this.nextHandler = handler;
return handler;
}
public void run(String url) {
process(url);
if (nextHandler != null) {
nextHandler.run(url);
}
}
protected abstract void process(String url);
}
class ProtocolHandler extends Handler {
@Override
protected void process(String url) {
int index = url.indexOf("://");
if (index != -1) {
System.out.println("PROTOCOL: " + url.substring(0, index));
} else {
System.out.println("NO PROTOCOL");
}
}
}
class DomianHandler extends Handler {
@Override
protected void process(String url) {
int startIndex = url.indexOf("://");
int lastIndex = url.lastIndexOf(":");
System.out.print("DOMAIN : ");
if (startIndex == -1) {
if (lastIndex == -1) System.out.println(url);
else System.out.println(url.substring(0, lastIndex));
} else if (startIndex != lastIndex) {
System.out.println(url.substring(startIndex + 3, lastIndex));
} else {
System.out.println(url.substring(startIndex + 3));
}
}
}
class PortHandler extends Handler {
@Override
protected void process(String url) {
int index = url.lastIndexOf(":");
if (index != -1) {
String strPort = url.substring(index + 1);
try {
int port = Integer.parseInt((strPort));
System.out.println("PORT : " + port);
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
}
}
class Client{
public static void main(String[] args){
Handler handler1 = new ProtocolHandler();
Handler handler2 = new DomainHandler();
Handler handler3 = new PortHandler();
handler1.setNext(handler2).setNext(handler3);
String url1 = "http://www.youtube.com:80";
System.out.prinln("INPUT: " + url1);
handler1.run(url1);
String url2 = "https://www.inpa.tistory.com:443";
System.out.prinln("INPUT: " + url2);
handler1.run(url2);
}
}
- 사용 시기
- 특정 요청을 2개 이상의 여러 객체에서 판별하고 처리해야 할 때
- 특정 순서로 여러 핸들러를 실행해야 하는 경우
- 프로그램이 다양한 방식과 종류의 요청을 처리할 것으로 예상되나, 요청 유형과 순서를 미리 알 수 없는 경우
- 요청을 처리할 수 있는 객체 집합이 동적(런타임)으로 정의되어야 할 때
- 장점
- 다형성
- 클라이언트는 처리 객체의 체인 집합 내부의 구조를 알 필요가 없음
- 각각의 체인은 자신이 해야하는 일만 하기 때문에 새로운 요청 처리에 유연하게 확장 가능
- OCP 준수
- 클라이언트 코드를 변경하지 않고 체인을 동적으로 변경 가능
- 단점
- 실행 시에 코드의 흐름이 많아져서 과정을 살펴보기가 복잡함
- 무한 사이클로 체인이 구성되면 무한 루프에 빠질 수 있음
- 책임 연쇄로 인한 처리 지연 문제가 발생할 수 있음
Command Pattern
- 명령 패턴이란?
- 요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 재사용성을 높인 행동 패턴
- 요청을 보내는 쪽 (invoker)과 받는 쪽 (receiver) 을 커맨드를 이용하여 디커플링 하여 재사용을 높이고자 함
- 요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 재사용성을 높인 행동 패턴
- 명령 패턴이 필요한 상황
- 텍스트 편집기 앱을 개발하고 있는 상황에서, 편집기의 다양한 작업을 위한 여러 버튼이 있는 도구 모음(toolbar) 를 만들고 있다고 하자.
- 도구 모음의 버튼과 다양한 대화 상자들의 일반 버튼에 사용할 수 있는 Button 클래스를 생성
- 버튼의 모양은 비슷해 보이지만 각각 다른 기능을 수행해야 함
- 요청을 처리하기 위한 함수를 자식 클래스로 상속하고 오버라이딩하는 구조로 구성해야 한다면 기능별로 상속을 다 해주어야 함
- 이렇게 되면 Button 클래스를 수정할 때마다 자식 클래스가 영향을 받음
- 동일한 기능을 하는 로직이 버튼이 아닌 다른 형태로 표현될 때 해당 기능을 copy & paste 해야 함
- 로직 수정 시 관련 모든 클래스 수정해주어야 함
- 텍스트 편집기 앱을 개발하고 있는 상황에서, 편집기의 다양한 작업을 위한 여러 버튼이 있는 도구 모음(toolbar) 를 만들고 있다고 하자.
- 명령 패턴의 아이디어
- 인터페이스 객체들이 요청을 직접 보내는 것이 아닌 Command 를 경유해서 보내자.
- 명령 패턴에 의해 수정된 구조
- 더 이상 다양한 클릭 행동들을 구현하기 위한 버튼의 여러 자식 클래스는 필요하지 않음
- 커맨드는 사용자 인터페이스와 비즈니스 로직 레이어 간의 결합도를 줄이는 중간 레이어의 역할을 함
- 명령 패턴의 구조
- 명령 패턴이 적용되기 전의 예시 코드
// Receiver: 실제 작업을 수행하는 클래스 public class Light { private boolean isOn; public void on() { System.out.println("불을 켭니다."); this.isOn = true; } public void off() { System.out.println("불을 끕니다."); this.isOn = false; } public boolean isOn() { return this.isOn; } } public class Game { private boolean isStarted; public void start() { System.out.println("게임을 시작합니다."); this.isStarted = true; } public void end() { System.out.println("게임을 종료합니다."); this.isStarted = false; } public boolean isStarted() { return this.isStarted; } } // Invoker: 명령을 실행하는 클래스 public class Button { private Light light; public Button(Light light) { this.light = light; } public void press() { light.off(); // 특정 행동에 하드코딩되어 있음 } } // Main: 클라이언트 코드 public class MyApp { public static void main(String[] args) { Button button = new Button(new Light()); button.press(); // 불을 끕니다. button.press(); // 불을 끕니다. } }
- 명령 패턴 적용 예시
// Command: 명령의 인터페이스 public interface Command { void execute(); } // LightOnCommand: Light를 켜는 명령 public class LightOnCommand implements Command { private Light light; public LightOnCommand(Light light) { this.light = light; } @Override public void execute() { light.on(); } } // LightOffCommand: Light를 끄는 명령 public class LightOffCommand implements Command { private Light light; public LightOffCommand(Light light) { this.light = light; } @Override public void execute() { light.off(); } } // GameStartCommand: Game을 시작하는 명령 public class GameStartCommand implements Command { private Game game; public GameStartCommand(Game game) { this.game = game; } @Override public void execute() { game.start(); } } // GameEndCommand: Game을 종료하는 명령 public class GameEndCommand implements Command { private Game game; public GameEndCommand(Game game) { this.game = game; } @Override public void execute() { game.end(); } } public class Button{ public void press(Command command){ command.execute(); } } Button button = new Button(); button.press(new GameStartCommand(new Game())); button.press(new LightOnCommand(new Light()));
- 장점
- SRP, OCP 준수
- Invoker 와 Receiver 로 책임을 분리하고, 기존 클라이언트 코드에 변경 없이 새로운 커맨드 도입 가능
- 클라이언트와 수신자 간의 결합 감소
- 클라이언트는 어떤 커맨드가 어떤 객체에서 어떻게 실행되는지 세부사항을 알 필요가 없음
- 명령 기록과 실행 취소/재실행(undo/redo)
- Invoker 내부에 stack 을 사용하여 command history 관리 가능
- SRP, OCP 준수
- 단점
- Invoker 와 Receiver 사이에 완전히 새로운 레이어(command) 를 도입하기 때문에 코드가 더 복잡해진다.
반응형