하나의 클래스에 오직 하나의 인스턴스(객체)만 가지도록 보장하는 패턴입니다.
하나의 클래스를 기반으로 여러 개의 개별 인스턴스를 만들 수 있지만, 그렇게 하지 않고 단 하나의 인스턴스를 만들어 이를 기반으로 로직을 만드는 데 쓰이며, 보통 데이터베이스 연결 모듈처럼 프로그램 전체에서 유일해야 하는 객체에 사용됩니다.
📁싱글톤 패턴 요약 (ft. 얄코 버거 🍔)
영상(링크)에서는 싱글톤 패턴을 '얄코 버거' 프랜차이즈에 비유하여 설명합니다.
1. 클래스와 객체 (프랜차이즈 본사 vs 개별 매장)
- 클래스 (Class): '얄코 버거'라는 프랜차이즈 본사(설계도) 자체입니다.
- 인스턴스 객체 (Instance): 본사의 설계도를 바탕으로 여기저기 생긴 개별 매장입니다.
- 인스턴스 변수 (Instance Field): '매장 위치', '점장 이름'처럼 각 매장마다 따로 존재해야 하는 정보입니다. 매장(객체)이 100개면 이 정보도 100개 생깁니다.
- 정적 변수 (Static Field): '브랜드 이름(얄코 버거)', 'CEO 이름'처럼 프랜차이즈 본사 자체에 속한 정보입니다. 이 정보는 매장이 몇 개든 상관없이 프로그램 전체에서 단 하나만 존재합니다.
2. 인스턴스 변수 vs 정적 변수(매장 정보 vs 본사 정보)
싱글톤 패턴은 이 static 개념을 활용하여, 클래스가 "개별 매장(Instance)"을 여러 개 만들지 못하게 하고 "본사(Static)"처럼 단 하나만 존재하도록 강제하는 기술입니다.
예를 들어 '다크 모드' 설정 객체는 프로그램 전체에서 단 하나여야 합니다. 만약 여러 개가 생기면 UI가 꼬일 수 있기 때문이죠.
이것을 프랜차이즈 비유로 구현하는 방식은 다음과 같습니다.
- "가맹 신청을 막는다" (Private 생성자)
- 외부에서 new(새 객체 생성 명령어)를 사용해 "개별 매장(객체)"을 마음대로 만들 수 없도록, 생성자(객체 만드는 함수)를 private(비공개)으로 선언합니다.
- 이것은 마치 외부에서 가맹점 신청을 아예 막아버리는 것과 같습니다.
- "본사 내부에 직영 1호점을 차린다" (Static 변수)
- 외부에서는 매장을 못 만들지만, 본사(클래스) 내부에서는 스스로 객체를 만들 수 있습니다.
- 클래스는 "본사 건물 안에 유일한 직영 1호점", 즉 자기 자신 타입의 static 변수를 만들어 유일한 인스턴스를 이곳에 저장합니다.
- "유일한 공식 채널(정문)을 연다" (getInstance 메서드)
- 모든 고객(다른 코드)이 이 "유일한 직영 1호점"에 접근할 수 있도록, 유일한 공식 통로인 public static 메서드(예: getInstance())를 엽니다.
3. 작동방식
- 어떤 코드(예: '버튼')가 Theme.getInstance() (직영 1호점 안내)를 최초로 호출합니다.
- 메서드는 "아직 직영점이 안 열었네?" (Static 변수가 비어있는지 체크)를 확인하고, 단 한 번 "본사 직영 1호점"(객체)을 생성하여 Static 변수에 저장합니다.
- 이후 다른 코드(예: '입력창')가 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. 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 |