Singleton Parttern이란?
Singleton Pattern은 디자인 패턴의 종류(생성, 구조, 행동) 중 생성 디자인 패턴에 속한다.
하나의 클래스는 하나의 인스턴스만을 가지고, 그 생성된 1개의 인스턴스를 전역적으로 접근할 수 있는 객체를 만들어야할 때 사용함. 예를 들어 프로그램에서 딱 하나의 데이터 베이스 연결 객체만 필요할 때 사용한다.
예시)
1. 데이터 베이스 연결 모듈
2. 스프링 Bean
3. 캐시
4. 로깅
5. 설정 클래스(Configuration Class)
Singleton Pattern을 쓰는 이유(==기존 개발 형태에 대한 문제점)?
기존에는 싱글톤 패턴 없이 클래스 A를 사용하기 위한 인스턴스가 필요할 때마다 매번 생성했다.
그럼 A를 여러개 생성하면 자원도 많이 필요할 것이고 1번 모듈에서 사용하는 A 인스턴스와 2번 모듈에서 사용하는 A 인스턴스의 일관성을 유지하기도 어렵다. 그리고 다른곳에서 A 클래스를 의존하고 있을 때 1번 모듈에서 접근해야할지 2번 모듈에서 접근해야할지 복잡해진다.
즉, 다음과 같은 세가지 문제점이 생긴다.
1. 자원 낭비: 매번 객체를 생성하면 메모리와 CPU 자원이 낭비될 수 있음.
2. 일관성 결여: 여러 객체가 동일한 작업을 수행하면서 일관성 없는 상태를 만들 수 있음.
3. 동기화 문제: 멀티스레드 환경에서 여러 인스턴스가 동시에 접근할 때 동기화 문제가 발생할 수 있음.
자원 낭비
public class Logger {
public Logger() {
// Logger 초기화
}
public void log(String message) {
System.out.println(message);
}
}
public class App {
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
Logger logger = new Logger();
logger.log("Log message " + i);
}
}
}
위 코드는 로그를 남길 때마다 Logger 객체를 새로 생성하는 코드이다. 이렇게 하면 메모리와 CPU 자원이 낭비된다.
일관성 결여
public class Configuration {
private Map<String, String> settings;
public Configuration() {
settings = new HashMap<>();
// 기본 설정값 로드
settings.put("setting1", "value1");
settings.put("setting2", "value2");
}
public String getSetting(String key) {
return settings.get(key);
}
}
public class App {
public static void main(String[] args) {
Configuration config1 = new Configuration();
Configuration config2 = new Configuration();
System.out.println(config1.getSetting("setting1")); // value1
System.out.println(config2.getSetting("setting1")); // value1
// config1의 설정값을 변경
config1.settings.put("setting1", "newValue");
System.out.println(config1.getSetting("setting1")); // newValue
System.out.println(config2.getSetting("setting1")); // value1
}
}
여기서는 Configuration 객체를 각각 생성했기 때문에
config1만 설정값을 변경했을 경우, config1과 config2의 설정값이 달라진다.
이렇게 되면 일관성이 결여된다.
동기화 문제
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
public class App {
public static void main(String[] args) {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount()); // 예상: 2000, 실제: 예측 불가
}
}
위 코드에서는 두 개의 스레드가 동시에 Counter 객체에 접근해서 값을 증가시키는데, 동기화 문제로 인해 최종 결과가 예측하기 어려워진다.
Singleton Pattern(싱글톤 패턴) 구현 방법
1. 이른 초기화 (Eager Initialization)
2. 게으른 초기화 (Lazy Initialization)
3. 이중 체크 잠금 (Double-Checked Locking)
4. 정적 내부 클래스 (Bill Pugh Singleton Design)
우선 4개의 방법만 간단하게 코드로 보여주려고 한다.
코드를 보기전에 싱글톤 패턴 생성하는 방법의 기본적인 조건은 다음과 같다.
- 외부에서 쉽게 객체를 생성 할 수 없게끔 생성자에 private 접근 제어자를 지정해야 한다.
- 유일한 단일 객체를 반환할 수 있는 정적 메서드가 필요하다.
- 유일한 단일 객체를 참조할 정적 참조 변수가 필요하다.
그래서 다음과 같은 세가지 조건이 4개의 방법에서 모두 공통적으로 나타나는 것도 짚어보면서 확인할 수 있다.
이른 초기화 (Eager Initialization)
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {
// private constructor to prevent instantiation
}
public static EagerSingleton getInstance() {
return instance;
}
}
클래스가 로드될 때 인스턴스를 미리 생성하는 방법.
간단하고 쓰레드 안전하지만, 인스턴스가 필요하지 않을 경우에도 생성된다는 단점이 존재함.
게으른 초기화 (Lazy Initialization)
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
// private constructor to prevent instantiation
}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
인스턴스가 필요할 때 생성하는 방법.
메모리 낭비를 줄일 수 있지만, synchronized 키워드를 사용하여 성능 저하를 일으킬 수 있음.
이중 체크 잠금 (Double-Checked Locking)
public class DoubleCheckedLockingSingleton {
private static volatile DoubleCheckedLockingSingleton instance;
private DoubleCheckedLockingSingleton() {
// private constructor to prevent instantiation
}
public static DoubleCheckedLockingSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckedLockingSingleton.class) {
if (instance == null) {
instance = new DoubleCheckedLockingSingleton();
}
}
}
return instance;
}
}
성능 저하를 최소화하면서 쓰레드 안전성을 보장함.
정적 내부 클래스 (Bill Pugh Singleton Design)
public class BillPughSingleton {
private BillPughSingleton() {
// private constructor to prevent instantiation
}
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
게으른 초기화와 쓰레드 안전성을 모두 보장하는 방법. JVM의 클래스 로더 매커니즘을 이용해서 인스턴스를 생성함.
위 코드들과 다르게 내부 클래스를 생성할 때 유일한 단일 객체를 생성함.
Singleton Pattern의 장단점
장점
- 자원 절약: 인스턴스를 하나만 생성하므로 메모리와 자원을 절약할 수 있음.
- 일관성 유지: 단 하나의 인스턴스를 통해 전역 상태의 일관성을 유지할 수 있음.
- 글로벌 접근: 전역적으로 인스턴스에 접근할 수 있어 코드의 가독성과 유지보수성이 향상됨.
단점
- 단위 테스트 어려움: 전역 상태를 가지므로 단위 테스트 작성이 어려움
- 의존성 주입 어려움: 싱글톤 객체의 의존성을 주입하는 것이 어려움.
- 멀티스레드 이슈: 잘못 구현하면 멀티스레드 환경에서 동기화 문제가 발생할 수 있음.
문제된 상황을 싱글톤 패턴으로 개선해보면?
1. 자원 낭비 해결
public class Logger {
private static final Logger instance = new Logger();
private Logger() {
// Logger 초기화
}
public static Logger getInstance() {
return instance;
}
public void log(String message) {
System.out.println(message);
}
}
public class App {
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
Logger logger = Logger.getInstance();
logger.log("Log message " + i);
}
}
}
Logger 객체는 하나만 생성되고 모든 로그 작업에서 동일한 인스턴스를 사용하기 때문에 자원 낭비가 없어졌다.
2. 일관성 결여 해결
public class Configuration {
private static final Configuration instance = new Configuration();
private Map<String, String> settings;
private Configuration() {
settings = new HashMap<>();
// 기본 설정값 로드
settings.put("setting1", "value1");
settings.put("setting2", "value2");
}
public static Configuration getInstance() {
return instance;
}
public String getSetting(String key) {
return settings.get(key);
}
public void setSetting(String key, String value) {
settings.put(key, value);
}
}
public class App {
public static void main(String[] args) {
Configuration config1 = Configuration.getInstance();
Configuration config2 = Configuration.getInstance();
System.out.println(config1.getSetting("setting1")); // value1
System.out.println(config2.getSetting("setting1")); // value1
// config1의 설정값을 변경
config1.setSetting("setting1", "newValue");
System.out.println(config1.getSetting("setting1")); // newValue
System.out.println(config2.getSetting("setting1")); // newValue
}
}
Configuration 객체는 하나만 생성되므로 config1의 설정값만 변경해도 config2에서도 값이 바뀐 것을 알 수 있다.
즉, 일관성이 유지된다.
3. 동기화 문제 해결
public class Counter {
private static final Counter instance = new Counter();
private int count = 0;
private Counter() {
// private constructor to prevent instantiation
}
public static Counter getInstance() {
return instance;
}
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
public class App {
public static void main(String[] args) {
Counter counter = Counter.getInstance();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount()); // 예상: 2000, 실제: 2000
}
}
이제 Counter 객체도 하나만 생성되고, 모든 스레드가 동일한 인스턴스를 사용하므로 멀티스레드 속에서 동기화 문제가 해결된다.
그러나 싱글톤 패턴이 모든 객체를 생성하는데 쓰이는 것보다 적재적소가 중요하다.
남용하면 오히려 코드의 유연성을 떨어트린다.
주의할 점
지연 초기화: 필요 시에만 인스턴스를 생성하도록 게으른 초기화를 사용하는 것이 메모리 효율 측면에서 유리함.
테스트 용이성: 싱글톤 패턴은 전역 상태를 가지므로 단위 테스트 작성이 어려움. 이럴 경우 의존성 주입(Dependency Injection)을 활용하여 테스트 가능한 구조로 변경하는 것이 좋음.
의존성 주입과의 조화: 의존성이 강력해질 수 있기 때문에 의존성 주입을 통해 모듈 간의 결합을 조금 더 느슨하게 만들어 줄 수 있음.
예를 들어 스프링과 같은 프레임워크를 사용할 경우, 프레임워크의 DI(Dependency Injection) 컨테이너를 활용하여 싱글톤 패턴을 자연스럽게 적용할 수 있음.
'CS > 디자인패턴' 카테고리의 다른 글
[Design Pattern] Strategy Pattern(전략 패턴) (0) | 2024.06.02 |
---|---|
[Design Pattern] Factory Pattern(팩토리 패턴) (0) | 2024.06.01 |
[Design Pattern] Builder Pattern(빌더 패턴) (1) | 2024.02.07 |
[Design Pattern] Observer pattern(옵저버 패턴) (0) | 2024.01.24 |
[Design Pattern] 디자인 패턴이란? (0) | 2024.01.23 |