클래스의 인스턴스를 만들고자 할 때 new라는 Java 언어의 키워드를 사용해서 클래스 이름을 지정하고 인스턴스를 생성한다.
new ClassName();
new를 사용해 인스턴스를 만들 때는 클래스 이름을 반드시 지정해야만 한다.
- 클래스 이름을 지정하지 않고 인스턴스를 생성하고 싶을 때도 있다.
클래스에서 인스턴스를 생성하는 대신 인스턴스로부터 다른 인스턴스를 생성하는 Prototype 패턴
- 인스턴스로부터 다른 인스턴스를 생성하는 것은 복사기로 문서를 복사하는 것과 비슷하다.
- 원본 서류를 어떻게 만들었는지 모르더라도 복사기에 넣으면 같은 서류를 몇 장이든 만들 수 있다.
- ‘원형’이 되는 인스턴스를 바탕으로 새로운 인스턴스를 만든다.
- Java에서는 복제하는 조작을 ‘clone’이라고 부른다.
- Java 언어의 clone 메서드와 Cloneable 인터페이스 사용법도 학습해 보자
문자열을 테두리로 감싸서 표시하거나 밑줄을 그어 표시하는 프로그램
패키지 | 이름 | 설명 |
framework | Product | 추상 메서드 use와 createCopy가 선언되어 있는 인터페이스 |
framework | Manager | createCopy를 사용하여 인스턴스를 복제하는 클래스 |
이름 없음 | MessageBox | 문자열을 테두리로 감싸서 표시하는 클래스로 use와 createCopy를 구현 |
이름 없음 | Underlinepen | 문자열에 밑줄을 그어 표시하는 클래스로 use와 createCopy를 구현 |
이름 없음 | Main | 동작 테스트용 클래스 |
Product 인터페이스 ,Manager 클래스는 framework 패키지에 속하며 인스턴스를 복제한다.
- MessageBox 클래스나 UnderlinePen 클래스의 이름이 전혀 나오지 않는다.
- 이름이 전혀 나오지 않는다 → 독립적으로 수정할 수 있다.
소스 코드 안에 클래스 이름을 기술하면 그 클래스와 밀접한 관계가 생겨 버린다.
Prototype(원형) 역 - Product 인터페이스
인스턴스를 복사하여 새로운 인스턴스를 만들기 위한 메서드를 결정합니다.
java.lang.Cloneable 인터페이스를 상속한 Product 인터페이스는 복제가 가능하게 한다.
- 이 인터페이스를 구현하는 클래스의 인스턴스는 clone 메서드를 사용해서 자동으로 복제
package Prototype.framework;
public interface Product extends Cloneable{
public abstract void use(String s);
public abstract Product createCopy();
}
use 메서드의 ‘사용’이 무엇을 의하는지는 하위 클래스의 구현에 맡겨져 있다.
- createCopy는 인스턴스를 복제하기 위한 메서드
Client(이용자) 역 - Manager 클래스
인스턴스를 복하나는 메서드를 이용해 새로운 인스턴스를 만든다.
Product 인터페이스를 이용해 인스턴스를 복제하는 클래스
- createCopy를 호출하지만 구체적으로 어느 클래스의 인스턴스를 복제할지 관여하지 않는다.
- Product 인터페이스를 구현한 클래스이기만 하면 해당 인스턴스를 복제할 수 있다.
- 구체적인 개별 클래스 이름을 사용하지 않고 Product라는 인터페이스 이름만 사용한다. 이 인터페이스 만이 Manager 클래스와 다른 클래스를 연결하는 다리가 된다.
package Prototype.framework;
import java.util.HashMap;
import java.util.Map;
public class Manager {
public Map<String,Product> showcase = new HashMap<>();
// String으로 나타낸 이름과 Product 인터페이스를 구현한 클래스의 인스턴스와 대응 관계 Map으로 표현
public void register(String name, Product prototype){
showcase.put(name, prototype); //showcase 필드에 이름과 인스터스 쌍을 등록
}
public Product create(String prototypeName){
Product p = showcase.get(prototypeName);
//지정한 이름(key)에 대응하는 인스턴스를 얻을 수 있다.
return p.createCopy();
}
}
register 메서드의 파라미터 (Product prototype)는 Product 인터페이스를 구현한 클래스의 인스턴스를 말한다. (use, createCopy 메서드를 호출할 수 있는 인스턴스)
ConcretePrototype(구체적인 원형) 역 - MessageBox, UnderlinePen 클래스
- 인스턴스를 복사하여 새로운 인스턴스를 만드는 메서드를 구현한다.
MessageBox, Underlinepen 클래스는 모두 Product 인터페이스를 구현한 클래스이다.
- 이 인스턴스를 만들어서 Manager 클래스에 등록해 두면 원하는 대로 복제할 수 있다.
MessageBox 클래스
Product 인터페이스를 구현(implements)한 클래스
decochar가 ‘*’일 때 ‘Hello’라는 문자열을 use 메서드에 주면 다음과 같이 표시한다.
********
* Hello *
********
createCopy는 자기 자신을 복제하는 메서드이다.
- 여기서 호출하는 clone 메서드는 java 언어 사양으로 규정되어 있으며, 자기 자신의 복제를 생성하는 메서드
- 복제를 생성할 때 인스턴스가 가진 필드 값도 그대로 새 인스턴스에 복사된다.
clone 메서드로 복사할 수 있는 것은 java.lang.cloneable 인터페이스를 구현한 클래스뿐이다.
(이 인터페이스를 구현하지 않은 경우에는 CloneNotSupportedException 예외가 발생)-> try-catch
여기서 java.lang.cloneable 인터페이스는 Product 인터페이스가 extends 했고 단순한 표시로 이용될 뿐, 이 인터페이스가 선언하는 메서드는 없다.
package Prototype;
import Prototype.framework.Product;
public class MessageBox implements Product {
private char decochar;
public MessageBox(char decochar) {
this.decochar = decochar;
}
@Override
public void use(String s) {
int decolen = 1 + s.length() + 1;
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;
}
}
- Java 언어의 clone 메서드는 자신의 클래스(및 하위 클래스)에서만 호출할 수 있으므로, 다른 클래스의 요청으로 복제할 경우 createCopy와 같은 별도의 메소드로 clone을 감싸 주어야 한다.
UnderlinePen 클래스
MessageBox와 거의 같은 동작을 하는데, ulchar라는 필드가 ‘밑줄’로 사용된다.
- ulchar = ‘-’ 일 때 “Hello”라는 문자열을 use 메서드에 주면
Hello
-----
밑줄을 그어 Hello를 표시한다.
package Prototype;
import Prototype.framework.Product;
public class UnderlinePen implements Product {
private char ulchar;
public UnderlinePen(char ulchar){
this.ulchar = ulchar;
}
@Override
public void use(String s) {
int ulen = s.length();
System.out.println(s);
for (int i = 0; i < ulen; i++){
System.out.println(ulchar);
}
System.out.println();
}
@Override
public Product createCopy() {
Product p = null;
try {
p = (Product) clone();
} catch (CloneNotSupportedException e){
e.printStackTrace();
}
return p;
}
}
Main 클래스
Manager의 인스턴스를 만들고 Manager의 인스턴스에 UnderlinePen의 인스턴스와 MessageBox의 인스턴스에 이름을 붙여 등록한다.
이름 | 클래스 및 인스턴스 내용 |
“strong message” | UnderlinePen에서 ulchar = ‘-’ |
“warning box” | MessageBox에서 decochar = ‘*’ |
“slash box” | MessageBox에서 decochar = ‘/’ |
package Prototype;
import Prototype.framework.Manager;
import Prototype.framework.Product;
public class ProtoMain {
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. -> p1.use
-------------
***************
*Hello, world.* -> p2.use
***************
///////////////
/Hello, world./ -> p3.use
///////////////
*/
클래스 다이어그램
정리
클래스에서 인스턴스를 만들면 안되는 것일까?
new Something();
어떤 경우에 클래스로부터 인스턴스를 만드는 대신 인스턴를 복사해서 새 인스턴스를 만들까?
1. 종류가 너무 많아 클래스로 정리할 수 없는 경우
- 첫 번째는 취급할 오브젝트 종류가 너무 많아서, 하나하나 다른 클래스로 만들면 소스 파일을 많이 잘성해야 하는 경우이다.
예제에서는 3가지 원형이 등장했다.
- '-'를 사용하여 문자열에 밑줄을 긋는 것
- '*'를 사용하여 문자열에 테두리를 그리는 것
- '/'를 사용하여 문자열에 테두리를 그리는 것
더 많은 종류의 원형을 만들 수 있다. 원형을 모두 개별 클래스로 만들어 버리면, 클래스 수가 너무 많아져서 소스 프로그램을 관리하기 어렵다.
2. 클래스로부터 인스턴스 생성이 어려운 경우
- 두 번째는 생성하고 싶은 인스턴스가 복잡한 과정을 거쳐 만들어지는 것으로, 클래스로부터 만들기가 매우 어려운 경우이다.
- 그래픽 에디터 등에서 사용자가 마우스로 그린 도형을 나타내는 인스턴스를 프로그래밍해서 만들기는 어렵다.
- 사용자 조작으로 만들어진 인스턴스와 같은 것을 다시 만들고 싶은 경우에는 지금 만든 인스턴스를 일단 저장해 두고 만들고 싶을 때 그것을 복사한다.
- 클래스를 사용하는 것 보다 인스턴스를 복사해서 만드는 편이 간단하다.
3. 프레임워크와 생성하는 인스턴스를 분리하고 싶은 경우
- 세 번째는 인스턴스를 생성하는 프레임워크를 특정 클래스에 의존하지 않게 하고 싶은 경우
- 클래스 이름을 지정해서 인스턴스를 만드는 것이 아니라, 미리 ‘원형’이 될 인스턴스를 등록해 두고, 등록된 인스턴스를 복사해서 인스턴스를 생성한다.
- 예제에서는 인스턴스를 복제(clone)하는 부분을 framework 패키지 안에 두고 있다.
- Manager 클래스의 create 메서드에는 클래스 이름 대신 “strong message” , “slash box”라는 문자열을 인스턴스 생성을 위한 이름으로 부여한다.
Java 언어가 갖춘 인스턴스 생성 메커니즘인 new Something()형식을 더욱 일반화하여 클래스 이름의 속박으로부터 프레임워크를 분리했다고 할 수 있다.
클래스 이름은 속박인가?
소스 코드 안에 사용할 클래스 이름을 쓰는것은 당연한 일이 아닐까?
- 객체지향 프로그래밍의 목표 중 하나가 ‘부품으로서의 재사용’이라는 점을 다시 한번 상기하자
소스 코드 안에 이용할 클래스 이름이 쓰여 있으면, 그 클래스와 분리해서 재사용할 수 없게 된다.
- 클래스 이름은 변경할 수 있지만 ‘부품으로서의 재사용’에서는 소스 코드를 수정하는 것은 고려하지 않는다.
Java에서는 클래스 파일(.class)만 있어도 그 클래스가 재사용할 수 있는지가 중요하다.
- 소스 파일(.java)이 없어도 재사용할 수 있느냐가 포인트이다.
추가로 참고글
자바 .java와 .class 파일 차이점 정리
자바 프로그램을 작성할 때 사용하는 파일은 .java 파일인데 실제 구동할 때는 .class 파일을 가지고 사용하게 된다 자바는 .java 파일에서 텍스트로 소드 코드를 작성한 후에 저장하면 자바 컴파일
wakestand.tistory.com
밀접하게 결합해야 하는 클래스 이름이 소스 안에서 사용되는 것은 당연하고 문제가 없지만, 부품으로 독립
시켜야 하는 클래스 이름이 쏘스 안에서 사용되는 것은 문제가 된다.
추가 내용
clone 메서드와 java.lang.cloneable 인터페이스
Java 언어에서는 인스턴스를 복사하는 장치로 clone 메서드가 준비되어 있다.
- clone 메서드를 실행할 경우 복사 대상이 되는 클래스는 java.lang.Cloneable 인터페이스를 구현해야 한다.
- 복사 대상이 되는 클래스가 직접 구현해도 되고 상위 클래스 어딘가에서 구현해도 된다.
- 또한 Cloneable 인터페이스의 하위 인터페이스를 구현해도 상관없다. (예제 Product인터페이스)
- Cloneable 인터페이스를 구현한 클래스의 인스턴스는 clone 메서드를 호출하면 복사된다.
- clone 메서드의 반환값은 복사로 만들어진 인스턴스가 된다.
- (내부에서 하는 일은 원본 인스턴스와 같은 크기의 메모리를 확보하고 원본 인스턴스의 필드 내용을 복사하는 것이다.)
- clone 메서드의 반환값은 복사로 만들어진 인스턴스가 된다.
CloneNotSupportedException(clone이 지원되지 않는 예외)이 발생?
Cloneable 인터페이스를 구현하지 않은 클래스의 인스턴스가 clone 메서드를 호출하면 예외가 발생한다.
- Cloneable 인터페이스를 구현한 클래스의 인스턴스
-> 복사된다.
- Cloneable 인터페이스를 구현하지 않은 클래스의 인스턴스
-> CloneNotSupportedException이 발생한다.
java.lang 패키지라서 암묵적으로 import되어 있어, 간단히 Cloneable이라고 쓸 수 있다.
모든 클래스에서 clone 메서드를 쓸 수 있는 이유?
Clone 메서드는 java.lang.Object 클래스에 정의되어 있어 Java 클래스의 계층의
최상위 클래스이므로 모든 클래스에서 clone 메서드를 상속하게 된다.
Cloneable이 요구하는 메서드?
Cloneable 인터페이스는 메서드가 하나도 선언되어 있지 않는다. 이 인터페이스는 단순히
"clone" 메서드로 복제를 허용한다는 의도적인 표시로 사용된다.
- 이런 표시를 하는 인터페이스를 마커 인터페이스(marker interface)라고 한다.
- Java 표준 라이브러리에서 Cloneable이 마커 인터페이스인 것은 바꿀 수 없지만, 원래는 Cloneable 안에서 clone을 선언하도록 규정해야 했을 것이다. 그렇게 되면 CloneNotSupportedException 예외도 필요 없어진다.
clone 메서드는 얕은 복사를 한다.
clone 메서드의 동작은 필드 내용을 그대로 복사하는 것이다.
- 필드가 가리키는 인스턴스의 내용까지는 고려하지 않는다.
- clone 메서드로 필드에 있는 배열을 복사할 경우, 그 배열에 대한 참조만 복사되고 배열 요소가 하나하나 복사되진 않는다.
필드 대 필드(field-for-field copy)를 얕은 복사(shallow copy)라고 부른다.
- clone 메서드가 하는 일은 ‘얕은 복사’이다.
clone의 얉은 복사만으로 충분하지 않다면?
클래스 설계자가 clone 메서드를 오버라이드해서 필요로 하는 ‘복사’를 정의할 수 있다.
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
- clone 메서드를 오버라이드한 경우에는 super.clone()으로 상위 클래스의 clone 메서드를 호출해야 한다.
clone은 단지 복사만 한다.
생성자를 호출하는 것은 아니라는 점에 주의해야 한다.
- 인스턴스를 생성할 때 뭔가 특수한 초기화가 필요한 클래스에서는 clone 메서드 안에 처리를 기술할 필요가 있다.
- Java API 레퍼런스에서 java.lang.Object 클래스의 clone 메서드 및 java.lang.cloneable 인터페이스 항목을 살펴보자.
clone은 사용하기 어렵다.
여러 종류의 필드를 가진 클래스의 인스턴스를 복제하기 위해 clone 메커니즘을 이용하는 것은 쉬운 일이 아니다.
- java.lang.Object 클래스의 clone 메서드가 protected로 지정되어 있어 상속 관계가 없는 클래스의 clone 메서드를 호출하기가 어렵기 때문이다.
- 실제 Java 프로그램으로 인스턴스를 복제하는 클래스를 설계할 경우, clone 메커니즘에 의존하지 않고 복사 생성자나 복사 팩토리를 사용하는 편이 좋다.
관련 패턴
Flyweight 패턴
Memento 패턴
Composite 패턴 및 Decorator 패턴
Command 패턴
'책 > Java 언어로 배우는 디자인 패턴 입문' 카테고리의 다른 글
[디자인 패턴] Singleton패턴 (0) | 2023.03.11 |
---|---|
[디자인 패턴] Factory Method패턴 (0) | 2023.03.11 |
[디자인 패턴] Template Method패턴 (0) | 2023.03.11 |
[디자인 패턴] Adapter패턴 (0) | 2023.03.11 |
[디자인 패턴] Iterator패턴 (0) | 2023.02.10 |