💡 싱글톤 패턴 (Singleton Pattern)

2025. 9. 9. 15:58·Computer Science/Design Pattern

하나의 클래스에 오직 하나의 인스턴스(객체)만 가지도록 보장하는 패턴입니다.

하나의 클래스를 기반으로 여러 개의 개별 인스턴스를 만들 수 있지만, 그렇게 하지 않고 단 하나의 인스턴스를 만들어 이를 기반으로 로직을 만드는 데 쓰이며, 보통 데이터베이스 연결 모듈처럼 프로그램 전체에서 유일해야 하는 객체에 사용됩니다.


 

📁싱글톤 패턴 요약 (ft. 얄코 버거 🍔)

영상(링크)에서는 싱글톤 패턴을 '얄코 버거' 프랜차이즈에 비유하여 설명합니다.

 

1. 클래스와 객체 (프랜차이즈 본사 vs 개별 매장)

  • 클래스 (Class): '얄코 버거'라는 프랜차이즈 본사(설계도) 자체입니다.
  • 인스턴스 객체 (Instance): 본사의 설계도를 바탕으로 여기저기 생긴 개별 매장입니다.
  • 인스턴스 변수 (Instance Field): '매장 위치', '점장 이름'처럼 각 매장마다 따로 존재해야 하는 정보입니다. 매장(객체)이 100개면 이 정보도 100개 생깁니다.
  • 정적 변수 (Static Field): '브랜드 이름(얄코 버거)', 'CEO 이름'처럼 프랜차이즈 본사 자체에 속한 정보입니다. 이 정보는 매장이 몇 개든 상관없이 프로그램 전체에서 단 하나만 존재합니다.

2. 인스턴스 변수 vs 정적 변수(매장 정보 vs 본사 정보)

싱글톤 패턴은 이 static 개념을 활용하여, 클래스가 "개별 매장(Instance)"을 여러 개 만들지 못하게 하고 "본사(Static)"처럼 단 하나만 존재하도록 강제하는 기술입니다.

예를 들어 '다크 모드' 설정 객체는 프로그램 전체에서 단 하나여야 합니다. 만약 여러 개가 생기면 UI가 꼬일 수 있기 때문이죠.

이것을 프랜차이즈 비유로 구현하는 방식은 다음과 같습니다.

  1. "가맹 신청을 막는다" (Private 생성자)
    • 외부에서 new(새 객체 생성 명령어)를 사용해 "개별 매장(객체)"을 마음대로 만들 수 없도록, 생성자(객체 만드는 함수)를 private(비공개)으로 선언합니다.
    • 이것은 마치 외부에서 가맹점 신청을 아예 막아버리는 것과 같습니다.
  2. "본사 내부에 직영 1호점을 차린다" (Static 변수)
    • 외부에서는 매장을 못 만들지만, 본사(클래스) 내부에서는 스스로 객체를 만들 수 있습니다.
    • 클래스는 "본사 건물 안에 유일한 직영 1호점", 즉 자기 자신 타입의 static 변수를 만들어 유일한 인스턴스를 이곳에 저장합니다.
  3. "유일한 공식 채널(정문)을 연다" (getInstance 메서드)
    • 모든 고객(다른 코드)이 이 "유일한 직영 1호점"에 접근할 수 있도록, 유일한 공식 통로인 public static 메서드(예: getInstance())를 엽니다.

3. 작동방식

  1. 어떤 코드(예: '버튼')가 Theme.getInstance() (직영 1호점 안내)를 최초로 호출합니다.
  2. 메서드는 "아직 직영점이 안 열었네?" (Static 변수가 비어있는지 체크)를 확인하고, 단 한 번 "본사 직영 1호점"(객체)을 생성하여 Static 변수에 저장합니다.
  3. 이후 다른 코드(예: '입력창')가 getInstance()를 또 호출하면, 이미 "직영 1호점"이 존재하므로 새로 만들지 않고 항상 그 유일한 1호점 객체를 돌려줍니다.

