목차
누가 처리할지 미리 못 정한다
지출 결재를 생각하자. 금액에 따라 승인권자가 다르다. 10만 원 이하면 팀장, 100만 원 이하면 부장, 그 위는 임원이 결재한다. 요청을 올리는 사람은 자기 요청을 누가 승인할지 미리 알 수 없다. 금액을 보고 판단해야 하기 때문이다.
public void requestApproval(int amount) {
if (amount <= 100_000) {
teamLead.approve(amount);
} else if (amount <= 1_000_000) {
manager.approve(amount);
} else {
director.approve(amount);
}
}요청하는 쪽이 승인자 전부와 각자의 한도를 다 알고 있어야 한다. 결재 단계가 하나 늘거나(예: 부장과 임원 사이에 이사) 한도가 바뀌면, 이 if-else를 찾아 고쳐야 한다. 승인 요청을 올리는 코드가 여러 군데면 그만큼 복붙된다. 요청자가 조직의 결재 구조를 속속들이 아는 것 자체가 문제다.
책임 연쇄 = 사슬에 흘려보낸다
책임 연쇄 패턴이 이걸 푼다.
요청을 처리자(handler)들의 사슬에 흘려보내, 처리할 수 있는 자가 처리하거나 처리 못 하면 다음 처리자에게 넘긴다.
요청자는 사슬의 맨 앞에만 요청을 건넨다. 그러면 팀장이 받아 보고 자기 한도 안이면 처리하고, 아니면 부장에게 넘기고, 부장도 안 되면 임원에게 넘긴다. 요청자는 누가 최종 처리할지 몰라도 된다. 그저 사슬에 던지면, 처리는 사슬을 따라 흘러가며 알아서 이뤄진다.
Handler와 next
각 처리자는 공통 뼈대를 갖는다. 자기 뒤에 올 다음 처리자(next) 참조를 들고 있고, 요청을 처리하는 메서드를 갖는다.
public abstract class Approver {
protected Approver next; // 다음 처리자
public Approver setNext(Approver next) {
this.next = next;
return next; // 체이닝 편의를 위해 반환
}
public abstract void approve(int amount);
}next가 핵심이다. 이 참조가 처리자들을 한 줄로 잇는다. 팀장의 next가 부장을, 부장의 next가 임원을 가리키면, 이 참조들이 이어져 사슬이 된다.
처리하거나 넘긴다
각 처리자의 규칙은 단순하다. 내가 처리할 수 있으면 처리하고, 아니면 next로 넘긴다.
public class TeamLead extends Approver {
@Override
public void approve(int amount) {
if (amount <= 100_000) {
System.out.println("팀장 승인: " + amount);
} else if (next != null) {
next.approve(amount); // 못 하면 다음으로
}
}
}
public class Manager extends Approver {
@Override
public void approve(int amount) {
if (amount <= 1_000_000) {
System.out.println("부장 승인: " + amount);
} else if (next != null) {
next.approve(amount);
}
}
}
public class Director extends Approver {
@Override
public void approve(int amount) {
System.out.println("임원 승인: " + amount); // 최종 처리자
}
}각 처리자는 자기 판단만 한다. 팀장은 “10만 원 이하인가”만 보면 되고, 그 위 금액을 누가 처리하는지는 신경 쓰지 않는다. 자기가 못 하면 그냥 next로 넘길 뿐이다. 처리자끼리 서로의 한도를 알 필요가 없다.
사슬을 엮는다
처리자들을 setNext로 이어 사슬을 만든다. 이 조립이 이 패턴의 조립 지점이다.
Approver teamLead = new TeamLead();
Approver manager = new Manager();
Approver director = new Director();
teamLead.setNext(manager).setNext(director);
// 요청은 사슬 맨 앞에만 건넨다
teamLead.approve(500_000); // 팀장→부장에서 처리50만 원 요청은 팀장이 받아 “내 한도 초과”라며 부장에게 넘기고, 부장이 “100만 이하”라 처리한다. 요청자는 teamLead.approve() 한 번만 불렀다. 흐름은 사슬이 알아서 탔다.
유연하게 추가하고 순서를 바꾼다
값어치는 여기서 나온다. 결재 단계를 하나 끼우거나 순서를 바꾸는 일이 사슬 조립만 고치면 끝난다.
부장과 임원 사이에 이사 결재를 넣는다고 하자. Director처럼 처리자 클래스 하나를 만들고, 조립할 때 그 자리에 끼우면 된다.
teamLead.setNext(manager).setNext(executive).setNext(director);기존 TeamLead·Manager 코드는 한 글자도 안 바뀐다. 각 처리자가 자기 판단만 하고 뒤는 next에 맡기기 때문에, 뒤에 누가 오든 상관없다. 요청자 코드도 그대로다. 여전히 사슬 맨 앞에 요청을 건넬 뿐이다. 처리자 추가와 순서 변경이 조립 한 줄로 흡수된다.
아무도 처리 못 하면
사슬 끝까지 갔는데 처리자가 아무도 못 맡는 경우가 있다. next가 null이라 넘길 곳도 없는 상황이다.
이건 설계에서 정해야 할 지점이다. 두 갈래가 있다.
- 조용히 흘려보낸다 - 아무도 처리하지 않고 요청이 그냥 사라진다. 로그 처리처럼 “관심 있는 자만 처리하고 나머지는 무시”가 자연스러운 경우다.
- 끝에서 막는다 - 사슬 마지막에 “무엇이든 받아 예외를 던지거나 기본 처리를 하는” 처리자를 둔다. 결재처럼 반드시 누군가 처리해야 하는 경우다.
위 예제의 Director가 후자다. 임원은 금액을 따지지 않고 무조건 처리하는 마지막 방어선이라, 어떤 금액이 와도 사슬이 허공에서 끝나지 않는다. 요청이 처리되지 않고 조용히 사라지는 게 위험한 도메인이라면, 사슬 끝을 이렇게 막아야 한다.
실무: 승인 워크플로
책임 연쇄는 단계적 승인 워크플로에서 특히 자연스럽다. 지출 결재, 휴가 승인, 문서 결재처럼 “금액이나 등급에 따라 결재선이 갈리는” 구조가 그대로 사슬이 된다.
그 밖에도 로그 레벨별 처리(DEBUG 핸들러가 못 받으면 INFO로), 이벤트 핸들링, 예외 처리 체인 등에서 쓰인다. 공통점은 “요청을 누가 처리할지 보내는 쪽이 미리 정하지 않고, 처리자들이 이어받으며 스스로 판단한다”는 것이다. 처리자 목록과 순서가 자주 바뀌고, 보내는 쪽을 그 변화로부터 떼어 놓고 싶을 때 책임 연쇄가 답이다.
정리
| 책임 연쇄란 | 요청을 처리자 사슬에 흘려보내 처리 가능한 자가 맡는다 |
| 문제 | 요청자가 모든 처리자와 규칙을 다 알아야 한다 |
| 고침 | 각 처리자에 next 참조, 처리하거나 다음으로 넘긴다 |
| 조립 | 처리자 추가·순서 변경이 사슬 엮기 한 줄로 흡수된다 |
| 끝 처리 | 아무도 못 맡으면 조용히 흘리거나 끝에서 막는다 |
| 실무 | 결재·승인 워크플로, 로그 레벨, 예외 처리 체인 |
요청을 사슬에 흘려보내 처리자가 이어받게 하고, 보내는 쪽을 처리자 구성으로부터 떼어 놓는 것 - 그게 책임 연쇄다.