싱글톤(Singleton)패턴에 대한 개념 이해하기

CS 지식중 디자인패턴에 대해서 공부하면서 각 패턴에 대해서 정리하는 시간을 가지고자 합니다.
먼저 싱글톤 패턴을 정리해보겠습니다.


싱글톤 패턴은 무엇인가?

  1. 싱글톤 패턴은 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴입니다.
  2. 보통 데이터베이스 연결에 많이 사용되는 패턴입니다.

자바에서의 싱글톤 패턴을 예로 들자면 아래와 같은 로직이 나옵니다.

class Singleton {
    private static class singleInstanceHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return singleInstanceHolder.INSTANCE;
    }
}

싱글톤 패턴은 왜 사용할까?

  1. 메모리 절약을 위해, 인스턴스를 재생성하지 않고 기존의 인스턴스를 활용할 수 있기 때문
  2. 우리가 각 함수에 같은 변수를 선언하는 지역변수를 사용하지 않고, 전역변수를 사용하여 모든 함수에 참조할 수 있게 하는 의미와 비슷하다.
  3. 특히나 싱글톤 패턴이 필요한 경우는 리소스를 많이 차지하는 경우에 어울린다고 볼 수 있다.
    • 위에서 언급한 데이터베이스 연결 모듈이나, 네트워크 통신, 캐시, 로그 등이 있을 수 있다.

싱글톤 패턴의 단점

물론 좋은점만 있는것은 아니다.
특히나, TDD(Test Driven Development)를 수행할때 문제가 생길 수 있다.
보통 TDD의 경우에는 단위테스를 수행하게 되는데, 단위테스트는 각 테스트들이 독립적으로 흘러가야 하는데, 동일한 인스턴스로 테스트를 진행하면 각 테스트마다 독립적인 인스턴스를 생성하기 어렵다.

TDD란?
Test Driven Development의 약자로 '테스트 주도 개발' 이라고 한다.
짧은 개발 주기의 반복에 의존하는 개발 프로세스이며, eXtream Programming(XP)의 'Test-First' 개념에 기반을 둔 단순한 설계를 중요시한다.
대표적으로 TDD Tool 중에 하나로 JUnit을 뽑을 수 있다.


싱글톤 패턴의 단점 해결

의존성 주입 (DI)

갑자기 의존성 주입이 왜 나오냐는 질문이 생길수있다.
하지만 싱글톤패턴은 사용하기 쉽고 실용적이지만, 모듈 간의 결합을 강하게 만들 수 있다는 단점이 있다.
이럴 때, 의존성 주입 (DI, Dependency Inject)을 통해 모듈간의 겹합을 좀 느슨하게 만들 수 있다.

의존성이란?
의존성이란 종속성이라도 하고, 만약 A가 B에 의존성이 있다는 것은 B의 변경 사항에 대해 A도 변해야 한다는 의미다.

의존성 주입에 대해서는 이후에 정리된 글을 업데이트하겠다.


싱글톤 패턴 구현

  • Eager Initialization
  • Static block initialization
  • Lazy initialization
  • Thread safe initialization
  • Double-Checked Locking
  • Bill Pugh Solution (권장👍)
  • Enum 이용 (권장👍)

위의 7가지의 방법으로 싱글톤 패턴이 구현 가능하다.
더 자세한 내용은 맨 하단의 참조 해 놓았지만 이분의 글을 많이 참조하였으니 방문 해서 더 깊게 보시는것을 추천한다.

 

💠 싱글톤(Singleton) 패턴 - 꼼꼼하게 알아보자

Singleton Pattern 싱글톤 패턴은 디자인 패턴들 중에서 가장 개념적으로 간단한 패턴이다. 하지만 간단한 만큼 이 패턴에 대해 코드만 던져주고 끝내버리는 경우가 있어, 어디에 쓰이는지 어떠한 문

inpa.tistory.com


Eager Initialization

한번만 미리 만들어두는 가장 간편하고 직관적인 기법이다.

class Singleton {
    // 싱글톤 
    private static final Singleton INSTANCE = new Singleton();

    // 외부에서 접근할 수 없게 private 선언
    private Singleton() {}

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

Static block initialization

Static Block
클래스가 로딩되고 클래스 변수가 준비된 후 자동으로 실행되는 블록

class Singleton {
    // 싱글톤 
    private static Singleton instance;

    // 외부에서 접근할 수 없게 private 선언
    private Singleton() {}

    // static block && 예외처리
    static {
        try {
            instance = new Singleton();
        } catch (Exception e) {
            throw new RuntimeException("싱글톤 객체 생성 오류");
        }
    }

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

Lazy initialization

메서드를 호출했을 때 인스턴스 변수의 Null 유무에 따라 초기화 또는 기존것을 반환하는 기법

class Singleton {
    // 싱글톤
    private static Singleton instance;

    // 외부에서 접근할 수 없게 private 선언
    private Singleton() {}

    // 외부에서 호출할시 생성
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); 
        }
        return instance;
    }
}

Thread safe initialization

synchronized 를 통해 Thread 접근을 하나하나씩 설정 (동기화)

class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

Double-Checked Locking

매번 synchronized 동기화가 문제라면, 최초 초기화할때만 적용하고 이미 만들어진 반환할때는 사용하지 않도록 하는 기법

class Singleton {

    private static volatile Singleton instance; // volatile ?? 

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            // 동기화
            synchronized (Singleton.class) { 
                if(instance == null) { 
                    instance = new Singleton(); // 최초 초기화만 동기화 작업이 일어나서 리소스 낭비를 최소화
                }
            }
        }
        return instance; // 최초 초기화가 되면 앞으로 생성된 인스턴스만 반환
    }
}
// Java에서는 Thread를 여러개 사용할 경우, 각각의 Thread들은 변수를 캐시메모리에서 가져온다.
// votile 키워드를 통해 캐시메모리가 아닌 메인메모리에서 접근하도록 설정한다.

Bill Pugh Solution (LazyHolder) 👍👍

매우 권장되는 방법이다.
static 내부 클래스를 이용하여 getInstance 메소드가 호출되어야 인스턴스가 생성된다.
final을 사용하여 INSTANCE를 더이상 선언하지 않도록 막는다.

class Singleton {

    private Singleton() {}

    private static class SingleInstanceHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingleInstanceHolder.INSTANCE;
    }
}

Enum 이용 👍👍

매우 권장되는 방법이다.
enum은 멤버를 만들때 private로 만들고 한번만 초기화하기 때문에 Thread Safe

enum SingletonEnum {
    INSTANCE;

    private final Client dbClient;

    SingletonEnum() {
        dbClient = Database.getClient();
    }

    public static SingletonEnum getInstance() {
        return INSTANCE;
    }

    public Client getClient() {
        return dbClient;
    }
}

public class Main {
    public static void main(String[] args) {
        SingletonEnum singleton = SingletonEnum.getInstance();
        singleton.getClient();
    }
}

마무리

프레임워크를 사용한다면 이러한 패턴은 드물겠지만,
프레임워크 없이 개발을 진행한다고 보았을때, 한번쯤은 적용해볼만한 패턴이 아닌가 싶다.
물론 각 구현방식마다 장단점이 있기때문에, 상황에 맞게 잘 사용하면 좋을 것 같다. 😎


참조👏
싱글톤(Singleton) 패턴 - 꼼꼼하게 알아보자
TDD란? 테스트 주도 개발