책임연쇄(Chain of Responsibility) 패턴The Chain of Responsibility Pattern

파이썬 디자인 패턴 : 책임연쇄Python Design Patterns: Chain of Responsibility

written by tiaz0128

책임연쇄(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
    1. 보고서의 권한 레벨과 결재자의 권한을 확인.
    2. 승인처리 결과에 따라서
    3. 다음 결재자에게 승인 요청
  • 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) : 주어진 레벨에 따라서 콘솔, 파일, 이메일 등의 단계를 높이면서 로깅 내용을 전달

The Chain of Responsibility Pattern

The Chain of Responsibility pattern is a behavioral design pattern that works by passing a request along a chain of objects, each of which can handle it. It’s commonly used in situations like the ones below.

Use Cases

  • Logging systems
  • Event handling systems
  • Request filtering and processing (e.g. web server middleware)
  • Approval processes (e.g. report approval)

Report Approval

Here, we’ll learn about the Chain of Responsibility pattern by implementing ‘report approval handling’.

The Situation

A report written by an employee gets final approval by going through the team lead and then the president. For reports that don’t need the president’s sign-off, the team lead sometimes gives the final approval.

Report approval

The Report Class

First, let’s create the report class. A report has a content attribute and a required_approval_level attribute that indicates the approval level needed for final approval.

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

The Approver Interface

Next, we define the handler interface that processes requests. An approver has an approval level and a next approver (next_approver). Through this chain of next values, the report gets approved step by step.

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 : Sets the next approver and returns that approver object. Used to build the chain
  • process_report
    1. Checks the report’s required level against the approver’s authority.
    2. Depending on the approval result,
    3. Requests approval from the next approver
  • approve : The specific approval logic for each approver

Each Approver

Now we create the approver classes, the concrete handlers that actually implement the handler interface above. Each approver implements 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 = 'approved' if is_approved else 'rejected'

        print(f'The team lead {approve_msg} the ~{report.content}~ report')
        return is_approved

class President(Approver):
    def __init__(self):
        super().__init__(ApprovalLevel.PRESIDENT)

    def approve(self, report):
        is_approved = choice([True, False])
        approve_msg = 'finalized' if is_approved else 'rejected'

        print(f'The president {approve_msg} the ~{report.content}~ report')

        return is_approved

Building the Chain

Now we link the handler objects into a chain using set_next. What matters here is the order in which you connect the chain. Just as the team lead reviews the report first and the president signs off afterward, you need to decide carefully which object links to which.

You can create and add as many other approvers as you need and link them into the chain.

def setup_approval_chain():
    team_lead = TeamLead()
    president = President()

    team_lead.set_next(president)

    return team_lead

Client Code

The client creates a report object and passes it in. The report is passed along the chain to the next approver object to go through the approval process. If there’s no next approver, the chain ends.

There are many ways to implement this, depending on the handler logic. For a report like ‘Annual Strategic Plan’, with the process_report implementation above, if the team lead rejects it, the report never reaches the president and the chain simply ends there.

approval_chain = setup_approval_chain()

report1 = Report("Daily Work Report", ApprovalLevel.TEAM_LEAD)
report3 = Report("Annual Strategic Plan", ApprovalLevel.PRESIDENT)

approval_chain.process_report(report1)
approval_chain.process_report(report2)
The team lead rejected the ~Daily Work Report~ report

The team lead approved the ~Annual Strategic Plan~ report
The president finalized the ~Annual Strategic Plan~ report

Class Diagram

%%{
  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

Wrapping Up

Structure

  • Handler: Defines the interface for handling requests
  • Concrete Handler: Implements the actual request handling
  • Client: Sends the request to the first handler

How It Works

Each handler holds a reference to the next handler. When a request comes in, each handler decides whether it can handle it. It does its processing and passes the request on to the next handler. This continues until the end of the chain is reached or the request has been handled.

Things to Consider When Implementing

  • Clearly define when the chain ends
  • Design the order of the chain carefully, since it can matter

Things to Try Next

It could be fun to tackle other problems with the Chain of Responsibility pattern, too. 😊

  • Coin calculator: Starting from the 500-won coin, calculate how many of each coin are needed for a given amount
  • Logger: Depending on the given level, pass the log along escalating stages such as the console, a file, and email
python 디자인 패턴 책임연쇄

tiaz0128

Eat Sleep Coding.

Never Never GiveUp.

Security  |  BackEnd  |  Multi Cloud