05. 싱글턴 패턴 - 하나뿐인 특별한 객체 만들기

솔트커피·2022년 7월 11일
0

하나만 있어도 충분한 객체

  • 스레드 풀
  • 캐시
  • 대화 상자
  • 사용자 설정
  • 레지스트리 설정 처리 객체
  • 로그 기록용 객체
  • 디바이스 드라이버

고전적인 싱글턴 패턴 구현법 (p207)

// NOTE: This is not thread safe!

public class Singleton {
	private static Singleton uniqueInstance;
 
	private Singleton() {}
 
	public static Singleton getInstance() {
		if (uniqueInstance == null) {
			uniqueInstance = new Singleton();
		}
		return uniqueInstance;
	}
 
	// other useful methods here
	public String getDescription() {
		return "I'm a classic Singleton!";
	}
}

패턴 집중 인터뷰 (p208)

  • 한 애플리케이션에 들어있는 어떤 객체에서도 같은 자원을 활용 가능
  • 연결 풀, 스레드 풀과 같은 자원 풀 관리에 적절함
  • 객체 인스턴스가 여러 개 생겨서 의도치 않은 버그를 맞닥뜨릴 가능성을 없애줌
  • 생성자는 private
    • 싱글턴 객체가 필요할 때는 getInstance()로 인스턴스를 달라고 요청해야 함
    • 이미 만들어져 있는 상태에서 한 객체를 도와주다가 호출을 받고 다른 객체에게 불려간다

싱글턴 패턴의 정의 (p211)

싱글턴 패턴(Singleton Pattern)은 클래스 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공합니다.

  • 클래스에서 하나뿐인 인스턴스를 관리하도록 만들면 됨
  • 그리고 다른 어떤 클래스에서도 자신의 인스턴스를 추가로 만들지 못하게 해야함
  • 어디서든 인스턴스에 접근할 수 있도록 전역 접근 지점을 제공함
  • 요청이 들어오면 하나뿐인 인스턴스를 건네주도록 만들어야 함

멀티스레딩 문제 살펴보기 (p213)

ChocolateBoiler biler = ChocolateBoiler.getInstance();
boiler.fill();
boiler.boil();
boiler.dtain();

2개의 스레드에서 이 코드를 실행한다고 가정해 보고, 두 스레드가 다른 보일러 객체를 사용하게 될 가능성은 없는지 따져 봅시다.

정답

멀티스레딩 문제 해결하기

getInstance()를 동기화 하면 멀티스레딩과 관련된 문제가 간단하게 해결됩니다.

// NOTE: This is not thread safe!

public class Singleton {

	...
 
	public static synchronized Singleton getInstance() {
		if (uniqueInstance == null) {
			uniqueInstance = new Singleton();
		}
		return uniqueInstance;
	}
 
	...
}
  • 이러면 문제가 해결되긴 하겠지만, 동기화할 때 속도 문제가 생기지 않나요?
    • 사실 동기화가 꼭 필요한 시점은 이 메소드가 시작되는 때 뿐이라는 사실을 알 수 있습니다.
    • 일단 unuqueInstance 변수에 Singleton 인스턴스를 대입하면 굳이 이 메소드를 동기화 된 상태로 유지할 필요가 없습니다.
    • 처음을 제외하면 동기화는 불필요한 오버헤드만 증가시킬 뿐입니다.

더 효율적인 해결 방법 (p215)

방법 1. getInstance()의 속도가 그리 중요하지 않다면 그냥 둡니다

다만 메소드를 동기화하면 성능이 100배 정도 저하된다는 사실만은 기억해둡시다.

방법 2. 인스턴스가 필요할 때는 생성하지 말고 처음부터 만듭니다.

public class Singleton {
    // 정적 초기화 부분에서 Singleton의 인스턴스를 생성합니다. 이러면 스레드를 
	private static Singleton uniqueInstance = new Singleton()
 
	private Singleton() {}
 
	public static Singleton getInstance() {
		return uniqueInstance;
	}
}
  • 클래스가 로딩될 때 JVM에서 Singleton의 하나뿐인 인스턴스를 생성해 줍니다.
  • 하나뿐인 인스턴스를 생성하기 전까지 그 어떤 스레드도 uniqueInstance 정적 변수에 접근할 수 없습니다.

