[디자인패턴] 생성 패턴 - 프로토타입(Prototype) 패턴

 

 

 

*프로토타입 패턴

- 기존 인스턴스를 복제하여 새로운 인스턴스를 만드는 방법

  • 복제 기능을 갖추고 있는 기존 인스턴스를 프로토타입으로 사용해 새 인스턴스를 만들 수 있다.

 

 

 

 

- Prototype, ConcretePrototypeA, ConcretePrototypeB

package me.whiteship.designpatterns._01_creational_patterns._05_prototype.practice;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class DatabaseInfo {
    private String db;
    private String host;
    private int port;
}
package me.whiteship.designpatterns._01_creational_patterns._05_prototype.practice;

import lombok.Getter;
import lombok.Setter;

import java.util.Objects;

@Getter @Setter
public class JdbcConnectionInfo implements Cloneable{
    private int id;
    private String title;
    private DatabaseInfo databaseInfo;

    public JdbcConnectionInfo(DatabaseInfo databaseInfo) {
        this.databaseInfo = databaseInfo;
    }

    public String getConnection() {
        return String.format("jdbc:%s://%s/%d",
                this.databaseInfo.getDb(),
                this.databaseInfo.getHost(),
                this.databaseInfo.getPort());
    }

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

Prototype 인터페이스는 java에서 제공하는 Object 클래스에 있는 clone 메소드를 사용한다. 그러기 위해선 Cloneable 인터페이스를 구현해야 하는데 Clonable 인터페이스를 구현하면 clone 메소드를 오버라이딩 해서 사용할 수 있다. 기본 오버라이딩을 하게되면 부모 클래스에 있는 clone을 가져온다.

 

 

- Client

package me.whiteship.designpatterns._01_creational_patterns._05_prototype.practice;

public class App {
    public static void main(String[] args) throws CloneNotSupportedException {
        DatabaseInfo databaseInfo = new DatabaseInfo();
        databaseInfo.setDb("mysql");
        databaseInfo.setHost("localhost");
        databaseInfo.setPort(3306);

        JdbcConnectionInfo jdbcConnection = new JdbcConnectionInfo(databaseInfo);
        jdbcConnection.setId(1);
        jdbcConnection.setTitle("first connection");

        String connection = jdbcConnection.getConnection();
        System.out.println(connection); // jdbc:mysql://localhost/3306

        JdbcConnectionInfo clone = (JdbcConnectionInfo) jdbcConnection.clone();
        
        databaseInfo.setDb("oracle");
        
        System.out.println(clone != jdbcConnection); // true
        System.out.println(clone.equals(jdbcConnection)); // false
        System.out.println(clone.getClass() == jdbcConnection.getClass()); // true
        System.out.println(clone.getDatabaseInfo() == jdbcConnection.getDatabaseInfo()); // true

        System.out.println(jdbcConnection.getConnection()); 
        // jdbc:oracle://localhost/3306
        System.out.println(clone.getConnection()); 
        // jdbc:oracle://localhost/3306
    }
}

Object의 clone 메소드를 사용하면 복제하려고 하는 인스턴스의 정보를 가져올 수 있으며, 이 방법은 싱글톤이 아니다. 객체는 다른 객체의 레퍼런스를 참조하지만 값은 그대로 복제 할 수 있다.

여기서, clone 메소드는 기본적으로 깊은 복사(deep copy)가 아닌 얕은 복사(shallow copy)를 한다. 따라서 JdbcConnectionInfo(ConcretePrototypeA)에서 사용하고 있는 DataBaseInfo(ConcretePrototypeB)는 기존의 인스턴스와 clone 인스턴스가 같은 레퍼런스를 참조한다.

 

 

- ConcreatePrototypeA(equals, hashCode 재정의)

package me.whiteship.designpatterns._01_creational_patterns._05_prototype.practice;

import lombok.Getter;
import lombok.Setter;

import java.util.Objects;

@Getter @Setter
public class JdbcConnectionInfo implements Cloneable{
    ...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        JdbcConnectionInfo that = (JdbcConnectionInfo) o;
        return id == that.id && Objects.equals(title, that.title) && Objects.equals(databaseInfo, that.databaseInfo);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, title, databaseInfo);
    }

	...
}

먼저 equals 메소드와 같이 객체의 주소 값이 아닌 객체 안에 있는 값들의 비교를 원한다면 equals 메소드를 재정의 할 필요가 있다. 이 때 hashCode 메소드도 같이 변경한다.

 

 

- Client(equals, hashCode 재정의)

package me.whiteship.designpatterns._01_creational_patterns._05_prototype.practice;

