트렌드브리프
책임 연쇄 패턴, 요청을 핸들러 사슬로 넘기는 설계

책임 연쇄 패턴, 요청을 핸들러 사슬로 넘기는 설계

· 게시 · 약 6분 분량

책임 연쇄 패턴은 요청을 처리할 여러 객체를 사슬로 연결해 각 핸들러가 처리하거나 다음으로 넘기게 하는 GoF 행동 패턴이다. 서블릿 필터와 스프링 시큐리티 필터 체인이 대표 사례이며, 구현 구조와 도입 시 트레이드오프를 정리했다.

목차
  1. 책임 연쇄 패턴이란 무엇인가
  2. 언제 이 패턴을 써야 하나
  3. 핸들러 체인을 코드로 구현하기
  4. 서블릿 필터부터 미들웨어까지, 실무 속 책임 연쇄
  5. 도입 전 알아둘 트레이드오프

책임 연쇄 패턴(Chain of Responsibility)은 요청을 처리할 수 있는 여러 객체를 사슬처럼 이어 놓고, 요청이 그 사슬을 따라 흐르며 처리할 객체를 만날 때까지 넘어가게 하는 설계 방식이다. 요청을 보내는 쪽은 누가 처리할지 몰라도 되고, 각 객체는 자기가 처리하거나 다음으로 넘기기만 한다. 1994년에 나온 디자인 패턴 책 『Design Patterns』(네 저자의 이름을 따 흔히 GoF로 부른다)가 정리한 23개 패턴 중 하나이며, 행동(behavioral) 패턴에 속한다.

이 글은 패턴의 구조와 동작 원리, 그리고 서블릿 필터·스프링 시큐리티처럼 이미 실무에서 쓰이는 사례를 개발자 눈높이에서 다룬다. 특정 언어의 문법 세부나 프레임워크 설정법까지 파고들지 않고, 언어를 가리지 않는 설계 관점에 집중한다.

책임 연쇄 패턴이란 무엇인가

한 요청을 여러 후보 처리기(핸들러)에게 순서대로 넘기며, 그중 하나가 처리할 때까지 전달하는 구조다. 각 핸들러는 두 가지만 결정한다. '내가 이 요청을 처리할 수 있는가', 그리고 '처리하지 못하면 다음 핸들러에게 넘길 것인가'.

이 구조의 핵심은 요청을 보내는 쪽(sender)과 처리하는 쪽(receiver)을 떼어 놓는 것이다. sender는 사슬의 맨 앞 핸들러에게 요청을 던질 뿐, 실제로 누가 처리하는지 알 필요가 없다. 핸들러를 추가하거나 순서를 바꿔도 요청을 보내는 코드는 그대로다. Refactoring.guru의 디자인 패턴 정리는 이 패턴의 이점으로 결합도 감소, 핸들러를 동적으로 추가·제거·재배치할 수 있는 유연성, 각 핸들러가 한 가지 책임만 지는 단일 책임을 든다.

사슬을 이루는 방식은 자료구조로 보면 익숙하다. 각 핸들러가 다음 핸들러를 가리키는 참조를 하나씩 들고 있어, 노드가 포인터로 이어지는 연결 리스트와 같은 모양이 된다. 다른 점은 데이터를 저장하려고 잇는 게 아니라, 처리 책임을 순서대로 넘기려고 잇는다는 데 있다.

언제 이 패턴을 써야 하나

요청 하나를 처리할 수 있는 후보가 여럿이고, 그중 누가 처리할지가 상황에 따라 달라질 때 어울린다. 특히 '이 조건이면 A, 저 조건이면 B'식으로 계속 늘어나는 긴 if-else나 switch 분기를, 조건마다 독립된 핸들러로 쪼개고 싶을 때 효과가 크다. 조건 하나를 고치려고 거대한 분기문 전체를 다시 읽지 않아도 되기 때문이다.

실무에서 자주 나오는 상황은 이렇다.

반대로 요청을 처리할 객체가 늘 하나로 정해져 있거나, 모든 요청이 반드시 같은 순서로 모든 단계를 빠짐없이 거쳐야 한다면 굳이 사슬로 만들 이유가 적다. 사슬은 요청을 건너뛸 수 있고 처리 주체가 미리 정해지지 않았을 때라야 이점이 산다.

요청 문서가 세 결재 데스크를 차례로 지나 앞의 둘은 넘기고 마지막이 도장을 찍어 처리하는 선화 일러스트
요청이 핸들러 사슬을 따라 흐르며 각자 처리하거나 다음으로 넘긴다 — 처리할 이가 나올 때까지.

핸들러 체인을 코드로 구현하기

기본 뼈대는 세 부분이다. 공통 인터페이스(다음 핸들러 참조와 처리 메서드), 실제 처리 로직을 담은 구체 핸들러들, 그리고 이들을 순서대로 잇는 조립 코드. 아래는 결재 금액에 따라 처리 주체가 갈리는 예시를 자바 문법으로 간단히 옮긴 것이다.

abstract class Approver {
 protected Approver next;

