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

18.1.1 싱글턴이란? - 세상에 하나뿐인 특별한 존재

thejavascript4kids 2025. 7. 26. 20:15

📘 18.1.1 싱글턴이란? - 세상에 하나뿐인 특별한 존재

어떤 것들은 세상에 하나만 있어야 할 때가 있습니다. 마치 우리 각자의 마음이 그러하듯이, 복제될 수 없고 대체될 수 없는 유일한 존재들이 있어요. 오늘부터 우리는 정말 깊이 있고 아름다운 디자인 패턴이라는 것을 배워볼 거예요. 코드의 세계에도 오랜 시간 검증된 지혜로운 해결 방법들이 있거든요.

그 중에서도 오늘은 "세상에 하나뿐인 특별한 존재"를 만드는 방법인 싱글턴 패턴에 대해 알아보겠습니다!

🎓 잠깐! 고급 내용 알림 🎓
싱글턴 패턴은 조금 어려운 고급 개념이에요. 지금까지 배운 클래스, 함수, 모듈을 잘 이해하고 있다면 충분히 따라올 수 있을 거예요. 어려우면 나중에 다시 와서 읽어도 괜찮아요!

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

싱글턴 패턴을 배우기 전에, 우리가 오늘 만날 새로운 개념들과 먼저 인사해보겠습니다.

용어 의미
싱글턴 패턴 하나의 클래스에서 오직 단 하나의 객체만 만들 수 있게 하는 특별한 만들기 방법이에요
인스턴스 클래스라는 설계도를 바탕으로 실제로 만들어진 구체적인 객체를 말해요
디자인 패턴 프로그래밍에서 자주 발생하는 문제들을 해결하기 위한 검증된 해결 방법의 모음이에요
전역 접근점 프로그램의 어느 곳에서든 접근할 수 있는 공통 통로를 의미해요

싱글턴(Singleton)이라는 이름은 "single(하나의)"과 "ton(무언가)"이 합쳐진 말로, "오직 하나만 존재하는 것"이라는 뜻이에요. 마치 우리 반에서 반장이 한 명뿐인 것처럼 말이에요!

✨ 싱글턴 패턴의 핵심 개념

싱글턴 패턴은 정말 특별한 개념이에요! 여러분이 우리 학교에 다니는 학생이라고 상상해보세요. 이 학교에는 아주 특별한 규칙이 있어요.

가장 중요한 특징은 클래스에서 오직 하나의 객체만 만들 수 있다는 점이에요. 일반적인 클래스는 new 키워드를 사용해서 원하는 만큼 많은 객체를 만들 수 있지만, 싱글턴은 달라요. 첫 번째 객체가 만들어진 후에는 더 이상 새로운 객체를 만들 수 없고, 대신 이미 만들어진 그 하나의 객체를 계속 사용하게 되어요.

두 번째 특징은 어디서든 접근할 수 있는 공용 통로를 제공한다는 것이에요. 프로그램의 어느 부분에서든 같은 객체에 접근할 수 있어서, 모든 곳에서 일관된 정보와 기능을 사용할 수 있어요.

세 번째로는 메모리와 성능을 효율적으로 사용할 수 있다는 장점이 있어요. 무거운 객체를 여러 번 만들지 않고 한 번만 만들어서 재사용하니까 컴퓨터 자원을 절약할 수 있어요.

마지막으로 공유되는 자원이나 상태를 안전하게 관리할 수 있어요. 여러 부분에서 같은 정보를 사용해야 할 때, 싱글턴을 통해 일관성 있게 관리할 수 있어요.

비유로 이해하기: 우리 학교의 유일한 교장 선생님

싱글턴 패턴을 더 가까이에서 이해하기 위해서 '우리 학교의 교장 선생님' 이야기를 들려드릴게요.

우리 학교에는 교장 선생님이 딱 한 분만 계세요. 1학년 교실에서도, 6학년 교실에서도, 음악실에서도, 운동장에서도 "교장 선생님"이라고 하면 모두 같은 그 한 분을 가리켜요.

만약 학교에 교장 선생님이 여러 분 계신다면 어떨까요? 1학년용 교장 선생님, 2학년용 교장 선생님, 3학년용 교장 선생님... 이렇게 되면 대혼란이 일어날 거예요! 각 교장 선생님마다 다른 규칙을 만드시고, 다른 결정을 내리실 수 있으니까요.

