|
| 1 | +# 싱글톤 패턴을 구현하는 여러가지 방법 |
| 2 | + |
| 3 | +Created by: 이재현 |
| 4 | +Created time: 2023년 7월 12일 오전 1:07 |
| 5 | +Tags: Design Pattern, JAVA, Spring, Thread |
| 6 | + |
| 7 | +- **대략적인 서술 순서** |
| 8 | + |
| 9 | +`**싱글톤 패턴의 정의` - `싱글톤 패턴을 구현하는 여러 방법` - `싱글톤 패턴의 문제점`** |
| 10 | + |
| 11 | +## 💡싱글톤 패턴이란? |
| 12 | + |
| 13 | +### `정의` |
| 14 | + |
| 15 | +- 어떤 클래스에 대한 인스턴스가 오직 하나임을 보장시키고, 필요 시 해당 인스턴스에 접근할 수 있는 전역적인 메서드를 고려하는 디자인 (생성) 패턴 |
| 16 | + |
| 17 | +### `왜?` |
| 18 | + |
| 19 | +- 프로그램 내에서 하나로 공유해야 하는 객체가 있을 때, 이를 싱글톤으로 구현하면 해당 객체를 여러 개 생성할 필요 없이 공유하며 효율적으로 이용할 수 있다. |
| 20 | + - **메모리** |
| 21 | + : 하나의 인스턴스만 메모리를 할당하면 된다. |
| 22 | + - **속도** |
| 23 | + : 새로운 객체를 인스턴스화하는 과정을 거치지 않으므로 속도가 더 빨라진다. |
| 24 | + - **********************데이터 공유********************** |
| 25 | + : 여러 클래스에서 데이터를 공유하며 사용할 수 있다. |
| 26 | + - **하지만 이로부터 발생하는 문제점이 있다!** |
| 27 | + |
| 28 | + Concurrency 문제가 발생할 수 있다. 기본적으로 프로그램을 설계할 때에는 멀티 쓰레드 환경을 고려해야 할 텐데, 언제나 다른 객체로부터 접근이 가능하다면 의도치않은 동작을 야기할 수 있으므로 이를 추가적으로 고려해주어야 한다. |
| 29 | + |
| 30 | + |
| 31 | +### `어떻게?` |
| 32 | + |
| 33 | +- 대략 아래의 6 가지 패턴이 대표적이라고 할 수 있으며, 가장 널리 쓰이는 방법은 `Holder` 방법이고, effective java 의 저자는 `**enum singleton**` ****방법이 best practice 라고 저서에서 언급하였다. |
| 34 | +- 간단한 구현 예제부터 살펴보자. |
| 35 | + |
| 36 | +--- |
| 37 | + |
| 38 | +## (1) `Eager Initialization` |
| 39 | + |
| 40 | +- 클래스 로딩 단계에서 싱글톤 클래스의 인스턴스를 생성하도록 한다. |
| 41 | +- 코드를 바로 보면 더 이해가 쉽다! **아마도** |
| 42 | + |
| 43 | + ```java |
| 44 | + public class Singleton { |
| 45 | + |
| 46 | + private static final Singleton instance = new Singleton(); // A |
| 47 | + |
| 48 | + // private constructor to avoid client applications to use constructor |
| 49 | + private Singleton(){} // B |
| 50 | + |
| 51 | + public static Singleton getInstance(){ // C |
| 52 | + return instance; |
| 53 | + } |
| 54 | + } |
| 55 | + ``` |
| 56 | + |
| 57 | + - A 라인에서 바로 Singleton 인스턴스를 생성한다. (클래스 로딩 단계에서 생성) |
| 58 | + - 그리고 생성자를 private 로 막아놓는다. (B 라인) |
| 59 | + - 외부에서 해당 인스턴스에 접근하는 메소드를 구현해놓는다. (C 라인) |
| 60 | +- **클래스를 로딩할 때, 인스턴스화를 미리 해버리는 (Eager Initialization) 방법이다.** |
| 61 | +- 그런데 **클래스 로딩 단계에서 인스턴스화하는 것**으로부터 발생하는 문제가 있다. |
| 62 | +- **리소스 제한 문제** |
| 63 | + |
| 64 | + 리소스 제한 문제라는 명칭은 제가 붙인거라.. 공식적이진 않지만 여튼 리소스가 제한되어 있어 서 뒤따르는 문제가 있다. |
| 65 | + 클래스 로딩단계에서 객체를 인스턴스화해버리기 때문에 |
| 66 | + 1. **그 클래스를 이후에 실제로 사용을 하든지 말든지 그냥 메모리를 할당해버린다.** |
| 67 | + |
| 68 | + - 위 예제처럼 간단한 클래스라면 큰 문제가 되지 않겠으나, File System, DB Connection 과 같이 리소르를 많이 사용하는 클래스를 Eager Initialization 으로 구현하면 효율적이지 않다. |
| 69 | + 1. **클래스 로딩 단계에서 발생할 수 있는 예외를 처리할 수 없다.** |
| 70 | + - 예외 처리 관련된 부분은 생각해보면 쉽게 개선할 수 있다. 이는 `**Static block Initialization**` 섹션에서 소개하도록 하겠다. |
| 71 | + |
| 72 | +## (2) `Static Block Initialization` |
| 73 | + |
| 74 | +- Eager Initialization 에서 발생하는 대표적인 두 가지 문제점 중 하나인 예외 처리를 개선한 것이다. |
| 75 | + |
| 76 | +```java |
| 77 | +public class Singleton { |
| 78 | + |
| 79 | + private static Singleton instance; |
| 80 | + |
| 81 | + private Singleton(){} |
| 82 | + |
| 83 | + //static block initialization for exception handling |
| 84 | + static{ |
| 85 | + try{ |
| 86 | + instance = new Singleton(); |
| 87 | + }catch(Exception e){ |
| 88 | + throw new RuntimeException("Exception occured in creating singleton instance"); |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + public static Singleton getInstance(){ |
| 93 | + return instance; |
| 94 | + } |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +- 위와 같이 Singleton 객체 생성단계에서 발생할 수 있는 예외를 처리할 수 있다. |
| 99 | + - 하지만 여전이 리소스 관련 문제에 대해 명쾌하게 해답을 제시하진 못했다. |
| 100 | + - 어떻게 해결할 수 있을까? |
| 101 | + - **************************************************************************************************************************클래스를 실제로 사용하지 않으면 굳이 인스턴스화하지 않고, 사용하게 되는 최초 시점에만 인스턴스화하도록 하면 될 것이다.************************************************************************************************************************** |
| 102 | + - 기억을 되새겨보면 분명히 김영한님이 알려주셨을..것 🫠 |
| 103 | + |
| 104 | +## (3) `Lazy Initialization` |
| 105 | + |
| 106 | +- 클래스 로딩 단계에서 초기화(Eager Initialization)하는 것이 아닌, 해당 객체를 사용하려고 하는 시점에 초기화(Lazy Initialization)하는 방법이다. |
| 107 | + |
| 108 | +```java |
| 109 | +public class Singleton { |
| 110 | + |
| 111 | + private static Singleton instance; // A 라인 |
| 112 | + |
| 113 | + private Singleton(){} // B 라인 |
| 114 | + |
| 115 | + public static Singleton getInstance(){ // C |
| 116 | + if(instance == null){ // C-1 |
| 117 | + instance = new Singleton(); // C-2 |
| 118 | + } |
| 119 | + return instance; // C-3 |
| 120 | + } |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +- 클래스 로딩 단계에서 인스턴스화를 시키지 않는다. (A 라인) |
| 125 | +- Eager Initialization 과 마찬가지로 생성자를 private 로 지정하여 보호한다. (B 라인) |
| 126 | +- C 라인을 보면, `**getInstance()**` 메소드가 호출되는 시점에, 객체가 이미 할당되어 있으면 할당된 객체를 반환하고, 객체가 할당되어 있지 않으면(최초 호출) 그제서야 인스턴스화를 해서 반환한다. |
| 127 | + - 이렇게 하면 이전 방법들에서 발생한 리소스 문제에 대해 어느정도 해결할 수 있다..! |
| 128 | + - 하지만.. 또 다른 문제점이 있다. |
| 129 | +- **Lazy Initialization 의 문제점** |
| 130 | + - 위 방법은 single-thread 환경이 보장되면 좋은 방법이다. |
| 131 | + - 무슨말이냐면 실제 서비스할 때, 위 객체에 접근하는 쓰레드가 하나가 아니라면 문제가 발생할 수 있다는 것이다. |
| 132 | + - 쓰레드가 여러개 존재하면, 각자 프로세스를 실행시킬 수 있겠다. |
| 133 | + - 하지만 그 순서는 OS 에 의해 스케쥴링되며, 고려할 상황이 많기 때문에 예측하기 쉽지 않다. |
| 134 | + - 예를 들어 A 쓰레드가 먼저 getInstance 메서드를 호출했다고 생각해보자. |
| 135 | + - A 가 C-1 까지 실행해서, 현재 Instance 가 할당되지 않았음을 확인했다. |
| 136 | + - 그럼 A 는 C-2 를 수행하려고 마음을 먹고있는데.... |
| 137 | + - **이때 갑자기 OS 에 의해 B 쓰레드가 실행되기 시작했다.** |
| 138 | + - B 는 C-1, C-2 를 거쳐 Instance 를 생성하고 작업을 모두 끝마친다. |
| 139 | + - 이제 OS 가 A 쓰레드에게 "하려고 하던 거 해" 라고 지시할 수 있으며, |
| 140 | + - A 쓰레드는 원래 수행하려던 C-2 를 실행한다. |
| 141 | + - 여기서 문제가 발생했다. |
| 142 | + - 싱글톤 객체를 두 개 이상 생성하게 되었다 ⚡ |
| 143 | + - 위와 같이 Lazy Initialization 방법에서는 Thread - safe 를 고려하지 않았기 때문에 이를 해결해주어야 한다. |
| 144 | + - 어떻게 할 수 있을까? |
| 145 | + - 적어도 저 getInstance 메소드에 대해서는, 반드시 하나의 쓰레드만 진입가능하게 해주면 되지 않을까? |
| 146 | + - 공중 전화 박스에는 한 사람만 들어갈 수 있도록 하자는 것이다. |
| 147 | + - 이를 자바로 구현하기 위해서는 해당 메소드에 한 키워드만 얹어주면 된다. |
| 148 | + |
| 149 | +## (4) `Lazy Initialization with Synchronization` |
| 150 | + |
| 151 | +```java |
| 152 | +public class Singleton { |
| 153 | + |
| 154 | + private static Singleton instance; |
| 155 | + |
| 156 | + private Singleton(){} |
| 157 | + //////////////////////////////// |
| 158 | + public static synchronized Singleton getInstance(){ |
| 159 | + if(instance == null){ |
| 160 | + instance = new Singleton(); |
| 161 | + } |
| 162 | + return instance; |
| 163 | + } |
| 164 | + //////////////////////////////// |
| 165 | +while(!) |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +- `getInsatnce()` 메서드에 대해 한 쓰레드만 진입 가능하도록 보장한다. |
| 170 | + - 자바에서는 `synchronized` 라는 키워드를 메소드 이름 앞에 붙여주어서 구현할 수 있다. |
| 171 | +- `synchronized` 를 붙여주어야 하는 메소드를 어떻게 판단할 수 있을까? |
| 172 | + - 대략적으로만 설명하면 공유 자원에 여러 쓰레드가 접근하여 문제가 발생할 수 있는 메소드에 대해서 `synchronized` 를 고려하면 된다. |
| 173 | + - **학부 운영체제 전공과목에서 이러한 코드라인을 Critical Section (임계 영역) 이라고 한다.** |
| 174 | +- **오 근데 문제가 또 있다.** |
| 175 | + |
| 176 | + `synchronized` 키워드 자체에 대한 비용이 큰 편이다. 이 부분을 이해하려면 운영체제 혹은 데이터베이스 전공 과목을 수강하면 좋을 것 같긴 하지만...! 여튼 간단히 설명해보겠다. |
| 177 | + 우리가 `synchronized` 를 사용해서 얻고자 하는 이점은, Critical Section 에 진입가능한 쓰레드를 하나로 보장하기 위함이다. |
| 178 | + |
| 179 | + - Critical Section 에 진입가능한 쓰레드를 하나로 보장하기 위해서 `Locking` 을 사용한다. |
| 180 | + - 여러 가지 Lock 기법이 있지만, 기본적으로 Lock 을 사용함에따라 발생하는 overhead 가 존재한다. 이를 lock overhead 라고 한다. |
| 181 | + - 가령, spin-lock 이라고 해서 특정 쓰레드가 Critical section 에 진입할 수 있는지 여부를 무한루프를 돌면서 계속 확인하는 방법이 있다. |
| 182 | + - 여튼 `synchronized` 키워드를 남발하기에는 lock overhead 에 의해 프로그램의 성능이 저하될 수 있다. |
| 183 | + - 해당 싱글톤 객체의 호출 빈도에 따라서 이 overhead 는 점점 증가할 것이다. |
| 184 | + - 그러면 어떻게 이를 해결할 수 있을까? |
| 185 | + |
| 186 | +## (5) `Lazy initialization with Double checked Locking` |
| 187 | + |
| 188 | +- 운영체제 떄 배웠던 내용을 조금 더 생각해보자..! |
| 189 | + - critical section 을 lock 으로 감싸서 하나의 쓰레드만 진입 가능함(mutual exclusion)을 구현할 수 있음은 당연하다. |
| 190 | + - 그런데 critical section 에 완전 fit 하게 감싸는 게 아니라, 적당히 마진을 두고 감싸면 어떨까? |
| 191 | + - 혹은 현재 critical section 에 대해서 조금 여유롭게 lock 으로 감싸져있다면, 이를 반드시 필요한 만큼만 fit 하게 감싸면 overhead 를 더 최적화할 수 있지 않을까? |
| 192 | +- 이제 다시 아래의 메서드를 보자. |
| 193 | +- `synchronized` 키워드를 사용함으로써 메서드 전체를 critical section 으로 지정했다. |
| 194 | + |
| 195 | +```java |
| 196 | + public static synchronized Singleton getInstance(){ |
| 197 | + if(instance == null){ |
| 198 | + instance = new Singleton(); |
| 199 | + } |
| 200 | + return instance; |
| 201 | + } |
| 202 | +``` |
| 203 | + |
| 204 | +- 고민해보자. |
| 205 | +- 고민해봤다면, |
| 206 | + |
| 207 | + 메소드 단위로 lock 을 거는게 아니라 instance 가 실제로 null 인 경우에만 lock 이 동작하도록 하면 된다. |
| 208 | + |
| 209 | + ```java |
| 210 | + public static Singleton getInstance(){ |
| 211 | + if(instance == null){ |
| 212 | + synchronized (Singleton.class) { |
| 213 | + if(instance == null){ |
| 214 | + instance = new Singleton(); |
| 215 | + } |
| 216 | + } |
| 217 | + } |
| 218 | + return instance; |
| 219 | + } |
| 220 | + ``` |
| 221 | + |
| 222 | + |
| 223 | +## (6) `Holder` |
| 224 | + |
| 225 | +- 현재 가장 널리 사용되는 방법이라고 한다. |
| 226 | + |
| 227 | + 코드를 바로 보자! |
| 228 | + |
| 229 | + |
| 230 | +```java |
| 231 | +public class Singleton { |
| 232 | + |
| 233 | + private Singleton(){} |
| 234 | + |
| 235 | + private static class SingletonHolder{ |
| 236 | + private static final Singleton INSTANCE = new Singleton(); |
| 237 | + } |
| 238 | + |
| 239 | + public static Singleton getInstance(){ |
| 240 | + return SingletonHelper.INSTANCE; |
| 241 | + } |
| 242 | +} |
| 243 | +``` |
| 244 | + |
| 245 | +- Singleton 클래스 안에 Inner class 로 SingletonHolder 을 두었다. |
| 246 | + - 이로써 Singleton 객체는 SingletonHolder 이라는 싱글톤 인스턴스를 가지게 된다. |
| 247 | + - 이와 같이 inner class 로 선언하였기 떄문에, 클래스 로드 단계가 아닌 실제로 객체에 접근하려는 타이밍에 SingletonHolder 가 인스턴스화 된다. |
| 248 | +- Singleton class 의 getInstance() 를 호출하면, SingletonHolder 클래스 내의 Instance 를 반환하도록 하여 Singleton 을 구현하였다. |
| 249 | +- 위와 같이 구현하면 `synchronized` 키워드를 사용하지 않았기 때문에 `Lazy Initialization with DCL` 에서의 퍼포먼스 문제가 해결된다...! |
| 250 | +- 구현도 굉장히 쉽다. |
| 251 | +- 그래서 이 방법이 현재 가장 널리 쓰인다고 한다. |
| 252 | + |
| 253 | +--- |
| 254 | + |
| 255 | +하지만.. |
| 256 | + |
| 257 | +위에서 언급한 6 가지 방법은 JAVA 에서 제공하는 `Reflection` 이나 직렬화-역직렬화 도중에 파괴될 수 있다고 한다. |
| 258 | + |
| 259 | +자세하게 살펴보면 좋겠지만, 이번주는 유독 시간이 없었어서... 이부분은 아직 공부하지 못했다. |
| 260 | + |
| 261 | +[자바 Reflection이란?](https://medium.com/msolo021015/자바-reflection이란-ee71caf7eec5) |
| 262 | + |
| 263 | +- Reflection 을 고려하더라도 여전히 안전하게 싱글톤을 구현하기 위해 enum 을 사용한 패턴이 등장한다. |
| 264 | + |
| 265 | +## (7) `Enum Singleton` |
| 266 | + |
| 267 | +```java |
| 268 | +public enum Singleton { |
| 269 | + INSTANCE; |
| 270 | + public static void doSomething(){ |
| 271 | + //do something |
| 272 | + } |
| 273 | +} |
| 274 | +``` |
| 275 | + |
| 276 | +- 상당히.. 간단한데 |
| 277 | + - 이렇게 하면 Reflection 이나 직렬화-역직렬화 과정에서 의도치 않게 하나보다 많은 인스턴스가 생성되는 것을 방지할 수 있다고 한다. (이부분은 추가 공부가 필요할 것 같다... 더 공부해서 정리할 것!) |
| 278 | + |
| 279 | +> **a single-element `enum` type is the best way to implement a singleton. - Joshua Bloch** |
| 280 | +> |
| 281 | +> - 근데 왜 best 인지 아직 잘 모르겠긴하다. |
| 282 | +> - 디자인 패턴에 대해서 얕게 공부해서 그런듯... 한번쯤 깊게 다뤄봐야할텐데 싶다! |
| 283 | + |
| 284 | +--- |
| 285 | + |
| 286 | +### 싱글톤 패턴 구현 방법론의 대략적인 공통점 |
| 287 | + |
| 288 | +- |
| 289 | + 1. **private 생성자만을 정의해 외부 클래스로부터 인스턴스 생성을 차단합니다.** |
| 290 | + 2. **싱글톤을 구현하고자 하는 클래스 내부에 멤버 변수로써 private static 객체 변수를 만듭니다.** |
| 291 | + 3. **public static 메소드를 통해 외부에서 싱글톤 인스턴스에 접근할 수 있도록 접점을 제공합니다.** |
| 292 | + |
| 293 | +--- |
| 294 | + |
| 295 | +## 다시 원론으로 돌아와서, |
| 296 | + |
| 297 | +### ☔ 싱글톤 패턴의 문제점 |
| 298 | + |
| 299 | +- 클라이언트는 변화하기 쉬운 구체적인 코드(getInstance)에 의존하므로 DIP 원칙을 위반한다. |
| 300 | + - 마찬가지의 이유로 OCP 원칙을 위반할 가능성이 높다. |
| 301 | +- 테스트하기 까다롭다. |
| 302 | +- 유연성이 많이 떨어진다. |
| 303 | + |
| 304 | +위와 같은 이유로 싱글톤 패턴을 안티패턴이라고 하기도 한다. |
| 305 | + |
| 306 | +### 🍀 스프링에서는? |
| 307 | + |
| 308 | +- 위와 같은 싱글톤 패턴의 문제점들을 해결해주면서, 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리해준다..! |
| 309 | + - 싱글톤 패턴의 모든 단점을 해결하면서 싱글톤으로 관리해주기 때문에, 개발자는 싱글톤을 구현하기 위한 추가적인 코드 작성을 할 필요 없이 핵심 비지니스 로직에 집중할 수 있게 된다. |
| 310 | + - 이와 같은 이유로, 김영한님이 `**스프링은 개발자가 객체지향적인 프로그램을 잘 만들 수 있도록 지원하는 툴`** 이라고 기억하라고 하신 것 같다! |
| 311 | + |
| 312 | +### ⭐ 싱글톤 패턴과 Statelessness (무상태속성) |
| 313 | + |
| 314 | +- 여러 객체에서 공유하는 객체이므로 당연히 상태를 유지하지 않도록 하는 것이 좋겠죠?! |
| 315 | + |
| 316 | +## 한 문단으로 정리하자면, |
| 317 | + |
| 318 | +<aside> |
| 319 | +📌 **싱글톤 패턴을 구현함으로써 리소스 낭비, 프로그램 퍼포먼스, 데이터 공유 등에서 이점을 얻을 수 있다. 구현하는 방법은 여러 가지가 있지만 대표적으로 Holder 패턴이 가장 널리 쓰이며, 이펙티브 자바의 저자는 Enum 패턴이 가장 좋다고 언급하였다. 하지만 싱글톤 패턴의 고질적인 문제가 여럿 존재하는데, 스프링 컨테이너에서는 이를 모두 해결해준다.** |
| 320 | + |
| 321 | +</aside> |
| 322 | + |
| 323 | +-**끄읕-** |
| 324 | + |
| 325 | +--- |
| 326 | + |
| 327 | +## Reference |
| 328 | + |
| 329 | +[SOLID 원칙 5 - DIP: 의존성 역전 원칙 (Dependency Inversion)](https://dreamcoding.tistory.com/69) |
| 330 | + |
| 331 | +[](https://www.drdobbs.com/jvm/creating-and-destroying-java-objects-par/208403883?pgno=3) |
| 332 | + |
| 333 | +[[생성 패턴] 싱글톤(Singleton) 패턴을 구현하는 6가지 방법](https://readystory.tistory.com/116) |
| 334 | + |
| 335 | +### 더 공부하고 생각해봐야 할 내용 |
| 336 | + |
| 337 | +- **JAVA Reflection ?** |
| 338 | +- **Reflection, 직렬화-역직렬화 과정에서 인스턴스가 여러 개 생성되어, 의도한 싱글톤 패턴이 깨질 수 있는 이유** |
| 339 | + - **조슈아 블로크씨는 enum pattern 이 왜 best practice 라고 생각하신..걸까?** |
0 commit comments