-
▼ Why ?
-
▼ 싱글톤 패턴 (Singleton Pattern)
-
싱글톤 패턴 ?
-
싱글톤 패턴은 '안티 패턴(Anti pattern)' 인 이유
-
싱글톤 패턴을 구현하는 정석(?)적인 기법 - Lazy Initailization
-
▼ 싱글톤 패턴 구현 방법
-
싱글톤 패턴을 구현하는 7가지 코드 기법 ?
-
Eager Initialization
-
Static block Initialization
-
Lazy Initialization
-
Thread safe Initialization
-
Double-Checked Locking
-
Bill Pugh Solution (LazyHolder; 성능이 중요)
-
Enum 사용 (안정성이 중요)
▼ Why ?
자바 스터디를 진행할 때 '객체지향 프로그래밍' 챕터에서 생성자를 공부하다 '싱글톤 패턴' 에 대해선 가벼운 정도로만 공부하고 넘어갔었던 적이 다. 그런데, 이번에 스프링(Spring)에서 중요한 개념 중 하나인 '빈(Bean)' 을 공부하다보니, 스프링 컨테이너가 'CGLib' 를 이용해 보장해주는 '싱글톤 패턴' 에 대해 좀 더 공부해야 할 필요성을 느꼈고, 이에 대한 정리를 따로 해두는 것이 좋을 것 같다. '싱글톤 패턴' 을 구현하는 다양한 기법들이 있는데, 싱글톤 패턴을 구현하는 과정을 이해하는데 도움이 될 것 같아 추가적으로 공부해봤다.
▼ 싱글톤 패턴 (Singleton Pattern)
싱글톤 패턴 ?
- 싱글톤 패턴을 따르는 클래스는, 생성자가 여러번 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 이미 생성한 객체를 리턴한다.
➙ 즉, 단 하나의 유일한 객체를 만들기 위한 소프트웨어 디자인 패턴을 '싱글톤 패턴' 이라고 한다.
➙ 메모리 절약을 위해, 어떤 인스턴스가 필요할 때 해당 인스턴스가 이미 생성돼 있으면 새로 생성하지 않고 기존의 인스턴스를 가져와 사용한다는 것이다.
( 어떤 동일한 지역변수를 여러 곳에 선언해줄 필요없이 그냥 전역변수로 지정해서 사용하는 것과 비슷한 맥락이다. )
- 따라서, 싱글톤 패턴을 적용할 만한 클래스는 생성한 객체가 리소스를 많이 차지하는 무거운 클래스이다.
(+) 'Database Connection Pool' 처럼 공통된 객체를 여러 번 생성하는 경우에 적합니다.
- 싱글톤 클래스는 일정 메모리만을 가지고 하나의 객체(인스턴스)만 사용하기 때문에 메모리 낭비를 방지할 수 있다.
싱글톤 패턴은 '안티 패턴(Anti pattern)' 인 이유
- 모듈 간 의존성이 높아진다.
- 객체지향 5원칙인 SOLID를 따르지 않는 경우가 많다.
- TDD(Test-Driven-Development)의 경우 단위 테스트를 할 때,
싱글톤 패턴이 적용된 클래스에서 생성된 인스턴스는 리소스를 공유하기 때문에 매번 인스턴스의 상태를 초기화시켜줘야 한다.
➙ 하지만, 상속에 의존하는 테스트 프레임워크 특성상 싱글톤 패턴이 적용된 클래스는 테스트하기 어렵다.
➙ '스프링 컨테이너' 와 같은 프레임워크를 이용하면 이러한 싱글톤 패턴의 단점을 보완하여 사용할 수 있다.
싱글톤 패턴을 구현하는 정석(?)적인 기법 - Lazy Initailization
- 사실 정석적인 기법이라고 할 수 없는 것이, 이 기법은 멀티 쓰레드 환경에서 안정적 X !
- 자바(Java) 코드를 예로 들면,
그냥 생성자 메서드를 'private' 로 선언해주면 된다.
➙ 외부에서 new 생성자를 통해 마구잡이로 '인스턴스화(instantiate)' 하는 것을 방지할 수 있다.
( 클래스에 'final' 을 선언해준 이유는 상속할 수 없는 클래스라는 것을 알리기 위함이다. )
final class Singleton { // Singleton Pattern
...
private static Singleton instance = new Singleton();
// getInstance()에서 사용할 수 있도록 인스턴스가 미리 생성되어 있어야 하므로 static이어야 한다.
private Singleton() {}
pubilc static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
...
}
- 싱글톤을 적용한 클래스의 객체를 'getInstance' 메서드를 이용해서 생성해주면,
해당 클래스로부턴 아무리 많은 객체를 생성하더라도 같은 객체를 불러오게 된다.
class SingletonTest {
public static void main(String[] args) {
Singleton s = new Singleton(); // Compile Error!
Singleton s = Singleton.getInstance(); // 객체 최초 생성
Singleton s1 = Singleton.getInstance(); // s에 담긴 주소 = s1에 담긴 주소
}
}
싱글톤 패턴 (참고 자료) - [JAVA] 객체지향 프로그래밍 II (OOP : Object-Oriented Programming) (+ 싱글톤(Singleton)) — Uykm_Note (tistory.com)
[JAVA] 객체지향 프로그래밍 II (OOP : Object-Oriented Programming) (+ 싱글톤(Singleton))
🌑 상속 (Inheritance) ✔️ 상속 🔹 코드의 재사용성 · 프로그램의 생산성 · 유지보수성 ⇑ class SubClass_Name extends SuperClass_Name 🔻 Sub class의 instance를 생성하면 Super class의 instance를 생성하지 않아도
ukym-tistory.tistory.com
▼ 싱글톤 패턴 구현 방법
싱글톤 패턴을 구현하는 7가지 코드 기법 ?
- 아래의 7가지 코드 기법들은 모두 '싱글톤 패턴(Singleton pattern)' 을 구현하기 위해 나온 기법들인데, 각 기법마다 장단점이 있다고 하고 위에서 아래로 갈수록 단점을 보완해가며 만들어진 기법이라고 한다. 그래서 각 기법들을 모두 공부해보는 것이 싱글톤 패턴을 구현 과정에 대해서 이해하는 데에 도움이 될 것 같아서 정리해봤다.
- Eager Initialization
- Static block Initialization
- Lazy Initialization
- Thread safe Initializaion
- Double-Checked Locking
- Bill Push Solution (LazyHolder)
- Enum 사용
- 현재 개발된 싱글톤 패턴을 구현하기에 적합한 기법들 외에도 초기 기법까지 살펴본 이유는 다음과 같다.
'그때그때' 발생하는 단점들을 보완하는 방식으로 개발되어왔기 때문에 이전의 기법들을 이해하는 것이 현재 주로 사용되고 있는 기법들을 이해하는 데 도움이 될 것이라 생각했기 때문 !
➙ 아래의 내용이 간단하게 그 흐름을 정리해놓은 것이다.
- 단순하게 싱글톤 패턴 적용만 생각했더니,
예외 처리를 생각 못했다. - 그래서 static 블록을 사용해서 예외 처리를 해줬더니,
미리 객체를 생성해뒀던 점 때문에 메모리 낭비가 심하다. - 그렇게 매번 객체를 생성하지 않고 기존의 객체를 가져오도록 했더니,
멀티 쓰레드 환경에서 동기화(synchronized) 문제가 발생했다. ( "Thread safe" 하지 않다. ) - 그렇게 동기화 문제를 해결하기 위해 synchronized 메서드를 생성했더니,
객체를 가져올 때마다 동기화 메서드가 호출돼서 성능(performance) 저하 문제가 발생했다. - 그래서 성능 저하 문제를 해결하기 위해 객체가 최초로 초기화(생성)했을 때만 동기화 적용을 하도록 했더니,
"I/O 불일치 문제" 가 발생했고 이 때문에 'volatile' 키워드 사용이 요구된다. - 앞서 말한 점들을 보완하여 현재 권장되는 기법들이 'LazyHolder', 'Enum 사용 기법'인 것이다 !
( 사실 이 기법들도 언제까지나 권장될 것이라는 보장은 없는 것 같다. )
- 단순하게 싱글톤 패턴 적용만 생각했더니,
Eager Initialization
- 단어에서도 알 수 있듯이 객체를 한 번만 미리(Eager) 만들어두는 기법이다.
- 'static final' 로 객체를 생성해주기 때문에 멀티쓰레드 환경에서도 안전하다.
➜ 하지만, 알다시피 static으로 변수를 선언해준 경우엔 사용하지 않는다해도 이미 메모리를 차지하고 있기 떄문에 메모리 낭비가 발생할 수 있다. - 예외 처리 불가능 !
➜ 'Static block Initialization' 기법으로 해결 가능 !
class Singleton {
// 싱글톤이 적용된 클래스 객체를 담을 인스턴스 변수
private static final Singleton INSTANCE = new Singleton();
// 생성자를 private으로 선언해서, getInstance 메서드를 통해서만 객체 생성 가능
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
Static block Initialization
- static 블록을 이용해서 예외를 처리해줄 수 있다.
- 하지만 여전히 객체를 담을 인스턴스 변수를 static로 선언해주는 점 때문에 메모리 낭비는 여전하다 !
➜ 이 단점을 해결하기 위한 기법이 'Lazy Initialization' 이다 !
class Singleton {
// 싱글톤이 적용된 클래스 객체를 담을 인스턴스 변수
private static final Singleton instance = new Singleton();
// 생성자를 private으로 선언해서, getInstance 메서드를 통해서만 객체 생성 가능
private Singleton() {}
// Static block
static {
try {
instance = new Singleton();
} catch (Exception e) {
throw new RuntimeException("싱글톤 객체 생성 오류");
}
}
public static Singleton getInstance() {
return instance;
}
}
Lazy Initialization
- 객체 생성에 대한 관리를 내부적으로 처리해준다
➙ 객체 반환(생성) 메서드를 호출했을 때 해당 객체를 담고 있는 인스턴스 변수의 값이 null인지 아닌지에 따라 객체를 생성(초기화)하거나 기존의 인스턴스를 반환하는 방식 !
➙ 이러한 방식 덕분에 일정 메모리를 고정적으로 차지하는 기법의 단점이자 한계를 극복할 수 있다.
( 메모리 관리 측면에서 효율적 ) - 하지만, 앞서 말한 기법들과 달리 멀티 쓰레드 환경에서 불안정해진다.
➙ 즉, "쓰레드 세이프(Thread Safe)" 하지 않는 치명적인 단점이 있다.
class Singleton {
// 싱글톤이 적용된 클래스 객체를 담을 인스턴스 변수 (final X)
private static Singleton instance;
// 생성자를 private으로 선언해서, getInstance 메서드를 통해서만 객체 생성 가능
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance new Singleton(); // 유일한 객체 생성
}
return instance;
}
}
- "자바의 정석" 를 공부할 때도, '싱글톤 패턴' 을 설명할 때 이 'Lazy Initalization' 을 예로 들은 것처럼 싱글톤 패턴의 정석이라 소개되는 경우가 있다고 한다.
➙ 하지만 '자바' 는 멀티 쓰레드 언어이기 때문에, 멀티 쓰레드 환경에서 안전하지 않은 'Lazy Initialization' 이 정석적인 기법이라 하는 것은 모순적이다 !
멀티 쓰레드 환경에서 어떤 문제가 발생하는걸까?
- 위의 코드처럼 생성한 싱글톤 클래스가 있고, 쓰레드 A, 쓰레드 B가 있다고 가정하자.
- 쓰레드 A가 if문에서 이미 생성된 객체가 없다고 판단(instance == null)하고 새로운 인스턴스 생성 (instance = new Singleton();)넘어갔다.
- 이때 쓰레드 B도 if문을 수행한다면?
➙ 쓰레드 A가 아직 인스턴스화(instantiate) 코드를 실행하지 않았기 때문에, if문에서 이미 생성되어 있는 객체가 없으므로 참(true)이라고 판단하게 된다. - 결과적으로 쓰레드 A와 B가 인스턴스를 두 번 초기화(생성)하게 되는 것이다.
➙ 이 현상을 쓰레드 간의 "Race condition(경쟁상태)" 라고 말한다 !
- 위의 코드처럼 생성한 싱글톤 클래스가 있고, 쓰레드 A, 쓰레드 B가 있다고 가정하자.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
// 싱글톤 클래스
class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 유일한 객체 생성
}
return instance;
}
}
public class Main {
public static void main(String[] args) {
// 1. 싱글톤 객체를 담을 배열
Singleton[] singleton = new Singleton[10];
// 2. 쓰레드 풀 생성
ExecutorService service = Executors.newCachedThreadPool();
// 3. 반복문을 통해 10개의 쓰레드가 동시에 인스턴스 생성
for (int i = 0; i < 10; i++) {
final int num = i;
service.submit(() -> {
singleton[num] = Singleton.getInstance();
});
}
// 4. 종료
service.shutdown();
// (이 예외처리 코드는 확인하려는 문제와 관계 X)
// ExecutorService가 작업을 완료할 때까지 기다리게 하여
// singleton 배열을 출력하기 전에 모든 쓰레드가 작업을 완료하도록 예외 처리해줬다.
// 예외 처리 코드가 없으면 singleton 배열에 null 값이 남아있는 채로 출력될 수 있다.
try {
service.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
// 인터럽트 처리
}
// 5. 싱글톤 객체 주소 출력
for(Singleton s : singleton) {
System.out.println(s.toString());
}
}
}

