의도

원형이 되는 객체를 만들어 놓고 필요할때 이를 복사해서 새로운 객체를 생성한다.

 

기존에 존재하는 객체와 비슷하거나 일부만 변경된 객체가 필요한 경우에 사용된다. new를 통해서 계속 새로운 인스턴스를 생성할 수 있지만, 비슷한 어트리뷰트를 가지는 경우, clone을 통해서 인스턴스를 복제하는 것이 더 간편하다.

 

 ex) DB에서 필요한 정보를 가져오는 경우, 거의 비슷한 객체가 반복적으로 사용된다면 요청이 올때마다 DB에 붙는거보다 한번만 붙어서 정보를 가져오고 이후부터는 해당 정보를 가지고 있는 객체를 복사해서 사용하는 것이 효율적이다.

 

프로토타입 패턴을 사용할 때 필수적인 내용은 프로토타입 인터페이스의 구현체 서브클래스에서 Clone() 연산을 구현해야 한다는 점이다.

 

활용성

이미 존재하는 인스턴스의 정보를 복제해서 새로운 인스턴스를 생성하기 때문에 Product의 생성, 복합, 표현 방법에 독립적인 신규 Product를 생성하고자 할 때 사용한다.

  • 인스턴스화할 클래스를 런타임에 지정할 때 (동적 로딩)
  • 클래스의 인스턴스들이 서로 다른 상태 조합 중에 어느 하나의 값을 가질 때, 해당 상태에 대한 프로토타입을 미리 생성해두고 나중에는 이를 복제해서 사용하고 싶을 때

 

구조

  • Client : Prototype인터페이스에 복제를 요청해서 새로운 객체를 전달 받는다
  • Prototype : 자신을 복제하는데 필요한 인터페이스를 정의한다.
  • ConcretePrototypeA, B : clone()을 구현하는 객체

 

장단점

  • (+) 복잡한 객체를 생성하는 과정을 clone() 메서드에 숨길 수 있다
  • (+) 새로운 인스턴스르 생성하는것보다 비용적인 측면에서 효율적일 수 있다
  • (+) 서브클래스의 수를 줄일 수 있다.
  • (+) 팩토리 메서드처럼 Creator클래스를 구현하는 ConcreteCreator가 제품 수 만큼 나와야 하는 것이 아니라, 프로토타입의 복제를 통해서 create액션을 처리하므로 Creator를 따로 상속받아서 구현하는 서브 클래스가 필요없다. Creator객체가 이미 프로토타입의 역할을 하기 때문이다.
  • (-) 복잡한 객체는 만드는 과정 자체가 복잡할 수 있다 (특히, 순환참조의 경우)

 

구현

  • 자바에서는 Object 클래스에서 제공하는 clone() 메서드를 사용할 수 있음
  • Cloneable interface를 구현한 (clone() 메서드 구현) ConcreateProtoType 객체
public class GithubIssue implements Cloneable {
  private Integer id;
  private String title;

  private GithubRepository repository;

  public GithubIssue(GithubRepository repository){
      this.repository = repository;
  }

  public Integer getId() {
      return id;
  }

  public void setId(Integer id) {
      this.id = id;
  }

  public String getTitle() {
      return title;
  }

  public void setTitle(String title) {
      this.title = title;
  }

  public String getUrl(){
      return String.format("httsp://github.com/%s/%s/issues/%s", this.repository.getUser(), this.repository.getName(), this.id);
  }

  @Override
  public GithubIssue clone() {
      try {
          // 기본적인 clone()은 shallow copy지원을 하기 때문에 deep copy를 원하면 여기에 추가로 구현
          GithubIssue clone = (GithubIssue) super.clone();
          return clone;
      } catch (CloneNotSupportedException e) {
          throw new AssertionError();
      }
  }
}

 

  • Client (main)
public class PrototypeMain {
  public static void main(String[] args) {

      GithubRepository repository = new GithubRepository();
      repository.setUser("user1");
      repository.setName("dev-repo");

      GithubIssue issue1 = new GithubIssue(repository);
      issue1.setId(1);
      issue1.setTitle("issue1 title here");
      // issue1
      System.out.println(issue1.getUrl());

            // issue1을 복제한 객체
      GithubIssue clone = (GithubIssue) issue1.clone();
      System.out.println(clone.getUrl());

  }
}

