나를 기록하다
article thumbnail
반응형

JAVA 언어로 배우는 디자인 패턴 입문

Prototype 패턴

Something 클래스의 인스턴스를 만들고자 할 때 우리는 다음과 같이 new라는 Java 언어의 키워드를 사용해서 클래스 이름을 지정하고 인스턴스를 생성한다.

new Something()

이처럼 new를 사용해 인스턴스를 만들 때는 클래스 이름을 반드시 지정해야 한다. 그러나 클래스 이름을 지정하지 않고 인스턴스를 생성하고 싶을 때도 존재한다. 다음과 같은 경우에는 클래스로부터 인스턴스를 만드는 대신 인스턴스를 복사해서 새 인스턴스를 만든다.

 

종류가 너무 많아 클래스로 정리할 수 없는 경우

  • 취급할 오브젝트 종류가 너무 많아서, 하나하나 다른 클래스로 만들면 소스 파일을 많이 작성해야 하는 경우

클래스로부터 인스턴스 생성이 어려운 경우

  • 생성하고 싶은 인스턴스가 복잡한 과정을 거쳐 만들어지는 것으로, 클래스로부터 만들기가 매우 어려운 경우
  • 예시: 그래픽 에디터 등에서 사용자가 마우스로 그린 도형을 나타내는 인스턴스
    • 사용자 조작으로 만들어진 인스턴스를 프로그래밍해서 만들기 어려움
    • 다시 만들고 싶은 경우에는 지금 만든 인스턴스를 일단 저장해 두고 만들고 싶을 때 그것을 복사

프레임워크와 생성하는 인스턴스를 분리하고 싶은 경우

  • 인스턴스를 생성하는 프레임워크를 특정 클래스에 의존하지 않게 하고 싶은 경우
  • 클래스 이름을 지정해서 인스턴스를 만드는 것이 아닌 미리 '원형'이 될 인스턴스를 등록해 두고, 등록된 인스턴스를 복사해서 인스턴스 생성

인스턴스로부터 다른 인스턴스를 생성하는 것은 복사기로 문서를 복사하는 것과 비슷하다. 원본 서류를 복사기에 넣으면 같은 서류를 몇 장이든 만들 수 있다. 인스턴스를 생성하는 대신 인스턴스로부터 다른 인스턴스를 생성하는 것이 바로 Prototype 패턴이다. Prototype 패턴은 원형(모범)이 되는 인스턴스를 바탕으로 새로운 인스턴스를 만든다.


예제 프로그램

문자열을 테두리로 감싸서 표시하거나 밑줄을 그어 표시하는 예제 프로그램

클래스 및 인터페이스 목록

패키지 이름 설명
framework Product 추상 메서드 use와 createCopy가 선언되어 있는 인터페이스
framework Manager createCopy를 사용하여 인스턴스를 복제하는 클래스
이름 없음 MessageBox 문자열을 테두리로 감싸서 표시하는 클래스로 use와 createCopy를 구현
이름 없음 UnderlinePen 문자열에 밑줄을 그어 표시하는 클래스로 use와 createCopy를 구현
이름 없음 Main 동작 테스트용 클래스

클래스 다이어그램


Product 인터페이스

  • java.lang.Cloneable 인터페이스를 상속한 Product 인터페이스는 복제를 가능하게 한다.
    이 인터페이스를 구현하는 클래스의 인스턴스는 clone 메서드를 사용해서 자동으로 복제할 수 있게 된다.
  • use 메서드는 '사용'하기 위한 것이다. '사용'이 무엇을 의미하는지는 하위 클래스의 구현에 맡겨져 있다.
  • createCopy는 인스턴스를 복제하기 위한 메서드이다.
package ch06_Prototype.framework;

public interface Product extends Cloneable {
    public abstract void use(String s);

    public abstract Product createCopy();
}

