18. 똑똑한 코드 패턴 (디자인 패턴)/18.1 싱글턴 패턴

18.1.2 싱글턴 구현하기 - 세상에 하나뿐인 특별한 객체 만들기

thejavascript4kids 2025. 7. 27. 05:56

📘 18.1.2 싱글턴 구현하기 - 세상에 하나뿐인 특별한 객체 만들기

어떤 것들은 세상에 하나만 있어야 할 때가 있습니다. 이전 시간에 싱글턴 패턴이 무엇인지 배웠으니, 이제 실제로 그 특별한 존재를 만들어볼 시간이에요. 바로 진짜로 싱글턴을 구현해보는 거예요!

마치 세상에 하나뿐인 특별한 보물상자를 직접 만드는 것처럼, 우리만의 독특한 클래스를 만들어보겠습니다. 이 상자는 아무리 많은 사람이 "새 상자 주세요!"라고 해도, 항상 같은 상자를 건네주는 특별한 상자가 될 거예요.

🧠 먼저 용어를 알아볼까요?

싱글턴 구현에 필요한 용어들을 먼저 차근차근 알아보겠습니다.

용어 의미
싱글턴 구현 딱 하나만 존재할 수 있는 특별한 클래스를 실제로 만드는 과정을 말해요
인스턴스 확인 이미 객체가 만들어져 있는지 확인하는 과정이에요
전역 접근점 프로그램 어디서든 같은 객체에 접근할 수 있게 해주는 특별한 방법이에요
보호된 생성자 다른 사람이 마음대로 새 객체를 만들지 못하게 보호하는 장치예요

✨ 싱글턴 구현의 핵심 개념

싱글턴을 구현한다는 것은 마치 세상에 하나뿐인 특별한 장난감을 만드는 공장을 설계하는 것과 같아요. 이 공장에는 아주 특별한 규칙이 있어요.

첫 번째 규칙은 "이미 장난감이 있는지 먼저 확인하기"예요. 누군가 "새 장난감 주세요!"라고 오면, 공장은 먼저 창고를 확인해요. 이미 만들어진 장난감이 있으면 새로 만들지 않고 그것을 줘요.

두 번째 규칙은 "특별한 창고에 보관하기"예요. 만들어진 장난감은 아무나 들어갈 수 없는 특별한 창고에 보관해요. 이 창고는 오직 공장 직원만 접근할 수 있답니다.

세 번째 규칙은 "정해진 방법으로만 받기"예요. 장난감을 받고 싶은 사람은 정해진 창구로만 와야 해요. 다른 방법으로는 장난감을 받을 수 없어요.

이렇게 세 가지 규칙을 지키면, 아무리 많은 사람이 와도 항상 같은 장난감을 받게 되는 특별한 공장이 완성돼요!

비유로 이해하기: 우리 반의 유일한 반장 뱃지

싱글턴 구현을 더 가까이에서 이해하기 위해 '우리 반의 유일한 반장 뱃지'에 비유해보겠습니다.

우리 반에는 반장 뱃지가 딱 하나만 있어요. 이 뱃지는 매우 특별해서, 아무리 많은 학생이 "저도 반장 뱃지 주세요!"라고 해도 새로 만들어지지 않아요.

뱃지 관리소(클래스)에는 특별한 규칙이 있어요:

  1. 뱃지 확인 규칙: 누군가 뱃지를 달라고 하면, 먼저 "이미 뱃지를 가진 사람이 있나요?"라고 확인해요. 있으면 그 사람을 찾아가라고 안내해주고, 없으면 새로 만들어줘요.

  2. 안전 보관 규칙: 뱃지는 특별한 금고에 보관되어 있어서, 아무나 마음대로 가져갈 수 없어요. 오직 담임선생님(클래스의 특별한 메서드)만 꺼낼 수 있어요.

  3. 정해진 절차 규칙: 뱃지를 받고 싶으면 반드시 "뱃지 관리소 창구"로 가야 해요. 다른 방법으로는 뱃지를 받을 수 없어요.

이렇게 하면 우리 반에는 항상 딱 하나의 반장 뱃지만 존재하고, 모든 학생이 같은 뱃지를 알 수 있게 되는 거예요!