빌더 패턴

 

의도

복잡한 객체를 생성하는 방법과 표현하는 방법을 정의하는 클래스를 별도로 분리한다.

⇒ 객체 생성과 조립 알고리즘의 분리

 

활용성

  1. 복합 객체의 생성 알고리즘이 이를 합성하는 요소 개체들의 조립 방법에 독립적일 때
  2. 합성할 객체들의 표현이 서로 달라도 생성 절차에서 이를 지원해야 할 때

 

구조

* Builder : 객체의 일부 요소들을 생성하기 위한 추상 인터페이스를 정의

* ConcreteBuilder : 빌더 인터페이스를 구현하며 제품의 부품들을 모아서 요소들을 모아서 Product를 생성한다.

* Director : 빌더 인터페이스를 사용하는 객체

* Product : 생성할 객체

클라이언트는 Director 객체를 생성하고 생성된 Director는 원하는 빌더 객체를 통해서 Product에 필요한 요소를 추가한다. 최종적으로 클라이언트에서는 빌더 객체에서 Product를 가져온다.

 

장점과 단점

  1. (+) Product에 대한 내부 표현을 다양하게 변화할 수 있다. 빌더 인터페이스를 사용함으로써 실제 사용되는 요소가 무엇인지를 외부에는 숨길 수 있다. 또한 Product의 요소 복합 방법이 바뀐다면 해당 인터페이스를 구현하는 서브클래스를 새로 정의하면 된다
  2. (+) 생성과 표현에 필요한 코드를 분리해서 관리할 수 있다. 실제 객채를 생성하는 부분과 필요한 요소를 조립하는 부분을 분리하기 때문에 Product의 내부 요소를 정의한 클래스를 따로 알 필요 없이 빌더를 통해서 필요한Product를 가져올 수 있다
  3. (+) 불완전한 객체를 생성하지 못하게 끔 validate 할 수 있다
  4. (-) 원하는 객체 생성을 위해서 Director, ConcreteBuilder 객체를 미리 생성해야 한다

 

코드 예시

Director가 요청하는 각각의 요소를 생성하는 연산은 빌더 인터페이스에 구현한다. 그리고 이를 구현하는 서브클래스 concreteBuilder에서 실제로 필요한 요소의 생성 로직을 구현한다.

 

* 빌더 인터페이스

public interface TourPlanBuilder {

    // TourPlanBuilder를 리턴함으로써 메서드 체이닝이 가능하도록 함
  TourPlanBuilder title(String title);
  TourPlanBuilder nightsAndDays(int nights, int days);
    TourPlanBuilder whereToStay(String whereToStay);

    // 최종적으로 생성할 인스턴스의 상태 검증도 가능
  TourPlan getPlan();
}

 

* 빌더 구현체

public class DefaultTourPlanBuilder implements TourPlanBuilder{
  private String whereToStay;
  private String title;
  private Integer nights;
  private Integer days;

  @Override
  public TourPlanBuilder title(String title) {
      this.title = title;
      return this;
  }

  @Override
  public TourPlanBuilder nightsAndDays(int nights, int days) {
      this.nights = nights;
      this.days = days;
      return this;
  }

  @Override
  public TourPlanBuilder whereToStay(String whereToStay) {
      this.whereToStay = whereToStay;
      return this;
  }

  @Override
  public TourPlan getPlan() {
      // 필요하다면 여기서 validate
      return new TourPlan(whereToStay, title, nights, days);
  }
}

 

* Director

/* 자주 사용하는 빌더를 director클래스에 모아두고 사용할 수 있음 */
public class TourDirector {

  private TourPlanBuilder builder;

  public TourDirector(TourPlanBuilder builder){
      this.builder = builder;
  }

  public TourPlan defaultTrip(){
      return builder.title("title")
              .whereToStay("seoul")
              .nightsAndDays(2,3)
              .getPlan();
  }
}

 

* 실제 사용