하지만 교장 선생님이 한 분뿐이시면:

  • 모든 학생이 같은 교장 선생님을 만날 수 있어요 (일관성 있는 관리)
  • 학교 전체의 규칙이 통일되어 있어요 (전역 상태 관리)
  • 교장실을 여러 개 운영할 필요가 없어서 효율적이에요 (자원 절약)
  • 어떤 교실에 있든 교장 선생님은 항상 같은 분이에요 (전역 접근점)
// 교장실 싱글턴 예시
class PrincipalOffice {
    static instance = null;  // 학교에 교장실은 하나뿐

    constructor() {
        if (PrincipalOffice.instance) {
            return PrincipalOffice.instance;  // 이미 있는 교장실 반환
        }
        PrincipalOffice.instance = this;
    }

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

    makeAnnouncement(message) {
        console.log(`📢 교장실 방송: ${message}`);
    }
}

// 어느 교실에서든 같은 교장실에 접근
let officeFromRoom1 = PrincipalOffice.getInstance();
let officeFromRoom6 = PrincipalOffice.getInstance();

console.log(officeFromRoom1 === officeFromRoom6);  // true - 같은 교장실!

이처럼 싱글턴 패턴은 "하나뿐이어야 하는 특별한 존재"를 프로그래밍으로 구현하는 방법이에요.

🎯 싱글턴 패턴을 배우는 이유

그렇다면 우리는 왜 이런 단축키를 만들어야 할까요? 실생활과 프로그래밍에서 정말 유용한 이유들이 많이 있어요.

첫째, 중요한 자원을 효율적으로 관리할 수 있어요. 데이터베이스 연결, 파일 시스템 접근, 네트워크 연결처럼 만드는 데 비용이 많이 드는 객체들을 한 번만 만들어서 재사용할 수 있어요. 마치 학교에서 복사기를 한 대만 두고 모든 선생님이 공유해서 사용하는 것과 같죠.

둘째, 전역 설정과 상태를 일관되게 관리할 수 있어요. 프로그램 전체에서 사용하는 설정 정보나 현재 상태를 한 곳에서 관리하면, 어디서 변경하든 모든 곳에 즉시 반영되어요. 마치 학교의 종이 하나뿐이어서 모든 교실이 같은 시간에 수업을 시작하고 끝내는 것과 같아요.

셋째, 안전한 접근 제어가 가능해요. 중요한 리소스에 대한 접근을 하나의 통로로 제한해서 무분별한 사용을 막고, 필요한 검증이나 기록을 할 수 있어요.

넷째, 메모리 사용량 최적화에 도움이 돼요. 같은 기능을 하는 객체를 여러 개 만들지 않고 하나만 만들어서 공유하니까 메모리를 절약할 수 있어요.

마지막으로, 실제 개발 현장에서 자주 사용되는 패턴이기 때문이에요. 로깅 시스템, 설정 관리자, 캐시 시스템 등 많은 곳에서 싱글턴 패턴을 사용하고 있어서, 이를 이해하면 실무에서 큰 도움이 돼요.

⚙️ scripts 작성법 배우기

이제 실제로 npm scripts를 작성하는 방법을 배워보겠습니다.

기본 싱글턴 구조:

class Singleton {
    // 유일한 인스턴스를 저장할 정적 변수
    static instance = null;

    // 생성자 - 여러 번 호출되어도 같은 인스턴스 반환
    constructor() {
        if (Singleton.instance) {
            return Singleton.instance;
        }
        Singleton.instance = this;
    }

    // 인스턴스를 가져오는 공식 방법
    static getInstance() {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
}

실제 사용 예시:

// 두 변수 모두 같은 객체를 가리킴
let obj1 = Singleton.getInstance();
let obj2 = Singleton.getInstance();

console.log(obj1 === obj2); // true - 같은 객체!

싱글턴의 핵심 요소들:

  1. 정적 인스턴스 변수: 유일한 객체를 저장하는 곳
  2. 생성자 제어: 외부에서 직접 객체를 만들지 못하게 제어
  3. getInstance() 메서드: 인스턴스에 접근하는 공식 방법
  4. 인스턴스 확인 로직: 이미 있으면 기존 것을 반환

🧪 예제로 익혀보기

이제 실제 예제를 통해 싱글턴 패턴이 어떻게 작동하는지 자세히 살펴보겠어요.

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

첫 번째 예제에서는 게임의 전역 설정을 관리하는 싱글턴을 만들어보겠어요.

// 게임 설정을 관리하는 싱글턴 클래스
class GameSettings {
    // 유일한 인스턴스를 저장할 정적 변수
    static instance = null;

