Chain Of Responsibility

최완식·2023년 1월 9일
0
post-thumbnail

책임 연쇄 패턴은 무엇일까?

위키에 실린 예시

@FunctionalInterface
public interface Logger {
    public enum LogLevel {
        INFO, DEBUG, WARNING, ERROR, FUNCTIONAL_MESSAGE, FUNCTIONAL_ERROR;

        public static LogLevel[] all() {
            return values();
        }
    }

    abstract void message(String msg, LogLevel severity);

    default Logger appendNext(Logger nextLogger) {
        return (msg, severity) -> {
            message(msg, severity);
            nextLogger.message(msg, severity);
        };
    }

    static Logger writeLogger(LogLevel[] levels, Consumer<String> stringConsumer) {
        EnumSet<LogLevel> set = EnumSet.copyOf(Arrays.asList(levels));
        return (msg, severity) -> {
            if (set.contains(severity)) {
                stringConsumer.accept(msg);
            }
        };
    }

    static Logger consoleLogger(LogLevel... levels) {
        return writeLogger(levels, msg -> System.err.println("Writing to console: " + msg));
    }

    static Logger emailLogger(LogLevel... levels) {
        return writeLogger(levels, msg -> System.err.println("Sending via email: " + msg));
    }

    static Logger fileLogger(LogLevel... levels) {
        return writeLogger(levels, msg -> System.err.println("Writing to Log File: " + msg));
    }
}

class Runner {
    public static void main(String[] args) {
        // Build an immutable chain of responsibility
        Logger logger = consoleLogger(LogLevel.all())
                .appendNext(emailLogger(LogLevel.FUNCTIONAL_MESSAGE, LogLevel.FUNCTIONAL_ERROR))
                .appendNext(fileLogger(LogLevel.WARNING, LogLevel.ERROR));

        // Handled by consoleLogger since the console has a LogLevel of all
        logger.message("Entering function ProcessOrder().", LogLevel.DEBUG);
        logger.message("Order record retrieved.", LogLevel.INFO);

        // Handled by consoleLogger and emailLogger since emailLogger implements Functional_Error & Functional_Message
        logger.message("Unable to Process Order ORD1 Dated D1 For Customer C1.", LogLevel.FUNCTIONAL_ERROR);
        logger.message("Order Dispatched.", LogLevel.FUNCTIONAL_MESSAGE);

        // Handled by consoleLogger and fileLogger since fileLogger implements Warning & Error
        logger.message("Customer Address details missing in Branch DataBase.", LogLevel.WARNING);
        logger.message("Customer Address details missing in Organization DataBase.", LogLevel.ERROR);
    }
}
  • 이건 좀 어려우니 정리해서 보자.

Logger Class

public abstract class Logger {
    private EnumSet<LogLevel> logLevels;
    private Logger next;

    public Logger(LogLevel[] levels) {
        this.logLevels = EnumSet.copyOf(Arrays.asList(levels));
    }

    public Logger setNext(Logger next) {
        this.next = next;

        return this.next;
    }
  • 추상 클래스로 선언하여 공통된 동작을 지정함
  • logLevel이라는 enum set으로 값들을 집합으로 들고 있고
  • next로 나다음의 로거를 가지고 있다.
  • 생성자에서 levels를 받는데, 이는 해당 로그에서 처리할 레벨을 정해준다.
    • 처리할 메시지들의 타입을 정해줌
  • setNext를 보니 fluent interface와 비슷하게 생겼다.
  • 근데 문제는 자기 자신을 반환하는게 아니고 next를 반환한다.
    • 응?
    • fluent에서 한단계 더 나아갔네?
    • 클라이언트 쪽에서는 자기를 반환하겠지 하고 생각하지만 그게 아닌 꼴
    • 좋은 디자인인 것 같지는 않음
    public final void message(String msg, LogLevel severity) {
        if (logLevels.contains(severity)) {
            log(msg);
        }

        if (this.next != null) {
            this.next.message(msg, severity);
        }
    }

    protected abstract void log(String msg);
}    
  • message 함수는 final이니 상속 불가다.
    • 메시지하고 로그 레벨 보내서 찍는 함수
    • 내부적으로 로거가 처리가능한 레벨인지 확인하고 찍어줌
    • 그리고 다음 로거에 전달해서 똑같이 찍어달라고 요청
  • log 함수는 상속받는 클래스에서 구현

ConsoleLogger Class

public class ConsoleLogger extends Logger {
    public ConsoleLogger(LogLevel[] levels) {
        super(levels);
    }

    @Override
    protected void log(String msg) {
        System.err.println("Writing to console: " + msg);
    }
}

EmailLogger Class

