책임 연쇄 패턴

임현규·2022년 12월 6일
0

책임 연쇄 패턴 소개

handler 인터페이스를 정의하고 여러 concrete 클래스를 생성한다. Client는 매번 Handler를 체인으로 엮어서 활용하는데 이름 책임 연쇄 패턴이라고 한다. 책임 연쇄 패턴은 어떤 작업을 순차적으로 진행하는 데 이를 메서드 한 블록 내에서 처리하는 것이 아닌 클래스를 분리하여 chain형태로 연결해서 처리한다. 기본적으로 전략 패턴과 같이 분리해서 다형성으로 처리하는 것은 같은 맥락이나 다형성을 가진 인터페이스를 연쇄적으로 사용자가 연결해서 호출하는 차이점이 있다.

기존의 연쇄 패턴

public class Main {
    public static void main(String[] args) {
        Chain alice = new NoChain("Alice");
        Chain bob = new LimitChain("Bob", 100);
        Chain charlie = new SpecialChain("Charlie", 429);
        Chain diana = new LimitChain("Diana", 200);
        Chain elmo = new OddChain("Elmo");

        alice.setNext(bob).setNext(charlie).setNext(diana).setNext(elmo);

        for (int i = 0; i < 500; i += 33) {
            alice.execute(new Trouble(i));
        }
    }
}

연쇄적으로 핸들러를 연결하며 사용한다. 그러나 이것의 문제점은 Chain을 연결하면서 최종 chain의 입력은 계속해서 변한다. 그렇기 때문에 체인의 시작점을 기억해야하고 execute()를 호출해야한다.
이러한 단점을 보완하기 위해 파이프 패턴을 사용한다.

파이프 패턴

public class PipeExecutor {

    public static void main(String[] args) {
        String result = new Pipeline<>((Handler<Pair, Integer>) input -> input.a + input.b)
                .addHandler(input -> input * 2)
                .addHandler(Object::toString)
                .execute(new Pair(1, 3));
        System.out.println(result);
    }

    static class Pair {
        int a;
        int b;

        public Pair(int a, int b) {
            this.a = a;
            this.b = b;
        }
    }
}

파이프 패턴도 기본적으로 책임 연쇄 패턴으로 사용하지만 Java의 Stream처럼 이용가능하다. 그리고 Input의 타입이 변하지 않으므로 바로 리턴해서 사용할 수 있는 장점이 있다.

파이프 패턴의 구현 방법을 살펴보자

파이프 패턴 구현하기

// handler 인터페이스
@FunctionalInterface
public interface Handler<I, O> {
    O process(I input);
}

파이프에서 사용할 핸들러이다. 제네릭으로 I와 O를 정의해주었는데 그 이유는 Input 타입을 변하게 하지 않기 위해서 제네릭을 통해 정보를 가져와야 하기 때문이다.

// 핸들러 체인 연결을 위한 파이프라인
public class Pipeline<I, O> {

    private final Handler<I, O> currentHandler;

    public Pipeline(Handler<I, O> currentHandler) {
        this.currentHandler = currentHandler;
    }

    <K> Pipeline<I, K> addHandler(Handler<O, K> newHandler) {
        return new Pipeline<>(input -> newHandler.process(currentHandler.process(input)));
    }

    O execute(I input) {
        return currentHandler.process(input);
    }
}

다음은 Pipeline 클래스이다. 기본적으로 handler를 가지고 해당 핸들러의 execute 메서드를 통해 process 메서드를 실행한다. 여기서 특별한점은 addHandler인데 해석하면 다음과 같다.

K 제네릭 타입은 중간에 변할 매개 타입이다. 파이프 라인이 연쇄적으로 연결되려면 I -> O -> K와 같이 등록이 되어야 한다. 예제를 살펴보자.

A 핸들러 -> <Integer, String>
B 핸들러 -> <String, Long>

이 때 A 핸들러를 가진 pipeline이 B 핸들러를 추가하려 한다. 그러나 이때 Pipeline의 입력 타입이 변하면 안된다. 방법은 간단하다.

Handler <Integer, String> --- <String, Long> ---- > Handler <Integer, Long>

Pipeline은 handler와 동일한 제네릭 타입을 가지므로 최종 파이프 라인의 제네릭 타입은 다음과 같다.

Pipeline<Integer, Long>

결론

정말 간단한 코드지만 핸들러를 체인하기 때문에 재사용성이 뛰어나다. pipe 패턴은 Java의 Stream 패턴과 유사하게 사용할 수 있기 때문에 함수형 프로그래밍에도 유연하고, 한눈에 파악하기 쉽다.

다만 책임 연쇄 패턴은 매번 입출력 인스턴스를 생성해서 소통하기 때문에 성능이 매우 중요하거나 인스턴스 생성 비용이 크다면 이러한 패턴을 사용하는 것이 비효율적이다.

그러나 절차적으로 작성한 메서드를 블록에서 제거하고 객체의 협업을 통해 동작이 가능하기 때문에 테스트가 쉬워지고 chain형태를 통해 다른 패턴에 비해 코드 가독성이 매우 향상되는 이점이 있다.

Java에서 Pipe 패턴을 활용할 수 있는 방법은 자바의 Stream과 Optional API 이다.

코드 깃허브

https://github.com/E1psycongr00/chain_of_reponsibility

참고

https://medium.com/@deepakbapat/the-pipeline-design-pattern-in-java-831d9ce2fe21
https://java-design-patterns.com/patterns/pipeline/#class-diagram

profile
엘 프사이 콩그루

0개의 댓글