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

18.1.3 싱글톤 패턴 - 언제 사용하면 좋을까요?

thejavascript4kids 2025. 7. 27. 05:58

📘 18.1.3 싱글톤 패턴 - 언제 사용하면 좋을까요?

안녕하세요, 여러분. 집에서 텔레비전 리모컨을 찾아 헤맨 적이 있나요? 방마다 하나씩 있으면 어디에 두었는지 기억하기 어렵죠. 하지만 우리 집 현관문은 어떤가요? 하나만 있어도 가족 모두가 불편함이 없어요.

오늘은 프로그래밍에서도 이처럼 '하나만 있어도 충분한 것'과 '여러 개가 필요한 것'을 구분하는 지혜에 대해 이야기해보려 해요. 마치 우리가 살아가면서 무엇을 나누고 무엇을 각자 가져야 하는지 알아가는 것처럼요.

🧠 새로운 용어들과 만나기

프로그래밍의 세계에는 때로 생소한 용어들이 나타납니다. 하지만 걱정하지 마세요. 하나씩 차근차근 알아가다 보면 어느새 익숙해질 거예요.

용어 의미
활용 상황 싱글톤 패턴이 도움이 되는 상황들을 의미해요
효율적 관리 컴퓨터의 자원을 아껴서 현명하게 사용하는 것이에요
문제가 되는 방법 겉보기엔 좋아 보이지만 실제로는 더 큰 문제를 만드는 방법이에요
연결 관계 코드들이 서로 얼마나 밀접하게 엮여있는지를 나타내요

이런 용어들이 처음엔 어색할 수 있어요. 우리가 새로운 동네로 이사 갔을 때 길 이름이 낯선 것처럼요. 하지만 시간이 지나면 자연스럽게 익숙해질 거예요.

✨ 싱글톤 패턴의 본질

싱글톤 패턴은 정말 유용한 도구예요. 하지만 모든 곳에 사용해서는 안 되는 특별한 도구이기도 해요. 음식을 만들 때 소금을 생각해보세요. 적당히 넣으면 맛이 살아나지만, 너무 많이 넣으면 먹을 수 없게 되죠.

우리 동네에는 도서관이 하나 있어요. 동네 사람들 모두가 이용하지만 하나로 충분해요. 여러 개 있다면 같은 책을 여러 번 사야 하고, 어느 도서관에 어떤 책이 있는지 찾기도 어려울 거예요. 컴퓨터에서도 정보 저장소에 연결하는 통로, 프로그램의 설정을 관리하는 곳, 중요한 일들을 기록하는 시스템 같은 것들은 하나만 있어도 되는 경우가 많아요.

하지만 모든 것을 하나만 만드는 건 좋지 않아요. 우리 동네 카페에서 머그컵 하나로 모든 손님이 돌아가며 커피를 마신다고 생각해보세요. 정말 불편하고 비위생적이겠죠? 프로그래밍에서도 사용자 정보, 게시물, 상품 데이터 같은 것들은 각각 독립적으로 여러 개가 필요해요.

가장 중요한 건 상황에 맞게 현명하게 판단하는 거예요. 언제 하나면 충분하고, 언제 여러 개가 필요한지 구분할 수 있어야 해요.

우리 동네로 이해하는 싱글톤

우리가 사는 동네를 떠올려보세요. 어떤 시설은 하나만 있어도 되고, 어떤 건 여러 개가 필요해요.

하나만 있어도 충분한 것들 (싱글톤이 적합한 경우):

우리 동네의 우체국을 생각해보세요. 편지와 택배를 처리하는 곳이에요. 동네에 우체국이 여러 개 있다면 배송이 꼬이고 관리가 복잡해질 거예요. 하나만 있어도 모든 주민들이 편리하게 이용할 수 있어요.

동네 방송 시스템도 그래요. 긴급상황이나 중요한 소식을 전할 때 하나의 시스템에서 일관성 있게 방송하는 것이 좋아요. 여러 개가 있다면 서로 다른 내용을 방송해서 주민들이 혼란스러워할 수 있어요.

공용 주차장 관리 시스템은 어떨까요? 주차 공간을 효율적으로 배정하려면 하나의 시스템에서 전체 현황을 파악해야 해요. 여러 개 시스템이 각각 관리한다면 같은 자리를 중복으로 배정할 수 있어요.

동네 도서관도 하나만 있으면 돼요. 모든 책을 한 곳에 모아두니까 찾기 쉽고, 사서분도 효율적으로 관리할 수 있어요.

여러 개가 필요한 것들 (싱글톤이 부적절한 경우):

반면에 은 어떤가요? 가족마다 자신들의 집이 필요해요. 동네 사람들이 모두 한 집에 살 수는 없잖아요. 각 가족의 생활 패턴도 다르고, 필요한 공간도 달라요.

은행 계좌도 마찬가지예요. 각 사람마다 자신만의 계좌가 있어야 해요. 모든 사람이 하나의 계좌를 공유한다면 누구 돈인지 구분할 수 없을 거예요.

휴대폰도 개인마다 필요해요. 가족이 휴대폰 하나를 돌아가며 사용한다면 연락받기도 어렵고 개인 프라이버시도 지킬 수 없어요.

자동차는 어떨까요? 대중교통이 아닌 개인 차량은 각 가정마다 필요에 따라 가지고 있어요. 동네에 차 한 대만 있고 모든 사람이 돌아가며 사용한다면 정말 불편할 거예요.

이처럼 싱글톤도 정말 하나만 있으면 되는 상황에서만 사용하는 것이 좋아요.

🎯 싱글톤 패턴이 빛나는 순간들

어떤 상황에서 싱글톤을 사용하면 좋은지 구체적으로 살펴보겠어요.

