[디자인 패턴] 싱글톤 패턴

문돌이 개발자·2023년 2월 25일

싱글톤 패턴이란

application 전역에서 사용되는 클래스에 대해 필요할 때마다 객체를 생성하는 것이 아니라 한번 객체를 생성하면 해당 객체를 계속 반환하여 사용하기 위해 사용하는 패턴이다.

public class MySingleton {
    private static MySingleton instance = new MySingleton();

    private MySingleton(){}

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

구현 예시는 위와 같으며 생성자를 private으로 만들어 외부에서의 객체 생성을 막는다. 클래스 변수로 해당 객체를 생성하고 할당한다 이후 getInstance() 메소드를 통해 계속 같은 객체를 반환한다.

싱글톤 패턴의 장점

하나의 instance만 존재하는 클래스의 경우 객체를 다시 생성하는 것은 비효율적이다. 즉, 싱글톤 패턴을 적용하는 것이 메모리 관리 측면에서 효율적이다.

계속 다시 생성해서 사용하지 않고 하나의 클래스에서 instance를 관리하기 때문에 코드를 읽고 관리하기가 쉬워진다. 유지, 보수에 있어 이점을 가진다.

ex) 네트워크 통신을 하는 Retrofit 객체는 application 전역에서 사용될 수 있고 하나의 객체만 필요하므로 싱글톤 패턴을 적용하기에 좋다.

싱글톤 패턴의 단점

application 전역에서 사용되기 때문에 싱글톤이 적용되는 코드부분을 개별 테스트하기에 어려움이 있을 수 있다.

싱글톤 패턴은 하나의 instance만을 사용하므로 이에 의존하는 코드가 많아질수록 이에 대한 수정은 다른 많은 코드의 수정을 동반하게 된다. 즉 유지,보수에 어려움이 생길 수 있다.

싱글톤 패턴을 적용한 클래스에 내부적으로 관리해야하는 상태가 있는 경우 멀티스레드 환경에서 문제가 생길 수 있다. synchronized를 통해 이를 관리해야 한다.

싱글톤 패턴을 구현하는 6가지 방법

Eager Initialization

class MySingleton {
	private static MySingleton instance = new Singleton(); 
	
	private MySingleton () {}
	
	public static MySingleton getInstance() {
		return instance;
	}
}

싱글톤 패턴을 구현하는 가장 기본적인 방법으로 static을 통해 클래스가 jvm에 의해 로드될 때 객체를 생성한다.

하지만 이 방법은 객체를 사용하지 않더라도 객체가 무조건 생성되기 때문에 자원의 비효율성을 야기한다. 또한 Exception에 대한 처리를 하지 않는다.

Static Block Initialization

class MySingleton {
    private static MySingleton instance ;

    private MySingleton() {
    }

    static {
        try {
            instance = new MySingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }

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

Eager Initialization 방법과 비슷하지만 Static Block을 사용하여 Exception 처리를 해준다.

static Block은 초기화 블록(Initialization Block)이라고 불리며 클래스가 처음 로딩될 때 한 번만 수행된다.

하지만 이 방법도 클래스 로딩 단계에서 객체를 생성하기 때문에 자원의 비효율성을 야기한다.

Lazy Initialization

class MySingleton {
    private static MySingleton instance ;

    private MySingleton() {}

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

getInstance() 메소드를 호출하여 객체가 존재하지 않으면 새로운 객체를 하나 생성하고, 존재하면 기존 객체를 반환한다.

필요할 때만 객체를 생성하여 반환하므로 위의 두 방법에 비해 메모리 관리에 있어 효율적이다.

하지만 멀티스레드 환경에서 여러 스레드에 의해 getInstance() 메소드가 호출된다면 동시성 문제가 발생하여 여러개의 객체가 생성될 수 있다.

Thread safe singleton

class MySingleton {
    private static MySingleton instance ;

    private MySingleton() {}

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

    private MySingleton() {
    }

    public static MySingleton getInstance() {
        if (instance == null)
//이 지점
            synchronized (MySingleton.class) {
                if (instance == null) {
                    instance = new MySingleton();
                }
            }
        return instance;
    }
}

Lazy Initialization의 동시성 문제를 해결하기 위해서 synchronized 키워드를 통해 어느 한 시점에 하나의 스레드만 getInstance() 메소드를 실행할 수 있게 한다. 이를 통해 여러개의 객체가 생성될 수 있는 경우를 방지한다.

하지만 synchronized 키워드는 키워드 자체 비용이 크기 때문에 해당 객체를 자주 사용하여 getInstance()를 자주 호출하게 되면 성능이 떨어지게 된다. 이를 방지하기 위해 아래 코드와 같이getInstance() 메소드가 아닌 객체를 생성해야 하는 경우에만 synchronized를 사용하는 것이 좋다.

  • null체크를 두번하는 이유: 널체크를 하고 synchronized를 통해 바로 객체를 생성하게 되면 주석처리된 지점에 여러 스레드가 도달한 경우 null 체크는 이미 된 상태에서 객체 생성만 순차적으로 여러개 하기 때문에 객체 생성 직전에 null체크를 한번 더 해주어야 한다.

Bill Pugh Singleton Implementaion

class MySingleton {
    private MySingleton() {
    }

    private static class SingletonHelper {
        private static final MySingleton INSTANCE = new MySingleton();
    }

    public static MySingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

앞선 방식이 안고 있는 문제점들을 대부분 해결한 방식으로, 현재 가장 널리 쓰이는 싱글톤 구현 방법이다.

SingletonHelper클래스는 MySingleton클래스가 jvm에 의해 로드될 때 객체를 생성하지 않기 때문에 Eager Initialization이나 Static Block Initialization와 같이 메모리상의 비효율성이 존재하지 않으며 Synchronized 키워드도 사용하지 않기 때문에 동시성의 문제도 존재하지 않는다.

하지만 이 방법을 포함한 이전의 방법들 모두 Reflection에 의해 싱글톤의 정의가 깨질 수 있어 Joshua Bloch는 Enum으로 싱글톤을 구현하는 방법을 제안했다.

Enum Singleton

public enum EnumSingleton {
 
    INSTANCE;
    
    public static void doSomething(){}
}

Enum Singleton은 스레드에 안전하고 또한 Reflection에서도 안정성을 보장하지만 역시 객체를 사용하기 전에 메모리에 할당하므로 메모리 활용 관점에서 비효율적이다.

참고:

[Java] 싱글톤 패턴(Singleton Pattern) 구현 방법

[생성 패턴] 싱글톤(Singleton) 패턴을 구현하는 6가지 방법

[디자인 패턴] 싱글톤 패턴

profile
까먹고 다시 보려고 남기는 기록

0개의 댓글