public class App {
    public static void main(String[] args) throws CloneNotSupportedException {
        ...
        
        System.out.println(clone != jdbcConnection); // true
        System.out.println(clone.equals(jdbcConnection)); // true
        System.out.println(clone.getClass() == jdbcConnection.getClass()); // true
        System.out.println(clone.getDatabaseInfo() == jdbcConnection.getDatabaseInfo()); // true

        ...
    }
}

clone과 jdbcConnection의 equals 비교값이 바뀌었다.

 

 

- ConcretePrototypeA(clone 메소드 수정)

package me.whiteship.designpatterns._01_creational_patterns._05_prototype.practice;

import lombok.Getter;
import lombok.Setter;

import java.util.Objects;

@Getter @Setter
public class JdbcConnectionInfo implements Cloneable{
    ...

    @Override
    protected Object clone() throws CloneNotSupportedException {
        DatabaseInfo databaseInfo = new DatabaseInfo();
        databaseInfo.setDb(this.databaseInfo.getDb());
        databaseInfo.setHost(this.databaseInfo.getHost());
        databaseInfo.setPort(this.databaseInfo.getPort());

        JdbcConnectionInfo jdbcConnection = new JdbcConnectionInfo(databaseInfo);
        jdbcConnection.setId(this.getId());
        jdbcConnection.setTitle(this.getTitle());
        
        return jdbcConnection;
    }

    ...
}

얕은 복사를 깊은 복사로 만드는 방법은 clone 메소드를 재정의 하는 방법을 사용할 수 있다. ConcretePrototype에서 clone을 통해 복사할 데이터의 범위를 직접 명시하면된다.

 

 

- Client(clone 메소드 수정)

package me.whiteship.designpatterns._01_creational_patterns._05_prototype.practice;

public class App {
    public static void main(String[] args) throws CloneNotSupportedException {
        DatabaseInfo databaseInfo = new DatabaseInfo();
        databaseInfo.setDb("mysql");
        databaseInfo.setHost("localhost");
        databaseInfo.setPort(3306);

        JdbcConnectionInfo jdbcConnection = new JdbcConnectionInfo(databaseInfo);
        jdbcConnection.setId(1);
        jdbcConnection.setTitle("first connection");

        String connection = jdbcConnection.getConnection();
        System.out.println(connection); // jdbc:mysql://localhost/3306

        JdbcConnectionInfo clone = (JdbcConnectionInfo) jdbcConnection.clone();
        
        databaseInfo.setDb("oracle");
        
        System.out.println(clone != jdbcConnection); // true
        System.out.println(clone.equals(jdbcConnection)); // false
        System.out.println(clone.getClass() == jdbcConnection.getClass()); // true
        System.out.println(clone.getDatabaseInfo() == jdbcConnection.getDatabaseInfo()); // false

        System.out.println(jdbcConnection.getConnection()); 
        // jdbc:oracle://localhost/3306
        System.out.println(clone.getConnection()); 
        // jdbc:mysql://localhost/3306
    }
}

얕은 복사에서 깊은 복사로 변경 후에 ConcretePrototypeA(JdbcConnectionInfo)가 참조하는 ConcreatePrototypeB(DatabaseInfo)의 정보가 변경되었을 때 서로 다른 레퍼런스를 참조하기 때문에 equals 메소드나 참조하고 있는 databaseInfo의 값이 다르다는 것을 알수있다. 또한 서로 값 변경에 대해 영향을 주지 않는다.

 

 

- 장점

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

- 단점

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

 

 

 

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

 

[디자인패턴] 생성 패턴 - 빌더(Builder) 패턴

 

 

 

*빌더 패턴

- 동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법.

  • (복잡한) 객체를 만드는 프로세스를 독립적으로 분리할 수 있다.

 

 

 

 

- Product

package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

import lombok.Getter;
import lombok.Setter;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

@Getter @Setter
public class Photograph {
    private String title;
    private String place;
    private LocalDate date;
    private String dress;
    private List<Suit> suits;

    public Photograph() {
        if (this.suits == null) {
            this.suits = new ArrayList<Suit>();
        }
    }

    public Photograph(String title, String place, LocalDate date, String dress, List<Suit> suits) {
        this.title = title;
        this.place = place;
        this.date = date;
        this.dress = dress;
        this.suits = suits;
    }

    public void addSuit(String color, String shape) {
        this.suits.add(new Suit(color, shape));
    }

    @Override
    public String toString() {
        return "Photograph{" +
                "title='" + title + '\'' +
                ", place='" + place + '\'' +
                ", date=" + date +
                ", dress='" + dress + '\'' +
                ", suits=" + suits +
                '}';
    }
}
package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class Suit {
    private String color;
    private String shape;

    public Suit(String color, String shape) {
        this.color = color;
        this.shape = shape;
    }

    @Override
    public String toString() {
        return "Suit{" +
                "color='" + color + '\'' +
                ", shape='" + shape + '\'' +
                '}';
    }
}