첫 번째로, 데이터베이스 연결 관리예요. 데이터베이스는 중요한 정보들이 저장된 큰 창고 같은 곳이에요. 이 창고에 연결하는 것은 비용이 많이 들어서, 하나의 연결통로를 만들어서 여러 기능들이 함께 사용하는 것이 효율적이에요. 마치 우리 아파트 단지에서 관리사무소 하나로 모든 동의 업무를 처리하는 것처럼요.

두 번째로, 로깅 시스템이에요. 프로그램에서 일어나는 모든 일들을 기록하는 일기장 같은 거예요. 여러 개의 일기장에 흩어져서 기록하면 나중에 찾기도 어렵고 시간 순서도 맞추기 힘들어요. 하나의 일기장에 차례대로 기록하는 것이 체계적이에요.

세 번째로, 애플리케이션 설정 관리예요. 앱의 언어, 테마, 볼륨 크기 같은 설정들은 앱 전체에서 동일하게 적용되어야 해요. 마치 우리 집의 온도 조절기가 하나여서 집 전체의 온도를 조절하는 것처럼요.

네 번째로, 캐시 시스템이에요. 자주 사용하는 데이터를 임시로 저장해두는 곳인데, 여러 개 있으면 같은 데이터가 중복으로 저장되어 메모리가 낭비돼요. 마치 자주 쓰는 조미료를 부엌 한 곳에 모아두는 것이 찾기 쉽고 관리하기 편한 것처럼요.

다섯 번째로, 파일 시스템 접근 제어예요. 중요한 파일에 여러 프로그램이 동시에 접근하면 파일이 손상될 수 있어서, 하나의 관리자가 순서를 정해주는 것이 안전해요. 마치 도서관에서 인기 있는 책 한 권을 여러 사람이 동시에 빌릴 수 없는 것처럼, 질서가 필요한 거예요.

⚠️ 싱글톤을 피해야 하는 상황들

반대로 싱글톤을 사용하면 오히려 문제가 되는 상황들도 있어요.

첫 번째로, 비즈니스 로직 부분들이에요. 실제 프로그램의 핵심 기능들은 독립적으로 만드는 것이 좋아요. 온라인 쇼핑몰에서 주문 처리, 결제 처리, 배송 처리 같은 기능들은 각각 따로 만들어야 나중에 수정하거나 새로운 기능을 추가하기 쉬워요. 마치 레고 블록처럼 각각 독립적으로 만들어야 자유롭게 조합할 수 있는 것처럼요.

두 번째로, 여러 인스턴스가 필요한 경우예요. 사용자 계정, 게시물, 상품 정보처럼 동시에 여러 개가 존재해야 하는 것들은 절대 싱글톤으로 만들면 안 돼요. 학급에서 학생들이 각자 자신만의 가방을 가져야 하는 것처럼요.

세 번째로, 테스트가 중요한 코드예요. 싱글톤은 테스트하기가 까다로워서, 안정성이 중요한 기능에는 적합하지 않아요. 복잡한 기계는 부품별로 따로따로 검사해야 안전한 것처럼, 코드도 각각 독립적으로 테스트할 수 있어야 해요.

네 번째로, 동시성이 중요한 경우예요. 여러 작업을 동시에 처리해야 할 때는 각각 독립적인 처리기가 필요할 수 있어요. 마치 카페에서 여러 명의 바리스타가 동시에 다른 주문을 처리하는 것처럼요.

다섯 번째로, 느슨한 결합이 중요한 경우예요. 코드들 사이의 연결을 최소화하고 싶을 때는 싱글톤보다는 다른 방법이 좋아요. 퍼즐 조각들이 각자 독립적이어야 자유롭게 맞출 수 있는 것처럼요.

🧪 실제 상황으로 이해하기

이론만으로는 이해하기 어려우니, 실제 예제를 통해 알아보겠어요.

🔹 예제 1: 게임 설정 관리자 만들기

게임을 할 때 설정은 게임 전체에서 일관되게 적용되어야 하겠죠? 게임 설정을 관리하는 싱글톤을 만들어보겠어요.

// Ex1) 게임 설정을 체계적으로 관리해보자
class 게임설정관리자 {
  // 비공개 인스턴스 변수 (단 하나만 존재)
  static #인스턴스;

  // 게임의 모든 설정을 담는 공간
  #설정들 = {
    볼륨: 50,         // 소리 크기
    화면밝기: 80,     // 화면 밝기
    언어: "한국어",   // 게임 언어
    난이도: "보통"    // 게임 난이도
  };

  // 새로운 관리자를 만들려 해도 기존 것을 돌려줍니다
  constructor() {
    // 이미 만들어진 관리자가 있다면 그것을 돌려줍니다
    if (게임설정관리자.#인스턴스) {
      return 게임설정관리자.#인스턴스;
    }

    console.log("🎮 게임 설정 관리자가 새롭게 준비되었어요!");
    게임설정관리자.#인스턴스 = this;  // 자기 자신을 저장합니다
  }

  // 관리자를 가져오는 공용 방법
  static 가져오기() {
    // 아직 만들어지지 않았다면 새로 만듭니다
    if (!게임설정관리자.#인스턴스) {
      게임설정관리자.#인스턴스 = new 게임설정관리자();
    }
    return 게임설정관리자.#인스턴스;  // 관리자를 돌려줍니다
  }

  // 설정을 새로운 값으로 바꾸기
  설정변경(이름, 값) {
    // 해당 설정이 실제로 있는지 확인합니다
    if (this.#설정들.hasOwnProperty(이름)) {
      const 이전값 = this.#설정들[이름];    // 이전 값을 기억해둡니다
      this.#설정들[이름] = 값;              // 새로운 값으로 바꿉니다
      console.log(`⚙️ ${이름} 설정이 ${이전값}에서 ${값}으로 바뀌었어요!`);
      this.#설정적용();                     // 변경된 설정을 게임에 반영합니다
    } else {
      console.log(`❌ ${이름}은 존재하지 않는 설정이에요!`);
    }
  }

  // 현재 설정 값 확인하기
  설정가져오기(이름) {
    // 해당 설정이 실제로 있는지 확인합니다
    if (this.#설정들.hasOwnProperty(이름)) {
      console.log(`📋 ${이름} 설정: ${this.#설정들[이름]}`);
      return this.#설정들[이름];           // 설정 값을 돌려줍니다
    } else {
      console.log(`❌ ${이름}은 존재하지 않는 설정이에요!`);
      return null;                         // 없으면 null을 돌려줍니다
    }
  }

  // 모든 설정을 한 번에 보기
  모든설정보기() {
    console.log("📊 현재 게임 설정:");
    // 모든 설정을 하나씩 보여줍니다
    Object.keys(this.#설정들).forEach(키 => {
      console.log(`   ${키}: ${this.#설정들[키]}`);
    });
    return { ...this.#설정들 };           // 설정들의 사본을 돌려줍니다
  }

  // 설정을 실제 게임에 적용하는 내부 기능
  #설정적용() {
    console.log("🔄 새로운 설정이 게임에 적용되었어요!");
    // 실제로는 여기서 게임 화면이나 소리를 바꾸는 코드가 들어갑니다
  }
}

