이번 글에서는 디자인 패턴의 종류 즁 하나인 싱글톤 패턴에 대해서 알아보자.
싱글톤 패턴은 디자인 패턴 중 가장 유명한 패턴 중 하나로, OOP를 공부한 사람이라면 알고 있는 패턴이다.
하지만 정작 사용하려고 하면 어슬프게 적용을 하거나 왜 써야하는지 모르는 경우가 많은 것 같다. 이번 글을 통해서 디자인 패턴 관점에서 개념과 역할에 대해 살펴보고, Java를 통해 예시 코드까지 작성해보자.
생성 패턴은 객체의 생성에 관련된 패턴으로 인스턴스의 생성 절차를 추상화하는 패턴이다. 객체를 생성, 합성, 표현 방법을 시스템과 분리한다.
생성 패턴은 다음과 같은 2가지 특징이 있다.
1. 생성 패턴은 시스템이 어떤 구체 클래스(concrete class)를 사용하는지에 대한 정보를 캡슐화한다.
2. 생성 패턴은 이들 클래스의 인스턴스들이 어떻게 만들고 어떻게 결합하는지에 대한 부분을 완전히 가려준다.
즉, 생성 패턴을 이용하면 객체의 생성을 캡술화 하여, 무엇이 생성되는지, 어떻게 생성되는지, 언제 생성되는지, 누가 생성한건지를 가려줌으로써 유연성을 확보하는 것이다.
싱글톤 패턴은 어떤 클래스의 인스턴스가 오직 하나임을 보장하며, 이 인스턴스에 접근할 수 있는 전역적인 접촉점을 제공하는 패턴이다. 즉, 어플리케이션의 시작부터 끝까지 특정 클래스의 인스턴스는 메모리 상 단 한개만 존재하고 이 인스턴스에 대해 어디서나 접근할 수 있도록 하는 패턴이다.
어플리케이션을 개발하다보면 특정 클래스의 인스턴스가 하나만 있는게 좋을 때가 있다.
예를 들어 설정이라는 클래스가 있다고 가정하자. 사용자가 설정을 변경할 때마다 설정 인스턴스가 생성이 되면 전역적인 성질을 가지고 있는 설정이라는 객체의 의미가 없어지게 된다.
싱글톤 패턴을 구현하는 방법은 꽤 다양하다.
이 방법들은 다음과 같은 공통점이 있다.
이제 싱글톤 패턴을 구현하는 방법에 대해 살펴보자.
class Singleton {
private final static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
Eager Initialization은 클래스의 인스턴스를 로딩 시점에서 생성하는 방법이다. 하지만 이 방법은 인스턴스 1개를 무조건 생성하기 때문에 만약 해당 클래스를 사용하지 않는다면 리소스가 낭비될 수 있다.
class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
이 방식은 어플리케이션 내에서 처음 getInstance가 호출될 때 인스턴스가 생성이 되고 그 이후로는 그 인스턴스를 재사용하는 방식이다. 1번 방법의 단점을 보안하는 방법이다.
하지만 이 방법은 multi-thread 환경에서 동기화 문제가 있을 수 있다. 만약 인스턴스가 생성되지 않은 시점에서 여러 쓰레드가 동시에 getInstance()를 호출한다면 인스턴스가 여러개가 생길 수 있다.
그렇기 때문에 이 방법은 single-thread 환경이 보장 되었을 때 사용하는 방법이다.
class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Thread Safe Singleton은 2번 방법의 문제를 해결하기 위한 방법이다. synchronized 키워드는 임계 영역(Critical Section)을 형성해 해당 영역에 오직 하나의 쓰레드만 접근 가능하게 락을 걸어준다. 그래서 멀티 스레드 환경에서도 싱글톤이 정상 작동하게 된다.
하지만 synchronized 키워드 자체에 대한 비용이 크기 때문에 getInstance를 자주 호출하게 되면 성능이 떨어지게 된다.
class Singleton {
private Singleton() {}
private static class singletonHelper {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return singletonHelper.INSTANCE;
}
}
이 방식은 앞선 문제들을 대부분 해결한 방식으로, 가장 많이 쓰이는 싱글톤 구현 방법이다. inner class를 이용하여 static final 키워드를 통해 싱글톤을 구현한 방법이다.
singletonHelper는 로드 될 때 생성되지 않고 getInstance로 호출 되었을 때 메모리에 로딩 되고, 인스턴스를 생성한다. 그리고 synchronize 키워드를 사용하지 않아 많이 호출되어도 성능 부담이 없다.
첫 번째 스레드가 getInstance() 메서드를 호출하면 JVM은 singletonHelper 클래스를 로드하게 된다.
이미 메모리에는 올라가 있으니, JVM은 이것을 한 번만 로드한다.
이때 중요한 것은 두 번째 스레드가 geInstance()를 호출하더라도, JVM은 두 번 로드하지 않고 첫번째 로드가 끝나고 초기화가 완료될 때까지 기다리게 된다.
지금까지 싱글톤 패턴이 무엇인지, 어떻게 구현하는지 알아봤다.
싱글톤 패턴의 특징을 정확히 알고 프로젝트에 적용해보자!