프로토타입 패턴 (Prototype Pattern)
- 기존의 인스턴스를 복제하여 새로운 인스턴스를 만드는 방법이다.
- 클래스에 의존하지 않으면서도, 기존 객체를 복제하게 해준다.
- 실제로 복제되는 객체에 복제 프로세스를 위임한다.
- 복제를 지원하는 객체를
프로토타입
이라고 하기도 한다.
프로토타입 패턴이 해결하는 문제
객체 복사는 쉬워보이는데 왜 프로토타입 패턴이 필요할까?
- 비공개 필드가 있는 경우 생각보다 쉽지 않다.
- 클라이언트 입장에서는 인터페이스만 알고, 구현체에 대해 모를 수도 있어서 생각보다 복사하기 까다로울 수 있다.
- 많은 상속을 통해 다양한 필드를 늘려온 객체의 경우 필드 값을 복제하는 일이 생각보다 쉽지 않다.
- 프로토타입 레지스트리를 사용하면, 설정이 복잡하지만 자주 사용되는 객체를 어디 저장해두고 계속 복사해서 사용할 수도 있다.
다이어그램으로 살펴보기
현실 요구사항 살펴보기
- 메이플스토리와 비슷한 게임을 만들고 있다.
- 필드에 붉은 돼지를 대량으로 출몰시켜야 한다.
- 그리고 가끔 보스 붉은 돼지도 출몰시켜야 한다.
@AllArgsConstructor
public class Field {
private final String name;
private final int height;
private final int width;
}
@AllArgsConstructor
@Builder
public class Monster {
private final Field appearIn;
private final String name;
private final int exp;
private final int hp;
private final String species;
private final int speed;
private final int power;
private final int defense;
private final int size;
}
public class App {
public static void main(String[] args) {
Field field10 = new Field("들판", 500, 2400);
Monster redPig = Monster.builder()
.name("붉은 돼지")
.hp(100)
.defense(5)
.exp(10)
.power(10)
.species("돼지 종족")
.speed(10)
.size(5)
.appearIn(field10)
.build();
// TODO: 붉은 돼지를 대량으로 출몰시켜야 한다..
// TODO: 매번 위의 빌드 과정을 다 거치기보다, .clone() 메서드 한번으로 복사할 수는 없을까?
// TODO: 모든 붉은 돼지는 독립적인 객체여야 하니 clone != redPig 가 true 여야 한다.
// TODO: 그러나 내용만 보면 같은 객체기 때문에 clone.equals(redPig) 도 true 가 나와야 한다.
}
}
- 필드와 몬스터 객체를 구현하고 한마리의 붉은 돼지를 출현시키는 코드를 짰다.
- 이 붉은 돼지를 쉽게 복사해서 다른 곳에 뿌릴 수 없을까?
Object 의 clone()
메서드를 이용해 프로토타입 패턴 구현해보기
- 직접 인터페이스를 만들고 구현하지 않아도
Object.clone()
메서드를 통해서 프로토타입 패턴을 구현할 수 있다. clone()
은 자바의Object
가 기본으로 제공하는 메서드 중 하나이다.protected
접근자를 가지고 있는데, 사실상 모든 자바 객체는Object
를 상속하니 모든 객체에서 사용이 가능하다.- 단,
Cloneable
이라는 인터페이스를 상속하고 구현해야 사용 가능하다. 믹스인을 의도한 메서드이다. - clone() 을 오버라이드할 때의 주의점 도 있다.
- 사실 믹스인 인터페이스 를 의도했는데, 설계미스로 상속 메서드가 되어버렸다.
Monster
를 Cloneable
하게 변경
@AllArgsConstructor
@Builder
public class Monster implements Cloneable{
private final Field appearIn;
private final String name;
private final int exp;
private final int hp;
private final String species;
private final int speed;
private final int power;
private final int defense;
private final int size;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Monster monster = (Monster) o;
return exp == monster.exp && hp == monster.hp && speed == monster.speed && power == monster.power && defense == monster.defense && size == monster.size && Objects.equals(appearIn, monster.appearIn) && Objects.equals(name, monster.name) && Objects.equals(species, monster.species);
}
@Override
public int hashCode() {
return Objects.hash(appearIn, name, exp, hp, species, speed, power, defense, size);
}
}
equals()
와hashCode()
는 IDE 에서 만들어주는 그대로 생성했다.
App
에서 테스트
public class App {
public static void main(String[] args) throws CloneNotSupportedException {
Field field10 = new Field("들판", 500, 2400);
Monster redPig = Monster.builder()
.name("붉은 돼지")
.hp(100)
.defense(5)
.exp(10)
.power(10)
.species("돼지 종족")
.speed(10)
.size(5)
.appearIn(field10)
.build();
Monster cloneRedPig = (Monster) redPig.clone();
System.out.println(cloneRedPig != redPig); // true
System.out.println(cloneRedPig.equals(redPig)); // true
System.out.println(cloneRedPig.getClass() == redPig.getClass()); // true
}
}
- 대략 구현이 완료된 것 같다.
clone()
에는 사실 치명적인 약점이 있다.deep copy
가 아닌shallow copy
를 지원한다는 것이다.- 객체 안의 객체가 가진 필드는 복사되지 않는다.
deep copy
확인해보기
public class Item {
private final String name;
private final String description;
}
- 붉은 돼지에 아이템 드롭 기능을 만드려고 한다.
- 모든 아이템은 참조가 아니라 고유의 객체여야 한다.
- 같은 주소값을 가진 아이템을 2명의 유저가 가지고 있는 것은 말이 안된다.
- 모든 유저의 인벤토리는 독립적이어야 한다.
public class Monster implements Cloneable{
// ...
private final Item dropItem;
// ...
}
dropItem
이라는 필드를 추가했다.
public class App {
public static void main(String[] args) throws CloneNotSupportedException {
Field field10 = new Field("들판", 500, 2400);
Item redPigHeart = Item.builder()
.name("붉은 돼지의 심장")
.description("붉은 돼지의 심장이다.")
.build();
Monster redPig = Monster.builder()
.name("붉은 돼지")
.hp(100)
.defense(5)
.exp(10)
.power(10)
.species("돼지 종족")
.speed(10)
.size(5)
.appearIn(field10)
.dropItem(redPigHeart)
.build();
Monster cloneRedPig = (Monster) redPig.clone();
System.out.println(cloneRedPig.getDropItem() == redPig.getDropItem()); // true
}
}
clonedRedPig
의dropItem
과redPig
의dropItem
의 참조 값이 같다.- 아이템은 각각 독립적인 것이라 참조 값이 같으면 안 된다.
deep copy
를 위해 clone()
메서드 재구현하기
public class Item implements Cloneable {
private final String name;
private final String description;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
- 먼저
Item
클래스에도clone()
메서드를 구현해주었다.
@Override
protected Object clone() throws CloneNotSupportedException {
Object clone = super.clone();
Monster clonedMonster = (Monster) clone;
if (this.dropItem != null) {
clonedMonster.dropItem = (Item) this.dropItem.clone();
}
return clone;
}
Monster
클래스 내부에 있는clone()
메서드도 재구현했다.dropItem
으로 설정된 것이 있다면, 해당dropItem
을clone()
하여 할당한다.
프로토타입 패턴의 장점
- 복잡한 객체를 만드는 과정을 숨길 수 있다.
clone()
메서드 안에 숨겨놓았다.- 팩토리 메서드 패턴에서는 이 구현을 구체 클래스에게 떠넘겼었다.
- 기존 객체를 복제하는 과정이 새 인스턴스를 만드는 것보다 비용(시간 혹은 메모리)적인 면에서 효율적일 수 있다.
- 미리 만들어진 프로토타입들을 복제하는 방법도 가능하다.
- 구체 클래스에 의존하지 않고, 객체를 복제할 수도 있다.
- 추상적인 타입을 반환할 수 있다.
- 복잡한 객체들에 대해 사전 설정을 처리할 때, 상속 대신 사용할 수도 있다.
프로토타입 패턴의 단점
- 객체를 복제하는 과정 자체가 복잡할 수 있다.
- 순환 참조가 있는 경우 그렇다.
- 간단한
deep copy
문제에서도 여러 코드를 추가해줘야 했듯이,clone()
메서드를 구현하는 과정 자체가 매우 복잡해질 수 있다.
자바와 스프링에서 패턴 찾아보기
ArrayList
에서 제공하는 clone()
메서드
public class JavaCollectionExample {
public static void main(String[] args) {
Student stu1 = new Student("stu1");
Student stu2 = new Student("stu2");
ArrayList<Student> students = new ArrayList<>();
students.add(stu1);
students.add(stu2);
ArrayList<Student> clone = (ArrayList<Student>) students.clone();
System.out.println(students == clone); // false
System.out.println(students.get(0) == clone.get(0)); // true
}
}
ArrayList
에서clone()
메서드를 지원한다.- 단점은
ArrayList
라는 구체 클래스에서 제공하는 것으로 인터페이스인List
에서 제공하는 것이 아니다.
- 단점은
- 보통 실무에서는
ArrayList
의 생성자를 이용해 복제한다.new ArrayList<>(students);
- 이 방식을 이용하면
List
인터페이스로 받아볼 수 있다.
ModelMapper
라이브러리
- 객체의 필드를 그대로 다른 객체로 옮겨줄 때 사용하는 라이브러리이다.
- 자바의 리플렉션을 이용해 구현한 라이브러리이다.
- 객체의 필드 정보를 복제해준다.
- 명확히 프로토타입 패턴이라고 말할 수는 없지만, 비슷한 역할을 한다.
레퍼런스
https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/dashboard
반응형
'Java > 자바 디자인 패턴' 카테고리의 다른 글
자바 디자인 패턴, 객체 생성 관련 패턴 (Object Creational Patterns) 이란? (0) | 2023.02.17 |
---|---|
어댑터 패턴 (Adapter Pattern) 이란? (1) | 2023.01.29 |
빌더 패턴 (Builder Pattern) 이란? (0) | 2023.01.26 |
추상 팩토리 패턴 (Abstract Factory Pattern) 이란? (0) | 2023.01.24 |
팩토리 메서드 패턴 (Factory Method Pattern) 이란? (0) | 2023.01.23 |