도서 요약 / / 2023. 2. 23. 20:20

책임 연쇄(Chain of Responsibility) 패턴

책임 연쇄는 무엇인가?

  • 요청을 보내는 코드와 요청을 처리하는 코드의 결합도를 낮추고 싶다.
  • 일반적으로 요청을 하는 코드는 처리하는 객체상의 특정 메소드를 호출하고 강하게 결합되어 있다. 책임 연쇄는 요청을 처리하는 하나 이상의 객체를 만듦으로써 이러한 문제를 해결해준다.
  • 체인에서 그 다음에 나오는 객체의 참조정보를 알고 있는 객체와 연결된 객체를 만든다. 체임에서 첫번째 객체에 요청을 보내고 처리할 수 없다면 체인에서 다음 객체에 요청을 전달한다.

UML

책임 연쇄 구현

  • 처리할 인터페이스/추상 클래스를 정의하면서 시작
    • 처리기는 요청을 받는 메소드를 정의해야 한다.
    • 처리기는 체인에서 다음 요청을 전달받는 메소드를 정의해야 한다. 추상 클래스라면 전달받는 클래스를 유지할 수 있다.
  • 다음으로 하나 이상의 구체 처리기에서 처리기를 구현한다. 구체 처리기는 요청을 처리할 수 있는 체크해야 한다. 처리할 수 없다면 다음 처리기에 요청을 전달해야 한다.
  • 다음 객체의 체인을 생성해야 한다. 클라이언트에서 할 수도 있다. 일반적으로 실상에서 이 작업은 프레임워크나 초기화 코드에서 할 수 있다.
  • 클라이언트는 체인에서 첫 번째 객체만 알고 있어야 한다. 이 객체에 요청을 전달한다.

예제: UML

여기에 연차 처리를 위한 애플리케이션에 대한 요구사항이 있다. 누군가 연차 신청을 한다면 승인되어야 한다. 누가 연차를 신청했느냐에 따라, 무급이냐 유급이지를 기반으로 한다.

예제에서 LeaveApprover가 있고 처리기 인터페이스이고 요청을 처리할 메소드가 정의되어 있고 연차 애플리케이션을 처리할 것이다. 이 메소드가 직원들이 적용되거나 생성되는 애플리케이션의 객체를 주입할 것이다.

그리고 Employee라는 추상 클래스가 있고 어떤 직원도 조직에서 하는 역할에 따라 연차 승인자가 될 수 있다. 연차 승인자를 구현하는 Employee가 있고 여러 조직 역할을 나타내는 여러 역할이 있다.

연차의 특별한 타입의 승인을 하는 Lead가 있고 하루 이틀 병가가 있고 Lead가 승인할 수 있고 유급 휴가를 승인하는 매니저가 있고 무급 휴가를 승인할 수 있는 Directory가 있다.

애플케이션에서 이러한 역할을 하는 객체를 만들 것이고 이 객체들을 체인할 것이다.

누군가 휴가 신청을 하면 그는 Lead에서 신청할 것이다. 그리고 나서 매니저 객체에 참조를 가질 것이고 Lead가 승인할 수 없다면 Manager에게 전달할 것이다. 만일 Manager가 승인할 수 없다면 Directory로 전달할 것이다.

Java 구현

이제 책임 연쇄 디자인 패턴을 구현해 볼 것이다.

애플리케이션에서 여러가지 승인자가 있고 ProjectLead, Manager, Director와 같은 다양한 역할이 있다. 그리고 각 역할은 프로젝트의 특정 타입만 적용될 수 있다. ProjectLead는 기본적으로 이틀보다 적거나 같은 경우만 승인이 가능하고 Manager는 이틀 이상인 휴가 유형이나 병가를 승인할 수 있다. Director는 결근을 승인할 수 있다.

public class LeaveApplication {

    public enum Type {Sick, PTO, LOP};

    public enum Status {Pending, Approved, Rejecetd };

    private Type type;

    private LocalDate from;

    private LocalDate to;

    private String processedBy;

    private Status status;

    public LeaveApplication(Type type, LocalDate from, LocalDate to) {
        this.type = type;
        this.from = from;
        this.to = to;
        this.status = Status.Pending; 
    }

    public Type getType() {
        return type;
    }

    public LocalDate getFrom() {
        return from;
    }

    public LocalDate getTo() {
        return to;
    }

    public int getNoOfDays() {
        return Period.between(from, to).getDays();
    }

    public String getProcessedBy() {
        return processedBy;
    }

    public Status getStatus() {
        return status;
    }

    public void approve(String approverName) {
        this.status = Status.Approved;
        this.processedBy = approverName;
    }

    public void reject(String approverName) {
        this.status = Status.Rejecetd;
        this.processedBy = approverName;
    }

