주요 디자인 패턴(생성, 구조, 행동) 정리

winluck·2024년 10월 20일
0

OOP 복습

  • 객체지향의 핵심: 추상화, 캡슐화, 상속, 다형성
  • SOLID: SRP, OCP, LSP, ISP, DIP
  • ADT: 데이터 및 관련 연산을 하나의 의미있는 단위로 캡슐화한 것
  • 클래스: ADT + 상속 + 다형성
  • 다형성: 하나의 연산이 각 클래스마다 다르게 동작할 수 있는 객체지향 SW의 특성
    • 정적 다형성: 메서드 오버로딩 - 컴파일 타임
    • 동적 다형성: 메서드 오버라이딩 - 런타임
  • 컴파일러는 클래스가 정의되었을 때 클래스(인터페이스)를 일종의 타입으로 간주한다.
  • 추상 클래스
    • abstract로 정의된 클래스
    • 인스턴스화 될 수 없음
    • 적어도 하나의 추상 메서드를 갖는다면 추상 클래스이다. (역은 성립하지 않음)
  • 추상 클래스와 인터페이스
    • 자식-부모 클래스 간 관계가 “is a”거나 추상 클래스로도 적절한 추상화 제공 가능 → 추상 클래스
    • 메서드들이 클래스에서 작은 부분만 차지하거나, 서브클래스가 다른 클래스로부터 상속받아야 할 때, 메서드 중 어떤 것도 합리적으로 구현할 수 없을 때 → 인터페이스
  • 클래스의 관계

주요 디자인 패턴: 생성/구조/행동

  • 생성 패턴: 객체 생성 과정을 유연하게 만들어 코드의 강한 결합을 방지
    • 싱글톤, 팩토리 메서드, 추상 팩토리, 빌더, 프로토타입
  • 구조 패턴: 클래스와 객체를 조합하여 더 큰 구조를 형성하며, 다양한 구조를 통해 클래스와 객체의 관계를 강화
    • 어댑터, 브릿지, 컴포지트, 데코레이터, 퍼사드, 플라이웨이트, 프록시
  • 행동 패턴: 객체들 사이의 통신과 책임의 분배에 초점을 맞추며, 복잡한 흐름 제어를 간소화하고, 객체 간의 상호작용을 더 효과적으로 만듦
    • 책임 연쇄, 커맨드, 인터프리터, 이터레이터, 중재자, 메멘토, 옵저버, 상태, 전략, 템플릿 메서드, 비지터

1. 생성 패턴

객체 생성 과정을 유연하게 만들어 코드의 강한 결합을 방지


싱글톤(Singleton) 패턴

  • 목적: 시스템 내에서 클래스의 인스턴스가 오직 하나만 존재하도록 보장한다.
  • 사용 예시
    • 클래스의 인스턴스가 정확히 하나만 필요할 때.
    • 단일 객체에 대한 제어된 접근이 필요할 때
  • 기본적인 싱글톤 패턴
    • 단점: 멀티스레드가 경쟁 상태에 돌입할 경우 여러 싱글톤 객체가 생성

      public class Singleton {
          private **static** Singleton uniqueInstance;
      
          **private** Singleton() {}
      
          public static Singleton getInstance() {
              **if (uniqueInstance == null) {
                  uniqueInstance = new Singleton();
              }**
              return uniqueInstance;
          }
      }
  • 바로 초기화하는 싱글톤 패턴
    • 단점: 사용하기 전 미리 초기화되어 서 있는 전역 변수에 대한 오버헤드

      public class Singleton {
          private **static** Singleton uniqueInstance = new Singleton();
          
          **private** Singleton() {}
      
          public static Singleton getInstance() {
              return uniqueInstance;
          }
      }
  • Thread-Safe한 싱글톤 패턴 (synchronized)
    • 단점: synchronized로 인한 성능 악화

      public class Singleton {
          private static Singleton uniqueInstance;
      
          private Singleton() {}
      
          // Synchronized to prevent thread interference
          public static **synchronized** Singleton getInstance() {
              if (uniqueInstance == null) {
                  uniqueInstance = new Singleton();
              }
              return uniqueInstance;
          }
      }
  • double-checked locking으로 성능을 최적화한 싱글톤 패턴
    public class Singleton {
        private **volatile** static Singleton uniqueInstance;
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            **if (uniqueInstance == null) {
                synchronized (Singleton.class) {
                    if (uniqueInstance == null) {
                        uniqueInstance = new Singleton();
                    }
                }
            }**
            return uniqueInstance;
        }
    }
    
    • double-checked locking에서 volatile의 의미
      • Java에서 각 스레드는 자신만의 캐시 메모리를 갖기에 변수의 값을 캐싱하여 사용한다.
      • 그러나 volatile은 메인 메모리에 직접 읽고 쓰는 것을 보장하기에, 한 스레드가 volatile 변수를 수정한 직후 모든 스레드가 변경된 값을 사용하게 된다.
  • 추상 팩토리 패턴, 빌더 패턴, 프로토타입 패턴은 싱글톤 패턴이 적용될 수 있다.
  • Facade 패턴은 하나의 Facade 객체만 요구되기에 보통 싱글톤 패턴이다.
  • 상태 객체도 보통 싱글톤 패턴이다.

