CS 지식중 디자인패턴에 대해서 공부하면서 각 패턴에 대해서 정리하는 시간을 가지고자 합니다.
먼저 싱글톤 패턴을 정리해보겠습니다.
싱글톤 패턴은 무엇인가?
- 싱글톤 패턴은 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴입니다.
- 보통 데이터베이스 연결에 많이 사용되는 패턴입니다.
자바에서의 싱글톤 패턴을 예로 들자면 아래와 같은 로직이 나옵니다.
class Singleton {
private static class singleInstanceHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return singleInstanceHolder.INSTANCE;
}
}
싱글톤 패턴은 왜 사용할까?
- 메모리 절약을 위해, 인스턴스를 재생성하지 않고 기존의 인스턴스를 활용할 수 있기 때문
- 우리가 각 함수에 같은 변수를 선언하는 지역변수를 사용하지 않고, 전역변수를 사용하여 모든 함수에 참조할 수 있게 하는 의미와 비슷하다.
- 특히나 싱글톤 패턴이 필요한 경우는 리소스를 많이 차지하는 경우에 어울린다고 볼 수 있다.
- 위에서 언급한 데이터베이스 연결 모듈이나, 네트워크 통신, 캐시, 로그 등이 있을 수 있다.
싱글톤 패턴의 단점
물론 좋은점만 있는것은 아니다.
특히나, TDD(Test Driven Development)를 수행할때 문제가 생길 수 있다.
보통 TDD의 경우에는 단위테스를 수행하게 되는데, 단위테스트는 각 테스트들이 독립적으로 흘러가야 하는데, 동일한 인스턴스로 테스트를 진행하면 각 테스트마다 독립적인 인스턴스를 생성하기 어렵다.
TDD란?
Test Driven Development의 약자로 '테스트 주도 개발' 이라고 한다.
짧은 개발 주기의 반복에 의존하는 개발 프로세스이며, eXtream Programming(XP)의 'Test-First' 개념에 기반을 둔 단순한 설계를 중요시한다.
대표적으로 TDD Tool 중에 하나로 JUnit을 뽑을 수 있다.
싱글톤 패턴의 단점 해결
의존성 주입 (DI)
갑자기 의존성 주입이 왜 나오냐는 질문이 생길수있다.
하지만 싱글톤패턴은 사용하기 쉽고 실용적이지만, 모듈 간의 결합을 강하게 만들 수 있다는 단점이 있다.
이럴 때, 의존성 주입 (DI, Dependency Inject)을 통해 모듈간의 겹합을 좀 느슨하게 만들 수 있다.
의존성이란?
의존성이란 종속성이라도 하고, 만약 A가 B에 의존성이 있다는 것은 B의 변경 사항에 대해 A도 변해야 한다는 의미다.
의존성 주입에 대해서는 이후에 정리된 글을 업데이트하겠다.
싱글톤 패턴 구현
- Eager Initialization
- Static block initialization
- Lazy initialization
- Thread safe initialization
- Double-Checked Locking
- Bill Pugh Solution (권장👍)
- Enum 이용 (권장👍)
위의 7가지의 방법으로 싱글톤 패턴이 구현 가능하다.
더 자세한 내용은 맨 하단의 참조 해 놓았지만 이분의 글을 많이 참조하였으니 방문 해서 더 깊게 보시는것을 추천한다.
💠 싱글톤(Singleton) 패턴 - 꼼꼼하게 알아보자
Singleton Pattern 싱글톤 패턴은 디자인 패턴들 중에서 가장 개념적으로 간단한 패턴이다. 하지만 간단한 만큼 이 패턴에 대해 코드만 던져주고 끝내버리는 경우가 있어, 어디에 쓰이는지 어떠한 문
inpa.tistory.com
Eager Initialization
한번만 미리 만들어두는 가장 간편하고 직관적인 기법이다.
class Singleton {
// 싱글톤
private static final Singleton INSTANCE = new Singleton();
// 외부에서 접근할 수 없게 private 선언
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
Static block initialization
Static Block
클래스가 로딩되고 클래스 변수가 준비된 후 자동으로 실행되는 블록
class Singleton {
// 싱글톤
private static Singleton instance;
// 외부에서 접근할 수 없게 private 선언
private Singleton() {}
// static block && 예외처리
static {
try {
instance = new Singleton();
} catch (Exception e) {
throw new RuntimeException("싱글톤 객체 생성 오류");
}
}
public static Singleton getInstance() {
return instance;
}
}
Lazy initialization
메서드를 호출했을 때 인스턴스 변수의 Null 유무에 따라 초기화 또는 기존것을 반환하는 기법
class Singleton {
// 싱글톤
private static Singleton instance;
// 외부에서 접근할 수 없게 private 선언
private Singleton() {}
// 외부에서 호출할시 생성
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Thread safe initialization
synchronized
를 통해 Thread 접근을 하나하나씩 설정 (동기화)
class Singleton {
private static Singleton instance;
private Singleton() {}
// synchronized
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Double-Checked Locking
매번 synchronized
동기화가 문제라면, 최초 초기화할때만 적용하고 이미 만들어진 반환할때는 사용하지 않도록 하는 기법
class Singleton {
private static volatile Singleton instance; // volatile ??
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
// 동기화
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton(); // 최초 초기화만 동기화 작업이 일어나서 리소스 낭비를 최소화
}
}
}
return instance; // 최초 초기화가 되면 앞으로 생성된 인스턴스만 반환
}
}
// Java에서는 Thread를 여러개 사용할 경우, 각각의 Thread들은 변수를 캐시메모리에서 가져온다.
// votile 키워드를 통해 캐시메모리가 아닌 메인메모리에서 접근하도록 설정한다.
Bill Pugh Solution (LazyHolder) 👍👍
매우 권장되는 방법이다.
static 내부 클래스를 이용하여 getInstance 메소드가 호출되어야 인스턴스가 생성된다.
final을 사용하여 INSTANCE를 더이상 선언하지 않도록 막는다.
class Singleton {
private Singleton() {}
private static class SingleInstanceHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingleInstanceHolder.INSTANCE;
}
}
Enum 이용 👍👍
매우 권장되는 방법이다.
enum은 멤버를 만들때 private로 만들고 한번만 초기화하기 때문에 Thread Safe
enum SingletonEnum {
INSTANCE;
private final Client dbClient;
SingletonEnum() {
dbClient = Database.getClient();
}
public static SingletonEnum getInstance() {
return INSTANCE;
}
public Client getClient() {
return dbClient;
}
}
public class Main {
public static void main(String[] args) {
SingletonEnum singleton = SingletonEnum.getInstance();
singleton.getClient();
}
}
마무리
프레임워크를 사용한다면 이러한 패턴은 드물겠지만,
프레임워크 없이 개발을 진행한다고 보았을때, 한번쯤은 적용해볼만한 패턴이 아닌가 싶다.
물론 각 구현방식마다 장단점이 있기때문에, 상황에 맞게 잘 사용하면 좋을 것 같다. 😎