이종관
글 목록으로

생성 패턴(Creational Patterns): 객체 생성의 캡슐화

Singleton, Factory, Builder, Prototype, Object Pool과 DI까지 — 생성 패턴 총정리

2025년 1월 9일·13 min read·
architecture
oop
design-pattern
creational-pattern
dependency-injection

개요

생성 패턴은 객체 생성 과정의 복잡성을 캡슐화하여, 시스템이 어떤 객체를 어떻게 생성하는지에 독립적이도록 만드는 디자인 패턴이다. GoF(Gang of Four)가 정의한 5가지 생성 패턴과 함께, 현대적 대안인 의존성 주입(DI)과 함수형 합성(Functional Composition) 접근법까지 다룬다.

Singleton 패턴

정의

클래스의 인스턴스가 오직 하나만 존재하도록 보장하고, 이에 대한 전역적 접근점(Global Access Point)을 제공하는 패턴이다.

활용 사례

  • 데이터베이스 커넥션 풀 (Connection Pool): 커넥션 생성 비용이 높으므로 풀을 공유
  • 로거 (Logger): 전역적으로 일관된 로깅
  • 설정 관리자 (Configuration Manager): 애플리케이션 설정의 단일 접근점
  • 캐시 매니저 (Cache Manager): Redis 클라이언트 인스턴스 공유

구현 예시

java
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 컨테이너의 스코프 관리

java
// Spring: Singleton 스코프 (기본값)
@Component
@Scope("singleton") // DI 컨테이너가 인스턴스 관리
public class DatabasePool {
    // 직접 Singleton 패턴을 구현할 필요 없음
    // 테스트 시 Mock으로 대체 용이
}

Factory Method 패턴

정의

객체 생성을 위한 인터페이스를 정의하되, 어떤 클래스의 인스턴스를 생성할지는 서브클래스에 위임하는 패턴이다.

문제 상황

java
// 직접 생성: 새로운 타입 추가 시 모든 생성 코드 수정 필요
Notification notification;
if (type.equals("email")) {
    notification = new EmailNotification();
} else if (type.equals("sms")) {
    notification = new SmsNotification();
}
// 새 타입마다 else if 추가... → OCP 위반

Factory Method 적용

java
// 생성자 인터페이스 정의
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

java
// 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 MethodAbstract Factory
생성 범위단일 제품제품 가족 (여러 관련 객체)
추상화 수준메서드 수준객체(팩토리) 수준
확장 방식서브클래스 추가새로운 팩토리 구현
일관성 보장개별 제품관련 제품 간 일관성

Builder 패턴

정의

복잡한 객체의 생성을 단계별로 수행할 수 있도록 하며, 동일한 생성 절차로 다른 표현을 만들 수 있게 하는 패턴이다.

문제 상황: 텔레스코핑 생성자 (Telescoping Constructor)

java
// 매개변수가 많으면 가독성이 급격히 저하
new HttpRequest("GET", "/api/users", null, headers, timeout, retry, cache, auth);

Builder 적용

java
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)**하여 새 객체를 생성하는 패턴이다. 객체 생성 비용이 높을 때 유용하다.

활용 사례

java
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에 포함되지 않지만 고성능 시스템에서 매우 중요하다.

활용 사례

java
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의 세 가지 유형

java
// 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 BootJava, KotlinIoC 컨테이너의 표준, 자동 설정
NestJSTypeScript데코레이터 기반 DI, Angular 영향
.NET DIC#내장 DI 컨테이너
Dagger / HiltAndroid (Kotlin)컴파일 타임 DI, 성능 최적
WireGo컴파일 타임 코드 생성 기반

함수형 합성 vs 상속 (Functional Composition vs Inheritance)

현대적 관점: "합성이 상속보다 낫다" (Composition over Inheritance)

2026년 현재, 함수형 프로그래밍의 영향으로 전통적인 상속 기반 생성 패턴의 대안이 널리 사용된다.

부분 적용 (Partial Application)

typescript
// 팩토리 대신 부분 적용으로 객체 생성
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 failed

Reader Monad를 통한 DI

함수형 프로그래밍에서는 Reader Monad로 의존성 주입을 구현한다:

typescript
// 의존성을 환경(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)

가장 고급 수준의 합성 패턴으로, 독립적인 함수를 다양한 방식으로 조합할 수 있다:

typescript
// 각 함수는 독립적으로 구현
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