본문 바로가기
Design Pattern

[디자인 패턴] 총 정리 (3) - 싱글톤(Singleton) 패턴 [생성 패턴]

by 노력남자 2022. 2. 12.
반응형

이번 포스팅부터 드디어 디자인 패턴을 정리하게 되었네요.

 

이번 포스팅에서 알아볼 패턴은 생성 패턴의 싱글톤(Singleton) 패턴입니다.

 

싱글톤(Singleton) 패턴이란?

 

싱글톤 패턴은 인스턴스를 오직 1개 생성하고 이를 리턴해주는 패턴입니다.

 

 

싱글톤의 사전적 의미를 네이버 사전에서 찾아보니 단독 개체, 독신자, 외둥이 등이 나옵니다.

 

싱글톤은 단독 개체라는 뜻으로 보시면 되겠습니다.

 

언제 사용하나?

 

인스턴스를 오직 1개만 만들어야 하는 경우에 사용합니다. 예) 환경설정 정보, 아이템 창

 

코드

 

생각보다 인스턴스를 1개만 만들어 주는 작업이 쉬운 거 같으면서도 막상 개발해보면 어려습니다.

 

왜 어려운지 어떤 걸 고려해야 하는지 싱글톤을 구현하는 여러 개발 방법을 알아보고 각 개발 방법의 장/단점을 알아보겠습니다.

 

1. 기본 코드

 

기본적으로 인스턴스를 1개만 만들려면 아래와 같이 new 를 사용하지 않아야 합니다. 

 

public static void main(String[] args) {
    Singleton1 singleton1 = new Singleton1();
    Singleton1 singleton2 = new Singleton1();

    System.out.println(singleton1 == singleton2); // false
}

 

그래서 아래와 같이 생성자를 private으로 만들어줘야 합니다.

 

엥 그럼 생성자로 인스턴스를 못 받는데 어떻게 가져올 수 있죠?

 

getInstance() 메소드로 가져올 수 있습니다. 

 

근데 아래와 같이 getInstance()에서도 new를 사용하면 인스턴스가 호출될 때마다 생성되기 때문에 싱글톤이 될 수 없습니다.

 

public class Singleton1 {
    
    private Singleton1() {} // 생성자 호출 방지

    // 매번 다른 객체가 생성
    public static Singleton1 getInstance() {
        return new Singleton1();
    }
}

 

그럼 어떻게 해야 할까요?

 

2. 단순 static 필드 사용

 

바로 static 필드를 사용하면 됩니다.

 

getInstance가 호출될 때 instance가 없으면 초기화 해주고 있으면 바로 리턴을 해주도록 구현하면 됩니다.

 

public class Singleton2 {

    private Singleton2() {}

    private static Singleton2 instance;

    // 멀티 쓰레드 환경에선 문제가 발생
    public static Singleton2 getInstance() {
        if (instance == null) { // <- 쓰레드가 동시에 사용하는 경우 문제 발생
            instance = new Singleton2();
        }

        return instance;
    }
}

 

단점 

 

위 코드는 문제가 있는데 멀티 쓰레드 환경에선 싱글톤이 유지가 안 됩니다.

 

if (instance == null) 부분을 쓰레드가 동시에 타는 경우 instance = new Singleton2();가 동시에 실행이 되어 싱글톤 유지가 안 됩니다.

 

3. getInstance에 synchronized 사용

 

이번엔 멀티쓰레드에서 사용할 수 있게 getInstance에 synchronized를 붙혀줬습니다.

 

public class Singleton3 {

    private Singleton3() {}

    private static Singleton3 instance;

    // 성능상 문제가 발생할 여지가 있다.
    public static synchronized Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }

        return instance;
    }
}

 

문제점 

 

getInstance는 static method라 synchronized를 붙히면 클래스 락이 걸려 1개의 쓰레드의 작업이 다 끝날 때 까지 다른 쓰레드들은 대기해야 하기 때문에 성능에 문제가 생길 수도 있습니다.

 

4. 이른 초기화 (eager initialization) 사용

 

synchronized를 사용하지 않고 instance 변수를 이른 초기화를 해주는 방법을 사용할 수 있습니다.

 

public class Singleton4 {

    private Singleton4() {}

    private static Singleton4 instance = new Singleton4();

    // 초기 생성 비용이 적은 경우 사용할 수 있다.
    public static Singleton4 getInstance() {
        return instance;
    }
}

 

