-
Singleton Pattern 싱글톤 패턴Programming/Java 2020. 12. 30. 21:49
Singleton Pattern
singleton pattern은 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 design pattern이다. 생성된 객체는 어디에서든지 참조 할 수 있다. 그렇기 때문에 멀티 스레드 환경에서 다수의 스레드가 해당 객체를 사용하기 때문에 Thread-safe가 보장되어야 한다.
Java에서 singleton pattern을 생성하는 방법 다양하다.
Eager initialization
public class Singleton { private static final Singleton instance = new Singleton(); public static Singleton getInstance() { return instance; } private Singleton() {} public void print() { System.out.println("이것은 singleton pattern이 보장된 객체입니다."); } }
private static final Singleton instance = new Singleton();
static 을 사용하여 클래스 로더가 초기화 하는 시점에 해당 인스턴스를 메모리에 등록한다. static 영역에 객체를 1개만 생성한다. 이렇게 컴파일 시점에 객체가 생성되기 때문에 Thread-safe한 것을 알 수 있다.
public static Singleton getInstance() { return instance; }
객체 인스턴스 사용을 위해서는 오직 getInstance 메소드를 통해서만 조회가 가능하다. 이 메소드를 호출하면 하상 같은 인스턴스를 반환하는 것을 보장한다.
private Singleton() {}
1개의 객체 인스턴스만 존재해야 하기 때문에 생성자를 private으로 막아서 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.
synchronized를 사용한 Lazy initialization
public class Singleton { private static final Singleton instance; public static synchronzied Singleton getInstance() { if (instance == null) instance = new Singleton(); return instance; } private Singleton() {} public void print() { System.out.println("이것은 singleton pattern이 보장된 객체입니다."); } }
컴파일 시점에 인스턴스를 생성하는 것이 아니라 인스턴스가 필요한 시점, 즉 런타임 시점에 인스턴스가 생성된다. synchronized 키워드를 사용하였기 때문에 해당 객체의 lock, unlock이 반복되면 성능이 떨어지게 된다.
DCL (Double Checking Locking) 방식을 사용한 Lazy initialization
public class Singleton { private volatile static final Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } private Singleton() {} public void print() { System.out.println("이것은 singleton pattern이 보장된 객체입니다."); } }
DCL 방식을 활용한 Lazy Initialization은 인스턴스가 생성되지 않은 경우에만 synchronized block이 실행하도록 구현한 방식이다. 이전 코드에서 보지 못한 volatile 키워드가 사용된다.
volatile
Java 변수를 Main Memory에 저장하겠다는 것을 명시한다. 변수의 값을 Read 할 때 CPU cache에 저장된 값이 아닌 Main Memory에서 읽는다. 마찬가지로 변수의 값을 write할 때마다 Main Memory에 작성한다.
멀티 스레드 환경에서는 스레드가 변수 값을 읽어올 때 각각의 CPU cache에 저장된 값이 다르기 때문에 변수의 불일치 문제가 발생할 수 있다. volatile 키워드는 이러한 문제를 해결해준다.
정리하면 volatile 변수는 Main Memory에 값을 read and write하기 때문에 변수 값 불이치 문제를 예방한다.
Enum을 사용한 Lazy initialization
public enum Singleton { INSTANCE; }
enum은 Java 1.5 부터 지원한다. enum 인스턴스의 생성은 기본적으로 Thread-safe하다. 그렇기 때문에 스레드를 위한 코드가 없어도 된다. 하지만 enum내의 다른 메소드가 있는 경우 해당 메소드가 Thread-safe임을 보장하는 것은 온전히 개발자의 책임이 된다.
enum 방식은 복잡한 직렬화 상황이나, 리플렉션 공격에도 다수의 인스턴스가 생성되는 것을 막는다. 하지만 만드려는 Singleton 인스턴스가 enum 외의 클래스를 상속해야 한다면 해당 방식을 사용할 수 없다.
LazyHolder 방식을 사용한 Lazy initialization
public class Singleton { private static class InnerInstanceClass { private static final Singleton instance = new Singleton(); } public static Singleton getInstance() { return InnerInstanceClass.instance; } private Singleton() {} public void print() { System.out.println("이것은 singleton pattern이 보장된 객체입니다."); } }
LazyHolder 방식은 가장 많이 사용되는 singleton 구현 방식이다. volatile, synchronized 키워드 없이 동시성 문제를 해결하기 때문에 성능이 매우 뛰어나다.
private static class InnerInstanceClass { private static final Singleton instance = new Singleton(); }
Singleton 클래스에는 InnerInstanceClass의 변수가 존재하지 않는다. static 멤버 클래스일지라도 컴파일 시점에서 초기화 되는 것이 아니고 getInstance() 메소드를 호출할 때 초기화 된다. 즉 런타임 시점에 초기화됨과 동시에 Thread-safe하다.
Singleton Pattern에 문제점
1. 싱글톤 패턴 구현을 위한 부가적인 코드가 많이 들어간다. 예를 들면 static final intance, private 생성자 등
2. 의존관계에 있어서 클라이언트가 구체 클래스 자체에 의존하게 된다. DIP(의존 관계 역전의 원칙)를 위반하게 된다. 클라이언트가 구체 클래스에 의존하기 때문에 OCP(개방-폐쇄 원칙) 또한 위반할 가능성이 높다.
3. 테스트 작성이 어렵다.
4. 내부 속성을 변경하거나 초기화하기 어렵다.
5. private 생성자이기 때문에 자식 클래스를 만들기 어렵다. 유연성 또한 떨어진다.
References.
'Programming > Java' 카테고리의 다른 글
[Spring] Singleton Container (0) 2021.01.08 Java란? (0) 2021.01.03 SOLID, 좋은 객체 지향 설계의 5가지 원칙 (0) 2020.12.23 Spring 간단 정리 (0) 2020.12.23 EJB (Enterprise Java Beans) (0) 2020.12.23