싱글톤 패턴(Singleton Pattern)이란?
어떤 클래스가 최초 한번만 메모리를 할당하고(Static) 그 메모리에 객체를 만들어 사용하는 디자인 패턴을 의미한다.
즉, 인스턴스가 오직 1개만 생성되어야 하는 경우에, 클래스에 하나의 객체만을 생성해 이후에 호출된 곳에서는 생성된 객체를 반환하여 프로그램 전반에서 하나의 인스턴스만을 사용하게 하는 패턴이다.
싱글톤 패턴 생성 규칙
- 생성자를 private로 만든다.
외부 클래스에서 마음대로 인스턴스를 생성할 수 없게하기 위함이다.
- 클래스 내부에 private와 static으로 유일한 인스턴스를 생성한다.
static으로 해당 변수를 인스턴스화 하지 않고 사용할 수 있게 하면서, 접근제어자 private로 외부에서 이 인스턴스에 접근하지 못하게 한다.
- 외부에서 참조할 수 있는 public 메서드를 만든다 ( getInstance() )
private로 선언한 유일한 인스턴스를 외부에서도 사용할 수 있도록 public 메서드를 생성하고 그 유일한 인스턴스를 반환해준다.
이때, 인스턴스를 반환하는 메서드는 반드시 static으로 선언해야한다. getInstance() 메서드는 인스턴스 생성과 상관없이 호출할 수 있어야 하기 때문이다.
public class ExampleClass {
//Instance
private static ExampleClass instance = new ExampleClass();
//private construct
private ExampleClass() {}
public static ExampleClass getInstance() {
return instance;
}
}
위 코드에서는 instance라는 전역 변수를 선언하는데 static을 줌으로써 인스턴스화 하지 않고 사용할 수 있게 하였지만 접근 제한자가 private로 되어 있어 직접적인 접근은 불가능하다.
또한 생성자도 private으로 되어 있어 new를 통한 객체 생성도 불가능하다.
결국 getInstance 메서드를 통해서 해당 인스턴스를 얻을 수 있게 된다.
싱글톤 패턴을 사용하는 이유
https://elfinlas.github.io/2019/09/23/java-singleton/
- 한번의 객체 생성으로 재 사용이 가능하기 때문에 메모리 낭비를 방지할 수 있다.
- 싱글톤으로 생성된 객체는 무조건 한번 생성으로 전역성을 띄기에 다른 객체와 공유가 용이하다.
인스턴스가 1개만 생성되는 특징을 가진 싱클턴 패턴을 이용하면, 하나의 인스턴스를 메모리에 등록해서 여러 스레드가 동시에 해당 인스턴스를 공유하여 사용하게끔 할 수 있으므로, 요청이 많은 곳에서 사용하면 효율을 높일 수 있다.
싱글톤 패턴의 문제점
- Multi-Thread 환경에서 안전하지 않다. 즉, Thread-safe하지 않다.
여러 쓰레드가 공유되고 있는 상황에서 여러개의 인스턴스가 생성될 위험이 있기 때문이다.
- 싱글톤으로 만든 객체의 역할이 복잡한 경우라면, 해당 싱글톤 객체를 사용하는 다른 객체간의 결합도가 높아져서 객체 지향 설계 원칙에 어긋나게 된다.
싱글톤을 구현하는 방법
https://limkydev.tistory.com/67
Eager Initialization(이른 초기화, Thread-safe)
public class Singleton {
// Eager Initialization
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return uniqueInstance;
}
}
싱글톤의 가장 기본적인 방식으로 싱글톤 객체를 미리 생성해놓는 방식이다.
항상 싱글톤 객체가 필요하거나 객체 생성비용이 크게 들어가지 않는 경우에 사용한다.
장점
static으로 생성된 변수에 싱글톤 객체를 선언했기 때문에 클래스 로더에 의해 클래스가 로딩 될 때 싱글톤 객체가 생성된다. 그러므로 Thread-safe하다.
단점
싱글톤객체 사용유무와 관계없이 클래스가 로딩되는 시점에 항상 싱글톤 객체가 생성되고, 메모리를 잡고있기 때문에 비효율적일 수 있다.
Lazy Initialization with synchronized (동기화 블럭, Thread-safe)
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
// Lazy Initailization
public static synchronzied Singleton getInstance() {
if(uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
synchronized 키워드를 이용한 방식으로 Eager initialization(이른 초기화방식)과 다르게 클래스가 로딩되는 시점이 아닌 클래스의 인스턴스가 사용되는 시점에서 싱글톤 인스턴스를 생성하는 방식이다.
사용시점까지 싱글톤 객체 생성을 미루기 때문에 사용하기 전까지 메모리를 점유하지 않는다.
장점
기본 Lazy Initialization방식보다 Thread-safe하다.
단점
synchronized 키워드를 사용할 경우 자바 내부적으로 해당 영역이나 메서드를 lock, unlock 처리하기 때문에 많은 cost가 발생한다. 따라서 많은 thread 들이 getInstance()를 호출하게 되면 프로그램 전반적인 성능저하가 발생한다.
※아래 방식들을 배우고 나서는 위 방식을 사용하면 안된다고 한다.. (굳이 좋은 방식이 있는데 안좋은 방식을 쓸 필요는 없으니!)
Lazy Initialization. Double Checking Locking(DCL, Thread-safe)
public class Singleton {
private volatile static Singleton uniqueInstance;
private Sigleton() {}
// Lazy Initialization. DCL
public Singleton getInstance() {
if(uniqueInstance == null) {
synchronized(Singleton.class) {
if(uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
위의 동기화 블럭 방식을 개선한 방식으로 해당 코드를 설명하자면
첫번째 if문에서 instance 가 null인 경우 synchronized 블럭에 접근하고 한번 더 if문으로 instance의 null 유무를 체크한다. 2번 모두다 instance가 null인 경우에 new를 통해 인스턴스화 시킨다. 그 후에는 instance가 null이 아니기 때문에 synchronized 블럭을 타지 않는다.
이런 식으로 인스턴스가 생성되지 않은 경우에만 동기화 블럭이 실행되게끔 구현하는 방식이다.
위 코드에서 등장하는 volatile키워드는 멀티쓰레딩을 쓰더라도 uniqueInstance 변수가 Sigleton 인스턴스로 초기화 되는 과정이 올바르게 진행되도록 할 수 있게한다.
※volatile 키워드가 필요한 이유 ?
volatile 변수를 사용하고 있지 않는 멀티 스레드 어플리케이션에서는 작업(Task)을 수행하는 동안 성능 향상을 위해 Main Memory 에서 읽은 변수 값을 CPU Cache 에 저장하게 됩니다.
만약에 멀티 스레드 환경에서 스레드가 변수 값을 읽어올 때 각각의 CPU Cache 에 저장된 값이 다르기 때문에 변수 값 불일치 문제가 발생하게 되는데, volatile 키워드가 이런 문제를 해결해줍니다.
즉, volatile 변수는 Main Memory 에 값을 저장하고 읽어오기 때문에(read and write) 변수 값 불일치 문제가 생기지 않습니다.
- 멀티 스레드 환경에서 하나의 스레드는 read and write 하며, 나머지 스레드는 read 만 하는 경우 변수의 최신 값을 보장
- 멀티 스레드 환경에서 여러개의 스레드가 write 하는 상황이라면 동기화 블럭(synchronized) 을 지정해서 원자성(atomic) 을 보장해야 한다.
Lazy Initialization. LazyHolder(게으른 홀더, Thread-safe)
public class Singleton {
private Singleton() {}
/**
* static member class
* 내부클래스에서 static변수를 선언해야하는 경우 static 내부 클래스를 선언해야만 한다.
* static 멤버, 특히 static 메서드에서 사용될 목적으로 선언
*/
private static class InnerInstanceClazz() {
// 클래스 로딩 시점에서 생성
private static final Singleton uniqueInstance = new Singleton();
}
public static Singleton getInstance() {
return InnerInstanceClazz.instance;
}
}
가장 많이 사용되는 싱글톤 구현방식으로 volatile 이나 synchronized 키워드 없이도 동시성 문제를 해결하기 때문에 성능이 뛰어나다.
싱글톤 클래스에는 InnerInstanceClazz 클래스의 변수가 없기 때문에, static 멤버 클래스더라도 클래스 로더가 초기화 과정을 진행할때 InnerInstanceClazz 메서드를 초기화 하지 않고, getInstance() 메서드를 호출할 때 싱글톤 객체를 생성하여 리턴한다.
InnerInstanceClazz 내부 인스턴스는 static 이기 때문에 클래스 로딩 시점에 한번만 호출된다는 점을 이용한것이며, final을 써서 다시 값이 할당되지 않도록 한다.
Lazy Initailization. Enum(열거 상수 클래스, Thread-safe)
public enum Singleton {
INSTANCE;
}
Enum이 가지는 특징을 이용해서 싱글톤 패턴을 심플하게 구현한 방식
장점
아주 복잡한 직렬화 상황이나, 리플렉션 공격에도 제 2의 인스턴스가 생성되는 것을 막아준다.
단점
만드려는 싱글톤이 Enum외의 클래스를 상속해야하는 경우에는 사용할 수 없다.
※참고
'Study > JAVA' 카테고리의 다른 글
[JAVA] 디자인패턴 - 빌더 패턴(Builder pattern) (0) | 2022.01.27 |
---|---|
[JAVA] 날짜와 시간 API 정리 (0) | 2022.01.24 |
[JAVA] String, StringBuffer, StringBuilder 차이 및 장단점 (0) | 2021.11.27 |
[Java] 람다식 (Lambda Expression) (0) | 2021.11.26 |
[Java] Stream API (0) | 2021.11.24 |