하리Hari

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환불처리> 둘 다에 그렇지 않고 만약을 같은 자리에 넣어야 해요. 한쪽만 고치면 창구마다 규칙이 어긋나요.
  • 처리자마다 할 일이 커지면 함수가 부풀어요. 상담원이 “환불 사유도 확인하기”, 팀장이 “기록 남기기”를 하게 되면, 그 모든 일이 갈래 하나하나 안에 들어가요.

해결책: 처리자들을 사슬로 잇기

책임 연쇄 패턴은 처리자 하나하나를 객체로 만들고, 그 객체들을 사슬처럼 이어요.

  1. 모든 처리자의 공통 뼈대를 부모에 둬요. “내가 맡을 수 있으면 맡고, 아니면 다음에게 넘긴다”예요. 다음이 없으면 본사로 안내해요.
  2. 처리자마다 다른 것, 맡을 수 있는 범위와 맡는 방법만 자식이 채워요.
  3. 각 처리자는 다음 처리자 하나만 알아요. 사슬 전체가 어떻게 생겼는지는 몰라요.
  4. 사슬은 쓰는 쪽이 이어요. 창구마다 다르게 이을 수 있어요.

비유: 회사의 결재 라인

휴가 신청서를 내면 먼저 팀장이 봐요. 하루짜리면 팀장이 바로 승인하고, 일주일이면 “제 권한 밖이에요” 하고 부장에게 올려요. 부장도 못 하면 사장에게 올리고요. 신청서를 낸 사람은 누가 최종 승인할지 몰라도 돼요. 맨 앞 사람에게 내기만 하면, 맡을 수 있는 사람이 나올 때까지 저절로 올라가요. 이 결재 라인이 바로 “책임 연쇄”예요.

누가 무엇을 하나요?

역할 이 예제에서 하는 일
처리자 뼈대 [처리자] (밑설계) 맡을지 넘길지 정하고, 다음 처리자를 기억해요
구체적인 처리자 [자동승인], [상담원], [팀장] 맡을 수 있는 범위와 맡는 방법을 알아요
사슬을 잇는 쪽 '일반창구', '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)이 부팀장에게 가는지 확인해 보세요. 일반 창구를 만드는 줄은 새로운 [자동승인](새로운 [상담원](새로운 [부팀장](새로운 [팀장]())))이 될 거예요.

언제 쓰면 좋을까요?

  • 부탁을 처리할 수 있는 쪽이 여럿이고, 누가 맡을지는 부탁을 봐야 알 때
  • 처리하는 순서가 상황마다 달라야 할 때 (창구마다, 사용자마다)
  • 처리자를 끼우거나 빼는 일이 자주 있을 때
  • 부탁하는 쪽이 누가 최종 처리하는지 몰라도 되게 하고 싶을 때

좋은 점과 아쉬운 점

좋은 점 아쉬운 점
처리자마다 자기 일만 알아서 단순해요 부탁이 누구에게서 처리됐는지 코드만 봐서는 바로 안 보여요
같은 처리자들을 상황마다 다르게 이을 수 있어요 사슬 끝에서 무엇을 할지 정하지 않으면 부탁이 사라질 수 있어요
처리자를 끼우고 빼기 쉬워요 사슬이 길면 앞에서부터 하나씩 물어서 느려질 수 있어요
부탁하는 쪽은 맨 앞만 알면 돼요 이어 붙인 순서를 잘못하면(팀장을 맨 앞에) 엉뚱한 처리자가 맡아요

너무 이른 패턴은 독이에요.
처리자가 두셋이고 순서가 늘 같다면, 만약 사다리 하나가 더 읽기 쉬워요. 순서가 상황마다 달라지거나, 처리자를 자주 끼우고 빼기 시작할 때 사슬로 바꿔도 늦지 않아요.

다른 패턴과의 관계

  • 데코레이터 패턴과 모양이 비슷해요. 둘 다 객체가 다음 객체를 품고 일을 넘겨요. 데코레이터는 모두가 자기 몫을 더하며 끝까지 넘기고, 책임 연쇄는 누군가 맡으면 거기서 멈춰요.
  • 템플릿 메서드 패턴으로 뼈대를 만들었어요. “맡을까, 넘길까”라는 순서는 부모가 정하고, 범위와 방법만 자식이 채웠어요.
  • 커맨드 패턴과 함께 쓸 수 있어요. 부탁을 금액 같은 값 대신 명령 객체로 만들어 사슬에 흘려보내면, 처리자가 그 명령을 실행하거나 다음에 넘길 수 있어요.
  • 컴포지트 패턴의 나무에서도 자주 써요. 안쪽 항목이 처리하지 못한 부탁을 바깥 묶음으로 올려 보내는 것도 책임 연쇄예요.
놀이터
결과
코드를 작성하고 '실행' 버튼을 눌러보세요.