[목차]
1. 싱글톤 패턴이란
2. 싱글톤 패턴은 왜 사용하는 것일까?
3. 미리 알아두면 이해가 잘되는 사전준비
4. 싱글톤 패턴의 구현 방법
1. Eager Initialization
2. Static Block Initialization
3. Lazy Initialization
3-1. Lazy Initialization with Synchronzied
3-2. Lazy Initialization with DCL(Double Checking Locking)
3-3. Lazy Initialization with Enum
3-4. Lazy Initialization with Holder (Bill Pugh)
| Singleton Pattern - 싱글톤 패턴이란?
- 싱글톤 패턴은 생성 패턴(Creational Pattern)에 속하는 패턴입니다.
- 클래스 인스턴스가 하나만 만들어지게 하며, 해당하는 인스턴스에 대한 전역 접근을 제공합니다.
- 어떠한 클래스가 최초 한 번만 메모리('static area')에 할당하도록 하고 해당하는 객체를 만들어 사용하는 디자인 패턴입니다.
-> 하나의 인스턴스를 메모리에 등록해서 여러 쓰레드가 해당 인스턴스를 공유하여 사용할 수 있도록 하는 것입니다.
| Singleton Pattern은 왜 사용하는 것일까?
- 메모리 영역에 할당되어 생성된 인스턴스를 지속적으로 사용하기 때문에 이전보다 메모리 낭비를 방지할 수 있습니다.
- 생성된 인스턴스는 전역 인스턴스이기 때문에 다른 클래스의 인스턴스들이 데이터 공유를 하기가 쉽습니다.
- 공통된 객체를 여러개 생성해서 사용하는 경우에 많이 사용합니다.
ex) 캐시혹은 설정 관련
[Singleton Pattern의 단점]
- SOLID 원칙을 위배할 수 있습니다.
- 수정이 어려워지고 테스트하기가 어렵습니다.
- 서버 환경에서는 싱글톤이 1개만 생성된다고 보장할 수 없습니다.
-> 클래스 로더를 어떻게 구성하냐에 따라 복수의 객체가 만들어질 수 있습니다.
- private 생성자를 갖고 있어서 상속이 불가능합니다.
[Singleton Pattern을 구현할 때 주의해야하는 부분]
- 싱글톤 패턴을 구현할 때 조심해야할 부분은 동시성(Concurrency) 문제입니다.
- 싱글톤의 역할이 커질수록 해당 싱글톤 객체를 사용하는 다른 객체간의 결합도가 높아져객체 지향 설계 원칙에 어긋나게 됩니다.
(객체 지향 설계의 '개방-폐쇄')
-> 수정이 어려워지고 테스트하기가 어려워집니다.
| 여기서 잠깐! 구현 방법을 보기 전에 이것만큼은 간략히 알고 가자!
싱글톤 패턴의 구현 방법을 설명하다보면, 메모리나 바인딩, 객체 지향 설계에 관한 여러 이야기를 함께 할 수 밖에 없습니다. 그러므로, 사전 지식으로 간략하게 아 이런거구나! 하는 짧은 정리를 보고 아래 글을 읽는 다면 보다 잘 이해가 될 것입니다.
1. 정적 바인딩 vs 동적 바인딩
- 정적 바인딩(Static Bindiing) : 컴파일 시점에 성격이 결정되며, 컴파일을 하게 되면 static 메모리에 올라가게 됩니다.
- 동적 바인딩(Dynamic Binding) : 파일을 실행하는 시점에 성격이 결정됩니다. (상속해서 메소드를 오버라이딩 하는 경우가 있을겁니다!
2. 객체지향설계 5대 원칙(SOLID)중 OCP(Open Close Principle) - 개방폐쇄 원칙
- OCP : 기존의 코드의 변경을 최소화 혹은 변경 하지 않으면서 기능을 추가할 수 있도록 설계해야 한다는 의미입니다.
3. 클래스 로딩 단계
- Java ClassLoader
- Java는 클래스 파일을 동적으로 읽습니다. JVM이 동작하다가 어떠한 클래스 파일을 참조하는 순간 해당하는 파일을 읽으며 로드되는 것입니다. - 그러한 과정 중, 클래스 로딩 단계는 첫번째 단계에 해당합니다. .class파일을 읽고, 읽은 파일에 따라 바이트 코드를 만들고 메소드 영역에 저장하는 동작을 합니다.
4. Enum의 특징
- 모든 Enum 타입은 프로그램 내에서 한번 초기화 되는 점을 이용하여 싱글톤을 구현할 수 있습니다.
- Enum 자체가 serialization와 thread-safe를 보장합니다.
(Enum 타입은 JVM에 의해 thread-safe한 점을 이용하여 싱글톤 패턴을 구현할 수 있습니다.)
| 싱글톤 패턴의 구현 방법
1. Eager Initialization
public class EagerInitialization {
private static EagerInitialization instance = new EagerInitialization();
private EagerInitialization() {
}
public static EagerInitialization getInstance() {
return instance;
}
public void printHashCode() {
System.out.println("HashCode of Instance : " + instance.hashCode());
}
}
- 클래스가 호출될 때 인스턴스를 생성하는 방법입니다.
- 인스턴스를 사용하지 않더라도 인스턴스가 생성되기때문에 효율성 측면에서 다른 방법에 비해 떨어집니다.
(인스턴스를 사용하던, 사용하지 않던 클래스 로딩 단계에서 생성한다 -> 문제가 발생할 수 있다.) - Eager Initialization은 Exception에 대한 Handling을 제공하지 않습니다.
- instance라는 전역 변수를 선언하는데 private로 되어 있어 직접적인 접근이 불가능합니다.
- 생성자도 private으로 되어있기에 new를 사용하여 객체를 생성할 수 없습니다. 즉, getInstance() 메소드를 통해서 해당 인스턴스를 얻을 수 있습니다.
2. Static Block Initialization
- Eager Initialization에서 Exception Handling을 하지 못한 단점을 static block을 사용하여 보완한 방법입니다.
public class StaticBlockInitialization {
private static StaticBlockInitialization instance;
public StaticBlockInitialization() {
}
static {
try {
instance = new StaticBlockInitialization();
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
public static StaticBlockInitialization getInstance() {
return instance;
}
public void printHashCode() {
System.out.println("HashCode of Instance : " + instance.hashCode());
}
}
- static 블럭을 사용하여 Exception Handling에 대한 처리를 할 수 있지만, Eager Initialization처럼 클래스 로딩 단계에서 인스턴스를 생성하기 때문에 여전히 문제가 남아있습니다.
3. Lazy Initialization
- 컴파일 시점에 인스턴스를 생성하는 것이 아니라, 인스턴스가 필요한 시점에 요청하여 동적 바인딩을 통해 인스턴스를 생성하는 방식을 말합니다.
public class LazyInitialization {
private static LazyInitialization instance;
public LazyInitialization() {
}
public static LazyInitialization getInstance() {
if (instance == null) {
instance = new LazyInitialization();
}
return instance;
}
public void printHashCode() {
System.out.println("HashCode of Instance : " + instance.hashCode());
}
}
- if 조건문을 이용하여 instance가 null인 경우에만 new를 사용해 객체를 생성합니다. 이 방식을 이용해 사용시점에
인스턴스를 생성하기 때문에 메모리에 적재되는 시점의 부담을 줄일 수 있습니다. - 하지만, 멀티 쓰레드의 환경이라면 만약 동일 시점에 getInstance() 메소드가 호출되어 인스턴스가 두번 생성될 수 있기 때문에 싱글톤 패턴이 안전하지 않을 수 있습니다.
3-1. Lazy Initialization with Synchronized
- synchronized 키워드를 이용한 Lazy Initialization 방법이다. 메소드에 동기화 블럭을 지정해서 thread-safe를 보장합니다.
public class LazyinitializationSynchronized {
private static LazyinitializationSynchronized instance;
private LazyinitializationSynchronized() {
}
public static synchronized LazyinitializationSynchronized getInstance() {
if (instance == null) {
instance = new LazyinitializationSynchronized();
}
return instance;
}
public void printHashCode() {
System.out.println("HashCode of Instance : " + instance.hashCode());
}
}
- synchronized 키워드를 이용한 Initialization 방법은 thread-safe하지만, 인스턴스가 생성되었든 안되었든 무조건 동기화 블록을 거치게 되어있다. 또한 synchronized 키워드를 사용하면 성능이 떨어지게 되는 단점이 있습니다.
3-2. Lazy Initialization with DCL(Double Checking Locking)
public class LazyInitializationDoubleCheck {
private static LazyInitializationDoubleCheck instance;
// private volatile static LazyInitializationDoubleCheck instance;
public LazyInitializationDoubleCheck() {
}
public static LazyInitializationDoubleCheck getInstance() {
if (instance == null) {
synchronized (LazyInitializationDoubleCheck.class) {
if (instance == null) {
instance = new LazyInitializationDoubleCheck();
}
}
}
return instance;
}
public void printHashCode() {
System.out.println("HashCode of Instance : " + instance.hashCode());
}
}
- synchronized 키워드로 인해 성능 저하 문제가 발생하는데 이를 해결하기 위하여 getInstance() 메소드에 lock을 거는 것이 아니라 instance가 null일 때 한해서만 synchronized 키워드가 동작하도록 설계한 것이다.
3-3. Lazy Initialization with Enum
- Enum 타입을 이용한 싱글톤 패턴은 Enum 타입의 특징을 이용해서 구현하는 것입니다.
- 인스턴스 생성을 보장하며 사용이 간편합니다. 또한 직렬화가 자동으로 처리되고 여러 객체가 생길 일이 없어 멀티 쓰레드환경에서도 안전합니다.
public enum SingletonWithEnum {
INSTANCE;
public SingletonWithEnum getInstance() {
return INSTANCE;
}
public void printHashCode() {
System.out.println("HashCode of Instance : " + INSTANCE.hashCode());
}
}
- 하지만, Enum 내부에 구현하는 메소드에 대해서도 모두 thread-safe하지는 않습니다.
- Enum을 이용하여 구현한 싱글톤 패턴 관련한 사이트
(읽어보시면 Enum의 특징과 Enum일 이용한 Singleton Pattern의 특징을 보다 잘 이해할 수 있으실 겁니다.)
->https://shunix.com/efficient-thread-safe-singleton-in-java/
-> https://www.geeksforgeeks.org/advantages-and-disadvantages-of-using-enum-as-singleton-in-java/
3-4. Lazy Initialization / LazyHolder / Bill Pugh Singleton
- Bill pugh가 기존의 Java Singleton 패턴의 문제를 해결하고자 제안한 새로운 Singleton 패턴입니다.
public class LazyInitializationHolder {
public LazyInitializationHolder() {
}
private static class Singleton {
private static final LazyInitializationHolder instance = new LazyInitializationHolder();
}
public static LazyInitializationHolder getInstance() {
return Singleton.instance;
}
public void printHashCode() {
System.out.println("HashCode of Instance : " + Singleton.instance.hashCode());
}
}
- 내부 클래스인 Singleton 클래스의 변수가 없기 때문에 Singleton 내부 클래스가 호출되더라도 인스턴스가 구현되지 않으며 getInstance() 메소드를 호출될 때 JVM 메모리에 로드되고 인스턴스를 생성하게 됩니다.
- 동적바인딩의 특징을 이용하여 thread-safe하여 성능이 뛰어납니다. Singleton 내부 인스턴스는 클래스 로딩시 한번만 호출된다는 점을 이용한 것이며, final 키워드를 사용하여 다시 값이 할당되지 않도록 합니다.
- LazyHolder의 구조는 inner staic class로 넣어서 인스턴스를 이용하도록 합니다. 이와 같은 방식은 synchronized 키워드를 사용하지 않아도 클래스 로딩 시점에 Lock을 사용하기 때문에 멀티 쓰레드 환경에서 안정성을, 성능저하 문제까지 해결할 수 있습니다.
| Conclusion
Singleton Pattern이란?
- 하나의 인스턴스를 메모리에 등록해서 여러 쓰레드가 해당 인스턴스를 공유하여 사용할 수 있도록 하는 것입니다.
왜 Singleton Pattern을 사용하는가?
- 메모리 낭비를 방지
- 데이터 공유를 해야하는 경우 효율성 고려
Singleton Pattern 구현 방법
- Eager Initialization
- Static Block Initialization
- Lazy Initialization
- Lazy Initialization with Synchronized
- Lazy Initialization with DCL(Double Checking Locking)
- Lazy Initialization with Enum
- Lazy Initialization with Holder (Bill Pugh)
Singleton Pattern을 공부하며
- 싱글톤 패턴을 공부하며 바인딩에 관해서, OCP에 대해서, Enum이 왜 멀티 쓰레드 환경에서 안전한지에 대해서, 클래스 로딩 단계에 대해서 부가적인 공부를 많이 할 수 있었습니다. 그리고 공부하면 할수록 부족한 것에 대해서 많이 알게 된 시간이었습니다. 이 포스팅을 시작으로 디자인 패턴에 대해서, 그리고 공부하며 부가적으로 공부하게 된 부분에 대해서도 하나씩 포스팅 해보도록 하겠습니다.
(참고)
'Develop > JAVA' 카테고리의 다른 글
Java 디자인 패턴 네번째 이야기 - 팩토리 메소드 패턴(Factory Method Pattern) (0) | 2021.08.16 |
---|---|
Java 디자인 패턴 세번째 이야기 - 빌더 패턴(Builder Pattern) (0) | 2021.08.13 |
Java 디자인 패턴 두번째 이야기 - 프록시 패턴(Proxy Pattern) (0) | 2021.08.10 |
Object 클래스의대표적인 메소드(equals(),hashcode(),toString()) (0) | 2021.08.04 |
String과 StringBuffer 그리고 StringBuilder의 차이 (0) | 2021.08.03 |