결과: 프로그램의 모든 부분(버튼, 입력창 등)이 동일한 하나의 객체(유일한 직영 1호점)를 참조하게 됩니다. 따라서 이 싱글톤 객체의 설정을 'Dark'로 바꾸면, 이 객체를 참조하던 모든 UI 요소가 즉시 'Dark'로 바뀌는 것이 보장됩니다.

4. 싱글톤 패턴의 장점과 단점

  • 👍 장점: 하나의 인스턴스를 만들어 놓고 해당 인스턴스를 다른 모듈들이 공유하며 사용하기 때문에, 인스턴스를 생성할 때 드는 비용(메모리 사용 등)이 줄어듭니다.
  • 👎 단점: 싱글톤 인스턴스는 프로그램 어디서든 접근할 수 있는 '전역 상태'처럼 작동하므로, 모듈 간의 의존성(결합도)이 매우 높아집니다.

💻 코드 예시

1. 자바스크립트(JavaScript)에서의 싱글톤 패턴

자바스크립트는 클래스 생성자(constructor) 자체에서 이 로직을 구현할 수 있습니다.

class Singleton {
  constructor() {
    // 'Singleton.instance'라는 static 속성이 존재하지 않으면
    if (!Singleton.instance) {
      // this (현재 생성된 객체)를 instance로 할당합니다.
      Singleton.instance = this;
    }
    // 그리고 이미 instance가 존재하든, 방금 만들었든 무조건 instance를 반환합니다.
    return Singleton.instance;
  }

  getInstance() {
    return this;
  }
}

const a = new Singleton();
const b = new Singleton();
console.log(a === b); // true (a와 b는 결국 동일한 하나의 객체입니다)

 

 

[활용 예시: 데이터베이스 연결 모듈]

DB 연결은 비용이 비싼 작업이므로, 프로그램이 실행되는 동안 단 하나의 연결만 유지하는 것이 효율적입니다.

const URL = 'mongodb://localhost:27017/myapp';
const createConnection = (url) => ({ "url": url }); // DB 연결을 흉내 내는 함수

class DB {
  constructor(url) {
    if (!DB.instance) {
      // 유일한 DB 연결 객체를 생성하여 static 속성에 저장
      DB.instance = createConnection(url);
    }
    // 항상 유일한 그 연결 객체만 반환
    return DB.instance;
  }

  connect() {
    return this.instance;
  }
}

const a = new DB(URL);
const b = new DB(URL); // new를 또 호출해도...
console.log(a === b); // true (새로 연결을 만들지 않고 기존 연결을 재사용합니다)

 

2. 자바(Java)에서의 싱글톤 패턴

자바에서는 스레드 안정성(Thread Safety)과 지연 로딩(Lazy Loading)을 모두 만족시키기 위해 'Lazy Holder'라는 정적 내부 클래스 기법을 많이 사용합니다.

  • 지연 로딩: getInstance()가 호출되기 전까지는 싱글톤 객체를 미리 만들지 않고 아끼는 방식.
  • 스레드 안정성: 여러 작업(스레드)이 동시에 getInstance()를 호출해도 객체가 여러 개 생성되는 재앙을 막는 것.
class Singleton {
    // 1. 생성자를 private으로 선언하여 외부 생성을 막습니다.
    private Singleton() { }

    // 2. static 내부 클래스(Holder)를 만듭니다.
    // 이 Holder 클래스는 Singleton.getInstance()가 호출될 때까지 로드되지 않습니다. (지연 로딩)
    private static class singleInstanceHolder {
        // 3. Holder가 로드되는 시점에 단 하나의 INSTANCE가 생성됩니다. (JVM이 스레드 안정성을 보장)
        private static final Singleton INSTANCE = new Singleton();
    }

    // 4. 유일한 접근 통로
    public static Singleton getInstance() {
        return singleInstanceHolder.INSTANCE;
    }
}