팩토리 메서드(Factory Method) 패턴

  • 목적: 객체를 생성하는 메서드를 노출하여, 서브클래스가 실제 생성 과정을 제어할 수 있도록 한다. 서브클래스가 초기화되기 위한 객체를 결정하기 위해 상속을 사용한다.
  • SOLID에서 DIP와 관련된 패턴
  • Class-scope 패턴이며 상속(Inheritance)을 활용한다.
  • 사용 예시
    • 어떤 클래스가 어떤 객체를 생성해야 할지 미리 알 수 없을 때
    • 서브클래스가 어떤 객체를 생성할지 지정해야 할 때
    • 부모 클래스가 객체 생성 책임을 서브클래스에게 위임하고자 할 때
  • “서브클래스가 인스턴스화할 클래스를 결정한다”의 의미
    • 런타임에 어떤 클래스를 생성하는지 결정하는 것이 아니라 구현 단계에서 어떤 구체적인 클래스를 생성할지 미리 정의하는 것이다.
public **abstract** class PizzaStore {

		public Pizza orderPizza(String type) {
				Pizza pizza;
				pizza = createPizza(type);
				pizza.prepare();
				pizza.bake();
				pizza.cut();
				pizza.box();
				return pizza;
		}
		
		protected **abstract** Pizza createPizza(String type);
}

public class DependentPizzaStore {
		public Pizza createPizza(String style, String type) {
				Pizza pizza = null;
				if (style.equals("NY")) {
						if (type.equals("cheese")) {
								pizza = new NYStyleCheesePizza();
						} else if (type.equals("veggie")) {
								pizza = new NYStyleVeggiePizza();
						}
				} else if (style.equals("Chicago")) {
						if (type.equals("cheese")) {
						pizza = new ChicagoStyleCheesePizza();
						} else if (type.equals("veggie")) {
						pizza = new ChicagoStyleVeggiePizza();
						}
				} else {
						System.out.println("Error: invalid type of pizza");
						return null;
				}
				
				pizza.prepare();
				pizza.bake();
				pizza.cut();
				pizza.box();
				return pizza;
		}
}
  • 위 코드가 잘못된 이유는?
    • 문제점: 새로운 피자를 추가할 때마다 코드를 수정해야 하므로 OCP를 위반하고 있고, 구체적인 클래스에 대한 결합도가 높기에 확장성이 악화된다.
    • 해결책: 팩토리 메서드로 서브클래스에서 객체 생성 로직을 위임한다.

추상 팩토리(Abstract Factory) 패턴

  • 목적: 하나 이상의 구체적인 클래스에 객체 생성 요청을 위임하여, 특정 객체를 제공하는 인터페이스를 제공
  • SOLID에서 DIP와 관련된 패턴
  • Object-scope 패턴이며 조합(Composition)과 위임(Delegation)을 활용한다.
  • 사용 예시
    • 객체 생성이 그 객체를 사용하는 시스템과 독립적이어야 할 때
    • 시스템이 여러 객체 군(families)을 사용할 수 있어야 할 때
    • 객체 군이 함께 사용되어야 할 때
    • 구현 세부 사항을 노출하지 않고 라이브러리를 배포해야 할 때
    • 구체적인 클래스가 클라이언트와 분리되어야 할 때

  • AbstractFactory : abstract product 객체들을 생성하는 동작을 위한 인터페이스를 선언한다.
  • ConcreteFactory : Product 객체를 생성하는 동작을 구현한다.
  • AbstractProduct : product 객체 타입을 위한 인터페이스를 선언한다.
  • ConcreteProduct : concrete factory 에서 생성할 product 객체를 정의한다. AbstractProduct 인터페이스를 구현한다.
  • Client : AbstractFactory와 AbstractProduct 클래스들에서 선언된 인터페이스만 사용한다.
  • 적용 예시

  • 한 시스템의 여러 제품군 중 하나를 사용하되, 제품군 내부 제품들을 함께 사용하도록 강제해야 할 때 유용
  • 핵심 특징
    • 클라이언트를 구현체로부터 분리하며, 부품 생성의 책임 및 과정을 캡슐화
    • 구체적인 Factory는 한 번만 인스턴스화되며 쉽게 교체 가능
    • 제품군 간 일관성이 유지되며 함께 사용하도록 설계된 제품을 사용 가능
    • 생성되는 제품의 집합이 고정되어 있기 때문에 새로운 제품 종류를 추가하는 것이 어려움

