개발자로 후회없는 삶 살기
디자인 패턴 PART.싱글톤 패턴 본문
서론
※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.
https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/dashboard
본론
- 싱글톤 패턴
오직 한 개의 인스턴스만 만들어서 글로벌하게 접근하도록하는 패턴입니다.
public class App {
public static void main(String[] args) {
Settings settings1 = new Settings();
Settings settings2 = new Settings();
System.out.println(settings1 != settings2);
}
}
Settings를 new로 만들면 매우 많이 만들 수 있고 각 객체들이 서로 같지 않습니다. 따라서 싱글톤 패턴을 사용하려면 절대로 new를 사용하면 안되며 private 생성자를 만들어서 클래스 밖에서 new를 사용하지 못하게 해야합니다.
public class Settings {
private static Settings instance;
private Settings() {
}
public static Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
}
이렇게 하면 밖에서는 이 객체를 생성할 수 없고 안에서만 만들 수 있습니다. 이제 이 객체를 글로벌하게 접근할 수 있도록 하는 방법을 제공해야 합니다.
instance가 null일 때만 새롭게 만들고 이미 만들어져 있다면 만들어진 instance를 리턴하게 한다면 매번 같은 객체가 반환됩니다. 이 방법이 가장 간단하게 private 생성자와 static한 메서드로 싱글톤 패턴을 구현하는 방법입니다.
하지만, 이 방법은 멀티 쓰레드에서 안전할까요? ✅
안전하지 않습니다. 따라서 멀티 쓰레드에 안전하게 싱글톤을 만드는 방법을 알아보겠습니다.
전에 배운 방법은 쓰레드 safe하지 않습니다. 최초에 쓰레드 1이 인스턴스를 만들기 전에 쓰레드 2가 if 문을 통과해서 안으로 들어오는 경우를 보면 쓰레드 1, 2 둘 다 new를 실행하게 되어 1, 2의 객체가 달라지게 됩니다.
=> 안전한 방법
1. 동기화
public static synchronized Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
쓰레드 안전하게 만드는 방법은 메서드에 동기화를 하는 방법이 있습니다. 이렇게 하면 이 메서드는 동시에 여러 쓰레드가 들어올 수 없기 때문에 안전함을 보장할 수 있습니다.
다만 이 방법의 단점은 getInstance를 할 때마다 동기화를 처리하는 작업 때문에 성능에 단점이 있습니다. 동기화 매커니즘이 락을 얻고 닫고 하면서 성능에 부화가 생깁니다.
2. 이른 초기화(eager init)
public class Settings {
private static final Settings instance = new Settings();
private Settings() {
}
public static Settings getInstance() {
return instance;
}
}
약간의 성능을 신경쓰고 싶고 꼭 이 객체를 나중에 만들지 않아도 되고 이 객체를 만드는 비용이 그렇게 비싸지 않다면 미리 만들어 둬도 됩니다. 미리 만들고 if 문을 제거하면 되며 이른 초기화라고 합니다. 이 방법은 여러 쓰레드가 들어와도 객체만 반환해주니 쓰레드 세이프합니다.
다만 미리 만드는 것이 단점이 될 수 있습니다. 만약 이 객체를 만드는 과정이 길고 오래 걸리고 메모리를 많이 먹고 만들어 놨는데 쓰지 않으면 어플 로딩에서 굉장히 많은 리소스를 사용하였는데 사용하지 않게 되는 것입니다. 따라서 getInstance를 호출할 때(객체를 사용할 때) 만들면 좋겠는데 이 방법은 그렇지 못한 것입니다.
3. double checked locking
public class Settings {
private static volatile Settings instance;
private Settings() {
}
public static Settings getInstance() {
if (instance == null) {
synchronized (Settings.class) {
if (instance == null) {
instance = new Settings();
}
}
}
return instance;
}
}
사용할 때 만들고 싶은데 syn 블락의 비용이 신경쓰인다면 syn을 getInstance에서 사용하는 것이 아니라 if문으로 먼저 체크를 한 후에 if문 안에서 한번 더 체크를 하며 내부 if문에서 syn을 합니다.
체크를 두 번한다고 해서 더블 checked lock이라고 하며 volatile이라는 키워드를 사용하여 반드시 이 객체를 메인 메모리에 읽고 쓰게 해야합니다.
이것이 쓰레드 세이프하면서 syn의 성능 저하가 없고 객체를 사용할 때 생성하게 되는 지 보겠습니다. ✅
-> 설명
1) 쓰레드 세이프
쓰레드 1이 외부 if로 들어와서 syn 블럭으로 들어가고 객체를 만들지 않은 상태에서 쓰레드 2가 들어오면 syn 때문에 더 이상 진입할 수 없습니다.
2) 객체 사용시 생성
최초에 만든 싱글톤 패턴처럼 getInstance에서 인스턴스를 만들기 때문에 사용할 때 객체를 생성합니다.
3) syn
위 코드는 getInstance를 호출할 때마다 매번 syn가 걸리지는 않습니다. 객체가 이미 있는 경우에는 외부 if 문에서 걸려 동기화 매커니즘이 동작하지 않습니다. 하지만 이 방법은 매우 복잡한 방법이며 자바 1.5부터만 동작합니다.
4. static inner 클래스 사용
public class Settings {
private Settings() {
}
private static class SettingsHolder {
private static final Settings INSTANCE = new Settings();
}
public static Settings getInstance() {
return SettingsHolder.INSTANCE;
}
}
필드 자체가 필요가 없으므로 지우고 private한 익명 내부 객체(홀더)를 하나 만듭니다. 그리고 그 안에 인스턴스를 정의하고 get을 할 때 홀더의 인스턴스를 반환합니다. 이렇게 하면 쓰레드 세이프하고 get 할 때 객체를 생성하게 되어 lazy 로딩도 가능한 코드가 되며 복잡하지 않습니다. 이 방법이 권장하는 방법 중에 중 하나입니다.
=> 싱글톤을 깨트리는 방법
1. 리플랙션
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Settings settings1 = Settings.getInstance();
Constructor<Settings> constructor = Settings.class.getDeclaredConstructor();
constructor.setAccessible(true);
Settings settings2 = constructor.newInstance();
System.out.println(settings1 != settings2);
}
지금까지 멀티 쓰레드 세이프한 싱글톤 패턴을 자바로 구현해 봤습니다. 하지만 사용하는 측에서 잘못사용하면 싱글톤이 깨지게 됩니다. 현재 get으로 밖에 못 만들지만 리플랙션을 사용하면 싱글톤이 깨져서 다른 객체가 생성됩니다.
2. 직렬화 역직렬화
자바에서는 객체를 파일 형태로 저장했다가 다시 읽을 수 있습니다. 이 객체를 파일로 저장하고 다시 로딩할 때 새로운 객체를 만들기 때문에 다른 객체가 됩니다. 이렇게 제대로 만들어도 사용하는 측에서 다르게 사용하면 깨질 수 있습니다.
public enum Settings {
INSTANCE;
Settings() {
}
}
리플랙션이나 직렬화를 고려하여 안전하고 단순하게 구현하는 방법을 알아봅니다. enum을 사용하면 됩니다. enum 같은 경우에는 기본이 싱글톤이고 필드와 메서드, 생성자(기본이 private)까지 만들 수 있습니다.
public class App {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Settings settings1 = Settings.INSTANCE;
Settings settings2 = Settings.INSTANCE;
System.out.println(settings1 == settings2);
}
}
사용하는 측에서는 enum의 상수를 객체로 사용하면 됩니다. 이렇게 하면 리플랙션에 절대 뚫을 수 없는 안전한 코드가 됩니다. 리플랙션 자체에서 enum의 생성자를 newInstance로 만드는 것을 유일하게 막아 놨습니다. 하지만 단점은 미리 만들어진다는 것입니다. 이 단점이 크게 문제가 되지 않는다면 이 방법이 가장 완벽한 방법 중 하나가 될 것입니다. 이렇게 권장하는 방법 2가지를 기억하고 있어야 합니다.
'[백엔드] > [Java | 학습기록]' 카테고리의 다른 글
디자인 패턴 PART.빌더 패턴 (0) | 2023.08.17 |
---|---|
디자인 패턴 PART.추상 팩토리 패턴 (0) | 2023.08.13 |
디자인 패턴 PART.팩토리 메서드 패턴 (0) | 2023.08.13 |
[문법] Stream (0) | 2023.06.18 |
[문법] 자바에서 제공하는 함수형 인터페이스 (0) | 2023.06.13 |