public static void main(String[] args) {

    // director를 통해서 객체를 받아옴
  TourDirector director = new TourDirector(new DefaultTourPlanBuilder());

  System.out.println(director.defaultTrip());
}

 

실제 사용되는 빌더 패턴의 예시

GoF에 비해서 이펙티브 자바에서 말하는 빌더패턴은 좀 더 실제 사용성에 무게를 둔다. 코드의 가독성, 유지보수가 편리해지므로 빌더패턴 사용을 권한다.

아래는 자주 사용되는 빌더패턴 구현코드이다.

참고 : https://johngrib.github.io/wiki/builder-pattern/

// Effective Java의 Builder Pattern
public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {
        // Required parameters(필수 인자)
        private final int servingSize;
        private final int servings;

        // Optional parameters - initialized to default values(선택적 인자는 기본값으로 초기화)
        private int calories      = 0;
        private int fat           = 0;
        private int carbohydrate  = 0;
        private int sodium        = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings    = servings;
        }

        public Builder calories(int val) {
            calories = val;
            return this;    // 이렇게 하면 . 으로 체인을 이어갈 수 있다.
        }
        public Builder fat(int val) {
            fat = val;
            return this;
        }
        public Builder carbohydrate(int val) {
            carbohydrate = val;
            return this;
        }
        public Builder sodium(int val) {
            sodium = val;
            return this;
        }
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

팩토리 메서드 패턴과의 차이점

  팩토리 메서드 패턴에서는 조건에 따른 적당한 객체의 생성을 팩토리 클래스로 위임하여 팩토리 메서드를 통해서 객체를 생성한다.

  추상 팩토리 패턴은 서로 관련이 있는 객체를 다 묶어서 팩토리 클래스로 만들고 이 팩토리 클래스를 조건에 따라서 생성하도록 다시 팩토리로 만들어서 객체를 생성한다.


즉, 추상 팩토리 패턴은 팩토리 메서드 패턴을 한 단계 더 캡슐화한 버전이라고도 볼 수 있겠다.


  ex) 컴퓨터 생성 시, 부속품(키보드, 마우스)의 생성을 S사, L사 중에서 어떤걸로 할지 정한다고 했을떄, 가장 상위 Creator인 컴퓨터 Product 생성 시점에 구체 클래스(팩토리) 분기처리를 하고, 각 팩토리에서는 동일한 성격을 가지는 부속품 Product를 생성하도록 한다.

  → 팩토리 클래스 내부에 각 부속품에 대한 팩토리 클래스가 있는 구조

팩토리 메서드 패턴은 팩토리(creator)를 구현하는 방법 (inheritance)에 초점을 두고 구체적인 객체 생성 과정을 하위(구체클래스) 클래스로 옮기는 것이 목적이고

추상 팩토리 패턴은 팩토리를 사용하는 방법 (composition)에 초점을 두고 관련있는 여러 객체를 구체 클래스에 의존하지 않고 만들 수 있게 해주는 것이 목적이다

- 인프런 강의 정리

 

의도

  상세화된 서브클래스를 정의하지 않고도 서로 관련성이 있거나 독립적인 여러 객체 군을 생성하기 위한 인터페이스를 제공한다.

 

활용성

추상 팩토리는 하기 경우에 사용한다.

  • 객체의 생성, 구성, 표현 방식과 무관하게 시스템을 독립적으로 만들고자 할 때
  • 여러 제품군 중 하나를 선택해서 시스템을 설정해야하고 이를 다른 것으로 대체할 수 있을 때
  • 관련된 제품이 함께 사용되도록 설계되었고, 이 부분에 대한 제약이 외부에도 지켜지도록 할 때
  • 제품에 대한 클래스 라이브러리를 제공하고 구현이 아닌 인터페이스를 외부에 노출하고자 할 때

 

구조

 

협력 방법

  일반적으로 하나의 ConcreteFactory 클래스의 인스턴스가 런타임 시 생성된다. 이 인스턴스는 특정 구현을 갖는 제품 객체를 생성한다. 다른 제품 객체를 생성하려면 다른 ConcreteFactory 의 인스턴스를 받아서 사용해야 하고