    public static Builder getBuilder() {
        return new Builder();
    }

    @Override
    public String toString() {
        return type + " leave for "+getNoOfDays()+" day(s) "+status
                + " by "+processedBy;
    }
    public static class Builder {
        private Type type;
        private LocalDate from;
        private LocalDate to;
        private LeaveApplication application;

        private Builder() {

        }

        public Builder withType(Type type) {
            this.type = type;
            return this;
        }

        public Builder from(LocalDate from) {
            this.from = from;
            return this;
        }

        public Builder to(LocalDate to) {
            this.to = to;
            return this;
        }

        public LeaveApplication build() {
            this.application = new LeaveApplication(type, from, to);
            return this.application;
        }

        public LeaveApplication getApplication() {
            return application;
        }
    }
}
public interface LeaveApprover {

    void processLeaveApplication(LeaveApplication application);

    String getApproverRole();
}

processLeaveApplication는 애플리케이션을 처리하기 위해 클라이언트에서 사용될 것이다.

public abstract class Employee implements LeaveApprover{

    private String role;

    private LeaveApprover successor;

    public Employee(String role, LeaveApprover successor) {
        this.role = role;
        this.successor = successor;
    }

    @Override
    public void processLeaveApplication(LeaveApplication application) {
        if(!processRequest(application) && successor != null) {
            successor.processLeaveApplication(application);
        }
    }

    protected abstract boolean processRequest(LeaveApplication application);

    @Override
    public String getApproverRole() {
        return role;
    }    
}
public class ProjectLead extends Employee{

    public ProjectLead(LeaveApprover successor) {
        super("Project Lead", successor);
    }

    @Override
    protected boolean processRequest(LeaveApplication application) {
        //type is sick leave & duration is less than or equal to 2 days
        if(application.getType() == LeaveApplication.Type.Sick) {
            if(application.getNoOfDays() <= 2) {
                application.approve(getApproverRole());
                return true;
            }
        }
        return false;
    }
}
public class Manager extends Employee {

    public Manager(LeaveApprover nextApprover) {
        super("Manager", nextApprover);
    }

    @Override
    protected boolean processRequest(LeaveApplication application) {
        switch (application.getType()) {
        case Sick:
            application.approve(getApproverRole());
            return true;
        case PTO:
            if(application.getNoOfDays() <= 5) {
                application.approve(getApproverRole());
                return true;
            }
        }
        return false;
    }
}
public class Director extends Employee {

    public Director(LeaveApprover nextApprover) {
        super("Director", nextApprover);
    }

    @Override
    protected boolean processRequest(LeaveApplication application) {
        if(application.getType() == LeaveApplication.Type.PTO) {
            application.approve(getApproverRole());
            return true;
        }
        return false;
    }
}
public class Client {

    public static void main(String[] args) {
       LeaveApplication application = LeaveApplication.getBuilder().withType(LeaveApplication.Type.Sick)
                                         .from(LocalDate.now()).to(LocalDate.of(2018, 2, 28))
                                         .build();
       System.out.println(application);
       System.out.println("**************************************************");
       LeaveApprover approver = createChain();
       approver.processLeaveApplication(application);
       System.out.println(application);
    }

    private static LeaveApprover createChain() {
        Director director = new Director(null);
        Manager manager = new Manager(director);
        ProjectLead lead = new ProjectLead(manager);
        return lead;
    }

}

구현 고려사항

  • Java의 단일 상속 규칙을 신경쓰지 않고 책임연쇄를 구현하기 위해 핸들러를 인터페이스로 사용하는 것이 좋다.
  • 핸들러는 요청을 처리하더라도 요청을 다시 전파할 수도 있다. 서블릿 필터 체인은 요청에 특정 액션을 수행하더라도 다음 필터로 요청을 넘긴다.
  • 체인은 코드를 수정하지 않고 체인에서 핸들러를 추가 혹은 삭제할 수 있도록 XML 혹은 JSON을 사용해서 나타낼 수 있다.

디자인 고려사항

  • 때로는 기존 코드에서 연결하거나 객체간 체인을 사용하는 것을 생각해 볼 수 있다. 예를 들면 컴포지트 패턴을 사용한다면 행동을 구현하기 위한 체인을 이미 구현했을 수도 있다.

위험요소

  • 패턴에서 요청이 처리될 것이라는 보장이 없다. 요청은 체인 전체를 관통하지만 처리되지 않고 끝난다는 사실을 우리는 모른다.
  • 다음 체인을 연결할 때 체인을 잘못 구성하기 쉽다. 패턴에서 그런 문제를 알려주는 방법은 없다. 어떤 핸들러는 체인에 연결되지 않을 수도 있다.



반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유