Design Pattern : 행동 패턴(Behavioral Patterns)

김하영·2021년 2월 8일
5
post-custom-banner

행동패턴(Behavioral Patterns) 이란?

객체나 클래스 사이의 알고리즘이나 책임 분배에 관련된 패턴이다.
한 객체가 수행할 수 없는 작업을 여러 개의 객체로 어떻게 분배하며 객체 사이의 결합도 최소화에 중점을 둔다.
패턴을 주로 클래스에 적용하는지 아니면 객체에 적용하는지에 따라 구분되는 패턴이다.

행동패턴 종류

행동패턴에는 아래와 같은 디자인 패턴이 존재한다.

  1. 책임연쇄 패턴(Chain of responsibility)
    : 책임들이 연결되어 있어 내가 책임을 못 질 것 같으면 다음 책임자에게 자동으로 넘어가는 구조이다.

  2. 커맨드 패턴(Command Pattern)
    : 명령어를 각각 구현하는 것보다는 하나의 추상 클래스에 메서드를 하나 만들고 각 명령이 들어오면 그에 맞는 서브 클래스가 선택되어 실행한다.

  3. 인터프리터 패턴(Interpreter Pattern)
    : 문법 규칙을 클래스화한 구조를 갖는 SQL 언어나 통신 프로토콜 같은 것을 개발할 때 사용한다.

  4. 이터레이터 패턴 (Iterator Pattern)
    : 반복이 필요한 자료구조를 모두 동일한 인터페이스를 통해 접근할 수 있도록 메서드를 이용해 자료구조를 활용할 수 있도록 해준다.

  5. 옵저버 패턴(Observer Pattern)
    : 어떤 클래스에 변화가 일어났을 때, 이를 감지하여 다른 클래스에 통보해준다.

  6. 전략 패턴 (Strategy Pattern)
    : 알고리즘 군을 정의하고 각각 하나의 클래스로 캡슐화한 다음, 필요할 때 서로 교환해서 사용할 수 있게 해준다.

  7. 템플릿 메서드 패턴 (Template method pattern)
    : 상위 클래스에서는 추상적으로 표현하고 그 구체적인 내용은 하위 클래스에서 결정된다.

  8. 방문자 패턴 (visitor Pattern)
    : 각 클래스의 데이터 구조로부터 처리 기능을 분리하여 별도의 visitor 클래스로 만들어놓고 해당 클래스의 메서드가 각 클래스를 돌아다니며 특정 작업을 수행한다.

  9. 중재자 패턴 (Mediator Pattern)
    : 클래스간의 복잡한 상호작용을 캡슐화하여 한 클래스에 위임해서 처리한다.

  10. 상태 패턴 (State Pattern)
    : 동일한 동작을 객체의 상태에 따라 다르게 처리해야 할 때 사용한다.

  11. 기념품 패턴 (Memento Pattern)
    : Ctrl + z 와 같은 undo 기능 개발할 때 유용한 디자인패턴. 클래스 설계 관점에서 객체의 정보를 저장한다.

1. 책임연쇄 패턴(Chain of responsibility)

  • 책임연쇄 패턴이란?

클라이언트 요청을 처리할 수 있는 처리객체를 집합(Chain)으로 만들어 부여함으로 결합을 느슨하기 위해 만들어진 디자인 패턴이다. 실제로 굉장히 많이 쓰이는 패턴 중 하나로 여러 개의 객체 중에서 어떤 것이 요구를 처리할 수 있는 지 사전에 알 수 없을 때 사용된다. 요청 처리가 들어오게 되면 그것을 수신하는 객체가 자신이 처리 할 수 없는 경우에는 다음 객체에게 문제를 넘김으로써 최종적으로 요청을 처리 할 수 있는 객체의 의해 처리가 가능하도록 하는 패턴이다.

  • 책임연쇄 패턴이 적용되는 경우
  1. 요청의 발신자와 수신자를 분리하는 경우

  2. 요청을 처리할 수 있는 객체가 여러개일 때 그 중 하나에 요청을 보내려는 경우

  3. 코드에서 처리객체(handler)를 명시적으로 지정하고 싶지 않은 경우

