276. 책임 연쇄 (Chain of Responsibility) - 요청을 처리할 수 있는 객체를 찾을 때까지 고리 전달
핵심 인사이트 (3줄 요약)
- 본질: 책임 연쇄(Chain of Responsibility) 패턴은 클라이언트의 요청을 처리할 수 있는 여러 객체들(Handler)을 체인(고리)처럼 연결해 두고, 첫 번째 객체가 처리를 못하면 다음 객체로 책임을 떠넘기는(Pass) 방식으로 동작하는 행동 패턴이다.
- 가치: 요청을 보내는 쪽(Sender)과 처리하는 쪽(Receiver) 사이의 강한 결합을 끊어내고, 런타임에 처리자(Handler)의 순서를 바꾸거나 새로운 처리자를 동적으로 추가/삭제할 수 있는 엄청난 유연성을 제공한다.
- 융합: 웹 개발에서 가장 많이 쓰이는 미들웨어(Middleware) 체인, 필터(Filter) 파이프라인, 로깅 시스템, 그리고 예외 처리(Try-Catch) 메커니즘의 아키텍처적 근간이 되는 패턴이다.
Ⅰ. 개요 및 필요성 (Context & Necessity)
-
개념: 책임 연쇄 패턴은 요청(Request)을 처리할 수 있는 여러 핸들러(Handler)들을 단방향 링크드 리스트(Linked List) 형태로 연결하여, 요청이 들어오면 자신이 처리할지 말지 결정하고, 못하면
nextHandler.handle()을 호출해 책임을 토스(Toss)하는 기법이다. -
필요성: 회사에서 품의서를 올린다고 치자. 10만 원 이하는 팀장 전결, 100만 원 이하는 본부장 전결, 1,000만 원 이하는 사장 전결이다. 클라이언트(사원) 코드에
if (cost <= 10) 팀장.승인() else if (cost <= 100) 본부장.승인() ...식으로 모든 결재권자를 명시(하드코딩)해 두면, 회사 규정이 바뀌거나 새로운 결재 라인(부사장)이 생길 때마다 코드를 대대적으로 수정해야 한다. 사원은 그냥 "제출"만 하고, 결재 라인이 알아서 위로 전달하게 만들 수는 없을까? -
💡 비유: ARS 고객센터에 전화하는 것과 같습니다. "1번은 요금 문의, 2번은 고장 신고, 상담원 연결은 0번입니다" 안내 멘트를 듣고 고장 신고를 누르면 해당 부서로 넘어가고, 그 부서에서 해결을 못하면 전문 엔지니어에게 전화를 돌려주는(토스) 방식과 완벽히 동일합니다.
-
등장 배경 및 발전 과정:
- 요청-수신 객체의 강결합 (Hard-wired Routing): 과거에는 이벤트를 발생시키는 객체가 수신할 객체의 클래스와 메서드를 직접 참조해야만 했다.
- 처리자 파이프라인 형성 (Pipeline/Chain): 조건문을 밖으로 빼내어, 각 객체가 "내가 처리할 수 있는가?"라는 작은 책임만 지도록 분산(Decoupling)시켰다.
- 인터셉터/미들웨어로의 진화: 엔터프라이즈 웹 서버(Express.js, Spring)에서 HTTP 요청이 컨트롤러에 도달하기 전 인증, 로깅, 인코딩 필터들을 거쳐가게 하는 근본 아키텍처로 발전했다.
-
📢 섹션 요약 비유: 수건 돌리기 게임처럼, 질문(요청)을 받은 친구가 답을 모르면 옆 친구에게 질문지를 그대로 넘겨주고, 답을 아는 친구가 나올 때까지 계속 옆으로 넘기는 게임 룰입니다.
Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)
구성 요소 (클래스 다이어그램)
| 요소명 | 역할 | 비유 |
|---|---|---|
| Handler (처리자 인터페이스) | 요청을 처리하는 인터페이스. 다음 핸들러(Next)를 지정하는 setNext()와 요청을 처리하거나 떠넘기는 handleRequest() 메서드를 갖는다. | 모든 결재권자(직책)의 공통 매뉴얼 |
| ConcreteHandler | 실제 요청을 처리하는 구체적 클래스. 조건을 확인하여 자신이 처리 가능하면 로직을 실행하고, 불가능하면 다음 핸들러로 위임(Delegate)한다. | 팀장, 본부장, 사장 |
| Client (클라이언트) | 체인의 첫 번째 핸들러에게 요청을 던지는 객체. 그 이후 요청이 누구에게 전달되는지 전혀 신경 쓰지 않는다. | 품의서를 기안하는 사원 |
동작 메커니즘 (코드 뼈대 구조)
책임 연쇄 패턴의 핵심은 ConcreteHandler 내부의 if-else 구조다. 자신이 처리할 수 없으면 next.handleRequest()를 호출해 핑퐁을 치는 재귀적 구조를 띤다.
┌─────────────────────────────────────────────────────────────┐
│ 책임 연쇄 패턴의 릴레이 동작 흐름 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [Client] │
│ 1. 체인 구성: 팀장.setNext(본부장); 본부장.setNext(사장); │
│ 2. 팀장.handle(50만원_결재요청); │
│ │ │
│ ▼ │
│ [ConcreteHandler 1: 팀장] │
│ public void handle(요청) { │
│ if (요청금액 <= 10만) { "팀장이 승인" } │
│ else if (next != null) { next.handle(요청); } ───┐ │
│ } │ │
│ │ │
│ ┌─────────────────────────────────────────────┘ │
│ ▼ │
│ [ConcreteHandler 2: 본부장] │
│ public void handle(요청) { │
│ if (요청금액 <= 100만) { "본부장이 승인" } │
│ else if (next != null) { next.handle(요청); } ───┐ │
│ } │ │
│ │ │
│ ┌─────────────────────────────────────────────┘ │
│ ▼ │
│ [ConcreteHandler 3: 사장] │
│ public void handle(요청) { │
│ if (요청금액 <= 1000만) { "사장이 승인" } │
│ else { "반려: 회삿돈이 부족합니다." } │
│ } │
└─────────────────────────────────────────────────────────────┘
[다이어그램 해설] 클라이언트는 팀장.handle(50만 원)으로 던지기만 했다. 본부장이 50만 원을 처리할지, 사장이 처리할지 클라이언트는 모른다. 내부적으로 팀장은 자신의 한도를 넘어섰으므로 next(본부장)에게 떠넘겼고, 본부장은 자신의 한도 이내이므로 승인하고 릴레이를 끝냈다. 체인의 순서나 구성원을 바꾸고 싶다면 클라이언트 쪽에서 setNext() 순서만 조작하면 끝난다.
두 가지 파생 모델 (Pure vs Impure)
책임 연쇄 패턴은 "한 명이 처리하면 끝인가, 아니면 모두 다 훑고 지나가는가?"에 따라 실무적으로 두 가지로 나뉜다.
| 분류 | 동작 방식 | 주요 사용처 | 비유 |
|---|---|---|---|
| Pure (순수) | 핸들러 중 단 1개만 요청을 처리하고, 처리하는 순간 체인이 즉시 종료된다. | 권한 검사, 오류/예외 처리 핸들러 체인 | 퀴즈의 정답을 아는 단 한 명이 답하고 종료 |
| Impure (변형) | 한 핸들러가 처리했더라도 멈추지 않고, 체인 끝까지 통과하며 모든 핸들러가 돌아가며 연산을 덧붙인다. | 웹 필터(인코딩, 압축, 암호화), 로깅 파이프라인 | 조립 라인에서 각 로봇이 부품을 하나씩 계속 붙임 |
- 📢 섹션 요약 비유: 순수 방식은 '콜센터'처럼 한 부서에서 해결되면 전화를 끊는 것이고, 변형 방식은 '자동차 공장 컨베이어 벨트'처럼 첫 로봇이 문짝을 달고 다음 로봇이 바퀴를 달아 완성품이 나올 때까지 끝까지 통과시키는 것입니다.
Ⅲ. 융합 비교 및 다각도 분석
1. 책임 연쇄 패턴 vs 커맨드 패턴 (Command Pattern)
두 패턴 모두 "요청을 보내는 자와 받는 자를 분리한다"는 공통점이 있다.
| 패턴 | 목적 (Intent) | 수신자 (Receiver)의 확정성 |
|---|---|---|
| 커맨드 (Command) | 요청 자체를 객체로 캡슐화하여 큐잉, 로깅, Undo를 수행 | 누가 처리할지 캡슐 안에 미리 확정되어 있음 |
| 책임 연쇄 (CoR) | 여러 객체에게 요청을 넘겨 처리자를 동적으로 탐색 | 체인을 따라가다 누가 처리할지 런타임에 결정됨 |
과목 융합 관점
-
소프트웨어 공학 (SE): Spring 프레임워크의
Filter와HandlerInterceptor, Node.js(Express)의Middleware아키텍처가 책임 연쇄(Impure)의 완벽한 구현체다.next()나chain.doFilter()를 호출하여 다음 필터로 계속 요청을 넘기며 처리(인증 → 로깅 → 비즈니스 로직)를 누적한다. -
운영체제 (OS) / 시스템 프로그래밍: 예외 처리 메커니즘(
try-catch-finally)의 버블링(Bubbling) 현상이 전형적인 책임 연쇄(Pure)다. 현재 함수 스택에서catch를 못하면 부모 함수로 예외를 토스하고, 또 못하면 그 위의 부모로 던져 최종적으로 OS(JVM)가Unhandled Exception으로 앱을 죽이는 과정이 완벽한 체인이다. -
📢 섹션 요약 비유: 커맨드 패턴이 "이 택배를 무조건 '홍길동'에게 배달해라"라고 지정된 특급 우편이라면, 책임 연쇄 패턴은 "이 동네 사람들 중에 이 택배 받을 사람 있나요?" 하고 동네를 돌며 묻는 전단지와 같습니다.
Ⅳ. 실무 적용 및 기술사적 판단
실무 시나리오
-
시나리오 — 거대한 인증/인가 분기문 폭발 방지: REST API 서버에 요청이 들어오면 "1) 블랙리스트 IP 검사 → 2) 토큰 유효성 검사 → 3) 관리자 권한 검사 → 4) 데이터 무결성 검사"를 해야 한다. 이 4가지를
if-else로 한 컨트롤러 안에 다 짜넣으면 코드가 100줄을 넘어간다.- 아키텍트의 해결책: 각 검사 로직을
IpFilter,AuthFilter,RoleFilter등의 독립된 핸들러 클래스로 분리하고 체인으로 연결한다. 어느 하나라도 실패하면 체인을 끊고 예외 응답을 내보낸다. 향후 "5) 초당 호출 횟수(Rate Limit) 검사" 로직이 추가되어도 기존 코드는 1줄도 건드리지 않고 새로운 필터를 체인 중간에 끼워 넣기(setNext())만 하면 된다.
- 아키텍트의 해결책: 각 검사 로직을
-
시나리오 — 이벤트 버블링 (DOM Event Bubbling): 프론트엔드 환경에서 사용자가 웹 페이지의 '버튼'을 클릭했다. 이 클릭 이벤트가 버튼 자체에 달린 리스너에서 처리될지, 버튼을 감싼 'DIV 상자'에서 처리될지, 전체 '문서(Document)' 단위에서 처리될지 결정해야 한다.
- 아키텍트의 해결책: 브라우저 렌더링 엔진은 이벤트 타겟(버튼)부터 최상위 요소(Window)까지 DOM 트리를 거슬러 올라가며 이벤트를 토스하는 **책임 연쇄 패턴(버블링)**을 기본 내장하고 있다. 개발자는
event.stopPropagation()을 호출해 체인을 끊음으로써(Pure 방식) 이벤트 처리를 자신이 원하는 계층에서 독점할 수 있다.
- 아키텍트의 해결책: 브라우저 렌더링 엔진은 이벤트 타겟(버튼)부터 최상위 요소(Window)까지 DOM 트리를 거슬러 올라가며 이벤트를 토스하는 **책임 연쇄 패턴(버블링)**을 기본 내장하고 있다. 개발자는
도입 체크리스트
- 기술적: 체인의 마지막까지 갔는데도 아무도 요청을 처리하지 못하는 상황에 대한 대비책(Default Handler)이 마련되어 있는가? 자칫하면 클라이언트의 요청이 허공으로 사라져 무응답(Timeout) 장애가 발생할 수 있다.
- 성능적: 체인의 길이가 100개, 1000개로 길어지는가? 체인을 순회하는 데 걸리는 시간 오버헤드와, 스택 트레이스(Stack Trace)가 깊어져 디버깅이 불가능해질 위험을 고려했는가?
안티패턴
-
스택 오버플로(Stack Overflow)를 유발하는 순환 체인: 체인 설정(Configuration) 실수로
A.setNext(B),B.setNext(A)처럼 순환 고리가 생겨버리면, 요청이 영원히 핑퐁을 치다가 콜스택 초과로 서버가 즉사한다. -
비대한(Fat) 필터: 체인 1개가 너무 많은 비즈니스 로직(DB 저장, 외부 API 호출 등)을 수행하여, 하나의 무거운 필터 때문에 전체 파이프라인의 레이턴시(Latency)가 급증하는 병목 현상.
-
📢 섹션 요약 비유: 전단지를 동네 사람들에게 돌릴 때, "아무도 이 전단지를 안 받으면 어떻게 할지(쓰레기통에 버릴지, 다시 들고 올지)" 정해두지 않으면, 전단지가 영원히 동네를 떠도는 유령 편지가 되어버릴 수 있습니다. (디폴트 핸들러의 중요성)
Ⅴ. 기대효과 및 결론
정량/정성 기대효과
| 구분 | 조건문 중첩 (AS-IS) | 책임 연쇄 도입 (TO-BE) | 개선 효과 |
|---|---|---|---|
| 정량 | 조건식/검증 로직 추가 시 Cyclomatic Complexity 기하급수적 증가 | 분리된 객체로 인해 클래스당 복잡도 1~2 유지 | 코드 복잡도 통제 및 가독성 향상 |
| 정량 | 결재 순서 등 런타임 변경 불가 (재컴파일 필요) | setNext() 재조립만으로 런타임 변경 완료 | 동적 워크플로우 구성 및 유지보수비용 제로화 |
| 정성 | 모든 처리자가 클라이언트 코드에 하드코딩됨 | 클라이언트와 처리자 간 완벽한 디커플링 | 도메인 로직과 검증(Filter) 로직의 완벽한 분리(관심사 분리) |
미래 전망
- 함수형 파이프라인(Functional Pipeline): 모던 언어에서는 거창한
Handler인터페이스와 클래스를 선언하지 않는다. 일급 객체(함수)들을 배열에 담아reduce나compose함수로 묶어 연달아 실행하는 고차 함수(Higher-order Function) 기법이 책임 연쇄 패턴을 대체하고 있다. (예: JS의pipe(filter1, filter2, filter3)(request)) - 서버리스(Serverless) 워크플로우: AWS Step Functions처럼 독립된 마이크로서비스(람다)들을 시각적인 체인으로 엮어, 1번 람다의 출력이 2번 람다의 입력이 되도록 하는 클라우드 인프라 레벨의 파이프라인 오케스트레이션으로 진화했다.
참고 표준
- GoF (Gang of Four): Behavioral Patterns - Chain of Responsibility
- Java EE:
javax.servlet.Filter인터페이스와FilterChain - Spring Framework:
HandlerInterceptor및 Security의FilterChain
책임 연쇄 패턴은 **"나의 책임이 끝나면 미련 없이 남에게 넘겨라"**라는 단일 책임 원칙(SRP)의 정수를 보여준다. 기술사는 시스템 내에서 여러 가지 조건이나 검증 로직이 한 곳에 떡 지어 있는(Spaghetti) 것을 발견했을 때, 이를 무자비하게 쪼개어 단방향의 우아한 필터 파이프라인(Chain)으로 재조립함으로써, 누구든 중간에 선 하나만 끊고 새로운 파이프를 끼워 넣을 수 있는 '숨 쉬는 아키텍처'를 만들어야 한다.
- 📢 섹션 요약 비유: 정수기의 필터와 같습니다. 물(요청)이 위에서 떨어지면 숯 필터, 모래 필터, 자갈 필터를 차례대로 거치면서 더러운 찌꺼기가 하나씩 걸러집니다. 필터를 더 촘촘하게 하고 싶으면 그저 중간에 필터 하나를 더 끼워 넣기만 하면 됩니다.
📌 관련 개념 맵 (Knowledge Graph)
| 개념 명칭 | 관계 및 시너지 설명 |
|---|---|
| 데코레이터 패턴 (Decorator) | 겹겹이 포장해 나간다는 구조가 체인과 유사하나, 데코레이터는 기능을 '덧붙이는' 것이고 체인은 '처리를 떠넘기거나 중단'하는 성격이 강하다. |
| 커맨드 패턴 (Command) | 요청을 객체로 만든 커맨드를 책임 연쇄 체인에 던져, 누가 이 커맨드를 가장 잘 실행할 수 있을지 런타임에 찾게 하는 융합 조합. |
| 미들웨어 (Middleware) 아키텍처 | Node.js나 Spring WebFlux 등 현대 웹 서버에서 요청/응답 객체를 가공하기 위해 연쇄적으로 쌓아 올린 책임 연쇄 패턴의 동의어. |
| 단일 책임 원칙 (SRP) | 한 클래스가 하나의 조건이나 검증 로직만 담당하도록 쪼개어 체인으로 연결하게 만드는 객체 지향의 근본 원칙. |
| 이벤트 버블링 (Event Bubbling) | 웹 브라우저가 DOM 트리의 자식 요소부터 부모 요소로 마우스/키보드 이벤트를 타고 올라가며 전달하는 순수 책임 연쇄 시스템. |
👶 어린이를 위한 3줄 비유 설명
- 학교에서 어려운 수학 문제를 풀다가 모르면 짝꿍에게 "너 이거 풀 줄 알아?" 하고 넘겨주죠?
- 짝꿍도 모르면 그 옆 친구에게 넘기고, 결국 반에서 제일 수학을 잘하는 똑똑이 친구에게 도달하면 그 친구가 정답을 풀어줍니다.
- 이렇게 나한테 들어온 숙제(요청)를 내가 못 풀면 풀 수 있는 사람을 만날 때까지 옆으로 계속 넘겨주는 릴레이 방법을 **'책임 연쇄 패턴'**이라고 부른답니다!