public class EmailLogger extends Logger {
    public EmailLogger(LogLevel[] levels) {
        super(levels);
    }

    @Override
    protected void log(String msg) {
        System.err.println("Sending via email: " + msg);
    }
}

FileLogger Class

public class FileLogger extends Logger {
    public FileLogger(LogLevel[] levels) {
        super(levels);
    }

    @Override
    protected void log(String msg) {
        System.err.println("Writing to file: " + msg);
    }
}

enum LogLevel

public enum LogLevel {
    INFO,
    DEBUG,
    WARNING,
    ERROR,
    FUNCTIONAL_MESSAGE,
    FUNCTIONAL_ERROR;

    public static LogLevel[] all() {
        return values();
    }
}

실제로 사용해보기

Logger logger = new ConsoleLogger(LogLevel.all());
logger
    .setNext(new EmailLogger(new LogLevel[]{LogLevel.FUNCTIONAL_MESSAGE, LogLevel.FUNCTIONAL_ERROR}))
    .setNext(new FileLogger(new LogLevel[]{LogLevel.WARNING, LogLevel.ERROR}));

// ConsoleLogger에서 처리 -> 모든 로그레벨 처리 가능
logger.message("Entering function ProcessOrder().", LogLevel.DEBUG);
logger.message("Order record retrieved.", LogLevel.INFO);

// ConsoleLogger, EmailLogger에서 처리 가능
logger.message("Unable to Process Order ORD1 Dated D1 For Customer C1.", LogLevel.FUNCTIONAL_ERROR);
logger.message("Order Dispatched.", LogLevel.FUNCTIONAL_MESSAGE);
  • next로 메시지를 호출하게 해서 연쇄적으로 다음 친구를 호출할 수 있게 된다.

올바른 책임 연쇄 패턴 예시

public final class LogManager {
    private static LogManager instance;

    private ArrayList<Logger> loggers = new ArrayList<Logger>();

    public static LogManager getInstance() {
        if (instance == null) {
            instance = new LogManager();
        }

        return instance;
    }

    public void addHandler(Logger logger) {
        this.loggers.add(logger);
    }

    public void message(String msg, LogLevel severity) {
        for (Logger logger : this.loggers) {
            logger.message(msg, severity);
        }
    }
}
  • 왜 굳이 next 호출해줌?
  • 이걸 다 관리하는 객체 하나만 있으면 되는데?
public abstract class Logger {
    private EnumSet<LogLevel> logLevels;

    public Logger(LogLevel[] levels) {
        this.logLevels = EnumSet.copyOf(Arrays.asList(levels));
    }

    public final void message(String msg, LogLevel severity) {
        if (logLevels.contains(severity)) {
            log(msg);
        }
    }

    protected abstracy void log(String msg);
}
  • next가 없다.
  • 나는 그냥 로그하나 찍고 끝!
LogManager logManager = LogManager.getInstance();

logManager.addHandler(new ConsoleLogger(LogLevel.all()));
logManager.addHandler(new EmailLogger(new LogLevel[]{LogLevel.FUNCTIONAL_MESSAGE, LogLevel.FUINCTIONAL_ERROR}));

logManager.message("Entering function ProcessOrder().", LogLevel.DEBUG);
  • 이게 훨씬 간단..
  • 굳이 "책임 연쇄 패턴" 이라는 것을 설명하기 위해 위의 예를 갖다둔게 잘못
  • 디자인 패턴 잘못 배우면 위험하다의 단적인 예
  • "연쇄"는 확인함. 근데 "책임"은 어디?

올바른 책임 연쇄 패턴

  • 어떤 메시지를 처리할 수 있는 여러 개체가 있음
  • 이 개체들은 차례대로 메시지를 처리할 수 있는 기회를 받음
  • 만약 그 중 한 개체가 메시지를 처리하면 그것에 대한 책임을 짐
    • 즉 다음 개체는 메시지를 처리할 기회를 받지 못함
    • 그래서 "책임" 연쇄 패턴임
  • iOS에서는 Responder chain이 그예라 할 수 있겠다.

위키피디아를 제대로 바꾸면..

public final void message(String msg, LogLevel severity) {
    if (logLevels.contains(severity)) {
        log(msg);
    }

    if (this.next != null) {
        this.next.message(msg, severity);
    }
}
public final void message(String msg, LogLevel severity) {
    if (logLevels.contains(severity)) {
        log(msg);
    } else if (this.next != null) {
        this.next.message(msg, severity);
    }
}
  • 내가 처리못하면 다음으로 넘겨
  • 내가 할 수 있으면 내가 하고 끝!

Reference

profile
Goal, Plan, Execute.

0개의 댓글