AbstractFactory는 필요한 Product 객체를 생성하는 책임을 ConcreteFactory 서브 클래스에 위임한다.

 

장단점

  1. (+) 구체적인 클래스를 분리
    Product 객체를 생성하는 과정, 책임을 캡슐화해서 구체적인 구현 클래스가 클라이언트에게서 분리된다.

  2. (+) 제품군을 쉽게 대체
    Creator의 구체 클래스의 인스턴스를 받아오면 전체 제품군에 대한 생성이 가능하기 때문에 일괄적으로 제품군을 받아올 수 있다.

  3. (+) Product 사이에 일관성을 증진
    하나의 군 안에 속한 제품 객체들이 함께 동작하도록 설계되어있을때, 이들 사이의 일관성이 중요하다. 추상 팩토리 패턴을 통해서 제품군에 대한 생성 등을 하나의 팩토리 클래스로 관리하기 때문에 이를 보장한다.

  4. (-) 새로운 종류의 Product를 추가하기 어려움
    생성되는 Product는 기존 추상 팩토리가 생성할 수 있는 제품 집합에만 고정되어있다. 새로운 Product가 추가된다면 해당 추상 팩토리의 모든 서브클래스를 이에 대응해서 변경해야 한다.

 

구현

  1. 팩토리를 단일체로 정의한다.
    하나의 제품군에 대해서 하나의 ConcreteFactory 클래스의 인스턴스만 있기 때문에 인스턴스를 중복해서 생성할 필요가 없다.

  2. 제품의 생성
    AbstractFactory는 제품을 생성하기 위한 인터페이스를 선언하고 실제 그것을 생성하는 책임은 Product의 서브클래스인 ConcreteProduct에 있다. AbstractFactory를 구현한 서브클래스에서는 필요한 제품군의 생성을 관리한다.

  3. 확장 가능한 팩토리들을 정의
    AbstractFactory에는 생성할 각 제품의 종류별로 서로 다른 생성 연산을 정의한다. (하나의 제품군에 포함되어 있는 여러 제품의 생성을 담당)
    이때 생성되는 제품의 종류는 메서드의 시그니처를 통해서 알 수 있다.

싱글톤

오직 한 개의 클래스 인스턴스만을 갖도록 보장하고, 이에 대한 전역적인 접근점을 제공한다. 어떤 클래스는 정확히 하나의 인스턴스만을 갖도록 하는 것이 좋다.

ex) 시스템에 많은 프린터가 있어도 프린터 스풀은 하나여야 한다.

ex) 파일 시스템도 하나로 운영되어야 한다.

해당 클래스에 대한 하나의 인스턴스만 만들고 전역변수를 통해서 이에 접근시킬 수 있지만 더 좋은 방법은 클래스 자신이 자기의 유일한 인스턴스로 접근하는 방법을 자체적으로 관리하는 것이다. 다른 인스턴스의 생성을 방지하고 클래스 자신이 단일 인스턴스에 접근 방법을 제공하는 패턴을 싱글톤 패턴이라고 한다.

 

활용성

다음과 같은 상황에서 사용할 수 있다.

  • 클래스의 인스턴스가 오직 하나임이 보장되어야 하고, 정의된 접근점을 통해서 모든 사용자가 접근해야 할 때
  • 유일한 인스턴스가 서브클래싱으로 확장되어야 하마, 사용자는 코드의 수정없이 확장된 서브클래스의 인스턴스를 사용할 수 있어야 할 때

 

사용 용례

  • 자바 Runtime 객체
  • Spring프레임워크 ApplicationContext에 등록된 bean을 getBean() 을 통해서 꺼내올때 항상 같은 인스턴스 반환
    → 싱글톤 scope이고 싱글톤패턴과는 좀 다름

 

구조

  • Singleton : Instance() 연산을 통해서 유일한 인스턴스에 접근할 수 있도록 한다.

 

결과