추후 빌더 패턴을 통해서 만들어낼 인스턴스의 Product(데이터)이다.

 

 

- Client

package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

import java.time.LocalDate;

public class App {
    public static void main(String[] args) {
        Photograph firstPhotograph = new Photograph();
        firstPhotograph.setTitle("wedding photos");
        firstPhotograph.setDate(LocalDate.of(2022, 8, 20));
        firstPhotograph.setPlace("the studio");
        firstPhotograph.setDress("White flowing dress");
        firstPhotograph.addSuit("beige", "basic");
        firstPhotograph.addSuit("navy", "stripe");
        System.out.println("firstPhotograph = " + firstPhotograph);

        Photograph secondPhotograph = new Photograph();
        secondPhotograph.setTitle("wedding photos");
        secondPhotograph.setDate(LocalDate.of(2022, 8, 20));
        secondPhotograph.setPlace("the outside");
        secondPhotograph.setDress("Pink flowing dress");
        secondPhotograph.addSuit("brown", "basic");
        System.out.println("secondPhotograph = " + secondPhotograph);

        Photograph thirdPhotograph = new Photograph();
        thirdPhotograph.setTitle("wedding photos");
        thirdPhotograph.setDate(LocalDate.of(2022, 8, 20));
        thirdPhotograph.setPlace("the outside");
        thirdPhotograph.setDress("White slim dress");
        thirdPhotograph.addSuit("charcoal", "basic");
        System.out.println("thirdPhotograph = " + thirdPhotograph);
    }
}

빌더 패턴을 사용하지 않고 인스턴스를 만들게 되면 위와 같이 setter나 생성자를 통해서 인스턴스를 만들 수 있지만 입력해야 하는 값이 많아질수록 setter와 생성자는 입력해야 하는 값이 많아지고 또한 생성자는 값을 입력하지 않으려면 null 값이나 기본값을 입력해야 한다.

 

 

- Builder

package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

import java.time.LocalDate;

public interface PhotographBuilder {
    PhotographBuilder title(String title);

    PhotographBuilder place(String place);

    PhotographBuilder startDate(LocalDate date);

    PhotographBuilder choiceDress(String dress);

    PhotographBuilder addSuit(String color, String shape);

    Photograph getPlan();
}

Builder 인터페이스를 만들고 필요한 메소드를 만든다. 각 메소드들은 Builder 인터페이스를 타입으로 갖게되어 Builder 인터페이스를 구현하는 구현체로 반환된다. 따라서 메소드 체인처럼 사용하고자 하는 메소드를 사용할 수 있다. 마지막에 Product를 반환하는 메소드를 만든다.

 

 

- ConcreteBuilder

package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class DefaultPhotographBuilder implements PhotographBuilder{

    private String title;
    private String place;
    private LocalDate date;
    private String dress;
    private List<Suit> suits;

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

    @Override
    public PhotographBuilder place(String place) {
        this.place = place;
        return this;
    }

    @Override
    public PhotographBuilder startDate(LocalDate date) {
        this.date = date;
        return this;
    }

    @Override
    public PhotographBuilder choiceDress(String dress) {
        this.dress = dress;
        return this;
    }

    @Override
    public PhotographBuilder addSuit(String color, String shape) {
        if (this.suits == null) {
            this.suits = new ArrayList<>();
        }
        this.suits.add(new Suit(color, shape));
        return this;
    }

    @Override
    public Photograph getPlan() {
        return new Photograph(this.title, this.place, this.date, this.dress, this.suits);
    }

    @Override
    public String toString() {
        return "DefaultPhotographBuilder{" +
                "title='" + title + '\'' +
                ", place='" + place + '\'' +
                ", date=" + date +
                ", dress='" + dress + '\'' +
                ", suits=" + suits +
                '}';
    }
}

추상화된 빌더 인터페이스를 구현하는 클래스이다. 각 메소드마다 Builder 인터페이스를 구현하는 구현체를 반환 해야하기 때문에 this를 리턴하고 마지막에 모든 값들을 반환하는 Product를 리턴한다.

 

 

- Client

package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

import java.time.LocalDate;

public class BuilderApp {
    public static void main(String[] args) {
        PhotographBuilder photographBuilder = new DefaultPhotographBuilder();
        Photograph firstPhotograph = photographBuilder
                .title("wedding photos")
                .startDate(LocalDate.of(2022, 8, 20))
                .place("the studio")
                .choiceDress("White flowing dress")
                .addSuit("beige", "basic")
                .addSuit("navy", "stripe")
                .getPlan();
        System.out.println("firstPhotograph = " + firstPhotograph);

        Photograph secondPhotograph = photographBuilder
                .title("wedding photos")
                .startDate(LocalDate.of(2022, 8, 20))
                .place("the outside")
                .choiceDress("Pink flowing dress")
                .addSuit("brown", "basic")
                .getPlan();
        System.out.println("secondPhotograph = " + secondPhotograph);

        Photograph thirdPhotograph = photographBuilder
                .title("wedding photos")
                .startDate(LocalDate.of(2022, 8, 20))
                .place("the outside")
                .choiceDress("White slim dress")
                .addSuit("charcoal", "basic")
                .getPlan();
        System.out.println("thirdPhotograph = " + thirdPhotograph);

    }
}

