← all posts
DEV 2026.05.02 · 12 min read Intermediate

Java 생성 패턴, 무엇을 왜 선택하는가

Singleton의 Thread-safety부터 Object Pool의 재사용 철학까지, Java 생성 패턴 6개를 관통하는 하나의 질문을 추적한다.


Singleton, Factory Method, Builder, Prototype, Abstract Factory, Object Pool — Java의 생성 패턴 6개는 모두 다른 문제를 해결하는 것처럼 보인다. 그런데 이 패턴들을 가로지르는 공통 질문이 하나 있다. “객체 생성의 제어권을 누가 가져야 하는가?”

제어권의 첫 번째 형태: 인스턴스 개수 통제

Singleton은 가장 단순한 주장을 한다 — 어떤 객체는 전 생애주기에서 딱 하나만 존재해야 한다. 설정 파일을 세 곳에서 각각 읽으면 메모리가 낭비되고 상태가 불일치한다. 커넥션 풀이 서비스마다 따로 생기면 DB 연결이 폭발한다.

구현 선택지는 여러 가지이지만 결론은 명확하다.

// Bill Pugh: JVM 클래스 로딩을 동기화 메커니즘으로 활용
public class BillPughSingleton {
    private static class SingletonHolder {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }
    public static BillPughSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

// Enum: Serialization·Reflection 공격까지 막는 가장 안전한 방법
public enum EnumSingleton {
    INSTANCE;
}

Lazy Initialization이 필요 없고 공격 방어가 중요하다면 Enum, 복잡한 초기화가 필요하다면 Bill Pugh. DCL(Double-Checked Locking)은 volatile 없이 쓰면 명령어 재배치로 깨진다 — 레거시 코드에서 조용히 버그가 나는 이유가 여기에 있다.

전역 상태의 대가

Singleton은 암묵적 의존성을 만든다. 테스트에서 상태를 격리하기 어렵고, 단일 책임 원칙을 어기기 쉽다. Spring 같은 DI 컨테이너가 있다면, 직접 Singleton을 구현하는 대신 빈 스코프로 위임하는 것이 더 나은 선택이다.

제어권의 두 번째 형태: 생성 로직 캡슐화

클라이언트가 구체 클래스를 직접 new로 생성하면, 새로운 타입이 추가될 때마다 클라이언트 코드도 수정해야 한다. Factory Method는 이 문제를 상속으로 푼다 — 어떤 클래스를 만들지는 서브클래스가 결정한다.

// Creator: 팩토리 메서드 선언
public abstract class Logistics {
    protected abstract Transport createTransport();  // 서브클래스가 결정

    public void planDelivery(String origin, String dest, int distance) {
        Transport transport = createTransport();     // 구체 타입 몰라도 됨
        transport.deliver(dest);
    }
}

// ConcreteCreator: 실제 타입 결정
public class RoadLogistics extends Logistics {
    @Override
    protected Transport createTransport() { return new Truck(); }
}

드론을 추가하려면 DroneLogistics를 하나 더 만들면 된다. 기존 LogisticsTruck은 건드리지 않는다. OCP(개방-폐쇄 원칙)가 이 구조로 지켜진다.

Abstract Factory는 한 단계 더 나아간다. 단일 객체가 아니라 관련 객체들의 집합(제품군) 을 일관성 있게 생성해야 할 때 쓴다. WindowsButtonMacCheckbox를 섞을 수 없도록 팩토리가 제품군 전체를 책임진다.

public interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
    TextField createTextField();
}

// 클라이언트는 GUIFactory만 알면 된다
public class Application {
    public Application(GUIFactory factory) {
        this.button = factory.createButton();     // Windows면 WindowsButton
        this.checkbox = factory.createCheckbox(); // 섞일 수 없음
    }
}

제어권의 세 번째 형태: 복잡한 초기화의 단계 분리

생성자 매개변수가 7개를 넘어가면 코드를 읽기 어렵다. 어떤 null이 어떤 필드인지 알 수 없고, 매개변수 순서를 틀려도 컴파일 에러가 나지 않는다. Builder는 이 문제를 Fluent API로 푼다.

User user = new User.Builder("john", "john@email.com")  // 필수만 생성자로
    .age(25)
    .phone("010-1234-5678")
    .newsletter(true)
    .build();  // build()에서 유효성 검증

핵심은 세 가지다. 필수 매개변수는 Builder 생성자에, 선택 매개변수는 메서드로, build()에서 유효성 검증. 모든 필드를 final로 선언하면 불변 객체가 된다 — Setter를 노출하지 않아도 된다.

Prototype은 다른 각도에서 초기화 비용을 공격한다. 객체 생성 자체가 무거울 때, 처음 한 번만 만들고 이후엔 복제한다. 게임 캐릭터를 클래스별로 한 번씩만 초기화해두고, 플레이어가 선택할 때마다 clone()으로 찍어내는 방식이다.

얕은 복사와 깊은 복사

super.clone()은 얕은 복사다. List, Map 같은 참조 타입 필드는 복제본이 원본과 같은 객체를 가리킨다. 복제 후 한쪽을 수정하면 다른 쪽도 바뀐다. clone() 안에서 참조 타입은 반드시 새로 생성해야 한다.

제어권의 네 번째 형태: 생명주기 관리

Object Pool은 생성 패턴 중 유일하게 소멸까지 관리한다. 생성 비용이 크고 재사용 가능한 객체 — DB 커넥션, 스레드, 게임 총알 — 는 만들고 버리는 것보다 대여하고 돌려받는 것이 훨씬 효율적이다.

acquire() → 작업 → release() → reset() → 다시 acquire() ...

reset()이 핵심이다. 반납할 때 이전 상태를 완전히 지워야 다음 사용자가 오염된 객체를 받지 않는다. 이 책임을 Reusable 인터페이스로 강제하는 구조가 안전하다.

실무에서 직접 구현하는 경우는 드물다. ExecutorService(스레드), HikariCP(커넥션), Apache Commons Pool2(범용)가 이미 검증된 구현을 제공한다.

트레이드오프

패턴 선택 기준
상황패턴이유
전역 유일 인스턴스Singleton (Enum)직렬화·Reflection 안전
단일 타입 생성 분리Factory Method서브클래스가 타입 결정
제품군 일관성Abstract Factory섞임 방지
복잡한 초기화, 불변 객체BuilderFluent API + 유효성 검증
무거운 초기화 한 번만Prototype복제가 생성보다 빠를 때
생성 비용이 크고 재사용 가능Object Pool대여·반납으로 GC 절감

패턴은 도구다. Factory Method는 클래스 수를 늘리고, Abstract Factory는 새 제품 추가가 어렵고, Builder는 보일러플레이트가 많다(Lombok으로 줄일 수 있다). Singleton의 전역 상태는 테스트를 어렵게 만든다. 비용을 알고 선택하는 것이 패턴을 아는 것보다 더 중요하다.

정리

  • 생성 패턴 6개는 모두 “객체 생성의 제어권”을 클라이언트에서 분리하는 방법이다.
  • Singleton·Prototype·Object Pool은 인스턴스 수명을 통제하고, Factory Method·Abstract Factory·Builder는 생성 로직을 캡슐화한다.
  • 각 패턴의 트레이드오프를 모르면 패턴이 오히려 복잡도를 키운다.

다음 글에서는 구조 패턴으로 넘어간다 — 이미 만들어진 객체들을 어떻게 조합하고 감싸는가.