싱글톤 패턴에 대해 학습하며 작성된 글입니다. 학습 과정에서 작성된 글이기 때문에 잘못된 내용이 있을 수 있으며, 이에 대한 지적이나 피드백은 언제든 환영입니다.
1. 싱글톤 패턴
싱글톤 패턴은 생성자가 여러 번 호출되더라도 단 하나의 객체만 반환하는 패턴을 말합니다. 이를 통해 여러 개의 인스턴스가 생성되는 것을 방지하며, 객체 생성 비용을 줄일 수 있습니다.
In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to a singular instance. One of the well-known “Gang of Four” design patterns, which describe how to solve recurring problems in object-oriented software, the pattern is useful when exactly one object is needed to coordinate actions across a system.
다이어그램을 보면 다음과 같습니다.
2. 구현, 고려 사항
싱글톤은 아래와 같은 패턴으로 구현할 수 있습니다. 하지만 여기에는 동기화, 성능과 같은 고려해야 할 점이 있는데, 이에 대해 살펴보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class DataSourceConfiguration {
private static DataSourceConfiguration instance = new DataSourceConfiguration();
private DataSourceConfiguration() {
}
public static DataSourceConfiguration getInstance() {
if (instance == null) {
instance = new DataSourceConfiguration();
}
return instance;
}
}
2-1. 동기화 문제
우선 동기화 문제에 대해 살펴보겠습니다. 아래 코드는 멀티 쓰레드 환경에서 이슈가 생길 수 있는데, 이는 instance의 null 체크를 하는 코드 블록에 동시에 여러 개의 쓰레드가 들어갈 수 있기 때문입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class DataSourceConfiguration {
private static DataSourceConfiguration instance = new DataSourceConfiguration();
private DataSourceConfiguration() {
}
public static DataSourceConfiguration getInstance() {
// 해당 블럭에 여러 쓰레드가 접근 가능
if (instance == null) {
instance = new DataSourceConfiguration();
}
return instance;
}
}
이 문제를 해결하기 위해서는 아래와 같이 synchronized 키워드를 사용해 여러 개의 쓰레드가 동시에 접근하는 문제를 해결할 수 있습니다. synchronized 블럭 내부로 들어가기 전 항상 동기화를 체크하기 때문입니다. 하지만 이 또한 완전한 싱글톤은 보장할 수 없는데, 리플렉션 때문입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class DataSourceConfiguration {
private static volatile DataSourceConfiguration instance = new DataSourceConfiguration();
private DataSourceConfiguration() {
}
// Double Checked Locking
public static DataSourceConfiguration getInstance() {
if (instance == null) {
synchronized (DataSourceConfiguration.class) {
if (instance == null) {
instance = new DataSourceConfiguration();
return instance;
}
}
}
return instance;
}
}
리플랙션으로 private 생성자를 호출해서 객체를 생성하는 방법은 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@DisplayName("[UnitTest] 싱글톤 단위 테스트")
class SingletonUnitTest {
@Test
@DisplayName("리플렉션으로 private 생성자를 호출해 객체를 생성할 수 있다.")
void new_instance_create_with_reflection_test() throws Exception {
Class<DataSourceConfiguration> clazz = DataSourceConfiguration.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
DataSourceConfiguration object = (DataSourceConfiguration) constructor.newInstance();
assertNotNull(object);
}
}
두 번째 방법은 Holder를 사용한 방법으로, 내부 클래스에 해당 클래스 객체를 static으로 만들어 두고 사용하는 것입니다. 아쉽지만 이 방법 또한 리플랙션 때문에 완전한 싱글톤은 보장할 수 없습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class DataSourceConfiguration {
private static volatile DataSourceConfiguration instance = new DataSourceConfiguration();
private DataSourceConfiguration() {
}
public static DataSourceConfiguration getInstance() {
return DataSourceHolder.instance;
}
private static class DataSourceHolder {
private static final DataSourceConfiguration instance = new DataSourceConfiguration();
}
}
현재까지 싱글톤을 깨트리는 방법에 대해 살펴보았는데, 완전한 해결책은 자바 코드로 구현하면 너무 길어지기에 '이런 방식을 통해 싱글톤을 깨트릴 수 있다.' 정도만 이해하고 넘어가면 좋을 것 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class DataSourceConfiguration {
private static DataSourceConfiguration instance = new DataSourceConfiguration();
private DataSourceConfiguration() {
}
public static DataSourceConfiguration getInstance() {
if (instance == null) {
instance = new DataSourceConfiguration();
}
return instance;
}
}
2-2. 직렬화/역 직렬화
두 번째 고려할 점은 직렬화/역 직렬화 시 싱글톤이 깨질 수 있다는 점입니다. 객체를 직렬화해서 파일로 만든 후 역 직렬화하면 객체를 생성할 수 있기 때문입니다.
아래와 같이 싱글톤이 깨진 것을 볼 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@DisplayName("[UnitTest] 싱글톤 단위 테스트")
class SingletonUnitTest {
private static final String PATH = "data/config.obj";
@Test
@DisplayName("직렬화/역 직렬화를 시키면 싱글톤이 깨질 수 있다.")
void when_serialize_and_deserialize_then_singleton_should_be_broken() throws Exception {
DataSourceConfiguration configuration = DataSourceConfiguration.getInstance();
DataSourceConfiguration instance = null;
try (ObjectOutput objectOutput =
new ObjectOutputStream(new FileOutputStream(PATH))
) {
objectOutput.writeObject(configuration);
}
try (ObjectInput objectInput = new ObjectInputStream(
new FileInputStream(PATH))) {
instance = (DataSourceConfiguration) objectInput.readObject();
}
assertNotEquals(configuration, instance);
}
}
물론 readResolve( ) 메서드를 사용하면 역 직렬화 과정에서도 동등성을 보장받을 수 있습니다만, 리플렉션의 경우 여전히 문제점이 존재합니다.
1
2
3
4
5
6
7
8
public class DataSourceConfiguration {
......
protected Object readResolve(){
return getInstance();
}
}
2-3. Enum
Enum은 Java 언어 차원에서 지원하는 싱글톤 클래스 입니다. 따라서 Enum을 사용하면 완벽한 싱글톤을 구현할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum EnumDataSourceConfiguration {
MYSQL_CONFIGURATION(new MySQLConfiguration()),
ORACLE_CONFIGURATION(new OracleConfiguration());
private final DataSourceConfiguration dataSourceConfiguration;
EnumDataSourceConfiguration(DataSourceConfiguration dataSourceConfiguration) {
this.dataSourceConfiguration = dataSourceConfiguration;
}
public DataSourceConfiguration getDataSourceConfiguration() {
return dataSourceConfiguration;
}
}
이런 경우까지 고려할 수 있다는 점을 알고 있는 것으로 충분하지 않을까 싶습니다. 이런 극단적인 사례는 잘 발생하지 않으며, 특히 스프링을 사용한다면 @Bean, @Configuration, @Component 등을 통해 싱글톤을 충분히 보장할 수 있기 때문입니다.
3. 정리
싱글톤은 객체를 한 번만 생성하기 때문에 객체 생성 비용을 줄일 수 있습니다. 이를 통해 한 번 생성된 객체를 재활용해서 메모리를 효율적으로 사용할 수 있게 할 수 있습니다. 단, 이때 동시성 이슈와 같은 문제가 발생할 수 있으므로 싱글톤은 상태를 갖지 않는 것이 중요합니다.