빌더 패턴을 적용하고 인스턴스를 적용했다. 기존에 setter나 생성자를 사용하는 방식은 장황하게 만들어지는 데이터를 입력하는 부분에 수 많은 null값이나 기본 값을 넣었어야 한다. 빌더 패턴은 사용하고자 하는 부분만 입력하고 반환하면 된다.

 

 

- Director

package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

import java.time.LocalDate;

public class PhotographDirector {

    private PhotographBuilder photographBuilder;

    public PhotographDirector(PhotographBuilder photographBuilder) {
        this.photographBuilder = photographBuilder;
    }

    public Photograph firstPhotograph() {
        return this.photographBuilder
                .title("wedding photos")
                .startDate(LocalDate.of(2022, 8, 20))
                .place("the studio")
                .choiceDress("White flowing dress")
                .addSuit("beige", "basic")
                .addSuit("navy", "stripe")
                .getPlan();
    }

    public Photograph secondPhotograph() {
        return this.photographBuilder
                .title("wedding photos")
                .startDate(LocalDate.of(2022, 8, 20))
                .place("the outside")
                .choiceDress("Pink flowing dress")
                .addSuit("brown", "basic")
                .getPlan();
    }

    public Photograph thirdPhotograph() {
        return this.photographBuilder
                .title("wedding photos")
                .startDate(LocalDate.of(2022, 8, 20))
                .place("the outside")
                .choiceDress("White slim dress")
                .addSuit("charcoal", "basic")
                .getPlan();
    }
}

또한 빌더 패턴에서 만들어지는 프로세스 등이 자주 반복이 된다면 미리 만들어져있는 부분을 Director에 넣어놓고 재사용할 수 있다.

 

 

- Client

package me.whiteship.designpatterns._01_creational_patterns._04_builder.practice;

public class DirectorApp {
    public static void main(String[] args) {
        PhotographDirector photographDirector = new PhotographDirector(new DefaultPhotographBuilder());
        Photograph FirstPhotograph = photographDirector.firstPhotograph();
        System.out.println("FirstPhotograph = " + FirstPhotograph);
        Photograph secondPhotograph = photographDirector.secondPhotograph();
        System.out.println("secondPhotograph = " + secondPhotograph);
        Photograph thirdPhotograph = photographDirector.thirdPhotograph();
        System.out.println("thirdPhotograph = " + thirdPhotograph);
    }
}

Client는 Builder가 아닌 Director를 통해서 자주 사용되거나 반복되는 Photograph에 관한 인스턴스를 구현 할 수 있다.

 

 

 

- 장점

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

- 단점

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

 

빌더 패턴은 만드는 순서가 복잡한 인스턴스 같은 경우는 빌더 패턴의 메소드를 사용해야 하는 순서를 통해서만 만들어지도록 사용하는 방식을 강제할 수도 있다.

생성자 하나만으로 사용하게 되어 복잡해질 수 있는 부분을 분산할 수도 있다. 예를 들어, 입력 받은 값들을 검증하는 코드 등을 추가하는 등의 작업을 할 수 있다.

클라이언트 코드가 깔끔하게 만들 수 있고 같은 프로세스에 하위클래스 등을 만들어서 추가적이고 세부적인 것들이 추가 될 수 있다. (VIP용 등)

getPlan()과 같은 빌더패턴을 종결하는 메소드를 호출하기 전까진 메소드를 사용할 수 없기 때문에 에러를 방지할 수 있다.

 

 

 

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

 

[디자인패턴] 생성 패턴 - 추상 팩토리(Abstract Factory) 패턴

 

 

 

*추상 팩토리 패턴

- 서로 관련있는 여러 객체를 만들어주는 인터페이스

  • 구체적으로 어떤 클래스의 인스턴스를(concrete product)를 사용하는지 감출 수 있다. (클라이언트에서 사용하는 인스턴스를 만들어 쓰는 코드를 인터페이스로 추상화한다)

- 구체적인 팩토리에서 구체적인 인스턴스를 만드는 부분은 팩토리 메소드 패턴과 비슷하지만 초점은 팩토리를 사용하는 클라이언트에 맞춰져 있다. 

 

 

 

 

- Product(Factory Method)

package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

