책임 연쇄 패턴
"오늘의AI위키"의 AI를 통해 더욱 풍부하고 폭넓은 지식 경험을 누리세요.
1. 개요
책임 연쇄 패턴은 여러 객체가 요청을 처리하도록 하는 디자인 패턴이다. 요청을 보낸 객체는 어떤 객체가 요청을 처리하는지 알지 못하며, 요청은 객체들의 체인을 따라 전달된다. 이 패턴은 객체 간의 결합도를 낮추고, 요청 처리의 유연성을 높이는 데 기여한다. 이 패턴은 다양한 프로그래밍 언어와 프레임워크에서 구현될 수 있으며, 특히 Cocoa와 Cocoa Touch 프레임워크에서 이벤트 처리에 널리 사용된다.
더 읽어볼만한 페이지
- 소프트웨어 디자인 패턴 - 모델-뷰-컨트롤러
모델-뷰-컨트롤러(MVC)는 소프트웨어 디자인 패턴으로, 응용 프로그램을 모델, 뷰, 컨트롤러 세 가지 요소로 분리하여 개발하며, 사용자 인터페이스 개발에서 데이터, 표현 방식, 사용자 입력 처리를 분리해 유지보수성과 확장성을 높이는 데 기여한다. - 소프트웨어 디자인 패턴 - 스케줄링 (컴퓨팅)
스케줄링은 운영 체제가 시스템의 목적과 환경에 맞춰 작업을 관리하는 기법으로, 장기, 중기, 단기 스케줄러를 통해 프로세스를 선택하며, CPU 사용률, 처리량 등을 기준으로 평가하고, FCFS, SJF, RR 등의 알고리즘을 사용한다.
| 책임 연쇄 패턴 | |
|---|---|
| 개요 | |
| 유형 | 행위 디자인 패턴 |
| 목적 | 요청을 처리할 수 있는 객체의 체인을 만들어 요청을 보낸다. |
| 동기 | 요청을 보낸 객체와 요청을 처리할 객체 사이의 결합도를 줄인다. |
| 적용 가능성 | 요청을 처리할 객체를 미리 알 수 없는 경우 여러 객체가 요청을 처리할 수 있고, 누가 처리할지 자동으로 결정해야 하는 경우 요청을 처리하는 객체 집합을 동적으로 지정해야 하는 경우 |
| 구조 | Handler: 요청을 처리하는 인터페이스를 정의한다. ConcreteHandler: 요청을 처리할 수 있으면 처리하고, 그렇지 않으면 다음 핸들러에게 전달한다. Client: 요청을 핸들러 체인에 보낸다. |
| 참여자 | Handler: 요청을 처리하는 인터페이스를 정의한다. ConcreteHandler: 요청을 처리할 수 있으면 처리하고, 그렇지 않으면 다음 핸들러에게 전달한다. Client: 요청을 핸들러 체인에 보낸다. |
| 협업 | 클라이언트는 요청을 체인의 첫 번째 핸들러에게 보낸다. |
| 결과 | 요청을 보낸 객체와 요청을 처리할 객체 사이의 결합도를 줄인다. 시스템에 유연성을 더한다. 요청 처리 흐름을 쉽게 추가하거나 변경할 수 있다. 요청이 처리될 것이라는 보장이 없다. |
| 구현 | 핸들러 체인을 구성하는 방법 요청 전달 방법 |
| 예제 | 자바의 서블릿 필터 자바의 로거 .NET의 HTTP 핸들러 |
| 알려진 용도 | 서블릿 필터 로거 HTTP 핸들러 |
| 관련 패턴 | 커맨드 패턴 중재자 패턴 옵서버 패턴 |
2. 역사적 배경
책임 연쇄 패턴[2]은 구현, 변경, 테스트 및 재사용이 더 쉬운 객체 지향 소프트웨어를 설계할 때 발생하는 반복적인 설계 문제에 대한 일반적인 솔루션들을 모은 23가지의 잘 알려진 ''GoF 디자인 패턴'' 중 하나이다.
3. 구조