🔻 출력된 결과를 보면 Singleton 클래스는 싱글톤 패턴을 적용한 클래스임에도 불구하고 두 개의 객체가 생성된 모습을 확인할 수 있다.
➙ 이 문제를 해결하기 위한 기법이 'Thread safe Initialization' 이다.
Thread safe Initialization
- 'synchronized' 키워드를 이용해 동기화 문제를 해결한다.
➙ 이 키워드를 메서드에 적용하면 쓰레드들에 하나씩 접근하도록 설정 가능 ! - 하지만, 이 기법 역시도 단점이 있다.
➙ 여러 개의 모듈들이 객체를 가져올 때마다 'synchronized' 메서드를 매번 호출하게 되어 동기화 처리 작업에 오버헤드(overhead)가 발생해 성능(performance)이 저하된다 !
➙ 이 단점을 해결하기 위한 기법 또한 존재한다.
➙ 'Double-Checked Locking'
class Singleton {
private static Singleton instance;
private Singleton() {}
// synchronized 메서드
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
🔻 synchronized
: 멀티 쓰레드 환경에서 두 개 이상의 스레드가 하나의 변수에 동시에 접근할 때 "Race condition(경쟁 상태)" 이 발생하지 않도록 하는 키워드.
➙ 쉽게 말하면, 어떤 쓰레드가 해당 메서드를 실행하는 동안에는 다른 쓰레드가 접근하지 못하도록 막는 것(lock)이다.

Double-Checked Locking
- 객체를 가져올 때마다 synchronized 메서드를 호출, 즉 매번 동기화해주는 것이 문제이다.
➙ 그럼 객체를 최초로 초기화(생성)해줄 때만 동기화를 적용하고 이미 생성되어 있는 기존의 인스턴스를 반환할 때는 동기화를 적용하지 않도록 하는 기법이 'Double-Checked Locking' 이다.
➙ 쉽게 말하면, 메서드에 동기화를 적용하는 것이 아닌 클래스 자체에 동기화를 적용하는 것이다 ! - 이로 인해 발생할 수 있는 "I/O 불일치 문제" 를 해결하기 위해 'volatile' 키워드도 사용하자.
➙ 근데 'volatile' 키워드는 JVM 1.5 이상에서만 지원하기도 하고, JVM에 따라서 "쓰레드 세이프(Thread safe)" 하지 않는 경우도 발생하기 때문에, 'volatile' 키워드 사용이 요구되는 'Double-Checked Locking' 기법은 지양하자 !
class Singleton {
private static volatile Singleton instance; // volatile 키워드 사용
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
// 메서드가 아닌 Singleton 클래스 자체에 동기화를 적용
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton(); // 최초 초기화만 동기화 작업이 수행돼서 리소스 낭비를 최소화
}
}
}
return instance; // 최초 초기화가 되면 앞으로 생성된 인스턴스만 반환
}
}
🔻 volatile
: 캐시(Cache)에서 값을 읽지 않고 메인 메모리에서 읽어오도록 지정하는 키워드.
'volatile' 이 왜 필요한가?
➙ 자바의 멀티 쓰레드 환경에선 성능을 위해서 각각이 쓰레드들을 변수를 메인 메모리(RAM)에서 가져오는 것이 아니라 캐시(cache) 메모리에서 가져온다 !
➙ 이러한 점 때문에 '비동기(Asychronous)' 작업으로 변수값을 캐시(cache)에 저장하다가, 각 쓰레드마다 캐시 메모리의 변수값이 일치하지 않을 수도 있다는 문제가 발생한다.
➙ 이때 'volatile' 를 변수에 적용하면 해당 변수값을 읽어올 때 캐시(cache)가 아닌 메인 메모리(RAM)에서 읽어오도록 할 수 있다 !

