Lazy Holder 패턴은 static 내부 클래스를 이용해서 싱글톤 인스턴스를 지연 생성(Lazy Initialization)하는 방법입니다.
public class Singleton {
// 1. private 생성자 - 외부에서 new 불가
private Singleton() {
System.out.println("Singleton 인스턴스 생성됨!");
}
// 2. static 내부 클래스 - 핵심!
// 이 클래스는 Singleton 클래스가 로딩될 때 로딩되지 않음
// getInstance()가 호출될 때 처음 로딩됨
private static class LazyHolder {
// 3. LazyHolder가 로딩될 때 인스턴스 생성
private static final Singleton INSTANCE = new Singleton();
}
// 4. 외부에서 인스턴스를 가져오는 메서드
public static Singleton getInstance() {
// 이 메서드가 처음 호출될 때 LazyHolder 클래스가 로딩됨
return LazyHolder.INSTANCE;
}
// 5. 실제 비즈니스 메서드들
public void doSomething() {
System.out.println("작업 수행 중...");
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
// === 1단계 ===
System.out.println("1. 프로그램 시작");
// 아직 Singleton 클래스도 로딩 안됨
Thread.sleep(1000);
// === 2단계 ===
System.out.println("\n2. Singleton 클래스 참조");
Class clazz = Singleton.class;
// Singleton 클래스만 로딩됨
// LazyHolder 클래스는 아직 로딩 안됨!
Thread.sleep(1000);
// === 3단계 ===
System.out.println("\n3. getInstance() 첫 호출");
Singleton s1 = Singleton.getInstance();
// 이때! LazyHolder 클래스가 처음 로딩됨
// LazyHolder의 INSTANCE가 생성됨
// 출력: "Singleton 인스턴스 생성됨!"
Thread.sleep(1000);
// === 4단계 ===
System.out.println("\n4. getInstance() 두 번째 호출");
Singleton s2 = Singleton.getInstance();
// 이미 생성된 INSTANCE를 반환
// 생성자 호출 안됨!
System.out.println("\n5. 같은 인스턴스? " + (s1 == s2)); // true
}
}
실행 결과:
1. 프로그램 시작
2. Singleton 클래스 참조
3. getInstance() 첫 호출
Singleton 인스턴스 생성됨!
4. getInstance() 두 번째 호출
5. 같은 인스턴스? true
[프로그램 시작]
↓
[Singleton.class 참조]
↓ (Singleton 클래스만 메모리에 로드)
↓ (LazyHolder는 아직 로드 안됨!)
↓
[getInstance() 첫 호출]
↓
→ LazyHolder 클래스 로드 시작
→ LazyHolder.INSTANCE 초기화 시작
→ new Singleton() 실행
→ 인스턴스 생성 완료
→ INSTANCE에 저장
↓
[getInstance() 반환]
↓
→ 저장된 INSTANCE 반환 (생성 안함)
// 여러 Thread가 동시에 getInstance() 호출
public class MultiThreadTest {
public static void main(String[] args) {
// 10개의 Thread가 동시에 getInstance() 호출
for (int i = 0; i < 10; i++) {
final int threadNum = i;
new Thread(() -> {
Singleton instance = Singleton.getInstance();
System.out.println("Thread " + threadNum + ": " +
System.identityHashCode(instance));
}).start();
}
}
}
실행 결과:
Singleton 인스턴스 생성됨!
Thread 0: 123456789
Thread 1: 123456789
Thread 2: 123456789
Thread 3: 123456789
...
(모두 같은 해시코드 = 같은 인스턴스)
왜 Thread-Safe 한가?
JVM의 클래스 로더가 보장
static final의 특성
// JVM이 내부적으로 이렇게 동작
synchronized (LazyHolder.class) {
if (!LazyHolder.isLoaded()) {
// 클래스 로딩
// INSTANCE 초기화
LazyHolder.markAsLoaded();
}
}
public class DatabaseConnection {
private Connection connection;
// private 생성자 - DB 연결 초기화
private DatabaseConnection() {
try {
System.out.println("DB 연결 초기화 중...");
// 실제로는 시간이 오래 걸리는 작업
Thread.sleep(2000);
this.connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mydb",
"user",
"password"
);
System.out.println("DB 연결 완료!");
} catch (Exception e) {
throw new RuntimeException("DB 연결 실패", e);
}
}
// Lazy Holder
private static class Holder {
private static final DatabaseConnection INSTANCE =
new DatabaseConnection();
}
public static DatabaseConnection getInstance() {
return Holder.INSTANCE;
}
public Connection getConnection() {
return connection;
}
public void executeQuery(String sql) {
System.out.println("쿼리 실행: " + sql);
// 실제 쿼리 실행 로직
}
}
// 사용
public class Application {
public static void main(String[] args) {
System.out.println("애플리케이션 시작");
System.out.println("여러 작업 수행 중...");
// 필요한 시점에만 DB 연결 생성
System.out.println("\nDB 작업 필요!");
DatabaseConnection db = DatabaseConnection.getInstance();
db.executeQuery("SELECT * FROM users");
}
}
실행 결과:
애플리케이션 시작
여러 작업 수행 중...
DB 작업 필요!
DB 연결 초기화 중...
DB 연결 완료!
쿼리 실행: SELECT * FROM users
public class ConfigManager {
private Properties properties;
private ConfigManager() {
System.out.println("설정 파일 로딩 중...");
properties = new Properties();
try {
// 설정 파일 읽기 (시간 소요)
FileInputStream fis = new FileInputStream("config.properties");
properties.load(fis);
System.out.println("설정 로딩 완료!");
} catch (IOException e) {
// 기본 설정 사용
properties.setProperty("app.name", "MyApp");
properties.setProperty("app.version", "1.0.0");
properties.setProperty("max.connections", "100");
}
}
private static class Holder {
private static final ConfigManager INSTANCE = new ConfigManager();
}
public static ConfigManager getInstance() {
return Holder.INSTANCE;
}
public String getProperty(String key) {
return properties.getProperty(key);
}
public int getIntProperty(String key, int defaultValue) {
String value = properties.getProperty(key);
return value != null ? Integer.parseInt(value) : defaultValue;
}
}
// 사용
public class Service {
public void start() {
// 필요할 때만 설정 로드
ConfigManager config = ConfigManager.getInstance();
String appName = config.getProperty("app.name");
int maxConnections = config.getIntProperty("max.connections", 10);
System.out.println("앱 이름: " + appName);
System.out.println("최대 연결 수: " + maxConnections);
}
}
장점:
단점:
Enum(열거형)은 Java에서 상수들의 집합을 정의하는 특별한 클래스입니다. Enum의 각 상수는 해당 Enum 타입의 인스턴스이며, JVM이 자동으로 싱글톤을 보장합니다.
// 가장 단순한 Enum - 요일
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
// 사용
public class EnumBasic {
public static void main(String[] args) {
Day today = Day.MONDAY;
System.out.println(today); // MONDAY
// 비교
if (today == Day.MONDAY) {
System.out.println("월요일입니다!");
}
// switch 문
switch (today) {
case MONDAY:
System.out.println("한 주의 시작");
break;
case FRIDAY:
System.out.println("불금!");
break;
}
}
}
// Enum에 필드와 메서드 추가
public enum Day {
// 각 상수는 Day 클래스의 인스턴스
MONDAY("월요일", 1),
TUESDAY("화요일", 2),
WEDNESDAY("수요일", 3),
THURSDAY("목요일", 4),
FRIDAY("금요일", 5),
SATURDAY("토요일", 6),
SUNDAY("일요일", 7);
// 필드
private final String koreanName;
private final int dayNumber;
// 생성자 (항상 private)
Day(String koreanName, int dayNumber) {
this.koreanName = koreanName;
this.dayNumber = dayNumber;
}
// 메서드
public String getKoreanName() {
return koreanName;
}
public int getDayNumber() {
return dayNumber;
}
public boolean isWeekend() {
return this == SATURDAY || this == SUNDAY;
}
}
// 사용
public class EnumWithFields {
public static void main(String[] args) {
Day today = Day.FRIDAY;
System.out.println(today.getKoreanName()); // 금요일
System.out.println(today.getDayNumber()); // 5
System.out.println(today.isWeekend()); // false
// 모든 Enum 상수 순회
for (Day day : Day.values()) {
System.out.println(day + " = " + day.getKoreanName());
}
}
}
public enum Day {
MONDAY, TUESDAY;
}
public class EnumSingleton {
public static void main(String[] args) {
Day day1 = Day.MONDAY;
Day day2 = Day.MONDAY;
// 같은 인스턴스!
System.out.println(day1 == day2); // true
System.out.println(System.identityHashCode(day1)); // 같은 해시코드
System.out.println(System.identityHashCode(day2)); // 같은 해시코드
}
}
public enum Singleton {
INSTANCE; // 이게 싱글톤 인스턴스!
// 필드
private int value;
private String data;
// 생성자 - Enum 상수가 생성될 때 딱 한 번만 호출됨
Singleton() {
System.out.println("Singleton 인스턴스 생성!");
this.value = 0;
this.data = "초기 데이터";
}
// 메서드들
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public void doSomething() {
System.out.println("작업 수행: " + data);
}
}
public class Main {
public static void main(String[] args) {
// INSTANCE로 접근
Singleton s1 = Singleton.INSTANCE;
s1.setValue(100);
s1.setData("첫 번째 데이터");
s1.doSomething();
// 다른 곳에서 접근해도 같은 인스턴스
Singleton s2 = Singleton.INSTANCE;
System.out.println(s2.getValue()); // 100 (s1에서 설정한 값)
System.out.println(s2.getData()); // "첫 번째 데이터"
// 같은 인스턴스 확인
System.out.println(s1 == s2); // true
}
}
실행 결과:
Singleton 인스턴스 생성!
작업 수행: 첫 번째 데이터
100
첫 번째 데이터
true
// Enum은 내부적으로 이렇게 동작
public final class Singleton extends Enum {
// JVM이 자동으로 생성
public static final Singleton INSTANCE = new Singleton();
private Singleton() {
// 생성자는 JVM만 호출 가능
}
}
public class ReflectionAttackTest {
public static void main(String[] args) {
try {
// Lazy Holder 패턴 - 리플렉션으로 공격 가능
Constructor constructor1 =
LazyHolderSingleton.class.getDeclaredConstructor();
constructor1.setAccessible(true);
LazyHolderSingleton instance1 = constructor1.newInstance();
LazyHolderSingleton instance2 = constructor1.newInstance();
System.out.println("LazyHolder 공격 성공: " + (instance1 != instance2));
// Enum 싱글톤 - 리플렉션 방어!
Constructor constructor2 =
EnumSingleton.class.getDeclaredConstructor();
constructor2.setAccessible(true);
EnumSingleton instance3 = constructor2.newInstance(); // 예외 발생!
} catch (Exception e) {
System.out.println("Enum 방어 성공: " + e.getMessage());
// "Cannot reflectively create enum objects" 출력
}
}
}
public class SerializationTest {
public static void main(String[] args) throws Exception {
// Enum 싱글톤 직렬화
Singleton instance1 = Singleton.INSTANCE;
instance1.setValue(100);
// 직렬화
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("singleton.ser"));
oos.writeObject(instance1);
oos.close();
// 역직렬화
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("singleton.ser"));
Singleton instance2 = (Singleton) ois.readObject();
ois.close();
// 같은 인스턴스! (Enum은 자동으로 보장)
System.out.println(instance1 == instance2); // true
System.out.println(instance2.getValue()); // 100
}
}
public enum ApplicationContext {
INSTANCE;
private final Map beans = new ConcurrentHashMap<>();
private boolean initialized = false;
ApplicationContext() {
System.out.println("ApplicationContext 초기화");
}
public void initialize() {
if (initialized) {
return;
}
// 빈 등록
beans.put("userService", new UserService());
beans.put("orderService", new OrderService());
beans.put("emailService", new EmailService());
initialized = true;
System.out.println("빈 초기화 완료");
}
@SuppressWarnings("unchecked")
public T getBean(String name, Class type) {
Object bean = beans.get(name);
if (bean == null) {
throw new IllegalArgumentException("Bean not found: " + name);
}
return (T) bean;
}
public void registerBean(String name, Object bean) {
beans.put(name, bean);
}
}
// 사용
public class Application {
public static void main(String[] args) {
// 어디서든 같은 컨텍스트 접근
ApplicationContext context = ApplicationContext.INSTANCE;
context.initialize();
// 빈 가져오기
UserService userService = context.getBean("userService", UserService.class);
userService.createUser("홍길동");
// 다른 곳에서도 같은 컨텍스트
ApplicationContext ctx2 = ApplicationContext.INSTANCE;
OrderService orderService = ctx2.getBean("orderService", OrderService.class);
orderService.createOrder("ORDER-001");
}
}
public enum Logger {
INSTANCE;
private final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public void info(String message) {
log("INFO", message);
}
public void error(String message) {
log("ERROR", message);
}
public void warn(String message) {
log("WARN", message);
}
private void log(String level, String message) {
String timestamp = LocalDateTime.now().format(formatter);
System.out.println(String.format("[%s] [%s] %s",
timestamp, level, message));
}
}
// 사용 - 어디서든 같은 로거
public class Service {
public void processOrder() {
Logger.INSTANCE.info("주문 처리 시작");
try {
// 주문 처리 로직
Logger.INSTANCE.info("주문 처리 완료");
} catch (Exception e) {
Logger.INSTANCE.error("주문 처리 실패: " + e.getMessage());
}
}
}
public enum CacheManager {
INSTANCE;
private final Map cache = new ConcurrentHashMap<>();
private static class CacheEntry {
Object value;
long expireTime;
CacheEntry(Object value, long ttl) {
this.value = value;
this.expireTime = System.currentTimeMillis() + ttl;
}
boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}
public void put(String key, Object value, long ttlMillis) {
cache.put(key, new CacheEntry(value, ttlMillis));
}
public Object get(String key) {
CacheEntry entry = cache.get(key);
if (entry == null) {
return null;
}
if (entry.isExpired()) {
cache.remove(key);
return null;
}
return entry.value;
}
public void clear() {
cache.clear();
}
public int size() {
// 만료된 항목 제거
cache.entrySet().removeIf(e -> e.getValue().isExpired());
return cache.size();
}
}
// 사용
public class UserService {
public User getUser(String userId) {
// 캐시 확인
User cached = (User) CacheManager.INSTANCE.get("user:" + userId);
if (cached != null) {
System.out.println("캐시에서 조회");
return cached;
}
// DB에서 조회
System.out.println("DB에서 조회");
User user = findUserFromDB(userId);
// 캐시에 저장 (5분)
CacheManager.INSTANCE.put("user:" + userId, user, 5 * 60 * 1000);
return user;
}
}
장점:
단점:
// Lazy Holder - 더 복잡
public class LazyHolderSingleton {
private LazyHolderSingleton() {}
private static class Holder {
private static final LazyHolderSingleton INSTANCE =
new LazyHolderSingleton();
}
public static LazyHolderSingleton getInstance() {
return Holder.INSTANCE;
}
}
// Enum - 매우 간결
public enum EnumSingleton {
INSTANCE;
}
// 사용도 간결
LazyHolderSingleton.getInstance().doSomething(); // Lazy Holder
EnumSingleton.INSTANCE.doSomething(); // Enum
| 특징 | Lazy Holder | Enum |
|---|---|---|
| 코드 복잡도 | 중간 (내부 클래스 필요) | 매우 낮음 |
| Lazy Loading | ✅ 가능 | ❌ 불가 (클래스 로딩 시 생성) |
| Thread-Safe | ✅ (JVM 보장) | ✅ (JVM 보장) |
| 리플렉션 방어 | ❌ 취약 | ✅ 자동 방어 |
| 직렬화 안전 | ❌ 추가 코드 필요 | ✅ 자동 보장 |
| 상속 | ✅ 가능 | ❌ 불가 (인터페이스 구현만 가능) |
| 메모리 | 효율적 | 약간 더 사용 |
| 권장도 | 높음 | 매우 높음 |
// 무거운 객체 + Lazy Loading 필요 → Lazy Holder
public class HeavyResourceManager {
private HeavyResourceManager() {
// 10초 걸리는 초기화
loadHeavyResources();
}
private static class Holder {
private static final HeavyResourceManager INSTANCE =
new HeavyResourceManager();
}
public static HeavyResourceManager getInstance() {
return Holder.INSTANCE;
}
}
// 일반적인 경우 → Enum (권장!)
public enum ConfigManager {
INSTANCE;
private Properties config = new Properties();
public String getProperty(String key) {
return config.getProperty(key);
}
}
// 상속이 필요한 경우 → Lazy Holder
public abstract class BaseService {
public abstract void process();
}
public class ServiceManager extends BaseService {
private ServiceManager() {}
private static class Holder {
private static final ServiceManager INSTANCE = new ServiceManager();
}
public static ServiceManager getInstance() {
return Holder.INSTANCE;
}
@Override
public void process() {
// 구현
}
}
// 일반적인 경우: Enum 사용 (90% 케이스)
public enum ApplicationConfig {
INSTANCE;
// 간단하고 안전함
}
// 특별한 경우만 Lazy Holder 사용:
// 1. 초기화가 무겁고 사용 안 될 수도 있는 경우
// 2. 상속이 필요한 경우
// 3. 인터페이스 구현만으로는 부족한 경우