즉, 책임 연쇄 패턴은 요청을 처리할 수 있는 객체가 여러 개이고 처리객체가 특정적이지 않을 경우 권장되는 패턴이다.

  • 책임연쇄 패턴 장점
  1. 결합도를 낮추며, 요청의 발신자와 수신자를 분리시킬 수 있다.

  2. 클라이언트는 처리객체의 집합 내부의 구조를 알 필요가 없다.

  3. 집합 내의 처리 순서를 변경하거나 처리객체를 추가 또는 삭제할 수 있어 유연성이 향상된다.

  4. 새로운 요청에 대한 처리객체 생성이 매우 편리하다.

  • 책임연쇄 패턴 단점
  1. 충분한 디버깅을 거치지 않았을 경우 집합 내부에서 사이클이 발생할 수 있다.

  2. 디버깅 및 테스트가 쉽지 않다.

  • 책임연쇄 패턴 구조

  1. Handler
    요청을 수신하고 처리객체들의 집합에 전달하는 인터페이스이다.
    집합의 첫 번째 핸들러에 대한 정보만 가지고 있으며 그 이후의 핸들러에 대해서는 알 수 없다.

  2. Concrete Handler
    요청을 처리하는 실제 처리객체입니다.

  3. Client
    요청을 전달하는 클라이언트입니다.

  • 예제 코드

Chain : Handler

public interface Chain {
    void setNext(Chain nextInChain);
    void process(Number request);
}

Number

public class Number {
    private int number;

    public Number(int number) {
        this.number = number;
    }

    public int getNumber() {
        return number;
    }
}

NegativeProcessor : Concrete Handler

public class NegativeProcessor implements Chain {
    private Chain nextInChain;

    @Override
    public void setNext(Chain nextInChain) {
        this.nextInChain = nextInChain;
    }

    @Override
    public void process(Number request) {
        if (request.getNumber() < 0) {
            System.out.println("NegativeProcessor : " + request.getNumber());
        } else {
            nextInChain.process(request);
        }
    }
}

ZeroProcessor : Concrete Handler

public class ZeroProcessor implements Chain {
    private Chain nextInChain;

    @Override
    public void setNext(Chain nextInChain) {
        this.nextInChain = nextInChain;
    }

    @Override
    public void process(Number request) {
        if (request.getNumber() == 0) {
            System.out.println("ZeroProcessor : " + request.getNumber());
        } else {
            nextInChain.process(request);
        }
    }
}

PositiveProcessor : Concrete Handler

public class PositiveProcessor implements Chain {
    private Chain nextInChain;

    @Override
    public void setNext(Chain nextInChain) {
        this.nextInChain = nextInChain;
    }

    @Override
    public void process(Number request) {
        if (request.getNumber() > 0) {
            System.out.println("PositiveProcessor : " + request.getNumber());
        } else {
            nextInChain.process(request);
        }
    }
}

Main Class

public class ChainMain {
    public static void main(String[] args) {
        Chain c1 = new NegativeProcessor();
        Chain c2 = new ZeroProcessor();
        Chain c3 = new PositiveProcessor();

        c1.setNext(c2);
        c2.setNext(c3);

        c1.process(new Number(90));
        c1.process(new Number(-50));
        c1.process(new Number(0));
        c1.process(new Number(91));
    }
}

2. 커맨드 패턴(Command Pattern)

  • 커맨드 패턴이란?

커맨드 패턴은 해당 요청에 따라야하는 기능들을 캡슐화한 객체에 정리하여 실행할 수 있게 해주는 디자인 패턴이다.

즉, 요청에 따르는 기능들이 다양하고 변경 및 추가 삭제가 많은 경우 요청이 발생되는 클래스를 변경하지 않고 수정할 때 매우 유용하다.

  • 커맨드 패턴이 사용되는 경우
  1. 병렬처리(Parallel Processing) : 병렬로 여러 스레드에서 실행이 되어야하는 경우

  2. 매크로(Macro) : 특정 명령에 따른 동일한 일련의 작업을 반복적으로 수행해야 하는 경우

  3. 네트워킹(Networking) : 네트워크를 통해 일련의 작업을 보내야하는 경우(원격조작, 게임 캐릭터에 명령)

  • 커맨드 패턴 장점
  1. 기존 코드를 변경하지 않고 새 명령을 추가할 수 있어 코드확장이 수월하다.

  2. 호출자(invoker)와 수신자(receiver)의 결합도를 낮출 수 있다.

  • 커맨드 패턴 단점
  1. 개별 명령에 대한 클래스의 수가 증가할 수 있다.
  • 커맨드 패턴 구조

  1. Invoker
    해당 요청에 따르는 기능의 실행을 요청하는 호출자 클래스이다.

  2. Command
    실행될 기능에 대한 인터페이스.
    실행되는 기능들을 종합하는 execute를 선언한다.

  3. ConcreteCommand
    실제로 실행되는 기능을 구현한다.

  4. Receiver
    ConcreteCommand의 execute를 구현하는 클래스.
    기능을 실행하기 위해 필요한 수신자 클래스이다.

  5. Client
    요청을 전달하는 클라이언트이다.

  • 예제 코드