🔹 예제 2: 학교 방송실 시스템 만들기

학교에는 방송실이 하나만 있어야 질서정연하게 방송할 수 있겠죠?

// Ex2) 학교 방송실을 체계적으로 운영해보자
class 학교방송실 {
  // 비공개 인스턴스 변수
  static #인스턴스;

  // 방송실의 현재 상태
  #방송중 = false;            // 현재 방송 중인지 여부
  #현재방송내용 = "";         // 지금 방송하고 있는 내용
  #방송기록 = [];             // 오늘 진행한 방송들의 기록

  constructor() {
    // 이미 방송실이 있다면 그것을 돌려줍니다
    if (학교방송실.#인스턴스) {
      return 학교방송실.#인스턴스;
    }

    console.log("📻 학교 방송실이 새롭게 문을 열었어요!");
    학교방송실.#인스턴스 = this;    // 자기 자신을 저장합니다
  }

  static 가져오기() {
    // 아직 방송실이 없다면 새로 만듭니다
    if (!학교방송실.#인스턴스) {
      학교방송실.#인스턴스 = new 학교방송실();
    }
    return 학교방송실.#인스턴스;   // 방송실을 돌려줍니다
  }

  // 방송을 시작하기
  방송시작(방송자, 내용) {
    // 이미 방송 중이라면 새로 시작할 수 없습니다
    if (this.#방송중) {
      console.log(`❌ 이미 방송 중이에요! 현재 방송: "${this.#현재방송내용}"`);
      return false;              // 방송 시작 실패
    }

    this.#방송중 = true;                                  // 방송 중 상태로 변경
    this.#현재방송내용 = `${방송자}: ${내용}`;             // 현재 방송 내용 저장

    console.log(`📢 전교 방송을 시작합니다!`);
    console.log(`🎤 ${this.#현재방송내용}`);

    // 방송 기록에 추가합니다
    this.#방송기록.push({
      시간: new Date().toLocaleTimeString(),  // 방송한 시간 기록
      방송자: 방송자,                         // 누가 방송했는지 기록
      내용: 내용                              // 무엇을 방송했는지 기록
    });

    return true;                             // 방송 시작 성공
  }

  // 방송을 종료하기
  방송종료() {
    // 현재 방송 중이 아니라면 종료할 수 없습니다
    if (!this.#방송중) {
      console.log("❌ 현재 방송 중이 아니에요!");
      return false;             // 방송 종료 실패
    }

    console.log("📻 방송이 종료되었어요!");
    this.#방송중 = false;       // 방송 중 상태 해제
    this.#현재방송내용 = "";    // 현재 방송 내용 지우기

    return true;                // 방송 종료 성공
  }

  // 현재 방송 상태 확인하기
  방송상태확인() {
    if (this.#방송중) {
      console.log(`📡 현재 방송 중: ${this.#현재방송내용}`);
    } else {
      console.log("📻 현재 방송하지 않고 있어요!");
    }
    return this.#방송중;        // 방송 중인지 여부를 돌려줍니다
  }

  // 오늘의 방송 기록 보기
  방송기록보기() {
    // 방송 기록이 아직 없다면
    if (this.#방송기록.length === 0) {
      console.log("📋 아직 방송 기록이 없어요!");
      return [];                // 빈 배열을 돌려줍니다
    }

    console.log("📜 오늘의 방송 기록:");
    // 모든 방송 기록을 하나씩 보여줍니다
    this.#방송기록.forEach((기록, 순서) => {
      console.log(`   ${순서 + 1}. [${기록.시간}] ${기록.방송자}: ${기록.내용}`);
    });

    return [...this.#방송기록]; // 방송 기록의 사본을 돌려줍니다
  }
}

🔹 예제 3: 부적절한 예시 - 학생을 싱글톤으로 만들기

이번에는 싱글톤을 잘못 사용한 예시를 보면서 왜 문제가 되는지 알아보겠어요.

// Ex3) 학생을 하나만 만들면 어떤 문제가 생기는지 살펴보자 (부적절한 예시!)
class 잘못된학생예시 {
  static #인스턴스;

  constructor(이름, 나이, 학년) {
    // 이미 학생이 있다면 기존 학생을 돌려줍니다 (문제 발생!)
    if (잘못된학생예시.#인스턴스) {
      return 잘못된학생예시.#인스턴스;
    }

    this.이름 = 이름;           // 학생의 이름
    this.나이 = 나이;           // 학생의 나이
    this.학년 = 학년;           // 학생의 학년
    잘못된학생예시.#인스턴스 = this;  // 자기 자신을 저장합니다
  }

