행위 디자인패턴(1) - 책임연쇄 패턴, 커맨드 패턴, 인터프리터 패턴

책임연쇄 패턴

단일책임원칙의 책임이라고 생각하면 된다.

  public static void main(String[] args) {
        Request request = new Request("무궁화 꽃이 피었습니다.");
        RequestHandler requestHandler = new RequestHandler();
        requestHandler.handler(request);
    }
public class Request {

    private String body;
    public Request(String body) {
        this.body = body;
    }
    public String getBody() {
        return body;
    }
    public void setBody(String body) {
        this.body = body;
    }
}
public class RequestHandler {

    public void handler(Request request) {
        System.out.println(request.getBody());
    }
}

다음과 같은 코드가있을떄 인증/인가를 하려고한다.

public class RequestHandler {

     public void handler(Request request) {
        System.out.println("인증이 된건가");
        System.out.println("이 핸들러를 사용할수있는 유저인가");
        // 단일책임의 원칙을 위반한다 .

        System.out.println(request.getBody());
    }
}

첫번째방식으로 handler 의 코드를 바꾸는 방법이있다.
그러나 이방법은 SRP를 위반한다

public class AuthRequestHandler extends RequestHandler {

    public void handler(Request request) {
        System.out.println("인증이 되었나?");
        System.out.println("이 핸들러를 사용할 수 있는 유저인가?");
        super.handler(request);
    }
}

두번째방식은 새로운 핸들러를 만드는방법이다

  public static void main(String[] args) {
        Request request = new Request("무궁화 꽃이 피었습니다.");
        RequestHandler requestHandler = new AuthRequestHandler(); //변화 
       
        requestHandler.handler(request);
    }

변화되는 클라이언트의 코드
SRP를 지킬순있게되었지만 ,여전히 남아있는문제는
클라이언트가 AuthRequestHandler 를 선택해야한다.
그리고 만약 기능이 추가된다면

public class LoggingRequestHandler extends RequestHandler {

    @Override
    public void handler(Request request) {
        System.out.println("로깅");
        super.handler(request);
    }
}

다음과같이 로깅기능이 추가된다면

 public static void main(String[] args) {
        Request request = new Request("무궁화 꽃이 피었습니다.");
        RequestHandler requestHandler = new LoggingRequestHandler(); //변경
        requestHandler.handler(request);
    }

클라이언트는 또다시 로깅을 선택해야만한다.
클라이언트가 사용해야되는 핸들러를 직접 알아야된다는점이 문제다.
만약 로깅도 하고 인증도 하고싶으면어떡하지?

여기에 책임연쇄패턴을 적용한다.

클라이언트는 구체적인 핸들러타입을 모르게끔 변경하는것이 목표다.

public abstract class RequestHandler {
    private RequestHandler nextHandler;

    public RequestHandler(RequestHandler nextHandler) {
        this.nextHandler = nextHandler;
    }

    public void handle(Request request) {
        if (nextHandler != null) {
            nextHandler.handle(request);
        }
    }
}

public class PrintRequestHandler extends RequestHandler{
    public PrintRequestHandler(RequestHandler nextHandler) {
        super(nextHandler);
    }

    @Override
    public void handle(Request request) {
        System.out.println(request.getBody());
        super.handle(request);
    }
}
public class AuthRequestHandler extends RequestHandler{

    public AuthRequestHandler(RequestHandler nextHandler) {
        super(nextHandler);
    }

    @Override
    public void handle(Request request) {
        System.out.println("인증이 되었는가?");
        super.handle(request);
    }
}
public class LoggingRequestHandler extends RequestHandler{

    @Override
    public void handle(Request request) {
        System.out.println("로깅");
        super.handle(request);
    }

    public LoggingRequestHandler(RequestHandler nextHandler) {
        super(nextHandler);
    }
}
public class Client {

    private RequestHandler requestHandler;

    public Client(RequestHandler requestHandler) {
        this.requestHandler = requestHandler;
    }

    public void doWork() {
        Request request = new Request("이번 놀이는 뽑기입니다.");
        requestHandler.handle(request);
    }

