[디자인 패턴] (GoF 생성패턴) Singleton Pattern에 대하여
안녕하세요! GoF 디자인 패턴 연재, 이번 시간에는 생성 패턴(Creational Patterns)의 가장 유명하고 단순한 패턴 중 하나인 싱글턴(Singleton) 패턴에 대해 알아보겠습니다.
정의
싱글턴 패턴은 어떤 클래스의 인스턴스가 오직 하나만 생성되도록 보장하고, 그 인스턴스에 대한 전역적인 접근점(Global Point of Access)을 제공하는 패턴입니다. 쉽게 말해, "이 클래스로는 객체를 딱 하나만 만들 수 있어! 그리고 누구든 그 하나뿐인 객체를 쉽게 가져다 쓸 수 있게 해줄게." 라는 개념입니다.
예를 들어, 시스템의 환경 설정을 관리하는 객체나, 데이터베이스 연결을 관리하는 커넥션 풀 객체는 여러 개가 존재하면 설정값이 꼬이거나 자원이 낭비될 수 있습니다. 이런 경우, 시스템 전체에서 단 하나의 인스턴스만 존재하도록 강제하여 일관성을 유지하고 자원을 효율적으로 사용하기 위해 싱글턴 패턴을 사용합니다.
싱글턴 패턴의 구조는 매우 간단합니다. 핵심은 '외부에서 새로운 인스턴스를 생성하지 못하게 막고, 이미 만들어진 인스턴스가 있다면 그것을 반환하는 것'입니다.
- 비공개(private) 생성자: 외부에서 new 키워드로 새로운 인스턴스를 생성하는 것을 막습니다.
- 정적(static) 변수: 클래스 내부에서 유일한 인스턴스를 저장하기 위한 변수를 선언합니다.
- 정적(static) 메서드: 유일한 인스턴스를 반환하는 getInstance()와 같은 공개 메서드를 제공합니다. 이 메서드는 인스턴스가 없으면 생성하고, 이미 있다면 기존 인스턴스를 반환합니다.
사용예시
1. 가장 기본적인 구현 (Lazy Initialization)
getInstance()가 처음 호출될 때 인스턴스를 생성하는 방식입니다.
public class Settings {
// 1. 유일한 인스턴스를 저장할 정적 변수
private static Settings instance;
// 2. 외부에서 생성을 막기 위한 비공개 생성자
private Settings() {
// 설정 로딩 등 초기화 로직
System.out.println("Settings 인스턴스가 생성되었습니다.");
}
// 3. 인스턴스를 얻기 위한 정적 메서드
public static Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
// 예시 메서드
public void showMessage() {
System.out.println("현재 설정 값은...");
}
}
// --- 클라이언트 코드 ---
public class Client {
public static void main(String[] args) {
Settings settings1 = Settings.getInstance();
Settings settings2 = Settings.getInstance();
if (settings1 == settings2) {
System.out.println("settings1과 settings2는 같은 인스턴스입니다.");
} else {
System.out.println("settings1과 settings2는 다른 인스턴스입니다.");
}
settings1.showMessage();
// 실행 결과:
// Settings 인스턴스가 생성되었습니다.
// settings1과 settings2는 같은 인스턴스입니다.
// 현재 설정 값은...
}
}
* 주의: 위 코드는 멀티스레드 환경에서 동시에 getInstance()를 호출할 경우, 인스턴스가 여러 개 생성될 수 있는 문제가 있습니다.
2. 멀티스레드 환경에서 안전한 구현 (Thread-Safe)
방법 1: Eager Initialization (이른 초기화)
클래스가 로드될 때 미리 인스턴스를 생성하는 방식으로, 가장 간단하고 안전합니다.
public class Settings {
// 클래스 로딩 시점에 바로 인스턴스 생성
private static final Settings INSTANCE = new Settings();
private Settings() {}
public static Settings getInstance() {
return INSTANCE;
}
}
방법 2: synchronized 키워드 사용
getInstance() 메서드에 synchronized를 붙여 한 번에 하나의 스레드만 접근하도록 보장합니다. 하지만 매번 동기화 오버헤드가 발생하여 성능 저하의 원인이 될 수 있습니다.
public class Settings {
private static Settings instance;
private Settings() {}
// 동기화 처리로 스레드 안전성 보장
public static synchronized Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
}
결론
그럼 싱글턴 패턴은 언제 사용하는 걸까요?
- 시스템 전체에서 단 하나의 인스턴스만 존재해야 할 때
- 인스턴스가 하나만 있음을 보장하여 자원을 절약하고 데이터 일관성을 유지하고 싶을 때
- 전역 변수처럼 사용하지만, 객체 생성 시점을 제어하고 싶을 때 (Lazy Initialization)
단순성과 이해가능성이 높은 실무적으로 효율적인 패턴이지만 다음과 같은 조건을 고려해야합니다
- 단일 책임 원칙(SRP) 위배: 싱글턴 클래스는 인스턴스 관리와 본래의 비즈니스 로직 두 가지 책임을 갖게 됩니다.
- 테스트의 어려움: 전역 상태를 갖기 때문에 단위 테스트를 수행하기 어렵습니다. 의존성 주입(DI)이 힘들어 테스트 코드 작성이 까다로워집니다.
- 유연성 저하: 싱글턴은 '구체 클래스'에 직접 의존하므로, 코드의 유연성이 떨어질 수 있습니다.
싱글턴 패턴은 구현이 간단하고 명확한 목적을 가지고 있어 널리 사용되지만, 위와 같은 단점 때문에 안티 패턴(Anti-Pattern)으로 불리기도 합니다. 따라서 사용하기 전에 정말로 이 클래스의 인스턴스가 단 하나만 필요한지, 그리고 싱글턴으로 인해 발생할 수 있는 문제점은 없는지 신중하게 고민해야 합니다. 다음 시간에는 Builder 패턴에 대해서 알아보겠습니다!