디자인패턴

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

swimKind 2022. 8. 23. 22:58

[디자인패턴] 생성 패턴 - 프로토타입(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