본문 바로가기
카테고리 없음

[소프트웨어 분석 및 설계] L20. Chain of Responsibility Pattern, Command Pattern

by whiteTommy 2024. 12. 13.
반응형

Chain of Responsibility Pattern

  • 책임 연쇄 패턴 (chain of responsibility) 이란?
    • 클라이언트의 요청에 대한 세세한 처리를 한 객체가 전부 하는 것이 아닌, 여러 개의 처리 객체들로 나누고 이들을 사슬(chain) 처럼 연결해 연쇄적으로 처리하는 행동 패턴
      • Handler (핸들러) : 처리 객체를 지칭, 요청 받으면 각 핸들러는 요청을 처리할 수 있는지 판단하고, 없으면 다음 핸들러로 책임을 전가
  • 책임 연쇄 패턴이 필요한 상황
    • 온라인 주문 시스템을 개발하려고 하는 예시에서,
      • 인증된 사용자들만 주문을 생성할 수 있도록 시스템에 대한 접근을 제한
      • 관리 권한이 있는 사용자들에게는 모든 주문에 대한 전체 접근 권한을 부여
    • 이러한 검사들을 차례대로 수행하도록 구성했다고 가정


    • 검사 조건을 추가하는 상황
      • 당신은 요청 내의 데이터를 정제(sanitize) 하는 추가 유효성 검사 단계를 추가
      • 시스템이 무차별 대입 공격을 방어하기 위해 같은 IP 주소에서 오는 반복적으로 실패한 요청을 걸러내는 검사를 즉시 추가
      • 데이터가 포함된 반복 요청에 대해 캐시된 결과를 반환하여 시스템 속도를 개선
  • 책임 연쇄 패턴의 아이디어
    • 특정 행동들을 핸들러라는 독립 실행형 객체로 변환
    • 핸들러를 체인으로 연결해서 체인을 따라 요청을 처리할 수 있도록 함
    • 경우에 따라서는 핸들러가 요청을 체인 뒤로 더이상 전달 하지 않고 추가 처리를 중지하는 결정을 할 수도 있음
  • 책임 연쇄 패턴의 구조
  • 책임 연쇄 패턴 예시 코드
    class Client{
        public static void main(String[] args){
        	String url1 = "http://www.youtube.com:80";
            System.out.println("INPUT: " + url1);
            URLParser.run(url1);
            
            String url2 = "https://www.inpa.tistory.com:443";
            System.out.println("INPUT: " + url2);
            URLParser.run(url1);
            
            String url3 = "http://localhost:8080";
            System.out.println("INPUT: " + url3);
            URLParser.run(url1);
        }
    }

class URLParser {
    public static void run(String url) {
        // Protocol 처리
        int index = url.indexOf("://");
        if (index != -1) System.out.println("PROTOCOL: " + url.substring(0, index));
        else System.out.println("NO PROTOCOL");

        // Domain 처리
        int startIndex= url.indexOf("://");
        int lastIndex= url.lastIndexOf(":");
        
        System.out.print("DOMAIN : ");
        if (startIndex== -1) {
        	if (lastIndex== -1) System.out.println(url);
        	else System.out.println(url.substring(0, lastIndex));
        } else if (startIndex!= lastIndex) {
        	System.out.println(url.substring(startIndex+ 3, lastIndex));
        } else {
        	System.out.println(url.substring(startIndex+ 3));
        }

        // Port 처리
        int index2 = url.lastIndexOf(":");
        if (index2 != -1) {
        	String strPort= url.substring(index2 + 1);
        	try {
        		int port = Integer.parseInt((strPort));
        		System.out.println("PORT : " + port);
        	} catch (NumberFormatExceptione) {
        		e.printStackTrace();
        	}
        }
    }
}

abstract class Handler {
    protected Handler nextHandler = null;

    public Handler setNext(Handler handler) {
        this.nextHandler = handler;
        return handler;
    }

    public void run(String url) {
        process(url);
        
        if (nextHandler != null) {
            nextHandler.run(url);
        }
    }

    protected abstract void process(String url);
}

class ProtocolHandler extends Handler {
    @Override
    protected void process(String url) {
        int index = url.indexOf("://");
        if (index != -1) {
            System.out.println("PROTOCOL: " + url.substring(0, index));
        } else {
            System.out.println("NO PROTOCOL");
       }
    }
}

class DomianHandler extends Handler {
    @Override
    protected void process(String url) {
    	int startIndex = url.indexOf("://");
        int lastIndex = url.lastIndexOf(":");
        
        System.out.print("DOMAIN : ");
        if (startIndex == -1) {
            if (lastIndex == -1) System.out.println(url);
            else System.out.println(url.substring(0, lastIndex));
        } else if (startIndex != lastIndex) {
            System.out.println(url.substring(startIndex + 3, lastIndex));
        } else {
        	System.out.println(url.substring(startIndex + 3));
	}
    }
}


 class PortHandler extends Handler {
     @Override
     protected void process(String url) {
         int index = url.lastIndexOf(":");
         if (index != -1) {
             String strPort = url.substring(index + 1);
             try {
                 int port = Integer.parseInt((strPort));
                 System.out.println("PORT : " + port);
             } catch (NumberFormatException e) {
                 e.printStackTrace();
             }
         }
     }
 }
 
 class Client{
     public static void main(String[] args){
     	Handler handler1 = new ProtocolHandler();
        Handler handler2 = new DomainHandler();
        Handler handler3 = new PortHandler();
        
        handler1.setNext(handler2).setNext(handler3);
        
        String url1 = "http://www.youtube.com:80";
        System.out.prinln("INPUT: " + url1);
        handler1.run(url1);
        
        String url2 = "https://www.inpa.tistory.com:443";
        System.out.prinln("INPUT: " + url2);
        handler1.run(url2);
     }
 }

 

  • 사용 시기
    • 특정 요청을 2개 이상의 여러 객체에서 판별하고 처리해야 할 때
    • 특정 순서로 여러 핸들러를 실행해야 하는 경우
    • 프로그램이 다양한 방식과 종류의 요청을 처리할 것으로 예상되나, 요청 유형과 순서를 미리 알 수 없는 경우
    • 요청을 처리할 수 있는 객체 집합이 동적(런타임)으로 정의되어야 할 때
  • 장점
    • 다형성
    • 클라이언트는 처리 객체의 체인 집합 내부의 구조를 알 필요가 없음
    • 각각의 체인은 자신이 해야하는 일만 하기 때문에 새로운 요청 처리에 유연하게 확장 가능
    • OCP 준수
      • 클라이언트 코드를 변경하지 않고 체인을 동적으로 변경 가능
  • 단점
    • 실행 시에 코드의 흐름이 많아져서 과정을 살펴보기가 복잡함
    • 무한 사이클로 체인이 구성되면 무한 루프에 빠질 수 있음
    • 책임 연쇄로 인한 처리 지연 문제가 발생할 수 있음

 

