본문 바로가기

디자인패턴 - 생성패턴

객체지향 프로그래밍을 위한 디자인 패턴중 생성에 관한 패턴을 다룬다.

 

1. Simple Factory

2. Factory Method

3. Abstract Factory

4. Singleton

5. Builder

6. Prototype

 

1. Simple Factory

객체의 생성을 별도의 클래스로 위임하여 객체간 의존성을 줄인다.

간단한 팩토리 패턴이다. 

이 패턴은 굳이 디자인 패턴이라고 하지 않는 사람들도 많이 있다. 

이 패턴의 목적은 객체의 생성을 자기 자신이 하는것이 아닌 관련된 별도의 객체로 위임함으로써, 객체간 의존 관계를 줄일 수 있게 된다.

 

Simple Factoy Class Diagram

해당 다이어그램을 보면, 제품 클래스인 Product를 SimpleFactory라는 클래스가 생성해주게 된다.

코드는 다음과 같다.

 

Product.java

public interface Product {
    public void echo();
}

제품의 인터페이스인 Product 인터페이스다. 

간단한 echo 메소드 하나만 존재한다.

ConcreteProduct.java

public class ConcreteProduct1 implements Product {
    public void echo(){
        System.out.println("ConcreteProduct1");
    }
}

실제 제품 클래스인 ConcreteProduct 클래스다. Product 인터페이스를 구현한다.

SimpleFactory.java

public class SimpleFactory {
    public Product createProduct(String type){
        switch(type){
            case "product1":
                return new ConcreteProduct1();         
            default:
                return null;
        }
    }
}

ConcreteProduct를 생산하는 SimpleFactory 클래스다.

createProduct()  메소드의 파라미터로 넘어온 타입을 통해 객체를 생성하게 된다.

Demo.java

public class Demo {
    public void run() {
        SimpleFactory factory = new SimpleFactory();
        factory.createProduct("product1").echo();
    }
}

Output

ConcreteProduct1
ConcreteProduct2

 

Demo에서는 Product 객체를 만들기 위해 자기 자신이 new를 하지 않고 이 과정들을 SimpleFactory에게 위임한다.

이렇게 객체간 의존성이 줄어들게 되고, 향후 클래스의 수정이 생기더라도 SimpleFactory 부분만 수정해주면 된다.

 

2. Factory Method

객체의 생성을 서브 클래스(ConcreteFactory)가 담당하며, 부모(AbstractFactory)는 이 생성에 대해 관여하지 않는다.

 

쉽게말해 부모는 자식이 객체를 어떻게 생성할지 모른다. 

팩토리는 추상 클래스로 골격만을 잡으며, 해당 팩토리를 확장한 각 자식 팩토리에서 객체의 생성을 담당한다.

 

Factory Method Class Diagram

클래스 다이어그램에서는 팩토리를 만들기 위한 추상클래스 팩토리가 존재한다.

이 추상클래스(AbstractFactory)는 객체의 생성에 대해 추상메소드로 제공해주게 되고, 이 클래스를 상속받은 실제 구현체(ConcreteFactory)가 실제 객체의 생성을 담당하게 된다.

 

Product.java

public interface Product {
    public void echo();
}

제품의 인터페이스인 Product 인터페이스다. 

ConcreteProduct.java

public class ConcreteProduct1 implements Product {
    public void echo(){
        System.out.println("ConcreteProduct1");
    }
}

실제 제품 클래스인 ConcreteProduct 클래스다. Product 인터페이스를 구현한다.

 

AbstractFactory.java

public abstract class AbstractFactory {
    public abstract Product createProduct(String type);
}

AbstractFactory는 객체 생성에 대한 abstract 메소드만 제공한다.

 

ConcreteFactory.java

public class ConcreteFactory extends AbstractFactory {
    @Override
    public Product createProduct(String type){
        switch(type){
            case "product1":
                return new ConcreteProduct1();
            case "product2":
                return new ConcreteProduct2();
            default:
                return null;
        }
    }
}

위 추상클래스를 상속한 자식 Factory가 객체의 생성을 담당한다.

 

Demo.java

public class Demo {
    public void run(){
        ConcreteFactory factory = new ConcreteFactory();

        factory.createProduct("product1").echo();
        factory.createProduct("product2").echo();
    }
}

 

Output

ConcreteProduct1
ConcreteProduct2

팩토리 메소드의 핵심은 선언부와 구현부를 따로하여 자식 클래스가 실제 구현체의 생성을 결정하는 것이다.

 

3. Abstract Factory

추상 팩토리는 그룹별로 클래스를 생성하기 위해 사용된다

 

추상 팩토리는 같은 특성을 지닌 객체 군(Group)을 생성하는데 특화된 패턴이다. 

 

Abstract Factory Class Diagram