    // 게임 설정값들을 저장할 속성
    constructor() {
        if (GameSettings.instance) {
            console.log("이미 설정 관리자가 존재해요. 기존 것을 반환할게요.");
            return GameSettings.instance;
        }

        console.log("🎮 게임 설정 관리자가 생성되었어요!");

        // 설정값들 초기화
        this.settings = {
            playerName: "플레이어",
            difficulty: "보통",
            soundVolume: 50,
            musicVolume: 30,
            language: "한국어"
        };

        GameSettings.instance = this;
    }

    // 인스턴스를 가져오는 공식 메서드
    static getInstance() {
        if (!GameSettings.instance) {
            GameSettings.instance = new GameSettings();
        }
        return GameSettings.instance;
    }

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

    // 설정값 변경하기
    setSetting(key, value) {
        if (this.settings.hasOwnProperty(key)) {
            let oldValue = this.settings[key];
            this.settings[key] = value;
            console.log(`⚙️ ${key}: ${oldValue} → ${value}`);
            return true;
        } else {
            console.log(`❌ 알 수 없는 설정: ${key}`);
            return false;
        }
    }

    // 모든 설정 보기
    showAllSettings() {
        console.log("=== 현재 게임 설정 ===");
        for (let key in this.settings) {
            console.log(`${key}: ${this.settings[key]}`);
        }
    }

    // 설정 초기화
    resetSettings() {
        this.settings = {
            playerName: "플레이어",
            difficulty: "보통", 
            soundVolume: 50,
            musicVolume: 30,
            language: "한국어"
        };
        console.log("🔄 게임 설정이 초기화되었어요.");
    }
}

// 메인 메뉴에서 설정 관리자 사용
console.log("=== 메인 메뉴에서 설정 변경 ===");
let mainMenuSettings = GameSettings.getInstance();
mainMenuSettings.setSetting("playerName", "용감한 모험가");
mainMenuSettings.setSetting("difficulty", "어려움");

// 게임 화면에서 같은 설정 관리자 사용
console.log("\n=== 게임 화면에서 설정 확인 ===");
let gameScreenSettings = GameSettings.getInstance();
console.log(`현재 플레이어: ${gameScreenSettings.getSetting("playerName")}`);
console.log(`난이도: ${gameScreenSettings.getSetting("difficulty")}`);

// 두 변수가 같은 객체인지 확인
console.log(`\n같은 설정 관리자인가요? ${mainMenuSettings === gameScreenSettings}`);

// 옵션 메뉴에서 모든 설정 보기
console.log("\n=== 옵션 메뉴에서 전체 설정 확인 ===");
let optionMenuSettings = GameSettings.getInstance();
optionMenuSettings.showAllSettings();

이 과정을 단계별로 살펴보면, 먼저 게임의 모든 설정을 하나의 객체에서 관리할 수 있도록 설계했어요. 그다음 프로그램의 어느 부분에서든 getInstance()를 호출하면 항상 같은 설정 관리자를 받을 수 있게 했어요. 마지막으로 어느 곳에서 설정을 변경하든 모든 곳에서 즉시 변경된 값을 볼 수 있다는 것을 확인했어요.

🔹 예제 2: 학습 진도 추적기 만들기

두 번째 예제에서는 학습 진도를 추적하는 싱글턴을 만들어서 더 복잡한 상태 관리를 연습해보겠어요.

// 학습 진도를 추적하는 싱글턴 클래스
class StudyTracker {
    // 유일한 인스턴스 저장소
    static instance = null;

    // 생성자
    constructor(studentName = "학습자") {
        if (StudyTracker.instance) {
            console.log("기존 학습 추적기를 사용해요.");
            return StudyTracker.instance;
        }

        // 학습 데이터를 저장할 속성들
        this.studentName = studentName;
        this.subjects = {}; // 과목별 진도 정보
        this.totalStudyTime = 0; // 총 학습 시간 (분)
        this.currentSubject = null; // 현재 공부 중인 과목

        console.log(`📚 ${studentName}의 학습 추적기가 시작되었어요!`);
        StudyTracker.instance = this;
    }