  static 가져오기() {
    // 아직 학생이 없다면 기본 학생을 만듭니다
    if (!잘못된학생예시.#인스턴스) {
      잘못된학생예시.#인스턴스 = new 잘못된학생예시("이름없음", 0, 0);
    }
    return 잘못된학생예시.#인스턴스;
  }

  자기소개() {
    console.log(`안녕하세요! 저는 ${this.이름}이고, ${this.나이}살이며, ${this.학년}학년이에요!`);
  }
}

// 문제 상황을 직접 확인해보기
console.log("❌ 부적절한 예시: 학생을 싱글톤으로 만들기");

const 첫번째학생 = new 잘못된학생예시("철수", 10, 4);  // 철수 학생을 만듭니다
첫번째학생.자기소개(); // "안녕하세요! 저는 철수이고, 10살이며, 4학년이에요!"

const 두번째학생 = new 잘못된학생예시("영희", 9, 3);   // 영희 학생을 만들려고 시도합니다
두번째학생.자기소개(); // "안녕하세요! 저는 철수이고, 10살이며, 4학년이에요!" (문제!)

console.log("🤔 두번째학생도 철수가 되어버렸어요!");
console.log("첫번째학생과 두번째학생이 같은 학생인가요?", 첫번째학생 === 두번째학생); // true

// 올바른 방법: 일반 클래스로 학생 만들기
class 올바른학생 {
  constructor(이름, 나이, 학년) {
    this.이름 = 이름;        // 각 학생마다 자신만의 이름
    this.나이 = 나이;        // 각 학생마다 자신만의 나이
    this.학년 = 학년;        // 각 학생마다 자신만의 학년
  }

  자기소개() {
    console.log(`안녕하세요! 저는 ${this.이름}이고, ${this.나이}살이며, ${this.학년}학년이에요!`);
  }
}

console.log("\n✅ 올바른 방법: 일반 클래스로 학생 만들기");

const 올바른철수 = new 올바른학생("철수", 10, 4);   // 철수라는 학생
const 올바른영희 = new 올바른학생("영희", 9, 3);    // 영희라는 학생

올바른철수.자기소개(); // "안녕하세요! 저는 철수이고, 10살이며, 4학년이에요!"
올바른영희.자기소개(); // "안녕하세요! 저는 영희이고, 9살이며, 3학년이에요!"

console.log("🎉 이제 각각 다른 학생이 되었어요!");
console.log("철수와 영희가 다른 학생인가요?", 올바른철수 !== 올바른영희); // true

🔄 현명한 선택을 위한 단계별 과정

싱글톤을 사용할지 말지 결정하는 과정을 차근차근 정리해보겠어요.

첫 번째 단계는 정말 필요한지 깊이 생각해보기입니다. "정말로 하나만 있어도 될까요? 여러 개가 있으면 문제가 생길까요?"라고 스스로에게 물어보세요. 의사선생님이 환자를 진료하기 전에 증상을 자세히 살펴보는 것처럼, 신중하게 살펴보는 과정이 필요해요.

두 번째는 장점과 단점을 꼼꼼히 비교해보기입니다. 싱글톤의 장점(메모리 절약, 일관된 관리)과 단점(테스트 어려움, 코드 간 강한 결합)을 세심하게 따져보세요. 양팔 저울에 무게를 올려놓고 어느 쪽이 더 무거운지 비교하는 것처럼요.

세 번째는 다른 방법들도 생각해보기입니다. 싱글톤 말고 다른 방법으로도 같은 목적을 달성할 수 있는지 고민해보세요. 목적지에 가는 길이 여러 개 있는 것처럼, 프로그래밍에서도 여러 가지 방법이 있을 수 있어요.

네 번째는 미래의 변화 가능성 고려하기입니다. 나중에 요구사항이 바뀌어서 여러 개가 필요해질 가능성은 없는지 생각해보세요. 집을 지을 때 나중에 확장할 가능성을 미리 고려하는 것처럼요.

마지막은 신중하게 최종 결정하기입니다. 모든 것을 충분히 고려한 후에 정말 싱글톤이 최선의 선택인지 판단해보세요. 급한 마음보다는 차분하고 신중한 결정이 중요해요.

🧚‍♀️ 이야기로 되새기기: 작은 마을의 지혜로운 선택

지금까지 배운 내용을 따뜻한 이야기로 다시 정리해볼게요.

어느 작은 마을에 현명한 마을 사람들이 살고 있었어요. 이 마을에는 특별한 원칙이 있었는데, 어떤 시설은 마을에 하나만 두고 함께 사용하고, 어떤 건 각 가정마다 따로 가지는 것이었어요.

함께 사용하는 것들 (싱글톤이 적합한 경우):

마을 우체국은 하나만 있었어요. 모든 편지와 택배가 이곳을 거쳐 갔죠. 마을 사람들은 "우체국이 여러 개 있으면 편지가 어디로 갈지 헷갈릴 거야"라며 하나만 두기로 했어요.

마을 방송탑도 하나였어요. 중요한 소식이나 긴급상황을 알릴 때 한 곳에서 일관성 있게 방송했어요. "여러 개가 있으면 서로 다른 소식을 전해서 혼란스러울 거야"라고 생각했거든요.

마을 도서관도 하나만 있었어요. 모든 책을 한 곳에 모아두니까 찾기도 쉽고, 사서분도 효율적으로 관리할 수 있었어요.

각자 가지는 것들 (싱글톤이 부적절한 경우):

하지만 은 가족마다 각자 가졌어요. 가족의 생활 패턴이 모두 달랐고, 필요한 공간도 달랐거든요. "모든 가족이 한 집에 살면 정말 복잡할 거야"라며 각자 집을 가지기로 했어요.