클래스 다이어그램을 보면 두가지 제품(1,2)이 있고, 두개의 팩토리(A,B)가 존재한다.

FactoryA는 제품 A1, A2를 생성하게 되고, FactoryB는 제품 B1, B2를 생성하게 된다.

 

랩탑(Product1)과, 데스크탑(Product2)을 생성해야 할때 각 제조사군(Group) 별로 생성을 하고자 한다.
이럴때 삼성의 랩탑, 삼성의 데스크탑을 담당하는 팩토리(ConcreteFactoryA)를 만들고
LG의 랩탑과 LG의 데스크탑을 담당하는 팩토리(ConcreteFactoryB)를 만들어
삼성공장에서는 삼성랩탑, 데스크탑(ConcreteProductA1, A2)을 만들고
LG공장에서는 LG랩탑, 데스크탑(ConcreteProductB1, B2)를 만든다.

 

Product1.java

public interface Product1 {
    public void echo();
}

Product1에 대한 인터페이스다.

ConcreteProductA1.java

public class ConcreteProductA1 implements Product1 {
    public void echo(){
        System.out.println("ProductA-1");
    }
}

FactoryA에서 생성할 A1 제품의 실체화된 클래스다.

 

AbstractFactory.java

public abstract class AbstractFactory {
    abstract public Product1 createProduct1();
    abstract public Product2 createProduct2();
}

추상클래스인 AbstractFactory는 각 제품1, 제품2 에 대한 생성을 정의한다.

 

ConcreteFactoryA.java

public class ConcreteFactoryA extends AbstractFactory {

    @Override
    public Product1 createProduct1() {
        return new ConcreteProductA1();
    }

    @Override
    public Product2 createProduct2() {
        return new ConcreteProductA2();
    }
}

 추상클래스를 상속한 팩토리A는 제품 A1, A2를 생성한다.

 

Demo.java

public class Demo {
    public void run(){
        ConcreteFactoryA factoryA = new ConcreteFactoryA();
        ConcreteFactoryB factoryB = new ConcreteFactoryB();

        factoryA.createProduct1().echo();
        factoryA.createProduct2().echo();

        factoryB.createProduct1().echo();
        factoryB.createProduct2().echo();
    }
}

 

FactoryB, Product2는 동일하기때문에 굳이 언급하지 않았다.

 

output

ProductA-1
ProductA-2
ProductB-1
ProductB-2

 

이렇듯 추상 팩토리는 팩토리 메소드를 확장하여 같은 특징을 가지는 Group별로 객체를 생성하기 위한 패턴이다.

 

내가 생각하는 팩토리 패턴들은 이렇다.

심플 팩토리는 객체의 생성을 다른 별도의 객체로 위임하는것,

이것을 확장한 팩토리 메소드는 별도의 객체로 위임하면서 구현부와 선언부를 분리하여 자식이 해당 구현을 전담하도록,

이것을 확장한 추상 팩토리는 객체의 다형성을 이용하여 일정한 그룹별로 객체들을 생성하기 위해 사용하는 패턴이라 본다.

 

 

4. Singleton

유일한 객체를 생성하기 위해 사용하는 패턴.

싱글턴 패턴은 객체를 하나만 유지해야 할때 사용하는 패턴이다.

 

예를들어 공유된 자원에 접근하는 경우 (ex DB) 사용된다.

Singleton Class Diagram

클래스 다이어 그램의 경우도 단순하다.

전역변수로 자기 자신을 인스턴스로 가지며, 전역으로 설정된 getInstance를 통해 해당 인스턴스를 리턴해준다.

Singleton.java

public class Singleton {
    private static Singleton instance = null;
    private Singleton() {}

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

 

전역이지만, 외부에서 접근하면 안되는 싱글턴 객체를 private static 으로 선언한다.

객체의 생성을 막기위해 생성자를 private로 생성해주게 된다.

 

해당 싱글턴 객체는 전역 메소드인 getInstance를 통해 객체를 관리한다.

만약 객체가 존재한다면 해당 객체를 리턴, 객체가 존재하지 않는다면 객체를 만들어 리턴해준다.

 

Demo.java

public class Demo {
    public void run(){
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println("s1 : " + s1);
        System.out.println("s2 : " + s2);
    }
}

Output

s1 : Create.Singleton.Singleton@5ca881b5
s2 : Create.Singleton.Singleton@5ca881b5

두개의 싱글턴 객체가 동일한 객체임을 알 수 있다.

 

 

싱글턴은 유일하게 관리되어야 하는 객체를 생성할때 유용한 패턴이며, 자주 사용되는 패턴중 하나이다.

 

하지만 멀티 스레드 환경에서는, 몇가지 문제가 생길 수 있다.

만약 두개의 스레드에서 동시에 싱글턴의 getInstance()를 통해 싱글턴 객체를 생성하고자 한다면,

경합 과정을 거치다 두개의 싱글톤 객체가 생성될 수 있는 문제가 있다.

이러한 환경에서는 getInstance() 메소드에 동기화와 관련된 처리등을 해주어야 한다.

 

5. Builder

빌더는 커다란 객체를 생성하기 위한 복합 객체다.

 

빌더 패턴의 목적은 객체 생성시 많은 요소들이 필요한 커다란 복합 객체를 어떻게 하면 편하게 생성하는가에 있다.

이러한 복합객체의 생성과 표현을 분리하여 하나의 과정을 통해 여러가지 결과가 나올 수 있게 한다.

생성자 오버로딩

 

 

Builder Class Diagram

클래스 다이어그램을 보면 Product 클래스는 Builder를 필요로 하고,

Builder를 상속받은 실제 ConcreteBuilder클래스가 제품을 생성하는것을 알 수 있다.

 

Product.java

public class Product {
    String part1;
    String part2;
    String part3;