 Approver linkTo(Approver next) { // 다음 핸들러 연결 + 반환으로 체이닝
  this.next = next;
  return next;
 }

 abstract void handle(int amount);

 protected void passOn(int amount) { // 못 하면 다음으로
  if (next != null) next.handle(amount);
 }
}

class TeamLead extends Approver {
 void handle(int amount) {
  if (amount <= 500_000) System.out.println("팀장 승인: " + amount);
  else passOn(amount);
 }
}

class Director extends Approver {
 void handle(int amount) {
  if (amount <= 3_000_000) System.out.println("부장 승인: " + amount);
  else passOn(amount);
 }
}

사슬은 조립 코드에서 완성된다. 핸들러를 원하는 순서로 이으면, 요청을 맨 앞에 던지는 것만으로 처리 주체가 자동으로 정해진다. 여기서 Executive는 같은 틀에 상한만 없앤 최종 결재자다.

Approver chain = new TeamLead();
chain.linkTo(new Director()).linkTo(new Executive());

chain.handle(300_000); // 팀장이 처리
chain.handle(3_000_000); // 팀장을 지나 부장이 처리

핵심은 handle을 호출하는 쪽이 TeamLead·Director·Executive를 전혀 모른다는 점이다. 결재선을 바꾸고 싶으면 조립 코드의 linkTo 순서만 손대면 되고, 새 결재 단계를 넣을 때도 구체 핸들러 하나를 추가해 사슬에 끼우면 된다. 요청을 보내는 코드는 손댈 일이 없다.

서블릿 필터부터 미들웨어까지, 실무 속 책임 연쇄

직접 구현하지 않아도, 자바 웹 개발자라면 이 패턴을 매일 쓰고 있다. 대표가 서블릿 필터다. Jakarta EE(옛 Java EE) 서블릿 명세에는 Filter 인터페이스와 FilterChain이 있다. 각 필터의 doFilter(request, response, chain) 메서드는 자기 일을 한 뒤 chain.doFilter(request, response)를 호출해 다음 필터로 요청을 넘긴다. 어떤 필터가 chain.doFilter를 호출하지 않으면 뒤따르는 필터와 실제 서블릿은 실행되지 않는다. 요청을 넘길지 말지를 각 필터가 쥐고 있는, 책임 연쇄 그 자체다.

스프링 시큐리티도 같은 토대 위에 있다. 스프링 시큐리티 공식 문서에 따르면, 시큐리티는 표준 서블릿 필터를 기반으로 여러 보안 필터를 순서대로 엮은 필터 체인을 유지하며, FilterChainProxy가 요청 경로에 맞는 필터 체인을 골라 실행한다. 문서는 필터 순서가 중요하다고 강조한다. 인증을 담당하는 필터가 인가를 담당하는 필터보다 먼저 호출돼야 하기 때문이다. 순서가 곧 동작을 좌우한다는 점은 앞서 본 조립 코드의 linkTo 순서와 같은 원리다.

같은 구조는 다른 생태계에도 있다. Node.js의 Express는 미들웨어 함수가 next()를 호출해 다음 미들웨어로 넘기고, ASP.NET Core는 요청 파이프라인에 미들웨어를 순서대로 쌓는다. 자바 표준 로깅의 Logger도 자신이 처리하지 못한 로그 레코드를 상위 Logger로 올려 보내는 부모 사슬을 갖는다. 이름과 문법만 다를 뿐, '각 단계가 처리하고 다음으로 넘긴다'는 뼈대는 모두 책임 연쇄와 같다.

도입 전 알아둘 트레이드오프

사슬은 유연한 만큼 조심할 지점도 있다. 세 가지가 대표적이다.

첫째, 아무도 처리하지 않는 요청이다. 사슬 끝까지 갔는데 처리할 핸들러가 없으면 요청이 조용히 사라질 수 있다. 처리 여부를 반환값으로 알리거나, 사슬 끝에 '기본 처리기'를 두어 미처리 요청을 로깅하거나 예외로 드러내는 장치를 두는 편이 안전하다.

둘째, 디버깅이다. 요청이 어느 핸들러에서 처리됐는지 코드만 봐서는 한눈에 들어오지 않는다. 조립 순서가 실행 시점에 정해지면 더 그렇다. 각 핸들러가 넘기고 처리하는 지점에 로그를 남겨 두면 추적이 수월해진다.

셋째, 순서 의존성이다. 스프링 시큐리티가 인증·인가 필터 순서를 못 박아 두듯, 사슬은 순서가 틀리면 결과가 달라진다. 조립 코드 한 곳에서 순서를 관리하고, 왜 그 순서인지 주석이나 문서로 남겨 두는 게 좋다.

정리하면 책임 연쇄는 요청과 처리기를 떼어 놓고 처리 단계를 유연하게 조합하고 싶을 때 쓰는 도구다. 조건 분기가 늘 하나로 끝나는 단순한 코드에까지 사슬을 두르면 오히려 흐름만 흐려진다. 패턴은 문제의 모양에 맞을 때만 제값을 한다.

더 많은 글 보기 RSS 구독