텃밭도 집마다 따로 가졌어요. 가족마다 좋아하는 채소가 달랐고, 가꾸는 방식도 달랐어요. "하나의 텃밭을 모든 가족이 공유하면 누가 어떤 채소를 심을지 다툴 거야"라고 생각했어요.

일기장도 사람마다 따로 가졌어요. 개인적인 이야기와 느낌은 각자 간직하고 싶었거든요.

마을의 지혜:

이 작은 마을이 평화롭게 지낼 수 있었던 이유는 무엇을 공유하고 무엇을 개별적으로 가져야 하는지를 현명하게 구분했기 때문이에요.

공유하면 더 효율적인 것은 함께 사용하고, 개별적으로 가져야 하는 것은 각자 가지게 했죠. 만약 모든 것을 공유했다면 불편했을 거고, 모든 것을 개별적으로 가졌다면 자원이 낭비되었을 거예요.

프로그래밍에서도 마찬가지예요. 싱글톤은 마을의 우체국, 방송탑, 도서관 같은 역할을 하는 것에만 사용해야 하고, 일반 클래스는 집, 텃밭, 일기장 같은 역할을 하는 것들에 사용하는 거예요.

🧠 흔한 실수들과 주의점

싱글톤을 사용할 때 자주 하는 실수들을 미리 알아두면 같은 실수를 피할 수 있어요.

❌ 실수 1: 모든 것을 싱글톤으로 만들기

// 이렇게 하면 문제가 돼요!
class 잘못된예시_모든것싱글톤 {
  static #사용자인스턴스;      // 사용자 관리
  static #게시물인스턴스;      // 게시물 관리
  static #상품인스턴스;        // 상품 관리

  static 사용자가져오기() {
    // 사용자 인스턴스가 없다면 새로 만듭니다
    if (!잘못된예시_모든것싱글톤.#사용자인스턴스) {
      잘못된예시_모든것싱글톤.#사용자인스턴스 = { 이름: "", 나이: 0 };
    }
    return 잘못된예시_모든것싱글톤.#사용자인스턴스;  // 사용자 인스턴스 반환
  }

  // 게시물도 싱글톤, 상품도 싱글톤... (잘못된 방법!)
}

// 올바른 방법: 정말 필요한 것만 싱글톤으로!
class 설정관리자 {
  static #인스턴스;

  static 가져오기() {
    // 설정 관리자가 없다면 새로 만듭니다
    if (!설정관리자.#인스턴스) {
      설정관리자.#인스턴스 = new 설정관리자();
    }
    return 설정관리자.#인스턴스;    // 설정 관리자 반환
  }
}

// 사용자는 일반 클래스로!
class 사용자 {
  constructor(이름, 나이) {
    this.이름 = 이름;           // 각 사용자마다 자신만의 이름
    this.나이 = 나이;           // 각 사용자마다 자신만의 나이
  }
}

왜 문제가 될까요? 모든 것을 싱글톤으로 만들면 프로그램이 경직되고 유연성을 잃어버려요. 마치 모든 물건을 하나씩만 두고 모든 사람이 번갈아 가며 사용하는 것처럼 매우 불편해져요.

❌ 실수 2: 복잡한 로직을 싱글톤에 넣기

// 이렇게 하면 문제가 돼요!
class 잘못된예시_복잡한싱글톤 {
  static #인스턴스;

  복잡한게임로직() {
    // 게임의 모든 로직을 여기에 넣음 (문제!)
    this.캐릭터움직이기();      // 캐릭터 이동 처리
    this.몬스터생성하기();      // 몬스터 생성 처리
    this.아이템관리하기();      // 아이템 관리 처리
    this.점수계산하기();        // 점수 계산 처리
    this.사운드재생하기();      // 소리 재생 처리
    // ... 수백 줄의 코드
  }
}

// 올바른 방법: 단순하고 명확하게!
class 게임설정 {
  static #인스턴스;
  #볼륨 = 50;               // 볼륨 설정값
  #난이도 = "보통";         // 난이도 설정값

  볼륨설정(값) {
    this.#볼륨 = 값;         // 볼륨값 설정
  }

  볼륨가져오기() {
    return this.#볼륨;       // 현재 볼륨값 반환
  }
}

왜 문제가 될까요? 싱글톤에 너무 많은 기능을 넣으면 유지보수가 어려워지고, 다른 코드들과 강하게 연결되어 문제가 생겨요. 싱글톤은 간단하고 명확한 역할만 담당해야 해요.

❌ 실수 3: 핵심 기능을 싱글톤으로 만들기

// 이렇게 하면 문제가 돼요!
class 잘못된예시_주문처리싱글톤 {
  static #인스턴스;

  주문처리(상품, 수량, 사용자) {
    // 실제 핵심 기능 (문제!)
    console.log("주문 처리 중...");     // 주문 처리 메시지
    return "주문 완료";                 // 처리 결과
  }
}

// 올바른 방법: 핵심 기능은 일반 클래스로!
class 주문처리기 {
  constructor(결제시스템, 재고관리시스템) {
    this.결제시스템 = 결제시스템;           // 결제 시스템
    this.재고관리시스템 = 재고관리시스템;   // 재고 관리 시스템
  }

  주문처리(상품, 수량, 사용자) {
    console.log("주문 처리 중...");       // 주문 처리 메시지
    return "주문 완료";                   // 처리 결과
  }
}

왜 문제가 될까요? 핵심 기능은 프로그램의 가장 중요한 부분이므로 독립적이고 유연해야 해요. 싱글톤으로 만들면 테스트하기 어렵고 다양한 상황에 대응하기 힘들어져요.

✏️ 함께 풀어보는 연습문제