단점

 

이른 초기화로 인스턴스 생성을 먼저 하기 때문에 만약 생성에 많은 비용이 들어가는 경우 문제가 생길 수 있습니다.

 

5. double checked locking 적용

 

2, 3의 단점을 해결하기 위해 double checked locking을 적용했습니다.

 

double checked locking은 2의 문제점인 락 때문에 발생하는 성능 문제를 해결하기 위해 lock을 걸기 전에 1번, 걸고 나서 1번 체크하는 방법을 말합니다.

 

public class Singleton5 {

    private Singleton5() { }

    // 1.5 이상부터 사용 가능 volatile 사용!
    private static volatile Singleton5 instance;

    // double checked locking
    public static Singleton5 getInstance() {
        if (instance == null) {
            synchronized (Singleton5.class) {
                if (instance == null) {
                    instance = new Singleton5();
                }
            }
        }

        return instance;
    }
}

 

단점

 

double checked locking을 사용하려면 instance 필드에 volatile를 붙혀줘야 합니다.

 

이를 다 이해하고 써야 하는 단점이 있는데 사실 이건 그냥 공부하면 되는 문제인듯...

 

6. static inner class 사용 (추천)

 

static inner class를 사용해서 instance를 지연 초기화(lazy initialization)하는 방법을 사용할 수 있습니다.

 

getInstance를 호출할 때 Singleton6Holder가 static 영역에 올라가면서 INSTANCE가 생성되기 때문에 위에서 발생한 단점들을 모두 보완하는 방법이고 코드도 간단해서 추천합니다.

 

public class Singleton6 {

    private Singleton6() { }

    private static class Singleton6Holder {
        private static final Singleton6 INSTANCE = new Singleton6();
    }

    // getInstance가 호출될 때 로딩이 되기 때문에 멀티쓰레드 환경에서도 문제없다.
    public static Singleton6 getInstance() {
        return Singleton6Holder.INSTANCE;
    }
}

 

7. enum 사용 (추천)

 

아주아주 심플하고 간단하고 좋은 방법입니다.

 

enum Singleton7 {
    INSTANCE
}

 

단점

 

초기 생성 비용이 큰 경우 문제가 발생할 수 있습니다.

 

enum은 상속이 불가능합니다. 상속이 필요하면 5번을 사용하는 걸 추천합니다.

 

+ 싱글톤 패턴 구현을 깨는 방법이 궁금하다면 아래 더보기를 눌러주세요.

더보기

싱글톤 패턴 구현을 깨는 방법

 

1. 리플렉션(reflection) 사용 (enum 빼고 모든 방법에 통함)

 

이 방법은 리플렉션을 사용해 private 생성자를 이용하는 방법입니다.

 

위에서 소개한 방법 중 enum 빼고 모두 깨집니다.

 

enum은 newInstance가 안 되게 막혀있습니다.

 

Singleton singleton1 = Singleton.getInstance();

Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true); // private 메소드 접근 가능하게 변경
Singleton singleton2 = declaredConstructor.newInstance();

System.out.println(singleton1 == singleton2); // false

 

2. 직렬화, 역직렬화 (enum 빼고 모든 방법에 통함)

 

이 방법은 직렬화, 역직렬화를 사용하는 방법입니다.  

 

위에서 소개한 방법 중 enum 빼고 모두 깨집니다.

 

근데 다른 방법들도 안 통하게 할 수 있는데 readResolve 메소드를 정의하고 기존 instance를 리턴해주는 방법으로 로 해결할 수 있다.

 

Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = null;

try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("singleton.obj"))) { 
	out.writeObject(singleton1);
}
try (ObjectInput in = new ObjectInputStream(new FileInputStream("singleton.obj"))) { 
	singleton2 = (Singleton) in.readObject();
}

System.out.println(singleton1 == singleton2);

자바, 스프링에 싱글톤이 적용된 곳

 

1. Runtime()

2. 빈의 싱글톤 스코프 (구현 방식이 좀 다르다 여기에 정리가 잘 되어있다.)

3. 다른 패턴들

 

 

생성 패턴 중 하나인 싱글톤 패턴에 대해 알아봤습니다. 다음 포스팅에선 팩토리 메소드 패턴을 알아보겠습니다.

 

고생하셨습니다.

반응형

댓글