본문 바로가기
기타/Design Pattern

Singleton Pattern

by 코딩하는 랄로 2023. 11. 16.
728x90

Singleton Pattern

싱글톤 패턴의 정의는 다른 디자인 패턴에 비해 매우 단순하다. 싱글톤 패턴이란, 객체의 인스턴스가 오직 1개만 생성되는 패턴을 의미한다. 

 

하지만, 개념의 정의가 단순하다고 별다른 이해 없이 사용하게 된다면 메모리 등의 프로그램은 있는 대로 잡아먹으면서, 동작은 느려지는 프로그램을 마주하게 될 것이다.

 

이러한 일이 발생하지 않도록, 싱글톤 패턴은 어떻게 사용해야 하는지 무엇을 주의해서 사용해야 하는지 살펴보자.

 

 

 

기초적인 Singleton Pattern 구현 및 문제점

싱글톤 패턴을 구현하기 위해서는 몇 가지 필요한 조건이 있다.

 

일반적으로 클래스의 인스턴스를 생성하여 사용하기 위해서는 new 연산자와 생성자를 사용하여 인스턴스를 생성 해 준 후 사용하게 된다. 이 때, new 연산자를 사용할 때마다, 인스턴스는 새로 생성되고 이는 당연히 서로 다른 인스턴스로 취급이 된다.

public class Main {
    public static void main(String[] args) {
        A instance1 = new A();
        A instance2 = new A();

        if ( instance1 != instance2) {
            System.out.println("서로 다른 인스턴스입니다.");
        }
    }
}

class A {}

 

하지만, 싱글톤 패턴은 하나의 인스턴스만을 공유하여 사용하기 때문에 해당 인스턴스를 외부의 클래스에서 생성할 수 없어야 하고, 해당 인스턴스에 직접 접근할 수 없어야 한다. 그렇기 때문에 생성자와 해당 객체의 인스턴스를 담는 필드 변수는 private 접근자로 선언되어야 한다.

 

또한, 해당 객체를 반환 받기 위한 메소드는 static으로 선언되어야 하는데, 그 이유는 해당 클래스는 외부에서 생성자를 통해 인스턴스를 생성하지 못하기 때문에 인스턴스를 통한 메소드의 접근이 불가능하다.

 

이러한 조건을 토대로, 제일 기본적인 싱글톤 패턴을 구현해보면 다음과 같다.

package example;

