Study/Etc

[Design Pattern] 디자인 패턴의 개념과 종류 (3) - 행위 패턴

jonghne 2024. 1. 12. 18:21

디자인 패턴이란 ?

디자인 패턴이란 개발 과정에서 자주 발생하던 문제에 대한 해결책을 재사용 가능한 형태로 정리해 놓은 검증된 설계 방법 또는 솔루션이다.

 

디자인 패턴의 장점으로는 일관된 구조를 사용하기 때문에 팀원 간 의사소통을 쉽게 만들어주고 유지보수성이 좋다. 

 

또한 검증된 방법이기 때문에 신뢰하고 애플리케이션을 개발할 수 있고, 시스템의 확장성이 높아진다.

 

디자인 패턴은 크게 생성 패턴, 구조 패턴, 행위 패턴 3가지가 있다. 

https://m.hanbit.co.kr/channel/category/category_view.html?cms_code=CMS8616098823

이번 게시글에서는 행위 패턴의 종류에 대해 간략히 알아본다. 

 

행위 패턴

행위 패턴은 객체 간의 상호 작용하는 방법과 책임을 분배하는 방법에 중점을 두는 디자인 패턴이다. 

 

주요 패턴으로는 옵저버, 커맨드, 이터레이터, 전략, 템플릿 메서드 패턴이 있다.

 

옵저버 (Observer) 패턴

옵저버 패턴이란 객체의 상태가 변할 때 마다 관련 있는 객체들에게 알림을 보내 알려주는 디자인 패턴이다.

 

관찰 대상인 객체(Subject)는 구독하고 있는 관찰자(Observer)들을 가지고 있다가, 객체의 상태변화가 일어날 때 마다 notifyObserver() 메서드를 통해 Observer 객체의 update() 메서드를 호출한다.

  • Subject : 관찰 대상 인터페이스
  • ConcreteSubject : 관찰 대상 클래스
  • Observer : 관찰자 인터페이스 
  • ConcreteObserver : 관찰자 클래스

옵저버 패턴 예시

// 관찰 대상을 추상화한 인터페이스
public interface Subject {
    public void registerObserver(Observer observer);
    public void removeObserver(Observer observer);
    public void notifyObserver();
}

// 관찰 대상 클래스
public class ConcreteSubject implements Subject {
		private ArrayList<Observer> observers = new ArrayList<>();
    
    @Override
    public void registerObserver(Observer observer) {
    	observers.add(observer);
    }
    
    @Override
    public void removeObserver(Observer observer) {
    	int index = observers.indexOf(observer);
    	observers.remove(index);
    }
    
    @Override
    public void notifyObserver() {
    	for (Observer Observer : observers) {
        	observer.update("이벤트 발생");
        }
    }    
}

// 관찰 클래스를 추상화한 인터페이스
public interface Observer {
	public void update(String event);
}

// 관찰 클래스
public ConcreteObserver implement Observer {
    private Subject subject;
	  private List<String> events = new ArrayList<>();
    
    public ConcreteObserver(Subject subject) {
    	  this.subject = subject;
        subject.add(this);
    }
    
    @Override
    public void update(String event) {
				events.add(event);
        print();
    }
    
    private void print() {
			for(String event : events) {
	    	System.out.println("event" + event);
			}
    }
}

 

커맨드 (Command) 패턴

커맨드 패턴이란 객체의 행위를 각각의 클래스로 캡슐화 하는 패턴이다.

 

각각의 행위를 캡슐화 함으로써, 여러 객체가 서로 의존관계를 맺지 않고 원하는 기능을 사용할 수 있다. 

 

예를 들어 A 객체가 B 객체의 기능을 사용하고 싶다면, A 객체는 B 객체의 인스턴스를 생성한 뒤 메서드를 호출해야 한다. 

 

그런데 커맨드 패턴을 사용한다면 B 객체의 기능을 캡슐화해서 별도의 클래스를 만들기 때문에, A 객체는 B 객체에 직접적으로 의존하지 않아도 된다.

 

  • Command : 기능에 대한 인터페이스, 실행될 기능을 execute로 정의한다. 
  • ConcreteCommand : Command 인터페이스의 구현클래스로 execute() 메서드 안에 각 기능을 재정의한다.
  • Invoker : 기능의 실행을 요구하는 호출자 클래스 
  • Receiver : ConcreteCommand의 기능을 실행하기 위해 사용하는 수신자 클래스로, execute 메서드 구현에 필요한 클래스

 

커맨트 패턴 예시

아래의 예시는 스위치를 켜고 끄는 기능이다. 

 

Light 클래스(Receiver)와 Light 클래스 기능을 캡슐화한 TurnOnCommand, TurnOffCommand 클래스 (ConcreteCommand)가 있다. 

 