import lombok.Getter;
import lombok.Setter;
import me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.Oxidizer;
import me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.Tonic;

@Getter @Setter
public class HairDye {
    ...
    
    private DarkOxidizer oxidizer;
    private DarkTonic tonic;

    ...
}

기존의 구현체에 필요한 데이터 필드를 추가한다.

 

 

- ConcreateProductA, ConcreateProductB

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkOxidizer{
}
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkTonic{
}

추가된 구현체에 대한 클래스를 만든다.

 

 

- ConcreteCreator(Factory Method)

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.DarkHairDye;
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.DefaultHairDesigner;
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.HairDye;

public class DarkHairDesigner extends DefaultHairDesigner {
    @Override
    public HairDye dyingService() {
        HairDye hairDye = new DarkHairDye();
        hairDye.setOxidizer(new DarkOxidizer());
        hairDye.setTonic(new DarkTonic());
        return hairDye;
    }
}

팩토리(creator)에 구현체에 추가된 Oxidizer나 Tonic를 구현하기 위해서 현재는 new 키워드를 통해 직접적으로 구현하는데, 추상 팩토리 패턴으로 클라이언트 측에서 사용하는 구현체에 따라 인스턴스가 정해지도록 한다.

 

 

- Abstract Factory

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public interface HairDyeProducts {

    Oxidizer useOxidizer();

    Tonic useTonic();
}

추상 팩토리 패턴으로 만들 인터페이스를 정의한다. 이 부분엔 추가되는 구현체(Product)를 정의한다.

 

 

- ProductA, ProductB

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public interface Oxidizer {
}
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public interface Tonic {
}

추상 팩토리에서 사용될 구현체(Product)의 인터페이스를 정의한다.

 

 

- ConcreteProductA, ConcreteProductB

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkOxidizer implements Oxidizer{
}
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkTonic implements Tonic{
}

인터페이스로 정의된 구현체를 구체적인 클래스를 정의한다.

 

 

- ConcreteFactory

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkHairDyeProducts implements HairDyeProducts{
    @Override
    public Oxidizer useOxidizer() {
        return new DarkOxidizer();
    }

    @Override
    public Tonic useTonic() {
        return new DarkTonic();
    }
}

추상 팩토리를 구현할 구현 클래스를 정의한다.

 

 

- ConcreteCreator(Factory Method)

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.DarkHairDye;
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.DefaultHairDesigner;
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.HairDye;

public class DarkHairDesigner extends DefaultHairDesigner {

    private HairDyeProducts hairDyeProducts;

    public DarkHairDesigner(HairDyeProducts hairDyeProducts) {
        this.hairDyeProducts = hairDyeProducts;
    }

    @Override
    public HairDye dyingService() {
        HairDye hairDye = new DarkHairDye();
        hairDye.setOxidizer(hairDyeProducts.useOxidizer());
        hairDye.setTonic(hairDyeProducts.useTonic());
        return hairDye;
    }
}

기존 DarkHariDesigner에서 Oxidizer나 Tonic을 정의 하려면 new 인스턴스를 사용하기 때문에 의존성이 강하게 결합되어 있었지만, HairDyeProducts 인터페이스를 통해서 의존성의 결합을 느슨하게 변경했다.

 

 

추가적으로 형태는 같지만 다른 기능을 하는 클래스를 추가하는 경우,

 

- ConcreteProductC, - ConcreteProductD

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkExOxidizer implements Oxidizer{
}
package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkExTonic implements Tonic{
}

다른 구현체(Product)를 추가한다.

 

 

- ConcreteFactory

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

public class DarkHairDyeExProducts implements HairDyeProducts{
    @Override
    public Oxidizer useOxidizer() {
        return new DarkExOxidizer();
    }

    @Override
    public Tonic useTonic() {
        return new DarkExTonic();
    }
}

추상 팩토리를 구현하는 또 다른 구현체를 정의한다.

 

 

- Product(Factory Method)

package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

import lombok.Getter;
import lombok.Setter;
import me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.Oxidizer;
import me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.Tonic;

@Getter @Setter
public class HairDye {
    ...
    
    private Oxidizer oxidizer;
    private Tonic tonic;

    ...
}

기존에 DarkOxidizer, DarkTonic 등의 클래스 타입으로 되어있는 부분을 느슨한 결합을 위해 인터페이스 타입으로 수정한다.

 

 

- Client

package me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice;

import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.HairDesigner;
import me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice.HairDye;

public class Client {
    public static void main(String[] args) {
        HairDesigner hairDesigner = new DarkHairDesigner(new DarkHairDyeProducts());
        HairDesigner hairDesigner = new DarkHairDesigner(new DarkHairDyeExProducts());

        HairDye hairDye = hairDesigner.dyingService();
        System.out.println(hairDye.getOxidizer().getClass());
        System.out.println(hairDye.getTonic().getClass());
        
//      class me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.DarkOxidizer
//      class me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.DarkTonic

//      class me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.DarkExOxidizer
//      class me.whiteship.designpatterns._01_creational_patterns._03_abstract_factory.practice.DarkExTonic        
    }
}

