[Design Pattern] Singleton Pattern 싱글톤 패턴
단 하나의 유일한 객체를 만들기 위한 코드 패턴
개요
메모리 절약을 위해, 인스턴스가 필요할 때 똑같은 인스턴스를 생성하지 않고,
기존의 인스턴스를 활용할때 사용된다.
사용의 핵심은 메모리를 절약함에 포커스가 있다.
우리가 일반적으로 사용하는 패턴이고, 가장 기본적이기 때문에
간단히 예제코드를 살펴보고,
싱글톤패턴을 사용할때 스레드 단위에서 생기는 문제점에 대해 짚어본다.
구조
특징
- 생성자 함수의 접근 제한자가 private이다.
외부에서 new 키워드를 이용한 객체 생성을 막기 위해서 생성자 함수의 접근 제한자를 private으로 설정하고
객체의 초기화와 객체 가져오기는 오직 static public interface (getInstance())를 통해서만 가능하게 하기 위함 - 자신의 인스턴스를 저장하는 클래스 변수 (static 필드) 가진다.
자신의 인스턴스를 저장하고 있는 클래스 변수를 가지고 있고 이 값이 null 일 경우에는 인스턴스를 생성하고 클래스 변수에 인스턴스를 할당한다. 그렇지 않은 경우에는 Heap 메모리에 저장되어 있는 인스턴스 주소를 리턴한다.
구현 기법
Lazy initialization
class Singleton {
// 싱글톤 클래스 객체를 담을 인스턴스 변수
private static Singleton instance;
// 생성자를 private로 선언 (외부에서 new 사용 X)
private Singleton() {}
// 외부에서 정적 메서드를 호출하면 그제서야 초기화 진행 (lazy)
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 오직 1개의 객체만 생성
}
return instance;
}
}
일반적으로 잘 알려진 싱글톤 패턴의 구현 방법이다.
다만, 위의 일반적인 패턴으로 사용하게 되면
멀티스레드 환경에서는 동시성으로 인한 코드 실행 문제점이 발생하게 된다.
아래의 예를 통해 알아보자
>>>
스레드 A와 B가 존재하고, 코드의 흐름에 따라 스레드 A가 if문을 평가하고, 인스턴스 생성 코드로 진입한다.
이때 만약 스레드 B가 if문을 평가하게 된다면 아직 스레드 A가 인스턴스화 코드를 실행시키지 않았기 때문에,
스레드 B는 if문을 True로 반환하게 되고,
결과적으로 스레드 A와 B 모두 인스턴스를 초기화하는 코드를 실행하게 되는 결과가 나타날 우려가 있다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 싱글톤 객체
class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 오직 1개의 객체만 생성
}
return instance;
}
}
public class Main {
public static void main(String[] args) {
// 1. 싱글톤 객체를 담을 배열
Singleton[] singleton = new Singleton[10];
// 2. 스레드 풀 생성
ExecutorService service = Executors.newCachedThreadPool();
// 3. 반복문을 통해 10개의 스레드가 동시에 인스턴스 생성
for (int i = 0; i < 10; i++) {
final int num = i;
service.submit(() -> {
singleton[num] = Singleton.getInstance();
});
}
// 4. 종료
service.shutdown();
// 5. 싱글톤 객체 주소 출력
for(Singleton s : singleton) {
System.out.println(s.toString());
}
}
}
문제 해결
이러한 문제를 해결하기 위해 LazyHolder라는 방법을 사용하게 된다.
private static inner class를 사용해서 스레드 세이프티한 싱글톤 패턴을 구현한다.
JVM의 ClassLoader에 의해 클래스가 로드될때, 내부적으로 synchronized 키워드를 이용한다.
class Singleton {
private Singleton() {}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
// static 내부 클래스를 이용
// Holder로 만들어, 클래스가 메모리에 로드되지 않고 getInstance 메서드가 호출되어야 로드됨
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
멀티 스레드 기반으로 실행한다고 가정했을때,
쉽게 말하면 A 스레드가 getInstance() 메서드를 호출하면 JVM은 LazyHolder 클래스를 메모리에 로드하는데,
내부적인 synchronized 키워드로 인해 단 한번의 초기화만 보장하는 것이다.
이후 B 스레드가 인스턴스를 초기화 하려고 시도하더라도, A 스레드가 초기화를 완료할때 까지 기다리게 되고,
이 초기화가 끝나면 동일한 인스턴스를 반환받으며,
스레드가 여러개더라도 오직 하나의 인스턴스만 생성되는 안전한 싱글톤 패턴이다.
싱글톤 패턴은 개념이 어렵지않고 기본적인 거지만,
동시성에 대한 문제에 대해서는 개발자들이 자주 놓치는 실수라고 하니 알아두도록 하자.
하지만 Spring으로 개발하게 되면 객체는 Bean 으로 저장하는데,
Bean은 문서에 Singleton 패턴으로 구현된다고 명시되어 있기 때문에
개발자가 신경을 써야 할 상황이 거의 없다고
출처
'Studying > Design Pattern' 카테고리의 다른 글
[Design Pattern] Observer Pattern 옵저버 패턴 (1) | 2024.03.04 |
---|---|
[Design Pattern] Strategy Pattern 전략 패턴 (2) | 2024.02.27 |
[Design Pattern] Factory Method 팩토리 패턴 (0) | 2024.02.16 |
[Design Pattern] 디자인 패턴 (3) | 2024.02.15 |