책임 연쇄 패턴에서 `Sender`는 특정 수신자를 직접 참조하지 않고, `Handler` 인터페이스를 통해 요청을 처리한다. `Receiver1`, `Receiver2`, `Receiver3` 클래스들은 런타임 조건에 따라 요청을 처리하거나 다음 수신자에게 전달하는 방식으로 `Handler` 인터페이스를 구현한다.[4]
3. 1. UML 클래스 다이어그램
위의 UML 클래스 다이어그램에서 `Sender` 클래스는 특정 수신자 클래스를 직접 참조하지 않는다. 대신, `Sender`는 요청을 처리하기 위해 `Handler` 인터페이스를 참조한다( `handler.handleRequest()` ). 이로 인해 `Sender`는 어떤 수신자가 요청을 처리하는지와 독립적으로 작동한다. `Receiver1`, `Receiver2`, `Receiver3` 클래스는 런타임 조건에 따라 요청을 처리하거나 전달하여 `Handler` 인터페이스를 구현한다.
UML 시퀀스 다이어그램은 런타임 상호 작용을 보여준다. 이 예에서 `Sender` 객체는 `receiver1` 객체( `Handler` 타입)에 대해 `handleRequest()`를 호출한다. `receiver1`은 요청을 `receiver2`로 전달하고, `receiver2`는 다시 `receiver3`으로 전달하여 요청을 처리(수행)한다.
3. 2. UML 시퀀스 다이어그램
UML 시퀀스 다이어그램은 런타임 상호 작용을 보여준다. 이 예에서 `Sender` 객체는 `receiver1` 객체(`Handler` 타입)에 대해 `handleRequest()`를 호출한다. `receiver1`은 요청을 `receiver2`로 전달하고, `receiver2`는 다시 `receiver3`으로 전달하여 요청을 처리(수행)한다.
4. 예제
Java, C++, PHP를 이용한 책임 연쇄 패턴의 예제가 있다.[1][5]
4. 1. Java
아래의 Java 코드는 로그를 남겨주는 클래스를 보여준다. 각각의 로그 핸들러들은 로그 레벨과 다음 로그 핸들러로 넘겨주는 기능을 가지고 있다.[1]:Writing to stdout: Entering function y.
:Writing to stdout: Step1 completed.
:Sending via e-mail: Step1 completed.
:Writing to stdout: An error has occurred.
:Sending via e-mail: An error has occurred.
:Writing to stderr: An error has occurred.
이 예가 실제로 어떻게 로그 클래스를 남겨야 하는지에 대한 추천은 아니라는 것을 명심해야 한다.[1]
뿐만 아니라, 완벽한 책임 연쇄 패턴을 구현하기 위해서 로거는 메시지를 넘긴 이후 하위 체인으로 메시지를 전달할 수 없다. 이 예에서 메시지가 처리되는지 여부와 관계없이 하위 체인으로 전달된다.[1]
```java
abstract class Logger {
public static int ERR = 3;
public static int NOTICE = 5;
public static int DEBUG = 7;
protected int mask;
// The next element in the chain of responsibility
protected Logger next;
public Logger setNext(Logger log) {
next = log;
return log;
}
public void message(String msg, int priority) {
if (priority <= mask) {
writeMessage(msg);
}
if (next != null) {
next.message(msg, priority);
}
}
abstract protected void writeMessage(String msg);
}
class StdoutLogger extends Logger {
public StdoutLogger(int mask) {
this.mask = mask;
}
protected void writeMessage(String msg) {
System.out.println("Writing to stdout: " + msg);
}
}
class EmailLogger extends Logger {
public EmailLogger(int mask) {
this.mask = mask;
}
protected void writeMessage(String msg) {
System.out.println("Sending via email: " + msg);
}
}
class StderrLogger extends Logger {
public StderrLogger(int mask) {
this.mask = mask;
}
protected void writeMessage(String msg) {
System.err.println("Sending to stderr: " + msg);
}
}
public class ChainOfResponsibilityExample {
public static void main(String[] args) {
// Build the chain of responsibility
Logger logger, logger1;
logger1 = logger = new StdoutLogger(Logger.DEBUG);
logger1 = logger1.setNext(new EmailLogger(Logger.NOTICE));
logger1 = logger1.setNext(new StderrLogger(Logger.ERR));
// Handled by StdoutLogger
logger.message("Entering function y.", Logger.DEBUG);
// Handled by StdoutLogger and EmailLogger
logger.message("Step1 completed.", Logger.NOTICE);
// Handled by all three loggers
logger.message("An error has occurred.", Logger.ERR);
}
}
```
아래는 이 패턴을 Java로 구현한 다른 예제이다. 이 예에서 각각의 위임자와 제한을 각각 다르게 가지고 있다. 매번 역할에 참가하고 있는 사용자는 요청을 수신받고, 상한선과 위임자를 통과한 요청을 보내준다.[1]
아래는 추상 메소드 processRequest를 포함한 PurchasePower 추상 클래스이다.[1]
```java
abstract class PurchasePower {
protected final double base = 500;
protected PurchasePower successor;
public void setSuccessor(PurchasePower successor) {
this.successor = successor;
}
abstract public void processRequest(PurchaseRequest request);
}
```
Manager, Director, Vice President, President는 위 추상 클래스의 네 가지 구현체이다.[1]
```java
class ManagerPower extends PurchasePower {
private final double ALLOWABLE = 10 * base;
public void processRequest(PurchaseRequest request) {
if (request.getAmount() < ALLOWABLE) {
System.out.println("Manager will approve $" + request.getAmount());
} else if (successor != null) {
successor.processRequest(request);
}
}
}
class DirectorPPower extends PurchasePower {
private final double ALLOWABLE = 20 * base;
public void processRequest(PurchaseRequest request) {
if (request.getAmount() < ALLOWABLE) {
System.out.println("Director will approve $" + request.getAmount());
} else if (successor != null) {
successor.processRequest(request);
}
}
}
class VicePresidentPPower extends PurchasePower {
private final double ALLOWABLE = 40 * base;
public void processRequest(PurchaseRequest request) {
if (request.getAmount() < ALLOWABLE) {
System.out.println("Vice President will approve $" + request.getAmount());
} else if (successor != null) {
successor.processRequest(request);
}
}
}
class PresidentPPower extends PurchasePower {
private final double ALLOWABLE = 60 * base;
public void processRequest(PurchaseRequest request) {
if (request.getAmount() < ALLOWABLE) {
System.out.println("President will approve $" + request.getAmount());
} else {
System.out.println( "Your request for $" + request.getAmount() + " needs a board meeting!");
}
}
}
```
PurchaseRequest 클래스는 요청 데이터를 유지하며 Getter 메소드를 가지고 있다.[1]
```java
class PurchaseRequest {
private int number;
private double amount;
private String purpose;
public PurchaseRequest(int number, double amount, String purpose) {
this.number = number;
this.amount = amount;
this.purpose = purpose;
}
public double getAmount() {
return amount;
}
public void setAmount(double amt) {
amount = amt;
}
public String getPurpose() {
return purpose;
}
public void setPurpose(String reason) {
purpose = reason;
}
public int getNumber(){
return number;
}
public void setNumber(int num) {
number = num;
}
}
```
다음은 사용 예시이다. 승계는 Manager -> Director -> Vice President -> President 순서로 설정되어 있다.[1]
```java
class CheckAuthority {
public static void main(String[] args) {
ManagerPPower manager = new ManagerPPower();
DirectorPPower director = new DirectorPPower();
VicePresidentPPower vp = new VicePresidentPPower();
PresidentPPower president = new PresidentPPower();
manager.setSuccessor(director);
director.setSuccessor(vp);
vp.setSuccessor(president);
// Press Ctrl+C to end.
try {
while (true) {
System.out.println("Enter the amount to check who should approve your expenditure.");
System.out.print(">");
double d = Double.parseDouble(new BufferedReader(new InputStreamReader(System.in)).readLine());
manager.processRequest(new PurchaseRequest(0, d, "General"));
}
} catch(Exception e) {
System.exit(1);
}
}
}
```
다음은 Java 코드를 사용하여 로깅 클래스의 예를 통해 이 패턴을 보여주는 예시이다. 각 로깅 핸들러는 로그 레벨에 따라 어떠한 동작을 해야 하는지 결정하고, 다음 로깅 핸들러에게 메시지를 전달한다.[1]
출력:[1]
:Writing to debug output: Entering function y.
:Writing to debug output: Step1 completed.
:Sending via e-mail: Step1 completed.
:Writing to debug output: An error has occurred.
:Sending via e-mail: An error has occurred.
:Writing to stderr: An error has occurred.
이 예시는 로깅 클래스를 이런 방식으로 작성하는 것을 권장하는 것은 아니며, 또한 CoR의 "순수한" 구현에서는 로거가 메시지를 처리한 후에 책임을 후단으로 전달하지 않는다. 이 예시에서는 처리 여부와 관계없이 메시지가 체인의 다음으로 전달된다.[1]
```java
import java.util.*;
abstract class Logger {
public static final int ERROR = 3;
public static final int NOTICE = 5;
public static final int DEBUG = 7;
private final int mask;
protected Logger(int mask) { this.mask = mask; }
// The next element in the chain of responsibility
protected Logger next;
public Logger setNext(Logger l) {
next = l;
return this;
}
public void message(String msg, int priority) {
if (priority <= mask) {
writeMessage(msg);
if (next != null) {
next.message(msg, priority);
}
}
}
abstract protected void writeMessage(String msg);
}
class StdoutLogger extends Logger {
public StdoutLogger(int mask) { super(mask); }
@Override
protected void writeMessage(String msg) {
System.out.println("Writting to stdout: " + msg);
}
}
class EmailLogger extends Logger {
public EmailLogger(int mask) { super(mask); }
@Override
protected void writeMessage(String msg) {
System.out.println("Sending via email: " + msg);
}
}
class StderrLogger extends Logger {
public StderrLogger(int mask) { super(mask); }
@Override
protected void writeMessage(String msg) {
System.out.println("Sending to stderr: " + msg);
}
}
public class ChainOfResponsibilityExample {
public static void main(String[] args) {
// Build the chain of responsibility
final Logger l =
new StdoutLogger(Logger.DEBUG).setNext(
new EmailLogger(Logger.NOTICE).setNext(
new StderrLogger(Logger.ERROR)));
// Handled by StdoutLogger
l.message("Entering function y.", Logger.DEBUG);
// Handled by StdoutLogger and EmailLogger
l.message("Step1 completed.", Logger.NOTICE);
// Handled by all three loggers
l.message("An error has occurred.", Logger.ERROR);
}
}
4. 2. C++
이 C++11 구현은 책에 있는 C++98 이전 구현을 기반으로 한다.[5]```c++
#include
#include
typedef int Topic;
constexpr Topic NO_HELP_TOPIC = -1;
// 요청을 처리하기 위한 인터페이스를 정의한다.
class HelpHandler { // Handler
public:
HelpHandler(HelpHandler* h = nullptr, Topic t = NO_HELP_TOPIC)
: successor(h), topic(t) {}
virtual bool hasHelp() {
return topic != NO_HELP_TOPIC;
}
virtual void setHandler(HelpHandler*, Topic) {}
virtual void handleHelp() {
std::cout << "HelpHandler::handleHelp\n";
// (선택 사항) 승계자 링크를 구현한다.
if (successor != nullptr) {
successor->handleHelp();
}
}
virtual ~HelpHandler() = default;
HelpHandler(const HelpHandler&) = delete; // rule of three
HelpHandler& operator=(const HelpHandler&) = delete;
private:
HelpHandler* successor;
Topic topic;
};
class Widget : public HelpHandler {
public:
Widget(const Widget&) = delete; // rule of three
Widget& operator=(const Widget&) = delete;
protected:
Widget(Widget* w, Topic t = NO_HELP_TOPIC)
: HelpHandler(w, t), parent(nullptr) {
parent = w;
}
private:
Widget* parent;
};
// 책임이 있는 요청을 처리한다.
class Button : public Widget { // ConcreteHandler
public:
Button(std::shared_ptr
virtual void handleHelp() {
// ConcreteHandler가 요청을 처리할 수 있다면 그렇게 하고, 그렇지 않으면 승계자에게 요청을 전달한다.
std::cout << "Button::handleHelp\n";
if (hasHelp()) {
// 책임이 있는 요청을 처리한다.
} else {
// 승계자에 접근할 수 있다.
HelpHandler::handleHelp();
}
}
};
class Dialog : public Widget { // ConcreteHandler
public:
Dialog(std::shared_ptr
setHandler(h.get(), t);
}
virtual void handleHelp() {
std::cout << "Dialog::handleHelp\n";
// Dialog가 재정의하는 Widget 연산...
if(hasHelp()) {
// 대화 상자에 대한 도움말을 제공한다.
} else {
HelpHandler::handleHelp();
}
}
};
class Application : public HelpHandler {
public:
Application(Topic t) : HelpHandler(nullptr, t) {}
virtual void handleHelp() {
std::cout << "Application::handleHelp\n";
// 도움말 항목 목록을 표시한다.
}
};
int main() {
constexpr Topic PRINT_TOPIC = 1;
constexpr Topic PAPER_ORIENTATION_TOPIC = 2;
constexpr Topic APPLICATION_TOPIC = 3;
// 스마트 포인터는 메모리 누수를 방지한다.
std::shared_ptr
std::shared_ptr
5. 응용 사례
Cocoa 및 Cocoa Touch 프레임워크는 OS X 및 iOS 응용 프로그램 개발에 사용되며, 이벤트 처리를 위해 책임 연쇄 패턴을 사용한다.
5. 1. Cocoa 및 Cocoa Touch
Cocoa 및 Cocoa Touch 프레임워크는 각각 OS X 및 iOS 응용 프로그램에 사용되며, 이들은 이벤트를 처리하기 위해 책임 연쇄 패턴을 적극적으로 사용한다. 연쇄에 참여하는 객체를 '응답자' 객체라고 하며,NSResponder (OS X)/UIResponder (iOS) 클래스에서 상속받는다. 모든 뷰 객체(NSView/UIView), 뷰 컨트롤러 객체(NSViewController/UIViewController), 윈도우 객체(NSWindow/UIWindow) 및 응용 프로그램 객체(NSApplication/UIApplication)는 응답자 객체이다.일반적으로 뷰가 처리할 수 없는 이벤트를 수신하면, 해당 이벤트는 상위 뷰로 전달되며 뷰 컨트롤러나 윈도우 객체에 도달할 때까지 계속된다. 윈도우가 이벤트를 처리할 수 없는 경우, 이벤트는 연쇄의 마지막 객체인 응용 프로그램 객체로 전달된다. 예를 들어 다음과 같다.
- OS X에서, 마우스로 텍스처가 있는 윈도우를 이동하는 경우 슬라이더 컨트롤과 같이 드래그 이벤트를 처리하는 뷰가 해당 위치에 있지 않으면, 어느 위치에서든 이동할 수 있다(제목 표시줄뿐 아니라). 그러한 뷰(또는 상위 뷰)가 없으면 드래그 이벤트는 드래그 이벤트를 처리하는 윈도우로 연쇄를 따라 전송된다.
- iOS에서 뷰 자체를 서브클래싱하는 대신 뷰 계층 구조를 관리하는 뷰 컨트롤러에서 뷰 이벤트를 처리하는 것이 일반적이다. 뷰 컨트롤러는 관리되는 모든 하위 뷰 다음에 응답자 연쇄에 위치하므로, 모든 뷰 이벤트를 가로채서 처리할 수 있다.
6. 장점 및 단점
책임 연쇄 패턴은 유연하고 재사용 가능한 객체 지향 소프트웨어를 설계할 때 자주 발생하는 설계 문제에 대한 일반적인 해결책을 제시하는 23가지 GoF 디자인 패턴 중 하나이다.[2] 이 패턴은 구현, 변경, 테스트 및 재사용이 더 쉬운 객체를 만드는 것을 목표로 한다.
책임 연쇄 패턴은 다음을 가능하게 한다.
- 요청을 보내는 측과 받는 측의 결합을 피한다.
- 둘 이상의 수신자가 요청을 처리할 수 있도록 한다.
요청을 보내는 클래스 내에서 직접 요청을 처리하도록 구현하면 특정 수신자에 결합되어 여러 수신자를 지원하기 어렵기 때문에 유연성이 떨어진다.[3]
책임 연쇄 패턴에서는 런타임 조건에 따라 요청을 처리하거나 체인 상의 다음 수신자(있는 경우)에게 전달하는 책임을 가진 수신자 객체의 체인을 정의한다. 이를 통해 어떤 수신자가 요청을 처리하는지 알 필요 없이 수신자 체인에 요청을 보낼 수 있다. 요청은 수신자가 요청을 처리할 때까지 체인을 따라 전달되며, 요청을 보낸 측은 더 이상 특정 수신자에 결합되지 않는다.
참조
[1]
웹사이트
Chain of Responsibility Design Pattern
https://web.archive.[...]
2013-11-08
[2]
서적
Design Patterns: Elements of Reusable Object-Oriented Software
https://archive.org/[...]
Addison Wesley
[3]
웹사이트
The Chain of Responsibility design pattern - Problem, Solution, and Applicability
http://w3sdesign.com[...]
2017-08-12
[4]
웹사이트
The Chain of Responsibility design pattern - Structure and Collaboration
http://w3sdesign.com[...]
2017-08-12
[5]
서적
Design Patterns: Elements of Reusable Object-Oriented Software
Addison Wesley
본 사이트는 AI가 위키백과와 뉴스 기사,정부 간행물,학술 논문등을 바탕으로 정보를 가공하여 제공하는 백과사전형 서비스입니다.
모든 문서는 AI에 의해 자동 생성되며, CC BY-SA 4.0 라이선스에 따라 이용할 수 있습니다.
하지만, 위키백과나 뉴스 기사 자체에 오류, 부정확한 정보, 또는 가짜 뉴스가 포함될 수 있으며, AI는 이러한 내용을 완벽하게 걸러내지 못할 수 있습니다.
따라서 제공되는 정보에 일부 오류나 편향이 있을 수 있으므로, 중요한 정보는 반드시 다른 출처를 통해 교차 검증하시기 바랍니다.
문의하기 : help@durumis.com