[Java/개발자면접] 디자인패턴 '싱글턴' (싱글턴 구현 방식, 주의점, 단점)
1. 싱글턴이란?
싱글톤(Singleton) 패턴은 어떤 클래스의 인스턴스가 오직 하나임을 보장하며, 이 인스턴스에 접근할 수 있는 전역적인 접촉점을 제공하는 패턴입니다. 즉, 프로그램 시작부터 종료 시까지 어떤 클래스의 인스턴스가 메모리 상에 단 하나만 존재할 수 있게 하고, 이 인스턴스에 대해 어디에서나 접근할 수 있도록 하는 패턴입니다.
개발을 하다보면 어떤 클래스에 대해 단 하나의 인스턴스만을 갖도록 하는 것이 좋은 경우가 있습니다. 예를 들어, 로그를 찍는(Logging) 객체라던가 쓰레드 풀, 윈도우 관리자 등 여러 객체를 관리하는 역할의 객체는 프로그램 내에서 단 하나의 인스턴스를 갖는 것이 바람직합니다.
어떻게 접근할 수 있나?
전역변수? 틀린 말은 아니지만.. 더 좋은 방법은 클래스 자신이 자기의 유일한 인스턴스로 접근하는 방법을 자체적으로 관리하는 것입니다. 생성자를 private하게 만들어서 클래스 외부에서는 인스턴스를 생성하지 못하게 차단하고, 내부에서 단 하나의 인스턴스를 생성하여 외부에는 그 인스턴스에 대한 접근 방법을 제공할 수 있습니다.
싱글톤 패턴(Singleton)과 정적 클래스(Static) 차이
2. 싱글톤(Singleton) 패턴을 구현하는 6가지 방법
참고: https://readystory.tistory.com/116
방법들의 공통적 특징:
- private 생성자만을 정의해 외부 클래스로부터 인스턴스 생성을 차단.
- 싱글톤을 구현하고자 하는 클래스 내부에 멤버 변수로써 private static 객체 변수를 만듬.
- public static 메소드를 통해 외부에서 싱글톤 인스턴스에 접근할 수 있도록 접점을 제공.
전부 구현할 줄 알아야 한다기 보다는 한번 읽어서 이해하고, 싱글톤의 핵심과 한계 정도만 알아두면 좋을 것 같다.
(1) Eager Initialization (낭비 & Exception에 대한 Handling x)
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
싱글톤 클래스의 인스턴스를 클래스 로딩 단계에서 생성하는 방법.
이 방법을 사용할 때는 싱글톤 클래스가 다소 적은 리소스를 다룰 때여야 합니다. File System, Database Connection 등 큰 리소스들을 다루는 싱글톤을 구현할 때는 위와 같은 방식보다는 getInstance() 메소드가 호출될 때까지 싱글톤 인스턴스를 생성하지 않는 것이 더 좋습니다. 게다가 Eager Initializaion은 Exception에 대한 Handling도 제공하지 않습니다.
(2) Static Block Initialization (낭비)
public class Singleton {
private static Singleton instance;
private Singleton(){}
static{
try{
instance = new Singleton();
}catch(Exception e){
throw new RuntimeException("Exception occured in creating singleton instance");
}
}
public static Singleton getInstance(){
return instance;
}
}
방식 1에서 exception handling은 제공되었지만 여전히 낭비..
(3) Lazy Initialization (나중에 초기화하는 방법)
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
이는 global access 한 getInstance() 메소드를 호출할 때에 인스턴스가 없다면 생성하는 방식 입니다.
그러나 multi-thread 환경에서 동기화 문제가 있습니다. 만약 인스턴스가 생성되지 않은 시점에서 여러 쓰레드가 동시에 getInstance()를 호출한다면 예상치 못한 결과를 얻을 수 있을뿐더러, 단 하나의 인스턴스를 생성한다는 싱글톤 패턴에 위반하는 문제점이 야기될 수 있습니다. 그래서 이 방법으로 구현을 해도 괜찮은 경우는 single-thread 환경이 보장됐을 때입니다.
(4) Thread Safe Singleton
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
3번의 문제를 해결하기 위한 방법으로, getInstance() 메소드에 synchronized를 걸어두는 방식입니다. synchronized 키워드는 임계 영역(Critical Section)을 형성해 해당 영역에 오직 하나의 쓰레드만 접근 가능하게 해 오직 하나의 스레드만 접근 가능하게 해 줍니다. 그러나 synchronized 키워드 자체에 대한 비용이 크기 때문에 싱글톤 인스턴스 호출이 잦은 어플리케이션에서는 성능이 떨어지게 됩니다.
(5) Bill Pugh Singleton
public class Singleton {
private Singleton(){}
private static class SingletonHelper{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return SingletonHelper.INSTANCE;
}
}
Implementaion private inner static class를 두어 싱글톤 인스턴스를 갖게 합니다. 이때 1번이나 2번 방식과의 차이점이라면 SingletonHelper 클래스는 Singleton 클래스가 Load 될 때에도 Load 되지 않다가 getInstance()가 호출됐을 때 비로소 JVM 메모리에 로드되고, 인스턴스를 생성하게 됩니다. 아울러 synchronized를 사용하지 않기 때문에 4번에서 문제가 되었던 성능 저하 또한 해결됩니다.
(6) Enum Singleton
public enum EnumSingleton {
INSTANCE;
public static void doSomething(){
~ do something ~
}
}
앞서 1~5번에서 살펴본 싱글톤 방식은 사실 완전히 안전할 수 없습니다. 왜냐하면 Java의 Reflection을 통해서 싱글톤을 파괴할 수 있기 때문입니다.
그러나 이 방법 또한 1, 2번과 같이 사용하지 않았을 경우의 메모리 문제를 해결하지 못한 것과 유연성이 떨어진다는 면에서의 한계를 지니고 있습니다.
3. 싱글톤 단점
- 코드 자체가 많아짐
- 의존 관계상 클라이언트가 구체 클래스 의존 (DIP, OCP 위반)
- 유연성이 떨어짐
→ 스프링은 '스프링 컨테이너'로 이를 해결해줌
4. 싱글톤 방식 주의점
- 객체 인스턴스를 한개 만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다.
- 가급적 읽기만 가능해야 한다.
- 필드 대신 자바에서 공유되지 않는 지역변수나 스레드풀 등을 사용해야 한다.
자, 그런데 스프링 싱글톤 패턴 반면 스프링 싱글톤은 클래스 자체에 의해서가 아니라 '스프링 컨테이너(Bean Factory/Application Context)'에 의해 구현된다. 싱글턴 패턴과 어떤 차이가 있을까?