[디자인 패턴] 싱글톤 패턴(Singleton Pattern)

sinryuji·2025년 3월 2일
post-thumbnail

싱글톤 패턴(Singleton Pattern)이란?

싱글톤 패턴(Singleton Pattern)은 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴입니다. 보통 데이터베이스 연결 모듈에 많이 사용합니다.

하나의 인스턴스를 만들어 놓고 해당 인스턴스를 다른 모듈들이 공유하며 재사용하기 때문에 인스턴스를 생성할 때 드는 비용이 줄어드는 장점이 있습니다. 그래서 I/O Bound 작업에 많이 사용됩니다.

💡 I/O Bound란?

I/O Bound는 네트워크 연결, 데이터베이스, 파일 시스템에 대한 요청을 일컫는다.

JavaScript의 싱글톤 패턴

class Singleton {
    constructor() {
        if (!Singleton.instance) {
            Singleton.instance = this
        }
        return Singleton.instance
    }
    getInstance() {
        return this 
    }
}
const a = new Singleton()
const b = new Singleton() 
console.log(a === b) // true 

JavaScript로 싱글톤 패턴을 구현한다면 위와 같이 구현할 수 있겠습니다. if (!Singleton.instance) {}를 통해 싱글톤 인스턴스 여부를 채크하고, 인스턴스가 없다면 자기 자신을 할당합니다. 그 후 인스턴스를 반환합니다.

Mongoose의 싱글톤 패턴

Mongoose.prototype.connect = function(uri, options, callback). {
	const _mongoose = this instanceof Mongoose ? this : mongoose;
	const conn = _mongoose.connection;

	return _mongoose._promiseOrCallback(callback, cb => {
		conn.openUri(uri, options, err => {
			if (err != null) {
				return cb(err);
			}
			return cb(null, _mongoose);
		})
	})
}

앞서 데이터베이스 연결 모듈에 싱글톤 패턴이 많이 사용된다고 하였습니다. 실제 Mongoose의 connect 함수 코드입니다. const _mongoose = this instanceof Mongoose ? this : mongoose;를 보면 만약 this가 Mongoose 인스턴스를 그대로 사용하고 그렇지 않다면 전역 mongoose를 사용함으로써 싱글톤 패턴을 구현하고 있음을 확인할 수 있습니다.

싱글톤 패턴의 단점

싱글톤 패턴은 TDD(Test Driven Development)를 할 때 큰 장애물이 됩니다. TDD에서 각 단위 테스트는 독립적이어야 하며 어떠한 순서로든 실행이 되어야 합니다.

하지만 싱글톤 패턴은 오직 하나의 인스턴스만이 존재하므로 각 테스트마다 독립적인 인스턴스로 테스트를 하지 못합니다.

또 싱글톤 패턴은 모듈간의 결합을 강하게 만들 수 있다는 단점이 있습니다. 싱글톤 인스턴스를 사용하는 곳과 싱글톤 클래스간에는 의존성이 생길 수 밖에 없습니다.

이에 연관되어 싱글톤 패턴을 사용할 때 주의 사항으로 어떠한 상태를 갖는 인스턴스가 싱글톤이어서는 안된다는 것 입니다. 싱글톤 인스턴스를 사용하는 모든 스레드에서 싱글톤 인스턴스의 상태를 마구잡이로 바꾸고 이게 공유가 된다는 것은 매우 위함하기 때문입니다.

싱글톤 패턴을 구현하는 7가지 방법.

1. 단순한 메소드 호출

싱글톤 패턴 생성 여부를 확인하고 싱글톤이 없으면 새로 만들고 있으면 만들어진 인스턴스를 반환합니다.

public class Singleton {
    private static Singleton instance;

    private Singleton() {
        
    }

    public static Singleton getInstance() {
        if (instance == null) {  
            instance = new Singleton();
        }
        return instance;
    }
}

위 코드는 원자성이 결여되어 있음. 멀티스레딩 환경이라면 인스턴스를 2개 이상 만들 수 있습니다.

2. synchronized

인스턴스를 반환하기 전까지 격리 공간에 놓기 위해 synchronized 키워드로 잠글 수 있음. 최초로 접근한 스레드가 해당 메소드 호출 시에 다른 스레드가 접근하지 못하도록 잠급니다.

이 때 getInstance()를 호출 할 때 마다 lock이 걸리기에 성능이 저하된다는 단점이 있습니다.

public class Singleton {

    private static Singleton instance;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

3. 정적 멤버

정적(static) 멤버나 블록은 런타임이 아니라 JVM이 최초에 클래스를 로딩할 때 한 번만 메모리에 할당 됩니다.. 즉, 클래스 로딩과 동시에 인스턴스를 생성합니다. 그렇기에 모듈들이 싱글톤 인스턴스를 요청할 때 그냥 만들어진 인스턴스를 반환하면 됩니다.

이는 불필요한 자원 낭비라는 단점이 있습니다. 싱글톤 인스턴스가 필요하지 않은 상황에서도 무조건 인스턴스를 만들기 때문입니다.

public class Singleton {

    private final static Singleton instance = new Singleton();

    private Singleton() {
    
    }

    public static Singleton getInstance() {
        return instance;
    }
}

4. 정적 블록

정적 멤버와 모두 동일하나 static 초기화 블록을 사용하는 방식입니다. 멤버 변수 할당보다 뒤에 이루어진다는 차이가 있습니다.

public class Singleton {

    private static Singleton instance = null;

    static {
        instance = new Singleton();
    }

    private Singleton() {
        
    }

    public static Singleton getInstance() {
        return instance;
    }
}

5. 정적 멤버와 Lazy Holder(중첩 클래스)

singleInstanceHolder라는 내부 클래스를 하나 더 만듦으로써, Singleton 클래스가 최초에 로딩 되더라도 초기화 되지 않고, getInstance()가 호출 될 때 singleInstanceHolder 클래스가 로딩되어 인스턴스가 생성됩니다.

class Singleton {
    private static class singleInstanceHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return singleInstanceHolder.INSTANCE;
    }
}

6. 이중 확인 잠금(DCL, Double-checked locking)

인스턴스 생성 여부를 싱글톤 패턴 잠금 전에 한 번, 객체를 생성하기 전에 한 번, 총 두 번 체크하면 인스턴스가 존재하지 않을때만 잠글 수 있기 때문에 synchronized를 썼을때 생긴 성능 저하 문제를 해결할 수 있습니다.

public class Singleton {

    private volatile Singleton instance;

    private Singleton() {
        
    }

    public Singleton getInstance() {
        if (instance == null) {  
            synchronized (Singleton.class) {
                if (instance == null) {  
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

💡 volatile 키워드란?

https://velog.io/@sinryuji/Java-volatile-%ED%82%A4%EC%9B%8C%EB%93%9C

7. enum

enum 인스턴스는 기본적으로 Thread-safe함이 보장되기 때문에 이를 통해 싱글톤을 구현할 수 있습니다.

public enum SingletonEnum {
    INSTANCE;
    public void oortCloud() {

    }
}

무엇을 사용해야 할까?

보통 5번, 정적 멤버와 Lazy Holder를 이용하는 방법이 가장 많이 쓰입니다. 그리고 7번은 Effective Java의 저자 조슈아 블로치가 추천한 방법이라고 합니다.

profile
응애 개발자입니다.

0개의 댓글