실제 기능을 수행하는 주체인 RemoteController(Invoker)는 Light 클래스가 아닌 클라이언트로부터 주입받은 Command 인터페이스 구현체를 통해 스위치를 켜고 끄는 기능을 수행한다.

 

즉 Light 클래스는 Command 구현 클래스에게 기능만 제공하고, RemoteController와는 직접적인 의존관계를 맺지 않는다. 

// Command
interface Command {
    void execute();
}
// ConcreteCommand
class TurnOnCommand implements Command {
    private Light light;

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

    @Override
    public void execute() {
        light.turnOn();
    }
}
// ConcreteCommand
class TurnOffCommand implements Command {
    private Light light;

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

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

// Receiver
class Light {
    public void turnOn() {
        System.out.println("Light is ON");
    }

    public void turnOff() {
        System.out.println("Light is OFF");
    }
}

// Invoker
class RemoteControl {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressButton() {
        command.execute();
    }
}

// Client
public class Client {
    public static void main(String[] args) {
        Light light = new Light();
        Command turnOnCommand = new TurnOnCommand(light);
        Command turnOffCommand = new TurnOffCommand(light);

        RemoteControl remoteControl = new RemoteControl();

        remoteControl.setCommand(turnOnCommand);
        remoteControl.pressButton(); // Light is ON

        remoteControl.setCommand(turnOffCommand);
        remoteControl.pressButton(); // Light is OFF
    }
}

 

이터레이터 (Iterator) 패턴

컬렉션에 접근하는 방법을 표준화해서 외부에 노출하지 않고, 컬렉션의 모든 요소를 순회하는 기능을 제공해서 간편하게 컬렉션을 다룰 수 있게 하는 패턴이다.

  • 장점
    • 컬렉션 내부 구조를 몰라도 컬렉션 순회 기능을 사용할 수 있다.
    • 새로운 컬렉션 유형이나 순회 방법을 도입할 때, 클라이언트 코드의 수정 없이 새로운 이터레이터를 만들어 사용 가능하다.
    • 클라이언트 코드가 단순해진다. 
  • 단점
    • 추카 클래스와 인터페이스를 도입해야해서 코드가 복잡해질 수 있다.
    • 성능 오버헤드가 발생할 수 있다 (작은 크기의 컬렉션의 경우 직접 순회하는 것이 나을 수 있다)
    • 순회 순서 변경이 어렵다 (역순으로 순회하는 이터레이터를 추가하기위해서는 추가 작업이 필요할 수 있음)

 

 

  • Iterator : 컬렉션을 순회하며 각 요소에 접근하는데 사용하는 기능을 추상화한 인터페이스.
  • ConcreteIterator : Iterator 인터페이스의 구현클래스로 실제 컬렉션을 순회하는 기능을 정의한다. 
  • Aggregate : 컬렉션 객체를 나타내는 인터페이스로 Iterator 인터페이스를 Composition 을 통해 참조한다. 
  • ConcreteAggreate : Aggreate 인터페이스를 구현한 실제 컬렉션 클래스이다. 내부에서 ConcreteIterator를 생성 및 초기화 한다. 

 

이터레이터 패턴 예시

아래 예시는 간단한 리스트를 순회하는 이터레이터 패턴의 예시이다. 

 

컬렉션의 요소를 순회하는 기능을 추상화한 Iterator 인터페이스와 ListIterator 구현 클래스가 있고, 

List 컬렉션의 기능을 추상화한 ListAggregate와, 이름을 저장하는 컬렉션 클래스인 nameList 클래스가 있다.

 

nameList는 멤버 변수로 Iterator 인터페이스의 객체를 가지고, ListAggregate 구현 클래스로 초기화 하도록 했다.

 

그리고 클라이언트 코드에서 nameList 인스턴스의 createIterator 메서드를 통해 Iterator 인스턴스를 생성하고, 요소에 접근해서 하나씩 순회하게 된다. 

 

아래의 예시에서 Collection 클래스인 NameList 내부에는 요소 순회를 위한 어떠한 코드도 작성하지 않았고, 단지 Iterator 인터페이스와 ListIterator 구현 클래스를 Composition 한 뒤 간편하게 요소를 접근하거나 순회하는 것을 볼 수 있다.

// Iterator
interface Iterator {
    boolean hasNext();
    Object next();
}

// ConcreteIterator
class ListIterator implements Iterator {
    private List<String> list;
    private int position = 0;

    public ListIterator(List<String> list) {
        this.list = list;
    }

    @Override
    public boolean hasNext() {
        return position < list.size();
    }

    @Override
    public Object next() {
        if (this.hasNext()) {
            return list.get(position++);
        }
        return null;
    }
}

// Aggregate
interface ListAggregate {
    Iterator createIterator();
}

// ConcreteAggregate
class NameList implements ListAggregate {
    private List<String> names;