Manager 클래스

  • Product 인터페이스를 이용해 인스턴스를 복제하는 클래스
  • showcase 필드는 String으로 나타낸 '이름'과 Product 인터페이스를  구현한 클래스의 '인스턴스'와의 대응 관계를 java.util.Map<String, Product>로 표현했다. showcase 필드에 이름과 인스턴스의 쌍을 등록(put)해두면, 지정한 이름에 대응하는 인스턴스를 취득(get)할 수 있다.
  • 제품 이름과 Product 인터페이스를 주면, register 메서드에서 해당 쌍을 showcase에 등록한다.
  • 여기서 인수로 넘어오는 Product형의 prototype은 Product 인터페이스를 구현한 클래스의 인스턴스이다.
  • Product 인터페이스나 Manager 클랫의 소스 코드에 MessageBox 클래스나 UnderlinePen 클래스의 이름이 전혀 나오지 않는다.
    • Product와 Manager는 그 클래스들과는 독립적으로 수정이 가능하다는 것을 의미한다.
    • 소스 코드 안에 클래스 이름을 기술하면 그 클래스와 밀접한 관계가 생긴다.
  • Manager 클래스에서는 구체적인 개별 클래스 이름을 쓰지 않고. 오직 Product 인터페이스 이름만 사용한다.
    이 인터페이스만이 Manager 클래스와 다른 클래스를 연결하는 다리가 된다.
package ch06_Prototype.framework;

import java.util.HashMap;
import java.util.Map;

public class Manager {

    private Map<String, Product> showcase = new HashMap<>();

    public void register(String name, Product prototype) {
        showcase.put(name, prototype);
    }

    public Product create(String prototypeName) {
        Product p = showcase.get(prototypeName);
        return p.createCopy();
    }
}

MessageBox 클래스

  • Product 인터페이스를 구현한 클래스
  • decochar 필드: 문자열을 장식처럼 에워싸는 문자
  • use 메서드: 주어진 문자열을 decochar로 에워싼다

[예시] decochar가 '*'일 때 문자열을 use 메서드에 주었을 경우

*******
*Hello*
*******
  • createCopy
    • 자기 자신을 복제하는 메서드
    • 호출하는 clone 메서드는 Java 언어 사양으로 규정되어 있으며, 자기 자신의 복제를 생성하는 메서드
    • 복제를 생성할 때 인스턴스가 가진 필드 값도 그대로 새 인스턴스에 복사된다
    • clone 메서드로 복사할 수 있는 것은 java.lang.Cloneable 인터페이스를 구현한 클래스 뿐
    • 이 인터페이스가 구현되지 않는 경우에는 CloneNotSupportedException 예외가 발생할 수 있으므로 try-catch로 예외를 처리해야 한다.
    • MessageBox 클래스에서는 Product 인터페이스만 구현
      ➝ 하지만 Product 인터페이스는 Cloneable 인터페이스를 확장한 것
      ➝ CloneNotSupportedException이 발생하지 않는다
      추가) Cloneable 인터페이스는 단순한 표시로 이용될 뿐, 이 인터페이스가 선언하는 메서드는 없다
  • clone 메서드는 자신의 클래스 및 하위 클래스에서만 호출할 수 있으므로, 다른 클래스의 요청으로 복제할 경우에는 createCopy와 같은 별도의 메서드로 clone을 감싸줄 필요가 있다.
package ch06_Prototype;

import ch06_Prototype.framework.Product;

public class MessageBox implements Product {

    private char decochar;

    public MessageBox(final char decochar) {
        this.decochar = decochar;
    }

    @Override
    public void use(final String s) {
        int decolen = s.length() + 2;
        for (int i = 0; i < decolen; i++) {
            System.out.print(decochar);
        }
        System.out.println();
        System.out.println(decochar + s + decochar);
        for (int i = 0; i < decolen; i++) {
            System.out.print(decochar);
        }
        System.out.println();
    }

    @Override
    public Product createCopy() {
        Product p = null;
        try {
            p = (Product) clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return p;
    }
}

UnderlinePen 클래스

[예시] 'Hello'라는 문자열을 use 메서드에 줄 때

Hello
-----

 

package ch06_Prototype;

import ch06_Prototype.framework.Product;

public class UnderlinePen implements Product {

