디자인 패턴 - chain of responsibility

업데이트:

Chain of Responsibility

Chain of Responsibility디자인 패턴은 작업 요청을 받은 객체(sender)가 실제 작업자(receiver)에게 그 책임을 위임하는 구조에서 사용하는 설계 기법이다. 이 디자인 패턴의 장점으로는

  • 작업자 간에 연결 고리를 구축하여 작업을 나누어 처리할 수 있다.

  • 체인 방식이기 때문에 작업에 참여하는 모든 객체가 서로 알 필요가 없다.

  • 오직 자신과 연결된 다음 작업자만 알면 되기 때문에 객체 간에 결합도를 낮추는 효과가 있다.

  • 런타임에서 연결 고리를 변경하거나 추가할 수 있어, 상황에 따라 실시간으로 기능을 추가하거나 삭제할 수 있다.

  • 보통 필터링을 구현할 때 이 설계 기법을 많이 사용한다.

디자인 패턴을 적용하기 전

image

디자인 패턴을 적용하기 전에는 새로운 기능을 추가하기 위해서는 기존 클래스파일 사이사이에 코드를 집어넣어야 했다. 기존 코드 사이에 새 코드를 넣게 되면 가독성도 안좋을 뿐 아니라 유지보수도 힘들어진다.

디자인 패턴 적용 후

image

쉽게 생각하자면 요청자가 필터1을 호출하면 필터1은 그다음 필터를 계속 호출한다. 제일 마지막에 있는 필터가 최종적으로 작업자를 호출하고 작업자가 수행안 작업을 순차적으로 리턴받아 요청자에게 전달한다. chain을 구현하면 작업자가 작업 수행 전/후에 기능을 삽입하기가 쉬워 유지보수가 굉장히 좋아진다.

구체적인 구조

image

필터가 다음 필터를 호출하는 것은 LinkedList 자료구조와 비슷하다.

구현

필터에게 제공할 정보를 다루는 Request 정의

package com.eomcs.pms.handler;

import java.util.Map;

public class Request {
  String commandPath;
  Map<String, Object> context;

  public Request(String commandPath, Map<String,Object> context) {
    this.commandPath = commandPath;
    this.context = context;
  }

  public String getCommandPath() {
    return commandPath;
  }

  public Map<String, Object> getContext() {
    return context;
  }
}
  • 사용자 입력이 들어오면 Request객체를 준비할 수 있도록 App에서 Request객체를 생성한다.
Request request = new Request(inputStr, context);

커맨드 실행 전/후에 삽입되는 필터의 호출 규칙(인터페이스) 정의하기

CommandFilter인터페이스

필터 관리자가 호출할 메서드 규칙을 정의한다.

public interface CommandFilter {
  void doFilter(Request request, FilterChain next) throws Exception;
}

FilterChain인터페이스

필터가 다음 필터를 실행시키기 위해 필터 관리자에게 요청하는 메서드 규칙을 정의한다.

public interface FilterChain {
  void doFilter(Request request) throws Exception;
}

CommandFilterManager

필터들을 관리하고 실행 순서에 따라 필터를 실행시키는 클래스를 정의한다.

package com.eomcs.pms.filter;

import com.eomcs.pms.handler.Request;

public class CommandFilterManager {
  // 역할 :
  // CommandFilter 구현체를 관리하고 실행시킨다.

  Chain firstChain;
  Chain lastChain;

  public void add(CommandFilter filter) {
    Chain chain = new Chain(filter);
    if(lastChain == null) {
      firstChain = lastChain = chain;
      return;
    }
    lastChain.nextChain = chain;
    lastChain = chain;
  }

  public FilterChain getFilterChains() {
    return firstChain;
  }

  private static class Chain implements FilterChain {
    CommandFilter filter;
    Chain nextChain;

    public Chain(CommandFilter filter) {
      this.filter = filter;
    }

    @Override
    public void doFilter(Request request) throws Exception {
      filter.doFilter(request, nextChain);
    }
  }
}

App에 FilterManager와 체인 실행하기

// 필터 관리자 준비
CommandFilterManager filterManager = new CommandFilterManager();

// 사용자가 명령을 처리할 필터 체인을 얻는다.
FilterChain filterChain = filterManager.getFilterChains();

// 커맨드나 필터가 사용할 객체를 준비한다.
Request request = new Request(inputStr, context);

// 필터들의 체인을 실행한다.
if (filterChain != null) {
filterChain.doFilter(request);
}

DefaultCommandFilter - 사용자가 입력한 명령을 처리할 커맨드를 찾아 실행시키는 필터

import java.util.Map;
import com.eomcs.pms.handler.Command;
import com.eomcs.pms.handler.Request;

public class DefaultCommandFilter implements CommandFilter {
  @SuppressWarnings("unchecked")
  @Override
  public void doFilter(Request request, FilterChain next) throws Exception {

    // Request 보관소에서 context 맵 객체를 꺼낸다.
    Map<String, Object> context = request.getContext();

    // context맵에서 커맨드 객체가 들어 있는 맵을 꺼낸다.
    Map<String,Command> commandMap = (Map<String, Command>) context.get("commandMap");
    
    // 사용자가 입력한 명령에 따라 커맨드 객체를 실행한다.
    Command command = commandMap.get(request.getCommandPath());
    if (command != null) {
      try {
        command.execute(context);
      } catch (Exception e) {
        // 오류가 발생하면 그 정보를 갖고 있는 객체의 클래스 이름을 출력한다.
        System.out.println("--------------------------------------------------------------");
        System.out.printf("명령어 실행 중 오류 발생: %s\n", e);
        System.out.println("--------------------------------------------------------------");
      }
    } else {
      System.out.println("실행할 수 없는 명령입니다.");
    }
  }
}

AuthCommandFilter - 로그인 유효성 검사를 하는 필터

public class AuthCommandFilter implements CommandFilter {

  @Override
  public void doFilter(Request request, FilterChain next) throws Exception {
    if(request.getCommandPath().equalsIgnoreCase("/login")
        || request.getContext().get("loginUser") != null) {
      next.doFilter(request);
    } else {
      System.out.println("로그인이 필요합니다.");
    }
  }
}

이런식으로 Filter를 추가해나가면 최종적으로 App에 필터를 등록하는 것은 이렇게 된다.

    // 필터 관리자 준비
    CommandFilterManager filterManager = new CommandFilterManager();

    filterManager.add(new LogCommandFilter(new File("command.log")));
    filterManager.add(new AuthCommandFilter());
    filterManager.add(new DefaultCommandFilter());

    // 사용자가 명령을 처리할 필터 체인을 얻는다.
    FilterChain filterChain = filterManager.getFilterChains();

기능 추가와 삭제, 위치변경이 굉장히 쉬워진 것을 알 수 있다.

댓글남기기