14. 책임 연쇄 패턴 (Chain of Responsibility)
부탁을 처리자들의 사슬에 흘려보내서, 맡을 수 있는 처리자가 맡게 하는 책임 연쇄 패턴을 알아봅니다.
한 줄 요약
부탁을 처리할 수 있는 객체들을 사슬처럼 이어 두고, 부탁이 오면 맨 앞에서부터 “내가 맡을 수 있으면 맡고, 아니면 다음에게 넘기게” 하는 패턴이에요.
문제: 누가, 어떤 순서로 맡을지 한 함수에 다 들어 있는 코드
쇼핑몰의 환불 처리를 만든다고 해 볼게요. 금액에 따라 처리할 수 있는 사람이 달라요.
| 금액 | 처리하는 쪽 |
|---|---|
| 1만 원 이하 | 자동승인 (사람 없이 바로) |
| 10만 원 이하 | 상담원 |
| 100만 원 이하 | 팀장 |
| 그보다 많으면 | 본사 심사로 안내 |
그리고 VIP 창구는 자동승인 없이 상담원부터 시작해요. 가장 먼저 떠오르는 방법은 금액을 보고 만약으로 나누는 거예요.
(참고: 누가 처리할지, 어떤 순서로 물어볼지가 한 함수에 다 들어 있어요)
<환불처리>를 만들자 ([숫자]인 '금액'):
만약 ('금액' <= 10000) 라면:
틀"[자동승인] {'금액'}원 환불했어요."를 출력하자
그렇지 않고 만약 ('금액' <= 100000) 라면:
틀"[상담원] {'금액'}원 환불을 승인했어요."를 출력하자
그렇지 않고 만약 ('금액' <= 1000000) 라면:
틀"[팀장] {'금액'}원 환불을 승인했어요."를 출력하자
그렇지 않다면:
틀"[안내] {'금액'}원은 본사 심사가 필요해요."를 출력하자
(참고: VIP 창구는 자동승인 없이 상담원부터 시작해요. 그래서 거의 같은 함수를 또 만들었어요)
<VIP환불처리>를 만들자 ([숫자]인 '금액'):
만약 ('금액' <= 100000) 라면:
틀"[상담원] {'금액'}원 환불을 승인했어요."를 출력하자
그렇지 않고 만약 ('금액' <= 1000000) 라면:
틀"[팀장] {'금액'}원 환불을 승인했어요."를 출력하자
그렇지 않다면:
틀"[안내] {'금액'}원은 본사 심사가 필요해요."를 출력하자
<환불처리>(5000)를 실행하자
<환불처리>(50000)을 실행하자
<환불처리>(3000000)을 실행하자
<VIP환불처리>(5000)를 실행하자
실행 결과
[자동승인] 5000원 환불했어요.
[상담원] 50000원 환불을 승인했어요.
[안내] 3000000원은 본사 심사가 필요해요.
[상담원] 5000원 환불을 승인했어요.
잘 돌아가요. 그런데 규칙이 늘어나면 아파져요.
- “누가”와 “어떤 순서로”가 한 덩어리예요. 자동승인, 상담원, 팀장이 각각 무엇을 하는지와, 누구에게 먼저 물어볼지가
<환불처리>한 함수에 섞여 있어요. - 창구마다 순서가 다르면 함수가 복사돼요. VIP 창구는 순서만 조금 다른데, 거의 똑같은
<VIP환불처리>를 또 만들었어요. - 새 처리자를 끼우려면 모든 복사본을 고쳐야 해요. “50만 원 이하는 부팀장”이 생기면
<환불처리>와<VIP환불처리>둘 다에그렇지 않고 만약을 같은 자리에 넣어야 해요. 한쪽만 고치면 창구마다 규칙이 어긋나요. - 처리자마다 할 일이 커지면 함수가 부풀어요. 상담원이 “환불 사유도 확인하기”, 팀장이 “기록 남기기”를 하게 되면, 그 모든 일이 갈래 하나하나 안에 들어가요.
해결책: 처리자들을 사슬로 잇기
책임 연쇄 패턴은 처리자 하나하나를 객체로 만들고, 그 객체들을 사슬처럼 이어요.
- 모든 처리자의 공통 뼈대를 부모에 둬요. “내가 맡을 수 있으면 맡고, 아니면 다음에게 넘긴다”예요. 다음이 없으면 본사로 안내해요.
- 처리자마다 다른 것, 맡을 수 있는 범위와 맡는 방법만 자식이 채워요.
- 각 처리자는 다음 처리자 하나만 알아요. 사슬 전체가 어떻게 생겼는지는 몰라요.
- 사슬은 쓰는 쪽이 이어요. 창구마다 다르게 이을 수 있어요.
비유: 회사의 결재 라인
휴가 신청서를 내면 먼저 팀장이 봐요. 하루짜리면 팀장이 바로 승인하고, 일주일이면 “제 권한 밖이에요” 하고 부장에게 올려요. 부장도 못 하면 사장에게 올리고요. 신청서를 낸 사람은 누가 최종 승인할지 몰라도 돼요. 맨 앞 사람에게 내기만 하면, 맡을 수 있는 사람이 나올 때까지 저절로 올라가요. 이 결재 라인이 바로 “책임 연쇄”예요.
누가 무엇을 하나요?
| 역할 | 이 예제에서 | 하는 일 |
|---|---|---|
| 처리자 뼈대 | [처리자] (밑설계) |
맡을지 넘길지 정하고, 다음 처리자를 기억해요 |
| 구체적인 처리자 | [자동승인], [상담원], [팀장] |
맡을 수 있는 범위와 맡는 방법을 알아요 |
| 사슬을 잇는 쪽 | '일반창구', 'VIP창구' |
처리자들을 원하는 순서로 이어요 |
| 부탁하는 쪽 | <처리하기>(금액)을 부르는 곳 |
사슬 맨 앞에 부탁하기만 해요 |
하리로 만들기
같은 환불 처리를 책임 연쇄 패턴으로 다시 만들어 볼게요.
(참고: 1단계 — 처리자의 공통 뼈대. "내가 맡을 수 있으면 맡고, 아니면 다음에게 넘긴다")
[처리자]를 밑설계하자:
'다음'을 비어있음으로 정하여 숨기자
(참고: 만들 때 다음 처리자를 받아요. 안 주면 사슬의 끝이에요)
처음 만들어질 때 ('다음' = 비어있음) 다음과 같이 하자:
'나'의 '다음'을 '다음'으로 정하자
<처리하기>를 만들자 ([숫자]인 '금액'):
만약 ('나'의 <맡을수있나>('금액')가 참과 같다) 라면:
'나'의 <맡기>('금액')를 실행하자
그렇지 않고 만약 ('나'의 '다음'이 비어있음과 같다) 라면:
틀"[안내] {'금액'}원은 본사 심사가 필요해요."를 출력하자
그렇지 않다면:
'나'의 '다음'의 <처리하기>('금액')를 실행하자
(참고: 2단계 — 처리자마다 다른 것: 맡을 수 있는 범위와 맡는 방법)
<맡을수있나>가 있어야 한다 ([숫자]인 '금액')
<맡기>가 있어야 한다 ([숫자]인 '금액')
[처리자]를 바탕으로 [자동승인]을 설계하자:
[논리]를 돌려주는 <맡을수있나>를 만들자 ([숫자]인 '금액'):
('금액' <= 10000)을 돌려주자
<맡기>를 만들자 ([숫자]인 '금액'):
틀"[자동승인] {'금액'}원 환불했어요."를 출력하자
[처리자]를 바탕으로 [상담원]을 설계하자:
[논리]를 돌려주는 <맡을수있나>를 만들자 ([숫자]인 '금액'):
('금액' <= 100000)을 돌려주자
<맡기>를 만들자 ([숫자]인 '금액'):
틀"[상담원] {'금액'}원 환불을 승인했어요."를 출력하자
[처리자]를 바탕으로 [팀장]을 설계하자:
[논리]를 돌려주는 <맡을수있나>를 만들자 ([숫자]인 '금액'):
('금액' <= 1000000)을 돌려주자
<맡기>를 만들자 ([숫자]인 '금액'):
틀"[팀장] {'금액'}원 환불을 승인했어요."를 출력하자
(참고: 3단계 — 사슬은 쓰는 쪽이 이어요. 안쪽(끝)부터 만들어서 앞에 건네요. 창구마다 다르게 이을 수 있어요)
'일반창구'를 새로운 [자동승인](새로운 [상담원](새로운 [팀장]()))으로 정하자
'VIP창구'를 새로운 [상담원](새로운 [팀장]())으로 정하자
'일반창구'의 <처리하기>(5000)를 실행하자
'일반창구'의 <처리하기>(50000)을 실행하자
'일반창구'의 <처리하기>(3000000)을 실행하자
'VIP창구'의 <처리하기>(5000)를 실행하자
실행 결과
[자동승인] 5000원 환불했어요.
[상담원] 50000원 환불을 승인했어요.
[안내] 3000000원은 본사 심사가 필요해요.
[상담원] 5000원 환불을 승인했어요.
결과는 리팩터링 전과 한 줄도 다르지 않아요. 하지만 이제 VIP 창구를 위한 함수가 따로 없어요. 같은 처리자들을 다르게 이었을 뿐이에요.
단계별로 다시 보기
- 1단계 — 공통 뼈대.
[처리자]의<처리하기>는 “맡을 수 있나? → 맡기 / 다음이 없나? → 본사 안내 / 아니면 다음에게”라는 순서를 한 번만 적어요. 템플릿 메서드와 같은 모양이에요. - 2단계 — 처리자마다 다른 것만.
[상담원]은 “10만 원 이하면 맡을 수 있어요”와 “승인했어요”만 알아요. 자기 앞에 누가 있는지, 뒤에 누가 있는지는 몰라요. - 만들 때 다음을 건네기. 처리자는 만들어질 때 다음 처리자를 받아요. 그래서
새로운 [자동승인](새로운 [상담원](새로운 [팀장]()))처럼 사슬의 끝(팀장)부터 만들어 앞으로 건네요. 아무것도 안 주면'다음'이비어있음이라 그 처리자가 사슬의 끝이 돼요. 자식들은 생성자를 따로 만들지 않아도 부모의 생성자가 쓰여요. - 3단계 — 사슬은 쓰는 쪽이. 일반 창구는 자동승인 → 상담원 → 팀장, VIP 창구는 상담원 → 팀장이에요. 처리자 클래스는 한 벌인데, 잇는 순서만 달라요.
사슬 끝까지 아무도 못 맡으면요?
이 예제에서는 마지막 처리자의'다음'이비어있음이라서, 뼈대가 “본사 심사가 필요해요”라고 안내해요. 사슬 끝에서 무엇을 할지(안내, 에러, 기본 처리)는 꼭 정해 두세요. 정하지 않으면 부탁이 조용히 사라질 수 있어요.
새 처리자를 끼워 보면
“50만 원 이하는 부팀장”이 생겼다고 해 볼게요.
- 앞의 방식:
<환불처리>와<VIP환불처리>둘 다에 같은 갈래를 같은 자리에 넣어야 해요. - 책임 연쇄 패턴:
[처리자]를 바탕으로 [부팀장]하나만 만들고, 사슬을 잇는 줄에서 상담원과 팀장 사이에 끼우면 돼요. 다른 처리자 클래스는 그대로예요.
직접 해 보기
위 코드에[부팀장](50만 원 이하)을 더하고, 일반 창구와 VIP 창구 모두 상담원 다음, 팀장 앞에 끼워 보세요. 그리고'일반창구'의 <처리하기>(300000)이 부팀장에게 가는지 확인해 보세요. 일반 창구를 만드는 줄은새로운 [자동승인](새로운 [상담원](새로운 [부팀장](새로운 [팀장]())))이 될 거예요.
언제 쓰면 좋을까요?
- 부탁을 처리할 수 있는 쪽이 여럿이고, 누가 맡을지는 부탁을 봐야 알 때
- 처리하는 순서가 상황마다 달라야 할 때 (창구마다, 사용자마다)
- 처리자를 끼우거나 빼는 일이 자주 있을 때
- 부탁하는 쪽이 누가 최종 처리하는지 몰라도 되게 하고 싶을 때
좋은 점과 아쉬운 점
| 좋은 점 | 아쉬운 점 |
|---|---|
| 처리자마다 자기 일만 알아서 단순해요 | 부탁이 누구에게서 처리됐는지 코드만 봐서는 바로 안 보여요 |
| 같은 처리자들을 상황마다 다르게 이을 수 있어요 | 사슬 끝에서 무엇을 할지 정하지 않으면 부탁이 사라질 수 있어요 |
| 처리자를 끼우고 빼기 쉬워요 | 사슬이 길면 앞에서부터 하나씩 물어서 느려질 수 있어요 |
| 부탁하는 쪽은 맨 앞만 알면 돼요 | 이어 붙인 순서를 잘못하면(팀장을 맨 앞에) 엉뚱한 처리자가 맡아요 |
너무 이른 패턴은 독이에요.
처리자가 두셋이고 순서가 늘 같다면,만약사다리 하나가 더 읽기 쉬워요. 순서가 상황마다 달라지거나, 처리자를 자주 끼우고 빼기 시작할 때 사슬로 바꿔도 늦지 않아요.
다른 패턴과의 관계
- 데코레이터 패턴과 모양이 비슷해요. 둘 다 객체가 다음 객체를 품고 일을 넘겨요. 데코레이터는 모두가 자기 몫을 더하며 끝까지 넘기고, 책임 연쇄는 누군가 맡으면 거기서 멈춰요.
- 템플릿 메서드 패턴으로 뼈대를 만들었어요. “맡을까, 넘길까”라는 순서는 부모가 정하고, 범위와 방법만 자식이 채웠어요.
- 커맨드 패턴과 함께 쓸 수 있어요. 부탁을 금액 같은 값 대신 명령 객체로 만들어 사슬에 흘려보내면, 처리자가 그 명령을 실행하거나 다음에 넘길 수 있어요.
- 컴포지트 패턴의 나무에서도 자주 써요. 안쪽 항목이 처리하지 못한 부탁을 바깥 묶음으로 올려 보내는 것도 책임 연쇄예요.