일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- Easy
- SpringBoot 2
- R
- hash table
- heroku
- 자바입력
- 카데인 알고리즘
- 사칙연산
- array
- 자바 스레드 실행 순서 제어
- JAVA11
- 수학
- Kadane's Algorithm
- 자바 thread 실행 순서 제어
- scanner
- input
- Today
- Total
DeFacto-Standard IT
Prototype Pattern / Cloneable Interface 본문
Prototype Pattern / Cloneable Interface
defacto standard 2018. 2. 15. 17:371. 가정
- A4전용 복사기를 만든다.
- 복사기는 내용이 적인 A4종이를 복사하여, 똑같은 내용이 출력된 A4용지를 만들어낸다.
2. Naive Code
- Paper
public class Paper {
private String content;
public Paper(String content) {
this.content = content;
}
public String getContent() {
return content;
}
public void show() {
System.out.println(content);
}
}
Paper 클래스이다. 종이 생성 시 Content를 저장하고, show()를 통해 종이에 적힌 내용을 출력한다.
- PhotoCopier
public class PhotoCopier {
public Paper copy(Paper paper) {
return new Paper(paper.getContent());
}
}
복사기 클래스이다. Paper 객체를 인자로 받아서 해당 객체의 내용을 얻어 새로운 객체의 내용으로 넘긴다.
- NaiveClient
public class NaiveClient {
public static void main(String args[]) {
PhotoCopier photoCopier = new PhotoCopier();
Paper prototypePaper = new Paper("protoype's content");
Paper copiedPaper = photoCopier.copy(prototypePaper);
prototypePaper.show();
copiedPaper.show();
System.out.println(prototypePaper.hashCode());
System.out.println(copiedPaper.hashCode());
}
}
prototypePaper는 원본이며, PhotoCopier를 사용해서 복사한 객체의 내용을 copiedPaper에 세팅한다.
실행 결과는 다음과 같다.
아래 숫자는 원본 종이와 복사본 종이 객체에 대한 해시코드로서, Java에서 서로 다른 객체는 서로 다른 해시코드를 갖는다.
따라서 서로 다른 객체가 같은 내용을 출력하므로, copiedPaper 객체는 prototypePaper의 복사본이라고 할 수 있다.
3. 문제점
위 코드는 객체를 복사하긴 하지만, 유지보수에는 적절하지 않다.
예를 들어서 다음과 같은 요구사항이 추가되었다고 가정하자.
- 종이에는 제목, 작성시간, 출력시간 등의 요소가 추가되어야 한다.
- 여러 종류의 종이에 대해 입/출력을 모두 지원할 수 있어야 한다.
* 사실, 현실에서 복사기의 입장에서, 종이에 적여있는 모든 내용은 모두 '그림'으로 취급하므로
Content에 제목, 작성시간등이 모두 포함되는 개념이기 때문에 적절하지 않은 가정이라고 생각할 수 있다.
위 가정은 단순히 패턴을 설명하기 위한 예시이므로, 넘어가도록 한다.
다른 예시로는 복사기가 아니더라도 다른 객체를 복사하는 경우(아이템 복사 등)를 생각하면 될 것이다.
첫 번째 요구사항을 충족시켜보자. Paper클래스에 Content에 더해서, Title, editTime, printTime 등이 추가되었다고 가정하자.
PhotoCopier 클래스는 새로운 객체를 만들어서 반환하는데, 위 3가지 추가적인 필드의 복사를 지원해야한다.
이는, 새로운 필드가 추가될 때 마다 수행되어야 한다.
즉, 새로운 기능이 추가되는데 있어서 PhotoCopier의 코드변경이 발생하므로 OCP를 위반한다.
이제 두 번째 요구사항을 충족시켜야 한다고 생각해보자.
만약 A4용지에 해당하는 Paper라는 종이 클래스 하나만을 지원한다고 해보자.
종이에는 마분지, A4용지 등 여러가지 종류가 있을 수 있다. 각각 재질과 크기도 다르고, 출력하는데 사용하는 잉크의 재료도 다를 것이다.
만약 한가지 종이만 지원한다면, 어떤 종이를 쓰냐에 따라 복사기 클래스의 내용은 변경되어야 한다.
4. 해결방안
첫 번째 요구사항을 충족시키기 위해서는, 필드가 추가될 때 마다, 추가된 필드가 존재하는 것을 인식하여 이를 자동으로 복사되게 하여야 한다. 말만 들어도 난이도가 좀 높아보인다.
그래서 자바에서는 객체의 복사에 대해 지원을 한다.
Cloneable 인터페이스를 상속받고, clone()메서드를 구현하면 된다.
clone()메서드의 내용은, 상위 클래스의 clone()메서드를 호출하면 된다.
즉, super.clone()을 수행하도록 구현하면 된다.
자바에서 최상위 클래스는 Object Class인데, 여기에서는 protected 메서드인 clone()이 정의되어 있다.
Cloneable 인터페이스를 구현하는 서브클래스에서 super.clone()메서드를 수행하면, 복제된 객체는 원본 객체와 같은 필드를 가지며, 값 또한 복사된다.
따라서, Cloneable인터페이스를 통해 clone()을 구현한다는 것은 생성자를 호출하지 않으면서 생성 및 복제를 수행할 수 있는 방법이다.
단, clone()을 통해 return을 할 때는 서브클래스의 타입으로 캐스팅하여 넘겨야 한다.
두 번째 요구사항을 충족시키기 위해서는, 현재 Paper라는 클래스를 A4전용으로만 쓰는 것이 아니라, '종이'라는 추상적인 개념으로 전환하고, 서브 클래스로 A4와 마분지 등을 정의하고 Paper 클래스를 상속시켜야 한다.
복사기와 구체적인 종류의 종이(A4, 마분지)의 결합도를 낮추어 어떤 종이를 쓰더라도 복사기 클래스의 변경은 일어나지 않을 것이다.
5. Solution Code
- Prototype
public abstract class Prototype implements Cloneable {
int value = 0;
public Prototype clone() throws CloneNotSupportedException {
return (Prototype) super.clone();
}
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
Protoype 타입에 대한 복사를 지원하는 Prototype 추상클래스이다. Caretaker에서 추상적 개념을 사용하게 함으로써,
구체 클래스(ConcretePrototype)와의 디커플링을 실현시킨다.
Cloneable 인터페이스를 구현하고, Object클래스의 clone()을 수행함으로써 객체의 복제를 수행한다.
clone()을 바로 사용할 수 있는 이유는 자바에서 최상위 클래스인 Object클래스에서 이를 구현해놓았기 때문이다.
굳이 새롭게 정의할 필요 없이 정의된 clone() 메서드를 사용하기 위해 super.clone()을 사용한다.
Value필드와 이에 대한 setter/getter는 객체가 복사된다는 것을 보여주기 위해 선언하였다.
- ConcretePrototype
public class ConcretePrototype extends Prototype {
@Override
public Prototype clone() throws CloneNotSupportedException {
return (ConcretePrototype) super.clone();
}
}
Prototype 클래스를 상속받는 구체적 요소이다. 예제에서는 A4, 마분지 등이 해당한다.
- Caretaker
public class Caretaker {
private Prototype prototype;
public Caretaker(Prototype prototype) {
this.prototype = prototype;
}
public Prototype copyPrototype() throws CloneNotSupportedException {
return (Prototype) prototype.clone();
}
}
Prototype 객체를 인자로 받아서 복사를 수행하는 클래스이다. 예제에서는 복사기에 해당한다. 복사는 지역 레퍼런스로 세팅된 Prototype 객체에게 역할을 위임한다.
- Client
public class Client {
public static void main(String args[]) throws CloneNotSupportedException {
Prototype prototype = new ConcretePrototype(); // 원본
Prototype copiedPrototype = null; // 복사본
// 원본 객체의 초기 value값 : 1
prototype.setValue(1);
// 원본(prototype) 객체를 복사하여 복사본(copiedPrototype) 객체를 생성
Caretaker caretaker = new Caretaker(prototype);
copiedPrototype = caretaker.copyPrototype();
// 원본 객체의 value값 : 3
prototype.setValue(3);
// 복사본 객체의 value값 : (원본 객체로부터 복사된 시점의 값) +1
copiedPrototype.setValue( copiedPrototype.getValue()+1 );
/* clone()이 서로 다른 객체를 만들어 주는 것을 검증 */
// 해시값이 다르므로 다른 객체이다.
System.out.println("prototype객체의 hashCode : " + ((ConcretePrototype)prototype).hashCode() ); // 원본
System.out.println("copiedPrototype객체의 hashCode : " + ((ConcretePrototype)copiedPrototype).hashCode() ); // 복사본
// 원본 객체의 value값 : 3
System.out.println("prototype객체의 value값 : " + prototype.getValue());
// prototype 객체의 value값이 1일때 복사된 객체. +1 되어 value값은 2
System.out.println("copiedPrototype객체의 value값 : " + copiedPrototype.getValue());
}
}
copiedPrototype은 Caretaker로부터 추상화된 Prototype객체를 바인딩 받아 이를 복사한 객체를 바라보는 Prototype 레퍼런스이다.
prototype은 ConcretePrototype이라는 실제 구현체에 대한 추상화된 레퍼런스이다.
Caretaker 클래스의 생성자에 이 추상화된 prototype 레퍼런스를 인자로 넘겨 Caretaker의 copyConcretePrototype을 실행 시켰을 때, 바인딩 된 구체 클래스의 인스턴스에 대한 메서드(clone())를 호출하게 한다.
그리고 이 copyConcretePrototype()은 copiedPrototype에 객체를 복사하여 넘겨주는 역할을 한다. 소스코드 상에서 볼 수 있듯이,
인자로 넘어온 prototype 레퍼런스의 clone()을 사용한다.
여기서 유념해야할 것은 객체를 '복사'한다는 것이다.
즉, Caretaker에서 자신이 가지고 있는 Prototype을 통해 객체를 생성하는 것을, new 키워드가 아니라 clone()메서드를 통해서 수행한다는 것이다.
그 외에 잡다한 코드(getter/setter/hashCode) 는 prototype 객체와 copiedPrototype가 다르다는 것을 보여주기 위한 코드이다.
1. 처음에 prototype 객체에 대한 value를 1로 설정한다.
2. 이 prototype객체를 Caretaker의 생성자로 넘긴다.
3. Caretaker에서 copyConcretePrototype()을 호출하여, 생성자에서 넘어온 prototype객체를 사용하여 clone()을 통해 객체를 복사하고,
이에 대한 결과를 copiedPrototype에 넣는다.
4. prototype 객체에 대한 value를 3으로 설정한다.
5. copiedPrototype 객체에 대한 value를 1 증가시킨다.
6. prototype, copiedPrototype객체에 대한 hashCode와 value를 출력한다.
수행 결과는 다음과 같다.
이는 일반적인 팩토리 메서드를 사용한다면, 해시코드의 값은 new 키워드를 사용하므로 당연히 다를 것이다.
하지만 Clone의 개념이 아니라 Create의 개념이므로
prototype 객체의 value값은 3,
copiedPrototype 객체의 value값은 1이 나올 것이다.
그 이유는, 팩토리 메서드는 기존의 객체와는 상관없는, 값이 완전히 새로운 객체를 생성하기 때문이다.
객체 생성 시 value의 기본값이 0이므로, +1을 하여 1이 나오는 것이다.
하지만 new로 아예 새로운 객체를 만든 것이 아니라, clone()을 통해 새로운 객체를 '복사하여' 만든 것이다.
따라서, 기존 객체의 value값이 1이므로 복사된 객체의 value값 역시 1이 된다.
이에 1을 더하여 1+1=2가 나온 것이다.
6. Prototype Pattern
Prototype Pattern은 생성패턴의 한 종류이다. Prototpye이란 '원판' 또는 '초판' 이라는 뜻을 가진다.
제품이 상용으로 풀리기 전 다듬어지기 전의 샘플, 또는 개선되기 전의 중간 세이브와 같은 느낌을 주는 제품을 '프로토 타입 제품'이라고 한다.
따라서 Prototype은 완전한 객체를 내놓기 보다는 어느정도 템플릿을 갖춘 객체를 뽑아내는 것을 의미한다.
비슷한 역할을 하는 패턴으로는 팩토리 메서드 패턴이 있다. 차이점은 다음과 같다.
Factory Method Pattern - new 키워드를 사용하여 '완전히 새로운 내용을 가지는 객체'를 만드는 패턴. 기존 객체와 완전히 다른 정보를 가질 확률이 많은 경우 사용
Prototype Pattern - Cloneable인터페이스의 clone()을 구현하여, 기존 객체를 복사한 내용을 가지는, 새로운 객체를 만드는 패턴. 기존 객체에서 값을 가져와서 추가적인 작업이나 비교하는 등, 비교적 많은 정보가 겹치는 경우 사용.
Solution Code에는 Prototype에 변수가 value 하나지만, 다른 예를 들어보자.
한 학교에 대한 학생이라는 클래스를 생각해본다면 이름, 나이, 성별, 학년, 학교 등의 여러가지 정보가 존재할 것인데, 이 중에서 학교라는 정보는 고정적일 것이다.
이러한 고정적인 정보에 대해서는 고정 값을 가지는 인스턴스(프로토 타입 제품)를 생성하고,
달라지는 부분(이름, 나이, 성별, 학년)을 가지는 인스턴스는 위에서 생성한 프로토 타입 제품을 복사하여 달라지는 부분만 수정하면 된다.
이러한 방식으로 코드를 작성하게 되면, new 연산자를 줄일 수 있게 된다
또한, 필드가 많아진다고 하더라도 이를 그대로 '복사'하기 때문에, 클래스에 대한 변경도 없다.
- UML
* 위 UML에서 Client는,
Solution Code에서 Caretaker로 설명하였다.
7. 장단점
'Design Pattern > Creational Pattern' 카테고리의 다른 글
Builder Pattern (0) | 2018.02.16 |
---|---|
Factory Method Pattern과 Abstract Factory Pattern 공통점과 차이점 (0) | 2017.09.26 |
Abstract Factory Pattern (0) | 2017.09.26 |
Factory Method Pattern (기본) (0) | 2017.09.26 |
Singleton Pattern (0) | 2017.09.23 |