Bill Pugh Solution (LazyHolder; 성능이 중요)
- 권장되는 두 가지 기법 중 하나이다.
➙ 즉, 멀티 쓰레드 환경에서도 안전하고 "Lazy Loading(나중에 객체 생성)" 도 가능한 완벽한 싱글톤 기법이라고 한다 !
➙ 완벽하다곤 하지만, 클라이언트가 임의로 싱글톤을 파괴할 수 있다는 단점이 있긴 하다 !
( by. Reflection API, 직렬화/역직렬화 )
- 직렬화(Serialize)
: 자바 언어에서 사용되는 Object 또는 Data를 다른 컴퓨터의 자바 시스템에서도 사용할 수 있도록 바이트 스트림(stream of bytes) 형태로 연속적인(serial) 데이터로 변환하는 포맷 변환 기술을 말한다.
( 역직렬화(Deserialize)는 반대로 그렇게 변환된 포맷을 다시 Object나 Data로 변환시켜주는 기술이다. ) - Reflection API
: 클래스 객체를 통해 필드/메서드/생성자를 접근 제어자와 상관 없이 사용할 수 있도록 해주는 자바 API
- 직렬화(Serialize)
- 클래스 안에 '내부 클래스 Holder' 를 두어 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 기법이다.
➙ 쉽게 말하면, 싱글톤 클래스의 객체를 초기화 하는 책임을 JVM에게 떠넘기는 것이라 할 수 있다.
- 내부 클래스 Holder를 static으로 설정해줘야 한다.
( static 메서드에선 static 멤버만을 호출할 수 있기 때문에 ! )
➙ 내부 클래스를 static으로 선언해줬기 때문에, static 키워드 특성상 싱글톤 클래스가 초기화되더라도 Holder 내부클래스는 메모리에 로드(load)되지 않는다.
- 그럼 내부 클래스는 언제 초기화되나 ?
➙ 어떤 묘듈이 'getInstance' 메서드를 호출할 때, 내부 클래스인 Holder의 static 멤버 'INSTANCE'를 가져와 리턴하게 되는데 이때 단 한 번만 내부 클래스가 초기화되면서 싱글톤 클래스의 객체를 최초로 생성하고 리턴하게 된다.
➙ 혹시라도 싱글톤 클래스의 객체가 또 생성될 경우를 방지하기 위해 내부 클래스의 멤버를 final로 지정해 다시 값이 할당되지 않도록 한다.
- 앞의 내용들을 이해하기 위해선 JVM의 Class Loader의 클래스 로딩 및 초기화 과정과 내부(Inner) 클래스를 static으로 선언해주는 이유를 알고 있어야 한다.
public class Singleton {
private Singleton() {
}
private static class Holder {
public static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
클래스 로딩 및 초기화 (참고 자료) - [Java_JVM] (미완) 클래스 로더(Class Loader)의 클래스 로딩 및 초기화 과정 — Uykm_Note (tistory.com)
[Java_JVM] (미완) 클래스 로더(Class Loader)의 클래스 로딩 및 초기화 과정
▼ Why ? 이번에 '싱글톤 패턴' 에 대해 공부하다가 싱글톤 패턴을 구현하는 기법들에 대해서도 공부하게 됐는데, 여러 기법들 중 'LazyHolder' 기법은 중요한 기법이기도 하고 클래스 로더 매커니즘
ukym-tistory.tistory.com
static 내부 클래스 (참고자료) - [Java_JVM] 내부 클래스를 static으로 선언해주는 이유 — Uykm_Note (tistory.com)
[Java_JVM] 내부 클래스를 static으로 선언해주는 이유
▼ Why ? 이번에 '싱글톤 패턴' 에 대해 공부하다가 싱글톤 패턴을 구현하는 기법들에 대해서도 공부하게 됐는데, 여러 기법들 중 'LazyHolder' 기법은 중요한 기법이기도 하고 클래스 로더 매커니즘
ukym-tistory.tistory.com
Enum 사용 (안정성이 중요)
- 'LazyHolder' 기법처럼 싱글톤 패턴을 적용할 때 권장되는 기법들 중 하나인데 자바에서 싱글톤 패턴을 적용하는데 가장 좋은 기법이라고 한다.
➙ 싱글톤 클래스를 enum으로 생성해주는 방식이다.
- enum으로 싱글톤 클래스를 생성해주면 ?
➙ enum은 보통 생성자를 private으로 선언해주기 때문에 자연스럽게 싱글톤 패턴 방식이 적용된다.
➙ 그리고 enum 클래스의 멤버들은 public static final로 생성해주기 때문에 멀티 쓰레드 환경에서도 안정적이다 !
- 즉, Enum 사용하면 'LazyHolder' 기법과 달리 기본적으로 직렬화가 가능해서 Serializable 인터페이스를 따로 구현해줄 필요도 없고 Refleciton 문제도 발생하지 않기 때문에 가장 권장되는 기법인 것이다.
- enum 생성자를 왜 private으로 선언해줄까 ?
➙ enum은 서로 관련있는 상수들을 모아 특정한 타입으로 다루기 위한 클래스이다.
➙ 즉, "Enumeration(열거)" 라는 이름에서 알 수 있듯이 상수들이 열거된 형태의 집합이다.
➙ 따라서, 컴파일 타임(complie-time)에 모두 결정되어야 하는 값들이기 때문에, 컴파일 이후에 외부(다른 클래스, 패키지 등)에서 enum 클래스에 접근해 동적으로 변경되는 것을 막고자 생성자를 private으로 선언해주는 것이다 !
- enum 클래스의 멤버들은 왜 'public static final' 로 생성해줄까 ?
➙ enum은 상수를 단순한 정수가 아니라 객체화해서 객체처럼 다루기 위해 등장한 개념이기 때문에, enum의 멤버는 일종의 객체라고 볼 수 있다.
➙ 즉, enum의 멤버(상수) 하나하나가 자신의 인스턴스(instance)를 생성하고 'public static' 로 다른 클래스에 직접적으로 공유해주는 것이다.
➙ 그리고 'final' 을 선언해줌으로써 자신의 인스턴스가 딱 하나임을 보장해줄 수 있기 때문에 이 점에서도 싱글톤 패턴의 특징이 드러난다고 볼 수 있다.
enum (참고 자료) - [JAVA] 지네릭스(Generics) & 열거형(Enumeration) & 애너테이션(Annotation) — Uykm_Note (tistory.com)
[JAVA] 지네릭스(Generics) & 열거형(Enumeration) & 애너테이션(Annotation)
🌑 지네릭스 (Generics) ✔️ Generics 🔹 Generics 란 ? 다양한 타입의 객체들을 다루는 메서드나 Collection class에 컴파일 시의 타입체크(compile-time type check)를 해주는 기능 ➠ 객체의 타입 안정성을 제공
ukym-tistory.tistory.com
▼ Why ?
자바 스터디를 진행할 때 '객체지향 프로그래밍' 챕터에서 생성자를 공부하다 '싱글톤 패턴' 에 대해선 가벼운 정도로만 공부하고 넘어갔었던 적이 다. 그런데, 이번에 스프링(Spring)에서 중요한 개념 중 하나인 '빈(Bean)' 을 공부하다보니, 스프링 컨테이너가 'CGLib' 를 이용해 보장해주는 '싱글톤 패턴' 에 대해 좀 더 공부해야 할 필요성을 느꼈고, 이에 대한 정리를 따로 해두는 것이 좋을 것 같다. '싱글톤 패턴' 을 구현하는 다양한 기법들이 있는데, 싱글톤 패턴을 구현하는 과정을 이해하는데 도움이 될 것 같아 추가적으로 공부해봤다.
▼ 싱글톤 패턴 (Singleton Pattern)
싱글톤 패턴 ?
- 싱글톤 패턴을 따르는 클래스는, 생성자가 여러번 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 이미 생성한 객체를 리턴한다.
➙ 즉, 단 하나의 유일한 객체를 만들기 위한 소프트웨어 디자인 패턴을 '싱글톤 패턴' 이라고 한다.
➙ 메모리 절약을 위해, 어떤 인스턴스가 필요할 때 해당 인스턴스가 이미 생성돼 있으면 새로 생성하지 않고 기존의 인스턴스를 가져와 사용한다는 것이다.
( 어떤 동일한 지역변수를 여러 곳에 선언해줄 필요없이 그냥 전역변수로 지정해서 사용하는 것과 비슷한 맥락이다. )
- 따라서, 싱글톤 패턴을 적용할 만한 클래스는 생성한 객체가 리소스를 많이 차지하는 무거운 클래스이다.
(+) 'Database Connection Pool' 처럼 공통된 객체를 여러 번 생성하는 경우에 적합니다.
- 싱글톤 클래스는 일정 메모리만을 가지고 하나의 객체(인스턴스)만 사용하기 때문에 메모리 낭비를 방지할 수 있다.
싱글톤 패턴은 '안티 패턴(Anti pattern)' 인 이유
- 모듈 간 의존성이 높아진다.
- 객체지향 5원칙인 SOLID를 따르지 않는 경우가 많다.
- TDD(Test-Driven-Development)의 경우 단위 테스트를 할 때,
싱글톤 패턴이 적용된 클래스에서 생성된 인스턴스는 리소스를 공유하기 때문에 매번 인스턴스의 상태를 초기화시켜줘야 한다.
➙ 하지만, 상속에 의존하는 테스트 프레임워크 특성상 싱글톤 패턴이 적용된 클래스는 테스트하기 어렵다.
➙ '스프링 컨테이너' 와 같은 프레임워크를 이용하면 이러한 싱글톤 패턴의 단점을 보완하여 사용할 수 있다.
싱글톤 패턴을 구현하는 정석(?)적인 기법 - Lazy Initailization
- 사실 정석적인 기법이라고 할 수 없는 것이, 이 기법은 멀티 쓰레드 환경에서 안정적 X !
- 자바(Java) 코드를 예로 들면,
그냥 생성자 메서드를 'private' 로 선언해주면 된다.
➙ 외부에서 new 생성자를 통해 마구잡이로 '인스턴스화(instantiate)' 하는 것을 방지할 수 있다.
( 클래스에 'final' 을 선언해준 이유는 상속할 수 없는 클래스라는 것을 알리기 위함이다. )
final class Singleton { // Singleton Pattern
...
private static Singleton instance = new Singleton();
// getInstance()에서 사용할 수 있도록 인스턴스가 미리 생성되어 있어야 하므로 static이어야 한다.
private Singleton() {}
pubilc static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
...
}
- 싱글톤을 적용한 클래스의 객체를 'getInstance' 메서드를 이용해서 생성해주면,
해당 클래스로부턴 아무리 많은 객체를 생성하더라도 같은 객체를 불러오게 된다.
class SingletonTest {
public static void main(String[] args) {
Singleton s = new Singleton(); // Compile Error!
Singleton s = Singleton.getInstance(); // 객체 최초 생성
Singleton s1 = Singleton.getInstance(); // s에 담긴 주소 = s1에 담긴 주소
}
}
싱글톤 패턴 (참고 자료) - [JAVA] 객체지향 프로그래밍 II (OOP : Object-Oriented Programming) (+ 싱글톤(Singleton)) — Uykm_Note (tistory.com)
[JAVA] 객체지향 프로그래밍 II (OOP : Object-Oriented Programming) (+ 싱글톤(Singleton))
🌑 상속 (Inheritance) ✔️ 상속 🔹 코드의 재사용성 · 프로그램의 생산성 · 유지보수성 ⇑ class SubClass_Name extends SuperClass_Name 🔻 Sub class의 instance를 생성하면 Super class의 instance를 생성하지 않아도
ukym-tistory.tistory.com
▼ 싱글톤 패턴 구현 방법
싱글톤 패턴을 구현하는 7가지 코드 기법 ?
- 아래의 7가지 코드 기법들은 모두 '싱글톤 패턴(Singleton pattern)' 을 구현하기 위해 나온 기법들인데, 각 기법마다 장단점이 있다고 하고 위에서 아래로 갈수록 단점을 보완해가며 만들어진 기법이라고 한다. 그래서 각 기법들을 모두 공부해보는 것이 싱글톤 패턴을 구현 과정에 대해서 이해하는 데에 도움이 될 것 같아서 정리해봤다.
- Eager Initialization
- Static block Initialization
- Lazy Initialization
- Thread safe Initializaion
- Double-Checked Locking
- Bill Push Solution (LazyHolder)
- Enum 사용
- 현재 개발된 싱글톤 패턴을 구현하기에 적합한 기법들 외에도 초기 기법까지 살펴본 이유는 다음과 같다.
'그때그때' 발생하는 단점들을 보완하는 방식으로 개발되어왔기 때문에 이전의 기법들을 이해하는 것이 현재 주로 사용되고 있는 기법들을 이해하는 데 도움이 될 것이라 생각했기 때문 !
➙ 아래의 내용이 간단하게 그 흐름을 정리해놓은 것이다.
- 단순하게 싱글톤 패턴 적용만 생각했더니,
예외 처리를 생각 못했다. - 그래서 static 블록을 사용해서 예외 처리를 해줬더니,
미리 객체를 생성해뒀던 점 때문에 메모리 낭비가 심하다. - 그렇게 매번 객체를 생성하지 않고 기존의 객체를 가져오도록 했더니,
멀티 쓰레드 환경에서 동기화(synchronized) 문제가 발생했다. ( "Thread safe" 하지 않다. ) - 그렇게 동기화 문제를 해결하기 위해 synchronized 메서드를 생성했더니,
객체를 가져올 때마다 동기화 메서드가 호출돼서 성능(performance) 저하 문제가 발생했다. - 그래서 성능 저하 문제를 해결하기 위해 객체가 최초로 초기화(생성)했을 때만 동기화 적용을 하도록 했더니,
"I/O 불일치 문제" 가 발생했고 이 때문에 'volatile' 키워드 사용이 요구된다. - 앞서 말한 점들을 보완하여 현재 권장되는 기법들이 'LazyHolder', 'Enum 사용 기법'인 것이다 !
( 사실 이 기법들도 언제까지나 권장될 것이라는 보장은 없는 것 같다. )
- 단순하게 싱글톤 패턴 적용만 생각했더니,
Eager Initialization
- 단어에서도 알 수 있듯이 객체를 한 번만 미리(Eager) 만들어두는 기법이다.
- 'static final' 로 객체를 생성해주기 때문에 멀티쓰레드 환경에서도 안전하다.
➜ 하지만, 알다시피 static으로 변수를 선언해준 경우엔 사용하지 않는다해도 이미 메모리를 차지하고 있기 떄문에 메모리 낭비가 발생할 수 있다. - 예외 처리 불가능 !
➜ 'Static block Initialization' 기법으로 해결 가능 !
class Singleton {
// 싱글톤이 적용된 클래스 객체를 담을 인스턴스 변수
private static final Singleton INSTANCE = new Singleton();
// 생성자를 private으로 선언해서, getInstance 메서드를 통해서만 객체 생성 가능
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
Static block Initialization
- static 블록을 이용해서 예외를 처리해줄 수 있다.
- 하지만 여전히 객체를 담을 인스턴스 변수를 static로 선언해주는 점 때문에 메모리 낭비는 여전하다 !
➜ 이 단점을 해결하기 위한 기법이 'Lazy Initialization' 이다 !
class Singleton {
// 싱글톤이 적용된 클래스 객체를 담을 인스턴스 변수
private static final Singleton instance = new Singleton();
// 생성자를 private으로 선언해서, getInstance 메서드를 통해서만 객체 생성 가능
private Singleton() {}
// Static block
static {
try {
instance = new Singleton();
} catch (Exception e) {
throw new RuntimeException("싱글톤 객체 생성 오류");
}
}
public static Singleton getInstance() {
return instance;
}
}
Lazy Initialization
- 객체 생성에 대한 관리를 내부적으로 처리해준다
➙ 객체 반환(생성) 메서드를 호출했을 때 해당 객체를 담고 있는 인스턴스 변수의 값이 null인지 아닌지에 따라 객체를 생성(초기화)하거나 기존의 인스턴스를 반환하는 방식 !
➙ 이러한 방식 덕분에 일정 메모리를 고정적으로 차지하는 기법의 단점이자 한계를 극복할 수 있다.
( 메모리 관리 측면에서 효율적 ) - 하지만, 앞서 말한 기법들과 달리 멀티 쓰레드 환경에서 불안정해진다.
➙ 즉, "쓰레드 세이프(Thread Safe)" 하지 않는 치명적인 단점이 있다.
class Singleton {
// 싱글톤이 적용된 클래스 객체를 담을 인스턴스 변수 (final X)
private static Singleton instance;
// 생성자를 private으로 선언해서, getInstance 메서드를 통해서만 객체 생성 가능
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance new Singleton(); // 유일한 객체 생성
}
return instance;
}
}
- "자바의 정석" 를 공부할 때도, '싱글톤 패턴' 을 설명할 때 이 'Lazy Initalization' 을 예로 들은 것처럼 싱글톤 패턴의 정석이라 소개되는 경우가 있다고 한다.
➙ 하지만 '자바' 는 멀티 쓰레드 언어이기 때문에, 멀티 쓰레드 환경에서 안전하지 않은 'Lazy Initialization' 이 정석적인 기법이라 하는 것은 모순적이다 !
멀티 쓰레드 환경에서 어떤 문제가 발생하는걸까?
- 위의 코드처럼 생성한 싱글톤 클래스가 있고, 쓰레드 A, 쓰레드 B가 있다고 가정하자.
- 쓰레드 A가 if문에서 이미 생성된 객체가 없다고 판단(instance == null)하고 새로운 인스턴스 생성 (instance = new Singleton();)넘어갔다.
- 이때 쓰레드 B도 if문을 수행한다면?
➙ 쓰레드 A가 아직 인스턴스화(instantiate) 코드를 실행하지 않았기 때문에, if문에서 이미 생성되어 있는 객체가 없으므로 참(true)이라고 판단하게 된다. - 결과적으로 쓰레드 A와 B가 인스턴스를 두 번 초기화(생성)하게 되는 것이다.
➙ 이 현상을 쓰레드 간의 "Race condition(경쟁상태)" 라고 말한다 !
- 위의 코드처럼 생성한 싱글톤 클래스가 있고, 쓰레드 A, 쓰레드 B가 있다고 가정하자.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
// 싱글톤 클래스
class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 유일한 객체 생성
}
return instance;
}
}
public class Main {
public static void main(String[] args) {
// 1. 싱글톤 객체를 담을 배열
Singleton[] singleton = new Singleton[10];
// 2. 쓰레드 풀 생성
ExecutorService service = Executors.newCachedThreadPool();
// 3. 반복문을 통해 10개의 쓰레드가 동시에 인스턴스 생성
for (int i = 0; i < 10; i++) {
final int num = i;
service.submit(() -> {
singleton[num] = Singleton.getInstance();
});
}
// 4. 종료
service.shutdown();
// (이 예외처리 코드는 확인하려는 문제와 관계 X)
// ExecutorService가 작업을 완료할 때까지 기다리게 하여
// singleton 배열을 출력하기 전에 모든 쓰레드가 작업을 완료하도록 예외 처리해줬다.
// 예외 처리 코드가 없으면 singleton 배열에 null 값이 남아있는 채로 출력될 수 있다.
try {
service.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
// 인터럽트 처리
}
// 5. 싱글톤 객체 주소 출력
for(Singleton s : singleton) {
System.out.println(s.toString());
}
}
}

