단계별 해결의 지혜
고객센터에 전화를 걸어본 경험을 생각해 보죠. 먼저 자동 응답 시스템이 간단한 질문들을 처리합니다. "계정 잔액 확인은 1번을 누르세요", "비밀번호 재설정은 2번을 누르세요." 하지만 복잡한 문제라면 상담원에게 연결합니다.
1차 상담원이 해결할 수 없는 문제는 2차 전문 상담원에게 넘어갑니다. 그것도 안 되면 팀장에게, 더 복잡하면 기술팀 매니저에게, 최종적으로는 부서장에게까지 올라가죠. 각 단계마다 담당자는 자신이 처리할 수 있는 권한과 능력의 범위를 가지고 있습니다.
고객은 이 모든 복잡한 내부 구조를 알 필요가 없습니다. 단지 문제를 제기하면, 그 문제는 적절한 해결자를 찾을 때까지 자동으로 연쇄적으로 전달됩니다. 1차 상담원이 직접 부서장에게 연결해 주는 것이 아니라, 단계적으로 올라가면서 각 레벨에서 해결 가능성을 검토합니다.
이처럼 요청을 처리할 수 있는 여러 객체를 연쇄적으로 연결하여, 요청이 처리될 때까지 순차적으로 전달하는 것이 ‘책임 연쇄 패턴(Chain of Responsibility Pattern)’입니다.
1980년대 후반 객체지향 설계의 과정에서 개발자들은 요청 처리 로직을 설계할 때 주로 거대한 조건문이나 switch 문을 사용했습니다. 하지만 이런 방식은 새로운 처리기를 추가하거나 기존 로직을 수정할 때마다 코어 코드를 변경해야 하는 문제가 있었습니다.
더 심각한 문제는 요청자(Sender)와 처리자(Receiver) 간의 강한 결합이었죠. 요청을 보내는 객체가 누가 그 요청을 처리할지 미리 알고 있어야 했습니다. 이는 시스템의 유연성을 크게 제한했습니다. 예를 들어, GUI 시스템에서 마우스 클릭 이벤트를 처리할 때, 클릭한 객체가 처리할 수 없으면 부모 객체로, 그것도 안 되면 더 상위 객체로 전달해야 하는데, 기존 방식으로는 이런 동적인 처리가 어려웠습니다.
1994년 GOF는 이 문제를 해결하기 위해 Unix 운영체제의 파이프라인 개념과 법원의 상급심 제도에서 영감을 얻었습니다. 특히 Richard Helm은 Smalltalk-80 환경에서 작업하면서 이벤트 처리의 복잡성을 경험했다고 회고합니다. 당시 GUI 이벤트는 여러 레벨의 객체를 거쳐 처리되어야 했는데, 이를 우아하게 해결하는 방법이 필요했던 거죠.
핵심 아이디어는 "처리자들을 연쇄적으로 연결하되, 요청자는 구체적인 처리자를 알지 못하게 하는 것"이었습니다. 이는 전통적인 계층적 호출 구조를 벗어나 더 유연하고 확장 가능한 설계를 가능하게 했습니다.
GoF는 또한 이 패턴이 단순히 기술적 해결책이 아니라 현실 세계의 조직 운영 원리를 반영한다는 점을 강조했다. 기업의 의사결정 과정, 법원의 상소 시스템, 군대의 명령 체계 등에서 볼 수 있는 "단계적 권한 위임"의 원리를 소프트웨어에 적용한 것입니다.
직장인에게 친숙한 휴가 승인 시스템으로 예제를 보겠습니다.
// 휴가 신청서
class LeaveRequest {
private String employeeName;
private int days;
private String reason;
public LeaveRequest(String employeeName, int days, String reason) {
this.employeeName = employeeName;
this.days = days;
this.reason = reason;
}
public String getEmployeeName() { return employeeName; }
public int getDays() { return days; }
public String getReason() { return reason; }
@Override
public String toString() {
return String.format("%s님의 %d일 휴가 신청 (사유: %s)",
employeeName, days, reason);
}
}
// 승인자 추상 클래스
abstract class LeaveApprover {
protected LeaveApprover nextApprover;
protected String position;
protected int maxDays;
public LeaveApprover(String position, int maxDays) {
this.position = position;
this.maxDays = maxDays;
}
public void setNext(LeaveApprover nextApprover) {
this.nextApprover = nextApprover;
}
public void handleRequest(LeaveRequest request) {
if (canApprove(request)) {
approve(request);
} else if (nextApprover != null) {
System.out.println(position + ": " + request.getDays() +
"일은 제 권한을 초과합니다. 상위자에게 전달하겠습니다.");
nextApprover.handleRequest(request);
} else {
reject(request);
}
}
protected boolean canApprove(LeaveRequest request) {
return request.getDays() <= maxDays;
}
protected abstract void approve(LeaveRequest request);
protected void reject(LeaveRequest request) {
System.out.println(position + ": " + request + " - 승인 불가 (권한 없음)");
}
}
// 팀장
class TeamLead extends LeaveApprover {
public TeamLead() {
super("팀장", 3);
}
@Override
protected void approve(LeaveRequest request) {
System.out.println("✅ " + position + ": " + request + " - 승인완료!");
System.out.println(" � \"" + request.getDays() + "일 정도는 팀에서 처리 가능합니다.\"");
}
}
// 부서장
class DepartmentManager extends LeaveApprover {
public DepartmentManager() {
super("부서장", 7);
}
@Override
protected void approve(LeaveRequest request) {
System.out.println("✅ " + position + ": " + request + " - 승인완료!");
System.out.println(" � \"업무 인수인계만 잘 해주시면 됩니다.\"");
}
}
// 인사팀장
class HRManager extends LeaveApprover {
public HRManager() {
super("인사팀장", 15);
}
@Override
protected void approve(LeaveRequest request) {
System.out.println("✅ " + position + ": " + request + " - 승인완료!");
System.out.println(" � \"장기휴가이니 회사 정책에 따라 처리하겠습니다.\"");
}
}
// 대표이사
class CEO extends LeaveApprover {
public CEO() {
super("대표이사", 30);
}
@Override
protected void approve(LeaveRequest request) {
System.out.println("✅ " + position + ": " + request + " - 최종 승인!");
System.out.println(" � \"직원 복지가 우선입니다. 좋은 시간 보내세요.\"");
}
@Override
protected void reject(LeaveRequest request) {
System.out.println("❌ " + position + ": " + request + " - 승인 불가");
System.out.println(" � \"30일을 초과하는 휴가는 회사 정책상 불가능합니다.\"");
}
}
// 휴가 승인 시스템 데모
public class LeaveApprovalDemo {
public static void main(String[] args) {
// 승인 체인 구성
LeaveApprover teamLead = new TeamLead();
LeaveApprover deptManager = new DepartmentManager();
LeaveApprover hrManager = new HRManager();
LeaveApprover ceo = new CEO();
// 연쇄 연결
teamLead.setNext(deptManager);
deptManager.setNext(hrManager);
hrManager.setNext(ceo);
System.out.println("� 휴가 승인 시스템 시작!");
System.out.println("=".repeat(50));
// 다양한 휴가 신청 테스트
LeaveRequest[] requests = {
new LeaveRequest("김개발", 2, "감기몸살"),
new LeaveRequest("박디자인", 5, "가족 여행"),
new LeaveRequest("이기획", 10, "신혼여행"),
new LeaveRequest("최관리", 20, "육아휴직"),
new LeaveRequest("한대리", 35, "세계일주")
};
for (LeaveRequest request : requests) {
System.out.println("\n� " + request);
System.out.println("-".repeat(30));
teamLead.handleRequest(request);
}
System.out.println("\n" + "=".repeat(50));
System.out.println("� 모든 휴가 신청 처리 완료!");
}
}
위 코드에서 가장 중요한 부분은 `handleRequest()` 메서드입니다. 각 승인자는 자신이 처리할 수 있는 범위인지 확인하고, 가능하면 직접 처리하고, 불가능하면 다음 단계로 넘깁니다.
휴가 신청자는 누가 자신의 요청을 처리할지 미리 알 필요가 없습니다. 단지 첫 번째 승인자(팀장)에게만 요청하면, 나머지는 시스템이 자동으로 처리됩니다.
승인 체인을 런타임에 동적으로 구성할 수 있습니다. 예를 들어, 특정 부서는 인사팀장을 거치지 않고 바로 CEO에게 올라가도록 체인을 구성할 수도 있습니다.
각 승인자는 자신의 권한 범위에 대해서만 책임집니다. 팀장은 3일까지만, 부서장은 7일까지만 신경 쓰면 됩니다.
새로운 승인 단계(예: 프로젝트 매니저)를 추가하거나 기존 단계를 제거하는 것이 매우 쉽습니다. 기존 코드를 수정할 필요 없이 체인만 재구성하면 됩니다.
성공적인 조직에서는 모든 결정이 최고 경영진에게 올라가지 않습니다. 각 레벨에서 처리할 수 있는 일은 그 레벨에서 해결하면 됩니다. 이는 효율성을 높일 뿐만 아니라, 구성원들의 성장과 책임감 향상에도 도움이 됩니다.
복잡한 문제를 한 번에 해결하려 하지 말고, 단계별로 접근하는 것이 현명합니다. 먼저 간단한 해결책을 시도해 보고, 그것이 안 되면 좀 더 복잡한 방법을, 최후에는 근본적인 해결책을 고려하는 것입니다.
책임 연쇄 패턴의 가장 큰 장점은 유연성에 있습니다. 상황에 따라 처리 체인을 다르게 구성할 수 있고, 고정된 방식에 얽매이지 말고, 상황에 따라 유연하게 접근 방법을 바꿀 수 있어야 합니다.