자바의 디자인패턴은 상당히 다양한데 전부 다 꼼꼼하게 공부한 적은 없었다. 그래서 이번 부트캠프를 기회로 전부 상세하게 공부하고 내것으로 만들어보려고 한다.
싱글톤 패턴은 하나의 클래스에 오직 하나의 인스턴스만 존재하도록 보장하고, 이 인스턴스에 전역적으로 접근할 수 있는 방법을 제공하는 디자인 패턴이다.
즉, 프로그램 전체에서 단 하나의 객체만 필요할 때 사용하는 패턴이다.
공유 자원이 필요한 경우
설정이나 환경을 관리하는 객체
공용 캐시, 스레드 풀, 윈도우 관리자 등 프로그램 전체에서 일관성을 유지해야 하는 객체
Java
java.lang.Runtime (JVM 런타임 환경)
java.awt.Desktop (데스크탑 연동)
Logger 라이브러리들 (ex. Log4j, java.util.logging.Logger)
Spring Framework
(자세한 내용은 뒤에서 정리)
- 특징: 클래스가 로딩될 때 인스턴스를 미리 생성.
- 장점: 구현이 가장 간단, 멀티스레드 안전.
- 단점: 실제로 사용하지 않더라도 인스턴스가 생성되어 메모리 낭비 가능.
- 특징: static 블록을 사용하여 인스턴스를 초기화.
- 장점: 예외 처리를 넣을 수 있어 유연함.
- 단점: 이른 초기화와 동일하게 불필요한 인스턴스 생성 가능.(그냥 예외 처리만 추가한 것)
- 특징: 요청이 있을 때만 인스턴스를 생성.
- 장점: 메모리 효율적.
- 단점: 멀티스레드 환경에서는 안전하지 않음.
- 특징:
synchronized키워드로 동기화.- 장점: 멀티스레드 환경에서도 안전.
- 단점: 매번 동기화로 성능 저하 가능.
- 특징: 동기화 비용을 최소화하기 위해 2번 체크.
- 장점: 효율적이고 멀티스레드 안전.
- 단점: 코드가 다소 복잡.
volatile은 자바 키워드로, 멀티스레드 환경에서 변수의 값을 메인 메모리(Main Memory)에 항상 직접 읽고 쓰도록 강제하는 역할을 한다.
📌
volatile의 의미
- 자바에서 변수는 기본적으로 스레드마다 CPU 캐시에 복사되어 사용.
- 이렇게 되면 어떤 스레드가 값을 변경해도 다른 스레드에서는 변경된 값을 즉시 보지 못하는 문제(가시성 문제, visibility issue) 가 발생.
volatile키워드를 붙이면, 그 변수는 항상 메인 메모리에서 읽고 쓰기 때문에 모든 스레드가 동일한 최신 값을 보장.
📌 싱글톤에서
volatile이 필요한 이유Double-Checked Locking(DCL) 방식에서
instance를 생성할 때는new Singleton()이라는 명령이 실행됨
이 과정은 단순히 "메모리 할당 → 초기화 → 참조 변수 연결" 이 아니라, 컴파일러/CPU 최적화 때문에 순서가 바뀔 수 있음.
(사람이 코드를 작성한 순서와 다르게 동작할 수 있다는 의미. 컴파일러는 코드를 더 효율적으로 만들기 위해 메모리 접근 순서를 재배치하거나, CPU는 여러 명령어를 동시에 처리하면서 실제 실행 순서를 변경할 수 있기 때문.)
1. 메모리 할당
2. (최적화 때문에) 참조 변수 연결
3. 객체 초기화
이렇게 객체를 초기화 하기 전에 참조 변수를 연결하게 된다면, 다른 스레드가instance를 읽을 때 아직 초기화되지 않은 객체를 참조할 수 있는 문제가 발생.
volatile을 붙이면 이 명령어 재정렬(reordering)을 막아주어, 안전하게 객체가 초기화된 이후에 참조할 수 있도록 보장.
📌 정리
volatile= 모든 스레드가 변수의 최신 값을 볼 수 있도록 보장 + 명령어 재정렬 방지.- 싱글톤의 Double-Checked Locking 방식에서는
volatile이 없으면 잘못된 객체를 참조할 수 있으므로 필수.
- 특징: 정적 내부 클래스를 사용해 클래스 로딩 시점에 초기화. 내부클래스를 static으로 선언하였기 때문에, Singleton 클래스가 초기화되어도 SingleInstanceHolder 내부 클래스는 메모리에 로드되지 않음
- 장점: Lazy Initialization + 멀티스레드 안전 + 성능 우수.
- 단점: 약간의 문법 이해 필요. (실무에서 가장 많이 사용되는 방식) 다만 클라이언트가 임의로 싱글톤을 파괴할 수 있다는 단점을 지님 (Reflection API, 직렬화/역직렬화를 통해)
🤔 메서드로 정의하는거랑 클래스로 정의하는거랑 뭐가 다르냐❓
- 메서드 방식은 JMM(Java Memory Model)의 가시성/재배치 문제를 개발자가 직접 제어해서 안전성을 보장해야 함.
- 반면, Bill Pugh(정적 내부 클래스)는 JVM (Java Virtual Machine)의 클래스 로딩 & 초기화 메커니즘이 이미 원자적이고 스레드 안전하게 보장해줌. 따라서 개발자가 동기화를 구현해줄 필요가 없음.
- Bill Pugh 방식 = JVM 차원에서 안전 보장 → 가장 권장되는 구현
- 특징: Enum을 사용하여 싱글톤을 보장. enum은 애초에 멤버를 만들때 private로 만들고 한번만 초기화 하기 때문에 thread safe.
- 장점: 직렬화/역직렬화, 리플렉션(Reflection) 공격에도 안전.
- 단점: 싱글톤 클래스를 멀티톤(일반적인 클래스)로 마이그레이션 해야할때 처음부터 코드를 다시 짜야 되는 단점이 존재. (개발 스펙은 언제어디서 변경 될수 있기 때문에) 클래스 상속이 필요할때, enum 외의 클래스 상속은 불가능.
- 일반 싱클톤 클래스의 경우
- 위와 같이 강제로 생성자를 가져다가 접근 권한을 무시해서 새로운 인스턴스를 생성할 수 있다
- 코드 실행 결과 서로 다른 생성자가 생성된 것을 확인할 수 있다
반면,
- enum클래스의 경우
- 리플렉션을 시도하면,
- JVM이 아예 막아버려서 이렇게 에러가 나게 된다.
| 방식 | 멀티스레드 안전성 | 초기화 시점 | 장점 | 단점 |
|---|---|---|---|---|
| Eager Initialization | 안전 | 클래스 로딩 시 | 구현 간단 | 메모리 낭비 |
| Static Block | 안전 | 클래스 로딩 시 | 예외 처리 가능 | 메모리 낭비 |
| Lazy Initialization | 불안전 | 필요 시 | 메모리 절약 | 스레드 불안전 |
| Thread Safe (synchronized) | 안전 | 필요 시 | 구현 쉬움 | 성능 저하 |
| Double-Checked Locking | 안전 | 필요 시 | 효율적 | 코드 복잡 |
| Bill Pugh (Inner Class) | 안전 | 필요 시 | 가장 권장 | 문법 이해 필요 |
| Enum | 안전 | Enum 로딩 시 | 직렬화/리플렉션 안전 | Enum 문법 제약 |