public class HelloWorld {
    public static void main(String[] args) {
        Singleton a = Singleton.getInstance();
        Singleton b = Singleton.getInstance();

        // hashCode()는 객체의 고유 메모리 주소값입니다.
        System.out.println(a.hashCode());
        System.out.println(b.hashCode());

        if (a == b) { // 두 객체는 완벽히 동일합니다.
            System.out.println(true);
        }
    }
}

/*
출력 결과:
705927765
705927765
true
*/

🚫 싱글톤 패턴의 치명적인 단점

싱글톤 패턴은 편리하지만 심각한 단점이 있습니다. 바로 TDD (테스트 주도 개발)를 어렵게 만든다는 것입니다.

  • TDD (Test Driven Development)란: 실제 기능을 만들기 전에 테스트 코드부터 작성하는 개발 방식입니다.
  • 이때의 테스트(특히 단위 테스트(Unit Test))는 서로 독립적이어야 하며 어떤 순서로 실행되든 결과가 동일해야 합니다.
  • 하지만... 싱글톤 패턴은 프로그램 전체에서 단 하나의 인스턴스(전역 상태)를 공유합니다. 만약 A 테스트가 싱글톤 객체의 데이터를 'A'로 바꾸면, 이 상태가 B 테스트에도 영향을 미쳐 B 테스트가 실패할 수 있습니다. 각 테스트마다 독립적인 객체를 만들어 테스트할 수가 없습니다.

이처럼 모듈 간의 의존성이 강하게 묶이는 것을 '결합도가 높다(Tightly Coupled)'고 말합니다.


💉 이 문제를 해결하는 방법: 의존성 주입 (DI)

싱글톤의 '높은 결합도' 문제를 해결하기 위해 의존성 주입 (Dependency Injection, DI) 개념이 등장했습니다.

1. 의존성 주입(DI)이란?

  • 메인 모듈(예: 컨트롤러)이 사용해야 할 다른 모듈(예: 서비스 객체 또는 DB 객체)을 직접 생성(new)하지 않는 방식입니다.
  • 대신, 중간에 '의존성 주입자(DI 컨테이너)'가 끼어들어, 메인 모듈이 필요로 하는 모듈을 외부에서 간접적으로 주입(전달)해 줍니다.
  • 이 관계가 느슨해지는 것을 '디커플링(Decoupling, 결합도를 낮춘다)'이라고 부릅니다.

쉽게 비유하자면:

  • (Before DI): 내가(메인 모듈) 요리를 하기 위해 내 주방에서 직접 칼(의존 모듈)을 만듭니다. 다른 칼을 쓰려면 주방을 뜯어고쳐야 합니다.
  • (After DI): 나는 '칼이 들어올 자리'만 비워둡니다. 외부의 누군가(DI 주입자)가 상황에 맞는 칼(과일칼, 식칼 등)을 그 자리에 주입해 줍니다. (테스트할 때는 '가짜 칼(Mock Object)'을 주입하면 됩니다.)

2. 의존성 주입(DI)의 장단점

  • 장점:
    • 모듈들을 쉽게 교체(주입)할 수 있는 구조가 되어 테스팅하기 매우 쉽고 마이그레이션(이전)하기도 수월합니다.
    • 구현할 때 추상화 레이어(인터페이스)를 기반으로 구현체를 넣어주기 때문에 애플리케이션의 의존성 방향이 일관됩니다.
    • 모듈 간의 관계가 더 명확해져 애플리케이션 구조를 쉽게 추론할 수 있습니다.
  • 단점:
    • 모듈들이 더욱더 분리되므로 클래스 수가 늘어나 구조의 복잡성이 증가할 수 있으며, 약간의 런타임 패널티(성능 저하)가 발생할 수 있습니다.

3. 의존성 주입 원칙(DIP)