🔻 출력된 결과를 보면 Singleton 클래스는 싱글톤 패턴을 적용한 클래스임에도 불구하고 두 개의 객체가 생성된 모습을 확인할 수 있다.
➙ 이 문제를 해결하기 위한 기법이 'Thread safe Initialization' 이다.
Thread safe Initialization
- 'synchronized' 키워드를 이용해 동기화 문제를 해결한다.
➙ 이 키워드를 메서드에 적용하면 쓰레드들에 하나씩 접근하도록 설정 가능 ! - 하지만, 이 기법 역시도 단점이 있다.
➙ 여러 개의 모듈들이 객체를 가져올 때마다 'synchronized' 메서드를 매번 호출하게 되어 동기화 처리 작업에 오버헤드(overhead)가 발생해 성능(performance)이 저하된다 !
➙ 이 단점을 해결하기 위한 기법 또한 존재한다.
➙ 'Double-Checked Locking'
class Singleton {
private static Singleton instance;
private Singleton() {}
// synchronized 메서드
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
🔻 synchronized
: 멀티 쓰레드 환경에서 두 개 이상의 스레드가 하나의 변수에 동시에 접근할 때 "Race condition(경쟁 상태)" 이 발생하지 않도록 하는 키워드.
➙ 쉽게 말하면, 어떤 쓰레드가 해당 메서드를 실행하는 동안에는 다른 쓰레드가 접근하지 못하도록 막는 것(lock)이다.

Double-Checked Locking
- 객체를 가져올 때마다 synchronized 메서드를 호출, 즉 매번 동기화해주는 것이 문제이다.
➙ 그럼 객체를 최초로 초기화(생성)해줄 때만 동기화를 적용하고 이미 생성되어 있는 기존의 인스턴스를 반환할 때는 동기화를 적용하지 않도록 하는 기법이 'Double-Checked Locking' 이다.
➙ 쉽게 말하면, 메서드에 동기화를 적용하는 것이 아닌 클래스 자체에 동기화를 적용하는 것이다 ! - 이로 인해 발생할 수 있는 "I/O 불일치 문제" 를 해결하기 위해 'volatile' 키워드도 사용하자.
➙ 근데 'volatile' 키워드는 JVM 1.5 이상에서만 지원하기도 하고, JVM에 따라서 "쓰레드 세이프(Thread safe)" 하지 않는 경우도 발생하기 때문에, 'volatile' 키워드 사용이 요구되는 'Double-Checked Locking' 기법은 지양하자 !
class Singleton {
private static volatile Singleton instance; // volatile 키워드 사용
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
// 메서드가 아닌 Singleton 클래스 자체에 동기화를 적용
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton(); // 최초 초기화만 동기화 작업이 수행돼서 리소스 낭비를 최소화
}
}
}
return instance; // 최초 초기화가 되면 앞으로 생성된 인스턴스만 반환
}
}
🔻 volatile
: 캐시(Cache)에서 값을 읽지 않고 메인 메모리에서 읽어오도록 지정하는 키워드.
'volatile' 이 왜 필요한가?
➙ 자바의 멀티 쓰레드 환경에선 성능을 위해서 각각이 쓰레드들을 변수를 메인 메모리(RAM)에서 가져오는 것이 아니라 캐시(cache) 메모리에서 가져온다 !
➙ 이러한 점 때문에 '비동기(Asychronous)' 작업으로 변수값을 캐시(cache)에 저장하다가, 각 쓰레드마다 캐시 메모리의 변수값이 일치하지 않을 수도 있다는 문제가 발생한다.
➙ 이때 'volatile' 를 변수에 적용하면 해당 변수값을 읽어올 때 캐시(cache)가 아닌 메인 메모리(RAM)에서 읽어오도록 할 수 있다 !

