[Design Pattern] 생성(Creational) 패턴

01. 싱글톤 (Singleton) 패턴

  • 인스턴스를 오직 한개만 제공하는 패턴

구현 방법 1

  • private 생성자에 static 메소드
  • 멀티쓰레드 환경에서 안전하지 않음 (여러 쓰레드가 동시에 접근할 경우 여러 인스턴스가 생길수 있다)
public class Settings {

  private static Settings instance;

  private Settings() {
    // 외부에서 생성하지 못하게 private 으로 지정
  }

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

}

구현 방법 2

  • 동기화(synchronized)를 사용해 멀티쓰레드 환경에서 안전하게 만드는 방법
  • synchronized 특성상 성능이슈가 생길수있다.
public static synchronized Settings getInstance() {
  if (instance == null) {
    instance = new Settings();
  }
  
  return instance;
}

구현 방법 3

  • 이른 초기화 (eager initialization)을 사용하는 방법
  • Thread safe 하다.
  • 클래스 로딩 시점에 static field 가 초기화 되므로 객체 생성비용이 클 경우 단점이 될 수 있다.
private static final Settings INSTANCE = new Settings();

private Settings() {
}

public static Settings getInstance() {
  return INSTANCE;
}

구현 방법 4

  • double-checked locking
  • volatile 키워드를 이용해서 캐시 불일치 이슈를 방지 할 수 있다. (java 1.5 이상)
  • 필드에 instance 가 할당되지 않았을 경우에만 synchronized 블록을 실행하므로
    메서드에 synchronized 처리를 하는것보다 성능상 유리하다.
  • instance 가 필요한 시점에 만들수 있다.
public class Settings {

  private static volatile Settings instance;

  private Settings() {
  }

  public static Settings getInstance() {
    if (instance == null) {
      synchronized (Settings.class) {
        if (instance == null) {
          instance = new Settings();
        }
      }
    }
    return instance;
  }

}

구현 방법 5

  • static nested 클래스를 사용하는 방법 (Initialization-on-demand holder)
  • 클래스안에 클래스(Holder)를 두어 JVM 의 Class Loader 매커니즘과 Class 가 로드되는 시점을 이용한 방법
  • Settings 클래스에는 Holder 클래스의 필드가 없기 때문에 Settings 클래스 로딩 시 Holder 클래스를 초기화하지 않음
  • Lazy initialization 방식을 가져가면서 Thread 간 동기화 문제를 동시에 해결 가능
public class Settings {

  private static Settings instance;

  private static class SettingsHolder {
    private static final Settings SETTINGS = new Settings();
  }

  public static Settings getInstance() {
    return SettingsHolder.SETTINGS;
  }

}

구현 방법 6

  • enum 을 사용하는 방법
  • enum 은 상수 하나당 인스턴스가 만들어지며, 각각 public static final 로 공개한다.
  • 리플렉션을 통해 싱글톤을 깨트리는 공격에 안전하며, 직렬화를 보장한다.
public enum Settings { 
  INSTANCE
}

싱글톤 패턴을 깨트리는 방법

리플렉션 사용

Settings settings = Settings.getInstance();

Constructor<Settings> declaredConstructor = Settings.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Settings settings1 = declaredConstructor.newInstance();

System.out.println(settings == settings1); // false

직렬화 & 역직렬화 사용

Settings settings = Settings.getInstance();
Settings settings1 = null;
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
    out.writeObject(settings);
}

try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
    settings1 = (Settings) in.readObject();
}

System.out.println(settings == settings1); // false

02. 팩토리 메소드 (Factory Method) 패턴

  • 구체적으로 어떤 인스턴스를 만들지는 서브 클래스가 정하는 패턴
  • 다양한 구현체 (Product)가 있고, 그중에서 특정한 구현체를 만들 수 있는 다양한 팩토리 (Creator)를 제공할 수 있다.
  • 팩토리들은 객체의 인스턴스를 생성하는 부분을 캡슐화 하기 위해 사용된다.

구현 방법

// Product
public interface Pizza {
  void prepare();
  void bake();
  void box();
}

// ConcreteProduct
public class NYStyleCheesePizza implements Pizza { ... }
public class NYStylePepperoniPizza implements Pizza { ... }
public class ChicagoStyleCheesePizza implements Pizza { ... }
public class ChicagoStylePepperoniPizza implements Pizza { ... }
// Creator
public interface PizzaStore {
  
    default Pizza orderPizza(String type) {
      Pizza pizza = createPizza(type);
      pizza.prepare();
      pizza.bake();
      pizza.box();
      return pizza;
    }

    // factory method
    Pizza createPizza(String type);
}
// ConcreteCreator 1
public class NYPizzaStore extends PizzaStore {
  