    public static void main(String[] args) {
        RequestHandler chain = new AuthRequestHandler(new LoggingRequestHandler(new PrintRequestHandler(null)));
        Client client = new Client(chain);
        client.doWork();
    }
}

모든핸들러를 거쳐갈수있고, 하나의핸들러만 작동할수있게끔도 가능하다.
중요한건 클라이언트가 어떤핸들러를 써야할지 정하지않아도 된다는것이다.
요청/응답처리할때 많이사용되는 패턴이다.

장점

  • 클라이언트 코드를 변경하지않고 새로운 핸들러를 체인에 추가할수있다.
  • 각각의 체인은 자신이 해야하는일만 한다.
  • 체인을 다양한 방법으로 구성할수있다.

단점

  • 디버깅할때 어려울수있다.

사용사례

  public static void main(String[] args) {
        Filter filter = new Filter() {
            @Override
            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
                //  전처리
                chain.doFilter(request, response);
                //  후처리
            }
        };
    }

서블릿 필터에서 사용될수있다.
요청이 서블릿에 가기전에 여러개의 필터를 거치면서
최종적으로 서블릿까지 가게된다.


@WebFilter(urlPatterns = "/hello")
public class MyFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("게임에 참하신 여러분 모두 진심으로 환영합니다.");
        chain.doFilter(request, response);
        System.out.println("꽝!");
    }
}
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

@ServletComponentScan
@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

커맨드 패턴

요청을 캡슐화 하여 호출자(invoker)와 수신자를 분리하는 패턴

  • 요청을 처리하는 방법이 바뀌더라도 호출자의 코드는 변경되지않는다.

public class Light {

    private boolean isOn;

    public void on() {
        System.out.println("불을 켭니다.");
        this.isOn = true;
    }

    public void off() {
        System.out.println("불을 끕니다.");
        this.isOn = false;
    }

    public boolean isOn() {
        return this.isOn;
    }
}

public class Button {

    private Light light;

    public Button(Light light) {
        this.light = light;
    }

    public void press() {
        light.off(); 
        //버튼을 켜야한다면 이부분을 변경해야한다. 
    }

    public static void main(String[] args) {
        Button button = new Button(new Light());
        button.press();
        button.press();
        button.press();
        button.press();
    }
}



public class Game {

    private boolean isStarted;

    public void start() {
        System.out.println("게임을 시작합니다.");
        this.isStarted = true;
    }

    public void end() {
        System.out.println("게임을 종료합니다.");
        this.isStarted = false;
    }

    public boolean isStarted() {
        return isStarted;
    }
}


public class MyApp {

    private Game game;

    public MyApp(Game game) {
        this.game = game;
    }

    public void press() {
        game.start();
    }

    public static void main(String[] args) {
        Button button = new Button(new Light());
        button.press();
        button.press();
        button.press();
        button.press();
    }
}

불을끄거나, 게임클래스를쓰거나 변경될때마다 코드를 고쳐줘야한다.

Light, Game 이 Receiver에해당
버튼, MyApp 의코드들 이 Invoker에 해당한다.
커맨드패턴을 적용해보자

public interface Command {

    void execute();
}

public class GameStartCommand implements Command{

    private Game game;

    public GameStartCommand(Game game) {
        this.game = game;
    }

    @Override
    public void execute() {
        game.start();
    }
    
}

public class GameEndCommand implements Command{

    private Game game;

    public GameEndCommand(Game game) {
        this.game = game;
    }

    @Override
    public void execute() {
        game.end();
    }
}

public class LightOnCommand implements Command{

    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on();
    }
    
}

public class LightOffCommand implements Command{

    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.off();
    }

}
public class Button {

    private Command command;

    public Button(Command command) {
        this.command = command;
    }

    public void press() {
        command.execute();

    }

    public static void main(String[] args) {
        Button button = new Button(new LightOnCommand(new Light())); //불을키고싶다. 
        //게임을 시작하고싶다면 
        //new GameStartCommand(new Game()))); 
        
        
        button.press();
        button.press();
    }
}

