프로그래밍/디자인패턴

프로토타입 패턴(Prototype Pattern) 완벽 정리

Jinwookoh 2025. 12. 7. 18:14

"게임에서 몬스터 100마리를 소환해야 하는데, DB에서 몬스터 정보를 100번 조회하면 서버가 터집니다."

"복잡한 설정이 끝난 객체와 똑같은 쌍둥이를 하나 더 만들고 싶어요."

이럴 때 new 키워드로 처음부터 다시 만드는 대신, 이미 만들어진 객체를 원본(Prototype)으로 삼아 복제하는 것이 효율적입니다. 하지만 자바에서 제공하는 기본 복사 기능을 잘못 썼다간 대참사가 일어납니다.


1. 방식 1: 자바 기본 clone() (Shallow Copy) - "위험함"

자바의 모든 객체는 Cloneable 인터페이스를 구현하고 clone() 메서드를 오버라이딩하면 복제가 가능합니다. 하지만 이건 기본적으로 얕은 복사입니다.

치명적 문제점 (참조 공유)

객체 안에 있는 리스트(List)나 다른 객체까지 새로 복사되는 게 아니라, 주소값만 복사됩니다. 즉, 복제본의 리스트를 수정하면 원본의 리스트도 같이 바뀝니다.

코드 예시

Java
 
// 1. Cloneable 구현
class Monster implements Cloneable {
    String name;
    List<String> items; // 참조 타입

    public Monster(String name, List<String> items) {
        this.name = name;
        this.items = items;
    }

    @Override
    protected Monster clone() throws CloneNotSupportedException {
        // 기본 제공되는 clone 사용 (얕은 복사)
        return (Monster) super.clone();
    }
}

// 2. 대참사 시나리오
Monster original = new Monster("Orc", new ArrayList<>(Arrays.asList("Sword")));
Monster clone = original.clone(); // 복제

// 복제본의 아이템을 지웠는데...
clone.items.clear(); 

// 원본의 아이템도 사라짐! (주소를 공유하니까)
System.out.println(original.items); // [] (빈 리스트 출력됨)

주의: 실무에서는 Cloneable 인터페이스 구현을 권장하지 않는 경우가 많습니다. (CloneNotSupportedException 처리의 번거로움 + 얕은 복사 문제)


2. 방식 2: 카피 생성자 / 팩토리 (Deep Copy) - "안전함"

가장 권장되는 방식입니다. 자바의 clone() 매커니즘에 의존하지 않고, 직접 생성자를 하나 더 만들어서 모든 필드를 완벽하게 새로 만드는 방식입니다.

특징

  • 깊은 복사(Deep Copy): 내부의 리스트나 객체도 new를 통해 새로 만들어 담습니다. 원본과 복제본이 완벽하게 남남이 됩니다.

코드 예시

Java
 
class Monster {
    String name;
    List<String> items;

    public Monster(String name, List<String> items) {
        this.name = name;
        this.items = items;
    }

    // 카피 생성자 (Copy Constructor)
    public Monster(Monster target) {
        this.name = target.name;
        // 리스트를 새로 생성해서 담음 (핵심!)
        this.items = new ArrayList<>(target.items); 
    }
    
    // 혹은 팩토리 메서드 제공
    public Monster copy() {
        return new Monster(this);
    }
}

// 사용
Monster original = new Monster("Orc", new ArrayList<>(Arrays.asList("Sword")));
Monster clone = new Monster(original); // 혹은 original.copy();

clone.items.clear(); // 복제본만 지워짐
System.out.println(original.items); // ["Sword"] (원본 안전함)

Lombok 활용 팁 (@Builder)

Lombok을 쓴다면 toBuilder = true 옵션이 프로토타입 패턴과 유사한 효과를 냅니다.

Java
 
@Builder(toBuilder = true)
class User { ... }

User original = ...;
User clone = original.toBuilder().build(); // 값 복사 후 새 객체 생성

3. 실무 예시: 언제 쓰이나요?

1) java.util.ArrayList

자바의 컬렉션 프레임워크는 대부분 카피 생성자를 제공합니다.

Java
 
List<String> original = new ArrayList<>();
// 프로토타입 패턴의 실전 예시 (Deep Copy를 위해 새 리스트 생성)
List<String> clone = new ArrayList<>(original); 

2) ModelMapper / MapStruct

DTO와 Entity 간 변환을 할 때, 필드 값을 일일이 set 하지 않고 객체의 내용을 복사해서 새 객체를 만드는 라이브러리들도 넓은 의미의 프로토타입 패턴 활용입니다.


요약: clone() 쓸까요?

구분 Java Native clone() Copy Constructor / Factory
구현 방법 implements Cloneable + super.clone() public Class(Class target)
복사 방식 Shallow Copy (기본) Deep Copy (개발자가 직접 구현)
안전성 낮음 (참조 공유 문제 발생) 높음 (완벽한 분리)
추천 여부 비추천 (Effective Java에서도 비추천) 강력 추천

결론

"객체를 복사해야겠다"는 생각이 들면, 자바의 clone() 메서드는 잊어버리세요.

대신 **카피 생성자(Copy Constructor)**를 만들거나 별도의 copy() 메서드를 직접 구현하여 내부의 리스트나 객체까지 꼼꼼하게 new로 새로 생성해주는 것이 버그 없는 프로토타입 패턴의 정석입니다.