아무거나

[Design Pattern] Prototype Pattern 본문

Java & Kotlin/Java

[Design Pattern] Prototype Pattern

전봉근 2019. 12. 8. 05:10
반응형

프로토타입 패턴(Prototype Pattern)

복잡한 인스턴스를 복사할 수 있다. 즉, 생산 비용이 높은 인스턴스를 복사를 통해서 쉽게 생성할 수 있도록 하는 패턴

  • 인스턴스 생산 비용이 높은 경우
    • 종류가 너무 많아서 클래스로 정리되지 않는 경우
    • 클래스로부터 인스턴스 생성이 어려운 경우

프로토 타입 패턴 예시 - 1

[요구사항]

  • 일러스트레이터와 같은 그림 그리기 툴을 개발중이다. 어떤 모양(Shape) 그릴 수 있또록 하고 복사 붙여넣기 기능을 구현하자.

모양에 대한 함수를 만들자.
[Shape.java]

package com.bkjeon.prototype;

public class Shape implements Cloneable {

    private String id;

    public void setId(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }

}

여기서 Cloneable interface는 인스턴스가 복제 가능하도록 하기 위해서 구현해야 하는 인터페이스이다. clone() 이라는 함수를 사용하면 된다. 하지만 사용용도에 따라 불변성이 지켜지지 않을 수 있으므로 잘 확인하여 사용하자.

원형 클래스를 작성하자. 여기에선 clone() 을 사용하기 위하여 Shape 클래스를 상속받자.
[Circle.java]

package com.bkjeon.prototype;

public class Circle extends Shape {

    private int x,y,r;

    public Circle(int x, int y, int r) {
        this.x = x;
        this.y = y;
        this.r = r;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getX() {
        return x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getY() {
        return y;
    }

    public void setR(int r) {
        this.r = r;
    }

    public int getR() {
        return r;
    }

    public Circle copy() throws CloneNotSupportedException {
        Circle circle = (Circle) clone();
        return circle;
    }

}

메인 클래스를 작성하자.
[Main.java]

package com.bkjeon.prototype;

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {
        Circle circle1 = new Circle(1, 1, 3);
        Circle circle2 = circle1.copy();
        System.out.println(circle1.getX() + "," + circle1.getY() + "," + circle1.getR());
        System.out.println(circle2.getX() + "," + circle2.getY() + "," + circle2.getR());
    }

}

실행결과

1,1,3
1,1,3

프로토 타입 패턴 예시 - 2

[요구사항]

  • 복사 후 붙여 넣기를 하면 두 도형이 겹치는데 안겹치도록 살짝 옆으로 이동되게 하자.

이런 경우는 위의 Circle 클래스의 copy() 하는 과정에서 변경이 가능하다.
[Circle.java]

package com.bkjeon.prototype;

public class Circle extends Shape {

    private int x,y,r;

    public Circle(int x, int y, int r) {
        this.x = x;
        this.y = y;
        this.r = r;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getX() {
        return x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getY() {
        return y;
    }

    public void setR(int r) {
        this.r = r;
    }

    public int getR() {
        return r;
    }

    public Circle copy() throws CloneNotSupportedException {
        Circle circle = (Circle) clone();
        // 위치 변경
        circle.x += 1;
        circle.y += 1;

        return circle;
    }

}

실행결과

1,1,3
2,2,3

깊은 복사와 얕은 복사 - 1

Cloneable 인터페이스에서 제공하는 clone() 함수에 대하여 상세히 알아보자.

우선 알기 예시로 고양이 이름을 지어주는 클래스를 생성해보자.
[Cat.java]

package com.bkjeon.prototype2;

public class Cat {

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

}

메인 클래스에서 실행해보자.
[Main.java]

package com.bkjeon.prototype2;

public class Main {

    public static void main(String[] args) {
        Cat navi = new Cat();
        navi.setName("navi");

        System.out.println(navi.getName());
    }

}

실행결과

navi

이제 위의 Cat 인스턴스를 복사한 인스턴스에 yo 라는 이름의 고양이로 지정해주고 실행해보자.
[Main.java]

package com.bkjeon.prototype2;

public class Main {

    public static void main(String[] args) {
        Cat navi = new Cat();
        navi.setName("navi");

        // yo 고양이 이름 지정
        Cat yo = navi;
        yo.setName("yo");

        System.out.println(navi.getName());
        System.out.println(yo.getName());
    }

}

실행결과

yo
yo

이상하게도 둘다 yo로 이름이 지정되어있다. 해당 실행을 디버깅해보면 복사할 때 인스턴스의 주소값도 동일하게 복사해 가므로 이런 현상이 발생한 것이다. 이것이 바로 낮은 수준의 복사이다. 위와 같은 현상에서 값들만 복사하는것이 깊은 수준의 복사라고 한다.

깊은 수준의 복사를 위하여 Cloneable 인터페이스를 사용해보자.
[Cat.java]

package com.bkjeon.prototype2;

public class Cat implements Cloneable {

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    // 추가
    public Cat copy() throws CloneNotSupportedException {
        Cat ret = (Cat) this.clone();
        return ret;
    }

}

이제 메인클래스에서 깊은 복사로 변경해주자.
[Main.java]

package com.bkjeon.prototype2;

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {
        Cat navi = new Cat();
        navi.setName("navi");

        // yo 고양이 이름 지정
        Cat yo = navi.copy();
        yo.setName("yo");

        System.out.println(navi.getName());
        System.out.println(yo.getName());
    }

}

실행결과

navi
yo

깊은 복사와 얕은 복사 - 2

이해를 위하여 다른 케이스도 작성해보자. 고양이에게 나이를 설정할 수 있게 추가하자.

[Cat.java]

package com.bkjeon.prototype2;

public class Cat implements Cloneable {

