생성 패턴(Creational Patterns): 객체 생성의 캡슐화
Singleton, Factory, Builder, Prototype, Object Pool과 DI까지 — 생성 패턴 총정리
개요
생성 패턴은 객체 생성 과정의 복잡성을 캡슐화하여, 시스템이 어떤 객체를 어떻게 생성하는지에 독립적이도록 만드는 디자인 패턴이다. GoF(Gang of Four)가 정의한 5가지 생성 패턴과 함께, 현대적 대안인 의존성 주입(DI)과 함수형 합성(Functional Composition) 접근법까지 다룬다.
Singleton 패턴
정의
클래스의 인스턴스가 오직 하나만 존재하도록 보장하고, 이에 대한 전역적 접근점(Global Access Point)을 제공하는 패턴이다.
활용 사례
- 데이터베이스 커넥션 풀 (Connection Pool): 커넥션 생성 비용이 높으므로 풀을 공유
- 로거 (Logger): 전역적으로 일관된 로깅
- 설정 관리자 (Configuration Manager): 애플리케이션 설정의 단일 접근점
- 캐시 매니저 (Cache Manager): Redis 클라이언트 인스턴스 공유
구현 예시
public class DatabasePool {
private static volatile DatabasePool instance;
private final HikariDataSource dataSource;
private DatabasePool() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost/mydb");
this.dataSource = new HikariDataSource(config);
}
// Double-Checked Locking (멀티스레드 안전)
public static DatabasePool getInstance() {
if (instance == null) {
synchronized (DatabasePool.class) {
if (instance == null) {
instance = new DatabasePool();
}
}
}
return instance;
}
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}Singleton의 문제점
| 문제 | 설명 |
|---|---|
| 테스트 어려움 | 전역 상태로 인해 테스트 격리가 어려움 |
| 숨겨진 의존성 | 코드 어디서든 접근 가능 → 의존성 추적이 어려움 |
| 멀티스레드 이슈 | 동시성 제어 필요 (Double-Checked Locking, volatile) |
| SOLID 위반 | SRP 위반 가능성 (인스턴스 관리 + 비즈니스 로직) |
| Tight Coupling | 전역 접근으로 인한 강한 결합 |
현대적 대안: DI 컨테이너의 스코프 관리
// Spring: Singleton 스코프 (기본값)
@Component
@Scope("singleton") // DI 컨테이너가 인스턴스 관리
public class DatabasePool {
// 직접 Singleton 패턴을 구현할 필요 없음
// 테스트 시 Mock으로 대체 용이
}Factory Method 패턴
정의
객체 생성을 위한 인터페이스를 정의하되, 어떤 클래스의 인스턴스를 생성할지는 서브클래스에 위임하는 패턴이다.
문제 상황
// 직접 생성: 새로운 타입 추가 시 모든 생성 코드 수정 필요
Notification notification;
if (type.equals("email")) {
notification = new EmailNotification();
} else if (type.equals("sms")) {
notification = new SmsNotification();
}
// 새 타입마다 else if 추가... → OCP 위반Factory Method 적용
// 생성자 인터페이스 정의
public abstract class NotificationFactory {
public abstract Notification createNotification();
// 템플릿 메서드와 결합 가능
public void send(String message) {
Notification notification = createNotification();
notification.setMessage(message);
notification.deliver();
}
}
public class EmailNotificationFactory extends NotificationFactory {
@Override
public Notification createNotification() {
return new EmailNotification();
}
}
public class SmsNotificationFactory extends NotificationFactory {
@Override
public Notification createNotification() {
return new SmsNotification();
}
}활용 사례
- 프레임워크에서 사용자 정의 객체 생성 (Spring의
BeanFactory) - 결제 게이트웨이(Payment Gateway) 연동 시 제공자별 클라이언트 생성
- 문서 편집기에서 문서 타입별 생성기
Abstract Factory 패턴
정의
관련 있는 객체의 가족(Family)을 생성하는 인터페이스를 제공하되, 구체적인 클래스를 지정하지 않는 패턴이다. Factory Method의 상위 레벨 추상화이다.
구현 예시: 크로스 플랫폼 UI
// Abstract Factory
public interface UIFactory {
Button createButton();
TextField createTextField();
Dialog createDialog();
}
// 구체 팩토리: Windows 플랫폼
public class WindowsUIFactory implements UIFactory {
public Button createButton() { return new WindowsButton(); }
public TextField createTextField() { return new WindowsTextField(); }
public Dialog createDialog() { return new WindowsDialog(); }
}
// 구체 팩토리: macOS 플랫폼
public class MacUIFactory implements UIFactory {
public Button createButton() { return new MacButton(); }
public TextField createTextField() { return new MacTextField(); }
public Dialog createDialog() { return new MacDialog(); }
}
// 클라이언트 코드: 구체 팩토리에 의존하지 않음
public class Application {
private final UIFactory factory;
public Application(UIFactory factory) {
this.factory = factory;
}
public void createUI() {
Button button = factory.createButton();
TextField field = factory.createTextField();
}
}Factory Method vs Abstract Factory
| 비교 항목 | Factory Method | Abstract Factory |
|---|---|---|
| 생성 범위 | 단일 제품 | 제품 가족 (여러 관련 객체) |
| 추상화 수준 | 메서드 수준 | 객체(팩토리) 수준 |
| 확장 방식 | 서브클래스 추가 | 새로운 팩토리 구현 |
| 일관성 보장 | 개별 제품 | 관련 제품 간 일관성 |
Builder 패턴
정의
복잡한 객체의 생성을 단계별로 수행할 수 있도록 하며, 동일한 생성 절차로 다른 표현을 만들 수 있게 하는 패턴이다.
문제 상황: 텔레스코핑 생성자 (Telescoping Constructor)
// 매개변수가 많으면 가독성이 급격히 저하
new HttpRequest("GET", "/api/users", null, headers, timeout, retry, cache, auth);Builder 적용
public class HttpRequest {
private final String method;
private final String url;
private final Map<String, String> headers;
private final int timeout;
private final int retryCount;
private final boolean cacheEnabled;
private HttpRequest(Builder builder) {
this.method = builder.method;
this.url = builder.url;
this.headers = Map.copyOf(builder.headers); // 불변 복사
this.timeout = builder.timeout;
this.retryCount = builder.retryCount;
this.cacheEnabled = builder.cacheEnabled;
}
public static class Builder {
private final String method; // 필수
private final String url; // 필수
private Map<String, String> headers = new HashMap<>();
private int timeout = 30000;
private int retryCount = 3;
private boolean cacheEnabled = false;
public Builder(String method, String url) {
this.method = method;
this.url = url;
}
public Builder header(String key, String value) {
this.headers.put(key, value);
return this; // Fluent API
}
public Builder timeout(int ms) {
this.timeout = ms;
return this;
}
public Builder retry(int count) {
this.retryCount = count;
return this;
}
public Builder cache(boolean enabled) {
this.cacheEnabled = enabled;
return this;
}
public HttpRequest build() {
// 유효성 검증
if (method == null || url == null) {
throw new IllegalStateException("method and url are required");
}
return new HttpRequest(this);
}
}
}
// 사용: 가독성이 높은 Fluent API
HttpRequest request = new HttpRequest.Builder("POST", "/api/orders")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token")
.timeout(5000)
.retry(5)
.cache(true)
.build();Builder의 장점
- 불변 객체(Immutable Object) 생성에 적합
- 필수/선택 매개변수 구분이 명확
- Fluent API로 가독성 향상
- Java의 Lombok
@Builder, Kotlin의data class와 결합
Prototype 패턴
정의
**기존 객체를 복사(Clone)**하여 새 객체를 생성하는 패턴이다. 객체 생성 비용이 높을 때 유용하다.
활용 사례
public interface DocumentPrototype extends Cloneable {
DocumentPrototype clone();
}
public class SpreadsheetTemplate implements DocumentPrototype {
private List<Sheet> sheets;
private Map<String, Style> styles;
private Configuration config;
// 깊은 복사 (Deep Copy)
@Override
public SpreadsheetTemplate clone() {
SpreadsheetTemplate copy = new SpreadsheetTemplate();
copy.sheets = this.sheets.stream()
.map(Sheet::clone)
.collect(Collectors.toList());
copy.styles = new HashMap<>(this.styles);
copy.config = this.config.clone();
return copy;
}
}
// 프로토타입 레지스트리
public class TemplateRegistry {
private final Map<String, DocumentPrototype> templates = new HashMap<>();
public void register(String key, DocumentPrototype prototype) {
templates.put(key, prototype);
}
public DocumentPrototype create(String key) {
return templates.get(key).clone();
}
}깊은 복사 vs 얕은 복사
| 복사 방식 | 설명 | 주의사항 |
|---|---|---|
| 얕은 복사 (Shallow Copy) | 참조만 복사 | 원본과 복사본이 내부 객체 공유 |
| 깊은 복사 (Deep Copy) | 모든 내부 객체까지 재귀적 복사 | 순환 참조 주의, 비용 높음 |
Object Pool 패턴
정의
생성 비용이 높은 객체를 미리 생성해 풀(Pool)에 보관하고, 필요할 때 대여/반환하는 패턴이다. GoF에 포함되지 않지만 고성능 시스템에서 매우 중요하다.
활용 사례
public class ConnectionPool {
private final Queue<Connection> available = new ConcurrentLinkedQueue<>();
private final Set<Connection> inUse = ConcurrentHashMap.newKeySet();
private final int maxSize;
public ConnectionPool(int maxSize) {
this.maxSize = maxSize;
// 초기 커넥션 생성
for (int i = 0; i < maxSize; i++) {
available.add(createConnection());
}
}
public synchronized Connection acquire() {
Connection conn = available.poll();
if (conn != null) {
inUse.add(conn);
return conn;
}
throw new PoolExhaustedException("No available connections");
}
public synchronized void release(Connection conn) {
inUse.remove(conn);
if (conn.isValid()) {
available.offer(conn);
} else {
available.offer(createConnection()); // 손상된 커넥션 교체
}
}
}대표적 구현체
- HikariCP: Java 최고 성능의 JDBC 커넥션 풀
- gRPC Channel Pool: gRPC 연결 풀링
- Thread Pool:
Executors.newFixedThreadPool()등
의존성 주입 (Dependency Injection as a Pattern)
생성 패턴의 진화
의존성 주입은 Factory 패턴의 다음 진화 단계(evolutionary step)이다. Factory가 "무엇을 생성할지"를 결정한다면, DI는 "누가 생성할지"를 외부(컨테이너)에 위임한다.
DI의 세 가지 유형
// 1. 생성자 주입 (Constructor Injection) -- 가장 권장
public class OrderService {
private final PaymentGateway paymentGateway;
private final InventoryService inventoryService;
public OrderService(PaymentGateway paymentGateway,
InventoryService inventoryService) {
this.paymentGateway = paymentGateway;
this.inventoryService = inventoryService;
}
}
// 2. 세터 주입 (Setter Injection) -- 선택적 의존성
public class ReportService {
private CacheService cache;
public void setCacheService(CacheService cache) {
this.cache = cache;
}
}
// 3. 메서드 주입 (Method Injection) -- 호출 시점에 결정
public class NotificationService {
public void send(NotificationChannel channel, String message) {
channel.deliver(message);
}
}DI 컨테이너 생태계 (2026)
| 프레임워크 | 언어 | 특징 |
|---|---|---|
| Spring / Spring Boot | Java, Kotlin | IoC 컨테이너의 표준, 자동 설정 |
| NestJS | TypeScript | 데코레이터 기반 DI, Angular 영향 |
| .NET DI | C# | 내장 DI 컨테이너 |
| Dagger / Hilt | Android (Kotlin) | 컴파일 타임 DI, 성능 최적 |
| Wire | Go | 컴파일 타임 코드 생성 기반 |
함수형 합성 vs 상속 (Functional Composition vs Inheritance)
현대적 관점: "합성이 상속보다 낫다" (Composition over Inheritance)
2026년 현재, 함수형 프로그래밍의 영향으로 전통적인 상속 기반 생성 패턴의 대안이 널리 사용된다.
부분 적용 (Partial Application)
// 팩토리 대신 부분 적용으로 객체 생성
const createLogger = (level: string) => (module: string) => (message: string) => {
console.log(`[${level}][${module}] ${message}`);
};
const errorLogger = createLogger("ERROR");
const authErrorLogger = errorLogger("auth");
authErrorLogger("Login failed"); // [ERROR][auth] Login failedReader Monad를 통한 DI
함수형 프로그래밍에서는 Reader Monad로 의존성 주입을 구현한다:
// 의존성을 환경(Environment)으로 전달
type Reader<Env, A> = (env: Env) => A;
interface AppEnv {
db: Database;
logger: Logger;
cache: CacheService;
}
const getUser: Reader<AppEnv, Promise<User>> =
(env) => env.db.query("SELECT * FROM users WHERE id = ?");
const logAndGetUser: Reader<AppEnv, Promise<User>> =
(env) => {
env.logger.info("Fetching user");
return getUser(env);
};Kleisli 합성 (Kleisli Composition)
가장 고급 수준의 합성 패턴으로, 독립적인 함수를 다양한 방식으로 조합할 수 있다:
// 각 함수는 독립적으로 구현
const validate = (input: RawOrder): Result<ValidOrder> => { /* ... */ };
const enrich = (order: ValidOrder): Result<EnrichedOrder> => { /* ... */ };
const persist = (order: EnrichedOrder): Result<SavedOrder> => { /* ... */ };
// Kleisli 합성으로 파이프라인 구성
const processOrder = compose(validate, enrich, persist);생성 패턴 선택 가이드
| 상황 | 권장 패턴 |
|---|---|
| 인스턴스가 하나만 필요 | DI 컨테이너 Singleton 스코프 (직접 Singleton 지양) |
| 객체 타입이 런타임에 결정 | Factory Method |
| 관련 객체 가족 생성 | Abstract Factory |
| 매개변수가 많은 불변 객체 | Builder |
| 기존 객체 복제 | Prototype |
| 생성 비용이 높은 자원 | Object Pool |
| 테스트 가능성 중시 | 의존성 주입 (DI) |
| 함수형 스타일 | Partial Application, Reader Monad |