의존성 주입은 '의존성 역전 원칙 (Dependency Inversion Principle)'을 따릅니다.

  1. 상위 모듈(정책)은 하위 모듈(세부 구현)에서 어떠한 것도 가져오지 않아야 한다. (상위 모듈이 하위 모듈에 의존하면 안 된다.)
  2. 두 모듈(상위, 하위)은 모두 추상화(인터페이스)에 의존해야 한다.
  3. 이때 추상화는 세부 사항(구현체)에 의존해서는 안 된다. (세부 사항이 추상화에 의존해야 한다.)

 


1. 싱글톤 패턴에 대해 설명해주세요.

 

싱글톤 패턴은 클래스의 인스턴스가 단 하나만 생성되도록 보장하고, 어디서든 그 인스턴스에 접근할 수 있도록 하는 디자인 패턴입니다. 주로 데이터베이스나 애플리케이션 전체에서 단 하나만 존재해야 하는 리소스를 관리할 때 사용합니다. 이를 통해 인스턴스 생성 비용을 줄여 메모리를 효율적으로 사용할 수 있고, 데이터 공유를 쉽게 할 수 있다는 장점이 있습니다. 다만, 모듈 간의 결합도가 높아지고 단위 테스트가 어려워질 수 있다는 점은 주의해서 사용해야 한다고 생각합니다.

 

2. TDD에 대해 설명해주세요.

 

TDD, 즉 테스트 주도 개발은 실제 기능 구현에 앞서 실패하는 테스트 코드를 먼저 작성하는 개발 방법입니다. TDD는 Red, Green, Refactor라는 짧은 주기를 반복하는 것이 핵심인데요. 먼저 실패하는 테스트 케이스를 작성하고, 그 테스트를 통과시키는 가장 간단한 코드를 작성합니다. 마지막으로, 기능은 그대로 두면서 코드의 구조를 개선하는 리팩토링을 거칩니다. 이 과정을 통해 코드를 점진적으로 완성해가므로, 코드의 안정성과 유지보수성이 높아지는 효과가 있습니다.

 

3. 의존성 주입에 대해 설명해주세요.

 

의존성 주입이란 메인 모듈이 사용해야 할 다른 모듈을 직접 생성하지 않는 방식입니다. 대신 중간에 의존성 주입자가 메인 모듈이 필요로 하는 모듈을 외부에서 간접적으로 전달해 줍니다. 이렇게 하면 객체 간의 결합도가 낮아져 모듈들을 쉽게 교체할 수 있는 구조가 되어 테스팅이 쉽지만 모듈들이 더욱더 분리되어 구조의 복잡성이 증가할 수 있습니다.

 

'Computer Science > Design Pattern' 카테고리의 다른 글

⛓️ 프록시 패턴과 프록시 서버  (1) 2025.09.16
📡 옵저버 패턴 (Observer Pattern)  (0) 2025.09.09
⚔️ 전략 패턴 (Strategy Pattern)  (0) 2025.09.09
🏭 팩토리 패턴 (Factory Pattern)  (0) 2025.09.09
👨‍💻 디자인 패턴과 주요 원칙  (0) 2025.09.09
'Computer Science/Design Pattern' 카테고리의 다른 글
  • 📡 옵저버 패턴 (Observer Pattern)
  • ⚔️ 전략 패턴 (Strategy Pattern)
  • 🏭 팩토리 패턴 (Factory Pattern)
  • 👨‍💻 디자인 패턴과 주요 원칙
TECHNING
TECHNING
Hi! I'm techning
  • TECHNING
    TECHNING
    TECHNING
    • 분류 전체보기 (54)
      • Computer Science (45)
        • Design Pattern (11)
        • Programming Paradigm (4)
        • Network (15)
        • Operating System (6)
        • Database (6)
        • Data Structure (3)
      • Algorithm (5)
        • Python (3)
        • Java (1)
      • IT Insight (4)
  • hELLO· Designed By정상우.v4.10.4
TECHNING
💡 싱글톤 패턴 (Singleton Pattern)
상단으로

티스토리툴바