    private char ulchar;

    public UnderlinePen(final char ulchar) {
        this.ulchar = ulchar;
    }

    @Override
    public void use(final String s) {
        int ulen = s.length();
        System.out.println(s);
        for (int i = 0; i < ulen; i++) {
            System.out.print(ulchar);
        }
        System.out.println();
    }

    @Override
    public Product createCopy() {
        Product p = null;
        try {
            p = (Product) clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return p;
    }
}

Main 클래스

등록할 내용

이름 클래스 및 인스턴스의 내용
"strong message" UnderlinePen에서 ulchar가 '-'
"warning box" MessageBox에서 decochar가 '*'
"slash box" MessageBox에서 decochar가 '/'

 

package ch06_Prototype;

import ch06_Prototype.framework.Manager;
import ch06_Prototype.framework.Product;

public class Main {
    public static void main(String[] args) {

        //준비
        Manager manager = new Manager();
        UnderlinePen upen = new UnderlinePen('-');
        MessageBox mbox = new MessageBox('*');
        MessageBox sbox = new MessageBox('/');

        //등록
        manager.register("strong message", upen);
        manager.register("warning box", mbox);
        manager.register("slash box", sbox);

        //생성과 사용
        Product p1 = manager.create("strong message");
        p1.use("Hello, world!");

        Product p2 = manager.create("warning box");
        p2.use("Hello, world!");

        Product p3 = manager.create("slash box");
        p3.use("Hello, world!");
    }
}

 

실행 결과

Hello, world!
-------------
***************
*Hello, world!*
***************
///////////////
/Hello, world!/
///////////////

Prototype의 구성

클래스 다이어그램

Prototype(원형) 역할

인스턴스를 복사하여 새로운 인스턴스를 만들기 위한 메서드를 결정한다.

예제 프로그램에서는 Product 인터페이스가 이 역할을 맡았다.

ConcretePrototype(구체적인 원형) 역할

인스턴스를 복사하여 새로운 인스턴스를 만드는 메서드를 구현한다.

예제 프로그램에서는 MessageBox 클래스와 UnderlinePen 클래스가 이 역할을 맡았다.

Client(이용자) 역할

인스턴스를 복사하는 메서드를 이용해 새로운 인스턴스를 만든다.

예제 프로그램에서는 Manager 클래스가 이 역할을 맡았다.


생각해볼 내용

클래스에서 인스턴스를 만들면 안되는 것일까?

인스턴스를 만들고 싶으면 간단히 `new Something()`이라고 하면 되는데, 왜 Prototype 패턴이 필요한 걸까?

  1. 종류가 너무 많아서 클래스로 정리할 수 없는 경우
    • '-'로 문자열 밑줄 ,'*' 또는 '/'로 문자열에 테두리 등 수많은 종류의 원형을 만들 수 있는데 그 원형을 모두 개별 클래스로 만들어 버리면, 클래스 수가 너무 많아져서 소스 프로그램을 관리하기 어려움
  2. 클래스로부터 인스턴스 생성이 어려운 경우
    • 마우스로 그린 도형을 나타내는 인스턴스와 동일한 것을 만들고 싶을 때는 클래스 사용이 아닌 인스턴스를 복사해서 만드는 것이 간편
  3. 프레임워크와 생성하는 인스턴스를 분리하고 싶은 경우
    • Manager 클래스의 create 메서드에는 클래스 이름 대신 "strong message"나 "slash box"라는 문자열을 인스턴스 생성을 위한 이름으로 부여
    • 이는 Java 언어가 갖춘 인스턴스 생성 메커니즘인 `new something()` 형식을 더욱 일반화하여 클래스 이름의 속박으로부터 프레임워크를 분리했다고 말할 수 있다.

클래스 이름은 속박인가?

객체지향 프로그래밍의 목표 중 하나로 '부품으로서의 재사용'이라는 점을 생각해보자.

소스 코드 안에 이용할 클래스 이름이 쓰여 있으면, 그 클래스와 분리해서 재사용할 수 없게 된다.

물론 소스 코드를 수정해서 클래스 이름을 변경할 수 있지만 '부품으로서의 재사용'에서는 소스 코드를 수정하는 것은 고려하지 않는다.

소스 파일(.java)이 없어도 재사용할 수 있느냐가 포인트이다.


관련 패턴