    // 인스턴스 가져오기
    static getInstance(studentName) {
        if (!StudyTracker.instance) {
            StudyTracker.instance = new StudyTracker(studentName);
        }
        return StudyTracker.instance;
    }

    // 새로운 과목 추가하기
    addSubject(subjectName, totalLessons) {
        if (!this.subjects[subjectName]) {
            this.subjects[subjectName] = {
                totalLessons: totalLessons,
                completedLessons: 0,
                studyTime: 0 // 해당 과목 학습 시간
            };
            console.log(`📖 새 과목 추가: ${subjectName} (총 ${totalLessons}강의)`);
        } else {
            console.log(`이미 등록된 과목이에요: ${subjectName}`);
        }
    }

    // 학습 완료 처리
    completeLesson(subjectName, studyMinutes = 30) {
        if (!this.subjects[subjectName]) {
            console.log(`❌ 등록되지 않은 과목: ${subjectName}`);
            return false;
        }

        // 진도 업데이트
        let subject = this.subjects[subjectName];
        if (subject.completedLessons < subject.totalLessons) {
            subject.completedLessons++;
            subject.studyTime += studyMinutes;
            this.totalStudyTime += studyMinutes;

            console.log(`✅ ${subjectName} 1강의 완료!`);
            console.log(`   학습 시간: ${studyMinutes}분`);
            console.log(`   완료 강의: ${subject.completedLessons}/${subject.totalLessons}`);
            return true;
        } else {
            console.log(`🎉 ${subjectName} 과목을 모두 완료했어요!`);
            return false;
        }
    }

    // 특정 과목의 진도 확인
    getSubjectProgress(subjectName) {
        let subject = this.subjects[subjectName];
        if (!subject) {
            console.log(`❌ 등록되지 않은 과목: ${subjectName}`);
            return null;
        }

        let progressPercent = Math.round((subject.completedLessons / subject.totalLessons) * 100);
        return {
            subject: subjectName,
            completed: subject.completedLessons,
            total: subject.totalLessons,
            progress: progressPercent,
            studyTime: subject.studyTime
        };
    }

    // 전체 학습 현황 보기
    showOverallProgress() {
        console.log(`\n📊 === ${this.studentName}의 학습 현황 ===`);
        console.log(`총 학습 시간: ${this.totalStudyTime}분`);
        console.log(`등록 과목 수: ${Object.keys(this.subjects).length}개`);

        for (let subjectName in this.subjects) {
            let progress = this.getSubjectProgress(subjectName);
            console.log(`${subjectName}: ${progress.progress}% (${progress.completed}/${progress.total}) - ${progress.studyTime}분`);
        }
    }
}

// 수학 시간에 학습 추적기 사용
console.log("=== 수학 시간 ===");
let mathTracker = StudyTracker.getInstance("철수");
mathTracker.addSubject("수학", 20);
mathTracker.completeLesson("수학", 45);
mathTracker.completeLesson("수학", 40);

// 영어 시간에 같은 추적기 사용
console.log("\n=== 영어 시간 ===");
let englishTracker = StudyTracker.getInstance(); // 같은 인스턴스
englishTracker.addSubject("영어", 15);
englishTracker.completeLesson("영어", 35);

// 학부모가 전체 진도 확인
console.log("\n=== 학부모 진도 확인 ===");
let parentTracker = StudyTracker.getInstance(); // 여전히 같은 인스턴스
parentTracker.showOverallProgress();

// 모든 추적기가 같은 객체인지 확인
console.log(`\n모든 추적기가 같은 객체인가요? ${mathTracker === englishTracker && englishTracker === parentTracker}`);

이 예제를 통해 우리는 몇 가지 중요한 점을 배웠어요. 먼저 싱글턴이 복잡한 상태 정보를 안전하게 관리할 수 있다는 것이에요. 그다음 프로그램의 다른 부분에서 접근해도 항상 최신 상태를 유지한다는 점이죠. 마지막으로 여러 과목의 학습 상태를 일관되게 추적할 수 있다는 것을 확인했어요.

🔄 싱글턴 패턴 사용 과정 정리

지금까지 학습한 싱글턴 패턴의 사용 과정을 자연스럽게 정리해보겠어요.

첫 번째 단계는 싱글턴이 필요한 상황 판단이에요. 전역적으로 접근해야 하는 자원, 상태 정보, 또는 여러 개가 있으면 문제가 되는 객체인지 확인하는 것이죠. 모든 것을 싱글턴으로 만들 필요는 없어요.

두 번째는 싱글턴 클래스 설계 단계예요. 정적 인스턴스 변수, 생성자 제어, getInstance() 메서드를 포함한 기본 구조를 만들어요. 이때 인스턴스가 중복 생성되지 않도록 주의깊게 설계해야 해요.

세 번째는 기능 구현 단계예요. 싱글턴이 실제로 해야 할 일들(설정 관리, 로깅, 상태 추적 등)을 메서드로 구현해요. 이때 내부 데이터를 적절히 보호하는 것이 좋아요.

마지막으로 가장 중요한 것은 일관된 접근 방법 사용이에요. 프로그램의 모든 부분에서 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: new 키워드로 직접 객체 생성하기

class ConfigManager {
    static instance = null;

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

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

// 잘못된 방법: 직접 new 사용
let config1 = new ConfigManager(); 
let config2 = new ConfigManager();

// 올바른 방법: getInstance() 사용
let config3 = ConfigManager.getInstance();
let config4 = ConfigManager.getInstance();

console.log(config1 === config2); // true (생성자에서 같은 인스턴스 반환)
console.log(config3 === config4); // true (정상적인 싱글턴 사용)

이런 실수가 발생하는 이유는 싱글턴 패턴의 원칙을 제대로 이해하지 못했기 때문이에요. 싱글턴은 getInstance() 메서드를 통해서만 접근하는 것이 원칙이에요.

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

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

    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 = null;
    // 사용자 관리 전용
}

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

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

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

❌ 실수 3: 싱글턴의 내부 상태를 외부에서 직접 수정하기

// 잘못된 설계: 외부에서 직접 접근 가능
class GameState {
    static instance = null;

