| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 빌더 패턴
- 디자인 패턴
- Til
- Spring
- 이펙티브 자바
- 스케줄러
- 계산기
- redis
- 로드밸런서
- java
- 백엔드
- 템플릿 메서드 패턴
- 자바
- 추상클래스
- 성능 개선
- 스프링
- 토스
- GoF 23
- 트러블슈팅
- 코드카타
- spring boot
- 김영한
- 배치
- Spring Batch
- lv1
- 프로그래머스
- 프록시 패턴
- 스프링 배치
- DB
- Effective Java
- Today
- Total
김코딩
싱글톤 패턴(Singleton Pattern) 본문
디자인 패턴 시리즈의 첫 번째 주제는 싱글톤 패턴(Singleton Patter)이다.
싱글톤은 23가지 GoF 디자인 패턴 중 가장 간단하면서도, 가장 많이 사용되고, 동시에 가장 많이 오해받는 패턴이다.
Spring을 사용한다면 이미 매일 싱글톤 패턴을 사용하고 있을 것이다. 하지만 정작 "왜 싱글톤인가?"를 정확히 아는 개발자는 많지 않다.
이번 글에서는 싱글톤 패턴이 무엇인지, 왜 필요한지, 어떻게 구현하는지, 그리고 실무에서 어떻게 사용되는지 알아보자.
싱글톤 패턴의 정의
싱글톤 패턴(Singleton Pattern)은 클래스의 인스턴스가 오직 하나만 생성되도록 보장하고, 어디서든 그 인스턴스에 접근할 수 있도록 하는 생성 패턴이다.
핵심 특징:
- 클래스의 인스턴스가 단 하나만 존재한다
- 인스턴스에 대한 전역 접근점을 제공한다
- 생성자를 private으로 만들어 외부에서 직접 생성을 막는다
어떤 상황에서 싱글톤 패턴을 사용할까?
애플리케이션을 개발하다 보면, 전체 시스템에서 단 하나의 인스턴스만 필요한 객체들이 있다.
예를 들어:
- 데이터베이스 연결 풀(Connection Pool): 여러 개 만들면 리소스 낭비
- 설정 관리자(Configuration Manager): 앱 전체에서 같은 설정을 공유해야 함
- 로거(Logger): 모든 곳에서 동일한 로깅 시스템을 사용해야 함
- 캐시(Cache): 데이터를 공유하려면 하나의 캐시 인스턴스가 필요
만약 이런 객체들을 여러 번 생성하면 어떻게 될까?
DatabaseConnection conn1 = new DatabaseConnection();
DatabaseConnection conn2 = new DatabaseConnection();
DatabaseConnection conn3 = new DatabaseConnection();
메모리 낭비는 물론이고, 각 인스턴스가 서로 다른 상태를 가지게 되어 예상치 못한 버그가 발생할 수 있다. 특히 설정 객체가 여러 개 생성되면, 어떤 설정이 진짜인지 알 수 없는 상황이 벌어진다.
"이 클래스의 인스턴스는 절대 하나만 존재해야 해!"
이런 요구사항을 해결하는 것이 바로 싱글톤 패턴이다.
순수 Java로 구현하기
싱글톤 패턴은 여러 방법으로 구현할 수 있다. 각각의 장단점을 살펴보자.
1. Eager Initialization (이른 초기화)
가장 간단한 방법이다. 클래스 로딩 시점에 인스턴스를 미리 생성한다.
public class DatabaseConnection {
// 클래스 로딩 시점에 인스턴스 생성
private static final DatabaseConnection instance = new DatabaseConnection();
// private 생성자로 외부 생성 차단
private DatabaseConnection() {
System.out.println("DatabaseConnection 생성됨");
}
// 전역 접근점 제공
public static DatabaseConnection getInstance() {
return instance;
}
public void connect() {
System.out.println("DB 연결 성공!");
}
}
// 사용 예시
public class Main {
public static void main(String[] args) {
DatabaseConnection conn1 = DatabaseConnection.getInstance();
DatabaseConnection conn2 = DatabaseConnection.getInstance();
System.out.println(conn1 == conn2); // true - 같은 인스턴스!
conn1.connect();
}
}
장점:
- 구현이 간단하다
- Thread-safe하다 (클래스 로딩은 JVM이 보장)
단점:
- 사용하지 않아도 무조건 생성된다 (메모리 낭비 가능)
- 생성 시점에 예외 처리가 어렵다
2. Lazy Initialization (늦은 초기화)
실제로 사용될 때 인스턴스를 생성한다.
public class ConfigManager {
private static ConfigManager instance;
private ConfigManager() {
System.out.println("ConfigManager 생성됨");
}
// 사용 시점에 생성
public static ConfigManager getInstance() {
if (instance == null) {
instance = new ConfigManager();
}
return instance;
}
}
장점:
- 필요할 때만 생성된다 (메모리 효율적)
단점:
- Thread-safe하지 않다! 멀티스레드 환경에서 여러 인스턴스가 생성될 수 있음
3. Thread-Safe Lazy Initialization (동기화)
synchronized 키워드로 스레드 안전성을 보장한다.
public class Logger {
private static Logger instance;
private Logger() {}
// synchronized로 동기화
public static synchronized Logger getInstance() {
if (instance == null) {
instance = new Logger();
}
return instance;
}
}
장점:
- Thread-safe하다
- 필요할 때만 생성된다
단점:
- 성능 저하 (매번 동기화 비용 발생)
4. Double-Checked Locking
성능과 안전성을 모두 잡은 방법이다.
public class CacheManager {
private static volatile CacheManager instance;
private CacheManager() {}
public static CacheManager getInstance() {
if (instance == null) { // 첫 번째 체크 (동기화 X)
synchronized (CacheManager.class) {
if (instance == null) { // 두 번째 체크 (동기화 O)
instance = new CacheManager();
}
}
}
return instance;
}
}
장점:
- Thread-safe하다
- 성능이 좋다 (이미 생성된 경우 동기화 안 함)
단점:
- 코드가 복잡하다
5. Bill Pugh Solution
Inner Static Helper Class를 사용하는 방법으로, 가장 우아하고 효율적이다.
public class ApiClient {
private ApiClient() {}
// Inner Static Helper Class
private static class SingletonHelper {
private static final ApiClient INSTANCE = new ApiClient();
}
public static ApiClient getInstance() {
return SingletonHelper.INSTANCE;
}
}
장점:
- Thread-safe하다
- Lazy Loading이 가능하다 (Helper 클래스가 로딩될 때 생성)
- 동기화 비용이 없다
- 코드가 깔끔하다
가장 권장되는 방식이다.
6. Enum Singleton (최고의 방법)
Joshua Bloch가 "Effective Java"에서 제안한 방법이다.
public enum SystemConfig {
INSTANCE;
public void loadConfig() {
System.out.println("설정 로드");
}
}
// 사용
SystemConfig.INSTANCE.loadConfig();
장점:
- 가장 간결하다
- Thread-safe하다
- Serialization 문제 해결
- Reflection 공격 방어
단점:
- 상속이 불가능하다
- Enum을 싱글톤으로 쓴다는 게 직관적이지 않을 수 있다
싱글톤 패턴의 장점
- 메모리 절약: 하나의 인스턴스만 생성하므로 메모리 효율적
- 전역 접근: 어디서든 동일한 인스턴스에 접근 가능
- 데이터 공유: 여러 곳에서 같은 데이터를 공유해야 할 때 유용
- 초기화 제어: 인스턴스 생성 시점을 제어할 수 있음
싱글톤 패턴의 단점
- 테스트하기 어렵다: Mock 객체로 대체하기 어려움
- 전역 상태: 전역 변수처럼 동작하여 결합도가 높아짐
- 단일 책임 원칙 위반: 인스턴스 생성과 비즈니스 로직이 섞임
- 멀티스레드 문제: 구현 방법에 따라 동시성 이슈 발생 가능
"싱글톤은 안티패턴이다"라는 의견도 있다. 남용하면 코드가 강하게 결합되고 테스트가 어려워진다.
Spring에서의 싱글톤
Spring을 사용한다면, 이미 싱글톤 패턴을 매일 사용하고 있다.
@Service
public class UserService {
// Spring Container가 싱글톤으로 관리
}
@Repository
public class UserRepository {
// 이것도 싱글톤
}
Spring의 싱글톤 방식:
- Spring Container가 빈(Bean)을 기본적으로 싱글톤 스코프로 관리
- 개발자가 직접 싱글톤 코드를 작성할 필요 없음
- Thread-safe하게 관리됨
- 필요하면 @Scope("prototype")으로 변경 가능
중요한 차이:
- 순수 싱글톤: JVM당 하나의 인스턴스
- Spring 싱글톤: Spring Container당 하나의 인스턴스
Spring을 사용한다면 직접 싱글톤 코드를 구현할 일은 거의 없다. Spring Container에게 관리를 맡기는 것이 더 안전하고 편리하다.
실무에서 언제 사용할까?
사용하면 좋은 경우:
- 설정 관리 객체
- 로깅 시스템
- 데이터베이스 연결 풀
- 캐시 매니저
- 스레드 풀
사용하지 말아야 하는 경우:
- 상태를 가지는 객체 (Stateful)
- 여러 인스턴스가 필요한 경우
- 테스트가 중요한 경우
핵심 원칙:
"Spring을 사용한다면 직접 싱글톤을 구현하지 말고, Spring Container에게 맡겨라"
결론
싱글톤 패턴은 가장 간단하면서도 실무에서 가장 많이 사용되는 패턴이다.
기억해야 할 포인트:
- 인스턴스가 하나만 필요할 때 사용한다
- 여러 구현 방법이 있지만, Bill Pugh 방식 또는 Enum 방식을 권장한다
- Spring 환경에서는 직접 구현하지 말고 Container에게 관리를 맡긴다
- 남용하지 않는다 - 꼭 필요한 곳에만 사용한다
다음 글에서는 객체 생성을 더 유연하게 만드는 팩토리 메서드 패턴을 알아볼 것이다.
다음 글 예고: 팩토리 메서드 패턴 - 객체 생성의 책임을 분리하라
'디자인 패턴' 카테고리의 다른 글
| 프로토타입 패턴(Prototype Pattern) (0) | 2026.01.13 |
|---|---|
| 빌더 패턴(Builder Pattern) (0) | 2026.01.13 |
| 추상 팩토리 패턴(Abstract Factory Pattern) (0) | 2026.01.13 |
| 팩토리 메서드 패턴(Factory Method Pattern) (0) | 2026.01.13 |
| 왜? 디자인 패턴을 공부해야할까? (0) | 2026.01.13 |