  • Flyweight 패턴
    • Prototype 패턴에서는 현재 인스턴스와 동일한 상태의 별도의 인스턴스를 만들어 이용한다.
    • Flyweight 패턴에서는 하나의 인스턴스를 여러 장소에서 공유하여 이용한다.
  • Memento 패턴
    • Prototype 패턴에서는 현재 인스턴스와 동일한 상태의 별도의 인스턴스를 만든다.
    • Memento 패턴에서는 스냅샷과 undo를 실행하기 위해 현재 인스턴스 상태를 저장한다.
  • Composite 패턴 및 Decorator 패턴
    • Composite 패턴이나 Decorator 패턴을 많이 사용할 때 복잡한 구조의 인스턴스가 동적으로 만들어지는 경우가 존재하는데 이 때 Prototype 패턴을 사용하면 편리하다.
  • Command 패턴
    • Command 패턴에 등장하는 명령을 복제하고자 하는 경우 Prototype 패턴이 사용될 수 있다.

Clone 메서드와 java.lang.Cloneable 인터페이스

Java 언어의 clone

clone 메서드는 인스턴스를 복사하는 장치이다. clone 메서드를 실행할 경우 복사 대상이 되는 클래스는 java.lang.Cloneable 인터페이스를 구현해야만 한다. 복사 대상 또는 상위 클래스 어딘가에서 Cloneable 인터페이스를 구현해도 된다. 또한 Cloneable 인터페이스의 하위 인터페이스를 구현해도 상관없다. (예제에서 MessageBox 클래스와 UnderlinePen 클래스는 Product 인터페이스 구현, Product 인터페이스에서는 Cloneable 인터페이스의 하위 인터페이스로 되어 있음)

 

Cloneable 인터페이스를 구현한 클래스의 인스턴스는 clone 메서드를 호출하면 복사된다. 그리고 clone 메서드의 반환값은 복사로 만들어진 인스턴스가 된다.(원본 인스턴스와 같은 크기의 메모리를 확보하고 원본 인스턴스의 필드 내용을 복사하는 것)

 

만약 Cloneable 인터페이스를 구현하지 않은 클래스의 인스턴스가 clone 메서드를 호출하면 예외 CloneNotSupportedException이 발생한다.

 

또한 java.lang 패키지는 암묵적으로 import되어 있어, 소스 코드에 java.lang.Cloneable이라고 쓰지 않고 간단히 Cloneable이라고 쓸 수 있다.


Cloneable 인터페이스를 구현한 클래스의 인스턴스 ➝ 복사
Cloneable 인터페이스를 구현하지 않은 클래스의 인스턴스 ➝ CloneNotSupportedException 발생

clone 메서드는 어디에 정의되어 있을까?

java.lang.Object 클래스에 정의되어 있다. Object 클래스는 Java 클래스 계층의 최상위 클래스이므로 모든 클래스에서 clone 메서드를 상속하게 된다.


Cloneable이 요구하는 메서드는?

'Cloneable 인터페이스'라고 하면, 그 안에 clone 메서드가 선언된 것으로 생각하기 쉽지만, Cloneable 인터페이스에는 메서드가 하나도 선언되어 있지 않다. 이 인터페이스는 단순히 'clone 메서드로 복제를 허용한다'는 의도적인 표시로 사용된다. 이런 표시를 하는 인터페이스를 마커 인터페이스(marker interface)라고 부른다.

 

Java 표준 라이브러리에서 Cloneable이 마커 인터페이스인 것을 바꿀 수 없지만, 원래는 Cloneable 안에서 clone을 선언하도록 규정해야 했을 것이다. 그러면 CloneNotSupportedException 예외도 필요 없다.


clone 메서드는 얕은 복사를 한다

clone 메서드의 동작은 필드 내용을 그대로 복사하는 것이다. 즉, 필드가 가리키는 인스턴스의 내용까지는 고려하지 않는다.

예를 들어, 필드에 배열이 있다고 가정하자. clone 메서드로 복사할 경우, 그 배열에 대한 참조만 복사되고 배열 요소가 하나하나 복사되진 않는다.

이러한 필드 대 필드 복사(field-for-field copy)를 '얕은 복사(shallow copy)'라고 부른다.

 

clone의 얕은 복사만으로 충분하지 않다면, 클래스 설계자가 clone 메서드를 오버라이드해서 필요로 하는 '복사'를 정의할 수 있다(clone 메서드를 오버라이드한 경우에는 super.clone()으로 상위 클래스의 clone 메서드를 호출하는 것을 잊지 말라).

 

clone은 단지 복사만 하며, 생성자를 호출하는 것은 아니라는 점에 주의할 필요가 있다. 또, 인스턴스를 생성할 때 뭔가 특수한 초기화가 필요한 클래스에서는 clone 메서드 안에 처리를 기술할 필요가 있다.


얕은 복사와 깊은 복사

깊은 복사(Deep Copy)와 얕은 복사(Shallow Copy)는 객체를 복사하는 두 가지 주요 방법이다. 이 두 방법은 복사된 객체와 원본 객체 간의 관계를 어떻게 다루느냐에 따라서 정의된다.

 

얕은 복사 (Shallow Copy)

