Home Singleton Pattern
Post
Cancel

Singleton Pattern

싱글톤 패턴에 대해 학습하며 작성된 글입니다. 학습 과정에서 작성된 글이기 때문에 잘못된 내용이 있을 수 있으며, 이에 대한 지적이나 피드백은 언제든 환영입니다.

image







1. 싱글톤 패턴


싱글톤 패턴은 생성자가 여러 번 호출되더라도 단 하나의 객체만 반환하는 패턴을 말합니다. 이를 통해 여러 개의 인스턴스가 생성되는 것을 방지하며, 객체 생성 비용을 줄일 수 있습니다.

In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to a singular instance. One of the well-known “Gang of Four” design patterns, which describe how to solve recurring problems in object-oriented software, the pattern is useful when exactly one object is needed to coordinate actions across a system.









다이어그램을 보면 다음과 같습니다.

image









2. 구현, 고려 사항


싱글톤은 아래와 같은 패턴으로 구현할 수 있습니다. 하지만 여기에는 동기화, 성능과 같은 고려해야 할 점이 있는데, 이에 대해 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class DataSourceConfiguration {

    private static DataSourceConfiguration instance = new DataSourceConfiguration();

    private DataSourceConfiguration() {
    }

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









2-1. 동기화 문제

우선 동기화 문제에 대해 살펴보겠습니다. 아래 코드는 멀티 쓰레드 환경에서 이슈가 생길 수 있는데, 이는 instance의 null 체크를 하는 코드 블록에 동시에 여러 개의 쓰레드가 들어갈 수 있기 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class DataSourceConfiguration {

    private static DataSourceConfiguration instance = new DataSourceConfiguration();

    private DataSourceConfiguration() {
    }

    public static DataSourceConfiguration getInstance() {
        // 해당 블럭에 여러 쓰레드가 접근 가능
        if (instance == null) {
            instance = new DataSourceConfiguration();
        }
        return instance;
    }
}









이 문제를 해결하기 위해서는 아래와 같이 synchronized 키워드를 사용해 여러 개의 쓰레드가 동시에 접근하는 문제를 해결할 수 있습니다. synchronized 블럭 내부로 들어가기 전 항상 동기화를 체크하기 때문입니다. 하지만 이 또한 완전한 싱글톤은 보장할 수 없는데, 리플렉션 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class DataSourceConfiguration {

    private static volatile DataSourceConfiguration instance = new DataSourceConfiguration();

    private DataSourceConfiguration() {
    }

    // Double Checked Locking
    public static DataSourceConfiguration getInstance() {
        if (instance == null) {
            synchronized (DataSourceConfiguration.class) {
                if (instance == null) {
                    instance = new DataSourceConfiguration();
                    return instance;
                }
            }
        }
        return instance;
    }
}









리플랙션으로 private 생성자를 호출해서 객체를 생성하는 방법은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@DisplayName("[UnitTest] 싱글톤 단위 테스트")
class SingletonUnitTest {

    @Test
    @DisplayName("리플렉션으로 private 생성자를 호출해 객체를 생성할 수 있다.")
    void new_instance_create_with_reflection_test() throws Exception {
        Class<DataSourceConfiguration> clazz = DataSourceConfiguration.class;

        Constructor constructor = clazz.getDeclaredConstructor(null);
        constructor.setAccessible(true);

        DataSourceConfiguration object = (DataSourceConfiguration) constructor.newInstance();
        assertNotNull(object);
    }
}

image









두 번째 방법은 Holder를 사용한 방법으로, 내부 클래스에 해당 클래스 객체를 static으로 만들어 두고 사용하는 것입니다. 아쉽지만 이 방법 또한 리플랙션 때문에 완전한 싱글톤은 보장할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class DataSourceConfiguration {

    private static volatile DataSourceConfiguration instance = new DataSourceConfiguration();

    private DataSourceConfiguration() {
    }

    public static DataSourceConfiguration getInstance() {
        return DataSourceHolder.instance;
    }

    private static class DataSourceHolder {
        private static final DataSourceConfiguration instance = new DataSourceConfiguration();
    }
}









현재까지 싱글톤을 깨트리는 방법에 대해 살펴보았는데, 완전한 해결책은 자바 코드로 구현하면 너무 길어지기에 '이런 방식을 통해 싱글톤을 깨트릴 수 있다.' 정도만 이해하고 넘어가면 좋을 것 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class DataSourceConfiguration {

    private static DataSourceConfiguration instance = new DataSourceConfiguration();

    private DataSourceConfiguration() {
    }

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









2-2. 직렬화/역 직렬화

두 번째 고려할 점은 직렬화/역 직렬화 시 싱글톤이 깨질 수 있다는 점입니다. 객체를 직렬화해서 파일로 만든 후 역 직렬화하면 객체를 생성할 수 있기 때문입니다.

image









아래와 같이 싱글톤이 깨진 것을 볼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@DisplayName("[UnitTest] 싱글톤 단위 테스트")
class SingletonUnitTest {

    private static final String PATH = "data/config.obj";

    @Test
    @DisplayName("직렬화/역 직렬화를 시키면 싱글톤이 깨질 수 있다.")
    void when_serialize_and_deserialize_then_singleton_should_be_broken() throws Exception {
        DataSourceConfiguration configuration = DataSourceConfiguration.getInstance();
        DataSourceConfiguration instance = null;
        try (ObjectOutput objectOutput =
            new ObjectOutputStream(new FileOutputStream(PATH))
        ) {
            objectOutput.writeObject(configuration);
        }

        try (ObjectInput objectInput = new ObjectInputStream(
            new FileInputStream(PATH))) {
            instance = (DataSourceConfiguration) objectInput.readObject();
        }

        assertNotEquals(configuration, instance);
    }
}

image









물론 readResolve( ) 메서드를 사용하면 역 직렬화 과정에서도 동등성을 보장받을 수 있습니다만, 리플렉션의 경우 여전히 문제점이 존재합니다.

1
2
3
4
5
6
7
8
public class DataSourceConfiguration {
    
    ......
    
    protected Object readResolve(){
        return getInstance();
    }
}









2-3. Enum

Enum은 Java 언어 차원에서 지원하는 싱글톤 클래스 입니다. 따라서 Enum을 사용하면 완벽한 싱글톤을 구현할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum EnumDataSourceConfiguration {
    MYSQL_CONFIGURATION(new MySQLConfiguration()),
    ORACLE_CONFIGURATION(new OracleConfiguration());

    private final DataSourceConfiguration dataSourceConfiguration;

    EnumDataSourceConfiguration(DataSourceConfiguration dataSourceConfiguration) {
        this.dataSourceConfiguration = dataSourceConfiguration;
    }

    public DataSourceConfiguration getDataSourceConfiguration() {
        return dataSourceConfiguration;
    }
}

이런 경우까지 고려할 수 있다는 점을 알고 있는 것으로 충분하지 않을까 싶습니다. 이런 극단적인 사례는 잘 발생하지 않으며, 특히 스프링을 사용한다면 @Bean, @Configuration, @Component 등을 통해 싱글톤을 충분히 보장할 수 있기 때문입니다.









3. 정리


싱글톤은 객체를 한 번만 생성하기 때문에 객체 생성 비용을 줄일 수 있습니다. 이를 통해 한 번 생성된 객체를 재활용해서 메모리를 효율적으로 사용할 수 있게 할 수 있습니다. 단, 이때 동시성 이슈와 같은 문제가 발생할 수 있으므로 싱글톤은 상태를 갖지 않는 것이 중요합니다.

   - GOF Design Pattern
   - Singleton pattern


This post is licensed under CC BY 4.0 by the author.

Observer Pattern

좋은 코드 리뷰를 위한 규칙