싱글톤 패턴을 통해서 얻는 장점은 다음과 같다.

  • 애플리케이션 시작 시, 최초 한번만 메모리를 할당하기 때문에 메모리 낭비를 줄인다.
  • 싱글톤 클래스 자체가 인스턴스를 캡슐화하기 때문에 인스턴스 접근에 대한 제어가 가능하다.
  • 싱글톤 클래스는 상속이 가능하기 때문에 상속된 서브클래스를 통해서 새로운 인스턴스를 생성할 수 있도록 한다. (연산이나 표현이 변경되는 것에 유연함)
  • 인스턴스의 개수를 변경하는 것에 자유롭다. (클래스 연산(static)을 사용하는 것보다 설계 변경이 용이)
  • 싱글쓰레드 환경에서는 별 문제가 되지 않을 수 있으나 멀티쓰레드 환경에서는 Instance() 메서드가 동시에 다른 쓰레드에서 호출됨으로써 싱글톤 인스턴스의 단일성이 보장되지 않을 수 있다.

 

구현

싱글톤 패턴을 구현할 때는 인스턴스가 유일해야 함을 보장해야한다.

 

 

소스예제

 

1. 간단한 버전

  • 싱글톤 패턴 구현을 위해서는 new 를 사용해서 인스턴스를 만들게 허용하지 않아야하므로 생성자의 접근제어자를 private으로 처리
  • static으로 public getter를 만들고 이를 전달해주는 로직을 해당 클래스에서 관리 (하나의 인스턴스로 관리)
private static Settings instance;

private Settings(){}

public static Settings getInstance(){
    if(instance == null){
        instance = new Settings();
    }

    return instance;
}

이 방법은 쓰레드 세이프 하지 않음

 

2. 멀티쓰레드 환경에서 안전하게 구현

  1. 위의 getInstance() 메서드에 synchronized 키워드 사용
    → 동기화처리 때문에 성능 이슈 발생 가능성 있음 (lock 발생)
    → 메서드 레벨에 synchronized를 붙이지 않고 double checked locking 사용함으로써 이미 instance가 null이 아닌 경우는 sync 처리를 하지 않아도 되도록 구현
private static **volatile** Setting instance;

if(instance == null){
    synchronized (Settings.class){
        if(instance == null){
            instance = new Settings();
        }
    }
}
  1. instance 객체를 선언하는 시점에 이른 초기화(eager initialization)를 통해서 미리 만들어 두기
    → 인스턴스 생성이 길고 메모리 사용을 많이 한다면 비효율적일 수 있음
private static final Settings INSTANCE = new Settings();
  1. static inner 클래스 사용
    → getInstance()가 호출되는 시점에 inner 클래스가 로딩되고 그때 인스턴스 생성을 하기 때문에 lazy loading이 가능
// 3. static inner 클래스 사용
private static class SettingsHolder{
    private static final Settings INSTANCE = new Settings();
}
public static Settings getInstanceStaticInner(){
    return SettingsHolder.INSTANCE;
}

 

3. 안전하고 단순하게 구현하는 방법

  • Enum 사용
    → 리플렉션, 직렬/역직렬화에도 안전
    → 단점은 lazy loading없이 미리 만들어져 있음
public enum Settings{
    INSTANCE;
}

Settings instance = Setting.INSTANCE;

 

싱글톤 패턴 구현을 깨뜨리는 사용 방법

  1. 리플렉션을 사용해서 싱글톤 인스턴스를 만드는 경우
  2. 직렬화 & 역직렬화 해서 인스턴스를 사용하는 경우
    → readResolve() 메서드 안에서 싱글톤 인스턴스를 가져오게끔 수정하면 의도대로 사용 가능

 

질문거리

  • 자바에서 enum을 사용하지 않고 싱글톤 패턴을 구현하는 방법은?
    • synchronized를 사용한 double checked locking 방식으로 구현
    • 객체 선언 시점에 미리 초기화해두는 eager init 방식 사용
    • static inner class(holder) 를 사용하는 방식
  • private 생성자와 static 메서드를 사용하는 방법의 단점은?
    • thread-safe 하지 않음
  • enum을 사용해 싱글톤 패턴을 구현하는 방법의 장점과 단점은?
    • 코드가 간결하고 리플렉션, 직렬화/역직렬화 등에도 대응이 되지만 lazy loading하는 방식이 아니라 미리 객체 사용유무에 상관없이 선언되어 있어야함
    • 상속을 사용하지 못한다

 

