책임 연쇄 패턴
처리자들을 연결하고 요청을 맡을 수 있는 곳까지 전달하되 미처리 결과도 드러내는 행위 패턴.
· 마지막 업데이트: 2026-10-25 · 7 min read
개요
고객 문의가 청구 문제인지 기술 문제인지에 따라 담당자가 달라진다. 접수 화면이 모든 담당자의 판단을 알고 고르는 대신, 정해 둔 처리 순서를 따라 요청을 넘길 수 있다.
책임 연쇄(Chain of Responsibility)는 처리자가 요청을 맡거나 다음 처리자에게 전달하는 구조다. 이 예제는 처음으로 처리한 곳에서 멈추고, 끝까지 담당자를 찾지 못하면 미처리를 반환한다.
UML
호출부는 첫 Handler에 요청한다. 각 Handler는 다음 Handler를 참조하며, BillingHandler와 TechHandler가 자기 종류의 요청을 처리한다. next 연결은 실행 코드에서 정한다.
역사
GoF는 1994년 Design Patterns에서 Chain of Responsibility를 행위 패턴으로 정리했다. 요청의 발신자를 특정 수신자에 고정하지 않고 처리자들을 따라 전달하는 구조다.1
왜 사용하는가?
접수 화면을 바꾸지 않고 담당 순서나 구성을 조정할 수 있다. 처리할지 넘길지에 관한 판단은 해당 처리자에 남는다.
모든 단계를 반드시 실행하는 파이프라인과는 전달 규칙이 다르다. 체인의 끝에서 무시하지 말고, 미처리를 누가 사용자에게 알릴지도 정해야 한다.
예제
청구 처리자 다음에 기술 처리자를 연결하고 청구 문의 하나를 보낸다.
using System;
using System.Collections.Generic;
using System.Linq;
Handler chain = new BillingHandler(new TechHandler(null));
Reply result = chain.Handle(new Ticket("billing"));
Console.WriteLine(result.Handled + ": " + result.Message);
sealed record Ticket(string Kind);
sealed record Reply(bool Handled, string Message);
// 담당 판단은 처리자에 두고, 넘기는 공통 흐름은 기반 클래스에서 공유한다.
abstract class Handler(Handler? next)
{
public Reply Handle(Ticket ticket)
{
Reply? result = TryHandle(ticket);
if (result is not null) return result;
// null은 이 처리자의 담당이 아니라는 뜻이므로 다음 처리자로 넘긴다.
// 다음 처리자도 없으면 체인 전체의 미처리를 Handled=false로 반환한다.
return next?.Handle(ticket) ?? new Reply(false, "unhandled: " + ticket.Kind);
}
protected abstract Reply? TryHandle(Ticket ticket);
}
// 담당이 아니면 다음 처리자를 선택하지 않고 null만 반환한다.
sealed class BillingHandler(Handler? next) : Handler(next)
{
protected override Reply? TryHandle(Ticket ticket) =>
ticket.Kind == "billing" ? new Reply(true, "billing desk") : null;
}
sealed class TechHandler(Handler? next) : Handler(next)
{
protected override Reply? TryHandle(Ticket ticket) =>
ticket.Kind == "technical" ? new Reply(true, "technical desk") : null;
}
True: billing deskBillingHandler가 처리했으므로 TechHandler까지 넘어가지 않는다.
범위 넓힌 예제
기술 문의와 지원하지 않는 문의를 보낸다. 두 번째 결과는 성공한 척하지 않고 Handled=false로 나온다.
using System;
using System.Collections.Generic;
using System.Linq;
Handler chain = new BillingHandler(new TechHandler(null));
foreach (string kind in new[] { "technical", "other" })
{
Reply result = chain.Handle(new Ticket(kind));
Console.WriteLine(result.Handled + ": " + result.Message);
}
sealed record Ticket(string Kind);
sealed record Reply(bool Handled, string Message);
// 담당 판단은 처리자에 두고, 넘기는 공통 흐름은 기반 클래스에서 공유한다.
abstract class Handler(Handler? next)
{
public Reply Handle(Ticket ticket)
{
Reply? result = TryHandle(ticket);
if (result is not null) return result;
// null은 이 처리자의 담당이 아니라는 뜻이므로 다음 처리자로 넘긴다.
// 다음 처리자도 없으면 체인 전체의 미처리를 Handled=false로 반환한다.
return next?.Handle(ticket) ?? new Reply(false, "unhandled: " + ticket.Kind);
}
protected abstract Reply? TryHandle(Ticket ticket);
}
// 담당이 아니면 다음 처리자를 선택하지 않고 null만 반환한다.
sealed class BillingHandler(Handler? next) : Handler(next)
{
protected override Reply? TryHandle(Ticket ticket) =>
ticket.Kind == "billing" ? new Reply(true, "billing desk") : null;
}
sealed class TechHandler(Handler? next) : Handler(next)
{
protected override Reply? TryHandle(Ticket ticket) =>
ticket.Kind == "technical" ? new Reply(true, "technical desk") : null;
}
True: technical desk
False: unhandled: other순서를 바꾸면 여러 처리자가 맡을 수 있는 요청의 담당자가 달라질 수 있다. 이 예제의 체인은 두 단계로 끝나며, 동적으로 연결할 때는 순환 연결을 막아야 한다.
TypeScript 예제
접수 코드는 첫 처리자에게 요청만 보내고, 각 처리자는 자기 담당인지 판단한다. 담당 선택을 접수 화면에서 분리해 처리 순서와 구성을 바꾸기 쉽게 만든다.
type Ticket = Readonly<{ kind: string }>;
type Reply = Readonly<{ handled: boolean; message: string }>;
// 담당 판단은 처리자에 두고, 넘기는 공통 흐름은 기반 클래스에서 공유한다.
abstract class Handler {
constructor(private readonly next: Handler | null) {}
handle(ticket: Ticket): Reply {
const reply = this.tryHandle(ticket);
if (reply !== null) return reply;
return this.next?.handle(ticket) ?? { handled: false, message: "unhandled: " + ticket.kind };
}
protected abstract tryHandle(ticket: Ticket): Reply | null;
}
// 담당이 아니면 다음 처리자를 선택하지 않고 null만 반환한다.
class BillingHandler extends Handler {
protected override tryHandle(ticket: Ticket): Reply | null {
return ticket.kind === "billing" ? { handled: true, message: "billing desk" } : null;
}
}
class TechHandler extends Handler {
protected override tryHandle(ticket: Ticket): Reply | null {
return ticket.kind === "technical" ? { handled: true, message: "technical desk" } : null;
}
}
const chain = new BillingHandler(new TechHandler(null));
for (const kind of ["billing", "technical", "other"]) {
const result = chain.handle({ kind });
console.log(result.handled + ": " + result.message);
}true: billing desk
true: technical desk
false: unhandled: othernull은 현재 처리자가 맡지 않았다는 뜻이고, handled=false는 체인 끝의 최종 결과다. 이 예제는 처음 처리한 곳에서 멈춘다.
같이 보기
C# 예제는 각각 독립된 .NET 10 콘솔 프로그램이다.
TypeScript 예제는 ES2022 이상으로 컴파일한 JavaScript를 Node.js에서 실행한다.