    public Product(Builder builder){
        this.part1 = builder.part1;
        this.part2 = builder.part2;
        this.part3 = builder.part3;
    }
    public void echo(){
        System.out.printf("part1 : %s | part2 : %s | part3 : %s\n", this.part1, this.part2, this.part3);
    }
}

해당 예시에서 Product 객체는 3개의 요소를 필요로 하고, 이 요소들을 Builder로부터 받아오게 된다.

Builder.java

public abstract class Builder {
    protected String part1 = "";
    protected String part2 = "";
    protected String part3 = "";

    public abstract Builder setPart1(String part1);
    public abstract Builder setPart2(String part2);
    public abstract Builder setPart3(String part3);

    public abstract Product getProduct();
}

 

ConcreteBuilder.java

public class ConcreteBuilder extends Builder{
    public Builder setPart1(String part1){
        this.part1 = part1;
        return this;
    }
    public Builder setPart2(String part2){
        this.part2 = part2;
        return this;
    }
    public Builder setPart3(String part3){
        this.part3 = part3;
        return this;
    }

    public Product getProduct(){
        return new Product(this);
    }
}

Builder는 Product에서 필요한 요소들을 설정하게 되고, 해당 요소들을 통해 Product를 생성하게 된다.

 

Demo.java

public class Demo {
    public void run() {
        Builder builder1 = new ConcreteBuilder();
        builder1.setPart1("123").setPart2("234").setPart3("345");

        Builder builder2 = new ConcreteBuilder();
        builder2.setPart1("1");

        builder1.getProduct().echo();
        builder2.getProduct().echo();
    }
}

Output

part1 : 123 | part2 : 234 | part3 : 345
part1 : 1 | part2 :  | part3 :

위 예시에서처럼 빌더 패턴은 제품의 생성자로 Builder를 넘겨줌으로서, 해당 Builder가 제품을 생성하게 한다.

 

또한 다양한 Builder를 가지고 하나의 생성자를 통해 다양한 객체들을 생성할 수 있게 된다.

 

6. Prototype

하나의 프로토타입을 통해 다른 여러개의 객체를 생성하고자 한다.

 

프로토타입 패턴은 하나의 간단한 객체를 만들어 해당 객체를 바탕으로 여러 객체를 생성하는 패턴이다.

이 패턴은 객체의 깊은복사인 clone을 사용하여 객체를 복사한다.

 

clone의 경우 기존 객체의 모든 정보를 가지고 있는 상태로 복사가 진행되므로, 객체의 초기화가 진행되지 않는다. 

Prototype Class Diagram

Cloneable라는 인터페이스를 구현함으로써, Prototype 객체가 clone을 사용할 수 있게 된다.

 

Prototype.java

package Create.Prototype;

public class Prototype implements Cloneable{
    String part1;
    String part2;

    public Prototype(String part1, String part2){
        this.part1 = part1;
        this.part2 = part2;
    }

    public void setPart1(String part1){
        this.part1 = part1;
    }

    public void echo(){
        System.out.printf("%s >> part1 : %s | part2 : %s\n", this.toString(), this.part1, this.part2);
    }

    @Override
    protected Object clone() throws CloneNotSupportedException{
        return super.clone();
    }
}

맨 아래 clone메소드를 오버라이드 해주게 된다.

 

Demo.java

public class Demo {
    public void run(){
        Prototype p1 = new Prototype("1", "2");
        try{
            Prototype p2 = (Prototype) p1.clone();
            p2.setPart1("99");

            p1.echo();
            p2.echo();
        } catch(Exception e){
            System.err.println(e);
        }
    }
}

Output

Create.Prototype.Prototype@7a81197d >> part1 : 1 | part2 : 2
Create.Prototype.Prototype@24d46ca6 >> part1 : 99 | part2 : 2

깊은 복사가 일어났으므로, 기존 p1의 값을 가지고 있는 새로운 객체 p2가 생성이 된다.