🎯 싱글턴을 구현하는 이유

그렇다면 우리는 왜 이렇게 복잡한 규칙을 만들어서 싱글턴을 구현해야 할까요? 정말 중요한 이유들이 있어요.

첫째, 혼란을 방지할 수 있어요. 만약 게임에서 점수를 관리하는 객체가 여러 개 있다면, 어떤 객체가 진짜 점수를 가지고 있는지 헷갈리겠죠? 하나만 있으면 이런 문제가 생기지 않아요.

둘째, 메모리를 절약할 수 있어요. 큰 설정 파일을 읽어서 관리하는 객체가 있다면, 같은 것을 여러 번 만들 필요 없이 하나만 만들어서 계속 사용하면 컴퓨터 메모리를 아낄 수 있어요.

셋째, 모든 곳에서 같은 정보를 공유할 수 있어요. 웹사이트의 사용자 정보를 관리하는 객체가 하나만 있으면, 어디서든 같은 사용자 정보에 접근할 수 있어요.

마지막으로, 안전하게 관리할 수 있어요. 중요한 정보나 설정을 하나의 객체에서만 관리하면, 실수로 여러 곳에서 다르게 변경할 위험이 없어요.

⚙️ 싱글턴 구현의 기본 문법

이제 실제로 싱글턴을 만드는 코드를 배워보겠습니다.

기본 싱글턴 구조:

class MySingleton {
  // 1단계: 비밀 창고 만들기
  static instance;

  // 2단계: 객체 만들 때 확인하기
  constructor() {
    // 이미 객체가 있으면 그것을 주기
    if (MySingleton.instance) {
      return MySingleton.instance;
    }

    // 없으면 현재 객체를 보관하기
    MySingleton.instance = this;
  }

  // 3단계: 안전한 창구 만들기
  static getInstance() {
    // 객체가 없으면 새로 만들기
    if (!MySingleton.instance) {
      MySingleton.instance = new MySingleton();
    }

    // 보관된 객체 주기
    return MySingleton.instance;
  }
}

🧪 첫 번째 싱글턴 실습

이제 실제로 싱글턴을 만들어서 사용해보는 실습을 시작해보겠습니다!

🔹 예제 1: 게임 점수 관리자 만들기

먼저 게임에서 점수를 관리하는 특별한 관리자를 만들어보겠어요.

// 게임 점수 관리자 클래스 (세상에 하나뿐!)
class GameScoreManager {
  // 1단계: 비밀 창고 만들기
  static instance;

  // 게임 정보를 저장할 변수들
  constructor() {
    // 이미 관리자가 있으면 그것을 돌려줘요
    if (GameScoreManager.instance) {
      console.log("이미 점수 관리자가 있어요! 기존 관리자를 드릴게요.");
      return GameScoreManager.instance;
    }

    // 없으면 새 관리자를 만들고 저장해요
    console.log("🎮 새로운 게임 점수 관리자를 만들었어요!");

    // 게임 정보 초기화
    this.playerName = "";
    this.score = 0;
    this.level = 1;
    this.lives = 3;

    GameScoreManager.instance = this;
  }

  // 3단계: 안전한 창구 만들기
  static getInstance() {
    // 관리자가 없으면 새로 만들어요
    if (!GameScoreManager.instance) {
      GameScoreManager.instance = new GameScoreManager();
    }

    // 저장된 관리자를 돌려줘요
    return GameScoreManager.instance;
  }

  // 플레이어 이름 설정하기
  setPlayerName(name) {
    this.playerName = name;                                     
    console.log(`플레이어 이름을 ${name}로 설정했어요!`);
  }

  // 점수 추가하기
  addScore(points) {
    this.score += points;                                       
    console.log(`${points}점을 획득했어요! 총 점수: ${this.score}점`);

    // 레벨업 조건 확인하기
    if (this.score >= this.level * 100) {                    
      this.levelUp();                                            
    }
  }

  // 레벨업 하기
  levelUp() {
    this.level++;                                               
    console.log(`🎉 레벨 ${this.level}로 올라갔어요!`);
  }

