들어가며...
'싱글턴'이란 단어는 개발하면서 심심치 않게 듣는다. 아마 디자인 패턴 중에서 가장 먼저 접하는 패턴이지 않을까 싶다. 그래서 이번 포스팅에서는 먼저 '이펙티브 자바에서의 싱글턴'을 살펴보면서 본 포스팅의 주목적을 달성할 것이다. 다음으로는 GoF 디자인 패턴에서의 싱글턴을 통해 Lazy Initialization과 Double Checked Locking 1 방법을 통해 싱글턴을 바라보는 방법을 터득하고, 마지막으로 Spring Framework에서의 싱글턴을 정리하면서 프레임워크에서는 싱글턴을 어떻게 활용하는지 살펴볼 예정이다. 2
요약 ::
a. 이펙티브 자바에서의 싱글턴
b. GoF 디자인 패턴에서의 싱글턴
c. Spring Framework에서의 싱글턴
순서로 진행한다.
이펙티브 자바에서의 싱글턴
책에서는 싱글턴 패턴에 대해 아래와 같이 정의한다.
애플리케이션 전반에 걸쳐 단 하나의 인스턴스만 생성하는 클래스.
하나의 인스턴스만 생성될 수 있다면, 그 클래스는 '싱글턴이라고 하자' 라고 정의하는 것이다. 책에서 소개하는 구현 방법은 크게 세 가지이며, 편의상 예제 1, 예제 2, 예제 3이라고 칭하겠다. 예제 코드를 보자.
예제 1. public static field를 이용한 싱글턴 구현
public class FinalFieldSingleton {
public static final FinalFieldSingleton INSTANCE = new FinalFieldSingleton();
private FinalFieldSingleton() {}
public String getHello() {
return "Hello FinalFieldSingleton!";
}
}
예제 2. static factory method를 이용한 싱글턴 구현
public class StaticFactoryMethodSingleton {
private static final StaticFactoryMethodSingleton instance = new StaticFactoryMethodSingleton();
private StaticFactoryMethodSingleton() {}
public static StaticFactoryMethodSingleton getInstance() {
return instance;
}
public String getHello() {
return "Hello StaticFactoryMethodSingleton!";
}
}
예제 3. enum을 이용한 싱글턴 구현
public enum EnumSingleton {
INSTANCE;
public String getHello() {
return "Hello EnumSingleton!";
}
}
공통점을 살펴보면 세 예제 모두 Eager initailization을 사용한다. 그럼 차이점을 하나씩 살펴보자. 3
먼저 예제 1번은 private 생성자로 인스턴스 생성을 막고, public static field에 생성해 둔 하나의 인스턴스를 통해 요청을 처리한다.
단점으로 private 생성자를 리플렉션 API의 AccessibleObject.setAccessble를 사용하면 private 생성자를 호출할 수 있어 싱글턴을 보장하지 못할 수 있다.
예제 2번은 필드는 private으로 생성하고 이를 돌려주는 static factory method를 만들었다. '예제 1번과 별 차이가 없는데...?'라고 생각했다면 앞으로 쏟아질 싱글턴의 홍수 속에서 살아남을 마음가짐이 된 상태다.
static factory method는 언제든 싱글턴이 아닌 매번 새로운 객체(new)를 생성하도록 변경할 수 있다. getInstance()의 반환값에서 새로운 객체만 생성하는 방식으로 변경할 수 있다. 반면 예제 1번은 이와 같이 변하려면 클라이언트 코드 역시 변경이 필요하다. (책에서는 제네릭 싱글턴 팩토리로 만들수 있다는 점과 Supplier로 호출하는 방식에 대한 설명도 있지만 중요하지 않아 언급만 하고 생략한다..)
마찬가지로 리플렉션 API를 사용하여 싱글턴이 보장되지 못하는 것이 단점이 된다.
예제 1과 예제 2는 직렬화에 대해서도 자유롭지 못하다. 역직렬화 시 제공되는 숨겨진 메서드인 readResolve()를 통해 새로운 객체를 반환받을 수 있기 때문이다. 따라서 Overriding을 통해 생성된 객체를 반환하도록 수정해줘야하는 번거로움이 있다.
마지막으로 예제 3번은 enum을 사용한다. 책에 따르면 가장 권장되는 방식이다. 간결하며, 리플렉션을 통한 공격도 방어할 수 있고, 별다른 노력 없이 직렬화까지 구현된다. 유일한 단점으로 'enum 외 다른 클래스를 상속받아야 하는 경우'에는 사용할 수 없다.
요약 ::
a. 이펙티브 자바에서는 Eager initailization방식으로 싱글턴 구현방법을 서술한다.
b. 예제 1, 2는 직렬화 및 리플렉션 API의 단점이 있어 예제 3번의 구현을 권장한다.
c. 여러분은 어떤 방법을 선택할 것인가?
GoF 디자인 패턴에서의 싱글턴
이 책에서는 싱글턴을 성능과 멀티스레드 환경을 중심으로 구현할 수 있게 안내한다. 두 예제를 보자.
예제 4. Lazy initialization & synchronized를 활용한 싱글턴 구현
public class ModernSingleton2 {
private static ModernSingleton2 uniqueInstance;
private ModernSingleton2() {}
public static synchronized ModernSingleton2 getInstance() {
if( uniqueInstance == null ) {
uniqueInstance = new ModernSingleton2();
}
return uniqueInstance;
}
}
예제 5. Double Checked Locking을 통해 멀티스레드에 안전한 싱글턴 구현
public class ModernSingleton3 {
private volatile static ModernSingleton3 uniqueInstance;
private ModernSingleton3() {}
public static ModernSingleton3 getInstance() {
if( uniqueInstance == null ) {
synchronized (ModernSingleton3.class) {
if( uniqueInstance == null ) {
uniqueInstance = new ModernSingleton3();
}
}
}
return uniqueInstance;
}
}
예제 4번은 멀티스레드 환경에서 싱글턴 보장이 위배되지 않도록 syncronized 키워드를 사용했다. 그러면서 동시에 호출되는 시점에 객체가 생성되도록 Lazy initialization까지 구현했다. 성능과 thread-safety까지 확보할 수 있지만 그만큼의 기회비용이 발생한다. 바로 인스턴스를 호출하는 getInstance()가 동기화되면서 모든 요청이 직렬로 처리가 된다. 객체 생성을 늦춰 얻은 이점이 사용 시점에 동기화로 인해 상쇄가 된다.(개인적으로는 벼룩을 잡으려고 초가삼간을 태우는 격이라고 생각한다.)
이를 극복하기 위한 코드인 예제 5번을 살펴보자. 일명 Double Checked Locking이라고 불리는 방식으로 싱글턴을 만든다. 결론부터 말하자면 현재는 DCL방식은 현재는 사용하지 않으며, 과거에 이런 식으로 싱글턴을 만들었다는 개념으로 설명하기 위해 코드를 작성했다.
이 싱글턴의 포인트는 두 곳에 있다. 먼저 필드의 volatile 키워드와 getInstance() 내 두 번의 if문으로 인스턴스의 null 체크를 하는 코드블럭이다. 4 5
객체 호출로 인해 싱글턴 인스턴스가 생성되는 시점에만 동기화를 통해 체크하고, 사용되는 시점부터는 비동기 방식으로 사용할 수 있다.
요약 ::
a. GoF 디자인 패턴에서는 Lazy initailization방식으로 싱글턴 구현 방법을 서술한다.
b. Thread-safety를 구현하기 위한 방법을 서술한다.
c. 앞서 공부한 이펙티브 자바에서의 싱글턴과는 어떤 차이가 있고, 여러분은 어떤 방법을 선택하여 사용할 것 인지 고민해본다.
Spring Framework에서의 싱글턴
스프링 프레임워크는 Bean이라는 개념이 있다. 왜 싱글턴을 설명하다가 스프링을 설명하는지 궁금할 수도 있다. 스프링에서 관리하는 Bean이 싱글턴 객체이기 때문이다. 본인이 스프링을 통해 코드를 개발하고 있었다면 이미 수많은 싱글턴 객체를 다룬 것이다. 이번에는 예제 코드보다는 Bean의 특징을 통해 싱글턴을 살펴볼 것이다.
1. Eager initailization을 사용한다.
- 기본값은 Eager이며, Spring 3.0 이상부터는 @Lazy를 사용하여 Lazy 방식도 가능하게 변경되었다. 6
2. Bean으로 만들 클래스에는 제약이 없다. 다만 Bean을 관리하는 BeanFactory에 하나의 객체만 등록된다.
- Bean을 주입받아 사용할 수도 있지만, new를 통해 객체를 생성해서 사용할 수도 있다.
스프링의 전신은 EJB이다. EJB는 대규모 분산처리 시스템을 목표로 하는 프레임워크인 만큼 자원 관리가 중요했던 만큼 스프링도 이 부분에 주목했다. 따라서 싱글턴 패턴이 사용될 수밖에 없었다.
마지막으로 보충설명 요청이 들어왔던 자바 싱글턴과 Spring Bean의 차이를 간단히 설명하겠다.
자바 싱글턴은 클래스가 싱글턴의 조건을 갖춰야 한다. 가령 private 생성자를 활용해 외부 클래스의 객체 생성을 막고, 리플렉션 API를 통한 객체 생성을 방어한다. 말 그대로 클래스가 싱글턴의 조건을 충족시켜야만 싱글턴 패턴이라고 할 수 있다.
반면, Bean은 Spring에 의해, 정확히는 Bean Factory에 의해 구현된다. Bean이 될 객체는 싱글턴의 조건을 만족하지 않아도 Bean Factory에 등록될 조건만 갖추면 Spring이 싱글턴으로 관리해준다. 7
요약 ::
a. Spring Bean은 Eager initailization방식을 사용한다.
b. 다만 BeanFactory에서 관리되는 Bean은 싱글턴이지만, new 키워드를 통해 새로운 인스턴스도 생성할 수 있다.
c. 자바 싱글턴 패턴과 Spring Bean의 차이는 싱글턴 관리의 주체가 객체 자신에게 있는지, 프레임워크에게 있는지에 따라 달라진다.
마치며
우리는 세 가지 다른 관점으로 싱글턴 패턴을 살펴보았다. 우선 직렬 화문제와 리플렉션 API를 통한 우회를 막기 위한 코드를 보았다. 다음으로는 DCL방법으로 Thread-safety와 Lazy initialization를 통한 성능 향상에 주안점을 둔 코드도 보았다. 마지막으로는 Spring Framework의 Bean을 보면서 Bean Factory로 관리되는 싱글턴 객체도 보았다.
여기서 다시 한번 여러분에게 질문한다.
어떤 방법을 선택할 것인가?
싱글턴을 바라보는 관점이 이렇게 제각각인데, 나라면 과연 어떤 방법을 선택할 것이고, 또 그 방법은 항상 최선의 선택이 될 수 있다고 믿어도 되는지 지속적인 고민이 필요한 시점이 된 것이다.
개인적으로 실무에서의 개발은 "어떻게 구현할까?" 보다 "왜 이 방법을 선택했을까?"가 중요하다고 생각한다. 구현 방법은 구글링 조금만 하면 금방 찾을 수 있다. 다만 현 상황에 맞는 방법인지는 평소에 이런 생각이 쌓이지 않으면 쉽게 할 수 없다.
이번 포스팅이 단순히 싱글턴에 대한 학습으로 끝나지 않고 우리 모두가 깊이 있는 고민을 해보는 좋은 기회가 되었으면 좋겠다.
+ 2020. 07. 25 추가 보충
요즘 대중적으로 사용되는 싱글턴의 구현 방법은 Initialization-on-demand 이디엄을 사용한 방식이다. 먼저 코드를 보자. 8
public class LazyHolderSingleton {
private LazyHolderSingleton() {}
public static LazyHolderSingleton getInstance() {
return LazyHolder.instance;
}
private static class LazyHolder {
private static final LazyHolderSingleton instance = new LazyHolderSingleton();
}
public String getHello() {
return "Hello LazyHolderSingleton!";
}
}
static inner class을 통해 LazyHolder 클래스를 구현했고, 싱글턴 객체는 getInstance()를 통해 호출되면 LazyHolder 클래스의 static filed를 돌려준다.
어떻게 동작하는지를 설명하기 위해선 JVM의 동작원리를 이해해야 하기에 여기서는 생략하며, 성능과 Thread-safety, 추가로 가독성좋은 코드까지 한번에 잡은 권장되는 방법이라고만 설명하고 넘어가겠다.
보다 깊이 있는 학습을 위해서는 Java Language Specification을 참고하면 좋겠다. 9
Ref
![]() |
|
![]() |
|
Code Link
https://github.com/FourTeamProject/Charmander_EffectiveJava/tree/master/FourTeamEffectiveJavaStudy
FourTeamProject/Charmander_EffectiveJava
이펙티브 자바 스터디 입니다. Contribute to FourTeamProject/Charmander_EffectiveJava development by creating an account on GitHub.
github.com
https://github.com/bbubbush/head_first/tree/master/05_Singleton/java
bbubbush/head_first
헤드퍼스트 디자인 패턴 정리. Contribute to bbubbush/head_first development by creating an account on GitHub.
github.com
- docs.microsoft.com/ko-kr/dotnet/framework/performance/lazy-initialization [본문으로]
- en.wikipedia.org/wiki/Double-checked_locking [본문으로]
- Lazy initailization의 반대 개념. 생성 시점에 객체를 초기화한다. [본문으로]
- http://thswave.github.io/java/2015/03/08/java-volatile.html [본문으로]
- 이런 형태로 인해 DCL라는 이디엄으로 부르게 되었다. [본문으로]
- docs.spring.io/spring/docs/3.0.x/javadoc-api/org/springframework/context/annotation/Lazy.html [본문으로]
- 앞서 말했던 리플렉션 API가 여기서 활용된다. [본문으로]
- en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom [본문으로]
- docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4 [본문으로]
'자바' 카테고리의 다른 글
(이펙티브 자바) 아이템 06. 불필요한 객체 생성을 피하라 (0) | 2020.07.27 |
---|---|
(이펙티브 자바) 아이템 08. finalizer와 cleaner 사용을 피하라 (0) | 2020.07.26 |
(이펙티브 자바) 아이템 02. 생성자에 매개변수가 많다면 빌더를 고려하라 (3) | 2020.07.21 |
(이펙티브 자바) 아이템 04. 인스턴스화를 막으려거든 private 생성자를 사용하라 (1) | 2020.07.19 |
(이펙티브 자바) 아이템 01. 생성자 대신 정적 팩터리 메서드를 고려하라. (1) | 2020.07.19 |