    constructor() {
        if (GameState.instance) return GameState.instance;
        this.playerName = "";
        this.score = 0;
        this.level = 1;
        GameState.instance = this;
    }

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

// 문제가 되는 사용법
let gameState = GameState.getInstance();
gameState.score = 9999999; // 외부에서 직접 점수 조작!
gameState.level = 100;     // 외부에서 직접 레벨 조작!

// 올바른 방법: 메서드를 통한 제어된 접근
class BetterGameState {
    static instance = null;

    constructor() {
        if (BetterGameState.instance) return BetterGameState.instance;
        this._playerName = "";  // _ 표시로 private임을 나타냄
        this._score = 0;
        this._level = 1;
        BetterGameState.instance = this;
    }

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

    // 제어된 접근 메서드들
    getScore() {
        return this._score;
    }

    addScore(points) {
        if (points > 0) {
            this._score += points;
        }
    }

    levelUp() {
        this._level++;
        console.log(`레벨업! 현재 레벨: ${this._level}`);
    }
}

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

때로는 손끝으로 직접 써보는 것이 마음으로 이해하는 가장 확실한 길이 되기도 합니다. 프로그래밍에서도 마찬가지예요. 이제 배운 내용을 연습문제를 통해 차근차근 익혀보겠습니다.

Ex1) 간단한 점수 관리자 싱글턴을 만들어보자

// 점수를 관리하는 싱글턴 클래스를 만들어보세요
class ScoreManager {
    // 유일한 인스턴스를 저장할 정적 변수
    static instance = null;

    // 생성자
    constructor() {
        if (ScoreManager.instance) {
            console.log("기존 점수 관리자를 사용해요.");
            return ScoreManager.instance;
        }

        console.log("🎯 점수 관리자가 생성되었어요!");
        // 현재 점수와 최고 점수 초기화
        this.currentScore = 0;
        this.highScore = 0;

        ScoreManager.instance = this;
    }

    // 인스턴스 가져오기
    static getInstance() {
        if (!ScoreManager.instance) {
            ScoreManager.instance = new ScoreManager();
        }
        return ScoreManager.instance;
    }

    // 점수 추가하기
    addScore(points) {
        if (points > 0) {                                        
            this.currentScore += points;                        
            console.log(`+${points}점! 현재 점수: ${this.currentScore}점`);

            // 최고 점수 갱신 확인
            if (this.currentScore > this.highScore) {
                this.highScore = this.currentScore;            
                console.log("🏆 새로운 최고 점수예요!");
            }
        }
    }

