1. 의도
우리가 프로그램을 만들다 보면, 하나만 존재해야 하는 객체가 필요할 때가 있어요.
예를 들어:
- 설정 관리 객체 (Configuration Manager)
- 로깅 시스템 (Logger)
- DB 연결 객체 (Database Connection Pool)
이런 객체는 여러 개 만들면 오히려 문제를 일으키죠. 설정이 중복되거나 로그가 꼬일 수도 있고, DB 커넥션이 과하게 열릴 수도 있어요.
이런 경우, 객체는 하나만 존재해야 하고, 모든 곳에서 그 하나를 함께 써야 합니다.
바로 이럴 때 등장하는 게 싱글턴 패턴이에요.
프로그램 안에서 객체를 단 하나만 만들고,
필요할 때 언제든 그 객체를 가져다 쓸 수 있게 해주는 패턴이죠.
2. 문제
싱글턴 패턴은 사실 두 가지 문제를 동시에 해결하려는 방식입니다.
이 두 문제는 각각 독립적으로도 중요한데, 싱글턴은 이를 한 클래스 안에서 함께 처리하면서 단일 책임 원칙(SRP)을 위반할 소지가 있습니다.
1. 인스턴스를 오직 하나만 만들고, 공유 자원을 통제하고 싶다
- 어떤 클래스는 여러 개 만들면 문제가 생깁니다.
- 예: DB 연결, 파일 핸들러, 설정 객체 등은 여러 개 생기면 충돌, 낭비, 불일치가 발생할 수 있어요.
- 그래서 객체를 만들 때마다 새 인스턴스를 주는 게 아니라, 이미 있는 걸 재사용해야 하는 상황이 생깁니다.
- 하지만 일반 생성자는 호출할 때마다 새 객체를 만들어버리니, 생성자만으로는 이 요구를 만족시킬 수 없습니다.
2. 프로그램 어디서든 객체에 접근할 수 있어야 한다
- 전역 변수처럼 편하게 접근하고 싶은 경우도 있어요.
- 그러나 전역 변수는 누구나 값을 바꿀 수 있어서 위험합니다. 잘못 덮어쓰면 프로그램 전체에 영향을 줄 수 있죠.
- 싱글턴은 이런 접근성을 제공하면서도, 인스턴스를 외부에서 마음대로 바꾸지 못하게 보호합니다.
정리하자면, 싱글턴은
객체를 하나만 만들고 재사용하고 싶은 요구와
그 객체를 전역처럼 어디서든 쓰고 싶은 요구이 두 가지를 동시에 만족시킵니다.
하지만 이 두 역할이 하나의 클래스에 몰려 있다는 점에서,
잘못 쓰면 단일 책임 원칙을 어기고, 유지보수와 테스트가 어려워질 수 있습니다.
2. 해결책
싱글턴 패턴은 객체가 오직 하나만 만들어지도록 강제하면서도,
프로그램 어디에서든 그 객체에 접근할 수 있게 하기 위해 특정한 구조적 방법을 사용합니다.
핵심은 두 가지입니다:
1. 생성자를 막아서 직접 생성하지 못하게 한다
- 클래스의 생성자를 `private`(또는 `protected`)으로 선언합니다.
- 이렇게 하면 다른 클래스에서 `new` 키워드로 객체를 만들 수 없게 됩니다.
- 즉, 개발자가 실수로 객체를 여러 개 만들 수 없도록 차단하는 거죠.
2. 정적 메서드를 통해 단 하나의 인스턴스를 관리한다
- 클래스 내부에 `static` 필드로 인스턴스를 하나 저장해 둡니다.
- 외부에서는 오직 `getInstance()` 같은 정적 메서드를 통해서만 이 객체에 접근할 수 있습니다.
- 이 메서드는 처음 호출될 때만 객체를 생성하고, 그 이후엔 항상 같은 객체를 반환합니다.
public class Singleton {
private static Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
핵심은 객체 생성을 통제하고, 접근은 열어두는 구조를 만드는 것
생성은 내부에서 한 번만, 접근은 어디서든 가능하게!
이런 방식 덕분에 코드 어디에서든 싱글턴 인스턴스를 사용할 수 있고,
동시에 객체가 중복 생성되는 문제도 피할 수 있습니다.
구조 요약
- 싱글턴 클래스
- `getInstance()` 정적 메서드를 통해 접근
- 내부적으로 하나의 인스턴스를 관리
- 비공개 생성자
- 외부에서 직접 객체 생성을 막음
- 정적 필드
- 객체를 저장하고 재사용함
3. 구조
싱글턴 예시: `Database` 클래스
클래스: Database
목적: 앱 전역에서 하나의 DB 연결 인스턴스를 공유하도록 한다
1. 싱글턴 인스턴스를 저장할 정적 필드
private static instance: Database
- 클래스 내부에 `instance`라는 정적 필드를 선언
- 최초 한 번만 생성되어, 이후 재사용됨
2. 외부에서 직접 생성 못 하게 생성자를 `private`으로 선언
private constructor Database()
- `new Database()`를 외부에서 못 쓰게 막음
- 객체 생성은 클래스 내부에서만 가능함
3. 인스턴스를 생성하거나 반환하는 `getInstance()` 메서드
public static method getInstance()
if (instance == null)
acquireThreadLock()
if (instance == null) // 두 번째 체크 (double-checked locking)
instance = new Database()
return instance
- 이 메서드는 인스턴스가 없으면 새로 생성하고,
- 있으면 기존 인스턴스를 반환합니다.
- 멀티스레드 환경에서도 중복 생성되지 않도록 `lock`과 `double-check` 사용
4. 실제 비즈니스 로직을 수행하는 메서드
public method query(sql)
// 예: 쿼리 실행, 캐싱, 로깅 등
- 이 메서드를 통해 모든 DB 작업이 이루어짐
- 하나의 인스턴스를 공유하므로, DB 연결/캐시/스로틀링을 중앙 집중적으로 관리 가능
5. 클라이언트 코드 사용 예
method main()
db1 = Database.getInstance()
db1.query("SELECT * FROM users")
db2 = Database.getInstance()
db2.query("SELECT * FROM orders")
// db1과 db2는 동일한 인스턴스를 참조
4. 이 패턴은 언제 쓰면 좋을까?
1. 프로그램 전체에서 인스턴스를 하나만 공유해야 할 때
- 예: 데이터베이스 연결 객체, 설정 관리 객체, 로깅 시스템
- 이런 객체는 여러 개 만들면 충돌하거나 자원을 낭비할 수 있습니다.
- 싱글턴은 이런 객체를 딱 하나만 만들고, 전체에서 함께 사용할 수 있게 해줍니다.
2. 전역 변수처럼 쓰고 싶지만, 더 안전하게 통제하고 싶을 때
- 전역 변수는 접근은 쉽지만 제어가 어렵고 위험성이 큽니다.
- 싱글턴은 같은 전역 접근성을 제공하면서도,
- 외부에서 인스턴스를 덮어쓰지 못하도록 막고
- 생성 시점도 통제할 수 있어 더 안정적입니다.
3. 리소스 절약 + 일관성 유지가 중요한 경우
- 객체를 여러 번 만들 필요 없이 재사용하므로 메모리 절약
- 같은 객체를 쓰기 때문에 상태가 일관되게 유지됨
5. 장점
- 인스턴스를 하나로 제한할 수 있다
- 애초에 한 개만 있어야 하는 객체(DB 연결, 설정 등)에 적합합니다.
- 전역 접근 지점을 제공한다
- `getInstance()`를 통해 어디서든 동일한 객체에 접근할 수 있습니다.
- 요청 시 생성(Lazy Initialization)
- 처음 필요할 때까지 객체 생성을 미루기 때문에 불필요한 자원 낭비를 줄일 수 있습니다.
6. 단점
- 단일 책임 원칙(SRP) 위반 가능성
- 싱글턴은 인스턴스 생성 관리와 전역 접근 제공이라는 두 역할을 맡기 때문에, 책임이 두 개로 분산될 수 있습니다.
- 테스트가 어렵다
- 대부분의 싱글턴 클래스는 정적 메서드와 비공개 생성자를 사용합니다.
- 이는 Mocking이나 Dependency Injection이 어려워져 테스트 유연성을 해칩니다.
- 멀티스레드 환경에서 위험
- 동기화 처리를 하지 않으면 여러 개의 인스턴스가 동시에 생성될 수 있는 위험이 존재합니다.
- 강한 결합도(Coupling)
- 싱글턴 객체를 직접 참조하는 코드가 많아질수록,
- 해당 클래스에 대한 의존성이 높아지고 유지보수가 어려워집니다.
참고