2. 구조 패턴

클래스와 객체를 조합하여 더 큰 구조를 형성하며, 다양한 구조를 통해 클래스와 객체의 관계를 강화


데코레이터(Decorator) 패턴

  • 목적: 객체를 동적으로 감싸서 기존의 책임과 동작을 수정할 수 있도록 한다.
  • SOLID 중 OCP(개방-폐쇄 원칙)와 연관된 패턴
  • 사용 예시
    • 객체의 책임과 동작을 동적으로 수정할 필요가 있을 때
    • 구체적인 구현이 책임과 동작으로부터 분리되어야 할 때
    • 서브클래싱을 통해 수정을 하는 것이 비실용적이거나 불가능할 때
    • 특정 기능이 객체 계층 구조의 상위에 존재해서는 안 될 때
    • 구체적인 구현을 둘러싼 작은 객체들이 많은 것이 허용될 때
  • 아래 이미지는 DarkRoast → Mocha → Whip 순서로 Decorating하는 예시

  • 데코레이터 객체는 자신이 장식하는 객체와 같은 상위 타입(인터페이스 또는 추상 클래스)을 가져야 한다.
    • 원본 객체와 데코레이터 객체를 구분하지 않고 사용할 수 있기에 일관성 증가
    • 같은 상위 타입이기에 여러 데코레이터를 Chain처럼 연결할 수 있어 유연성 확보
  • 실제 아래처럼 Java I/O에서도 FileInputStream → BufferedInputStream → LineNumberInputStream으로 Decorating이 이루어진다.

어댑터(Adapter) 패턴

  • 목적: 서로 다른 인터페이스를 가진 클래스들이 공통된 객체를 통해 소통하고 상호작용할 수 있도록 허용
  • 사용 예시
    • 사용하려는 클래스가 요구되는 인터페이스를 충족하지 않을 때
  • Object Adapter: Composition & Delegation 활용
    • d

```java
// Adaptee : 클라이언트에서 사용하고 싶은 기존의 서비스 (하지만 호환이 안되서 바로 사용 불가능)
class Adaptee {
    void specificRequest() {
        System.out.println("기존 서비스 기능 호출 + ");
    }
}

// Client Interface : 클라이언트가 접근해서 사용할 고수준의 어댑터 모듈
interface Target {
    void request();
}

// Adapter : Adaptee 서비스를 클라이언트에서 사용하게 할 수 있도록 호환 처리 해주는 어댑터
class Adapter implements Target {
		Adaptee adaptee; // Composition

		// // 어댑터가 인스턴스화되면 호환시킬 기존 서비스를 설정
		Adapter(Adaptee adaptee){
				this.adaptee = adaptee;
		}

    // 어댑터의 메소드가 호출되면, 부모 클래스 Adaptee의 메소드를 호출
    public void request() {
        adaptee.specificRequest(); // Delegation
    }
}
```
  • Class Adapter: Inheritance 활용
    - Adaptee를 상속받기 때문에 객체 구현 없이 바로 코드 재사용 가능

    ://github.com/user-attachments/assets/a1fcc8dc-53df-4e7d-8a54-1a386829006c)
    ```java
    // Adaptee : 클라이언트에서 사용하고 싶은 기존의 서비스 (하지만 호환이 안되서 바로 사용 불가능)
    class Adaptee {
        void specificRequest() {
            System.out.println("기존 서비스 기능 호출 + ");
        }
    }
    
    // Client Interface : 클라이언트가 접근해서 사용할 고수준의 어댑터 모듈
    interface Target {
        void request();
    }
    
    // Adapter : Adaptee 서비스를 클라이언트에서 사용하게 할 수 있도록 호환 처리 해주는 어댑터
    class Adapter extends Adaptee implements Target {
    
        // 어댑터의 메소드가 호출되면, 부모 클래스 Adaptee의 메소드를 호출
        public void request(int data) {
            specificRequest(data);
        }
    }
    ```