클라이언트에서 구조는 같지만 비슷한 형태의 인스턴스를 변경하기만 하면 다른 결과 값을 얻을 수 있다.

 

 

 

 

팩토리 메소드 패턴과 추상 팩토리 패턴은 비슷하다. 이 두가지는 관점의 차이이다. 팩토리 메소드 패턴은 객체 또는 인스턴스를 만드는(구현하는) 과정에 관점이 집중이 되어있고, 추상 팩토리 패턴은 팩토리를 사용하는 쪽(클라이언트)의 관점에 집중하여 보고있다.

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

 

 

 

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

[디자인패턴] 생성 패턴 - 팩토리 메소드(Factory method) 패턴

 

 

 

*팩토리 메소드 패턴

- 구체적으로 어떤 인스턴스를 만들지 서브 클래스에서 정하게 한다.

  • 다양한 구현체(Product)가 있고, 그 중에서 특정한 구현체를 만들 수 있는 다양한 팩토리(Creator)를 제공할 수 있다.

- 확장엔 열려있고 변경엔 닫혀있는 OCP(Open Closed Principle)에 부합한다. 

 

 

 

 

 

예를 들어

 

HairDesigner(Creator)와 HairDye(Product)로 팩토리 메소드 패턴을 구현한다면

 

 

- Product

package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class HairDye {
    private String colorNameKr;
    private String colorNameEn;
    private String colorCode;

    @Override
    public String toString() {
        return "사용된 염색약은 " +
                colorNameKr +
                "(" + colorNameEn + "), " +
                "code: " + colorCode +
                " 입니다";
    }
}

팩토리 메소드 패턴에서 Product, 팩토리에서 만들어 낼 데이터 오브젝트이다.

 

 

- concreteProductor

package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

public class DarkHairDye extends HairDye{
    public DarkHairDye() {
        setColorCode("2N");
        setColorNameEn("Dark");
        setColorNameKr("흑색");
    }
}
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

public class BrownHairDye extends HairDye{
    public BrownHairDye() {
        setColorCode("5N");
        setColorNameEn("Brown");
        setColorNameKr("자연갈색");
    }
}

Product의 규격은 같으면서 다른 값들의 오브젝트를 구체화한다.

 

 

- Creator

package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

public interface HairDesigner {
    default HairDye dying(String color, int time) {
        validate(color);
        selectedDesigner();
        HairDye hairDye = dyingService();
        spreadHairDye(color);
        waitFor(time);
        return hairDye;
    }

    void selectedDesigner();

    HairDye dyingService();

    private void validate(String color) {
        if (color == null || color.isBlank()) {
            throw new IllegalArgumentException("색상이 선택되지 않았습니다");
        }
    }

    private void spreadHairDye(String color) {
        System.out.println(color + " 염색약을 바릅니다");
    }

    private void waitFor(int time) {
        System.out.println(time + "분간 기다립니다");
    }
}
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

public abstract class DefaultHairDesigner implements HairDesigner {
    public void selectedDesigner() {
        System.out.println("일반 디자이너 선생님이 작업합니다.");
    }
}

Product를 사용할 팩토리(Creator)의 기본적인 규격을 정해놓은 추상화된 객체이다. DefaultHairDesigner 같은 추상 클래스의 경우는 사용해도 되고 사용하지 않아도 된다. 추가적으로 디자이너를 선택할 수 있는 기능이 추가되면 파라미터로 변경할 수 있게 만들어도 된다. java 9 버전부터 interface에서 private 제한자를 사용할 수 있지만, 하위 버전에선 사용할 수 없다. 

 

 

- ConcreteCreator

package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

public class DarkHairDesigner extends DefaultHairDesigner {
    @Override
    public HairDye dyingService() {
        return new DarkHairDye();
    }
    
}
package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

public class BrownHairDesigner extends DefaultHairDesigner {
    @Override
    public HairDye dyingService() {
        return new BrownHairDye();
    }
}

팩토리(Creator)에서 추상화된 구현체를 구체화 한다. 각각의 클래스가 정해진 기능만을 수행할 수 있도록 한다.

 

 

- Client

package me.whiteship.designpatterns._01_creational_patterns._02_factory_method.practice;