방법 3. DCL(Double-Checked Locking)을 써서 getInstance()에서 동기화되는 부분을 줄입니다.

DCL을 사용하면 인스턴스가 생성되어 있는지 확인한 다음 생성되어 있지 않았을 때만 동기화할 수 있습니다.
이러면 처음에만 동기화 하고 나중에는 동기화하지 않아도 됩니다.

// java 1.4 이전 버전에서는 사용 불가

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

Q&A (p218)

Q2. 모든 메소드와 변수가 static으로 선언된 클래스를 만들어도 되지 않나요?

맞습니다. 하지만 필요한 내용이 클래스에 다 들어있고, 복잡한 초기화가 필요 없는 경우에만 그 방법을 쓸 수 있습니다.
다만 자바에서 정적 초기화를 처리하는 방법 때문에 일이 복잡해질 수도 있고 여러 클래스가 얽혀 있다면 꽤 지저분해집니다.
초기화 순서 문제로 찾아내기 어려운 버그가 생길 수도 있으니 특별한 이유가 없다면 쓰지 맙시다.

Q3. 클래스 로더 2개가 각기 다른 싱글턴 인스턴스를 가지게 될 수도 있지 않나요?

클래스 로더마다 서로 다른 네임스페이스를 정의하기에 2개 이상이라면 같은 클래스를 여러번 로딩할 수도 있습니다.
따라서 클래스 로더가 여러 개라면 싱글턴을 조심해서 사용해야 합니다.

Q4. 리플렉션, 직렬화, 역직렬화 문제도 있지 않나요?

역시 싱글턴에서 문제가 될 수 있으므로 염두에 둘 필요가 있습니다.

Q5. 싱글턴은 느슨한 결합 원칙에 위배되지 않나요? 싱글턴에 의존하는 객체는 전부 하나의 객체에 단단하게 결합되잖아요.

맞습니다. 싱글턴을 바꾸면 연결된 모든 객체도 바꿔야 할 가능성이 높으니까요.

Q6. 한 클래스는 한 가지 일만 해야한다고 배웠는데, 싱글턴을 사용하면 이를 위반하는 것 아닌가요?

맞습니다. 싱글턴은 자신의 인스턴스를 관리하는 일 외에도 원래 그 인스턴스를 사용하고자 하는 목적에 부합하는 작업을 책임져야 합니다.
따라서 2가지를 책임지고 있다고 말할 수도 있습니다. 하지만 클래스 내에 인스턴스 관리 기능을 포함한 클래스를 적지 않게 볼 수 있습니다. 그러면 전체적인 디자인을 더 간단하게 만들 수 있기 때문입니다.

Q8. 아직 전역 변수가 싱글턴보다 나쁜 이유를 잘 모르겠어요.

전역 변수는 게으른 인스턴스 생성을 할 수 없고, 처음부터 끝까지 인스턴스를 가지고 있어야 한다는 단점이 있습니다.
또한 간단한 객체의 전역 레퍼런스를 자꾸 만들게 되어 네임스페이스 역시 지저분하게 만들어집니다. 싱글턴은 이런 경우는 거의 없습니다.

enum으로 싱글턴의 문제점 해결하기(p219)

지금까지 논의한 동기화 문제, 클래스 로딩 문제, 리플렉션, 직렬화와 역직렬화 문제 등은 enum으로 싱글턴을 생성해서 해결 할 수 있습니다.

public enum Singleton {
	UNIQUE_INSTANCE;
    // 기타 필요한 필드
}

public class SingletonClient {
	public static void main(String[] args) {
		Singleton singleton = Singleton.UNIQUE_INSTANCE;
		System.out.println(singleton.getDescription());
	}
}

이제 싱글턴이 필요할 때면 바로 enum을 쓰면 됩니다.

핵심 정리 (p22)

  • 객체지향 원칙

    • 바뀌는 부분은 캡슐화한다.
    • 상속보다는 구성을 활용한다.
    • 구현보다는 인터페이스에 맞춰서 프로그래밍 한다.
    • 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.
    • 클래스는 확장에는 열려있어야 하지만 변경에는 닫혀있어야 한다.(OCP)
    • 추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.
  • 싱글턴 패턴

    • 클래스 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공합니다.

0개의 댓글