퍼사드(Facade) 패턴

  • 목적: 시스템 내 여러 인터페이스에 대해 단일 인터페이스를 제공
  • 사용 예시
    • 복잡한 시스템에 접근하기 위한 단순한 인터페이스가 필요할 때
    • 시스템 구현과 클라이언트 간의 의존성이 많을 때
    • 시스템과 서브시스템을 계층화해야 할 때
  • 기능을 추가하는 것이 아니라 인터페이스를 단순화하는 것
  • 서브시스템을 사용하기 쉽도록 높은 수준의 인터페이스를 정의한다.

컴포지트(Composite) 패턴

컴포지트(Composite) 패턴

  • 목적: 각 객체를 독립적으로 또는 동일한 인터페이스를 통해 중첩된 객체 집합으로 처리할 수 있는 객체 계층 구조의 생성을 용이하게 함
  • 복합 객체(Composite)와 단일 객체(Leaf)를 동일한 컴포넌트로 취급하여, 클라이언트에게 이 둘을 구분하지 않고 동일한 인터페이스를 사용하도록
  • SOLID에서 OCP와 관련
  • 사용 예시
    • 객체의 계층적 트리 표현이 필요할 때
    • 객체와 객체의 조합을 동일하게 처리해야 할 때
    • 예) 파일 시스템 구조
  • Composite vs Decorator
    • 공통점: 한계 없이 재귀적으로 객체를 구성
    • Composite: 기능 추가가 아닌 객체의 계층적 표현에 중점
    • Decorator: 서브클래스 없이 객체에 기능을 추가하는 게 중점
    • 상호보완적 관계이기에 종종 함께 사용

  • 위처럼 계층 구조이면서, if-else문으로 종류를 구분해야 하는 상황에서 확장성을 얻기 위해 사용한다.

  • Client : 클라이언트는 Component를 참조하여 단일 / 복합 객체를 하나의 객체로서 다룬다.

  • Component : Leaf와 Composite 를 묶는 공통적인 상위 인터페이스

  • Composite : 복합 객체로서, Leaf 역할이나 Composite 역할을 넣어 관리하는 역할을 한다.

    • Component 구현체들을 내부 리스트로 관리한다.
    • add 와 remove 메소드는 내부 리스트에 단일 / 복합 객체를 저장한다.
    • Component 인터페이스의 구현 메서드인 operation은 복합 객체에서 호출되면 재귀하여, 추가 단일 객체를 저장한 하위 복합 객체를 순회하게 된다.
  • Leaf: 단일 객체로서, 단순하게 내용물을 표시하는 역할을 한다.

    • Component 인터페이스의 구현 메서드인 operation은 단일 객체에서 호출되면 적절한 값만 반환한다.
  • 자식 요소들을 관리하는 add(), remove(), getChild()와 같은 메서드는 어디에 선언해야 할까?

    • Transparency를 위한 구현 또는 Safety를 위한 구현에 따라 다르다.
  • Transparency(투명성)

    • Leaf와 Composite 클래스의 구분 필요 없이 동일하게 다룰 수 있다.
    • 런타임에 Leaf 요소에 대한 Children 작업 등 의미 없는 작업을 시도할 수 있다.
  • Safety(안전성)

    • Leaf 요소에서 Children에 대한 작업 시도를 컴파일 타임에 미리 감지할 수 있다.
    • LeafComposite 구성 요소가 서로 다른 인터페이스를 갖게 된다.

3. 행동 패턴

객체 간 통신과 책임 분배에 초점을 맞추며, 복잡한 흐름 제어를 간소화하고, 객체 간의 상호작용을 효율화


전략(Strategy) 패턴

  • 목적: 특정 동작을 수행하기 위해 교체 가능한 캡슐화된 알고리즘을 정의
  • 사용 예시
    • 관련된 여러 클래스 간 차이점이 오직 행동일 때
    • 알고리즘의 여러 버전이나 변형이 필요할 때
    • 클래스의 동작을 런타임에 정의해야 할 때
    • 조건문이 복잡하고 유지보수가 어려울 때

감시자(Observer) 패턴

  • 목적: 시스템 내 하나 이상의 객체가 다른 객체의 상태 변화에 대해 알림을 받을 수 있도록 한다.
  • 사용 예시
    • 객체 간 통신에서 느슨한 결합(loose coupling)이 필요할 때
    • 한 객체 또는 여러 객체의 상태 변화가 다른 객체들의 동작을 유발해야 할 때
    • 브로드캐스팅 기능이 필요할 때
    • 객체들이 알림의 비용에 대해 인식하지 못할 때
  • 느슨한 결합(loose coupling)의 달성 방법
    • Subject는 observer가 특정 인터페이스를 구현한다는 것만 인지하며, 구체적인 변경이 다른 쪽에 영향을 미치지 않기에, 느슨한 결합이 유지된다.
  • Publisher 역할의 객체가 여러 개 있을 수 있다.
    • Subscriber 객체들은 원하는 만큼 여러 Publisher에 구독 신청이 가능하다.
    • 또한 한 Pubilsher는 다른 Publisher의 Subscriber가 될 수도 있다.

