프로그램의 시작과 종료까지 인스턴스가 단 하나만 생성되도록 보장하는 디자인 패턴을 의미
프로그램 전역에서 이 인스턴스를 공유하며 사용할 수 있음
새로운 인스턴스를 외부에서 생성하지 못하도록 생성자를 private 로 선언
프로그램 전역에서 자주 사용되고 일관되게 정해진 동작을 하는 기능을 가진 인스턴스가 있다면 싱글톤 패턴으로 구현하는 게 효율이 높을 수 밖에 없음
같은 동작을 하는 인스턴스를 메모리에 할당하고 해제하며 사용하기 보단 이미 생성해둔 인스턴스를 사용하는 것이 효율적
public class TestSingleton {
private static TestSingleton testSingleton;
// private 생성자: 외부에서 인스턴스 생성 방지
private TestSingleton() {}
// 인스턴스 생성 또는 반환
public static TestSingleton getInstance() {
if (testSingleton == null) { // 최초 호출 시에만 인스턴스 생성
testSingleton = new TestSingleton();
}
return testSingleton;
}
}
getInstance() 메서드를 통해 클래스의 유일한 인스턴스에 접근 가능
생성자를 private 처리하여 객체를 직접 생성할 수 없기 때문에 객체를 static 으로 정의하여 getInstance() 메소드를 사용할 수 있도록 해줌.
자바는 멀티 쓰레드를 지원하나 싱글톤 패턴은 멀티 쓰레드 환경에서 안전하지 않음
1. 멀티 쓰레드 환경에서 여러개의 인스턴스가 생길 수 있음
2. 멀티 쓰레드 환경에서 변수 값의 일관성이 없어질 가능성이 있음
public class TestSingleton {
private static TestSingleton testSingleton;
private TestSingleton() {}
public static synchronized TestSingleton getInstance() {
if (testSingleton == null) {
testSingleton = new TestSingleton();
}
return testSingleton;
}
}
synchronized로 동기화 처리를 해줄 수 있으나 성능 저하가 나타날 수 있음
public class TestSingleton {
private static volatile TestSingleton testSingleton;
private TestSingleton() {}
public static TestSingleton getInstance() {
if (testSingleton == null) {
synchronized (TestSingleton.class) {
if (testSingleton == null) {
testSingleton = new TestSingleton();
}
}
}
return testSingleton;
}
}
volatile 키워드를 통해서 인스턴스가 제대로 초기화 되었는지 보장해주며
성능 및 스레드 안정성을 모두 높일 수 있음
synchronized로 공유 객체에 대한 동기화 처리까지 해줘야 싱글톤 보장됨
volatile은 자바에서 변수의 가시성(visibility) 문제를 해결하기 위해 사용되는 키워드 (JVM 1.5 이상)
어떤 변수에 volatile 키워드를 붙이면 해당 변수는 모든 읽기와 쓰기 작업이 CPU 캐시가 아닌 메인 메모리에서 이루어지게 되며 이로써 해당 변수 값에 대한 가시성을 보장 (읽기, 쓰기 작업의 동시성 문제를 해결해줌)
즉, 멀티 스레드 환경에서 한 스레드가 변수의 값을 변경하면, 다른 스레드에서도 그 변경 사항을 즉시 볼 수 있도록 보장
public enum TestSingleton {
INSTANCE;
public void getMyName() {
System.out.println("고병갑 짱짱!");
}
}
간단한 싱글톤 구현 시 Enum으로 구현 가능(권장)
Enum 은 private로 만들며 한번만 초기화 하기에 thread에서 안전하게 사용 가능
Reflection으로부터도 안전함
하지만 만일 싱글톤 클래스를 멀티톤(일반적인 클래스)로 마이그레이션 해야할때 처음부터 코드를 다시 짜야 되는 단점이 존재
Enum을 사용하기 때문에 상속 불가능
public class TestSingleton {
private TestSingleton() {
// 리플렉션 방지
if (Holder.INSTANCE != null) {
throw new IllegalStateException("Instance already created");
}
}
// 정적 내부 클래스: TestSingleton 인스턴스를 보관
private static class Holder {
private static final TestSingleton INSTANCE = new TestSingleton();
}
// 싱글톤 인스턴스에 접근 메서드
public static TestSingleton getInstance() {
return Holder.INSTANCE;
}
}
가장 효율적으로 싱글톤을 구현할 수 있는 방법
Holder 클래스는 TestSingleton 클래스가 참조될 때까지 로드 되지 않음
즉, TestSingleton.getInstance()가 호출될 때 처음으로 Holder 클래스가 로드되고 그 시점에 INSTANCE가 생성됨
JVM의 클래스 로드 메커니즘 덕분에 인스턴스는 실제로 필요할 때만 생성 (Lazy Initialization(지연 초기화)) 👉 메모리와 초기화 비용 절감
JVM 클래스 로더가 클래스 로드와 초기화를 스레드 안전하게 처리하여 synchronized 키워드 사용하지 않아도 됨
생성자에서 INSTANCE가 이미 초기화되어 있다면 예외를 던지도록 설정해 리플렉션 공격을 방지