// 기본적이 싱글톤
// instance를 private static => 내부 메소드를 통해서 접근가능한 글로벌 인스턴스 생성
// 해당 인스턴스가 널이면, 내부 생성자(private)를 통해서 인스턴스 생성
// 인스턴스가 있으면 해당 인스턴스를 반환
// 단점 : 멀티쓰레드 작업에 취약하다.
public class Singleton {
    private static Singleton instance;
    private void Singleton() {}

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

 

위의 코드는 가장 단순한 싱글톤 패턴 구현 코드이지만, 위의 코드는 사실상 사용하지 않는다. 그 이유로는 쓰레드에 안전하지 않기 때문인데 그렇게 되면 다음과 같은 상황이 발생할 수 있다.

Thread A가 instance를 생성하기 직전에 Thread B 가 if 문에 들어오게 되면, Thread B에게도 아직 instance가 null이기 때문에 Thread A에서도 인스턴스를 생성하게 되고, Thread B에서도 인스턴스를 생성하게 되어 두 개의 인스턴스가 생성되는 것이다.

 

이처럼 프로그램은 여러 쓰레드에 의해 동작하는 경우가 많고, 여러 쓰레드가 동일한 자원에 접근할 경우 예상하지 못한 결과가 나올 수 있다. 특히, 하나의 인스턴스를 공유하는 싱글톤을 구현하기 위해서는 쓰레드에 안전한 코드로 작성을 해주어야 한다.

 

 

 

Thread-safe Singleton Pattern 구현

쓰레드에 안전한 코드를 프로그래밍에서는 thread-safe 하다고 말한다. 싱글톤 패턴은 하나의 인스턴스를 공유하기 때문에 thread-safe한 방법을 사용하여 구현해주어 하나의 인스턴스에 여러 접근이 동시에 몰려도 안전하게 동작할 수 있도록 구현 해주어야 한다.

 

방법1. synchronized method

첫번째 방법으로는 싱글톤 인스턴스를 접근, 생성하는 메소드( ex. getInstance() )에 자바에서 제공하는 synchronized 키워드를 붙여주는 방법이다. 자바는 synchronized 키워드를 메소드에 붙이면, 해당 메소드를 사용하고 있는 thread가 있으면 lock을 걸어, 다른 thread는 메소드를 사용하지 못하게 하기 때문에, thread-safe한 싱글톤을 구현할 수 있게 된다.

package example1;

// thread-safe 한 singleton
// synchronized 사용
// 특정 객체에서 접근할 때, 락을 걸어, 다른 객체들은 접근X
// 단점 : 락을 걸고 푸는 synchronized 작업에도 
// 코스트가 발생하기 때문에 많은 쓰레드가 쏠리게 되면 코스트를 많이 쓰게 됨
public class Singleton {
    private static Singleton instance;
    private Singleton() {}

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

 

하지만, 해당 방법은 자원의 소모 부분에서 단점을 가진다. synchronized를 통해 메소드를 사용할 때마다 락을 걸어주고 풀어주는 데에도 코스트가 발생하기 때문에 접근이 많아 질 수록 자원의 소모가 커지게 되는 것이다.

 

 

방법2. eager initialization

두번째 방법은 eager initialization이다. 직역하면 이른 초기화로 이름 그대로 인스턴스를 미리 초기화를 해 놓는 것이다. 

package example2;

// thread-safe 방법2
// 이른 초기화 방법 (eager initialization)
// 최초에 한번 미리 초기화를 해줌
// => 쓰레드에 안전하면서 이를 위한 cost를 소모X
// 단점 :
// 무조건 해당 클래스에 대한 인스턴스를 생성하기 때문에
// 생성하고 사용하지 않게 되면, 인스턴스를 생성하는데 사용된 cost가 낭비(cost가 크면 클수록 손해...)되게 되는 것임
public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}

 

이 방법은, synchronized 키워드를 사용하지 않고 thread-safe 싱글톤을 구현하였기 때문에 방법1의 단점을 없앨 수 있지만 만약, 해당 인스턴스를 사용하지 않게 된다면 의미 없는 인스턴스를 생성한게 되어버린다는 단점 또한 존재한다.

 

인스턴스가 생성되는 데 소모되는 자원(메모리 등)이 적다면, 괜찮지만 만약 자원을 많이 잡아먹는 인스턴스인데 사용하지 않는다면 의미없는 자원이 많이 소모되어 버린 것이다.

 

 

방법3. double checked locking

세번째 방법은 위의 두 가지 방법의 단점을 보완한 double checked locking 방식이다. 

package example3;

// thread-safe 방법 3
// double-checked locking
// synchronized 를 메소드 단계가 아닌 block 단계에서 사용
// instance 가 null => synchronized block에 진입 후 한번 더 instance 가 null 인지 검사
// instance 가 필요할 때마 초기화를 할 수 있고,
// method 단계에서 synchronized 를 하는 것이 아니기 때문에 메소드를 불러낼 때, 동기화에 사용하는 코스트가 X
// => null 일 때만 동기화
public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

 

필요할 때만 인스턴스를 생성하지만 thread-safe를 위해 synchronized를 다시 사용한 것을 볼 수 있다. 하지만 첫번째 방법과는 달리 메소드 레벨에서 사용하는 것이 아닌 블락( {} ) 레벨에서 synchronized를 사용한 것을 볼 수 있다.

 

위의 코드를 해석 해보면, 인스턴스가 아직 생성되지 않았을 때, 여러 쓰레드가 동시에 인스턴스 생성을 시도하려고 하는 상황에서만 synchronized를 걸어주어 thread-safe하게 만든 것이다.

 

즉, 방법 1은 인스턴스가 생성되든 안되든 무조건 synchronized를 호출하였지만, 위의 코드에서는 인스턴스가 없는 상황에서만 호출되도록, 경우의 수를 확 줄여 synchronized에 소모되는 자원을 줄인 것이다.

 

if문은 통해 인스턴스 널인지를 두번 검사하기 때문에 double checked locking ( synchronized를 사용하여 lock 해주므로 )이라고 부른다.

 

 

방법4. static inner class 

네번째 방법은 synchronized를 사용하지 않고, 방법1과 방법2를 보완할 수 있는 방법이다. 바로 static inner class를 사용하는 방법인 것이다.

package example4;

// thread-safe 방법4
// static-inner class 사용방법
public class Singleton {
    private Singleton() {}
    
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

 

static inner class 내에서 인스턴스를 초기화 해주는 방법이다. 초기화를 하는 코드가 inner class로 선언되어 졌기 때문에 singleton 클래스가 class loader에 의해 로딩 될 때는 로딩되지 않다가 getInstance() 가 호출되면서 해당 inner class로 접근할 때 JVM 메모리에 로드되고 인스턴스 객체를 생성하게 되는 것이다.

 

클래스가 로드될 때 객체가 생성되기 때문에 multi-thread 환경에서도 안전한 thread-safe 구현 코드인 것이다. 방법1과 방법2의 문제점을 보완하면서도, 방법 3처럼 synchronized 를 사용하지 않음으로 코스트는 더 줄이고, 코드도 더 간결해졌기 때문에 현재 가장 많이 사용하는 싱글톤 구현 방법이다.

 

 

 

Singleton Pattern 사용 이유

싱글톤 패턴의 사용 이유는 크게 두 가지가 있다. 먼저 메모리 측면에서 이점을 가질 수 있는데, 싱글톤 패턴의 경우 최초 한번의 new 연산자를 통해 고정된 메모리 영역을 사용하기 때문에 메모리 낭비도 줄이면서 이미 생성된 인스턴스를 활용하기 때문에 속도도 향상된다는 이점 또한 가진다.

 

두번째로는 클래스 간에 데이터 공유가 쉬워진다는 것이다. 싱글톤 인스턴스는 전역(static)으로 사용되는 인스턴스이기 때문에 다른 클래스에서 또한 접근하여 사용 가능한 것이다. 이러한 이유로 인해, 위에서 살펴본 방식대로 데이터에 동시에 접근할 수 있는 상황에 대비한 코드를 구현할 수 있어야 한다.

 

이 외에도 도메인 관점에서 인스턴스가 한 개만 존재하는 것을 보증하고 싶은 경우 싱글톤 패턴을 사용하기도 한다.

 

 

 

728x90

'기타 > Design Pattern' 카테고리의 다른 글

Template Method Pattern  (1) 2023.11.26