public class WeatherData implements Subject {
    private ArrayList<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        observers = new ArrayList<>();
    }

    public void registerObserver(Observer o) {
        observers.add(o);
    }

    public void removeObserver(Observer o) {
        int i = observers.indexOf(o);
        if (i >= 0) {
            observers.remove(i);
        }
    }

    public void notifyObservers() {
        for (int i = 0; i < observers.size(); i++) {
            Observer observer = (Observer) observers.get(i);
            observer.update(temperature, humidity, pressure);
        }
    }

    public void measurementsChanged() {
        notifyObservers();
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    // Other WeatherData methods (e.g., getTemperature, getHumidity, getPressure)
}
public class CurrentConditionDisplay implements Observer, DisplayElement {
    private float temperature;
    private float humidity;
    private Subject weatherData;

    public CurrentConditionDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

		@Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }
  
	  @Override
    public void display() {
        System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
    }
}

커맨드(Command) 패턴

[Design Pattern] Command Pattern(커맨드 패턴)

  • 목적: 요청을 객체로 캡슐화하여, 요청을 큐에 넣거나 콜백과 같은 객체 기반 관계에서 처리할 수 있도록 한다.
  • 사용 예시
    • 요청을 구체적으로 정의하고, 큐에 저장하거나 다양한 시간 또는 순서로 실행해야 할 때
    • 요청의 이력을 저장해야 할 때
    • 호출하는 객체(Invoker)가 요청을 처리하는 객체와 분리되어야 할 때

커맨드와 액티브 오브젝트 패턴

public interface Command {
    public void execute() throw Exception;
}

public class ActiveObjectEngine {
    LinkedList<Command> itsCommands = new LinkedList<>();

    public void addCommand(Command c) {
        itsCommands.add(c);
    }

    public void run() {
        while(!itsCommands.isEmpty()) {
            Command c = itsCommands.getFirst();
            itsCommands.removeFirst();
            c.execute();
        }
    }
}
public class SleepCommand implements Command {
    private Command wakeUpCommand = null;
    private ActiveObjectEngine engine= null;
    private long sleepTime = 0;
    private long startTime = 0;
    private boolean started = false;

    public SleepCommand(long st, ActiveObjectEngine e, Command wakeUp) {
        this.sleepTime = st;
        this.engine = e;
        this.wakeUpCommand = wakeUp;
    }

    public void execute() throws Exception {
        long currentTime = System.currentTimeMillis();
        if (!started) {
            started = true;
            startTime = currentTime;
            engine.addCommand(this);
        } else if ((currentTime - startTime) < sleepTime) {
            engine.addCommand(this);
        } else {
            engine.addCommand(wakeUpCommand);
        }
    }

}
public class DelayedTyper implements Command {
    private long itsDelay;
    private char itsChar;
    private static ActiveObjectEngine engine = new ActiveObjectEngine();
    private static boolean stop = false;

    public static void main(String[] args) {
        engine.addCommand(new DelayedTyper(100, '1'));
        engine.addCommand(new DelayedTyper(300, '3'));
        engine.addCommand(new DelayedTyper(500, '5'));
        engine.addCommand(new DelayedTyper(700, '7'));

        Command stopCommand = new Command() {
            public void execute() {
                stop = true;
            }
        }
        engine.addCommand(new SleepCommand(20000, engine, stopCommand));
        engine.run();
    }

    public DelayedTyper(long delay, char c) {
        itsDelay = delay;
        itsChar = c;
    }

    public void execute() throw Exception {
        System.out.print(itsChar);
        if(!stop) delayAndRepeat();
    }

    public void delayAndRepeat() throws Exception {
        engine.addCommand(new SleepCommand(itsDelay, engine, this));
    }
}

이터레이터(Iterator) 패턴

  • 목적: 집합 객체의 요소에 접근하되, 그 내부 표현에는 접근할 수 없도록 합니다.
  • SOLID의 SRP와 관련
  • 사용 예시
    - 전체 표현에 접근하지 않고 요소에 접근할 필요가 있을 때
    - 여러 번 또는 동시의 요소 순회가 필요하며, 순회를 위한 일관된 인터페이스가 필요할 때
    - 다양한 반복자의 구현 세부 사항에 미묘한 차이가 있을 때
    - 예) ArrayList와 Array로 구성된 집합 자료구조를 병합해야 할 때, Java의 Collections 자료구조

profile
Discover Tomorrow

0개의 댓글