책임 연쇄 패턴
개요
고객 문의가 청구 문제인지 기술 문제인지에 따라 담당자가 달라진다. 접수 화면이 모든 담당자의 판단을 알고 고르는 대신, 정해 둔 처리 순서를 따라 요청을 넘길 수 있다.
책임 연쇄(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;
return next?.Handle(ticket) ?? new Reply(false, "unhandled: " + ticket.Kind);
}
protected abstract Reply? TryHandle(Ticket ticket);
}
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;
return next?.Handle(ticket) ?? new Reply(false, "unhandled: " + ticket.Kind);
}
protected abstract Reply? TryHandle(Ticket ticket);
}
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순서를 바꾸면 여러 처리자가 맡을 수 있는 요청의 담당자가 달라질 수 있다. 이 예제의 체인은 두 단계로 끝나며, 동적으로 연결할 때는 순환 연결을 막아야 한다.
같이 보기
각 예제는 독립된 .NET 10 콘솔 프로그램이다.