☝️ 싱글톤 패턴을 구현하는 7가지 방법
면접에서 디자인 패턴을 물어보면 보통 개념 정도만 묻지, 실제로 구현 방법을 상세히 묻는 경우는 드물다고 한다. 그런데 Java 개발자라면 싱글톤 패턴을 구현하는 다양한 방법을 익히면서 얻어가는 것들이 많다고 한다. 그래서 이번 기회에 싱글톤 패턴을 구현하는 7가지 방법을 정리해보려고 한다.
🎯 싱글톤 패턴이란?
싱글톤 패턴(Singleton Pattern)이란, 애플리케이션 내에서 단 하나의 인스턴스만 생성되도록 보장하는 디자인 패턴이다.
대표적인 예로 데이터베이스 연결 객체, 로깅 시스템 등이 있다.
그렇다면, 싱글톤을 구현하는 다양한 방법이 존재하는 이유는 뭘까?
멀티스레드 환경에서 안전하게 단 하나의 인스턴스를 보장하기 위해서!
자, 그럼 하나씩 살펴보자.
1️⃣ 단순한 메서드 호출
처음 떠올릴 수 있는 가장 기본적인 방법이다.
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
겉으로 보기에는 잘 동작할 것 같지만... 멀티스레드 환경에서 문제가 생긴다!
두 개의 스레드가 getInstance()
를 거의 동시에 호출하면, 싱글톤 인스턴스가 2개 생성될 수 있다.
싱글톤이 여러 개 만들어지는 순간, 이 패턴은 무너진다.
2️⃣ synchronized
키워드 사용
멀티스레드 문제를 해결하려면 synchronized
키워드를 사용할 수 있다.
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
이제 첫 번째 스레드가 getInstance()
를 실행하는 동안, 다른 스레드는 기다려야 한다.
덕분에 멀티스레드 환경에서도 안전해졌지만, 모든 호출에 대해 synchronized가 걸리기 때문에 성능이 저하된다.
그럼, synchronized를 최소화하면서 안전성을 보장할 수 있는 방법은 없을까?
3️⃣ 정적(static) 멤버 사용
싱글톤 인스턴스를 클래스 로딩 시점에 미리 만들어 놓는 방식이다.
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
이 방식은 클래스가 로딩될 때 한 번만 실행되므로 멀티스레드 환경에서도 안전하다.
하지만! 인스턴스를 사용하지 않더라도 무조건 생성된다는 단점이 있다.
"무조건 생성되면 메모리가 낭비되지 않을까?"
"게다가, 정적 블록(static block)이라는 걸로도 만들 수 있다고 하는데… 이건 또 뭐지?" 🤔
4️⃣ 정적 블록(static block) 사용
정적 변수 초기화를 정적 블록에서 수행하는 방식이다.
public class Singleton {
private static final Singleton instance;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
이 방법은 예외 처리 등의 추가적인 작업을 정적 블록 안에서 할 수 있다는 점이 장점이다.
하지만 여전히 클래스 로딩 시 무조건 인스턴스를 생성한다는 단점은 남아 있다.
5️⃣ 정적 내부 클래스(Lazy Holder)
이 방식은 정적 멤버 방식의 단점을 개선한 방식으로, 클래스가 로딩될 때 인스턴스를 생성하지 않고, 필요할 때만 생성한다.
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
"이게 왜 좋은 걸까?"
"클래스가 로딩될 때 내부 클래스는 로딩되지 않으니까, getInstance()가 호출될 때 최초로 생성된다!"
이 방식은 성능 문제 없이 멀티스레드 환경에서도 안전하게 싱글톤을 구현할 수 있기 때문에 가장 많이 사용된다! 🎉
6️⃣ 이중 확인 잠금(Double-Checked Locking, DCL)
이 방법은 synchronized
를 최소화하면서 멀티스레드 환경에서도 안전하게 동작하도록 개선한 방법이다.
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
"여기서
volatile
키워드는 왜 필요할까?"
✅ volatile
의 역할
자바에서 멀티스레드는 각각 CPU 캐시 메모리를 사용하기 때문에, 변수 변경 사항이 즉시 반영되지 않을 수 있다.
이를 해결하려면 메모리에 직접 접근하도록 강제하는 volatile
키워드가 필요하다!
하지만! 이 방법은 코드가 길고 가독성이 떨어진다는 단점이 있다.
7️⃣ enum
을 활용한 싱글톤
마지막 방법은 enum
을 이용하는 것이다.
public enum SingletonEnum {
INSTANCE;
public void someMethod() {
// 기능 구현
}
}
이 방법은 자바에서 보장하는 가장 안전한 싱글톤 구현 방식이다.
이펙티브 자바를 쓴 조슈아 블로크(Joshua Bloch)도 추천한 방법!
"왜 안전할까?"
"enum
은 자바에서 기본적으로 스레드 안전(Thread Safe) 하고, 리플렉션을 통한 공격도 방어할 수 있다!"
단점이라면, 일반 클래스와 달리 확장성이 떨어진다.
(예를 들어, 클래스를 상속받거나 인터페이스를 구현하는 것이 어렵다.)
🧐 결론: 가장 많이 쓰는 방법은?
그럼 7가지 방법 중 어떤 방법을 선택해야 할까?
✅ 가장 많이 쓰는 방법 → 정적 내부 클래스(Lazy Holder, 방법 5)
✅ 가장 안전한 방법 → enum 싱글톤(방법 7)
- 5번 방식은 성능 저하 없이 멀티스레드 환경에서도 안전하기 때문에 가장 많이 사용된다.
- 7번 방식은 보안적으로 가장 안전하고, 가장 간결한 코드이지만, 확장성이 낮다고 한다.
결론적으로, 대부분의 상황에서는 5번 방식(Lazy Holder)을 사용하고,
특별한 보안이 필요한 경우에는 enum 방식을 고려하면 된다! 🎯
💡 싱글톤 패턴을 공부하면서 느낀 점
싱글톤 패턴 하나만 봐도 멀티스레드 환경, JVM 메모리 구조, synchronized의 성능 문제 등 다양한 개념을 접할 수 있었다.
'CS 공부' 카테고리의 다른 글
# 이터레이터(iterator) 패턴 (0) | 2025.02.20 |
---|---|
# 팩토리 패턴 (1) | 2025.02.20 |
# 싱글톤 패턴 (0) | 2025.02.17 |
# 라이브러리와 프레임워크 (0) | 2025.02.17 |
# 디자인 패턴 소개 (0) | 2025.02.17 |