Written by
Poogle
on
on
디자인 패턴 - 생성 패턴
참고 링크
Books
Joshua Bloch(2018). Effective Java(3rd ed.). Addison-Wesley Professional.
Articles
The Catalog of Design Patterns
Posts
디자인 패턴
- 객체 지향 설계를 하다 보면 이전과 비슷한 상황에서 사용했던 설계를 재사용하는 경우가 종종 발생합니다.
- 클래스, 객체의 구성, 객체 간 메세지 흐름에서 일정한 패턴이 생성됩니다.
- 따라서 상황에 맞는 올바른 설계를 더 빠르게 적용하기 위해 각 패턴의 장단점을 파악하면 -> 설계를 선택하는데 도움이 됩니다.
- 또한 설계 패턴의 이름을 부여해 시스템을 문서화할 수 있고 이해하기 쉬우며 유지 보수하는데 도움이 됩니다.
Creational Patterns 생성 패턴
- 객체 생성에 관련된 패턴입니다.
- 객체의 생성과 조합을 캡슐화 -> 특정 객체가 생성, 변경되어도 프로그램 구조에 영향을 크게 받지 않도록 유연성을 제공합니다.
<5가지 생성 패턴>
- Factory Method 팩토리 메서드 패턴
- Abstract Factory 추상 팩토리 패턴
- Builder 빌더 패턴
- Prototype 원형 패턴
- Singleton 싱글톤 패턴
Factory Method 팩토리 메서드 패턴
- 객체를 생성하는 인터페이스를 정의하지만 인스턴스를 만드는 클래스는 서브클래스에서 결정합니다.
- 객체 생성을 캡슐화합니다.
- Creator의 서브클래스에 팩토리 메서드를 정의하여 팩토리 메서드 호출로 적절한 ConcreteProduct 인스턴스를 반환합니다.
- 방법 1: Creator 추상 클래스로 정의, 팩토리 메서드는 abstract로 선언합니다.
- 방법 2: Creator가 구체 클래스이고, 팩토리 메서드의 기본 구현을 제공하는 방법입니다.
방법 2를 활용한 예제
Product
: Pizza 인터페이스ConcreteProduct
: 팩토리 메서드 호출로 반환될 인스턴스- NYStyleCheesePizza
- NYStylePepperoniPizza
- ChicagoStyleCheesePizza
- ChicagoStylePepperoniPizza
Creator
: PizzaStoreConcreteCreator
: 팩토리 메서드를 정의할 서브클래스- NYPizzaStore
- ChicagoPizzaStore
interface Pizza {
public void prepare();
public void bake();
public void box();
}
abstract class PizzaStore {
public Pizza orderPizza(String type) {
Pizza pizza = createPizza(type); // factory method 사용
pizza.prepare();
pizza.bake();
pizza.box();
return pizza;
}
// factory method
abstract Pizza createPizza(String type);
}
class NYPizzaStore extends PizzaStore {
@Override
Pizza createPizza(String type) {
if ("cheese".equals(type)) {
return new NYStyleCheesePizza();
} else if ("pepperoni".equals(type)) {
return new NYStylePepperoniPizza();
}
return null;
}
}
class ChicagoPizzaStore extends PizzaStore {
@Override
Pizza createPizza(String type) {
if ("cheese".equals(type)) {
return new ChicagoStyleCheesePizza();
} else if ("pepperoni".equals(type)) {
return new ChicagoStylePepperoniPizza();
}
return null;
}
}
사용
PizzaStore nyStore = new NYPizzaStore();
PizzaStore chicagoStore = new ChicagoPizzaStore();
Pizza pizza = nyStore.orderPizza("cheese");
Pizza pizza1 = chicagoStore.orderPizza("pepperoni");
Abstract Factory 추상 팩토리 패턴
- 상세화 된 서브클래스를 정의하지 않고도 서로 관련성이 있거나 독립적인 여러 객체의 군을 생성하기 위한 인터페이스를 제공합니다.
- 클라이언트에 영향을 주지 않으면서 사용할 제품(객체)군을 교체할 수 있습니다.
- 객체가 생성, 구성, 표현되는 방식과 무관하게 시스템을 독립적으로 만들 수 있습니다.
- 제품에 대한 클래스 라이브러리를 제공하고, 그들의 구현이 아닌 인터페이스를 노출시키고 싶을 때 사용합니다.
추상 팩토리 패턴 정의 예시 - JAVA JDBC API 구조
- 클라이언트는
Connection
(Connection
이 팩토리에 해당)을 이용 ->Statement
객체와PreparedStatement
객체를 생성합니다. - DBMS 별로 알맞은
Statement Concrete Class
와PreparedStatement Class
제공합니다. - 콘크리트 클래스의 객체를 생성해주는 Connection 구현 클래스를 제공합니다.
- ⏩ 클라이언트가 사용할 DBMS 변경할 경우 수정 없이 팩토리에 있는 Connection 객체만 교체합니다.
Builder 빌더 패턴
Effective Java - 아이템 2에서 소개되는 Builder 패턴을 기준으로 설명
- 빌더 패턴은 복잡한 객체를 생성하는 클래스와 표현하는 클래스를 분리하여, 동일한 절차에서도 서로 다른 표현을 생성하는 방법을 제공합니다.
- 객체의 생성 알고리즘이 조립 방법에 독립적일 때 & 합성할 객체들의 표현이 서로 다르더라도 생성 절차에서 표현 과정을 지원해야할 때 사용합니다.
점층적 생성자 패턴
public class User {
private final String name;
private final String location;
private final String hobby;
public User(String name) {
this(name, "x", "x");
}
public User(String name, String location) {
this(name, location, "x");
}
public User(String name, String location, String hobby) {
this.name = name;
this.location = location;
this.hobby = hobby;
}
}
- 필수, 선택 매개변수가 필요할 때마다 생성자를 늘려가는 패턴입니다.
- 단점: 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵습니다.
자바 빈즈 패턴(JavaBeans Pattern)
public class User {
private String name = "이름";
private String address = "주소";
private String hobby = "취미";
public User() {}
/// setter
User user = new User();
user.setName("Poogle");
user.setAddress("Seoul");
user.setHobby("driving");
- 매개변수가 없는 생성자로 객체를 만든 후, 세터를 호출해 원하는 매개변수의 값을 설정하는 방식입니다.
- 단점: 객체 하나를 만들려면 메서드를 여러 개 호출해야 함, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태 -> 클래스를 불변으로 만들 수 없습니다.!
- 점층적 생성자 패턴의 안정성 + 자바빈즈 패턴의 가독성 => 빌더 패턴
- 필수 매개변수만으로 생성자나 정적 팩터리를 호출해 빌더 객체를 얻습니다.
- 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정합니다.
- 매개변수가 없는
build()
메서드를 호출해 객체 생성합니다.
public class User {
private final String name;
private final String address;
private final int age;
private final String hobby;
public static class Builder {
//필수 매개변수
private final String name;
private final int age;
//선택 매개변수 - 기본값으로 초기화
private String address = "주소";
private String hobby = "취미";
public Builder(Strng name, int age) {
this.name = name;
this.age = age;
}
public Builder address(String val) {
address = val;
return this;
}
public Builder hobby(String val) {
hobby = val;
return this;
}
public User build() {
return new User(this);
}
}
private User(Builder builder) {
name = builder.name;
age = builder.age;
address = builder.address;
hobby = builder.hobby;
}
}
- User 클래스는 불변, 모든 매개변수의 기본값들을 한 곳에 모아 둠
- 빌더의 세터 메서드들은 빌더 자신을 반환 -> 연쇄적으로 호출 가능
사용
User user = new User.Builder("Poogle", 27) //필수값
.address("Seoul")
.hobby("driving")
.build(); //build로 객체 생성
장점
- 각 인자가 어떤 의미인지 알기 쉽습니다.
- setter 없음 -> 불변객체
- 한 번에 객체 생성 -> 객체 일관성이 깨지지 않음
- 유연함: 빌더 하나로 여러 객체를 순회하면서 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수 있습니다.
- 표현을 다양하게 변경할 수 있습니다.
- 생성과 표현 코드를 분리합니다.
-
복합 객체를 생성하는 절차를 세밀하게 나눌 수 있습니다.
- 참고: Lombok
@Builder
어노테이션으로 빌더 패턴을 쉽게 쓸 수 있습니다.@Builder public class User { private final String name; private final String address; private final int age; private final String hobby; }
Prototype 원형 패턴
- 객체를 생성하는데 비용이 많이 들고 비슷한 객체가 이미 있는 경우에 사용합니다.
- DB에 계속 접근해서 데이터를 수정하기보다 가져온 객체를 새로운 객체에 복사해서 데이터 수정 작업하기 (객체 복사의 비용이 훨씬 적기 때문)
- Java의
clone()
사용합니다.clone()
메서드 재정의를 위해 Cloneable 인터페이스를 구현합니다.
Singleton 싱글톤 패턴
- 클래스의 인스턴스가 하나임을 보장하고 접근할 수 있는 전역적인 접근점을 제공하는 패턴입니다.
- 동시성 문제 고려
- 인스턴스가 1개만 생성되는 특징 -> 하나의 인스턴스를 메모리에 등록해서 여러 스레드가 동시에 해당 인스턴스를 공유하여 사용하게끔 할 수 있습니다. => 요청이 많은 곳에서 효율⬆
- 객체 생성을 한 번으로 제한합니다.
- 전역 변수를 사용하지 않고 객체를 하나만 생성하도록 하며, 생성된 객체를 어디에서든지 참조할 수 있도록 하는 패턴입니다.
- 싱글턴(Singleton): 인스턴스를 오직 하나만 생성할 수 있는 클래스
- 전형적인 예: 무상태(stateless)객체, 설계상 유일해야 하는 시스템 컴포넌트
- 하나의 인스턴스만을 생성하며 getInstance메서드로 모든 클라이언트에게 동일한 인스턴스를 반환합니다.
Java와 Spring 싱글톤 차이점
- 싱글톤 객체의 생명주기 차이
- 자바에서 싱글톤 객체의 범위는 클래스 로더가 기준이지만, 스프링에서는 어플리케이션 컨텍스트가 기준이 됩니다.
- private 생성자를 가지는 특징을 가지며, 생성된 싱글톤 오브젝트는 저장할 수 있는 자신과 같은 타입의 스태틱 필드를 정의합니다.
싱글톤 패턴의 문제점
- 의존 관계상 클라이언트가 구체 클래스에 의존합니다.
- private 생성자 때문에 테스트가 어렵습니다.
- 객체 인스턴스를 하나만 생성해서 공유하는 방식 때문에 싱글톤 객체를 stateful하게 설계 했을 경우 큰 장애 발생요인이 됩니다.
- 싱글톤의 단점을 해결하기 위해 무상태(stateless)로 설계해야 합니다.
- 특정 클라이언트에 의존적인 필드가 있으면 안됩니다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안됩니다.
- 가급적 읽기 전용으로 만들고, 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용합니다.
이른 초기화 방식 (Eager Initialization)
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return uniqueInstance;
}
}
static
: 클래스 로더가 초기화하는 시점에서 정적 바인딩(static binding, 컴파일 시점에서 성격 결정)을 통해 인스턴스를 메모리에 등록합니다.- 클래스가 최초로 로딩될 때 객체가 생성 ⏩ Thread-safe 보장되어야 합니다.
- ⚠ 예외:
- 권한이 있는 클라이언트는 리플렉션 API인
AccessibleObject.setAccessible
을 사용해 private 생성자를 호출할 수 있습니다. - 이러한 공격을 방어하려면 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지게 하면 됩니다.
- 권한이 있는 클라이언트는 리플렉션 API인
늦은 초기화 방식 (Lazy Initialization - synchronized)
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if(uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
synchronized
: 메서드에 동기화 블럭을 지정해서 Thread-safe 보장합니다.- 컴파일 시점이 아닌 인스턴스가 필요한 시점에 요청하여 동적 바인딩(dynamic binding, 런타임 시점에서 성격 결정)을 통해 인스턴스를 생성합니다.
- 단점: 인스턴스 생성과 관계없이 무조건 동기화 블록을 거침 -> 성능이 떨어집니다.
DCL 방식 (Lazy initialization - Double Checking Locking)
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {}
public Singleton getInstance() {
if(uniqueInstance == null) {
synchronized(Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
- 인스턴스가 생성되지 않은 경우에만 동기화 블럭이 실행되게끔 구현하는 방식입니다.
volatile
: 멀티 쓰레딩을 쓰더라도 uniqueInstance 변수가 Singleton 인스턴스로 초기화 되는 과정이 올바르게 진행되도록 보장합니다.- 사용하지 않으면?: 작업 수행하는 동안 메인 메모리에서 읽은 변수값을 CPU Cache에 저장하게 되는데 멀티 쓰레드에서는 쓰레드가 변수 값을 읽어올 때 각각의 CPU Cache에 저장된 값이 달라서 변수값 불일치 문제가 발생합니다.
- ⏩ read and write
열거 타입 방식 (Lazy Initialization - Enum)
public enum Singleton {
INSTANCE;
}
- public 필드 방식과 비슷하나 더 간결하고 추가 노력 없이 직렬화 할 수 있습니다.
- 아주 복잡한 직렬화 상황이나 리플렉션 공격에서도 제 2의 인스턴스가 생기는 일을 막아줍니다.
- ⚠ 싱글턴을 만드는 가장 좋은 방법이지만 만들려는 싱글턴이 Enum 이외의 클래스를 상속해야 할 때는 사용할 수 없습니다.(열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있음)