"게임에서 몬스터 100마리를 소환해야 하는데, DB에서 몬스터 정보를 100번 조회하면 서버가 터집니다."
"복잡한 설정이 끝난 객체와 똑같은 쌍둥이를 하나 더 만들고 싶어요."
이럴 때 new 키워드로 처음부터 다시 만드는 대신, 이미 만들어진 객체를 원본(Prototype)으로 삼아 복제하는 것이 효율적입니다. 하지만 자바에서 제공하는 기본 복사 기능을 잘못 썼다간 대참사가 일어납니다.
1. 방식 1: 자바 기본 clone() (Shallow Copy) - "위험함"
자바의 모든 객체는 Cloneable 인터페이스를 구현하고 clone() 메서드를 오버라이딩하면 복제가 가능합니다. 하지만 이건 기본적으로 얕은 복사입니다.
치명적 문제점 (참조 공유)
객체 안에 있는 리스트(List)나 다른 객체까지 새로 복사되는 게 아니라, 주소값만 복사됩니다. 즉, 복제본의 리스트를 수정하면 원본의 리스트도 같이 바뀝니다.
코드 예시
// 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를 통해 새로 만들어 담습니다. 원본과 복제본이 완벽하게 남남이 됩니다.
코드 예시
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 옵션이 프로토타입 패턴과 유사한 효과를 냅니다.
@Builder(toBuilder = true)
class User { ... }
User original = ...;
User clone = original.toBuilder().build(); // 값 복사 후 새 객체 생성
3. 실무 예시: 언제 쓰이나요?
1) java.util.ArrayList
자바의 컬렉션 프레임워크는 대부분 카피 생성자를 제공합니다.
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로 새로 생성해주는 것이 버그 없는 프로토타입 패턴의 정석입니다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| 퍼사드 패턴(Facade Pattern) 완벽 정리 (1) | 2025.12.07 |
|---|---|
| 데코레이터 패턴(Decorator Pattern) 총정리 (0) | 2025.12.07 |
| 커맨드 패턴(Command Pattern) 총정리 (0) | 2025.12.07 |
| 책임 연쇄 패턴(Chain of Responsibility) 총정리 (0) | 2025.12.07 |
| 이터레이터 패턴(Iterator Pattern) 완벽 정리 (0) | 2025.12.07 |