    private String name;
    private Integer age;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Integer getAge() {
        return age;
    }

    // 추가
    public Cat copy() throws CloneNotSupportedException {
        Cat ret = (Cat) this.clone();
        return ret;
    }

}

메인 클래스도 변경하자.
[Main.java]

package com.bkjeon.prototype2;

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {
        Cat navi = new Cat();
        navi.setName("navi");
        navi.setAge(3);

        // yo 고양이 이름 지정
        Cat yo = navi.copy();
        yo.setName("yo");
        yo.setAge(1);

        System.out.println(navi.getName());
        System.out.println(yo.getName());

        System.out.println(navi.getAge());
        System.out.println(yo.getAge());
    }

}

실행결과

navi
yo
3
1

위와 같이 잘 나오지만 만약 age 같은 경우는 다양한 값을 가지고 나이를 나타낼 수 있을 것이다.

다양한 Age 값을 고려한 클래스를 작성해보자.
[Age.java]

package com.bkjeon.prototype2;

public class Age {

    int year;
    int value;

    public void setValue(int value) {
        this.value = value;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public int getValue() {
        return value;
    }

    public int getYear() {
        return year;
    }
    
}

Cat 함수에 선언되어있는 age에도 적용시키자.
[Cat.java]

package com.bkjeon.prototype2;

public class Cat implements Cloneable {

    private String name;
    private Age age;

    public Age(int year, int value) {
        this.year = year;
        this.value = value;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setAge(Age age) {
        this.age = age;
    }

    public Age getAge() {
        return age;
    }

    // 추가
    public Cat copy() throws CloneNotSupportedException {
        Cat ret = (Cat) this.clone();
        return ret;
    }

}

이제 마지막으로 메인함수도 수정하자.
[Main.java]

package com.bkjeon.prototype2;

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {
        Cat navi = new Cat();
        navi.setName("navi");
        navi.setAge(new Age(2012, 3));

        // yo 고양이 이름 지정
        Cat yo = navi.copy();
        yo.setName("yo");
        yo.setAge(new Age(2013, 2));

        System.out.println(navi.getName());
        System.out.println(yo.getName());

        System.out.println(navi.getAge().getYear());
        System.out.println(yo.getAge().getYear());
    }

}

실행결과

navi
yo
2012
2013

다른 방식으로도 변경해보자.
[Main.java]

package com.bkjeon.prototype2;

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {
        Cat navi = new Cat();
        navi.setName("navi");
        navi.setAge(new Age(2012, 3));

        // yo 고양이 이름 지정
        Cat yo = navi.copy();
        yo.setName("yo");
        yo.getAge().setYear(2013);
        yo.getAge().setValue(2);

        System.out.println(navi.getName());
        System.out.println(yo.getName());

        System.out.println(navi.getAge().getYear());
        System.out.println(yo.getAge().getYear());

        System.out.println(navi.getAge().getValue());
        System.out.println(yo.getAge().getValue());
    }

}

실행결과를 보면 이름을 제외한 Age에서는 깊은 복사가 이루어지지 않았음을 볼 수 있다.

navi
yo
2013
2013
2
2

해결방법은 Cat 클래스에서 copy하는 부분에서 우리가 명시적으로 깊은복사를 해주면 된다.
[Cat.java]

package com.bkjeon.prototype2;

public class Cat implements Cloneable {

    private String name;
    private Age age;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setAge(Age age) {
        this.age = age;
    }

    public Age getAge() {
        return age;
    }

    // 추가
    public Cat copy() throws CloneNotSupportedException {
        Cat ret = (Cat) this.clone();
        ret.setAge(new Age(this.age.getYear(), this.age.getValue()));
        return ret;
    }

}

실행결과

navi
yo
2012
2013
3
2

즉, 객체 내에 있는 멤버 변수는 원시 변수(int, char, float 등) , Immutable Class (String, Boolean, Integer 등) 또는 Enum 형식일 때는 원본의 값을 바로 대입해도 되지만, 그렇지 않을 때는 멤버변수의 clone을 호출하여 복사해야 한다. 멤버변수가 List, Map 등 여러 유형으로 복잡하게 만들어졌을경우 각각 복사를 해주는 등 다양한 케이스에 유연하게 대응해야 한다.

 

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

반응형
Comments