    // 현재 점수 가져오기
    getCurrentScore() {
        return this.currentScore;
    }

    // 최고 점수 가져오기
    getHighScore() {
        return this.highScore;
    }

    // 게임 재시작 (현재 점수만 초기화)
    resetCurrentScore() {
        this.currentScore = 0;                                  
        console.log("🔄 게임이 재시작되었어요. 현재 점수: 0점");
    }

    // 모든 점수 초기화
    resetAllScores() {
        this.currentScore = 0;                                  
        this.highScore = 0;                                     
        console.log("🗑️ 모든 점수가 초기화되었어요.");
    }
}

// 게임 메인 화면에서 점수 관리자 사용
console.log("=== 게임 시작 ===");
let gameScore = ScoreManager.getInstance();
gameScore.addScore(100);
gameScore.addScore(250);

// 다른 게임 모듈에서 같은 점수 관리자 사용
console.log("\n=== 보너스 라운드 ===");
let bonusScore = ScoreManager.getInstance(); // 같은 인스턴스
bonusScore.addScore(500);

console.log(`\n현재 점수: ${bonusScore.getCurrentScore()}점`);
console.log(`최고 점수: ${bonusScore.getHighScore()}점`);
console.log(`같은 관리자인가요? ${gameScore === bonusScore}`);

이 연습을 통해 간단한 상태 관리를 하는 싱글턴의 기본 구조를 익힐 수 있어요.

📚 17단원 복습 문제

지난 시간에 배운 모듈 시스템을 복습해보겠습니다!

복습 문제 1: 모듈을 사용한 싱글턴 만들기

// scoreManager.js 파일 - 모듈로 내보내기
class ScoreManager {
    static instance = null;

    constructor() {
        if (ScoreManager.instance) {
            return ScoreManager.instance;
        }
        this.score = 0;
        ScoreManager.instance = this;
    }

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

    addScore(points) {
        this.score += points;
        console.log(`점수 추가: +${points}, 총점: ${this.score}`);
    }

    getScore() {
        return this.score;
    }
}

// ES6 모듈 방식으로 내보내기
export default ScoreManager;
// main.js 파일 - 모듈 가져와서 사용하기
// ES6 모듈 방식으로 가져오기
import ScoreManager from './scoreManager.js';

// 게임의 여러 부분에서 같은 점수 관리자 사용
const gameScore = ScoreManager.getInstance();
const menuScore = ScoreManager.getInstance();

gameScore.addScore(100);
console.log(`메뉴에서 확인한 점수: ${menuScore.getScore()}`); // 100

console.log(`같은 객체인가요? ${gameScore === menuScore}`); // true

해답 설명: 모듈 시스템과 싱글턴 패턴을 함께 사용하면, 모듈을 가져오는 모든 곳에서 동일한 싱글턴 인스턴스를 사용할 수 있어요. ES6 모듈의 특성상 한 번 가져온 모듈은 캐시되므로, 싱글턴 패턴과 잘 어울립니다.

복습 문제 2: CommonJS와 ES6 모듈의 차이점 설명

// CommonJS 방식 (Node.js)
// logger.js
class Logger {
    static instance = null;

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

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

    log(message) {
        console.log(`[LOG] ${new Date().toLocaleTimeString()}: ${message}`);
    }
}

// CommonJS 방식으로 내보내기
module.exports = Logger;
// main.js (CommonJS)
// CommonJS 방식으로 가져오기
const Logger = require('./logger.js');

const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();

logger1.log("첫 번째 로그");
logger2.log("두 번째 로그");

console.log(`같은 로거인가요? ${logger1 === logger2}`); // true

해답 설명:

  • CommonJS: require()module.exports 사용, 주로 Node.js 환경에서 사용
  • ES6 모듈: importexport 사용, 브라우저와 최신 Node.js에서 사용
  • 둘 다 모듈을 한 번만 로드하고 캐시하므로 싱글턴 패턴과 잘 맞아요
  • ES6 모듈이 더 최신 표준이고, 정적 분석이 가능해서 번들러에서 최적화하기 좋아요

지금까지 싱글턴 패턴의 모든 핵심 개념을 배웠어요. 이제 여러분도 "세상에 하나뿐이어야 하는 특별한 존재"들을 프로그래밍으로 안전하고 효율적으로 관리할 수 있을 거예요!

✅ 학습 완료 체크리스트

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

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

📂 마무리 정보

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

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


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