new DatabaseConnection()
new DatabaseConnection()
new DatabaseConnection()
이 코드가 문제인 이유를 처음엔 잘 모를 수 있다. DB 연결을 매번 새로 만들면 메모리도 낭비고, 연결 수도 무한정 늘어난다. 하나만 만들어서 계속 재사용하면 되는데, 그걸 보장하는 방법이 Singleton이다.
인스턴스를 딱 하나만 생성하고, 어디서든 그 하나를 공유해서 쓰는 패턴이다.
애플리케이션 전체에서 하나만 존재해야 하는 객체에 적합하다. 대표적으로 DB 연결, 로그 시스템, 설정(Config) 객체 같은 것들이다.
public class AppConfig {
// 클래스 자신을 담는 static 변수
private static AppConfig instance;
// 외부에서 new 못 하게 막음
private AppConfig() {}
public static AppConfig getInstance() {
if (instance == null) {
instance = new AppConfig();
}
return instance;
}
public String getDbUrl() {
return "jdbc:mysql://localhost:3306/mydb";
}
}
AppConfig config1 = AppConfig.getInstance();
AppConfig config2 = AppConfig.getInstance();
System.out.println(config1 == config2); // true — 같은 객체
핵심은 세 가지다.
private static 변수로 인스턴스를 직접 보관private 생성자로 외부에서 new 하는 것을 차단static 메서드로 인스턴스를 제공
위 구현에는 허점이 있다. 두 스레드가 동시에 getInstance()를 호출하면 instance == null 체크를 동시에 통과해서 인스턴스가 두 개 만들어질 수 있다.
이를 해결하는 가장 간단한 방법은 synchronized를 쓰는 것이지만, 호출마다 동기화 비용이 생긴다. 더 나은 방법은 클래스 로딩 시점에 인스턴스를 만들어버리는 것이다.
public class AppConfig {
// 클래스가 로드될 때 바로 생성
private static final AppConfig instance = new AppConfig();
private AppConfig() {}
public static AppConfig getInstance() {
return instance;
}
}
JVM은 클래스를 처음 로드할 때 static 필드를 초기화하는데, 이 과정은 스레드 안전이 보장된다. 인스턴스를 실제로 쓰기 전에 미리 만들어둔다는 뜻에서 Eager Initialization이라고 부른다.
반대로, 상태를 많이 갖는 객체를 Singleton으로 만들면 테스트하기 어려워진다. Singleton은 전역 상태(global state)이기 때문이다. 꼭 필요한 곳에만 쓰는 것이 좋다.
Singleton의 핵심은 "생성을 통제한다"는 것이다. new를 막고 하나의 경로만 열어두는 것, 그게 전부다.