[GoF의 디자인 패턴] 원형 패턴 (Prototype) (4/23)

Seyeong·2023년 2월 15일
0

GoF의 디자인패턴

목록 보기
5/5

원형 패턴은 흔히 프로토타입 패턴이라고 불립니다.
프로토타입 패턴이 무엇일까요?

프로토타입 패턴이란?

생성할 객체의 종류를 명세하는 데에 원형이 되는 예시물을 이용하고, 그 원형을 복사함으로써 새로운 객체를 생성하는 패턴입니다.

글로만 읽어서는 쉽게 이해되지 않으니 예시를 보며 이해해봅시다.

프로토타입 패턴의 구조

우선 프로토타입 패턴의 구조는 다음과 같습니다.

  • 원형(Prototype)

    자신을 복제하는데 필요한 인터페이스를 정의합니다.

  • 사용자(Client)

    원형에 자기 자신의 복제를 요청하여 새로운 객체를 생성합니다.

요구사항 (예시)

축구공을 복제하는 예시를 프로토타입 패턴을 적용하여 구현해보겠습니다.

사실 저는 축구에 관심이 없어서 잘 몰랐는데, 찾아보니 축구공이 사용 목적에 따라 여러 규격으로 나뉜다는 것을 확인하였습니다.

아래는 이번에 만들 축구공의 규격들입니다.

// 용도별 축구공의 규격

* 공의 무게: 410 ~ 450g, 공의 둘레: 68 ~ 70cm
* 5호 공 (지름 약 22 cm) : 가장 많이 볼 수 있는 사이즈의 축구공 규격으로, 월드컵 공인구의 사이즈이다. 12세 이상 나이부터 사용하는 것이 바람직하다.
* 4호 공 (지름 약 17~21 cm) : 8~12 세 나이의 초등학생 축구 경기에서 사용되거나, 성인 풋살용 공으로 사용된다.
* 3호 공 : (지름 약 18.5 cm) : 7세 이하의 어린이들이 사용하기에 적합한 공으로, 시합용이나 스킬볼 용도로는 잘 사용되지 않는다.
* 2호 공 : 1호 공과 비슷한 사이즈의 연습용 볼, 1호 공에 비해서 많이 생산되지 않는다.
* 1호 공 (지름 약 14~15 cm) : 0호 공과 마찬가지의 스킬볼로써 볼을 다루는 기술을 연습하는데 사용된다. 가장 대중적인 사이즈의 스킬볼이다.
* 0호 공 (지름 약 12 cm) : 아디다스사에서만 나오는 크기의 규격으로, 볼을 다루는 기술을 연습하는데 사용된다.

각 용도별로 공의 규격이 0호 ~ 5호로 나뉘어져 있습니다.

이러한 축구공을 생성하도록 구현하기 위해서는 각각 0호 ~ 5호로 나뉘어져 있는 각 규격들에 맞는 축구공 인스턴스들을 일일이 만들어주면 될 것입니다.

한번 만들어 볼까요?

프로토타입 패턴 적용 전

프로토타입 패턴을 적용하기 전과 후로 나뉘어 코드를 봐보면 더 쉽게 이해될 것입니다.

SoccerBall 클래스

public class SoccerBall {
    private final int weight = 450;
    private final int diameter; // 지름

    public SoccerBall(int diameter) {
        this.diameter = diameter;
    }
}

아직 인터페이스는 만들지 않고 단순히 클래스만 만들었습니다.
편의상 무게는 고정으로 두고 지름만 입력받아 축구공의 규격을 정하겠습니다.

이제 이를 이용해 각 호수에 맞는 축구공을 만들어보겠습니다.

Main

public class Main {
    public static void main(String[] args) {
        SoccerBall ballNo5 = new SoccerBall(22); // 5호 공
        SoccerBall ballNo4 = new SoccerBall(21); // 4호 공
        SoccerBall ballNo3 = new SoccerBall(19); // 3호 공
        SoccerBall ballNo2 = new SoccerBall(15); // 2호 공
        SoccerBall ballNo1 = new SoccerBall(15); // 1호 공
        SoccerBall ballNo0 = new SoccerBall(12); // 0호 공
    }
}

