도서 요약 / / 2023. 2. 10. 20:11

디자인 패턴 - 프로토타입(prototype) 패턴

udemy 강좌(Java Design Patterns & SOLID Design Principles)를 정리한 내용이다.

https://www.udemy.com/course/design-patterns-in-java-concepts-hands-on-projects/learn/lecture/9604610?start=0#overview


이제 생성 패턴으로 알려진 프로토타입 패턴을 알아볼 것이다. 이 디자인 패턴은 무엇이며 왜 사용하는가?

프로토타입은 무엇인가?

  • 객체가 있고 객체 생성 비용이 비싼 상황, 즉 성능 비용은 객체 생성에 필요한 연산이 필요한 경우나 우리가 제어할 수 없는 외부 리소스와 관련된 작업 같은 것이 있는 경우이다.
  • 그래서 그런 경우가 있을 때 여러 인스턴스를 만들기 위해 단일 객체를 사용하거나 프로토타입으로 기존 객체를 사용한다. 이 패턴은 기존 인스턴스의 복제본을 사용하여 처음부터 인스턴스를 만들지 않아도 된다.

UML

프로토타입 구현

  • Prototype 클래스를 만들면서 시작한다.
    • 클래스는 Cloneable 인터페이스를 구현해야 한다. Cloneable은 마커 인터페이스이며 마커 인터페이스는 어떤 메소드도 정의되어 있지 않다.
    • 클래스는 clone 메소드를 오버라이드해야 하고 그 복사본을 리턴한다.
    • 그 메소드는 cloning이 지원하는지 여부를 서브 클래스에서 결정하도록 CloneNotSuportedException을 처리해야 한다.
  • clone 메소드 구현은 깊은(deep)복사를 사용할 지 얕은(shallow) 복사를 사용할 지를 고려해야 한다. 얕은 복사는 속성을 단순히 새 객체로 복사하는 것이다. 깊은 복사는 프로토타입 객체에서 필요한 모든 객체를 새로 만드는 것이다.

예제: UML

프로토타입 클래스인 GameUnit 클래스로 시작해볼 것이다. 이 클래스 객체는 복제 작업을 지원할 것이다. 그리고 clone 메소드가 있고 public initialize와 이 메소드가 왜 필요한지 알게 될 것이다.

프로토타입을 구현할때마다 두 개의 서브클래스, 즉 GameUnit 클래스들, 하나는 Swordsman이고 다른 하나는 General이다.

우리가 해야할 일은 Swordsman은 복제를 지원할 것이고 General은 복제를 지원하지 않게 하는 것이다.

그리고 나서 초기 객체를 생성하고 프로토타입을 사용하기 위해 기존 객체를 복제하는 클라이언트가 있다.

Java 구현

이 클래스들은 좀 전에 봤던 UML 예제와 대응되는 코드이다.

@Getter
public abstract class GameUnit {

    private Point3D position;

    public GameUnit() {
        this.position = Point3D.ZERO;
    }

    public GameUnit(float x, float y, float z) {
        this.position = new Point3D(x, y, z);
    }

    public void move(Point3D direction, float distance) {
        Point3D finalMove = direction.normalize();
        finalMove = finalMove.multiply(distance);
        position = position.add(finalMove);
    }
}

우선, GameUnit 클래스가 있고 프로토타입 클래스의 기반 클래스이다. GameUnit은 게임상에서 지도 위에 있는 특정 유닛을 나타낸다. 이 클래스 객체는 지도 상에 있는 게임 유닛을 나타낸다. 이 클래스는 단일 속성이 있고 지도상의 게임 객체의 위치를 나타낸다.

GameUnit은 두 개의 자식 클래스가 있다.

@ToString
public class Swordman extends GameUnit {

    private String state = "idle";

    private void attack() {
        this.state = "attacking";
    }
}

우선, Swordsman 클래스가 있고 GameUnit을 확장하고 있다. state 라는 속성을 정의하고 특정 Swordsman의 현재 상태를 나타낸다.

// General은 clone을 지원하지 않는다.
@ToString
public class General extends GameUnit {

    private String state = "idle";

    public void boostMorale() {
        this.state = "MoralBoost";
    }
}