  • 정의: 얕은 복사는 객체를 복사할 때, 객체의 필드 값들만을 복사하는 방법이다. 복사된 객체는 원본 객체의 참조를 그대로 유지한다. 따라서 복사된 객체나 원본 객체 중 하나에서 필드 값이 변경되면, 다른 객체에도 영향을 미친다.

[예시]

class Person {
  String name;
  Address address;
}

// 얕은 복사
Person original = new Person();
original.name = "John";
original.address = new Address("123 Main St");

Person copy = (Person) original.clone();

 

위의 경우 `original`과 `copy`는 같은 `Address` 객체를 참조하게 된다.


깊은 복사 (Deep Copy)

  • 정의: 깊은 복사는 객체를 복사할 때, 객체의 필드 값뿐만 아니라 참조 객체들에 대해서도 새로운 인스턴스를 생성하여 복사하는 방법이다. 따라서 복사된 객체와 원본 객체는 서로 독립적인 객체가 된다.

 

[예시]

class Person {
  String name;
  Address address;
}

// 깊은 복사
Person original = new Person();
original.name = "John";
original.address = new Address("123 Main St");

Person copy = new Person();
copy.name = original.name;
copy.address = new Address(original.address.getStreet());

 

위의 경우 `original`과 `copy`는 각각 다른 `Address` 객체를 가지게 됩니다.

 


얕은 복사: 필드 값만 복사하며 참조 객체는 동일한 객체를 참조한다.
깊은 복사: 필드 값과 참조 객체까지 모두 복사하여, 원본과 복사본이 서로 독립적인 객체가 된다.

얕은 복사는 복사된 객체와 원본 객체 간의 연결을 유지하며, 깊은 복사는 두 객체를 완전히 분리한다. 선택은 상황에 따라 다르며, 특히 복사해야 하는 객체의 복잡도와 크기, 그리고 성능 등을 고려하여 결정해야 한다.

더 자세한 내용은 아래 블로그를 참고하자.

https://staticclass.tistory.com/77

 

01-4. [자바] clone(), 깊은복사, 얕은 복사 - Object 클래스

clone() 클론 하면 복제가 생각나기 마련인데 이 메서드 역시 자신을 복제하여 새로운 인스턴스를 생성한다👍 clone() 메서드를 오버라이딩 하려면 Cloneable을 구현해야한다. Cloneable인터페이스를 구

staticclass.tistory.com

반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...