    public NameList(List<String> names) {
        this.names = names;
    }

    @Override
    public Iterator createIterator() {
        return new ListIterator(names);
    }
}

// client 
public class Client {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
        List nameList = new NameList(names);

        Iterator iterator = nameList.createIterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

 

전략 (Strategy) 패턴

객체의 특정 전략(ex) 비즈니스 정책)를 인터페이스로 추상화 한 뒤, 클라이언트에서 구현 클래스를 교체하며 동적으로 전략을 변경하는 패턴이다.

 

객체의 행위를 변경하기 위해 직접 코드를 수정하는 것이 아닌, 이미 만들어 놓은 전략 클래스로만 바꿔 끼워주면 되기 때문에 OCP 원칙을 지킬 수 있다. 

 

또한 구현 클래스가 아닌 인터페이스에 의존하기 때문에 DIP 원칙도 지킬 수 있다 (DI를 통해 구현 클래스를 외부 주입해줘서 클라이언트 코드에는 인터페이스만 알 수 있게) 

 

예를 들어 할인 정책이 여러가지가 있다면, 미리 정책 별로 기능을 구현해놓고 그때 그때 전략을 교체하는 패턴이다

 

 

전략 패턴 예시

// Strategy
interface DiscountStrategy {
    double applyDiscount(double amount);
}

// ConcreteStrategy
class RegularCustomerDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double amount) {
        // 정액 할인을 적용하는 구체적인 전략
        return amount - 10.0;
    }
}

// ConcreteStrategy
class VIPCustomerDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double amount) {
        // 비율 할인을 적용하는 구체적인 전략
        return amount * 0.8;
    }
}

// Context
class ShoppingCart {
    private DiscountStrategy discountStrategy;

    public void setDiscountStrategy(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    public double checkout(double amount) {
        // 컨텍스트에서 전략을 사용
        return discountStrategy.applyDiscount(amount);
    }
}

public class Client {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();

        // 일반 고객에 대한 할인
        cart.setDiscountStrategy(new RegularCustomerDiscount());
        double regularCustomerTotal = cart.checkout(100.0);
        System.out.println("Regular Customer Total: " + regularCustomerTotal);

        // VIP 고객에 대한 할인
        cart.setDiscountStrategy(new VIPCustomerDiscount());
        double vipCustomerTotal = cart.checkout(100.0);
        System.out.println("VIP Customer Total: " + vipCustomerTotal);
    }
}

 

템플릿 메서드 패턴 

여러 클래스에서 공통으로 사용하는 메서드를 템플릿화 해서 추상 클래스로 만들고, 다른 별도의 메서드는 자식 클래스에서 각각 구현하도록 하는 패턴이다.

 

만약 A,B,C 클래스에 call()이라는 메서드가 중복되어 구현되어 있을 때, 추상 클래스를 만든 다음 call() 메서드를 하나만 만들고 A,B,C 클래스가 해당 추상클래스를 상속 받은 다음 나머지 기능은 각각 구현한다. 

 

템플릿 메서드 패턴은 "상속"이라는 기술을 극대화한 패턴으로, 중복 코드를 최소화 할 수 있다.

 

 

 

템플릿 메서드 패턴 예시

// AbstractClass
abstract class BeverageTemplate {
    // 템플릿 메서드
    public final void prepareBeverage() {
        boilWater();
        brew();
        pourInCup();
        if (customerWantsCondiments()) {
            addCondiments();
        }
    }

    // 서브클래스에서 구현할 메서드들
    abstract void brew();
    abstract void addCondiments();

    // 공통 메서드들
    void boilWater() {
        System.out.println("물 끓이는 중");
    }

    void pourInCup() {
        System.out.println("컵에 따르는 중");
    }

    // 후크 메서드 (기본 동작을 제공하되 서브클래스에서 오버라이딩 가능)
    boolean customerWantsCondiments() {
        return true;
    }
}

// ConcreteClass
class Tea extends BeverageTemplate {
    @Override
    void brew() {
        System.out.println("차를 우려내는 중");
    }

    @Override
    void addCondiments() {
        System.out.println("레몬을 추가하는 중");
    }
}

// ConcreteClass
class Coffee extends BeverageTemplate {
    @Override
    void brew() {
        System.out.println("커피를 내리는 중");
    }

    @Override
    void addCondiments() {
        System.out.println("설탕과 우유를 추가하는 중");
    }

    // 후크 메서드 오버라이딩
    @Override
    boolean customerWantsCondiments() {
        return false;
    }
}

// Client
public class Client {
    public static void main(String[] args) {
        System.out.println("차 만들기:");
        Tea tea = new Tea();
        tea.prepareBeverage();

        System.out.println("\n커피 만들기:");
        Coffee coffee = new Coffee();
        coffee.prepareBeverage();
    }
}