  // 목숨 잃기
  loseLife() {
    this.lives--;                                               
    console.log(`💔 목숨을 잃었어요! 남은 목숨: ${this.lives}개`);

    if (this.lives <= 0) {                                     
      console.log("💀 게임 오버!");                              
    }
  }

  // 현재 게임 상태 보여주기
  showGameStatus() {
    console.log("🎮 현재 게임 상태:");
    console.log(`   플레이어: ${this.playerName || "이름 없음"}`);
    console.log(`   점수: ${this.score}점`);
    console.log(`   레벨: ${this.level}`);
    console.log(`   목숨: ${this.lives}개`);
  }

  // 게임 리셋하기
  resetGame() {
    this.score = 0;                                             
    this.level = 1;                                             
    this.lives = 3;                                             
    console.log("🔄 게임을 리셋했어요!");
  }
}

이제 우리의 게임 점수 관리자를 사용해보겠어요:

// 첫 번째 게임 화면에서 점수 관리자 가져오기
const gameManager1 = GameScoreManager.getInstance();

// 플레이어 정보 설정하기
gameManager1.setPlayerName("용감한 모험가");
gameManager1.addScore(50);
gameManager1.showGameStatus();

// 다른 게임 화면에서 점수 관리자 가져오기
const gameManager2 = GameScoreManager.getInstance();

// 같은 관리자인지 확인해보기
console.log("같은 관리자인가요?", gameManager1 === gameManager2); // true

// gameManager2로 점수 추가하기
gameManager2.addScore(75);

// gameManager1에서 상태 확인하기 (같은 객체이므로 점수가 변경되어 있을 거예요)
gameManager1.showGameStatus();

🔹 예제 2: 우리반 출석부 관리자 만들기

이번에는 학교에서 사용할 수 있는 출석부 관리자를 만들어보겠어요.

// 우리반 출석부 관리자 클래스 (반에 하나뿐!)
class ClassAttendanceManager {
  // 비밀 창고
  static instance;

  // 관리자 만들기
  constructor() {
    // 이미 출석부 관리자가 있으면 그것을 돌려줘요
    if (ClassAttendanceManager.instance) {
      console.log("📋 이미 출석부 관리자가 있어요!");
      return ClassAttendanceManager.instance;
    }

    // 없으면 새로 만들어요
    console.log("📚 새로운 출석부 관리자를 만들었어요!");

    // 학생 목록과 출석 정보 초기화
    this.students = [];
    this.attendanceData = {};
    this.className = "";

    ClassAttendanceManager.instance = this;
  }

  // 안전한 창구
  static getInstance() {
    if (!ClassAttendanceManager.instance) {
      ClassAttendanceManager.instance = new ClassAttendanceManager();
    }
    return ClassAttendanceManager.instance;
  }

  // 반 이름 설정하기
  setClassName(name) {
    this.className = name;                                      
    console.log(`반 이름을 "${name}"로 설정했어요!`);
  }

  // 학생 추가하기
  addStudent(studentName) {
    if (!this.students.includes(studentName)) {                
      this.students.push(studentName);                         
      this.attendanceData[studentName] = { present: 0, absent: 0 }; 
      console.log(`👦👧 ${studentName} 학생을 추가했어요!`);
    } else {
      console.log(`${studentName} 학생은 이미 있어요!`);
    }
  }

  // 출석 체크하기
  markPresent(studentName) {
    if (this.attendanceData[studentName]) {                    
      this.attendanceData[studentName].present++;              
      console.log(`✅ ${studentName} 학생이 출석했어요!`);
    } else {
      console.log(`${studentName} 학생을 찾을 수 없어요.`);
    }
  }

  // 결석 체크하기
  markAbsent(studentName) {
    if (this.attendanceData[studentName]) {                    
      this.attendanceData[studentName].absent++;               
      console.log(`❌ ${studentName} 학생이 결석했어요.`);
    } else {
      console.log(`${studentName} 학생을 찾을 수 없어요.`);
    }
  }

  // 전체 출석 현황 보기
  showAttendanceReport() {
    console.log(`📊 ${this.className} 출석 현황:`);
    console.log("─".repeat(30));

    this.students.forEach(student => {                         
      const data = this.attendanceData[student];               
      console.log(`${student}: 출석 ${data.present}일, 결석 ${data.absent}일`);
    });

    console.log("─".repeat(30));
  }
}