그리고 General이라는 다른 클래스가 있고 GameUnit을 확장하고 있다. 우리가 여기서 해야할 일은 General 객체는 복제를 지원하지 않는 것이다. GeneralUnit은 게임상에서 유일한 존재이며 실수로 특정 객체의 복제본을 만들게 하고 싶지 않다.

clone을 지원하기 위해서는 Cloneable 인터페이스를 실체화한다. Cloneable은 java 인터페이스이며 clone 작업을 지원하기 위해 사용된다.

@Getter
public abstract class GameUnit implements Cloneable {

    private Point3D position;

    public GameUnit() {
        this.position = Point3D.ZERO;
    }

    public GameUnit(float x, float y, float z) {
        this.position = new Point3D(x, y, z);
    }

    @Override
    protected GameUnit clone() throws CloneNotSupportedException {
        GameUnit unit = (GameUnit) super.clone();
        unit.initialize();
        return unit;
    }

    protected void initialize() {
        this.position = Point3D.ZERO;
        reset();
    }

    protected abstract void reset();

    public void move(Point3D direction, float distance) {
        Point3D finalMove = direction.normalize();
        finalMove = finalMove.multiply(distance);
        position = position.add(finalMove);
    }
}

clone() 메소드를 구현한다. 여기서 얕은 복사를 할지, 깊은 복사를 할지 결정해야 한다. 여기서는 position 하나의 속성만 가지고 있고 immutable하기 때문에 얕은 복사를 사용할 것이다.

그리고 위치정보를 초기화하는 reset하는 메소드를 만든다.

Swordman에 reset을 추가한다.

@ToString
public class Swordman extends GameUnit {

    private String state = "idle";

    private void attack() {
        this.state = "attacking";
    }

    @Override
    protected void reset() {
        this.state = "idle";
    }
}

General은 clone을 지원하지 않을 것이라 clone을 사용하는 경우 예외를 던진다.

// General은 clone을 지원하지 않는다.
@ToString
public class General extends GameUnit {

    private String state = "idle";

    public void boostMorale() {
        this.state = "MoralBoost";
    }

    @Override
    protected GameUnit clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException("Ganerals are unique");
    }

    @Override
    protected void reset() {
        throw new UnsupportedOperationException("Reset not supported");
    }
}

다음은 클라이언트를 만들어보자.

public class Client {

    public static void main(String[] args) throws CloneNotSupportedException {
        Swordman s1 = new Swordman();
        s1.move(new Point3D(-10, 0, 0), 20);
        s1.attack();

        System.out.println(s1);

        Swordman s2 = (Swordman) s1.clone();
        System.out.println(s1 == s2);
        System.out.println(s2);
    }
}

구현 고려사항

  • 참조값에 대해 깊은 복사를 사용할지 얕은 복사를 사용할지 신경써야 한다. 복제 시 불변 필드는 깊은 복사를 할 필요가 없게 된다.
  • 프로토타입을 리턴하기 전에 변경되는 객체의 상태를 초기화해야 한다. 서브클래스에서 직접 자신을 초기화하는 방법이 좋다.
  • clone() 메소드는 Object 클래스에 protected로 선언되어 있고 클래스 외부에서 사용되기 위해 public으로 오버라이딩이 되어야 한다.
  • Cloneable은 마커 인터페이스이며 복제를 지원하는 클래스를 나타낸다.

디자인 고려사항

  • 프로토타입은 인스턴스에서 대부분의 상태가 변경되지 않는 객체가 많을 때 유용하다.
  • 프로토타입 레지스터리는 다른 코드에서 객체를 복제할 수 있도록 다양한 프로토타입을 등록할 수 있다. 이렇게 함으로써 객체를 최초 접근할 때의 문제를 해결할 수 있다.
  • 프로토타입은 컴포지트와 데코레이터 패턴과 같이 사용될 때 유용하다.

위험요소

  • 상태를 가지는 속성의 개수에 따라 불변이 되고 얕은 복사로 사용될 수 있다. 상태가 많은 불변이 아닌 객체로 구성되어 있다면 복제하는 데 복잡해 질수 있다
  • Java에서 기본 복제 동작은 얕은 복사를 하고 깊은 복사를 하려면 직접 구현해야 한다.
  • 서브클래스에서 복제를 지원하지 않는 경우 CloneNotSupportedException를 구현해야 하므로 코드가 복잡해질 수도 있다.



반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유