public class Client {
    public static void main(String[] args) {
        Client client = new Client();
        client.print(new DarkHairDesigner(), "검정색", 40);
        client.print(new BrownHairDesigner(), "갈색", 30);
    }
//        일반 디자이너 선생님이 작업합니다.
//        검정색 염색약을 바릅니다
//        40분간 기다립니다
//        사용된 염색약은 흑색(Dark), code: 2N 입니다

//        일반 디자이너 선생님이 작업합니다.
//        갈색 염색약을 바릅니다
//        30분간 기다립니다
//        사용된 염색약은 자연갈색(Brown), code: 5N 입니다

    private void print(HairDesigner hairDesigner, String color, int time) {
        System.out.println(hairDesigner.dying(color, time));
    }
}

사용자(Client)가 팩토리 메소드 패턴을 통해서 동일한 규격의 다른 데이터들을 구현할 수 있다.

 

 

 

 

팩토리 메소드 패턴은 팩토리(Creator)가 특정한 구현체(Product)를 다양한 방법으로 구현할 수 있다. 또한 기존의 팩토리(DarkHairDesigner, BrownHairDesigner) 뿐만 아니라 추가적인 팩토리(ex. YellowHairDesigner)를 만들 수 있으며(Open) 추가 하더라도 기존의 코드를 변경 또는 수정하지 않을 수 있다.(Closed)

하지만 단점으로, 기능을 계속 추가하게 되면 각자의 역할이 세분화 되어있기 때문에 클래스가 늘어나는 부분이 생긴다.

 

 

 

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

[디자인패턴] 생성 패턴 - 싱글톤(Singleton) 패턴

 

 

*싱글톤 패턴

- 인스턴스를 오직 한개만 제공하는 클래스

  • 인스턴스가 여러개 있을때 문제가 발생할 수 있는 경우를 대비해 한개의 인스턴스만 제공하는 클래스가 필요하다.

 

 

- 기본적인 싱글톤패턴

public class Singleton {
  private static Singleton instance;
  
  private Singleton() {}
  
  public static Singleton getInstance() {
    if(instance == null){
      instance = new Singleton();
    }
    
    return instance;
  }
}
public class App {
 public static void main(String[] arg){
   Singleton singleton = Singleton.getInstance;
   System.out.println(singleton == Singleton.getInstance); // true
 }
}

위와 같은 경우는 하나의 싱글톤 인스턴스를 반환하지만 문제가 있다. 웹 애플리케이션 같은 멀티스레드 환경에선 이 코드가 안전하지 않다.(Thread-Safe 하지 않다)

 

멀티스레드 환경에서 안전하려면 다음과 같은 방법으로 구현할 수 있다.

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

synchronized 키워드를 사용하게되면 여러 스레드에서 getInstance 메소드에 동시에 접근할 때, Thread-Safe하게 접근할 수 있다. 멀티 스레드 환경에서 synchroinzed 키워드를 만났을 때, 어떤 스레드가 해당 부분에 진입한 상태라면 다른 스레드는 그 스레드의 작업을 끝날 때까지 대기한다. 하지만 synchronized는 내부적으로 동기화라는 매커니즘으로 locking 작업을 하기때문에 성능적은 불이익이 생길 수 있다.

 

 

 

그래서 성능을 조금 더 신경쓰고 싶다면 다른 방법을 사용할 수도 있다.

public class Singleton {
  private static final Singleton INSTANCE = new Singleton();
  
  private Singleton() {}
  
  public static Singleton getInstance() { 
    return INSTANCE;
  }
}

synchronized를 사용하지 않고 인스턴스를 먼저 생성한다. 이른 초기화라고 하는데 생성자를 먼저 생성한 후에 getInstance 메서드는 생성된 인스턴스만을 반환한다. Thread-Safe하며 synchronized 동기화에 관한 성능이슈도 없지만 미리 만드는것 자체가 이슈가 될 수 있다.

생성하는 인스턴스 자체로 메모리에 할당을 하게 되는데 만약 미리 만들어졌지만 애플리케이션 내에서 사용하지 않는다면 ? 이것 또한 자원의 낭비가 될 것이다.

 

 

그래서 또 다른 방법이 있다.

public class Singleton {
  private static volatile Singleton instance; // volatile java 1.5 버전 이상부터 사용
  
  private Singleton() {}
  
