싱글톤이란
자바에서 싱글톤(Singleton) 패턴은 객체 생성 관련 디자인 패턴 중 하나이다.
싱글톤 패턴은 클래스의 인스턴스가 애플리케이션 내에서 단 하나만 생성되도록 보장해주는 패턴이다.
이 패턴의 목적은 전역 상태를 관리하고 리소스에 대한 중복 접근을 방지하는데에 있다.
시스템 런타임, 환경 세팅 관련 정보와 같이 인스턴스가 여러개일 때 문제가 생길 수 있는 경우에서 이러한 클래스가 필요할 수 있다.
싱글톤의 특징 및 장점
- 단일 인스턴스 : 클래스의 인스턴스가 하나만 생성되고, 이 인스턴스에 대한 전역 접근을 제공할 수 있다.
- 스레드 안전(Thread Safety) : 인스턴스가 하나만 생성되도록 싱글톤을 구현할 수 있고, 이로 인하여 멀티스레드 환경에서 안전하게 만들 수 있다.
- 메모리 절약 : 인스턴스가 단 하나만 생성되므로 중복 생성을 방지하여 메모리를 절약할 수 있다.
- 접근 제어 : 인스턴스에 대한 접근을 제어할 수 있어 전역 상태를 관리하기 쉬워진다.
- 데이터 공유 : 앱에서 공통된 데이터 혹은 서비스에 대한접근 구현을 싱글톤을 통해 공유할 수 있다.
주의사항
클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기 어려워질 수 있다.
타입을 인터페이스로 정의한 다음 그 인터페이스를 구현하여 먼든 싱글턴이 아니라면 싱글턴 인스턴스를 mock 구현으로 대체할 수 없기 때문이다.
싱글톤 생성 방법
- private 생성자에 static 메서드. 근데 이제 volatile과 synchronized를 곁들인.
생성자를 private로 하여 밖에서 생성자를 사용하지 못하게 한다. 그 후 getInstance() 메서드를 통하여 인스턴스가 만들어져 있는지의 여부를 체크하여 만약 인스턴스가 만들어져 있다면 그때 만들어 주고, 그렇지 않다면 기존에 만들어진 인스턴스를 제공하는 방법으로 구현한다.
이 때 volatile 키워드와 synchronized 키워드를 이용하여 스레드 안정성을 제공한다.public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 첫 번째 검사 (락(lock)을 걸기 전에 검사) synchronized (Singleton.class) { if (instance == null) { // 두 번째 검사 (락을 건 후에 검사) instance = new Singleton(); } } } return instance; } }
volatile 키워드를 통하여 Java 변수를 메인 메모리에 저장하겠다고 명시를 한다면 변수의 값을 읽을 때 CPU 캐시에 저장된 값이 아니라 메인 메모리에서 읽게 된다. 따라서 변수의 최신 상태를 스레드 간에 공유하게 되므로 스레드 안정성을 제공할 수 있다.
그리고 synchronized를 통해서 여러 스레드가 동시에 같은 자원에 접근하는 것을 제어한다. 한 시점에 단 하나의 스레드만이 특정 자원에 접근하게 하고(락) 다른 스레드는 락이 해제될때 까지 대기하게 한다. 이를 통하여 작업의 순서를 보장한다.
덫붙여, if문 두개를 이용한 더블체크 형식으로 두번의 락을 걸어주는 방식을 이용하여 매번 synchronized를 사용하지 않게 하여 synchronized 비용을 아껴서 좀더 효율적인 동작을 할 수 있도록 한다. - 정적 팩토리 메서드를 public static 멤버로 제공
public class Singleton { public static final Singleton INSTANCE = new Singleton(); private Singleton() { } public static synchronized Singleton getInstance() { return INSTANCE; } }
미리 인스턴스를 만들어 놓고 getInstance() 호출시 이 인스턴스를 리턴해주는 방식으로 싱글톤을 구현하였다.
public static 필드가 final이므로 절대 다른 객체를 참조할 수 없는 형태이다. 이 방식의 장점은 코드의 간결함이다.
하지만 만약 초기 인스턴스 생성 비용이 큰 클래스인 경우에는 비효율적인 방법이 될 수 있다. - Initialization-on-demand holder idiom
JVM의 클래스 로더 메커니즘의 특성을 이용하여 싱글톤 인스턴스를 지연 초기화 하는 방법으로 구현하는 방법이 있다. 클래스는 JVM에 의해 처음 참조될 때로 로드되고, static inner 클래스는 외부 클래스가 로드될 때가 아니라 해당 내부 클래스가 사용될 때 로드되는데, 이점을 이용하여 싱글톤 인스턴스의 초기화를 inner 클래스에 위임하는 방식으로 구현한다.
public class Singleton { // Private constructor to prevent instantiation private Singleton() {} // 정적 내부 클래스를 사용하여 Singleton 인스턴스를 지연 초기화 private static class Holder { static final Singleton INSTANCE = new Singleton(); } // getInstance 메서드는 Singleton 인스턴스에 접근할 때 Holder 클래스를 참조하여, // 이 시점에 Holder.INSTANCE가 초기화됩니다. public static Singleton getInstance() { return Holder.INSTANCE; } }
Singleton클래스의 getInstance() 메서드가 호출될 때, 이 메서드는 inner 클래스인 Holder의 INSTANCE를 리턴한다. 이 때의 접근이 Holder 클래스의 처음 사용이 되므로, 이 시점에 Holder 클래스가 로드되어 초기화되고 Singleton 인스턴스가 생성된다(Lazy load). 이후로 getInstance() 메서드를 통해 Singleton 인스턴스에 접근할 때마다 동일한 인스턴스를 반환하기 때문에 싱글톤이 보장된다.
JVM의 클래스 초기화 과정은 스레드에 안전하므로 멀티스레드 환경에서도 Singleton 인스턴스의 유일성과 스레드 안전성이 보장된다. 또한 getInstance() 메서드가 실제로 호출되기 전까지 Singleton 인스턴스가 생성되지 않으므로 자원을 효율적으로 사용할 수 있게 된다. 게다가 synchronized 를 사용하지 않으므로 이에 대한 비용이 없으므로 getInstance() 호출에 따른 성능 저하도 없다. - 열거타입(enum)을 사용한 구현
Enum으로 싱글톤을 구현하고 내부 메서드로 원하는 기능을 구현할 수 있다.
public enum SingletonEnum { INSTANCE // 필요한 추가적인 메서드나 속성을 이곳에 추가할 수 있습니다. public void doSomething() { // 원하는 동작을 수행합니다. } }
위 Enum 코드와 유사한 기능을 하는 일반 클래스를 코딩해 보자면 아래와 비슷한 코드가 될 것이다.
public class SingletonClass { private static final SingletonClass INSTANCE = new SingletonClass(); // private 생성자 private SingletonClass() { // 초기화 코드 } // 정적 팩토리 메서드 public static SingletonClass getInstance() { return INSTANCE; } // 필요한 추가적인 메서드나 속성을 이곳에 추가할 수 있습니다. public void doSomething() { // 원하는 동작을 수행합니다. } }
보다시피, 클래스를 싱글톤으로 만들기 위해 정적 팩토리 메서드를 이용하여 인스턴스를 제공하는 방법보다 Enum으로 구현한 코드가 간결하다.
간결함 이외에도 여러가지 장점을 가질 수 있다. 우선 Enum은 스레드 안정성을 보장한다. 그리고 Enum은 JVM 내에서 클래스가 로드될 때 단 한번만 인스턴스가 생성되기 때문에 생성비용이 큰 경우 메모리와 리소스를 효율적으로 관리할 수 있도록 해준다.
하지만 몇가지 단점도 있다. 우선 Enum은 상속이나 구현이 불가능하다. 따라서 유연성이 제한된다고 볼 수 있다. 또한 단위 테스트가 어려울 수 있다. Enum의 인스턴스는 변경할 수 없기 때문에 Mock을 두는 것에 어려움이 있을 수 있기 때문이다. 마지막으로 Enum의 인스턴스는 클래스 로딩시점에 생성된다고 하였는데, 이때문에 필요한 설정이나 환경에 따라 런타임에 동적으로 변경되어야 하는 경우에는 적합하지 않다.
'Java' 카테고리의 다른 글
어노테이션(Annotaion) + Custom Annotation (0) | 2024.04.01 |
---|---|
리플렉션(Reflection) (0) | 2024.03.14 |
불변 객체(Immutable Object) (0) | 2024.03.04 |
Builder Pattern (빌더 패턴) (0) | 2024.03.02 |
BigInteger.valueOf와 정적 팩토리 메서드 (0) | 2024.02.29 |