우리반 출석부 관리자를 사용해보겠어요:

// 아침에 담임선생님이 출석부 가져오기
const morningAttendance = ClassAttendanceManager.getInstance();

// 반 정보 설정하기
morningAttendance.setClassName("5학년 3반");

// 학생들 추가하기
morningAttendance.addStudent("김민수");
morningAttendance.addStudent("이영희");
morningAttendance.addStudent("박철수");

// 아침 출석 체크하기
morningAttendance.markPresent("김민수");
morningAttendance.markPresent("이영희");
morningAttendance.markAbsent("박철수");

// 점심시간에 다른 선생님이 출석부 가져오기
const lunchAttendance = ClassAttendanceManager.getInstance();

// 같은 출석부인지 확인하기
console.log("같은 출석부인가요?", morningAttendance === lunchAttendance); // true

// 점심시간 출석 현황 보기
lunchAttendance.showAttendanceReport();

🔹 예제 3: 간단한 설정 관리자 만들기

마지막으로 프로그램의 설정을 관리하는 관리자를 만들어보겠어요.

// 프로그램 설정 관리자 클래스 (프로그램에 하나뿐!)
class ConfigManager {
  // 비밀 창고
  static instance;

  // 설정 관리자 만들기
  constructor() {
    if (ConfigManager.instance) {
      console.log("⚙️ 이미 설정 관리자가 있어요!");
      return ConfigManager.instance;
    }

    console.log("🔧 새로운 설정 관리자를 만들었어요!");

    // 설정 정보들 초기화
    this.settings = {
      theme: "밝은 테마",
      language: "한국어",
      volume: 50,
      notifications: true
    };

    ConfigManager.instance = this;
  }

  // 안전한 창구
  static getInstance() {
    if (!ConfigManager.instance) {
      ConfigManager.instance = new ConfigManager();
    }
    return ConfigManager.instance;
  }

  // 설정 값 가져오기
  getSetting(key) {
    return this.settings[key];                                  
  }

  // 설정 값 변경하기
  setSetting(key, value) {
    this.settings[key] = value;                                 
    console.log(`⚙️ ${key} 설정을 "${value}"로 변경했어요!`);
  }

  // 모든 설정 보기
  showAllSettings() {
    console.log("🔧 현재 프로그램 설정:");
    console.log("─".repeat(25));

    for (const [key, value] of Object.entries(this.settings)) { 
      console.log(`${key}: ${value}`);                           
    }

    console.log("─".repeat(25));
  }

  // 설정 초기화하기
  resetSettings() {
    this.settings = {                                           
      theme: "밝은 테마",
      language: "한국어",
      volume: 50,
      notifications: true
    };
    console.log("🔄 설정을 초기값으로 되돌렸어요!");
  }
}

설정 관리자를 사용해보겠어요:

// 프로그램 시작 부분에서 설정 관리자 가져오기
const appConfig = ConfigManager.getInstance();

// 현재 설정 확인하기
appConfig.showAllSettings();

// 설정 변경하기
appConfig.setSetting("theme", "어두운 테마");
appConfig.setSetting("volume", 80);

// 다른 부분에서 설정 관리자 가져오기
const menuConfig = ConfigManager.getInstance();

// 같은 관리자인지 확인하기
console.log("같은 설정 관리자인가요?", appConfig === menuConfig); // true

// 변경된 설정 확인하기
console.log("현재 테마:", menuConfig.getSetting("theme"));
console.log("현재 볼륨:", menuConfig.getSetting("volume"));

🔄 싱글턴 구현 과정 정리

싱글턴을 만드는 전체 과정을 차근차근 정리해보겠어요.

1단계: 비밀 창고 준비하기

  • static instance 변수를 만들어서 유일한 객체를 보관할 곳을 준비해요
  • 필요하면 외부에서 접근할 수 없게 보호해요

2단계: 안전한 생성자 만들기

  • constructor에서 이미 객체가 있는지 확인해요
  • 있으면 기존 객체를 돌려주고, 없으면 새로 만들어서 저장해요