참고자료

  • GoF의 디자인 패턴 (개정판)
  • 인프런 강의 - 코딩으로 학습하는 GoF의 디자인 패턴

기존 코드를 클라이언트가 사용하는 인터페이스의 구현체로 바꿔주는 패턴

사용하는 이유

레거시 인터페이스를 신규 인터페이스로 교체하는 경우, 레거시에서 사용하고 있던 소스 변경 없이 그대로 사용하기 위해서는 두 가지 방법이 있다.

  • 신규 인터페이스에서 레거시 인터페이스의 구현을 모두 상속받는다. (클래스 버전)
  • 신규 인터페이스의 구현체에 레거시 구현체를 포함시키고 레거시 인터페이스를 사용해서 신규 인터페이스 구현체를 구현한다. (객체 버전)Adaptor에서는 기존 레거시에서 제공되지 못했던 추가적인 기능을 제공할 수 있다.
 레거시의 기능을 그대로 사용하는 더 확장된 신규 인터페이스 제공 가능
  • 활용성
    • 기존 클래스를 사용하고 싶은데 인터페이스가 맞지 않을 경우
    • 이미 만든 것을 재사용하고자 하나 이 재사용 가능한 라이브러리를 수정할 수 없는 경우
    • 이미 존재하는 여러 서브클래스를 사용할 때, 이 서브클래스의 상속을 통해서 이들의 인터페이스를 모두 수정하는 것이 어렵기 때문에 Adaptor를 사용해서 부모 클래스의 인터페이스를 수정하는 것이 더 효율적인 경우

 

구조

어댑터 패턴 구조

Adaptor(신규)는 Adaptee(레거시) 인스턴스를 포함함으로써 자신의 Request() 메서드 호출 시, Adaptee의 SpecificRequest() 를 호출할 수 있는 구조를 가진다.

장단점

  • 장점
    • 코드의 재사용성 증가
    • 서로 관계가 없는 여러 Adaptee 클래스를 하나의 Adaptor 클래스로 사용 가능
    • 기존의 코드 변경 없어도 됌 (객체지향원칙 OCP에 가까운 패턴)
  • 단점
    • 클래스 버전의 경우, Adaptee 클래스의 서브클래스가 있는 경우, Adaptor 에서 해당 서브클래스에 정의된 기능을 사용할 수 없음 (해당 케이스는 어댑터 패턴 적용 X)
    • 클래스 버전의 경우, 다중상속이 지원되지 않는 java 같은 언어에서는 adaptor당 하나의 adaptee 클래스만 상속받을 수 있음
    • 객체 버전의 경우, Adaptee 객체를 만들어야해서 추가적으로 관리해야하는 객체가 늘어난다

 

예제 코드

객체 버전의 어댑터 패턴 예제로 작성하였다.

class Adaptee{
    public void legacyMethod(){
        System.out.println("Hello World!");
    }
}

 

class Adaptor{
    Adaptee adaptee;

    Adaptor(Adaptee adaptee){
        this.adaptee = adaptee;
    }

    public void newMethod(){
                // 기존 레거시 메서드에 추가 기능 정의 
        System.out.println("new method");
        this.adaptee.legacyMethod();
    }

}

 

public class AdaptorMain {
    public static void main(String[] args){

        Adaptee adaptee = new Adaptee();
        Adaptor adaptor = new Adaptor(adaptee);

        adaptor.newMethod();
    }
}
/*
new method
Hellow World!
*/

 

참고 자료

전략 패턴

동일 계열의 알고리즘군을 정의, 캡슐화하고 이를 사용하는 클라이언트와 상관없이 독립적으로 알고리즘을 서로 교환해서 사용할 수 있다.

사용하는 이유