invoker 쪽의 코드는 바뀌지않지만
command 쪽의 코드만 변경하면된다.
버튼쪽코드가 변경되다가 커맨드쪽의 코드가 변경되는
조삼모사아닌가?

특정 조건에서 커맨드들은 바뀔수밖에 없다.
invoker 들의 코드가 변경되지않는것이 중요하고
변화되는부분이 최소화된다고 생각하면 될듯하다.

장점

  • 기존코드를 변경하지않고 새로운 커맨드를 만들수있다.
  • 수신자의 코드가 변경되어도 호출자의 코드는 변경되지않는다.
  • 커맨드 객체를 로깅,DB저장, 네트워크로 전송하는등 다양한 방법으로 활용할수도있다.

단점

  • 코드가 복잡하고 클래스가 많아진다.

인터프리터 패턴

어원은 통역하는사람 혹은 연주자의 뜻을 가질수도있다.

public class PostfixNotation {

    private final String expression;

    public PostfixNotation(String expression) {
        this.expression = expression;
    }

    public static void main(String[] args) {
        PostfixNotation postfixNotation = new PostfixNotation("123+-");
        postfixNotation.calculate();
    }

    private void calculate() {
        Stack<Integer> numbers = new Stack<>();

        for (char c : this.expression.toCharArray()) {
            switch (c) {
                case '+':
                    numbers.push(numbers.pop() + numbers.pop());
                    break;
                case '-':
                    int right = numbers.pop();
                    int left = numbers.pop();
                    numbers.push(left - right);
                    break;
                default:
                    numbers.push(Integer.parseInt(c + ""));
            }
        }

        System.out.println(numbers.pop());
    }
}

후위표기식을 계산해주는 코드가있다
후위표기식이라는것을 자주쓰고 123+- 라는 수치만 바뀐다면 우린 다른숫자를 넣기만하고
계산을 해주게끔 하는 식이 필요할수도있다.

Context = 공통된 정보가 들어있는곳. 글로벌한 값들이 모여있는곳
Expression = 그러한 값들을 참조
TerminalExpression = x,y,z 값


public interface PostfixExpression {
    int interpret(Map<Character, Integer> context);
}



public class PlusExpression implements PostfixExpression{

    private PostfixExpression left, right;

    public PlusExpression(PostfixExpression left, PostfixExpression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Map<Character, Integer> context) {

        return left.interpret(context) + right.interpret(context);
    }
}




public class MinusExpression implements PostfixExpression{

    private PostfixExpression left, right;


    public MinusExpression(PostfixExpression left, PostfixExpression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Map<Character, Integer> context) {

        return left.interpret(context) - right.interpret(context);
    }
}





public class PostfixParser {

    public static PostfixExpression parse(String expression) {
        Stack<PostfixExpression> stack = new Stack<>();
        for (char c : expression.toCharArray()) {
            stack.push(getExpression(c, stack));
        }
        return stack.pop();
    }

    private static PostfixExpression getExpression(char c, Stack<PostfixExpression> stack) {
        switch (c) {
            case '+':
                return new PlusExpression(stack.pop(), stack.pop());
            case '-':
                PostfixExpression right = stack.pop();
                PostfixExpression left = stack.pop();
                return new MinusExpression(left, right);
            default:
                return new VariableExpression(c);
        }

    }


}






public class VariableExpression implements PostfixExpression{

    private Character variable;

    public VariableExpression(Character variable) {
        this.variable = variable;
    }

    @Override
    public int interpret(Map<Character, Integer> context) {
        return context.get(variable);
    }
}






public class App {
    public static void main(String[] args) {
        PostfixExpression expression = PostfixParser.parse("xyz+-");
        int result = expression.interpret(Map.of('x', 1, 'y', 2, 'z', 3));
        System.out.println(result);
    }
}

인터프리터 패턴의 장점

  • 기존코드를 변경하지않고 Expression을 추가할수있다.

단점

  • 복잡도가 증가한다

구현비용과 자주쓸만한 패턴인지 신경써서 적용을 고려해야한다.

좋은 웹페이지 즐겨찾기