프로토타입은 코드를 그들의 클래스의 의존하지 않고 기존 객체를 복사할 수 있도록하는 생성 패턴이다. 만약 복사하고 싶은 객체가 있다면 어떻할까? 먼저 같은 클래스의 새 객체를 생성해야 한다. 그 후 원본의 모든 필드를 복사본 객체에 복사해야한다. 하지만 원본의 필드가 private이면 해당 필드에 접근을 하지 못하기 때문에 복사가 불가능하다.
또한 객체의 복사본을 생성하려면 객체의 클래스를 알아야 함으로 해당 클래스에 의존성을 가지게 됩니다. 예를 들어 원본이 다형성을 이용한 인터페이스라면 해당 구현체를 복사본이 알아야 하는데 그럴 수 없게된다.
프로토타입 패턴은 실제로 복사되는 객체들에 복제 프로세스를 위임한다. 이 말은 복제를 지원하는 모든 객체에 대한 공통 인터페이스를 선언하여 코드를 객체의 클래스에 결합하지 않고도 해당 객체를 복사할 수 있게 한다는 의미이다. Java에서 제공하는 clone 메서드가 이러한 역할을 한다.
clone 메서드의 구현은 모든 클래스에서 유사한데 현재 클래스의 객체를 만든 후 이전 객체의 모든 필드를 새 객체로 전달한다. 이렇게 하면 비공개 필드도 접근이 가능하게 된다.
복제 메서드(clone)을 선언하며, 이 메서드들이 대부분은 단일 메서드이다.
Prototype을 구현한 구현체로, 복제 메서드를 구현한다. 원본 객체의 데이터를 복제본에 복사하는 것 외에도 복제 프로세스와 관련된 일부 로직을 처리할 수 있다.
프로토타입 인터페이스를 따르는 모든 클래스를 복제할 수 있다.
public interface Prototype {
Prototype clone();
}
public class ConcretePrototype implements Prototype {
private String field;
public ConcretePrototype(String field) {
this.field = field;
}
public ConcretePrototype(ConcretePrototype prototype) {
this.field = prototype.field;
}
@Override
public Prototype clone() {
return new ConcretePrototype(this);
}
@Override
public int hashCode() {
return Objects.hash(field);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
ConcretePrototype concretePrototype = (ConcretePrototype) obj;
return Objects.equals(field, concretePrototype.field);
}
}
//
@Test
void 프로토타입_패턴_테스트(){
Prototype prototype = new ConcretePrototype("field");
Prototype clone = prototype.clone();
Assertions.assertThat(prototype).isEqualTo(clone);
}
interface로 Prototype을 구현하여 clone 메서드를 구현체에서 재정의하도록 하였다. Concrete Prototype은 clone을 재정의하고 이 클래스를 매개변수로 받는 생성자를 반드시 정의해야지 clone이 가능하다.
equals와 hashcode를 재정의하여 원복 객체와 복사한 객체가 동일한 객체인지를 판별하도록 하였다.
public class Circle implements Cloneable {
private int x, y, r;
public Circle(int x, int y, int r) {
this.x = x;
this.y = y;
this.r = r;
}
public Circle(Circle circle) {
this.x = circle.x;
this.y = circle.y;
this.r = circle.r;
}
public Circle clone() {
return new Circle(this);
}
@Override
public int hashCode() {
return Objects.hash(x, y, r);
}
@Override
public boolean equals(Object obj) {
if(!(obj instanceof Circle circle) || obj.hashCode() != this.hashCode()) {
return false;
}
return circle.x == this.x && circle.y == this.y && circle.r == this.r;
}
}
//결과
@Test
void Cloneable_인터페이스_테스트(){
Circle circle = new Circle(1, 2, 3);
Circle clone = circle.clone();
Assertions.assertThat(circle).isEqualTo(clone);
}
java.lang.Cloneable을 이용하여 clone 메서드를 재정의하는 방법이 있다. 이를 이용해서 clone을 하기 위해서도 자신 클래스를 매개변수로 받는 생성자를 이용해야 한다.
프로토타입 패턴을 이용하여 복제할 객체의 클래스에 의존하지 않아도 된다는 장점이 있고 반복되는 초기화 코드를 미리 정의되어진 메서드를 이용하여 제거할 수 있다. 또한 복잡한 객체를 더 쉽게 생성할 수 있게 된다.
반면, 순환 참조가 있는 복잡한 객체들을 복제할 때는 clone 메서드를 재정의하는 부분이 매우 까다로울 수 있다.
프로토타입 패턴은 객체를 생성하는 비용이 크거나, 시스템 성능에 큰 영향을 미칠 수 있는 경우에 주로 사용됩니다. 특히, 원래의 객체를 생성하는데 많은 자원이 필요하거나 복잡한 계산 또는 데이터베이스 쿼리 같은 비용이 많이 드는 연산을 수행해야 할 때 유용합니다.
참고 코드
참고: https://refactoring.guru/ko/design-patterns/prototype