➡️ 싱글톤 패턴 (Singleton Pattern)
객체지향 프로그래밍(OOP)에서 사용되는 디자인 패턴 중 하나로, 하나의 클래스에 대해 프로그램의 전체서 하나의 인스턴스만 가지는 소프트웨어 설계 패턴이다. 여러 개의 인스턴스로 만든다는 것의 의미는 메모리에 여러 번 각기 다른 인스턴스를 할당하는 것을 의미하는데, 그러지 않고 한 번만 메모리를 할당하여 여러 클래스에서 이를 공유하여 사용하는 방식이다. 하나의 인스턴스를 만들어 놓고 다른 여러 모듈에서 사용하기 때문에 인스턴스 생성 비용이 줄어드는 장점이 있는 반면, 의존성이 높아진다는 단점이 있기 때문에 이를 고려하여 해당 패턴을 사용해야 한다. 이러한 생성 비용이 중요한 데이터베이스 연결 모듈에 많이 사용되는 패턴으로 가장 많이 사용되는 디자인 패턴이다. 지금부터 프로그래밍 언어별로 싱글톤 패턴이 사용된 경우에 대해 살펴보자.
✅ 싱글톤 패턴의 장점
- 접근성 :싱글톤은 전역적으로 접근 가능한 유일한 인스턴스를 제공하기 때문에 객체를 생성하고 유지보수하는 데에 필요한 코드 양을 줄일 수 있다. 객체를 생성하는 데 필요한 코드를 모든 클라이언트 클래스에서 구현하지 않아도 된다.예를 들면 다수의 스레드가 공유하는 리소스를 처리하는 경우, 싱글톤 패턴을 사용하여 해다 리소스에 대한 접근을 동기화하고, 쉽게 관리할 수 있다. 이렇게 구현된 싱글톤 패턴은 코드를 단순화하고 복잡도를 줄일 수 있다.
- 메모리 절약 : 여러 인스턴스를 생성할 때 마다 메모리와 CPU 자원을 소비하게 되는데, 싱글톤 패턴은 단 하나의 인스턴스를 생성하므로 자원을 효율적으로 사용할 수 있다.
- 데이터 일관성 보장 : 여러 인스턴스를 생성하게 되면 각각의 인스턴스가 서로 다른 데이터가 가질 수 있지만, 싱글턴 패턴으로 인스턴스를 생성할 시 단 하나의 인스턴스만 생성하기에 인스턴스의 데이터에 대한 일관성을 보장할 수 있다.
✅ 싱글톤 패턴의 단점
CF. 안티 패턴 : 설계 상의 문제로 인해 소프트웨어 시스템이 예상대로 작동하지 않거나 유지보수하기 어렵게 만드는 패턴
- 의존성에 의한 테스트의 어려움 :싱글턴 인스턴스는 어디서든지 접근할 수 있는 객체로, 이 인스턴스가 다른 객체와 의존성이 있는 경우에는 해당 객체를 테스트하기 까다로워진다는 문제가 있다.
- 멀티스레드 환경에서의 인스턴스 중복 생성 문제 : 멀티스레드 환경에서 여러 개의 스레드가 싱글톤 인스턴스에 동시에 접근할 경우 인스턴스가 여러 번 생성될 수 있다. 이 문제를 해결하기 위해서는 동기화 처리를 해주어야하는데, 이 처리 자체가 성능에 영향을 미칠 수 있다.
- 객체지향적이지 못한 특성 : 싱글톤 패턴은 객체 지향 프로그래밍의 원칙(SOLID)들 중에 하나인 DIP(의존성 역전 원칙)를 위배한다. 싱글톤 패턴을 사용하면 싱글톤 클래스와 다른 클래스의 결합도가 높이지기 때문에 싱글톤 클래스의 수정이 일어나면 다른 클래스들에게까지 영향이 가기 때문이다.
✅ 싱글톤 패턴이 안티 패턴이 될 수 있는 경우
- 상속이 필요한 경우 : 싱글톤은 자신만이 객체를 생성할 수 있도록 생성자를 private으로 제한한다. 하지만 상속을 통해 다형성을 적용하기 위해서는 다른 기본생성자가 필요하므로 객체지향의 장점을 적용할 수 없다.
- 테스트의 완전한 격리가 필요한 경우 : 싱글톤 패턴은 전역 상태를 유지하는데 사용될 수 있다. 하지만 전역 상태를 사용하면 다른 모듈과의 의존성이 높아져서 테스트와 코드를 이해하기 어려워진다.
- 멀티스레드 환경에서 안전하지 않은 경우 : 위에서 언급했다시피 싱글톤 패턴은 멀티스레드 환경에서 안전하지 않을 수 있다. 동시에 여러 스레드가 싱글톤 객체에 접근하면 의도하지 않은 결과가 발생할 수 있다. 이러한 문제를 해결하기 위해서는 synchronized 또는 volatile 키워드를 사용하여 동기화 처리를 해주어야 한다.
- 객체의 수명주기가 제어되지 않는 경우 : 싱글톤 패턴은 객체의 수명 주기를 제어하기 어렵다. 객체의 생성과 소멸을 개발자가 직접 제어할 수 없기 때문에, 객체 생성 시점과 소멸 시점에 대한 유연성이 떨어진다.
- 단일 책임 원칙을 위배하는 경우 : 싱글톤 패턴은 하나의 클래스가 여러 가지 책임을 담당하게 되면서 단일 책임 원칙(SRP)을 위배할 수 있다. 싱글톤 클래스가 생성, 관리, 전역 상태 유지 등 여러 가지 책임을 담당하게 되면, 클래스의 크기가 커지고 유지보수가 어려워진다.
- 의존성 주입을 사용하는 경우 : 의존성 주입 패턴은 객체 간의 의존성을 외부에서 주입하여 객체 간의 결합도를 낮추는 패턴이다. 싱글톤 패턴은 의존성 주입 패턴과 호환되지 않는다. 싱글톤 클래스를 사용하는 다른 클래스들은 싱글톤 객체에 직접 의존하게 되어 결합도가 높아진다.
✅ 멀티스레드 환경에서 안전한 싱글톤 만드는 방법
[출처] https://gyoogle.dev/blog/design-pattern/Singleton%20Pattern.html
싱글톤 패턴(Singleton pattern) | 👨🏻💻 Tech Interview
싱글톤 패턴(Singleton pattern) 애플리케이션이 시작될 때, 어떤 클래스가 최초 한 번만 메모리를 할당(static)하고 해당 메모리에 인스턴스를 만들어 사용하는 패턴 즉, 싱글톤 패턴은 '하나'의 인스
gyoogle.dev
1. Lazy Initialization (게으른 초기화)
: private static으로 인스턴스 변수 만든다. 이는 private으로 생성자를 만들어 외부에서의 생성을 막고, synchronized 동기화를 활용해 스레드를 안전하게 하기 위함이다. 단, synchronized는 큰 성능저하를 발생시키므로 권장하는 방법은 아니다.
public class ThreadSafe_Lazy_Initialization{
private static ThreadSafe_Lazy_Initialization instance;
private ThreadSafe_Lazy_Initialization(){}
public static synchronized ThreadSafe_Lazy_Initialization getInstance(){
if(instance == null){
instance = new ThreadSafe_Lazy_Initialization();
}
return instance;
}
}
2. Lazy Initialization와 Double-checked Locking
: 앞선 방법의 성능 저하 문제를 완화시킨 방법으로, 조건문으로 인스턴스의 존재 여부를 확인한 다음 두번째 조건문에서 synchronized를 통해 동기화를 시켜 인스턴스를 생성하는 방법이다. 스레드를 안전하게 만들면서, 처음 생성 이후에는 synchronized를 실행하지 않기 때문에 성능저하 완화가 가능하다.
public class ThreadSafe_Lazy_Initialization{
private volatile static ThreadSafe_Lazy_Initialization instance;
private ThreadSafe_Lazy_Initialization(){}
public static ThreadSafe_Lazy_Initialization getInstance(){
if(instance == null) {
synchronized (ThreadSafe_Lazy_Initialization.class){
if(instance == null){
instance = new ThreadSafe_Lazy_Initialization();
}
}
}
return instance;
}
}
3. Initialization on demand holder idiom
: holder에 의한 초기화를 하는 방법으로, 클래스 안에 클래스(holder)를 두어 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한다. 두 번째 방법처럼 동기화를 사용하지 않는 방법을 안하는 이유는, 개발자가 직접 동기화 문제에 대한 코드를 작성하면서 회피하려고 하면 프로그램 구조가 그만큼 복잡해지고 비용 문제가 발생할 수 있음. 또한 코드 자체가 정확하지 못할 때도 많다고 한다. 이 때문에 JVM의 클래스 초기화 과정에서 보장되는 원자적 특성을 이용해 싱글톤의 초기화 문제에 대한 책임을 JVM에게 떠넘기는 걸 활용한다. 클래스 안에 선언한 클래스인 holder에서 선언된 인스턴스는 static이기 때문에 클래스 로딩시점에서 한번만 호출된다. 또한 final을 사용해서 다시 값이 할당되지 않도록 만드는 방식을 사용한다.
public class Something {
private Something() {
}
private static class LazyHolder {
public static final Something INSTANCE = new Something();
}
public static Something getInstance() {
return LazyHolder.INSTANCE;
}
}
➡️ 자바스크립트의 싱글톤 패턴
아래의 첫 번째 예시를 통해 알 수 있듯이 애플리케이션에서 SingletonClass 클래스를 여러 번 호출하더라도 항상 동일한 인스턴스를 사용하게 되므로, 이는 싱글톤 패턴이 사용되는 경우에 해당한다. 해당 코드는 SingletonInstance라는 하나의 인스턴스를 new Object를 통해 a, b를 생성하는 경우를 코드로 구현한 것이다. 즉, 싱글톤 패턴이라는 디자인 패턴이 사용된 예시다. 이렇게 함으로써 데이터 일관성을 유지할 수 있다는 장점이 있다.
//인스턴스 변수 (클래스의 인스턴스를 저장하기 위한 변수)
let SingletonInstance;
export default class SingletonClass {
constructor(data = 'Initial data') {
if (SingletonInstance) {
return SingletonInstance;
}
this.data = data;
SingletonInstance = this;
}
getData() {
return this.data;
}
//데이터 초기화 (인스턴스가 처음 생성될 때 this.data에 초기 데이터 설정)
setData(data) {
this.data = data;
}
}
const a = new SingletonClass()
const b = new SingletonClass()
console.log(a === b) //true
덧붙여 두 번째 예시는 싱글톤 패턴이 데이터베이스 연결 모듈에 많이 쓰인다는 것에 대한 예시다. 해당 예시는 DB.instance라는 하나의 인스턴스를 기반으로 new Object를 통해 a, b를 생성하는 경우를 코드로 구현한 것이다.
const URL = 'mongodb://localhost:27017/kundolapp'
const createConnection = url => ({"url" : url})
class DB {
constructor(url) {
if(!DB.instance) {
DB.instance = createConnection(url)
}
return DB.instance
}
connect() {
return this.instance
}
}
const a = new DB(URL)
const b = new DB(URL)
console.log(a == b) //true
참고로 DB.instance와 SingletonInstance는 사용되는 범위에 있어서 차이를 보인다. DB.instance는 클래스 내에서의 정적 속성으로 클래스 레벨에서 공유되는 경우다. 때문에 클래스 정의 내부에서 DB.instance를 사용하여 인스턴스를 관리할 수 있으며 클래스를 통해 직접 접근할 수 있어, 클래스의 책임과 관련하여 인스턴스 관리가 더욱 명확하다는 장점이 있다. 반면, SingleonIntance는 클래스 외부에 정의된 전역 변수에 해당하며, 모든 인스턴스가 이 전역 변수를 공유하게 된다. 또한 클래스 내부에서 SingletonInstance를 참조한다는 차이가 있다. 결국 싱글톤 패턴에서 어떤 방식을 사용하냐의 차이인데, SingletonInstance를 전역 변수로 선언하여 구현하는 경우 보다 유연하지만 코드의 복잡성을 증가시킬 수 있다. 두 가지 구현 방식을 보여주고자 각각 달리 사용했으며, 이를 참고하여 상황에 따라 더 유리한 방식을 선택하면 된다.
➡️ 자바에서의 싱글톤 패턴
아래 코드에서는 Singleton 클래스의 유일한 인스턴스임을 보증하기 위해 기본 생성자의 접근제어자를 private으로 만들어둔 것을 확인할 수 있다. 참고로 구현된 싱글턴의 인스턴스에 접근하는 방법으로는 getInstance라는 정적 팩토리 메서드를 사용하고 있는데, 이는 당연하게도 싱글톤 패턴에서의 필수 요소가 아닌 구현 방식 중 하나다.
public class Singleton {
private static String instance = null;
private Singleton() {
this.instance = "Singleton";
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
➡️ 참고자료
[Book]
면접을 위한 CS 전공지식 노트
[Blog]
https://gyoogle.dev/blog/design-pattern/Singleton%20Pattern.html
https://developer-nyong.tistory.com/29
'Computer Science > Software Design' 카테고리의 다른 글
디자인 패턴 (3) Strategy Pattern (0) | 2024.09.26 |
---|---|
디자인 패턴의 종류 (2) Factory Pattern (0) | 2024.09.26 |
아키텍처 패턴의 종류 (5) MVC Pattern (0) | 2024.07.04 |
아키텍처 패턴의 종류 (4) Sense-Compute-Control Pattern (0) | 2024.07.04 |
아키텍처 패턴의 종류 (3) State-Logic-Display Pattern (3-tier) (0) | 2024.07.04 |