  @Override
  public Pizza createPizza(String type) {
    if ("cheese".equals(type)) {
      return new NYStyleCheesePizza();
    } 
    if ("pepperoni".equals(type)) {
      return new NYStylePepperoniPizza();
    }
    throw new IllegalArgumentException();
  }
}

// ConcreteCreator 2
public class ChicagoPizzaStore extends PizzaStore {
  
  @Override
  public Pizza createPizza(String type) {
    if ("cheese".equals(type)) {
      return new ChicagoStyleCheesePizza();
    } 
    if ("pepperoni".equals(type)) {
      return new ChicagoStylePepperoniPizza();
    }
    throw new IllegalArgumentException();
  }
}
public static void main(String[] args) {
  PizzaStore nyStore = new NYPizzaStore();
  PizzaStore chicagoStore = new ChicagoPizzaStore();
  
  Pizza pizza = nyStore.orderPizza("cheese");
  Pizza pizza1 = chicagoStore.orderPizza("pepperoni");    
}

Simple Factory

  • 객체를 생성하는 역할을 하나의 팩토리 클래스가 전담하게 하는 방법.
  • 주어진 입력을 기반으로 다른 유형의 객체를 리턴하는 메소드가 있는 팩토리 클래스.
  • 디자인 패턴이라기 보다는 자주 사용되는 프로그래밍 기법으로 보면 된다.
public class PizzaFactory {

  public static Pizza createPizza(String type) {
    if ("cheese".equals(type)) {
      return new CheesePizza();
    } 
    if ("pepperoni".equals(type)) {
      return new PepperoniPizza();
    }
    throw new IllegalArgumentException();
  }
  
}

03. 추상 팩토리 (Abstract Factory) 패턴

  • 서로 연관되거나 의존적인 객체들의 조합을 만드는 인터페이스를 제공하는 패턴
  • 구체적으로 어떤 클래스의 인스턴스를(concrete product)를 사용하는지 감출 수 있다.

구현 방법

// Product
public interface Frame {
  void shape();
}
public interface Wheel {
  void size();
}

// ConcreteProduct
public class AFrame implements Frame { ... }
public class BFrame implements Frame { ... }
public class AWheel implements Wheel { ... }
public class BWheel implements Wheel { ... }
// AbstractFactory
public interface CarFactory {
  Frame createFrame();
  Wheel createWheel();
}

// ConcreteFactory 1
public class ACarFactory implements CarFactory {

  @Override
  public Frame createFrame() {
    return new AFrame();
  }

  @Override
  public Wheel createWheel() {
    return new AWheel();
  }
}

// ConcreteFactory 2
public class BCarFactory implements CarFactory {

  @Override
  public Frame createFrame() {
    return new BFrame();
  }

  @Override
  public Wheel createWheel() {
    return new BWheel();
  }
}
// Client 에서 사용할 대상
public class Car {
    private Frame frame;
    private Wheel wheel;

    public Car(Frame frame, Wheel wheel) {
        this.frame = frame;
        this.wheel = wheel;
    }

    public Frame getFrame() {
        return frame;
    }

    public Wheel getWheel() {
        return wheel;
    }
}
// Client
public class Client {

  private CarFactory carFactory;

  public Client(CarFactory carFactory) {
    this.carFactory = carFactory;
  }

  public Car createCar() {
    return new Car(carFactory.createFrame(), carFactory.createWheel());
  }
}
public static void main(String[] args) {
  Car car = new Client(new ACarFactory());
  System.out.println(car.getFrame().getClass());
  System.out.println(car.getWheel().getClass());

  Car car2 = new Client(new BCarFactory());
  System.out.println(car2.getFrame().getClass());
  System.out.println(car2.getWheel().getClass());
}

팩토리 메소드 패턴 vs 추상 팩토리 패턴

관점

  • 팩토리 메소드 패턴은 "팩토리를 구현하는 방법 (inheritance)" 에 초점을 둔다.
  • 추상 팩토리 패턴은 "팩토리를 사용하는 방법 (composition)" 에 초점을 둔다.

목적

  • 팩토리 메소드 패턴은 구체적인 객체 생성 과정을 하위 또는 구체적인 클래스로 옮기는 것이 목적.
  • 추상 팩토리 패턴은 관련있는 여러 객체를 구체적인 클래스에 의존하지 않고 만들 수 있게 해주는 것이 목적.

04. 빌더 (Builder) 패턴

  • 동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법.
  • (복잡한) 객체를 만드는 프로세스를 독립적으로 분리할 수 있다.

구현 방법

// Builder
public interface ToyBuilder {
  ToyBuilder name(String name);
  ToyBuilder size(int size);
  Toy build();
}