만약 텍스트 스트림을 라인으로 구분하는 기능(다수 알고리즘이 있음)이 필요하다고 했을 때, 이 알고리즘들을 클래스에 하드코딩하는 것은 하기 이유로 바람직하지 않다.

  • 기능에 대한 구현이 아닌 줄 분리 코드가 반복적으로 생김으로써 소스가 복잡해짐
  • 때에 따라서 필요한 알고리즘이 다르기 때문에 모든 알고리즘을 제공할 필요가 없음
  • 서비스로직과 줄 분리 알고리즘이 합쳐져 있으면 알고리즘 로직을 추상화시켜서 확장 시키기 어려움
  • 위 문제 해결을 위해서 서비스로직과 각각의 줄 분리 알고리즘을 하나씩 캡슐화 한 클래스를 따로 정의하고 이 캡슐화된 알고리즘을 전략이라고 지칭한다.

구조

전략패턴 구조

비슷한 계열의 알고리즘을 가지는 Strategy 인터페이스를 구현하는 N개의 전략 객체가 있고 Context(서비스로직)에서는 concrete한 전략 객체(알고리즘) 인스턴스를 주입받아서 직접 알고리즘 로직을 호출한다.

장단점

  • 장점
    • 코드 중복 방지
    • 런타임 시에 타겟 메서드 변경 가능
    • 동일 계열 알고리즘군을 정의했기 때문에 확장성, 재사용성이 높음
  • 단점
    • 여러 전략을 사용하는 케이스가 다 다르므로 이를 이해하고 있어야함
    • 전략의 복잡도와는 상관없이 Strategy객체는 Context객체에서 전달하는 미사용 매개변수를 받아야함

예제 코드

// Strategy 인터페이스
interface BillingStrategy{
    double GetActPrice(double rawPrice);
}

// 전략1 : Normal billing strategy (unchanged price)
class NormalStrategy implements BillingStrategy{
    public double GetActPrice(double rawPrice){
        return rawPrice;
    }
}

// 전략2 : Strategy for Happy hour (50% discount)
class HappyHourStrategy implements BillingStrategy{
    public double GetActPrice(double rawPrice){
        return rawPrice * 0.5;
    }
}

// Context
class Customer{
    private List<Double> drinks;
    public BillingStrategy strategy;

    public Customer(BillingStrategy strategy){
        this.drinks = new ArrayList<>();
        this.strategy = strategy;
    }

    // 전략 패턴 사용
    public void Add(double price, int quantity){
        drinks.add(strategy.GetActPrice(price * quantity));
    }

    // Payment of bill
    public void PrintBill(){
        double sum = 0;
        for(Double i : drinks){
            sum += i;
        }
        System.out.println(("Total due: " + sum));
        drinks.clear();
    }
}interface BillingStrategy{
    double GetActPrice(double rawPrice);
}

// 전략1 : Normal billing strategy (unchanged price)
class NormalStrategy implements BillingStrategy{
    public double GetActPrice(double rawPrice){
        return rawPrice;
    }
}

// 전략2 : Strategy for Happy hour (50% discount)
class HappyHourStrategy implements BillingStrategy{
    public double GetActPrice(double rawPrice){
        return rawPrice * 0.5;
    }
}

public class StrategyMain {

    public static void main(String[] args){
        Customer firstCustomer = new Customer(new NormalStrategy());

        // Normal billing
        firstCustomer.Add(1.0, 1);

        // Start Happy Hour
        firstCustomer.strategy = new HappyHourStrategy();
        firstCustomer.Add(1.0, 2);

        // New Customer
        Customer secondCustomer = new Customer(new HappyHourStrategy());
        secondCustomer.Add(0.8, 1);
        // The Customer pays
        firstCustomer.PrintBill();

        // End Happy Hour
        secondCustomer.strategy = new NormalStrategy();
        secondCustomer.Add(1.3, 2);
        secondCustomer.Add(2.5, 1);
        secondCustomer.PrintBill();
    }
}

/*
firstCustomer  -> Total due : 2.0
secondCustomer -> Total due : 5.5
*/

참고 자료

의도

  객체를 생성하기 위해서 인터페이스를 정의하고, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브클래스에서 내리도록 한다.

 

