본문 바로가기

CS/디자인패턴

[디자인패턴] Chain Of Responsibility 패턴

책임체인 패턴을 설명하는 그림

설명

 메시지를 보내는 객체와 해당 요청을 처리하는 객체 사이의 결합도를 낮춘다. 하나의 요청이 들어올 때 연결된 객체 리스트 사이를 이동하면서 해당 요청을 처리할 수 있는 객체까지 전달되어 처리한다. 요청을 처리하는 객체는 전달된 요청을 자신이 처리할 수 있다면 처리하고 아니면 자신이 알고 있는 다음 객체에게 넘긴다. 이처럼 책임 객체의 동작 방식이 체인처럼 연결되어 있어 책임 체인 패턴이라는 이름이 붙어 있다.

 웹 애플리케이션 서버를 만들고 있는 상황을 생각해 보자. 외부에서 들어온 요청은 handleRequest(request, response)라는 단일 함수 내에서 분류되어 처리된다. 이때 HTTP 헤더는 현재 사용되고 있는 메서드는 무엇인지, 원하는 데이터 타입은 무엇인지 뿐만 아니라 수많은 정보를 담고 있다. 이렇게 다양한 종류의 요청을 조건문 기반으로 분류하도록 구현한다면 말도 안 되는 비효율이 발생할 것이다.

function handleRequest(req,res) => {
    if(req.method==='get') {
        if(req.url === '/') {
            res.write("hello, world!");
            return res.end();
        } else if(req.url === '/hello') {
            // 무언가 코드...
        } else if(req.url === '/list') {
            // 무언가 코드 ...
        } else if(req.url === '/image/~') {
            if(req.headers.accept === 'image/*') {
                // 이미지 관련 무언가...
            } else if(req.headers.accept === undefined) {
                // 에러 처리 관련 무언가...
            }
        }
    } else if (req.method === 'post') {
        // 어떤 섬세한 작업들 ...
    }
    res.end();
})

  위 코드에서는 handleRequest 메서드 내에서 조건문을 통해 각 요청을 분류하는 모습을 보여주고 있다. 가장 큰 단위로는 get / post 요청을 나누고 있으며, 더 세부적으로는 요청으로 들어온 url이 무엇인지 분류하거나 세부적인 헤더에 대응한다. 물론 위 코드에서도 일부 내용은 다른 함수로 분리하도록 작성할 수 있다. 예를 들어 HTTP 메서드를 기준으로 세부 로직은 handleGetRequestHandler, handlePostRequestHandler 클래스로 분리하여 수행하도록 만드는 것이다. 그럼에도 handleRequest 함수는 여전히 각 요청에 대응되는 객체나 메서드를 구체적으로 알고 있어야 하기 때문에 결합이 강하다.

 이처럼 단순히 조건문 기반으로 요청을 처리하는 것은 클라이언트와 요청을 처리하는 객체들 사이의 결합도를 높이는 문제점이 존재한다. handleRequest는 각 객체 또는 메서드가 어떤 작업을 수행해야 하는지 알고, 대응되는 조건과 대응하여 각각을 직접 연결해야 한다. 따라서 이 상태에서는 책임을 변경하거나 확장하기 어려우며, handleRequest는 지나치게 구체적인 내용을 담게 되어 지나친 변동이 발생한다.

 이런 상황에서 책임 체인 패턴을 채택하여 책임을 분산할 수 있다. 요청 정보를 각 객체에서 체크하여 자신과 매칭되는 정보라면 요청에 대해 응답, 아니면 다른 객체에게 해당 요청을 넘기는 것이다. 이렇게 구현하는 경우 handleRequest에서 하나의 핸들러 함수에 요청을 넘기면 핸들러 함수들이 연속적으로 자신의 책임 영역인지 판단하여 처리할 수 있으므로 handleRequest 메서드의 수정 없이 효과적인 확장이 가능하다. 

특징

  1. 객체 간 결합도 감소: 각 객체는 다른 객체가 어떻게 요청을 처리하는지 알 필요가 없다. 클라이언트 입장에서는 자기가 요청을 넘길 첫번째 핸들러만 알고 있으면 되고, 각 핸들러들도 자기 다음에 넘길 핸들러만 알고 있으면 된다. 이를 통해 객체 간 상호작용이 단순화되는 효과가 있다.
  2. 책임 할당이 유연하다: 런타임에 책임 체인 내 데이터를 추가하거나 삭제하는 등 책임을 동적으로 분산할 수 있으므로 변경 및 확장이 쉽다.
  3. 요청을 처리한다는 확신은 없다: 각 객체가 자신의 책임만 알고 있으며, 어떤 객체가 어떤 책임을 가지고 있는지는 명시되지 않는다. 따라서 특정 요청을 처리하기 위한 객체가 정의되지 않았다면 요청이 버려질 수 있다.

후속 체인 구성법

 객체들의 구조 중에서는 이미 객체 사이의 연관이 존재하는 경우가 있다. 예를 들어 트리 구조의 부모-자식 관계 같은 연관이 사전에 존재한다면 해당 링크 구조 자체를 책임 체인 상에서 사용할 수 있다. 이 과정에서 컴포지트 패턴을 적용하는 등 좀 더 일반화도 가능하다.

 만약 이러한 링크가 없다면 책임에 대한 핸들러를 새로운 클래스로 구현하여 사용할 수 있다.