3단계: 안전한 창구 만들기

  • static getInstance() 같은 메서드로 객체에 접근할 수 있게 해요
  • 이 메서드를 통해서만 객체를 받을 수 있도록 해요

4단계: 필요한 기능 추가하기

  • 싱글턴 객체가 실제로 해야 할 일들을 메서드로 만들어요
  • 중요한 데이터는 적절히 보호해요

5단계: 올바르게 사용하기

  • 항상 getInstance() 메서드를 통해 객체를 가져와요
  • new 키워드로 직접 만들지 않도록 주의해요

🧚‍♀️ 이야기로 다시 배우기: 우리 동네 도서관

지금까지 배운 내용을 하나의 따뜻한 이야기로 다시 정리해볼까요?

우리 동네에는 정말 특별한 "중앙 도서관"이 있었어요. 이 도서관은 온 동네의 모든 책과 정보를 담고 있는 특별한 곳이었어요.

동네에는 여러 마을이 있었고, 각 마을에는 다양한 사람들이 살고 있었어요. 학생, 선생님, 회사원, 주부... 모두 다른 일을 하지만, 책이 필요할 때는 반드시 중앙 도서관에 가야 했어요.

처음에는 각 마을마다 자기만의 작은 도서관을 만들려고 했어요. 하지만 그랬더니 큰 문제가 생겼어요:

  • A마을 도서관: "해리포터 1권만 있음"
  • B마을 도서관: "해리포터 7권만 있음"
  • C마을 도서관: "해리포터가 없음"

같은 책을 찾아도 각 도서관마다 다른 상황이니까 온 동네가 불편했어요! 😵

그래서 동네의 현명한 동장님이 결정했어요:

"온 동네에 도서관은 오직 하나만 있어야 한다! 모든 마을의 모든 사람이 같은 도서관에서 같은 책들을 빌릴 수 있도록 하자!"

// 중앙 도서관 (싱글턴)
class CentralLibrary {
    static instance = null;

    constructor() {
        if (CentralLibrary.instance) {
            console.log("이미 중앙 도서관이 존재해요!");
            return CentralLibrary.instance;
        }

        console.log("📚 중앙 도서관이 개관했어요!");
        this.books = new Map(); // 동네의 모든 책
        this.books.set("해리포터 1권", { available: true, borrower: null });
        this.books.set("해리포터 2권", { available: true, borrower: null });
        CentralLibrary.instance = this;
    }

    static getInstance() {
        if (!CentralLibrary.instance) {
            CentralLibrary.instance = new CentralLibrary();
        }
        return CentralLibrary.instance;
    }

    borrowBook(bookTitle, borrowerName) {
        let book = this.books.get(bookTitle);
        if (book && book.available) {
            book.available = false;
            book.borrower = borrowerName;
            console.log(`📖 ${borrowerName}님이 "${bookTitle}"을(를) 대출했어요.`);
            return true;
        } else {
            console.log(`❌ "${bookTitle}"은(는) 대출 중이거나 없는 책이에요.`);
            return false;
        }
    }

    returnBook(bookTitle) {
        let book = this.books.get(bookTitle);
        if (book && !book.available) {
            console.log(`📚 "${bookTitle}"이(가) 반납되었어요.`);
            book.available = true;
            book.borrower = null;
            return true;
        }
        return false;
    }
}

// A마을의 학생
console.log("=== A마을 학생 ===");
let studentLibrary = CentralLibrary.getInstance();
studentLibrary.borrowBook("해리포터 1권", "민수");

// B마을의 선생님  
console.log("\n=== B마을 선생님 ===");
let teacherLibrary = CentralLibrary.getInstance();
teacherLibrary.borrowBook("해리포터 2권", "김선생님");

// C마을의 주부
console.log("\n=== C마을 주부 ===");
let parentLibrary = CentralLibrary.getInstance();
parentLibrary.borrowBook("해리포터 1권", "박엄마"); // 이미 대출 중!

console.log(`\n모든 도서관이 같은 곳인가요? ${studentLibrary === teacherLibrary && teacherLibrary === parentLibrary}`);