// ConcreteBuilder
public class DefaultToyBuilder implements ToyBuilder {

  private String name;
  private int size;

  @Override
  public ToyBuilder name(String name) {
    this.name = name;
    return this;
  }

  @Override
  public ToyBuilder size(int size) {
    this.size = size;
    return this;
  }

  @Override
  public Toy build() {
    return new Toy(name, size);
  }

}

// Product
public class Toy {
  private String name;
  private int size;
  
  public Toy(String name, String size) {
    this.name = name;
    this.size = size;
  }

  // getter, setter...
}
// Director
public class ToyDirector {
 
  private ToyBuilder toyBuilder;
  
  public ToyDirector(ToyBuilder toyBuilder) {
    this.toyBuilder = toyBuilder;
  }
  
  public Toy sampleToy() {
    return toyBuilder.name("sample")
        .size(5)
        .build();
  }
  
}
public static void main(String[] args) {
    ToyDirector director = new ToyDirector(new DefaultToyBuilder());
    Toy sampleToy = director.sampleToy();
    System.out.prinln(sampleToy);
}

장단점

장점

  • 필요한 데이터만 설정할 수 있다.
  • 만들기 복잡한 객체를 순차적으로 만들 수 있다.
  • 복잡한 객체를 만드는 구체적인 과정을 숨길 수 있다.
  • 동일한 프로세스를 통해 각기 다르게 구성된 객체를 만들 수도 있다. 불완전한 객체를 사용하지 못하도록 방지할 수 있다.

단점

  • 원하는 객체를 만들려면 빌더부터 만들어야 한다.
  • 구조가 복잡해 진다. (트레이드 오프)

사용하는곳

  • Java 8 Stream.Builder API
  • StringBuilder
  • Lombok @Builder
  • Spring UriComponentsBuilder
  • ...

05. 프로토타입 (Prototype) 패턴

  • 기존 인스턴스를 복제하여 새로운 인스턴스를 만드는 방법.
  • 복제 기능을 갖추고 있는 기존 인스턴스를 프로토타입으로 사용해 새 인스턴스를 만들 수 있다.
  • 이 패턴은 복사를 위하여 Java 에서는 Object 의 clone method 를 활용한다.

구현 방법

public class Employees implements Cloneable {

  private final List<String> values;

  public Employees() {
    this(new ArrayList<>());
  }

  public Employees(List<String> values) {
    this.values = values;
  }

  public List<String> getValues() {
    return values;
  }

  public void loadData() {
    values.add("AA");
    values.add("BB");
    values.add("CC");
    values.add("DD");
  }

  // shallow copy 일 경우
  @Override
  public Object clone() throws CloneNotSupportedException {
    return super.clone();
  }

  // deep copy 일 경우
  @Override
  public Object clone() throws CloneNotSupportedException {
    List<String> list = new ArrayList<>(values);
    return new Employees(list);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    Employees that = (Employees) o;
    return Objects.equals(values, that.values);
  }

  @Override
  public int hashCode() {
    return Objects.hash(values);
  }

}
public static void main(String[] args) throws CloneNotSupportedException {
  Employees employees = new Employees();
  employees.loadData();

  Employees clone = (Employees) employees.clone();

  assert employees != clone;
  assert employees.equals(clone);
  assert employees.getClass() == clone.getClass();

  // shallow copy, depp copy 동일
  assert employees.getValues().equals(clone.getValues());
  
  // shallow copy 일 경우
  assert employees.getValues() == clone.getValues();

  // deep copy 일 경우
  assert employees.getValues() != clone.getValues();
}

Shallow Copy vs Deep Copy

Shallow Copy

  • 객체를 복사할 때, 해당 객체만 복사하여 새 객체를 생성한다.
  • 복사된 객체의 인스턴스 변수는 원본 객체의 인스턴스 변수와 참조가 동일하다.

Deep Copy

  • 객체를 복사 할 때, 해당 객체와 인스턴스 변수까지 복사하는 방식.
  • 전부를 복사하여 새 주소에 담기 때문에 참조를 공유하지 않는다.

장단점

장점

  • 복잡한 객체를 만드는 과정을 숨길 수 있다.
  • 기존 객체를 복제하는 과정이 새 인스턴스를 만드는 것보다 비용(시간 또는 메모리)적인 면에서 효율적일 수도 있다.
  • 추상적인 타입을 리턴할 수 있다.

단점

  • 복제한 객체를 만드는 과정 자체가 복잡할 수 있다. (특히, 순환 참조가 있는 경우)

사용하는곳

  • Java Object 클래스의 clone 메서드, Cloneable Interface
  • ModelMapper

좋은 웹페이지 즐겨찾기