Command : Command

public interface Command {
    public void execute();
}

Light : Invoker

class Lights extends TreeDecorator {

    public Lights(ChristmasTree christmasTree) {
        super(christmasTree); // 여기가 포인트.
    }

    public String addLights() {
        return " with Lights";
    }

    @Override
    public String decorate() {
        return super.decorate() + addLights(); // 여기가 포인트.
    }
}

LightOffCommand : ConcreteCommand

public class LightOffCommand implements Command {
    Light light;

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

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

LightOnCommand : ConcreteCommand

public class LightOnCommand implements Command {
    Light light;

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

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

SimpleRemoteControl : Receiver

public class SimpleRemoteControl {
    Command slot;

    public SimpleRemoteControl() {
    }

    public void setCommand(Command command) {
        slot = command;
    }

    public void buttonWasPressed() {
        slot.execute();
    }
}

Main Class

public class RemoteControlMain {
    public static void main(String[] args) {
        SimpleRemoteControl remote = new SimpleRemoteControl();
        Light light = new Light();

        remote.setCommand(new LightOnCommand(light));
        remote.buttonWasPressed();
        remote.setCommand(new LightOffCommand(light));
        remote.buttonWasPressed();
    }
}

3. 인터프리터 패턴(Interpreter Pattern)

  • 인터프리터 패턴이란?

인터프리터 패턴은 언어 문법이나 표현을 평가할 수있는 방법을 제공한다.
이 패턴은 특정 컨텍스트를 해석하도록 지시하는 표현 인터페이스를 구현하는 것을 포함한다.
이 패턴은 SQL 구문 분석, 기호 처리 엔진 등에서 사용된다.

  • 인터프리터 패턴 구조

  1. AbstractExpression : interpret()를 정의한다.

  2. TerminalExpression : interpret()를 구현한다.

  3. NonTerminalExpression : Non-Terminal의 interpret()를 구현한다.

  4. Context : String 표현식이어야 하며 인터프리터에 보내는 정보이다.

  5. Client : interpret()를 호출한다.

  • 예제 코드

Expression : AbstractExpression

public interface Expression {
    boolean interpreter(String con);
}

TerminalExpression : TerminalExpression

public class TerminalExpression implements Expression {
    String data;

    public TerminalExpression(String data) {
        this.data = data;
    }

    @Override
    public boolean interpreter(String con) {

        return con.contains(data);
    }
}

OrExpression : TerminalExpression

public class OrExpression implements Expression {
    Expression expr1;
    Expression expr2;

    public OrExpression(Expression expr1, Expression expr2) {
        this.expr1 = expr1;
        this.expr2 = expr2;
    }

    public boolean interpreter(String con) {
        return expr1.interpreter(con) || expr2.interpreter(con);
    }
}

AndExpression : TerminalExpression

public class AndExpression implements Expression {
    Expression expr1;
    Expression expr2;

    public AndExpression(Expression expr1, Expression expr2) {
        this.expr1 = expr1;
        this.expr2 = expr2;
    }

    @Override
    public boolean interpreter(String con) {
        return expr1.interpreter(con) && expr2.interpreter(con);
    }
}

Main Class

public class ExpressionMain {
    public static void main(String[] args) {
        Expression person1 = new TerminalExpression("Kushagra");
        Expression person2 = new TerminalExpression("Lokesh");
        Expression isSingle = new OrExpression(person1, person2);

        Expression vikram = new TerminalExpression("Vikram");
        Expression committed = new TerminalExpression("Committed");
        Expression isCommitted = new AndExpression(vikram, committed);

        System.out.println(isSingle.interpreter("Kushagra"));
        System.out.println(isSingle.interpreter("Lokesh"));
        System.out.println(isSingle.interpreter("Achint"));

        System.out.println(isCommitted.interpreter("Committed, Vikram"));
        System.out.println(isCommitted.interpreter("Single, Vikram"));
    }
}

4. 이터레이터 패턴 (Iterator Pattern)