이제 온 동네의 모든 사람이 같은 중앙 도서관에서 일관된 서비스를 받을 수 있게 되었어요. 누가 어디서 책을 빌리든 같은 시스템을 사용하고, 책의 상태도 실시간으로 확인할 수 있게 되었답니다.

이처럼 싱글턴 패턴은 "하나뿐이어야 하는 특별한 것"을 프로그래밍으로 구현하는 아름다운 방법이에요!

🧠 자주 하는 실수와 주의할 점

싱글턴을 구현할 때 자주 하는 실수들을 미리 알아두면 도움이 될 거예요.

❌ 실수 1: 생성자에서 확인하지 않기

// 이렇게 하면 안 돼요!
class MusicPlayer {
  static instance;

  constructor() {
    // 이미 있는지 확인하지 않아서 new 키워드로 여러 개 만들 수 있어요!
  }

  static getInstance() {
    if (!MusicPlayer.instance) {
      MusicPlayer.instance = new MusicPlayer();
    }
    return MusicPlayer.instance;
  }
}

이렇게 하면 new MusicPlayer()로 여러 개를 만들 수 있어서 싱글턴이 깨져요.

❌ 실수 2: 싱글턴에 너무 많은 기능 넣기

// 잘못된 예: 모든 기능을 하나의 싱글턴에 넣기
class AppManager {
  static instance;

  constructor() {
    if (AppManager.instance) return AppManager.instance;
    AppManager.instance = this;
  }

  // 사용자 관리
  loginUser(username) { /* ... */ }
  logoutUser() { /* ... */ }

  // 설정 관리  
  setSetting(key, value) { /* ... */ }
  getSetting(key) { /* ... */ }

  // 파일 관리
  saveFile(content) { /* ... */ }
  loadFile() { /* ... */ }

  // 너무 많은 책임을 가진 싱글턴!
}

// 올바른 방법: 각각의 싱글턴으로 분리
class UserManager {
  static instance;
  // 사용자 관리 전용
}

class ConfigManager {
  static instance;  
  // 설정 관리 전용
}

class FileManager {
  static instance;
  // 파일 관리 전용
}

이런 실수가 발생하는 이유는 싱글턴이 편리하다고 해서 모든 전역적인 기능을 하나에 몰아넣는 경우예요. 이렇게 하면 유지보수가 어려워져요.

❌ 실수 3: new 키워드 직접 사용하기

// 이렇게 하면 안 돼요!
const manager1 = new GameManager();  // 직접 생성자 호출
const manager2 = new GameManager();  // 또 다른 객체가 만들어질 수 있어요!

// 이렇게 해야 해요!
const manager1 = GameManager.getInstance();
const manager2 = GameManager.getInstance();  // 같은 객체를 받아요!

✏️ 연습문제로 개념 다지기

어떤 지식은 손끝을 통해 마음으로 스며들기도 합니다. 코드를 직접 써보며 싱글턴의 원리를 체득해보는 시간을 가져보겠습니다. 각각의 예제는 서로 다른 상황에서 어떻게 싱글턴이 우리를 도와주는지 보여줄 거예요.

Ex1) 간단한 일기장 관리자 만들어보자

하나뿐인 일기장을 관리하는 싱글턴 클래스를 만들어보세요.

// 일기장 관리자 클래스 (세상에 하나뿐!)
class DiaryManager {
  // 비밀 창고
  static instance;

  // 일기장 만들기
  constructor() {
    // 이미 일기장이 있으면 그것을 돌려줘요
    if (DiaryManager.instance) {
      console.log("📖 이미 일기장이 있어요!");
      return DiaryManager.instance;
    }

    // 없으면 새 일기장을 만들어요
    console.log("📝 새로운 일기장을 만들었어요!");

    // 일기 내용들과 주인 초기화
    this.entries = [];
    this.owner = "";

    DiaryManager.instance = this;
  }

  // 안전한 창구
  static getInstance() {
    if (!DiaryManager.instance) {
      DiaryManager.instance = new DiaryManager();
    }
    return DiaryManager.instance;
  }

  // 일기장 주인 설정하기
  setOwner(name) {
    this.owner = name;                                          
    console.log(`📖 ${name}의 일기장이 되었어요!`);
  }