만약 이때 5호 공을 똑같이 한번 더 만들고 싶다면 어떻게 하면 될까요?

new SoccerBall(22);

이렇게 해주면 될 겁니다. 그런데 문제가 있습니다.

현재 SoccerBall의 필드는 파라미터로 받는 변수가 지름 한 가지 밖에 없습니다. 만약 축구공을 만들기 위해 무게, 색깔, 무늬, 공기압 등등 여러 조건이 추가된다면 매번 생성할 때마다 조건들을 줄줄이 나열해주어야 합니다.

new SoccerBall(22, "무게", "색깔", "무늬", "공기압", "등등...");

또한 이렇게 재생성한 축구공이 이전의 축구공과 일치한지 필드들을 일일이 확인해주어야 합니다.

이 문제를 프로토타입 패턴을 적용하면 이보다 훨씬 편리하게 사용할 수 있습니다.

프로토타입 패턴 적용 후

SoccerBall 클래스에 복제 기능을 넣어보겠습니다.

SoccerBall 클래스

public class SoccerBall implements Cloneable {
	
    ...

    public SoccerBall copy() throws CloneNotSupportedException {
        return (SoccerBall) super.clone();
    }
}

copy() 라는 이름으로 현재 축구공을 복제하는 기능을 구현하였습니다.

복제는 Object 클래스의 clone( ) 메서드를 이용해서 자기 자신을 복제하도록 만들었는데 clone( ) 메서드 문서를 봐보면 아래와 같은 설명이 있습니다.

The method clone for class Object performs a specific cloning operation. First, if the class of this object does not implement the interface Cloneable, then a CloneNotSupportedException is thrown.

Object 클래스의 clone 메서드는 복제 작업을 수행합니다. 만약 클래스가 Cloneable 인터페이스를 구현하지 않으면 CloneNotSupportedException이 발생합니다.

이 문서에서 알 수 있듯이 clone( ) 메서드를 이용하기 위해선 Cloneable 인터페이스를 구현하는 것이 필수이기에 위의 코드에서도 구현해주었습니다.

현재로는 굳이 인터페이스를 구현해서까지 clone( ) 을 써야하나?

return new SoccerBall(diameter);

해주면 간단하고 편하지 않나? 싶기도 하지만 앞으로 얼마나 많은 필드가 추가될지 모르는 상태에서, clone( ) 메서드는 유연하게 대처할 수 있게 해줍니다.

이제 복제를 하고 필드를 확인해봅시다.

그 전에 객체의 필드를 쉽게 확인할 수 있게 toString( ) 메서드를 재정의해줍시다.

public class SoccerBall implements Cloneable {
    
    ...

    @Override
    public String toString() {
        return "SoccerBall{" +
                "weight=" + weight +
                ", diameter=" + diameter +
                '}';
    }
}

Main

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
    
    	SoccerBall ballNo5 = new SoccerBall(22); // 5호 공
        ...
        
        SoccerBall copyBallNo5 = ballNo5.copy(); // 복제
        System.out.println("ballNo5 = " + ballNo5);
        System.out.println("copyBallNo5 = " + copyBallNo5);
    }
}

체크 예외때문에 코드가 지저분해보이지만 복제가 되었는지 확인하는데는 무리가 없어보입니다.

코드에서 5호 공을 복제하는 코드가 명확히 드러납니다. 또한 출력 결과를 확인해보면 필드들 또한 제대로 복사된 것을 확인할 수 있습니다.

출력 결과

이렇게만 끝나면 참 행복하겠지만 이대로는 문제가 있습니다. 바로 깊은 복사와 얕은 복사입니다.