  • 이터레이터 패턴이란?

이터레이터(Iterator)란 반복하다라는 의미로 어떠한 객체의 집합을 순서대로 명령을 처리할 수 있게 해주는 디자인 패턴이다. 컬렉션 구현 방법을 노출시키지 않으면서도 그 집합체안에 들어있는 모든 항목에 접근할 수 있게 해 주는 방법을 제공해 주는 패턴으로 간단하면서도 실제로도 굉장히 많이 쓰고있는 패턴이다.

Java 언어에서 배열 arr의 모든 요소를 표시하기 위해서는 다음과 같이 for문을 사용한다.

for (int i = 0; i < arr.length; i++) {
    System.out.println(arr[i]);
}

for문의 변수 i를 통해 배열 arr의 요소에 접근할 수 있다.
Iterator 패턴은 이런 변수 i의 기능을 추상화해서 일반화 한 것이다.

  • 이터레이터 패턴 구조

  1. Iterator : 순서대로 객체를 검색하는 인터페이스를 정한다.

  2. ConcreateIterator : Iterator에서의 인터페이스를 구현한다.

  3. Aggregate : Iterator의 역할을 만드는 인터페이스를 정한다.

  4. ConcreateAggreagate : Aggregate에서의 인터페이스를 구현한다.

  • 예제 코드

Aggregate : Aggregate

public interface Aggregate {
    public abstract Iterator iterator();
}

Iterator : Iterator

public interface Iterator {
    public abstract boolean hasNext();

    public abstract Object next();
}

Book

public class Book {
    private String name;

    public Book(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

BookShelf : ConcreateAggreagate

public class BookShelf implements Aggregate {
    private Book[] books;
    private int last = 0;

    public BookShelf(int maxsize) {
        this.books = new Book[maxsize];
    }

    public Book getBookAt(int index) {
        return books[index];
    }

    public void appendBook(Book book) {
        this.books[last] = book;
        last++;
    }

    public int getLength() {
        return last;
    }

    public Iterator iterator() {
        return new BookShelfIterator(this);
    }
}

BookShelfIterator : ConcreateIterator

public class BookShelfIterator implements Iterator {
    private BookShelf bookShelf;
    private int index;

    public BookShelfIterator(BookShelf bookShelf) {
        this.bookShelf = bookShelf;
        this.index = 0;
    }

    public boolean hasNext() {
        return index < bookShelf.getLength();
    }

    @Override
    public Object next() {
        Book book = bookShelf.getBookAt(index);
        index++;
        return book;
    }
}

Main Class

public class BookShelfMain {
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(4);
        bookShelf.appendBook(new Book("Around the World in 80 Days"));
        bookShelf.appendBook(new Book("Bible"));
        bookShelf.appendBook(new Book("Cinderella"));
        bookShelf.appendBook(new Book("Baddy-Long-Legs"));

        Iterator it = bookShelf.iterator();

        while (it.hasNext()) {
            Book book = (Book) it.next();
            System.out.println(book.getName());
        }
    }
}

Iterator를 사용함으로써 구현과 분리해서 하나씩 셀 수 있다.

while (it.hasNext()) {
    Book book = (Book)it.next();
    System.out.println(book.getName());
}

위의 코드에서 while 루프는 BookShelf의 구현에는 의존하지 않는다.
만약 BookShelf를 배열에서 Vector로 변경한다고 하더라도 hasNext()와 next()만 올바르게 수정한다면, 위의 while 루프를 전혀 변경하지 않아도 동작한다.

즉, 클래스를 부품처럼 사용할 수 있게 하고 하나의 부품을 수정해도 다른 부품에는 큰 영향 없이 작은 수정만으로 끝낼 수 있게 재이용화의 장점을 누릴 수 있다.

5. 옵저버 패턴(Observer Pattern)

  • 옵저버 패턴이란?

옵저버 패턴은 객체의 상태 변화를 관찰하는 관찰자 객체를 생성하여 사용하는 디자인 패턴이다.

즉, 객체의 변화가 발생하면 그에 따르는 종속객체들이 자동으로 변화가 통지되어 그에 따른 명령을 수행하도록하는 일대다의 의존성을 정의한다.

데이터의 변경이 발생했을 경우 상대 클래스나 객체에 의존하지 않으면서 데이터 변경을 통보하고자 할 때 유용하다.

Ex)
1. 새로운 파일이 추가되거나 기존 파일이 삭제되었을 때 탐색기는 다른 탐색기에게 즉시 변경을 통보해야 한다.
2. 차량 연료량 클래스는 연료량이 부족한 경우 연료량에 관심을 가지는 구체적인 클래스(연료량 부족 경고 클래스, 주행 가능 거리 출력 클래스)에 직접 의존하지 않는 방식으로 연료량 변화를 통보해야 한다.

