디자인 패턴 - 책임연쇄
책임연쇄(Chain of Responsibility) 패턴에 대해서 알아보겠습니다.
책임연쇄(Chain of Responsibility) 패턴
책임 연쇄 패턴(Chain of Responsibility)은 행동 디자인 패턴 중 하나로, 객체들의 체인을 따라 요청을 전달하고 처리하는 방식으로 작동합니다. 대표적으로 아래와 같은 상황에 많이 사용되는 패턴입니다.
사용 사례
- 로깅 시스템
- 이벤트 처리 시스템
- 요청 필터링 및 처리 (예: 웹 서버의 미들웨어)
- 승인 프로세스 (예: 보고서 승인)
보고서 승인
여기서는 ‘보고서 승인 처리’를 구현해보면서 책임연쇄 패턴에 대해 알아보겠습니다.
상황
사원이 작성한 보고서는 팀장-사장을 통해 최종 승인됩니다. 굳이 사장의 승인까지 필요없는 보고서는 팀장이 최종 승인하는 경우도 있습니다.
보고서 클래스 (Report)
우선 보고서 클래스를 만들겠습니다. 보고서는 내용(content)과 최종 승인에 필요한 승인 레벨(required_approval_level) 속성을 가집니다.
from enum import Enum, auto
class ApprovalLevel(Enum):
TEAM_LEAD = auto()
PRESIDENT = auto()
class Report:
def __init__(self, content, required_approval_level: ApprovalLevel):
self.content = content
self.required_approval_level = required_approval_level
결재자 인터페이스 (Approver)
요청을 처리하는 핸들러(handler) 인터페이스를 정의합니다. 결재자는 승인 레벨과 다음 결재자(next_approver)를 가집니다. 이 next 속성값 체인을 통해 보고서를 단계별로 승인 처리하는 것이죠.
from abc import ABC, abstractmethod
from enum import Enum, auto
class Approver(ABC):
def __init__(self, level: ApprovalLevel):
self.level = level
self.next_approver = None
def set_next(self, approver):
self.next_approver = approver
return approver
def process_report(self, report):
is_approved = False
if self.level.value <= report.required_approval_level.value:
is_approved = self.approve(report)
if self.next_approver and is_approved:
return self.next_approver.process_report(report)
return False
@abstractmethod
def approve(self, report):
pass
-
set_next
: 다음 결재자를 지정하고 다음 결재자 객체를 다시 리턴. 체인 만들때 사용 -
process_report
- 보고서의 권한 레벨과 결재자의 권한을 확인.
- 승인처리 결과에 따라서
- 다음 결재자에게 승인 요청
-
approve
: 결재자마다 구체적인 승인 방법
각 결재자
위에 만든 핸들러 인터페이스를 실제 구현하는 구체적 핸들러(Concrete Handler)인 각각의 결재자 클래스를 만듭니다. 각 결재자의 approve
를 구현해줍니다.
from random import choice
class TeamLead(Approver):
def __init__(self):
super().__init__(ApprovalLevel.TEAM_LEAD)
def approve(self, report):
is_approved = choice([True, False])
approve_msg = '승인' if is_approved else '반려'
print(f'팀장이 ~{report.content}~ 보고서를 "{approve_msg}"했습니다')
return is_approved
class President(Approver):
def __init__(self):
super().__init__(ApprovalLevel.PRESIDENT)
def approve(self, report):
is_approved = choice([True, False])
approve_msg = '종결' if is_approved else '반려'
print(f'사장이 ~{report.content}~ 보고서를 "{approve_msg}"했습니다')
return is_approved
체인 생성
이제 만들어둔 핸들러 객체들의 체인을 set_next
를 통해 만들어 줍니다. 여기서 중요한것은 체인을 연결하는 순서입니다. 먼저 팀장이 보고서를 검토하고 다음에 사장이 결재하는 것 처럼, 어떤 객체에서 다음 객체를 연결할지 신중하게 결정해야 합니다.
필요에 따라서 다른 결재자를 얼마든 만들어 추가하고 체인으로 연결 할 수 있습니다.
def setup_approval_chain():
team_lead = TeamLead()
president = President()
team_lead.set_next(president)
return team_lead
클라이언트 코드
보고서 객체를 만들어서 전달합니다. 보고서 객체를 체인 통해 다음 결자재 객체에 전달해서 승인 절차를 수행합니다. 다음 결자재가 없으면 체인 종료합니다.
핸들러에서 처리하는 로직에 따라서 다양하게 구현이 가능합니다. ‘연간 전략 계획’ 보고서 같은 경우, process_report
에서 구현에 따라 팀장이 반려하면 사장에게 보고서가 전달되지 않고 그대로 체인은 종료니다.
approval_chain = setup_approval_chain()
report1 = Report("일일 업무 보고", ApprovalLevel.TEAM_LEAD)
report3 = Report("연간 전략 계획", ApprovalLevel.PRESIDENT)
approval_chain.process_report(report1)
approval_chain.process_report(report2)
팀장이 ~일일 업무 보고~ 보고서를 "반려"했습니다
팀장이 ~연간 전략 계획~ 보고서를 "승인"했습니다
사장이 ~연간 전략 계획~ 보고서를 "종결"했습니다
클래스 다이어그램
%%{ init: { 'theme': 'base', 'themeVariables': { 'primaryColor': '#2a3844', 'lineColor': '#fff', 'primaryTextColor': '#fff', 'tertiaryColor': '#fff' } } }%% classDiagram direction LR class Approver { <<abstract>> -level: ApprovalLevel -next_approver: Approver +set_next(approver: Approver) +approve(report: Report)* } class TeamLead { +approve(report: Report) } class President { +approve(report: Report) } Approver <|-- TeamLead Approver <|-- President TeamLead o-- President
정리
구조 정리
- 핸들러(Handler): 요청을 처리하는 인터페이스를 정의
- 구체적 핸들러(Concrete Handler): 요청 처리의 실제 구현을 담당
- 클라이언트(Client): 첫 번째 핸들러에게 요청
작동 방식
각 핸들러는 다음 핸들러에 대한 참조를 가집니다. 요청이 들어오면 각 핸들러는 그 요청을 처리할 수 있는지 결정합니다. 처리를 수행하고 다음 핸들러로 요청을 전달합니다. 체인의 끝에 도달하거나 요청이 처리될 때까지 이 과정이 계속됩니다.
구현 시 고려사항
- 체인의 종료 조건을 명확히 설정
- 체인의 순서가 중요할 수 있으므로 신중히 설계
더 해볼 것
책임연쇄(Chain of Responsibility) 패턴을 통해서 또 다른 다양한 문제를 구현해보는 것도 좋을 듯합니다. 😊
- 동전 계산기 : 주어진 비용을 500원 부터 동전을 이용하여 몇개씩 필요한지 계산
- 로거(logger) : 주어진 레벨에 따라서 콘솔, 파일, 이메일 등의 단계를 높이면서 로깅 내용을 전달