2025-04-02 Prototype pattern

성현규·2025년 4월 2일
post-thumbnail

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) {
                // 1단계: Builder로 기본 캐릭터 생성
                Character warriorPrototype = new Character.CharacterBuilder()
                    .setName("기본 전사")
                    .setHealth(1)
                    .setDamage(10)
                    .build();
        
                // 2단계: 프로토타입 등록
                CharacterFactory.getInstance().register("warrior", warriorPrototype);
        
                // 3단계: 복사해서 커스터마이징
                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()); // 20
                System.out.println(myChar.getDamage()); // 10 -> 기본 능력치

                System.out.println(myChar2.getName()); // 나의 전사2
                System.out.println(myChar2.getHealth()); // 40
                System.out.println(myChar.getDamage()); // 10 -> 기본 능력치
            }
        }

class Character { // static을 사용하면 모든 객체가 같은 값을 공유하게된다. -> static 사용 금지
    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; // 빌더 패턴에서는 내부변수가 절대 static이면 안됨. -> 객체마다 커스텀 불가
        private  int health;
        private  int damage;

        public CharacterBuilder setName(String name) { // 객체를 생성하고 사용하기 때문에 static이 아님.
            if (name == null){  // null.equals가 될 수 있으므로 ==으로 비교한다. null은 메모리 주소랑 비교가능하다.
                throw new RuntimeException("케릭터의 이름은 필수 입력사항입니다.");
            }
            else {
                this.name = name; // this는 클래스를 의미하는 것이 아니라 현재 생성된 객체를 의미한다.
            }
            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);
    }

    //getter와 setter 사용 -> 복사된 개체를 내가 수정하더라도 개인정보는 지켜줘야 하니까...
    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<>(); // final을 참조를 고정함. -> 한 주소에 정보를 모을 수 있음.

    private class Holder {
        private static final CharacterFactory characterFactory = new CharacterFactory(); // 이러면 holder에 static된다.
    }
    
    public static CharacterFactory getInstance(){
        return Holder.characterFactory;
    }


    public void register(String name, Character prototype){ // 객체로 호출하면 되니까 statoic 쓸 필요가 없음. + 객체가 싱글턴이고 보호받고 있으므로 멀티 쓰레드에서도 안전함.
        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); // smartwarsher 프로토타입을 통해 복사본 생성 (물세탁 기능 기본포함)
        ww.addFeature("건조기능");
        ww.setModelName("super warsher");

        SmartWasher wwx = new SmartWasher(smartWasher); // smartwarsher 프로토타입을 통해 복사본 생성 (물세탁 기능 기본포함)
        wwx.addFeature("wwxspecial 기능");
        wwx.setModelName("ultra warsher");

        // 리스트를 그대로 출력해도 옆과 같이 출력이 된다.
        System.out.println(wwx.getFeatures()); // [물세탁기능, wwxspecial 기능]
        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);
    }

    // getter와 setter
    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(args);로 명시 호출해야 한다.
        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());
    }
}
profile
학생

0개의 댓글