새소식

design-pattern

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

  • -

프로토타입(Prototype) 패턴

개념

프로토타입 패턴은 생성할 객체들의 타입이 프로토타입(원형)인 인스턴스로부터 결정되도록 하며, 새 객체를 만들기 위해 인스턴스를 복제하는 인스턴스 생성과 관련된 디자인 패턴이다.

아래는 프로토타입 패턴을 클래스 다이어그램으로 표현한 것이다.

프로토타입 패턴 클래스 다이어그램

Client 클래스는 Prototype 타입의 객체를 register() 메서드를 통해 등록해두고, create() 메서드를 통해 등록한 Prototype 타입의 객체를 생성하여 반환하는 역할을 한다.
register() 메서드를 통해 등록된 객체는 Prototype 인터페이스의 구현체이다.
Prototype 인터페이스에는 createCopy() 메서드가 있는데, 이는 해당 타입의 인스턴스를 복사(copy)하여 반환하는 메서드이다.
따라서 Client에서 인스턴스를 생성할 때에는 register() 메서드를 통해 등록된 객체를 찾고, 찾은 객체의 createCopy() 메서드를 통해 해당 인스턴스를 복사, 이를 반환하는 흐름을 갖는다.

예시로 확인해보자.

예시

먼저 Prototype 인터페이스를 정의한다.
Prototype 인터페이스에는 Prototype을 복사하여 반환하는 createClone() 메서드와 Prototype의 구현체의 인스턴스 이름을 반환하는 getName() 메서드를 작성했다.
또한 복사를 편리하게 하기 위해서 Cloneable 인터페이스를 상속했다.

// Prototype
public interface Monster extends Cloneable {
    String getName();
    Monster createClone();
}

이제 Prototype 역할을 하는 Monster 인터페이스를 구현한 구현체들을 작성하자.

// ConcretePrototype
public class MonsterA implements Monster {
    private String name;

    public MonsterA(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Monster createClone() {
        Monster monster;
        try {
            monster = (Monster) clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
        return monster;
    }
}

// ConcretePrototype
public class MonsterB implements Monster {
    private String name;

    public MonsterB(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Monster createClone() {
        Monster monster;
        try {
            monster = (Monster) clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
        return monster;
    }
}

Monster 인터페이스를 구현한 구현체들을 작성했다.

이제 Monster 타입의 인스턴스를 등록해두고, 필요시 등록된 인스턴스 복사를 통해 객체를 생성하고 반환하는 역할을 하는 Client를 작성하자.

// Client
public class MonsterMaker {
    private MonsterRegistry registry;

    public MonsterMaker(MonsterRegistry registry) {
        this.registry = registry;
    }

    public void register(Monster monster) {
        registry.register(monster);
    }

    public Monster create(String monsterName) {
        return registry.getMonster(monsterName).createClone();
    }
}

// Registry
public class MonsterRegistry {
    private Map<String, Monster> registry;

    public MonsterRegistry() {
        this.registry = new HashMap<>();
    }

    public void register(Monster monster) {
        registry.put(monster.getName(), monster);
    }

    public Monster getMonster(String name) {
        return registry.get(name);
    }
}

Client 내부에 Registry를 두어도 되지만, 역할을 분리하기 위해 Registry를 다른 클래스로 분리했다.
ClientPrototype 타입의 인스턴스를 Registry에 등록, Registry에 등록된 원하는 Prototype 인스턴스를 꺼내어 인스턴스의 복사본을 생성하고 반환하는 역할을 한다.
RegistryPrototype 타입의 인스턴스를 등록, 조회하는 역할을 한다.

프로토타입 패턴의 구현을 완료했다.
이제 이를 사용해보자.

public class App {

    public static void main(String[] args) {
        MonsterMaker monsterMaker = new MonsterMaker(new MonsterRegistry());
        MonsterA mobA = new MonsterA("mobA");
        MonsterB mobB = new MonsterB("mobB");
        monsterMaker.register(mobA);
        monsterMaker.register(mobB);


        Monster mobA1 = monsterMaker.create("mobA");
        System.out.println(mobA.equals(mobA1)); // false
        System.out.println(mobA.getName().equals(mobA1.getName())); // true

        System.out.println();

        Monster mobB1 = monsterMaker.create("mobB");
        System.out.println(mobB.equals(mobB1)); // false
        System.out.println(mobB.getName().equals(mobB1.getName())); // true
    }
}

먼저 Client 역할을 하는 MonsterMakerPrototype의 구현체 역할을 하는 MonsterA|B 인스턴스를 생성하여 등록하였다.
이후 MonsterMakerString 타입으로 Monster 인스턴스 생성을 요청하였다.
이를 통해 생성된 인스턴스는, 기존 등록한 인스턴스와 비교시 동일하지 않다는 결과를 보여준다.
하지만 생성된 인스턴스와 등록된 인스턴스의 필드는 동일한 것을 확인할 수 있다.

결론

Client 역할을 하는 MonsterMakercreate() 메서드를 통해 인스턴스를 생성한다.
하지만 코드에 구체 클래스 타입이 존재하지 않는다.
인터페이스를 활용하여 유연한 코드를 작성할 수 있었다.

또한 MonsterMaker는 인스턴스를 생성하는 구체적인 방법은 몰라도 된다.
단지 Prototype 인터페이스의 createClone() 메서드를 통해 Prototype 인스턴스를 받기만 하면 된다.
인스턴스를 생성하는 구체적인 방법은 각 구현체에 작성한다.
각 구현체별로 적합한 방법으로 인스턴스를 생성해서 반환하기만 하면 된다.

위 예시에서는 Registry에 단순한 2가지 인스턴스만 등록되었는데, 만약 이렇게 사용되어야 할 객체의 종류가 엄청나게 많다거나, 인스턴스를 생성할 때 자원과 시간이 많이 들거나, 인스턴스 생성이 복잡하다면 프로토타입 패턴을 이용하면 간단하게 인스턴스를 복사 및 생성하여 사용할 수 있다.

정리하자면 아래와 같다.

장점

  • 인스턴스를 만드는 과정을 처리하는 코드와 인스턴스를 생성하는 코드를 분리할 수 있다.
    • 클라이언트는 새로운 인스턴스를 만드는 과정을 몰라도 된다.
    • 클라이언트는 구체적인 형식을 몰라도 객체를 생성할 수 있다.
  • 상황에 따라 객체를 새로 생성하는 것 보다 객체를 복사하는 것이 더 효율적일 수 있다.

단점

  • 때때로 객체의 복사본을 만드는 일이 매우 복잡할 수도 있다(얕은 복사로 인한 문제 해결 등).

활용법

  • 시스템의 복잡한 클래스 계층구조에 파묻혀 있는 다양한 형식의 객체 인스턴스를 새로 만들어야 할 때 유용하게 써먹을 수 있다.

어떤 클래스의 인스턴스를 만들 때 자원과 시간이 많이 들거나 복잡하다면 프로토타입 패턴 고려해보자.

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

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.