동기 (사용 예시)

  사용자에게 다양한 종류의 문서를 표현하는 응용프로그램 프레임워크가 있다고 가정했을때 다음 두 가지 추상화가 필요하다

  • Application 클래스

  • Document 클래스

    어떤 응용프로그램을 만드는지에 따라서 인스턴스로 만들어지는 Document의 서브 클래스가 달라지기 때문에 Application 클래스에서 어떤 Document의 인스턴스를 생성하는지 미리 예측할 수 없다.

    이러한 상황에서 이 패턴은 Document의 서브클래스 중 어느 것을 생성하는지에 정보를 캡슐화하고, 이를 Application 클래스와 분리한다.

    Application 클래스의 서브클래스는 추상화된 CreateDocument() 연산을 재정의해서 적당한 Document 클래스의 서브클래스를 반환하도록 한다.

참고할만한 예시

https://jdm.kr/blog/180

 

활용성

  팩토리 메서드 디자인 패턴을 사용하는 상황

  • 어떤 클래스가 자신이 생성해야 하는 객체의 클래스를 예측할 수 없을 때

  • 생성할 객체를 기술하는 책임을 자신의 서브클래스가 지정했으면 할 때

  • 객체 생성의 책임을 몇 개의 서브클래스 중 하나에 위임하고 어떤 서브클래스가 위임자인지에 대한 정보를 포괄적으로 관리하고 싶을 때 (반환받은 Document 클래스의 서브클래스를 사용하면 됌)

 

구조

  • Product(Document) : 팩토리 메서드가 생성하는 객체의 인터페이스 정의

  • ConcreateProduct(MyDocument) : Product 클래스(추상/인터페이스)를 실제로 구현한 내용

  • Creator(Application) : Product 타입의 객체를 반환하는 팩토리 메서드 정의, 위의 예시에서는 팩토리 메서드를 통해서 ConcreateProduct 객체를 반환한다.

  • ConcreateCreator(MyApplication) : 팩토리 메서드를 재정의하여 적당한 ConcreteProduct의 인스턴스 생성

    • 협력 방법

    Creator는 자신의 서브클래스를 통해서 실제 필요한 팩토리 메서드를 정의하고, 이를 통해서 상황에 맞는 적절한 ConcreteProduct의 인스턴스를 반환할 수 있게 한다.

 

결과

  팩토리 메서드 패턴을 적용함으로써 구상 클래스에 코드가 종속되지 않는다. 위의 예시에서 보면 Product 클래스에 정의된 인터페이스와 동작하기 때문에 서브클래스 ConcreateProduct가 추가되었을때 코드레벨에서 대응해야하는 양을 줄일 수 있다.

  • 클래스간의 결합도를 낮추기(클래스 변경점이 생겼을 때 다른 클래스에 영향도 최소화)

  • 직접 객체를 생성해 사용하는 것을 방지하고 서브클래스에 생성을 위임함으로써 의존성 제거, 확장에 용이

  • 인터페이스에(Product) 맞춰서 코딩을 함으로써 다형성으로 얻을 수 있는 장점 (여러 변화에 대처)

    다만 주의해야할 점은 팩토리 메서드는 새로운 concreateProduct를 추가하려고 할때마다 서브클래싱이 필요하다는 점이다. 불필요한 클래스 재정의 발생 가능성이 있다.

 

구현

  구현 시 고려해야하는 상황

  1. 구현 방법 정하기

    • Creator 클래스를 추상클래스로 정의하고 정의한 팩토리 메서드에 대한 구현은 제공 X

    • Creator를 구체 클래스로 정의하고 팩토리 메서드에 대한 기본 구현을 제공

    1. 팩토리 메서드 매개변수화

    팩토리 메서드가 매개변수를 받아서 어떤 종류의 Product를 생성할지 식별하게 할 수 있다.

    1. 언어별 구현 방법 상이

    2. 템플릿을 사용해서 서브클래싱

    팩토리 메서드를 사용함으로써 발생할 수 있는 단점은 Product 클래스를 추가하려고 할때마다 서브클래싱을 해야 한다는 점이다. Creator의 서브클래스가 되는 템플릿 클래스를 정의하고 이것이 Product 클래스로 매개변화되도록 구현한다.

    1. 명명 규칙의 중요성

    팩토리 메서드를 사용한다는 사실을 명확하게 할 수 있도록 Creator 클래스의 이름 등을 통일 (-Factory.java)

+ Recent posts