1. Prototype Pattern
- 같은 객체를 매번 생성자로 생성해야할때 객체를 복사하여 생성할 수 있도록 한다.
1.5 복사에서 기본형과 참조형 변수의 차이
| 구분 | 얕은 복사 (Shallow Copy) | 깊은 복사 (Deep Copy) |
|---|
| 정의 | 객체의 필드를 복사할 때, 참조형 필드는 주소만 복사 | 참조형 필드까지도 새로운 객체로 재귀적 복사 |
| 기본형 필드 | 값 자체를 복사 | 동일하게 값 복사 |
| 참조형 필드 | 주소만 복사 (공유) | 새 객체 생성해서 내용까지 복사 |
| 결과 | 복사본과 원본이 같은 객체 내부를 참조 | 복사본과 원본이 완전히 분리된 객체 |
| 안전성 | 빠르지만, 공유된 객체가 변경되면 함께 바뀔 위험 | 느리지만, 완전 독립된 객체 생성 가능 |
📌 1-1. 캐릭터 생성 시스템
🔍 핵심 개념 및 주의할 점
- 원래 clone()으로 하는 프로토타입 패턴이 일반적이나 실무에서는 "명시적이고 확장하기 좋기 때문"에 복제 생성자 방식을 선호한다. (예외도 없다.)
- 빌더 패턴과 프로토타입 패턴, 싱글턴 패턴을 조합하여 캐릭터 객체를 효율적으로 생성하고 복제한다.
- 빌더 클래스 내부의 필드는 static이 되면 안 되며, 객체별 커스터마이징을 위해 인스턴스 필드로 유지해야 한다.
- 복사 생성자를 활용하여 프로토타입 객체로부터 새로운 객체를 생성한다.
- null 체크 시
== 연산자를 사용하여 NPE를 방지한다.
- 공용 저장소는 싱글턴으로 구현해 인스턴스 공유 및 멀티스레드 안정성을 확보한다.
🧠 기억해야 할 패턴 또는 로직 흐름
- (1단계) 빌더 패턴을 통해 캐릭터의 기본 속성을 지정한 후 객체 생성
- (2단계) 생성된 캐릭터 객체를 팩토리에 프로토타입으로 등록
- (3단계) 프로토타입에서 복사 생성자를 통해 새로운 객체를 만들고 커스터마이징
- 싱글턴 패턴:
Holder 내부 클래스와 static final 필드를 이용하여 thread-safe한 단일 인스턴스 제공
- 프로토타입 패턴: 객체를 복제하여 새로운 인스턴스를 생성하되, 원본 객체는 수정하지 않음
💻 정답 코드 (Java)
public class Main {
public static void main(String[] args) {
Character warriorPrototype = new Character.CharacterBuilder()
.setName("기본 전사")
.setHealth(1)
.setDamage(10)
.build();
CharacterFactory.getInstance().register("warrior", warriorPrototype);
Character myChar = CharacterFactory.getInstance().create("warrior");
myChar.setName("나의 전사").setHealth(20);
Character myChar2 = CharacterFactory.getInstance().create("warrior");
myChar2.setName("나의 전사2").setHealth(40);
System.out.println(myChar.getName());
System.out.println(myChar.getHealth());
System.out.println(myChar.getDamage());
System.out.println(myChar2.getName());
System.out.println(myChar2.getHealth());
System.out.println(myChar.getDamage());
}
}
class Character {
private String name;
private int health;
private int damage;
private Character(CharacterBuilder builder){
this.name = builder.name;
this.health = builder.health;
this.damage = builder.damage;
}
public Character(Character original){
this.name = original.name;
this.health = original.health;
this.damage = original.damage;
}
public static class CharacterBuilder {
private String name;
private int health;
private int damage;
public CharacterBuilder setName(String name) {
if (name == null){
throw new RuntimeException("케릭터의 이름은 필수 입력사항입니다.");
}
else {
this.name = name;
}
return this;
}
public CharacterBuilder setHealth(int health) {
this.health = health;
return this;
}
public CharacterBuilder setDamage(int damage) {
this.damage = damage;
return this;
}
public Character build(){
return new Character(this);
}
}
public void attack(){
System.out.printf("\"%s\"가 %d의 체력과 %d의 공격력으로 공격을 시작합니다!\n", name, health, damage);
}
public String getName() {
return name;
}
public Character setName(String name) {
this.name = name;
return this;
}
public int getHealth() {
return health;
}
public Character setHealth(int health) {
this.health = health;
return this;
}
public int getDamage() {
return damage;
}
public Character setDamage(int damage) {
this.damage = damage;
return this;
}
}
class CharacterFactory {
private CharacterFactory(){}
private static final HashMap <String, Character> prototypes = new HashMap<>();
private class Holder {
private static final CharacterFactory characterFactory = new CharacterFactory();
}
public static CharacterFactory getInstance(){
return Holder.characterFactory;
}
public void register(String name, Character prototype){
prototypes.put(name, prototype);
}
public Character create(String name){
if (prototypes.containsKey(name)){
return new Character(prototypes.get(name));
}
else {
throw new RuntimeException("해당 케릭터의 prototype이 없습니다.");
}
}
}
📌 1-2. 스마트기기 생성 시스템
🔍 핵심 개념 및 주의할 점
- 자식 클래스의 생성자에서는 반드시
super() 또는 super(args)로 부모 생성자를 호출해야 한다.
super() 호출은 생성자의 첫 줄에 위치해야 하며, 생략 시 기본 생성자가 자동 호출된다.
- 부모 클래스에 기본 생성자가 없을 경우,
super(args)를 반드시 명시적으로 호출해야 한다.
- 복사 생성자에서는 부모 필드는 부모 클래스 생성자에서 복사 처리하도록
super(original)을 사용해야 한다.
- 공통 필드(
modelName, price, features)는 부모 클래스인 SmartAppliance에서 관리하며, 상속받은 클래스는 이를 활용한다.
features 필드는 List<String> 타입으로, 깊은 복사를 위해 new ArrayList<>(original.features)와 같이 복사 생성자에서 처리한다.
- 공통 기능(
addFeature, getFeatures, getModelName 등)은 부모 클래스에서 정의하며, 각 서브 클래스는 기능 특화 메서드(startWash, checkTemperature, setCoolingLevel)만 추가로 정의한다.
🧠 기억해야 할 패턴 또는 로직 흐름
- (1단계) 부모 클래스에서 공통 필드와 생성자, 공통 메서드를 정의한다.
- (2단계) 자식 클래스는
super() 또는 super(args)를 통해 부모 생성자를 먼저 호출한다.
- (3단계) 복사 생성자는 자식 클래스에서 정의하되, 부모 필드 복사는
super(original)을 통해 위임한다.
- (4단계) 객체 복사 후
addFeature, setModelName 등을 활용해 복사본 커스터마이징이 가능하다.
✅ 요점 주석 모음
super()는 부모 생성자를 호출하는 키워드이며, 생략 가능하지만 첫 줄에 위치해야 함
- 부모 클래스에 기본 생성자가 없으면, 반드시
super(args)로 명시적 호출 필요
- 복사 생성자에서는 부모 필드 복사를 위해
super(original) 호출 필요
features = new ArrayList<>(original.features); → 깊은 복사를 위한 처리
- 추상 클래스
SmartAppliance는 공통 속성 및 기능 제공 → 상속 구조의 기반 클래스
- 서브 클래스는 기능 특화 (
SmartWasher → startWash, SmartRefrigerator → checkTemperature, 등)
List<String> 타입의 features를 복사하고 조작하며, 개별 인스턴스마다 상태 분리 보장
System.out.println(인스턴스.getFeatures()) 호출 시, 해당 객체의 기능 목록 확인 가능
public class Main {
public static void main(String[] args) {
SmartWasher smartWasher = new SmartWasher("w1", 120);
SmartRefrigerator smartRefrigerator = new SmartRefrigerator("s1", 140);
smartWasher.addFeature("물세탁기능");
smartRefrigerator.addFeature("가열기능");
SmartWasher ww = new SmartWasher(smartWasher);
ww.addFeature("건조기능");
ww.setModelName("super warsher");
SmartWasher wwx = new SmartWasher(smartWasher);
wwx.addFeature("wwxspecial 기능");
wwx.setModelName("ultra warsher");
System.out.println(wwx.getFeatures());
System.out.println(ww.getFeatures());
System.out.println(smartWasher.getFeatures());
System.out.println(smartRefrigerator.getFeatures());
}
}
abstract class SmartAppliance {
private String modelName;
private int price;
private List <String> features = new ArrayList<>();
public SmartAppliance(String modelName, int price) {
this.modelName = modelName;
this.price = price;
}
SmartAppliance(SmartAppliance original){
this.modelName = original.modelName;
this.price = original.price;
this.features = new ArrayList<>(original.features);
}
abstract void showInfo();
public void addFeature(String feature) {
features.add(feature);
}
public String getModelName() {
return modelName;
}
public void setModelName(String modelName) {
this.modelName = modelName;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public List<String> getFeatures() {
return features;
}
public void setFeatures(List<String> features) {
this.features = features;
}
}
class SmartWasher extends SmartAppliance{
public void startWash(){
System.out.println("세탁을 시작합니다.");
}
SmartWasher(String modelName, int price){
super(modelName, price);
}
SmartWasher(SmartWasher original){
super(original);
}
@Override
void showInfo() {
System.out.printf("[모델명] %s [가격] %d\n", getModelName(), getPrice());
}
}
class SmartRefrigerator extends SmartAppliance{
public void checkTemperature(){
System.out.println("냉장 온도를 확인합니다.");
}
SmartRefrigerator(String modelName, int price){
super(modelName, price);
}
SmartRefrigerator(SmartRefrigerator original){
super(original);
}
@Override
void showInfo() {
System.out.printf("[모델명] %s [가격] %d\n", getModelName(), getPrice());
}
}
class SmartAirConditioner extends SmartAppliance{
public void setCoolingLevel(int level){
System.out.printf("냉방 세기를 %d단계로 설정합니다.\n", level);
}
SmartAirConditioner(String modelName, int price){
super(modelName, price);
}
SmartAirConditioner(SmartAirConditioner original){
super(original);
}
@Override
void showInfo() {
System.out.printf("[모델명] %s [가격] %d\n", getModelName(), getPrice());
}
}