본문으로 건너뛰기
Advertisement

17.2 생성 패턴 (Singleton, Factory, Builder)

1. Singleton (싱글톤 패턴)

"인스턴스가 오직 하나만 존재함을 보장" 합니다. 데이터베이스 연결, 로거, 설정 관리자처럼 전역에서 하나의 인스턴스만 필요할 때 사용합니다.

기본 구현 (Lazy Initialization + Thread-safe)

public class DatabaseManager {
// volatile: 멀티스레드 환경에서 가시성 보장
private static volatile DatabaseManager instance;
private String connectionUrl;

private DatabaseManager() { // private 생성자: 외부 생성 금지
this.connectionUrl = "jdbc:mysql://localhost:3306/mydb";
System.out.println("DB 매니저 초기화");
}

// Double-Checked Locking: 스레드 안전 + 성능 최적화
public static DatabaseManager getInstance() {
if (instance == null) {
synchronized (DatabaseManager.class) {
if (instance == null) {
instance = new DatabaseManager();
}
}
}
return instance;
}

public void query(String sql) {
System.out.println("[" + connectionUrl + "] 쿼리: " + sql);
}
}

// 사용
DatabaseManager db1 = DatabaseManager.getInstance();
DatabaseManager db2 = DatabaseManager.getInstance();
System.out.println(db1 == db2); // true (같은 인스턴스)
db1.query("SELECT * FROM users");

모던 자바: Enum Singleton (가장 안전한 방법)

// 직렬화, 리플렉션 공격에도 안전한 완벽한 싱글톤
public enum AppConfig {
INSTANCE;

private final String appName = "MyApp";
private final int maxConnections = 10;

public String getAppName() { return appName; }
public int getMaxConnections() { return maxConnections; }
public void log(String message) {
System.out.println("[" + appName + "] " + message);
}
}

// 사용
AppConfig.INSTANCE.log("애플리케이션 시작");
System.out.println(AppConfig.INSTANCE.getMaxConnections()); // 10

2. Factory Method (팩토리 메서드 패턴)

"객체 생성을 서브클래스에 위임" 합니다. 생성할 객체의 구체 클래스를 직접 명시하지 않고, 팩토리 메서드를 통해 결정합니다.

// 알림(Notification) 발송 예제
interface Notification {
void send(String message);
}

class EmailNotification implements Notification {
private final String email;
EmailNotification(String email) { this.email = email; }

@Override
public void send(String message) {
System.out.println("📧 이메일 [" + email + "] 전송: " + message);
}
}

class SmsNotification implements Notification {
private final String phone;
SmsNotification(String phone) { this.phone = phone; }

@Override
public void send(String message) {
System.out.println("📱 SMS [" + phone + "] 전송: " + message);
}
}

class PushNotification implements Notification {
private final String deviceId;
PushNotification(String deviceId) { this.deviceId = deviceId; }

@Override
public void send(String message) {
System.out.println("🔔 푸시 [" + deviceId + "] 전송: " + message);
}
}

// 팩토리 클래스
class NotificationFactory {
public static Notification create(String type, String target) {
return switch (type.toUpperCase()) {
case "EMAIL" -> new EmailNotification(target);
case "SMS" -> new SmsNotification(target);
case "PUSH" -> new PushNotification(target);
default -> throw new IllegalArgumentException("알 수 없는 알림 유형: " + type);
};
}
}

// 사용 - 클라이언트는 구체 클래스를 몰라도 됨
Notification n1 = NotificationFactory.create("EMAIL", "user@example.com");
Notification n2 = NotificationFactory.create("SMS", "010-1234-5678");
Notification n3 = NotificationFactory.create("PUSH", "device_abc123");

n1.send("주문이 완료되었습니다."); // 📧 이메일 전송
n2.send("배송이 시작되었습니다."); // 📱 SMS 전송
n3.send("새 메시지가 있습니다."); // 🔔 푸시 전송

3. Builder (빌더 패턴)

"복잡한 객체의 생성 과정을 단계별로 분리" 합니다. 생성자 매개변수가 많거나 선택적 필드가 많을 때 특히 유용합니다.

문제: 텔레스코핑 생성자 안티패턴

// ❌ 생성자가 너무 많고, 매개변수 순서 실수하기 쉬움
class Pizza {
Pizza(String size) { ... }
Pizza(String size, String crust) { ... }
Pizza(String size, String crust, boolean cheese) { ... }
Pizza(String size, String crust, boolean cheese, boolean tomato) { ... }
// 매개변수가 많아질수록 파악 불가능...
}

Builder 패턴 구현

public class Pizza {
// 필수 필드
private final String size;
private final String crust;
// 선택 필드
private final boolean cheese;
private final boolean pepperoni;
private final boolean mushroom;
private final boolean tomato;
private final String note;

private Pizza(Builder builder) {
this.size = builder.size;
this.crust = builder.crust;
this.cheese = builder.cheese;
this.pepperoni = builder.pepperoni;
this.mushroom = builder.mushroom;
this.tomato = builder.tomato;
this.note = builder.note;
}

@Override
public String toString() {
return String.format(
"Pizza[%s, %s크러스트, 치즈=%b, 페퍼로니=%b, 버섯=%b, 토마토=%b, 메모='%s']",
size, crust, cheese, pepperoni, mushroom, tomato, note);
}

// 정적 내부 빌더 클래스
public static class Builder {
// 필수
private final String size;
private final String crust;
// 선택 (기본값 설정)
private boolean cheese = false;
private boolean pepperoni = false;
private boolean mushroom = false;
private boolean tomato = false;
private String note = "";

public Builder(String size, String crust) {
this.size = size;
this.crust = crust;
}

public Builder cheese() { this.cheese = true; return this; }
public Builder pepperoni() { this.pepperoni = true; return this; }
public Builder mushroom() { this.mushroom = true; return this; }
public Builder tomato() { this.tomato = true; return this; }
public Builder note(String note) { this.note = note; return this; }

public Pizza build() { return new Pizza(this); }
}
}

// 사용: 읽기 쉽고 실수 없는 객체 생성
Pizza pizza1 = new Pizza.Builder("Large", "씬")
.cheese()
.pepperoni()
.mushroom()
.note("덜 맵게 해주세요")
.build();

Pizza pizza2 = new Pizza.Builder("Medium", "두꺼운")
.cheese()
.tomato()
.build();

System.out.println(pizza1);
System.out.println(pizza2);

Lombok @Builder 어노테이션 (실무)

실무에서는 Lombok 라이브러리의 @Builder 어노테이션으로 보일러플레이트 없이 빌더를 자동 생성합니다:

import lombok.Builder;
import lombok.ToString;

@Builder
@ToString
public class User {
private String name;
private int age;
private String email;
@Builder.Default
private boolean active = true;
}

// Lombok이 Builder 클래스를 자동 생성
User user = User.builder()
.name("홍길동")
.age(30)
.email("hong@example.com")
.build();

고수 팁

패턴 남용 주의:

  • Singleton: 전역 상태를 만들어 테스트를 어렵게 합니다. 스프링에서는 @Component, @Service 빈이 기본적으로 싱글톤이므로, 별도 싱글톤 구현이 거의 필요 없습니다.

  • Builder: Lombok @Builder를 사용하면 코드를 직접 작성할 필요가 없습니다. 그러나 불변 객체를 원하면 필드에 final을 붙이고 setter를 제거해야 합니다.

  • Factory: 생성 로직이 단순할 때는 switch 표현식이나 Map<String, Supplier<T>> 패턴으로 충분합니다.

Advertisement