  • 옵저버 패턴을 사용하는 경우
  1. 분산 이벤트 핸들링 시스템
  2. 이벤트 기반 프로그래밍
  • 옵저버 패턴 장점
  1. 객체간의 결합도가 느슨해진다.
  2. 실시간으로 효과적으로 데이터를 배분할 수 있다.
  • 옵저버 패턴 구조

  1. Observer
    데이터의 변경을 통보 받는 인터페이스
    즉, Subject에서는 Observer 인터페이스의 update 메서드를 호출함으로써 ConcreteSubject의 데이터 변경을 ConcreteObserver에게 통보한다.

  2. Subject
    ConcreteObserver 객체를 관리하는 요소
    Observer 인터페이스를 참조해서 ConcreteObserver를 관리하므로 ConcreteObserver의 변화에 독립적일 수 있다.

  3. ConcreteSubject
    변경 관리 대상이 되는 데이터가 있는 클래스(통보하는 클래스)
    데이터 변경을 위한 메서드인 setState가 있다.
    setState 메서드에서는 자신의 데이터인 subjectState를 변경하고 Subject의 notifyObservers 메서드를 호출해서 ConcreteObserver 객체에 변경을 통보한다.

  4. ConcreteObserver
    ConcreteSubject의 변경을 통보받는 클래스
    Observer 인터페이스의 update 메서드를 구현함으로써 변경을 통보받는다.
    변경된 데이터는 ConcreteSubject의 getState 메서드를 호출함으로써 변경을 조회한다.

  • 예제 코드

Observer : Observer

public interface Observer {
    public void update(String title, String news);
}

Publisher : Subject

public interface Publisher {
    public void registerObserver(Observer observer);
    public void removeObserver(Observer observer);
    public void notifyObservers();
}

NewsPublisher : ConcreteSubject

public class NewsPublisher implements Publisher{
    private ArrayList<Observer> observers;
    private String title;
    private String news;

    public NewsPublisher() {
        observers = new ArrayList<>();
        title = null;
        news = null;
    }

    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        for(Observer observer : observers) {
            observer.update(title, news);
        }
    }

    public void setNews(String title, String news) {
        this.title = title;
        this.news = news;
        notifyObservers();
    }
}

Publisher 인터페이스를 직접 이용하는 클래스로 observers를 ArrayList에 모을 수 있게 한다.

NewsSubscriber : ConcreteObserver

public class NewsSubscriber implements Observer{
    private String observerName;
    private String news; private
    Publisher publisher;

    public NewsSubscriber(String subscriber, Publisher publisher) {
        this.observerName = subscriber;
        this.publisher = publisher;
        publisher.registerObserver(this);
    }

    @Override
    public void update(String title, String news) {
        this.news = title + "!!! " + news; display();
    }

    private void display() {
        System.out.println("=== " + observerName + " 수신 내용 ===\n" + news + "\n");
    }
}

Main Class

public class ObserverPatternMain {
    public static void main(String[] args) {
        NewsPublisher newsPublisher = new NewsPublisher();
        NewsSubscriber newsSubscriber1 = new NewsSubscriber("옵저버1", newsPublisher);
        NewsSubscriber newsSubscriber2 = new NewsSubscriber("옵저버2", newsPublisher);
        newsPublisher.setNews("특보", "옵저버 패턴이 만들어졌습니다.");
        newsPublisher.setNews("정정", "옵저버 패턴으로 내용이 정정됨을 알립니다.");
        newsPublisher.removeObserver(newsSubscriber1);
        newsPublisher.setNews("속보", "누군가 구독 해제를 했습니다.");
    }
}

실행 결과

profile
Back-end Developer
post-custom-banner

2개의 댓글

comment-user-thumbnail
2021년 10월 26일

좋은 글 잘 읽고 있습니다.
그런데 커맨드 패턴 예제에서 Invoker인 Light 클래스가 저번 글 Decorator 클래스에서 변경되지 않은 채로 그대로 적혀있는 것 같네요. on(), off() 메소드가 적혀 있지 않아요.

답글 달기
comment-user-thumbnail
2022년 9월 7일

좋은 글 감사드립니다!

답글 달기