클래스가 오직 하나의 인스턴스만을 갖도록 보장하고,
해당 인스턴스에 대한 전역적 접근을 가능하게 하는 디자인 패턴
즉, 클래스가 한 번만 인스턴스화 될 수 있도록 하고
이후에 인스턴스 요청 시 언제나 같은 객체(메모리 주소가 같은)를 반환
💡 Java에서 싱글톤 패턴 구현?
클래스 생성자 메서드에private키워드 붙이기 !!
등등 중복/일관성 문제를 방지하는 등의 장점이 있음
그냥 일반 클래스에서 인스턴스를 한 번만 만들면 안되나?
그렇게 생각할 수 있지만, 혹시 모를 실수나 변수를 위해 단 한 번의 생성만 가능하도록
구조적인 제한을 두는 것이 싱글톤 패턴이다.
아래의 예시는 싱글톤 패턴을 구현하는 방법 중 하나인 Lazy initialization이다.
외부에서 getInstance() 메서드를 호출해야 초기화를 진행하기 때문에 붙은 이름인데,
덕분에 당장 객체를 사용하지 않을 경우 메모리 공간을 절약할 수 있다.
수업시간에 주로 다뤘던 싱글톤 패턴이므로
public class Singleton {
// 1. 단 하나의 인스턴스를 보관할 private static 필드
private static Singleton instance;
// 2. 외부에서 인스턴스를 생성하지 못하도록 private 생성자
private Singleton() { }
// 3. 단일 인스턴스를 제공하기 위한 public static 메서드
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
마찬가지로 아래는 Lazy Initialization 예시이다.
public class JdbcTemplate {
private String url;
private String user;
private String password;
private static JdbcTemplate instance; // 1. private static 필드 선언
// 2. 생성자를 private으로 지정
private JdbcTemplate(String url, String user, String password) {
this.url = url;
this.user = user;
this.password = password;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
throw new JdbcInitializeException(e.getMessage(), e);
}
}
// 3. init 메서드로 처음 한 번만 인스턴스 생성
public static JdbcTemplate init(String url, String user, String password) {
if (instance == null) {
instance = new JdbcTemplate(url, user, password);
}
return instance;
}
// 4. 이후 getInstance 메서드로 생성된 인스턴스 반환
public static JdbcTemplate getInstance(){
if (instance == null) {
throw new JdbcInitializeException("JdbcTemplate not initialized, please call init()");
}
return instance;
}
(.....생략.....)
}
DB의 연결 정보(url, user_id, password) 등을 담은 JdbcTemplate 인스턴스를 만들어둠
동일한 JdbcTemplate 인스턴스를 전역적으로 하나만 사용한다.
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
멀티 스레드 문제를 해결할 방법으로 우선 수업시간에 다뤘던 동기화(Synchronized)가 있다.
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
이렇게 메서드 앞에 synchronized 키워드를 넣으면 두 개 이상의 스레드가 하나의 변수에
동시에 접근하지 못하도록 lock을 건다.
하지만 이렇게 매번 메서드에 synchronized를 넣으면 Overhead가 발생할 수 있다.
👆오버헤드란 ?
- wait, 스레드 관리 등에 들어가는 비용
- 대기시간도 들고, 접근 권한 부여 및 회수 등 연산이 들어감
그래서 등장한 Double-Checked Locking 이다.
인스턴스 필드 선언 시에 volatile 키워드를 붙여주면 된다.
다만 명령어에 대한 이해가 필요하고 JVM 버전에 따라 지원하지 않는 경우가 있다고 한다.
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton instance;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckSingleton.class) {
if (instance == null) {
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
JVM 에서는 내부 클래스라도 static으로 선언되면, 바깥 클래스와 별개의 독립적인 클래스로 취급됨.
따라서 그 내부 클래스가 처음 사용될 때 로딩이 일어난다는 점을 이용한 것.
public class LazyHolderSingleton {
private LazyHolderSingleton() {}
private static class SingleInstanceHolder {
private static final LazyHolderSingleton INSTANCE = new LazyHolderSingleton();
}
public static LazyHolderSingleton getInstance() {
return SingleInstanceHolder.INSTANCE;
}
}
즉, getInstance가 호출되어야 싱글톤 객체를 최초로 생성 및 리턴하게 됨.
그 이후에는 이미 로딩이 끝났으므로 두 번째 호출부터는 생성된 인스턴스를 반환한다.
리플렉션과 같은 방법을 이용해 클라이언트가 싱글톤을 깰 수 있다는 것이 단점.
👆리플렉션 이란?
클래스의 메서드, 필드와 같은 구성요소를 조사하고 호출, 수정할 수 있는 매커니즘.
예) Java의java.lang.reflect
😝 응 리플렉션으로 인스턴스 또 만들면 그만이야~
Enum 상수는 처음 단 한 번만 생성하므로, 오직 1개의 인스턴스가 만들어짐.
Java에서 100% 보장하는 JVM에 하나만 존재하는 인스턴스.
단, 다른 클래스를 상속받을 수 없음.
public enum EnumSingleton {
INSTANCE;
// 예시로 필드를 하나 두고
private int value;
// 필요한 메서드
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void doSomething() {
System.out.println("EnumSingleton : " + value);
}
}
public class Main {
public static void main(String[] args) {
EnumSingleton.INSTANCE.setValue(42); // 이 시점에서 생성
EnumSingleton.INSTANCE.doSomething(); // "EnumSingleton : 42"
}
}
싱글톤 패턴은 분명한 Trade-Off 가 존재하는 패턴이므로, 적절히 상황을 고려해서 적용해야 한다.
특히 멀티 스레드 환경이라면 싱글톤 패턴이 정말 필요한지에 대해 신중히 체크하자.