[Design pattern] 3-2. 책임 연쇄 패턴(Chain of responsibility pattern)


Study/Design Pattern  2021. 11. 4. 19:09

안녕하세요. 명월입니다.


이 글은 디자인 패턴의 책임 연쇄 패턴(Chain of responsibility pattern)에 대한 글입니다.


책임 연쇄 패턴이란 클래스 안에 연결 리스트 알고리즘을 걸고, 특정 함수를 실행하면 연속적으로 실행하는 패턴을 이야기합니다.

출처 - https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern

이게 굉장히 많이 사용되는 패턴은 아닙니다만, 로그 처리나 하나의 처리로 여러가지 결과를 동시에 만들어야 할 때 사용하는 패턴입니다.

#pragma once
#include <stdio.h>
#include <iostream>
using namespace std;
// Logger 인터페이스
class ILogger {
public:
  // 추상 함수
  virtual void write(const char* message) = 0;
  ~ILogger() {}
};
// LoggerManager 클래스
class LoggerManager : public ILogger {
private:
  // 내부 연결 리스트를 만들기 위한 인라인 클래스
  class Pointer {
  public:
    // Logger 객체 변수
    ILogger* logger;
    // 다음 포인터 변수
    Pointer* next = nullptr;
    // 소멸자
    ~Pointer() {
      // logger를 메모리에서 제거한다.
      delete logger;
    }
  };
  // 연결리스트 포인터 변수
  Pointer* first = nullptr;
  Pointer* end = nullptr;
public:
  // 로그 작성 함수(재정의)
  virtual void write(const char* message) {
    // 연결리스트 처음 포인터
    Pointer* n = first;
    // 연결리스트 순서대로 루프
    while (n != nullptr) {
      // 등록된 Logger에 write 함수 호출
      n->logger->write(message);
      // 다음 포인터로 이동
      n = n->next;
    }
  }
  // Logger 설정
  void setLogger(ILogger* logger) {
    // 포인터 인스턴스 생성
    Pointer* p = new Pointer();
    // Logger 설정
    p->logger = logger;
    // 만약 리스트의 객체가 0개라면 first에 넣는다.
    if (first == nullptr) {
      first = p;
      end = p;
      return;
    }
    // 객체가 0개가 아니면 리스트를 연결한다.
    end->next = p;
    end = p;
  }
  // 소멸자
  ~LoggerManager() {
    // 리스트 처음 객체
    Pointer* d = first;
    // 연결리스트 순서대로 루프
    while (d != nullptr) {
      // swap
      Pointer* b = d->next;
      // 포인터 삭제
      delete d;
      // swap
      d = b;
    }
  }
};
// ConsoleLogger 클래스
class ConsoleLogger : public ILogger {
public:
  // 로그 작성(함수 재정의)
  virtual void write(const char* message) {
    // 콘솔 출력
    cout << "ConsoleLogger - " << message << endl;
  }
};
// FileLogger 클래스
class FileLogger : public ILogger {
public:
  // 로그 작성(함수 재정의)
  virtual void write(const char* message) {
    // 콘솔 출력
    cout << "FileLogger - " << message << endl;
  }
};

// 실행 함수
int main() {
  // LoggerManager 인스턴스 생성
  LoggerManager manager;
  // ConsoleLogger 인스턴스 추가
  manager.setLogger(new ConsoleLogger());
  // FileLogger 인스턴스 추가
  manager.setLogger(new FileLogger());
  // 로그 작성
  manager.write("hello world");

  return 0;
}

위 소스 코드를 작성하다 보니 완전히 연결리스트 알고리즘이 되어 버렸네요. 연결리스트 알고리즘을 잘 아시는 분들은 쉽게 눈에 들어올꺼 같은데, 알고리즘이 약하신 분들에게는 굉장히 복잡해 보일 수도 있겠습니다.

내용은 제가 LoggerManager에 ConsoleLogger와 FileLogger의 인스턴스를 넣었습니다. 그리고 write 함수를 호출하니 순서대로 콘솔에 출력이 되네요.

즉, setLogger 함수에 인스턴스를 넣은 수만큼 write 함수에서 연쇄적으로 출력하는 패턴입니다.