  // 일기 쓰기
  writeEntry(content) {
    const today = new Date().toLocaleDateString();              
    this.entries.push({                                        
      date: today,                                               
      content: content                                           
    });
    console.log(`📝 ${today}에 일기를 썼어요!`);
  }

  // 모든 일기 보기
  showAllEntries() {
    console.log(`📚 ${this.owner}의 일기장:`);
    console.log("─".repeat(30));

    this.entries.forEach((entry, index) => {                   
      console.log(`${index + 1}. ${entry.date}`);               
      console.log(`   ${entry.content}`);                       
      console.log("");
    });
  }

  // 일기 개수 확인하기
  getEntryCount() {
    return this.entries.length;                                
  }
}

Ex2) 우리 집 냉장고 관리자 만들어보자

집에 하나뿐인 냉장고를 관리하는 싱글턴 클래스를 만들어보세요.

// 냉장고 관리자 클래스 (집에 하나뿐!)
class RefrigeratorManager {
  // 비밀 창고
  static instance;

  // 냉장고 만들기
  constructor() {
    if (RefrigeratorManager.instance) {
      console.log("🧊 이미 냉장고가 있어요!");
      return RefrigeratorManager.instance;
    }

    console.log("❄️ 새로운 냉장고를 설치했어요!");

    // 냉장고 안의 음식들과 온도 초기화
    this.foods = [];
    this.temperature = 4; // 기본 온도 4도

    RefrigeratorManager.instance = this;
  }

  // 안전한 창구
  static getInstance() {
    if (!RefrigeratorManager.instance) {
      RefrigeratorManager.instance = new RefrigeratorManager();
    }
    return RefrigeratorManager.instance;
  }

  // 음식 넣기
  addFood(foodName, quantity = 1) {
    const existingFood = this.foods.find(food => food.name === foodName); 

    if (existingFood) {                                          
      existingFood.quantity += quantity;                         
    } else {                                                     
      this.foods.push({ name: foodName, quantity: quantity }); 
    }

    console.log(`🥗 ${foodName} ${quantity}개를 냉장고에 넣었어요!`);
  }

  // 음식 꺼내기
  takeFood(foodName, quantity = 1) {
    const foodIndex = this.foods.findIndex(food => food.name === foodName); 

    if (foodIndex === -1) {                                      
      console.log(`😞 ${foodName}이(가) 냉장고에 없어요!`);
      return false;
    }

    const food = this.foods[foodIndex];                         
    if (food.quantity < quantity) {                              
      console.log(`😞 ${foodName}이(가) ${quantity}개만큼 없어요! (현재 ${food.quantity}개)`);
      return false;
    }

    food.quantity -= quantity;                                   
    if (food.quantity === 0) {                                   
      this.foods.splice(foodIndex, 1);                         
    }

    console.log(`🍽️ ${foodName} ${quantity}개를 꺼냈어요!`);
    return true;
  }

  // 냉장고 안 확인하기
  checkContents() {
    console.log("🧊 냉장고 안 내용물:");
    console.log("─".repeat(20));

    if (this.foods.length === 0) {                              
      console.log("   (텅 비어있어요)");
    } else {                                                     
      this.foods.forEach(food => {                             
        console.log(`   ${food.name}: ${food.quantity}개`);      
      });
    }

    console.log(`   온도: ${this.temperature}도`);
    console.log("─".repeat(20));
  }

  // 온도 조절하기
  setTemperature(temp) {
    this.temperature = temp;                                    
    console.log(`🌡️ 냉장고 온도를 ${temp}도로 설정했어요!`);
  }
}

📚 18.1.1단원 복습 문제

지난 시간에 배운 싱글턴 패턴의 기본 개념을 복습해보겠습니다!

복습 문제 1: 싱글턴 패턴의 특징 설명하기

// 다음 코드를 보고 싱글턴 패턴의 특징을 설명해보세요
class SchoolBell {
    static instance = null;

    constructor() {
        if (SchoolBell.instance) {
            return SchoolBell.instance;
        }
        this.currentTime = "";
        SchoolBell.instance = this;
    }

    static getInstance() {
        if (!SchoolBell.instance) {
            SchoolBell.instance = new SchoolBell();
        }
        return SchoolBell.instance;
    }

