
싱글톤 패턴 정리
DesignPattern
2025.10.16.
1. 싱글톤 패턴
단 하나의 유일한 객체를 만들기 위한 코드 패턴
싱글톤 패턴의 목표는 크게 2가지이다.
- 인스턴스를 오직 한개만 만들어 제공하는 클래스가 필요
- 만들어진 한개의 인스턴스에 글로벌하게 접근할 수 있는 방법이 필요하다.
2. 싱글톤 패턴을 사용하는 이유
ex) DB커넥션과 Pool을 담당하는 인스턴스, 시스템 전역의 로깅을 담당하는 로거
- 시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러 개 일 때 문제가 생길 수 있는 객체들을 한 곳에서 제어하기 위해서이다.
-> 따라서 싱글톤 패턴은 2가지 목적이 존재
- 인스턴스를 오직 1개만 만들어야한다. (한 곳에서만 제어하게)
- 만든 인스턴스에 글로벌하게 접근하는 방식을 제공해야한다.
3. 싱글톤 패턴 구현하는 방법
3.1. private 생성자에 static 메소드
- 가장 간단하고 보편적인 방법이지만, 수많은 스레드가 동시에 생성 체크(if) 문에 접근 시 객체 생성 체크를 통과하기 때문에, 멀티스레드 환경에서는 안전하지 않은 방법
- 객체를 호출할 떄 인스턴스를 생성하는 Lazy initialization 방식
- instance 호출 시 만들어진 instance 가 있다면 반환해주고 없다면 생성
public class SettingsPrivateStatic {
private static SettingsPrivateStatic instance;
private SettingsPrivateStatic() {}
public static SettingsPrivateStatic getInstance() {
if (instance == null) {
instance = new SettingsPrivateStatic();
}
return instance;
}
}
다음을 직접 설명해라
1. 생성자를 private으로 만든 이유?
-> 외부에서의 **객체 생성을 막고, 한 곳에서 instance를 관리**하기 위해서
2. getInstance() 메소드를 static으로 선언한 이유?
-> 외부에서 글로벌하게 만들어진 **객체에 접근하는 방법**이 필요하기 때문
3. getInstance()가 멀티쓰레드 환경에서 안전하지 않은 이유?
-> 객체 생성 전, 동시에 생성함수에 스레드가 접근하게 되면, 올바른 객체 생성 체크가 이루어지지 않을 수 있기 때문
3.2. Thread safe initialization
- synchronized 를 사용하여 객체대한 접근에 lock 을 걸어 Thread Safe 하게 만드는 방법
- lock 을 사용해서 순차접근을 하는 것이기 때문에 성능저하가 일어날 수 있다.
public class SettingsSynchronized {
private static SettingsSynchronized instance;
private SettingsSynchronized() {}
public static synchronized SettingsSynchronized getInstance() {
if (instance == null) {
instance = new SettingsSynchronized();
}
return instance;
}
}
다음을 직접 설명해라
1. 자바의 동기화 블럭 처리 방법은?
-> 여러 스레드가 동시에 하나의 자원에 접글할 때 데이터 일관성과 정합성을 보장하기 위해 사용
-> synchronized 키워드 사용 (동기화 블럭 또는 메소드 지정), 내부적으로 모니터 락 또는 뮤텍스 개념을 이용
2. getInstance() 메소드 동기화시 사용하는 락(lock)은 인스턴스의 락인가 클래스의 락인가? 그 이유는?
-> 클래스 락이다.
- static 메소드에 붙으면 (클래스 락)
- 그 클래스 자체(Class 객체)에 락이 걸린다.
- 클래스 단위로 단 하나의 락만 존재
- 같은 객체로 메소드를 여러 스레드가 동시에 호출(어떤 인스턴스로 접근하든)하면 한 번에 한 스레드만 실행 가능하고 다른 스레드는 락이 풀릴 때까지 대기, 다른 객체로 접근하면 동시에 실행 가능 (락이 다르기 때문에)
- 인스턴스 메소드에 붙으면 (인스턴스 락)
- 그 메소드를 호출한 객체의 락(this)을 건다.
- 각 인스턴스마다 락이 따로 존재
- 모든 인스턴스가 공유하는 클래스 단위 락이기 때문에 한 스레드가 실행 중이면 다른 스레드는 어떤 인스턴스로도 그 static 메소드를 동시에 실행 불가
3.3. 이른 초기화 (eager initialization)
- 멀티쓰레드환경에서 안전한 싱글톤의 2번째 방법
- 클래스가 로드되는 시점에 객체가 미리 생성되므로, 별도의 동기화 없이 안전하게 사용할 수 있음
- 단점: 앱 구동 시점에 객체가 미리 생성되므로, 객체 생성 비용이 큰 경우 불필요하게 리소스를 점유할 수 있음
public class SettingsEagerInitialization {
private static final SettingsEagerInitialization instance = new SettingsEagerInitialization();
private SettingsEagerInitialization() {}
public static SettingsEagerInitialization getInstance() {
return instance;
}
}
다음을 직접 설명해 보세요.
1. 이른 초기화가 단점이 될 수도 있는 이유?
-> 앱 구동 시 미리 만드는데 객체 생성 비용이 큰 경우 너무 많은 리소스 사용
2. 만약에 생성자에서 checked 예외를 던진다면 이 코드를 어떻게 변경해야 할까요?
-> Static block initialization 방법 사용
- static block : 클래스가 로딩되고 클래스 변수가 준비된 후 자동으로 실행되는 블록
class Singleton {
// 싱글톤 클래스 객체를 담을 인스턴스 변수
private static Singleton instance;
// 생성자를 private로 선언 (외부에서 new 사용 X)
private Singleton() {}
// static 블록을 이용해 예외 처리
static {
try {
instance = new Singleton();
} catch (Exception e) {
throw new RuntimeException("싱글톤 객체 생성 오류");
}
}
public static Singleton getInstance() {
return instance;
}
}
3.4. double checked locking
- 매번
synchronized동기화가 문제이니, 최초 초기화 할때만 적용하고 이미 만들어진 인스턴스를 반환할떄는 사용하지 않도록 하는 기법 - I/O 불일치를 해결하기 위해 volatile 이라는 키워드를 사용해야 하는데, 이 키워드는 JVM에 대한 심층적인 이해가 필요하고, JVM에 따라서 여전히 쓰레드 세이프 하지 않는 경우가 발생하기 때문에 사용 지양
- volatile 키워드
- 메모리 가시성과 명령 재정렬 방지를 보장
- 메모리 가시성
- 멀티스레드 환경에서 여러 CPU 코어는 성능을 위해서 각각의 쓰레드들은 변수를 메인 메모리(RAM)으로부터 가져오는 것이 아니라 캐시(Cache) 메모리에서 가져오게 된다.
- 이로 인해 한 스레드가 변경한 변수
- 의 값이 다른 스레드의 캐시에는 반영되지 않아 서로 다른 값을 볼 수도 있다.
- 그래서 volatile 키워드를 통해 이 변수는 캐시에서 읽지 말고 메인 메모리에서 읽어오도록 지정해주는 것이다.
- 명령 재정렬 문제
- 자바에서 객체 생성 과정은 1. 메모리 공간 할당, 2. 생성자 호출, 3. 변수에 인스턴스 참조 대입
- 컴파일러나 CPU의 최적화 과정에서 2 -> 3 순서가 바뀌는 경우가 발생
- 한 스레드가 아직 생성자를 다 수행하지 않은 객체를 다른 스레드에게 참조로 넘기는 경우, 다른 스레드가 불완전한 객체에 접근
volatile을 사용하면 이러한 명령어 재배치(reordering)을 금지해 객체가 완전히 초기화된 후에만 다른 스레드가 볼 수 있도록 보장
- volatile 키워드
public class SettingsDoubleCheckSynchronized {
private static volatile SettingsDoubleCheckSynchronized instance;
private SettingsDoubleCheckSynchronized() {}
public static synchronized SettingsDoubleCheckSynchronized getInstance() {
if (instance == null) {
synchronized (SettingsDoubleCheckSynchronized.class) {
if (instance == null) {
instance = new SettingsDoubleCheckSynchronized();
}
}
}
return instance;
}
}
다음을 직접 설명해 보세요.
1. double check locking이라고 부르는 이유?
-> 두번의 조건 검사가 있기 때문에
- 첫번째 if에서 인스턴스가 이미 생성되어 있는지 1차로 검사, 이미 생성된 경우 synchronized 블록에 들어가지 않아도 되므로 성능 향상의 효과
- 여러 스레드가 동시에 첫번째 if문을 통과할 가능성이 있으므로 동기화 블록 안에서 한번 더 검사하여 실제 인스턴스를 한번만 생성되게 한다.
2. instacne 변수는 어떻게 정의해야 하는가? 그 이유는?
-> volatile 키워드를 사용, 메모리 가시성과 명령 재정렬 방지를 보장하기 위해
3.5. Bill Pugh Solution (LazyHolder)
- 권장하는 2가지 방법 중 1개
- 멀티스레드 환경에서도 안전하고, 호출될 때 instance 가 만들어지는 Lazy initialization방식
- 클래스 안에 내부 클래스를 두어 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 방법
- static 메소드에서는 static 멤버만을 호출할 수 있기 때문에 내부 클래스를 static으로 설정, 이밖에도 내부 클래스의 치명적인 문제점인 메모리 누수 문제를 해결하기 위하여 내부 클래스를 static으로 설정
public class SettingsStaticInner implements Serializable {
private static SettingsStaticInner instance;
private SettingsStaticInner() {}
private static class SettingsHolder {
private static final SettingsStaticInner INSTANCE = new SettingsStaticInner();
}
public static SettingsStaticInner getInstance() {
return SettingsHolder.INSTANCE;
}
}
다음을 직접 설명해 보세요.
1. 이 방법은 static final를 썼는데도 왜 지연 초기화 (lazy initialization)라고 볼 수 있는가?
-> static 필드는 클래스가 처음 로딩될 때 정적인 메모리 공간에 만들어지는데, holder가 가지고 있는 클래스가 로딩되는 시점은 getInstance()를 호출할 때 로딩되기 때문에 lazy-initialization이라고 한다.
이 방법은 클라이언트가 임의로 싱글톤을 파괴할 수 있다는 단점을 지닌다.
- 리플렉션
구체적인 클래스 타입을 알지 못해도, 클래스에 대한 메서드, 타입, 변수들을 접근할 수 있도록 해주는 Java API
- 자바에서 제공하는 리플렉션 API를 사용해서 내부 설정을 변경할 수 있다
- 리플렉션으로 생성자를 가져와, 접근 제어자에 대한 설정을 변경해 Singleton으로 구현한 객체를 새로운 인스턴스로 생성할 수 있다.
-> 해당 객체의 생성자를 불러오고 → 접근제어를 풀고 → 새로운 인스턴스를 생성하여 싱글톤 객체의 제어에서 벗어날 수 있다.
다음을 직접 설명해 보세요.
1. 리플렉션에 대해 설명하세요.
-> 구체적인 클래스 타입을 알지 못해도, 클래스에 대한 메서드, 타입, 변수들을 접근할 수 있도록 해주는 Java API
2. setAccessible(true)를 사용하는 이유는?→ 필드나 메서드의 접근제어 지시자에 대한 제어를 변경하기 위한 메소드
-> 즉 private으로 제어한 접근 설정에 대해 접근 권한을 주는 행위
- 직렬화 및 역직렬화의 사용
- 객체를 직렬화시킨 후 다시 역직렬화 시켜서 새로운 객체를 만드는 방법
- 자바에서 외부 파일을 역직렬화 할 시에는, 반드시 생성자를 사용하여 새로운 인스턴스를 만들어 준다.
- 이러한 설정을 이용해서, Singleton 설정을 깨뜨릴 수 있다.
다음을 직접 설명해 보세요.
1. 자바의 직렬화 & 역직렬화에 대해 설명하세요.
-> 직렬화(Serialization): 객체를 바이트 스트림으로 변환하여 파일, 네트워크, 데이터베이스 등에 저장하거나 전송할 수 있도록 만드는 과정
-> 역직렬화(Deserialization): 바이트 스트림을 다시 원래 객체로 복원하는 과정
2. SerializableId란 무엇이며 왜 쓰는가?
-> serialVersionUID는 직렬화된 객체의 버전을 관리하는 고유한 ID 값.
-> 객체의 클래스가 변경되었을 때 역직렬화 오류를 방지하기 위해 사용됨.
3. try-resource 블럭에 대해 설명하세요
-> try-with-resources는 AutoCloseable을 구현한 리소스를 자동으로 닫아주는 try문.
-> 자원(File, Socket, Stream 등)을 명시적으로 close() 호출하지 않아도 자동 해제됨.
3.6. Enum 이용
enum SingletonEnum {
INSTANCE;
private final Client dbClient;
SingletonEnum() {
dbClient = Database.getClient();
}
public static SingletonEnum getInstance() {
return INSTANCE;
}
public Client getClient() {
return dbClient;
}
}
public class Main {
public static void main(String[] args) {
SingletonEnum singleton = SingletonEnum.getInstance();
singleton.getClient();
}
}
4. 싱글톤 패턴 복습
최종 정리하자면, 싱글톤 패턴 클래스를 만들기 위해서는 Bill Pugh Solution기법을 사용하거나 Enum으로 만들어 사용
이 둘의 사용 선택은 자신의 싱글톤 클래스의 목적에 맞게 사용한다
- LasyHolder : 성능이 중요시 되는 환경
- Enum : 직렬화, 안정성이 중요시 되는 환경
4.1 싱글톤 패턴의 단점
- 모듈 간 의존성 증가
- 싱글톤 객체를 여러 모듈이 공유하기 때문에, 인스턴스 변경 시 참조 모듈도 수정 필요
- 남용 시 클래스 간 결합도가 높아져 패턴 사용이 오히려 문제를 일으킬 수 있음
- S.O.L.I.D 원칙 위배 가능
- 단일 책임 원칙(SRP): 하나의 싱글톤이 여러 책임을 가질 수 있음
- 개방-폐쇄 원칙(OCP): 다른 클래스와 강하게 결합되면 변경 시 영향 범위가 커짐
- 의존 역전 원칙(DIP): 클라이언트가 추상화가 아닌 구체 클래스에 의존하게 됨
- 단위 테스트(TDD) 어려움
- 인스턴스를 전역에서 공유하기 때문에 테스트 간 독립성이 깨질 수 있음
- 상태 초기화가 필요하며, Mock 객체 생성과 테스트가 어려움
5. 싱글톤 패턴 실무에서는 어떻게 쓰이나
- 스프링에서 빈의 스코프 중에 하나로 싱글톤 스코프
- 자바 java.lang.Runtime
- 다른 디자인 패턴 (빌더, 퍼사드, 추상 팩토리 등) 구현체의 일부로 쓰인다
Reference
코딩으로 학습하는 GoF의 디자인 패턴
싱글톤(Singleton) 패턴 - 꼼꼼하게 알아보자 - Inpa Dev