import java.util.LinkedList;
import java.util.List;
// Logger 인터페이스
interface ILogger {
  // 추상 함수
  void write(String msg);
}
// LoggerManager 클래스
class LoggerManager implements ILogger {
  // 연결 리스트 변수
  private List<ILogger> loggers = new LinkedList<>();
  // Logger 설정
  public void setLogger(ILogger logger) {
    // Logger 추가
    loggers.add(logger);
  }
  // 로그 작성 함수(재정의)
  public void write(String msg) {
    // list에 추가되었던 Logger
    for (var l : loggers) {
      // write 함수 호출
      l.write(msg);
    }
  }
}
// ConsoleLogger 클래스
class ConsoleLogger implements ILogger {
  // 로그 작성(함수 재정의)
  public void write(String msg) {
    // 콘솔 출력
    System.out.println("ConsoleLogger - " + msg);
  }
}
// FileLogger 클래스
class FileLogger implements ILogger {
  // 로그 작성(함수 재정의)
  public void write(String msg) {
    // 콘솔 출력
    System.out.println("FileLogger - " + msg);
  }
}
// 실행 함수 클래스
public class Program {
  // 실행 함수
  public static void main(String[] args) {
    // LoggerManager 인스턴스 생성
    var manager = new LoggerManager();
    // ConsoleLogger 인스턴스 추가
    manager.setLogger(new ConsoleLogger());
    // FileLogger 인스턴스 추가
    manager.setLogger(new FileLogger());
    // 로그 작성
    manager.write("hello world");
  }
}

Java에는 연결 리스트의 LinkedList 클래스가 있습니다. 즉, Java에는 연결 리스트가 구현이 되어 있어서 사용하면 됩니다.

물론 꼭 LinkedList일 필요는 없이 ArrayList여도 상관은 없습니다. 즉, 책임 연쇄 패턴(Chain of responsibility pattern)은 포인터로 연결해야 한다라고 하지만, C/C++에서도 vector 객체를 사용해도 구현 자체는 문제가 없습니다.

using System;
// Logger 추상 클래스
abstract class ALogger
{
  // 다음 Logger 포인터 프로퍼티
  protected ALogger Next
  {
    get; private set;
  }
  // 다음 포인터 Logger 설정
  public ALogger SetNextLogger(ALogger logger)
  {
    // 프로퍼티에 인스턴스 설정
    Next = logger;
    // 인스턴스 리턴
    return logger;
  }
  // 다음 인스턴스에 Write 함수 호출
  public virtual void Write(string data)
  {
    // 다음 포인터가 null 아니면
    if (Next != null)
    {
      // Write 함수 실행
      Next.Write(data);
    }
  }
}
// ConsoleLogger 클래스
class ConsoleLogger : ALogger
{
  // 함수 재정의
  public override void Write(string data)
  {
    // 콘솔 출력
    Console.WriteLine("ConsoleLogger - " + data);
    // 다음 포인터의 인스턴스의 함수 호출
    base.Write(data);
  }
}
// FileLogger 클래스
class FileLogger : ALogger
{
  // 함수 재정의
  public override void Write(string data)
  {
    // 콘솔 출력
    Console.WriteLine("FileLogger - " + data);
    // 다음 포인터의 인스턴스의 함수 호출
    base.Write(data);
  }
}
// MailLogger 클래스
class MailLogger : ALogger
{
  // 함수 재정의
  public override void Write(string data)
  {
    // 콘솔 출력
    Console.WriteLine("MailLogger - " + data);
    // 다음 포인터의 인스턴스의 함수 호출
    base.Write(data);
  }
}


// 실행 클래스
class Program
{
  // 실행 함수
  static void Main(string[] args)
  {
    // ConsoleLogger 인스턴스 생성
    var logger = new ConsoleLogger();
    // SetNextLogger 함수로 순서대로 FileLogger 인스턴스와 MailLogger 인스턴스 추가
    logger.SetNextLogger(new FileLogger())
          .SetNextLogger(new MailLogger());
    // 로그 작성
    logger.Write("Hello world");
    // 아무 키나 누르면 종료
    Console.WriteLine("Press Any key...");
    Console.ReadLine();
  }
}

굳이 LoggerManager라는 클래스가 필요없이 Logger 클래스에 다음 포인터를 연결해서 최초에 생성된 인스턴스의 함수를 호출하면 연쇄적으로 실행하는 방법도 있습니다.


책임 연쇄 패턴은 클래스의 결합도를 낮추어서 여러가지 응용에 꽤 좋은 패턴입니다만, 의외로 사용 빈도가 낮은 패턴입니다. 적용할 만한 사양이 많지가 않습니다.

대부분 퍼사드 패턴과 전략 패턴으로 해결되는 사양들이 많다보니...


여기까지 디자인 패턴의 책임 연쇄 패턴(Chain of responsibility pattern)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.