Bill Pugh Solution (LazyHolder; 성능이 중요)
- 권장되는 두 가지 기법 중 하나이다.
➙ 즉, 멀티 쓰레드 환경에서도 안전하고 "Lazy Loading(나중에 객체 생성)" 도 가능한 완벽한 싱글톤 기법이라고 한다 !
➙ 완벽하다곤 하지만, 클라이언트가 임의로 싱글톤을 파괴할 수 있다는 단점이 있긴 하다 !
( by. Reflection API, 직렬화/역직렬화 )
- 직렬화(Serialize)
: 자바 언어에서 사용되는 Object 또는 Data를 다른 컴퓨터의 자바 시스템에서도 사용할 수 있도록 바이트 스트림(stream of bytes) 형태로 연속적인(serial) 데이터로 변환하는 포맷 변환 기술을 말한다.
( 역직렬화(Deserialize)는 반대로 그렇게 변환된 포맷을 다시 Object나 Data로 변환시켜주는 기술이다. ) - Reflection API
: 클래스 객체를 통해 필드/메서드/생성자를 접근 제어자와 상관 없이 사용할 수 있도록 해주는 자바 API
- 직렬화(Serialize)
- 클래스 안에 '내부 클래스 Holder' 를 두어 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 기법이다.
➙ 쉽게 말하면, 싱글톤 클래스의 객체를 초기화 하는 책임을 JVM에게 떠넘기는 것이라 할 수 있다.
- 내부 클래스 Holder를 static으로 설정해줘야 한다.
( static 메서드에선 static 멤버만을 호출할 수 있기 때문에 ! )
➙ 내부 클래스를 static으로 선언해줬기 때문에, static 키워드 특성상 싱글톤 클래스가 초기화되더라도 Holder 내부클래스는 메모리에 로드(load)되지 않는다.
- 그럼 내부 클래스는 언제 초기화되나 ?
➙ 어떤 묘듈이 'getInstance' 메서드를 호출할 때, 내부 클래스인 Holder의 static 멤버 'INSTANCE'를 가져와 리턴하게 되는데 이때 단 한 번만 내부 클래스가 초기화되면서 싱글톤 클래스의 객체를 최초로 생성하고 리턴하게 된다.
➙ 혹시라도 싱글톤 클래스의 객체가 또 생성될 경우를 방지하기 위해 내부 클래스의 멤버를 final로 지정해 다시 값이 할당되지 않도록 한다.
- 앞의 내용들을 이해하기 위해선 JVM의 Class Loader의 클래스 로딩 및 초기화 과정과 내부(Inner) 클래스를 static으로 선언해주는 이유를 알고 있어야 한다.
public class Singleton {
private Singleton() {
}
private static class Holder {
public static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
클래스 로딩 및 초기화 (참고 자료) - [Java_JVM] (미완) 클래스 로더(Class Loader)의 클래스 로딩 및 초기화 과정 — Uykm_Note (tistory.com)
[Java_JVM] (미완) 클래스 로더(Class Loader)의 클래스 로딩 및 초기화 과정
▼ Why ? 이번에 '싱글톤 패턴' 에 대해 공부하다가 싱글톤 패턴을 구현하는 기법들에 대해서도 공부하게 됐는데, 여러 기법들 중 'LazyHolder' 기법은 중요한 기법이기도 하고 클래스 로더 매커니즘
ukym-tistory.tistory.com
static 내부 클래스 (참고자료) - [Java_JVM] 내부 클래스를 static으로 선언해주는 이유 — Uykm_Note (tistory.com)
[Java_JVM] 내부 클래스를 static으로 선언해주는 이유
▼ Why ? 이번에 '싱글톤 패턴' 에 대해 공부하다가 싱글톤 패턴을 구현하는 기법들에 대해서도 공부하게 됐는데, 여러 기법들 중 'LazyHolder' 기법은 중요한 기법이기도 하고 클래스 로더 매커니즘
ukym-tistory.tistory.com
Enum 사용 (안정성이 중요)
- 'LazyHolder' 기법처럼 싱글톤 패턴을 적용할 때 권장되는 기법들 중 하나인데 자바에서 싱글톤 패턴을 적용하는데 가장 좋은 기법이라고 한다.
➙ 싱글톤 클래스를 enum으로 생성해주는 방식이다.
- enum으로 싱글톤 클래스를 생성해주면 ?
➙ enum은 보통 생성자를 private으로 선언해주기 때문에 자연스럽게 싱글톤 패턴 방식이 적용된다.
➙ 그리고 enum 클래스의 멤버들은 public static final로 생성해주기 때문에 멀티 쓰레드 환경에서도 안정적이다 !
- 즉, Enum 사용하면 'LazyHolder' 기법과 달리 기본적으로 직렬화가 가능해서 Serializable 인터페이스를 따로 구현해줄 필요도 없고 Refleciton 문제도 발생하지 않기 때문에 가장 권장되는 기법인 것이다.
- enum 생성자를 왜 private으로 선언해줄까 ?
➙ enum은 서로 관련있는 상수들을 모아 특정한 타입으로 다루기 위한 클래스이다.
➙ 즉, "Enumeration(열거)" 라는 이름에서 알 수 있듯이 상수들이 열거된 형태의 집합이다.
➙ 따라서, 컴파일 타임(complie-time)에 모두 결정되어야 하는 값들이기 때문에, 컴파일 이후에 외부(다른 클래스, 패키지 등)에서 enum 클래스에 접근해 동적으로 변경되는 것을 막고자 생성자를 private으로 선언해주는 것이다 !
- enum 클래스의 멤버들은 왜 'public static final' 로 생성해줄까 ?
➙ enum은 상수를 단순한 정수가 아니라 객체화해서 객체처럼 다루기 위해 등장한 개념이기 때문에, enum의 멤버는 일종의 객체라고 볼 수 있다.
➙ 즉, enum의 멤버(상수) 하나하나가 자신의 인스턴스(instance)를 생성하고 'public static' 로 다른 클래스에 직접적으로 공유해주는 것이다.
➙ 그리고 'final' 을 선언해줌으로써 자신의 인스턴스가 딱 하나임을 보장해줄 수 있기 때문에 이 점에서도 싱글톤 패턴의 특징이 드러난다고 볼 수 있다.
enum (참고 자료) - [JAVA] 지네릭스(Generics) & 열거형(Enumeration) & 애너테이션(Annotation) — Uykm_Note (tistory.com)
[JAVA] 지네릭스(Generics) & 열거형(Enumeration) & 애너테이션(Annotation)
🌑 지네릭스 (Generics) ✔️ Generics 🔹 Generics 란 ? 다양한 타입의 객체들을 다루는 메서드나 Collection class에 컴파일 시의 타입체크(compile-time type check)를 해주는 기능 ➠ 객체의 타입 안정성을 제공
ukym-tistory.tistory.com