[Design Pattern] Singleton

Roy·2024년 1월 19일

Design Pattern

목록 보기
4/5

Singleton 패턴은 한 Class에 한 Instance만 존재하도록 하는 OOP의 설계 방법이다.

아주 단순하게 Singleton을 아래와 같이 구현할 수 있다.

public class Singleton {
    private static Singleton instance;

    private Singleton() { }

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

        return instance;
    }
}

이런 식의 구현은 Multi-Thread 환경에서 문제를 겪을 수 있다.

  • 두 개의 Thread가 getInstance를 실행하는 경우를 예로 들 수 있다.
    A Thread가 instance가 생성하는 도중에, B Thread는 인스턴스가 존재하는 지 검사하는 조건문을 실행한다.
    A Thread와 B Thread가 각각 다른 인스턴스를 갖게 되므로, Singleton의 설계 원칙을 위배한 것이다.

이를 해결하는 방법은 3가지가 있다.

- Thread-Safe Singleton in Java

1. Eager Initialization를 사용하는 방법

# Eager Initialization

public class Singleton {

    private static final Singleton INSTANCE = new Singleton();

    private Singleton() { }

    public static Singleton getInstance() {
        return INSTANCE;
    }

}

첫번째 방법은 클래스를 생성하는 즉시 인스턴스를 만드는 방법이다.
이 방법에서 getInstance가 Thread-Safe하게 된다.
그런데 인스턴스를 만드는 비용이 비싸다면, 더 효율적인 방법을 생각해야 한다.

2. synchronized를 사용하는 방법

synchronized를 사용하는 방법은 다시 2가지로 나뉜다.

synchronized를 getInstance라는 메소드 앞에 붙이면 클래스 내 존재하는 Thread의 Sync를 맞춰준다.

첫번째 방법은 getInstance 메소드에 단순히 synchronized를 추가하는 것이고,
두번째 방법은 Double Checked Locking이란 방법이다.

# just add synchronized

public class Singleton {

    private static Singleton instance;

    private Singleton() { }

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

        return instance;
    }

}

첫번째 방법의 문제는 Singleton의 getInstance 메소드를 Multi-Thread로 활용할 수 없다는 것이다.
Multi Thread를 자주 호출하는 서비스에서 성능이 저하될 수 있다.

이를 해결하는 방법은 synchronized를 getInstance 내부에 배치하는 것이고, Double Checked Locking이라고 불린다.

# Double Check Locking
public class Singleton {
    
    private static volatile Singleton instance;
    
    private Singleton() { }
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
    }
    
}

Double Checked Locking에선 Singleton Class의 Constructor에 volatile 이란 문법이 추가됐다.
이 문법은 Java의 메모리 관리에 대한 지식이 필요하다.
또 synchronized가 사용된 구조가 다른 방법에 비해 복잡하다.

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

위의 3가지 방법으로 Thread-Safe한 Singleton를 설계했다.

- Loopholes in Client Code

Thread-Safe하면서 프로세스 내 하나의 인스턴스를 갖도록 설계했으나,
이를 사용하는 Client 코드에서 Singleton 패턴을 깨뜨릴 수 있는 2가지 Loophole이 있다.

1. Reflection

첫번째는 리플렉션이다.

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class App {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException {
        Singleton singletonA = Singleton.getInstance();
        
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton singletonB = constructor.newInstance();
        
        System.out.println(singletonA == singletonB);
        # false
    }
    
}

2. Serialization & Deserialization

두번째는 직렬화와 역직렬화이다.
Java에서는 실행중인 객체를 파일 형태로 저장하고 불러올 수 있다.
이 방법으로도 Singleton 설계를 깨트릴 수 있다.

public class App {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Singleton singletonA = Singleton.INSTANCE;

        Singleton settingsB = null;
        try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
            out.writeObject(singletonA);
        }

        try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
            singletonB = (Singleton) in.readObject();
        }

        System.out.println(singletonA == singletonB);
        # false
    }

}

2가지 Loophole을 해결할 수 있는 방법은 enum을 이용하여 클래스를 설계하는 것이다.

enum : Solution for above Loopholes

public enum Singleton {

    INSTANCE;
    
}

이 방법의 단점은 class 생성시 인스턴스가 미리 생성된다는 것이다.

profile
Backend Engineer

0개의 댓글