Command Pattern

  • 명령 패턴이란?
    • 요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 재사용성을 높인 행동 패턴
      • 청을 보내는 쪽 (invoker)과 받는 쪽 (receiver) 을 커맨드를 이용하여 디커플링 하여 재사용을 높이고자 함
  • 명령 패턴이 필요한 상황
    • 텍스트 편집기 앱을 개발하고 있는 상황에서, 편집기의 다양한 작업을 위한 여러 버튼이 있는 도구 모음(toolbar) 를 만들고 있다고 하자.
      • 도구 모음의 버튼과 다양한 대화 상자들의 일반 버튼에 사용할 수 있는 Button 클래스를 생성
    • 버튼의 모양은 비슷해 보이지만 각각 다른 기능을 수행해야 함
    • 요청을 처리하기 위한 함수를 자식 클래스로 상속하고 오버라이딩하는 구조로 구성해야 한다면 기능별로 상속을 다 해주어야 함
    • 이렇게 되면 Button 클래스를 수정할 때마다 자식 클래스가 영향을 받음
    • 동일한 기능을 하는 로직이 버튼이 아닌 다른 형태로 표현될 때 해당 기능을 copy & paste 해야 함
      • 로직 수정 시 관련 모든 클래스 수정해주어야 함
  • 명령 패턴의 아이디어
    • 인터페이스 객체들이 요청을 직접 보내는 것이 아닌 Command 를 경유해서 보내자.
  • 명령 패턴에 의해 수정된 구조
    • 더 이상 다양한 클릭 행동들을 구현하기 위한 버튼의 여러 자식 클래스는 필요하지 않음
    • 커맨드는 사용자 인터페이스와 비즈니스 로직 레이어 간의 결합도를 줄이는 중간 레이어의 역할을 함
  • 명령 패턴의 구조
  • 명령 패턴이 적용되기 전의 예시 코드
    // Receiver: 실제 작업을 수행하는 클래스
    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 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 this.isStarted;
        }
    }
    
    // Invoker: 명령을 실행하는 클래스
    public class Button {
        private Light light;
    
        public Button(Light light) {
            this.light = light;
        }
    
        public void press() {
            light.off(); // 특정 행동에 하드코딩되어 있음
        }
    }
    
    // Main: 클라이언트 코드
    public class MyApp {
        public static void main(String[] args) {
            Button button = new Button(new Light());
            button.press(); // 불을 끕니다.
            button.press(); // 불을 끕니다.
        }
    }
  • 명령 패턴 적용 예시
    // Command: 명령의 인터페이스
    public interface Command {
        void execute();
    }
    
    
    // LightOnCommand: Light를 켜는 명령
    public class LightOnCommand implements Command {
        private Light light;
    
        public LightOnCommand(Light light) {
            this.light = light;
        }
    
        @Override
        public void execute() {
            light.on();
        }
    }
    
    // LightOffCommand: Light를 끄는 명령
    public class LightOffCommand implements Command {
        private Light light;
    
        public LightOffCommand(Light light) {
            this.light = light;
        }
    
        @Override
        public void execute() {
            light.off();
        }
    }
    
    // GameStartCommand: Game을 시작하는 명령
    public class GameStartCommand implements Command {
        private Game game;
    
        public GameStartCommand(Game game) {
            this.game = game;
        }
    
        @Override
        public void execute() {
            game.start();
        }
    }
    
    // GameEndCommand: Game을 종료하는 명령
    public class GameEndCommand implements Command {
        private Game game;
    
        public GameEndCommand(Game game) {
            this.game = game;
        }
    
        @Override
        public void execute() {
            game.end();
        }
    }
    
    public class Button{
        public void press(Command command){
        	command.execute();
        }
    }
    
    Button button = new Button();
    button.press(new GameStartCommand(new Game()));
    button.press(new LightOnCommand(new Light()));
  • 장점
    • SRP, OCP 준수
      • Invoker 와 Receiver 로 책임을 분리하고, 기존 클라이언트 코드에 변경 없이 새로운 커맨드 도입 가능
    • 클라이언트와 수신자 간의 결합 감소
      • 클라이언트는 어떤 커맨드가 어떤 객체에서 어떻게 실행되는지 세부사항을 알 필요가 없음
    • 명령 기록과 실행 취소/재실행(undo/redo)
      • Invoker 내부에 stack 을 사용하여 command history 관리 가능
  • 단점
    • Invoker 와 Receiver 사이에 완전히 새로운 레이어(command) 를 도입하기 때문에 코드가 더 복잡해진다.

 

 

 

 

 

 

반응형