본문 바로가기
  • 개발공부 및 일상적인 내용을 작성하는 블로그 입니다.
이론/디자인패턴

디자인 패턴 - 싱글톤 패턴(Singleton pattern)

by 방구석 대학생 2022. 1. 30.

 

애플리케이션이 시작될 때, 어떤 클래스가 최초 한 번만 메모리를 할당(static) 하고, 해당 메모리에 인스턴스를 만들어서 사용하는 패턴이다.

 

즉, 싱글톤 패턴은 '하나'의 인스턴스만 생성하여 사용하는 디자인 패턴을 말한다.

- 인스턴스가 필요할 때, 똑같은 인스턴스를 만들지 않고 기존의 인스턴스를 활용하는 것

 

생성자가 여러번 호출되도 실제로 생성되는 객체는 하나이며, 최초로 생성된 이후에 호출된 생성자는 이미 생성한 객체를 반환시키도록 만드는 것이다.

(java 에서는 생성자를 private 으로 선언해 다른 곳에서 생성하지 못하도록 만들고, getInstance() 메소드를 통해 받아서 사용하도록 구현한다.)

 

 

싱글톤 패턴은 왜 쓰는걸까?

먼저, 객체를 생성할 때마다 메모리 영역을 할당 받아야 한다.

하지만 한번의 new 를 통해 객체를 생성한다면 메모리 낭비를 방지할 수 있다.

 

또한 싱글톤으로 구현한 인스턴스는 '전역' 이므로 다른 클래스의 인스턴스 들이 데이터를 공유하는 것이 가능한 장점이 있다.

 

싱글톤을 많이 사용하는 경우는 언제일까?

주로 공통으로 사용해야 하는 객체를 여러개 생성해서 사용해야 하는 상황에 싱글톤 디자인 패턴이 쓰인다.

- 예시 : 데이터베이스에서 커넥션 풀, 스레드 풀, 캐시, 로그 기록 객체 등

또한 인스턴스가 절대적으로 한 개만 존재하는 것을 보증하고 싶을 때 사용한다.

 

싱글톤 패턴의 단점?

객체 지향 설계원칙 중에 개방 - 폐쇄 원칙 이라는 것이 존재한다.

 

만약 싱글톤 인스턴스가 혼자 너무 많은 일을 하거나 많은 데이터를 공유 시키면, 다른 클래스들간의 결합도가 높아지게 되는데, 이때 개방 - 폐쇄 원칙에 위배된다.

 

결합도가 높아지게 되면 유지보수가 힘들고 테스트도 원활하게 진행할 수 없는 문제점이 발생한다.

 

또한 멀티 스레드 환경에서 동기화 처리를 하지 않았을 때, 인스턴스가 2개가 생성되는 문제도 발생할 수 있다.

따라서, 반드시 싱글톤이 필요한 상황이 아니면 지양하는 것이 좋다고 한다.

(설계 자체에서 싱글톤 활용을 원활하게 할 자신이 있다면 괜찮음)

 

 

멀티 스레드 환경에서 안전한 싱글톤 패턴을 만드는 법

1. Lazy Initialization (게으른 초기화)

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;
    }
}

- private static 으로 인스턴스 변수를 만든다.

- private 으로 생성자를 만들어 외부에서의 생성을 막는다.

- synchronzied 동기화를 활용해 스레드를 안전하게 만든다.

* 하지만 synchronzied 는 큰 성능 저하를 발생 시키므로 권장하지 않는 방법이다.

 

 

2. Lazy Initialization + Double-checked Locking

- 1번의 성능 저하를 완화시키는 방법

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;
    }
}

- 1번과는 달리, 먼저 조건문으로 인스턴스의 존재 여부를 확인한 다음 두번째 조건문에서 synchronized 를 통해 동기화를 시켜 인스턴스를 생성하는 방법

- 스레드를 안전하게 만들면서, 처음 생성 이후에는 synchronized 를 실행하지 않기 때문에 성능 저하 완화가 가능하다.

* 하지만 완전히 완벽한 방법은 아니다.

 

 

3. Initialization on demand holder idiom (holder 에 의한 초기화)

클래스 안에 클래스(holder) 를 두어 JVM 의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 방법

 

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

- 1,2번 과 달리 동기화를 사용하지 않는 이유는, 개발자가 직접 동기화 문제에 대한 코드를 작성하면서 회피하려고 하면 프로그램 구조가 그만큼 복잡해지고 비용 문제가 발생할 수 있다.

또한 코드 자체가 정확하지 못할 때도 많다.

- 이 때문에, 3번과 같은 방식으로 JVM 의 클래스 초기화 과정에서 보장되는 원자적 특성을 이용해 싱글톤의 초기화 문제에 대한 책임을 JVM에게 떠넘기는 걸 활용한다. 

- 클래스 안에 선언한 클래스인 holder 에서 선언된 인스턴스는 static 이기 때문에 클래스 로딩 시점에서 한번만 호출된다.

또한 final 을 사용해서 다시 값이 할당되지 않도록 만드는 방식을 사용한 것

* 실제로 가장 많이 사용되는 일반적인 싱글톤 클래스 사용 방법이 3번이다.

 

 

참조 : https://gyoogle.dev/blog/

 

👨🏻‍💻 Tech Interview

 

gyoogle.dev

 

'이론 > 디자인패턴' 카테고리의 다른 글

디자인패턴 - SOLID(LSP, ISP, DIP)  (0) 2022.01.31
디자인패턴 - SOLID (SRP, OCP)  (0) 2022.01.30
디자인패턴 - 개요  (0) 2022.01.29