마음이 차분해지는 시간이에요. 배운 내용을 연습문제를 통해 천천히 익혀보겠어요.

Q1. 다음 상황들 중에서 싱글톤 패턴을 사용하면 좋을 것들을 모두 골라보세요!

A) 온라인 쇼핑몰의 상품 정보
B) 게임의 배경음악 재생기
C) 블로그의 게시물
D) 앱의 언어 설정 관리자
E) 채팅 앱의 메시지

// 정답: B, D

// B) 게임의 배경음악 재생기 (싱글톤이 적합해요!)
class 배경음악재생기 {
  static #인스턴스;
  #현재곡 = null;          // 현재 재생 중인 곡
  #볼륨 = 50;              // 음악 볼륨
  #재생중 = false;         // 재생 상태

  static 가져오기() {
    // 재생기가 없다면 새로 만듭니다
    if (!배경음악재생기.#인스턴스) {
      배경음악재생기.#인스턴스 = new 배경음악재생기();
    }
    return 배경음악재생기.#인스턴스;   // 재생기 반환
  }

  음악재생(곡이름) {
    this.#현재곡 = 곡이름;     // 재생할 곡 설정
    this.#재생중 = true;       // 재생 상태로 변경
    console.log(`🎵 ${곡이름}을 재생해요!`);
  }

  음악정지() {
    this.#재생중 = false;     // 재생 정지
    console.log("🔇 음악을 정지했어요!");
  }
}

// D) 앱의 언어 설정 관리자 (싱글톤이 적합해요!)
class 언어설정관리자 {
  static #인스턴스;
  #현재언어 = "한국어";       // 현재 설정된 언어

  static 가져오기() {
    // 언어 관리자가 없다면 새로 만듭니다
    if (!언어설정관리자.#인스턴스) {
      언어설정관리자.#인스턴스 = new 언어설정관리자();
    }
    return 언어설정관리자.#인스턴스;   // 언어 관리자 반환
  }

  언어변경(언어) {
    this.#현재언어 = 언어;      // 언어 설정 변경
    console.log(`🌍 언어가 ${언어}로 변경되었어요!`);
  }

  현재언어가져오기() {
    return this.#현재언어;      // 현재 언어 반환
  }
}

// 이유: 게임에서 배경음악은 하나만 재생되고, 앱의 언어 설정도 
// 전체 앱에서 동일하게 적용되어야 하므로 싱글톤이 적합해요!

Q2. 싱글톤이 적절하지 않은 경우의 예시를 만들고 왜 그런지 설명해보세요.

// 부적절한 예시: 학생 성적을 싱글톤으로 관리하기
class 잘못된예시_성적관리싱글톤 {
  static #인스턴스;
  #학생이름 = "";           // 학생의 이름
  #성적들 = [];             // 학생의 성적 목록

  static 가져오기() {
    // 성적 관리자가 없다면 새로 만듭니다
    if (!잘못된예시_성적관리싱글톤.#인스턴스) {
      잘못된예시_성적관리싱글톤.#인스턴스 = new 잘못된예시_성적관리싱글톤();
    }
    return 잘못된예시_성적관리싱글톤.#인스턴스;
  }

  학생설정(이름) {
    this.#학생이름 = 이름;     // 학생 이름 설정
    this.#성적들 = [];         // 성적 목록 초기화
  }

  성적추가(과목, 점수) {
    // 새로운 성적을 추가합니다
    this.#성적들.push({ 과목, 점수 });
  }

  성적확인() {
    console.log(`${this.#학생이름}의 성적:`, this.#성적들);
  }
}

// 문제 상황 보여주기
const 철수성적 = 잘못된예시_성적관리싱글톤.가져오기();
철수성적.학생설정("철수");                    // 철수 정보 설정
철수성적.성적추가("수학", 90);               // 철수 수학 성적 추가
철수성적.성적추가("국어", 85);               // 철수 국어 성적 추가

const 영희성적 = 잘못된예시_성적관리싱글톤.가져오기();
영희성적.학생설정("영희");                    // 영희 정보 설정 (철수 데이터가 사라져요!)
영희성적.성적추가("수학", 95);               // 영희 수학 성적 추가

철수성적.성적확인();  // "영희의 성적: [{ 과목: '수학', 점수: 95 }]" (문제!)

console.log("❌ 왜 적절하지 않을까요?");
console.log("1. 여러 학생의 성적을 동시에 관리할 수 없어요");
console.log("2. 한 학생의 정보를 바꾸면 다른 학생 정보가 사라져요");
console.log("3. 실제로는 학생마다 각자의 성적 정보가 필요해요");

// 올바른 방법: 일반 클래스 사용하기
class 올바른_학생성적 {
  constructor(학생이름) {
    this.학생이름 = 학생이름;   // 각 학생마다 자신만의 이름
    this.성적들 = [];           // 각 학생마다 자신만의 성적 목록
  }

  성적추가(과목, 점수) {
    // 성적을 추가합니다
    this.성적들.push({ 과목, 점수 });
  }

  성적확인() {
    console.log(`${this.학생이름}의 성적:`, this.성적들);
  }
}

console.log("\n✅ 올바른 방법:");
const 올바른철수 = new 올바른_학생성적("철수");  // 철수만의 성적표
const 올바른영희 = new 올바른_학생성적("영희");  // 영희만의 성적표

올바른철수.성적추가("수학", 90);               // 철수 성적 추가
올바른영희.성적추가("수학", 95);               // 영희 성적 추가

올바른철수.성적확인();  // "철수의 성적: [{ 과목: '수학', 점수: 90 }]"
올바른영희.성적확인();  // "영희의 성적: [{ 과목: '수학', 점수: 95 }]"

Q3. 간단한 앱 테마 관리자를 싱글톤으로 만들어보세요!

class 앱테마관리자 {
  static #인스턴스;