요청 정의 방법

 만약 요청이 여러 종류라면 다음과 같은 방법을 고려할 수 있다.

  1. 각 요청에 대응되는 핸들러 메서드를 각각 만들어서 사용한다. 간편하고 안전한 방식이지만, 정의된 연산에 대해서만 요청을 전달할 수 있다는 문제점이 있다. 만약 새로운 요청이 등장한다면 대응되는 코드를 책임 객체 각각에 대해 다시 작성해야 할 수 있다.
  2. 핸들러 함수는 하나만 두고, 요청은 Request 객체 형태로 파라미터를 통해 받는다. 세부적으로 Request 객체가 요청하고 있는 것이 무엇인지는 각 핸들러 내부에서 조건문을 통해 타입을 구체화하여 처리할 수 있다.

참여자

  1. Handler: 요청을 처리하는 기능과 후속 객체에 대한 연결을 제공하는 인터페이스로, ConcreteHandler이 구현한다.
  2. ConcreteHandler: 실제 요청에 대한 책임을 담당하는 객체이다. 자신에게 온 작업이라면 처리하고, 아니면 자신이 알고 있는 다음 객체에게 요청을 넘긴다.
  3. Client: 핸들러에게 요청을 보내는 존재로, 단일 핸들러만 알고 있어도 된다.

예시

 nodejs 진영에서 매우 많이 사용되는 express 서버는 현재 패턴의 장점을 잘 드러나게 사용하고 있다. express에서 가장 기본이 되는 단위는 미들웨어로, 다음과 같은구조를 가지고 있다.

export const function: RequestHandler = (req, res, next) => {
	// do something
    
    req.~ // request 객체의 정보를 이용하여 로직 처리
    
    res.~ // 사용자에게 응답해야 하는 경우 response 객체를 이용
    
    next() // 다음 미들웨어를 호출하는 경우 next() 사용.
}
  • req: 사용자가 요청한 정보를 담고 있는 객체로, 미들웨어 내부에서 사용되는 로직 등을 처리할 때 도움이 된다.
  • res: 요청에 대하여 사용자에게 응답할 때 사용되는 객체.
  • next: 다음 객체로 요청을 넘기고 싶을 때 호출하는 함수.

 위에서 설명한 책임체인 패턴과 다른 점은 express의 미들웨어는 다음 실행할 미들웨어가 누구인지도 모른다는 점이다. 사용자는 다음 올 미들웨어가 무엇인지 지정하지 않으며, 이런 체인의 관리는 express 자체가 수행한다.

https://medium.com/codex/the-design-behind-express-b1da569c7e43

 

The design behind Express

How express routing & middleware work from the design perspective

medium.com

 위 글에 따르면 express의 책임체인은 애초에 객체가 객체를 가리키는 방식이 아니라, 책임을 관리하는 객체가 배열 형태로 미들웨어를 관리, next( ) 가 호출되면 다음 미들웨어를 실행하는 방식으로 동작한다. 객체 호출 방식은 설명한 내용과 다르지만, 사용되는 철학은 책임체인 패턴에 해당한다.

// express 미들웨어 설정 일부
server.set('view engine', 'ejs');
server.use(e.static('public', {
    extensions: ['html', 'htm', 'js']
}));
server.use(e.json());
server.use(e.urlencoded({ extended: true }));
server.use(cookieParser());
server.use(authenticate);

예시 코드

// Chain Of Responsibility
interface StringChecker {
    check(str: string): boolean;
    setNext(next: StringChecker): void;
}

class UpperCaseChecker implements StringChecker {
    private next?: StringChecker;

    check(str: string) {
        for(let i = 0; i < str.length; i++) {
            if(str.charCodeAt(i) >= 97 && str.charCodeAt(i) <= 122)
            {
                console.log("소문자 감지");
                return false;
            }
        }
        return this.next? this.next.check(str) : true;
    }

    setNext(next:StringChecker) {
        this.next = next;   
    }
}

class AlnumChecker implements StringChecker {
    private next?: StringChecker;
    
    check(str: string) {
        for(let i = 0; i < str.length; i++) {
            const code = str.charCodeAt(i);
            if (!(code > 47 && code < 58) && // numeric (0-9)
                !(code > 64 && code < 91) && // upper alpha (A-Z)
                !(code > 96 && code < 123)) { // lower alpha (a-z)
              return false;
            } // https://stackoverflow.com/questions/4434076/best-way-to-alphanumeric-check-in-javascript
        }
        return this.next? this.next.check(str) : true;
    }

    setNext(next:StringChecker) {
        this.next = next;   
    }
}

 위 코드는 문자열에 대한 간단한 validation을 수행하는 클래스들을 포함한다. StringChecker 인터페이스는 Hander들에 대한 공통적인 인터페이스를 정의하고 있다. check는 일반적인 validation 기능을 의미하고, setNext는 다음 핸들러를 지정하는 데 사용된다.

 위와 같이 책임체인 패턴을 구성하면 여러개의 체커 클래스들이 연쇄적으로 문자열을 검사하여 결과를 반환하도록 구현할 수 있다. 현재 예제 같은 경우 체커 클래스를 늘릴 때마다 for문을 한번 더 순회해야 하고 효율이 좋지 않지만, checker을 문자 단위로 검사하도록 구현하고 메인이 되는 체커에서 문자 단위로 처리하면 for문 1번 순회로 처리가 가능하다. 만약 더 복잡한 validation을 요구한다면 책임체인 패턴이 유효할 수 있다. 

'CS > 디자인패턴' 카테고리의 다른 글

[디자인패턴] 팩토리 패턴들  (1) 2023.05.16
[디자인패턴] Observer 패턴  (0) 2023.05.08
[디자인패턴] Proxy 패턴  (0) 2023.05.02
[디자인패턴] Mediator 패턴  (0) 2023.04.30
[디자인패턴] Facade 패턴  (0) 2023.04.30