즉, 어떤 객체를 복제한다는 것은 인스턴스 변수들까지 복제하는 것인지, 아니면 이들 변수를 공유하도록 만드는 것인지에 관한 문제입니다.

간단하게 축구공에 색깔이라는 필드를 추가해서 확인해봅시다.

SoccerBall 클래스

public class SoccerBall implements Cloneable {
    private final int weight = 450;
    private final int diameter; // 지름
    private final Color color; // 색깔
    
    ...
}

색깔에 대한 필드를 단순히 String 원시값이 아니라 Color 라는 클래스의 인스턴스로 받게끔 해주었습니다. Color 클래스는 아래와 같습니다.

Color 클래스

public class Color {
    private final String color; // 색깔을 의미하는 문자열

    public Color(String color) {
        this.color = color;
    }
}

이제 이를 이용해 흰색 공을 만들고, 그를 복제한 뒤 복제된 공의 색깔을 변경해보도록 하겠습니다.

Main


public class PrototypeMain {
    public static void main(String[] args) throws CloneNotSupportedException {
        SoccerBall ballNo5 = new SoccerBall(22, new Color("White")); // 흰색 5호 공
	
    	...
    
        SoccerBall copyBallNo5 = ballNo5.copy(); // 복제
        copyBallNo5.getColor().setColor("Black"); // 복제된 공의 색깔을 변경

        System.out.println("ballNo5 = " + ballNo5);
        System.out.println("copyBallNo5 = " + copyBallNo5);
    }
}

중간에 색을 변경하는 부분에서 잠시 게터와 세터를 남발하여 코드를 지저분하게 만들었지만 이해하는데는 무리가 없을리라 생각합니다.

이제 출력문을 보면 처음에 만든 공은 흰색 공이고, 복제된 공은 색을 변경하였으므로 검은색 공이 나오길 기대하지만 출력은 다르게 나옵니다.

출력 결과

둘 다 검은색 공이 되었습니다.
복제된 공의 색을 변경하였지만, 원형의 공의 색깔도 동시에 변경되는 문제가 발생합니다.

사실 이러한 문제는 의도된 것입니다. Object의 clone( ) 메서드에 대한 문서를 더 살펴보면 아래와 같은 내용이 존재합니다.

the contents of the fields are not themselves cloned. Thus, this method performs a "shallow copy" of this object, not a "deep copy" operation.

필드의 내용은 복제되지 않는다. 따라서 이 메서드는 개체에 대해 "깊은 복사" 가 아니라 "얕은 복사"를 수행한다.

얕은 복사는 동작 원리가 간단하며 또 이것만으로도 충분할 때가 많습니다. 처음에 만들었던 예시도 얕은 복사만으로도 충분했습니다. 그러나, 구조가 복잡한 원형을 복제할 때는 대체적으로 깊은 복사가 필요합니다. 원형과 복제본이 서로 독립적이어야 하기 때문입니다. 이를 위해선 원형이 정의하는 각 요소를 모두 개별적으로 복제해야 합니다.

마지막으로 이들을 개별적으로 복제하는 것을 마지막으로 마무리 하겠습니다.

SoccerBall 클래스

public class SoccerBall {

	...
    
    public SoccerBall copy() {
        return new SoccerBall(this.diameter, color.copy());
    }
    
    ...
    
}

더이상 Cloneable 인터페이스의 clone( ) 메서드를 사용하지 않습니다. 일일이 개별적으로 복제해주었습니다. int와 같은 기본 타입은 그대로 담아주고 color와 같은 인스턴스는 각각 개별적으로 copy( ) 메서드를 호출해서 값을 깊은 복사해오는 방식으로 구현해주었습니다.

Color 클래스

public class Color {
    private String color
    
	...
    
    public Color copy() {
        return new Color(this.color); // 새로운 인스턴스를 생성함
    }
}

이렇게 해주면 각 color 인스턴스들이 서로 독립적인 객체이므로 값을 변경하여도 서로에게 영향을 주지 않을 것입니다.

출력 결과

0개의 댓글