  #현재테마 = "밝은테마";     // 현재 설정된 테마
  #사용가능테마 = ["밝은테마", "어두운테마", "컬러풀테마"];   // 사용 가능한 테마들

  constructor() {
    // 이미 테마 관리자가 있다면 기존 것을 돌려줍니다
    if (앱테마관리자.#인스턴스) {
      return 앱테마관리자.#인스턴스;
    }

    console.log("🎨 앱 테마 관리자가 준비되었어요!");
    앱테마관리자.#인스턴스 = this;    // 자기 자신을 저장합니다
  }

  static 가져오기() {
    // 테마 관리자가 없다면 새로 만듭니다
    if (!앱테마관리자.#인스턴스) {
      앱테마관리자.#인스턴스 = new 앱테마관리자();
    }
    return 앱테마관리자.#인스턴스;   // 테마 관리자 반환
  }

  테마변경(새테마) {
    // 새로운 테마가 사용 가능한 테마인지 확인합니다
    if (this.#사용가능테마.includes(새테마)) {
      const 이전테마 = this.#현재테마;      // 이전 테마 기억
      this.#현재테마 = 새테마;              // 새로운 테마로 변경
      console.log(`🌈 테마가 ${이전테마}에서 ${새테마}로 변경되었어요!`);
      this.#테마적용();                     // 테마를 실제로 적용
      return true;                          // 변경 성공
    } else {
      console.log(`❌ ${새테마}는 사용할 수 없는 테마예요!`);
      console.log(`📋 사용 가능한 테마: ${this.#사용가능테마.join(", ")}`);
      return false;                         // 변경 실패
    }
  }

  현재테마가져오기() {
    console.log(`🎯 현재 테마: ${this.#현재테마}`);
    return this.#현재테마;                  // 현재 테마 반환
  }

  사용가능테마목록() {
    console.log(`📋 사용 가능한 테마: ${this.#사용가능테마.join(", ")}`);
    return [...this.#사용가능테마];         // 테마 목록의 사본 반환
  }

  #테마적용() {
    console.log(`✨ ${this.#현재테마}가 앱 전체에 적용되었어요!`);
    // 실제로는 여기서 앱의 색상, 폰트 등을 변경하는 코드가 들어갑니다
  }
}

// 테마 관리자 사용해보기
const 테마관리자 = 앱테마관리자.가져오기();

테마관리자.현재테마가져오기();      // 현재 테마: 밝은테마
테마관리자.사용가능테마목록();      // 사용 가능한 테마들 표시
테마관리자.테마변경("어두운테마");   // 어두운테마로 변경
테마관리자.테마변경("무지개테마");   // 없는 테마라서 실패
테마관리자.현재테마가져오기();      // 현재 테마: 어두운테마

📚 이전에 배운 것들 되돌아보기

18단원을 배우고 있는 여러분! 지금까지 정말 많은 것들을 배웠네요. 이전에 배운 중요한 내용들을 잠시 되돌아보면서 기억을 되살려보겠어요.

🎯 되돌아보기 1: 모듈 시스템 (17단원)

// Q: 다음 코드에서 export와 import가 어떻게 작동하는지 설명해보세요!

// utils.js 파일
export function 더하기(a, b) {
  return a + b;
}

export function 곱하기(a, b) {
  return a * b;
}

export default function 인사하기(이름) {
  return `안녕하세요, ${이름}님!`;
}

// main.js 파일
import 인사하기, { 더하기, 곱하기 } from './utils.js';

console.log(인사하기("철수"));           // "안녕하세요, 철수님!"
console.log(더하기(5, 3));              // 8
console.log(곱하기(4, 6));              // 24

해답과 설명:

  • export function 더하기는 일반 내보내기로, 중괄호 {} 안에 넣어서 가져와야 해요
  • export default function 인사하기는 기본 내보내기로, 중괄호 없이 바로 가져올 수 있어요
  • 하나의 파일에서 기본 내보내기는 하나만 있을 수 있지만, 일반 내보내기는 여러 개 가능해요
  • 모듈을 사용하면 코드를 깔끔하게 정리할 수 있고, 재사용하기도 편해요!

🎯 되돌아보기 2: 클래스와 상속 (15-16단원)

// Q: 다음 클래스 상속 코드의 실행 결과를 예상해보세요!

class 동물 {
  constructor(이름) {
    this.이름 = 이름;
  }

  소리내기() {
    console.log(`${this.이름}가 소리를 내요!`);
  }
}

class 개 extends 동물 {
  constructor(이름, 품종) {
    super(이름);              // 부모 클래스의 constructor 호출
    this.품종 = 품종;
  }

  소리내기() {                 // 메서드 오버라이딩
    console.log(`${this.이름}가 멍멍 짖어요!`);
  }

  꼬리흔들기() {
    console.log(`${this.이름}가 꼬리를 흔들어요!`);
  }
}

const 내강아지 = new 개("초코", "골든리트리버");
내강아지.소리내기();           // ?
내강아지.꼬리흔들기();         // ?

해답과 설명:

  • 내강아지.소리내기(); → "초코가 멍멍 짖어요!" (자식 클래스의 메서드가 실행돼요)
  • 내강아지.꼬리흔들기(); → "초코가 꼬리를 흔들어요!" (자식 클래스만의 메서드예요)
  • extends 키워드로 상속받으면 부모의 모든 기능을 사용할 수 있어요
  • super()로 부모의 constructor를 호출해서 초기화할 수 있어요
  • 자식 클래스에서 같은 이름의 메서드를 만들면 부모 메서드를 덮어써요 (오버라이딩)

🎯 되돌아보기 3: 비동기 처리 (13단원)

// Q: 다음 Promise 코드가 어떤 순서로 실행될지 예상해보세요!

console.log("1. 시작!");

setTimeout(() => {
  console.log("2. 3초 후 실행!");
}, 3000);

Promise.resolve("3. Promise 완료!")
  .then(결과 => {
    console.log(결과);
    return "4. 두 번째 then!";
  })
  .then(결과 => {
    console.log(결과);
  });

console.log("5. 끝!");

해답과 설명:
실행 순서는 다음과 같아요:

  1. "1. 시작!" (동기 코드)
  2. "5. 끝!" (동기 코드)
  3. "3. Promise 완료!" (Promise - 마이크로태스크)
  4. "4. 두 번째 then!" (Promise - 마이크로태스크)
  5. "2. 3초 후 실행!" (setTimeout - 매크로태스크, 3초 후)
  • 동기 코드가 먼저 실행돼요
  • Promise는 setTimeout보다 우선순위가 높아요
  • 비동기 코드는 콜백큐에서 기다렸다가 메인 코드가 끝나면 실행돼요

🤔 조금 더 깊이 생각해보기

Q1. 다음 시나리오에서 싱글톤 패턴이 적합한지 판단하고 그 이유를 설명하세요.

시나리오들:

  • 온라인 게임의 플레이어 캐릭터
  • 웹브라우저의 쿠키 관리 시스템
  • 전자상거래 사이트의 주문 내역
  • 모바일 앱의 GPS 위치 서비스

:

  1. 온라인 게임의 플레이어 캐릭터: 적합하지 않음

    • 이유: 게임에는 여러 플레이어가 있고, 각각 독립적인 캐릭터를 가져야 합니다. 싱글톤으로 만들면 모든 플레이어가 같은 캐릭터가 되어버려요.
  2. 웹브라우저의 쿠키 관리 시스템: 적합함

    • 이유: 브라우저의 쿠키는 브라우저 전체에서 공유되는 자원입니다. 여러 개의 쿠키 관리자가 있으면 충돌이 생길 수 있어요.
  3. 전자상거래 사이트의 주문 내역: 적합하지 않음

    • 이유: 사용자마다 다른 주문 내역을 가져야 합니다. 싱글톤으로 만들면 모든 사용자의 주문이 섞여버려요.
  4. 모바일 앱의 GPS 위치 서비스: 적합함

    • 이유: 기기의 GPS는 하나이고, 앱 전체에서 동일한 위치 정보를 사용해야 합니다. 여러 개가 있으면 배터리도 많이 소모되고 충돌할 수 있어요.

Q2. 싱글톤 패턴 대신 사용할 수 있는 다른 방법들을 제시하고 설명해보세요.

:

  1. 모듈 시스템 사용하기

    // config.js 파일
    let theme = "라이트";         // 테마 설정값
    
    export function setTheme(newTheme) {
      theme = newTheme;           // 테마를 새로 설정해요
      console.log(`테마가 ${newTheme}로 변경되었어요!`);
    }
    
    export function getTheme() {
      return theme;               // 현재 테마를 돌려줘요
    }
    
    // 다른 파일에서 사용
    import { setTheme, getTheme } from './config.js';

    장점: 간단하고 자연스러우며, 모듈 시스템과 잘 어울려요.

  2. 일반 객체 하나 만들어 공유하기

    // 설정 객체 하나 만들기
    const appConfig = {
      theme: "라이트",           // 테마 설정
      language: "한국어",        // 언어 설정
    
      setTheme(newTheme) {
        this.theme = newTheme;   // 테마를 새로 설정해요
        console.log(`테마가 ${newTheme}로 변경되었어요!`);
      },
    
      getTheme() {
        return this.theme;       // 현재 테마를 돌려줘요
      }
    };
    
    // 필요한 곳에 전달해서 사용
    function setupUI(config) {
      console.log(`현재 테마: ${config.getTheme()}`);
    }
    
    setupUI(appConfig);          // 설정 객체를 전달해서 사용

    장점: 테스트하기 쉽고, 의존성을 명확하게 관리할 수 있어요.

  3. 정적 클래스 사용하기

    class ConfigUtils {
      static theme = "라이트";   // 정적 테마 변수
    
      static setTheme(newTheme) {
        this.theme = newTheme;   // 테마를 새로 설정해요
        console.log(`테마가 ${newTheme}로 변경되었어요!`);
      }
    
      static getTheme() {
        return this.theme;       // 현재 테마를 돌려줘요
      }
    }
    
    // 사용방법
    ConfigUtils.setTheme("다크");  // 테마를 다크로 설정
    console.log(ConfigUtils.getTheme());  // 현재 테마 확인

    장점: 인스턴스를 만들 필요가 없고, 간단한 유틸리티에 적합해요.

지금까지 싱글톤 패턴을 언제 사용하면 좋은지에 대해 배우면서, 현명한 판단을 내리는 방법을 알아보았어요. 가장 중요한 것은 무작정 싱글톤을 사용하는 것이 아니라, 정말 필요한 상황에서만 신중하게 사용하는 것이에요.

마치 요리에서 향신료를 적당히 사용해야 맛있는 음식이 되는 것처럼, 싱글톤도 적절한 곳에 적절하게 사용해야 좋은 프로그램을 만들 수 있답니다! 🌿✨

✅ 학습 완료 체크리스트

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

학습 내용 이해했나요?
싱글톤 사용이 좋은 상황들
싱글톤을 피해야 할 상황들
실전 예제를 통한 활용법
자주 하는 실수들과 주의점
대안 방법들

📂 마무리 정보

오늘 배운 18.1.3 내용이 여러분의 마음속 지식 상자에 잘 정리되었나요? 다음 시간에는 또 다른 흥미로운 내용으로 만나요!

기억할 점: 싱글톤은 정말 필요한 곳에만 사용하세요. 무분별하게 사용하면 오히려 문제가 될 수 있어요!


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