    ring(time) {
        this.currentTime = time;
        console.log(`🔔 ${time}에 종이 울려요!`);
    }
}

// 1교시에서 종 사용
const bell1 = SchoolBell.getInstance();
bell1.ring("9시");

// 2교시에서 종 사용  
const bell2 = SchoolBell.getInstance();
console.log("같은 종인가요?", bell1 === bell2); // true

해답 설명:

  • 유일성: 학교에는 종이 하나만 있어요. static instance로 하나의 객체만 보관해요.
  • 전역 접근: 어느 교실에서든 getInstance()로 같은 종에 접근할 수 있어요.
  • 상태 공유: 1교시에서 종을 울리면 2교시에서도 그 정보를 확인할 수 있어요.
  • 제어된 생성: 생성자에서 이미 있는지 확인해서 중복 생성을 막아요.

복습 문제 2: 싱글턴 패턴의 장점과 단점

// 게임 설정 관리자 예시
class GameSettings {
    static instance = null;

    constructor() {
        if (GameSettings.instance) {
            return GameSettings.instance;
        }
        this.settings = {
            difficulty: "보통",
            volume: 50,
            playerName: "플레이어"
        };
        GameSettings.instance = this;
    }

    static getInstance() {
        if (!GameSettings.instance) {
            GameSettings.instance = new GameSettings();
        }
        return GameSettings.instance;
    }

    setSetting(key, value) {
        this.settings[key] = value;
        console.log(`설정 변경: ${key} = ${value}`);
    }

    getSetting(key) {
        return this.settings[key];
    }
}

// 메인 메뉴에서 설정 변경
const menuSettings = GameSettings.getInstance();
menuSettings.setSetting("difficulty", "어려움");

// 게임 화면에서 설정 확인
const gameSettings = GameSettings.getInstance();
console.log("현재 난이도:", gameSettings.getSetting("difficulty")); // "어려움"

해답:

장점:

  1. 일관성: 모든 곳에서 같은 설정을 사용할 수 있어요
  2. 메모리 절약: 설정 객체를 하나만 만들어서 메모리를 아껴요
  3. 쉬운 접근: 어디서든 getInstance()로 설정에 접근할 수 있어요

단점:

  1. 테스트 어려움: 여러 테스트가 같은 객체를 공유해서 독립적이지 않아요
  2. 확장 어려움: 나중에 여러 개가 필요하면 코드를 많이 바꿔야 해요
  3. 의존성 숨김: 어떤 클래스가 싱글턴에 의존하는지 명확하지 않아요

지금까지 싱글턴 구현하기에 대해 자세히 알아보았어요. 이제 여러분도 세상에 하나뿐인 특별한 클래스를 만들 수 있게 되었어요. 중요한 것은 항상 세 가지 핵심 요소(비밀 창고, 안전한 생성자, 정적 접근 메서드)를 기억하고, 실수하지 않도록 주의깊게 코딩하는 것이에요!

✅ 학습 완료 체크리스트

이번 시간에 배운 내용들을 모두 이해했는지 확인해보세요!

학습 내용 이해했나요?
싱글턴 구현 기본 개념
구현 방법과 문법
자주 하는 실수들
실전 예제 이해
18.1.1단원 복습 내용

📂 마무리 정보

오늘 배운 18.1.2 내용이 여러분의 자바스크립트 지식에 잘 저장되었나요? 다음 시간에는 더 흥미로운 내용으로 만나요!

기억할 점: 오늘 배운 내용을 꼭 연습해보시고, 궁금한 점이 있으면 언제든 다시 돌아와서 읽어보세요.


🚀 더 체계적인 JavaScript 학습을 원하신다면?
이 포스팅에서 다룬 내용을 실제로 실습해보세요!
무료 JavaScript 학습 플랫폼에서 단계별 학습과 실시간 코드 실행을 통해
더욱 효과적이고 재미있게 학습하실 수 있습니다.
📝 실시간 코드 실행 📊 학습 진도 관리 👥 체계적 커리큘럼
📚 171개 체계적 학습레슨 · 📋 855개 4지선다 연습문제 · 🆓 완전 무료 · ⚡ 즉시 시작