  public static Singleton getInstance() {
    if(instance == null) {
      synchronized (Singleton.class) {
        if(instance == null) {
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}

synchronized 키워드를 사용하긴 하지만 메서드 레벨에서 사용하는 것이 아닌, 메서드 안의 로직에서 사용한다. double checked locking 이라는 기법으로 if 문을 통해서 2번의 체크하는 로직을 가진다.

여러 스레드가 동시에 getInstance 메서드에 진입하더라도 if문에서 한번 필터링을 하고(동기화 매커니즘까지 가지 않음) synchronized 블럭 안에서 한번 더 if로 필더링을 하기 때문에, 메서드에서 synchronized를 사용한 경우처럼 메서드를 호출할 때마다 동기화 locking 작업을 하지 않는다.

하지만, 이 방법은 복잡하다. 필드에 선언된 volatile을 사용한 이유는 자바 1.5버전 이하(자바 1.4부터)의 메모리처리 방법을 이해 해야한다.

 

 

따라서 또 다른 방법이 있다.

public class Singleton {
  private Singleton() {}
  
  private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
  }
  
  public static Singleton getInstance() {
    return SingletonHolder.INSTANCE;
  }
}

이 방법은 권장하는 방법중 하나이다. 싱글톤을 static inner 클래스를 만들어서 구현하는 방법이다.

이 방법은

1) Thread-safe하며

2) double check locking처럼 복잡하지도 않고

3) getInstance를 호출할 때 클래스 로딩을 통해서 인스턴스를 생성하기 때문에 lazy loading도 가능하다.

 

 


 

 

이러한 방법들은 싱글톤을 보장하며 멀티스레드 환경에서 적절한지, 성능상으로도 괜찮은지, 버전에 따른 복잡한 절차가 들어 있는지에 대한 문제에 대해 자유로울 수 있다.

하지만, 이러한 싱글톤의 구현을 깨뜨리는 방법 또한 존재한다. 사용자가 정해진 방법이 아닌 다른 방법으로 사용한다면 이러한 싱글톤을 보장하지 않을수도 있다.

  • 리플렉션
  • 직렬화와 역직렬화

이 두가지의 방법을 이용하면 위의 싱글톤을 깨뜨린다.

 

 

- 리플렉션

public class App {
  public static void main(String[] args) {
    Singleton singleton = Singleton.getInstance();
    
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessable(true); // private 접근
    Singleton singleton1 = constructor.newInstance();
    
    System.out.println(singleton == singleton1); // false
  }
}

우리가 의도한대로 만든 싱글톤과 리플렉션을 사용하여 클래스에 접근하여 만들어낸 생성자는 다른 인스턴스를 만들어낸다.

 

 

- 직렬화 & 역직렬화

public class Singleton implements Serializable {
  private Singleton() {}
  
  private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
  }
  
  public static Singleton getInstance() {
    return SingletonHolder.INSTANCE;
  }
}
public class App {
  public static void main(String[] args) throws IOException {
    Singleton singleton = Singleton.getInstance();
    Singleton singleton1 = null;
    
    try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("singleton.obj"))) {
      output.writeObject(singleton); // 직렬화
    }
    
    try (ObjectInput in = new ObjectInputStream(new FileInputStream("singleton.obj"))) {
      singleton1 = (Singleton) in.readObject(); // 역직렬화
    }
    
    System.out.println(singleton == singleton1); // false
  }
}

리플렉션과 마찬가지로 직렬화 & 역직렬화를 통해 만든 객체 인스턴스는 싱글톤으로 만들어낸 인스턴스와 다른 객체를 참조한다.

 

* 역직렬화 대응방안

public class Singleton implements Serializable {
  private Singleton() {}
  
  private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
  }
  
  public static Singleton getInstance() {
    return SingletonHolder.INSTANCE;
  }
  
  protected Object readResolve() {
    return getInstance();
  }
}

명시적으로 readResolve 메서드가 있는건 아니지만 이 메서드는 역직렬화 될때 사용된다. 그래서 readResolve 메서드의 구현을 getInstance 메서드를 반환하게 바꿔주면 동일한 인스턴스를 얻을 수 있다.

 

 

 

권장하는 방법중 또 하나는 enum 타입으로 구현하는 방법이 있다.

public enum Singleton {
  INSTANCE;
}

enum 타입으로 싱글톤을 구현하게 되면 class로 구현하게 되어 생기는 리플렉션으로 싱글톤이 깨지는 현상에 대응 할수 있다. 

 

public class App {
  public static void main(String[] args) {
    Singleton singleton = Singleton.INSTANCE;
    Singleton singleton1 = null;
    
    Constructor<?>[] constructors = Singleton.class.getDeclaredConstructors();
    for (Constructor<?> constructor: constructors) {
      constructor.setAccessable(true);
      singleton1 = (singleton) constuctor.newInstance("INSTANCE"); // cannot reflectively create enum objects
    }
    
    System.out.println(singleton == singleton1);
  }
}

enum은 리플렉션에서 new를 통한 생성자를 만들수 없다. 따라서 유일한 인스턴스가 보장이 된다. enum은 역직렬화나 리플렉션과 같은 이탈행위를 방지할 수 있다. 하지만 단점은 enum은 클래스를 로딩하는 순간 미리 인스턴스가 만들어진다. 또한 enum은 오로지 enum만 상속 받을 수 있기때문에 다른 class를 상속 받을 수 없다.

 

 

 

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

+ Recent posts