생성 패턴으로 객체 생성 책임 분리하기
싱글턴, 팩토리 메서드, 추상 팩토리, 빌더, 프로토타입 패턴으로 객체 생성 책임을 분리하는 설계 방법
2026-08-14 · 최초 발행 2025-05-23
객체 생성 코드를 설계 대상으로 다루는 이유
객체 지향 시스템에서 인스턴스를 만드는 일은 단순히 클래스를 생성하는 데서 끝나지 않는다. 어떤 구체 클래스를 선택할지, 언제 만들지, 어떤 구성으로 조립할지를 코드 곳곳에서 결정하기 시작하면 변경 범위가 빠르게 넓어진다.
생성 패턴은 이 책임을 별도 구조로 옮긴다. 클라이언트가 구체 클래스에 직접 의존하지 않도록 만들고, 객체의 생성 과정과 표현 방식을 분리한다. 그 결과 시스템 구성에 따라 인스턴스 생성 방식을 바꾸기 쉬워지고, 구성 요소 사이의 결합도와 중복 생성 코드도 줄일 수 있다.
단일 인스턴스를 통제하는 싱글턴
싱글턴 패턴은 특정 클래스의 인스턴스가 시스템 안에 하나만 존재하도록 보장하고, 그 인스턴스에 접근할 지점을 제공한다.
Java에서는 생성자를 외부에 노출하지 않고 정적 메서드로 인스턴스를 반환하는 방식으로 구현할 수 있다. 아래 예시는 인스턴스가 아직 없을 때만 동기화 블록 안에서 생성한다.
public class DatabaseConnection {
private static DatabaseConnection instance;
private DatabaseConnection() {
// 초기화 코드
}
public static DatabaseConnection getInstance() {
if (instance == null) {
synchronized(DatabaseConnection.class) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
public void executeQuery(String sql) {
// 쿼리 실행 로직
}
}
로깅 시스템, 데이터베이스 연결 관리, 환경설정 객체처럼 공유 대상이 분명한 곳에서 사용할 수 있다. 인스턴스를 하나만 생성하므로 메모리를 아끼고 생성 시점도 제어할 수 있다.
다만 전역 접근점은 단일 책임 원칙(SRP)을 침해할 여지가 있으며, 테스트를 복잡하게 만들 수 있다. 멀티스레드 환경에서는 인스턴스 생성 과정도 신중히 다뤄야 한다.
생성할 제품을 하위 클래스에 맡기는 팩토리 메서드
팩토리 메서드는 제품을 만드는 인터페이스를 제공하되, 실제로 어떤 클래스의 인스턴스를 만들지는 하위 클래스가 결정하게 한다. 사용 코드는 생성 결과를 다루고, 구체 제품 선택은 생성자 쪽으로 분리된다.
문서 처리 흐름은 상위 클래스에 두고, PDF와 Word 문서의 생성만 각각의 하위 클래스가 맡는 예시다.
public abstract class DocumentCreator {
public abstract Document createDocument();
public void processDocument() {
Document doc = createDocument();
doc.open();
doc.process();
doc.save();
}
}
public class PDFDocumentCreator extends DocumentCreator {
@Override
public Document createDocument() {
return new PDFDocument();
}
}
public class WordDocumentCreator extends DocumentCreator {
@Override
public Document createDocument() {
return new WordDocument();
}
}
프레임워크의 확장 모듈, 플러그인 시스템, 여러 UI 컴포넌트를 만드는 구조에 맞는다. 생성 코드와 사용 코드가 분리되어 결합도가 낮아지고, 새 제품을 추가하기 쉽다.
반대로 제품마다 하위 클래스를 늘리는 방식이므로 클래스 계층이 불필요하게 복잡해질 수 있다.
함께 바뀌는 객체군을 다루는 추상 팩토리
추상 팩토리는 관련된 객체들의 집합을 생성하는 인터페이스다. 개별 객체를 따로 선택하는 대신, 서로 호환되는 제품군을 한 팩토리에서 함께 만든다.
GUIFactory를 통해 버튼과 체크박스를 만들면, 애플리케이션은 Windows 스타일과 MacOS 스타일이라는 구체 제품군을 직접 알 필요가 없다.
// 추상 팩토리 인터페이스
public interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
// 구체 팩토리 - Windows 스타일
public class WindowsFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
}
// 구체 팩토리 - MacOS 스타일
public class MacOSFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacOSButton();
}
@Override
public Checkbox createCheckbox() {
return new MacOSCheckbox();
}
}
// 클라이언트 코드
public class Application {
private Button button;
private Checkbox checkbox;
public Application(GUIFactory factory) {
button = factory.createButton();
checkbox = factory.createCheckbox();
}
public void render() {
button.render();
checkbox.render();
}
}
크로스 플랫폼 UI 라이브러리, 데이터베이스 연결 관리, 운영체제나 실행 환경별 컴포넌트 생성에 적용할 수 있다. 제품군의 일관성을 유지하고, 제품군 전체를 교체하기도 쉽다.
대신 새로운 종류의 제품을 추가하려면 팩토리 인터페이스 자체를 수정해야 하며, 코드 구조도 복잡해진다.
조립 과정과 결과물을 분리하는 빌더
빌더 패턴은 복잡한 객체를 만드는 절차를 표현 결과와 분리한다. 같은 생성 과정이라도 빌더 구현에 따라 다른 결과물을 만들 수 있다.
다음 코드는 컴퓨터를 구성하는 작업을 빌더에 맡기고, 디렉터가 조립 순서를 관리하는 형태다.
// 제품 클래스
public class Computer {
private String cpu;
private String ram;
private String storage;
private String gpu;
private String operatingSystem;
// Getters...
}
// 빌더 인터페이스
public interface ComputerBuilder {
void buildCPU(String cpu);
void buildRAM(String ram);
void buildStorage(String storage);
void buildGPU(String gpu);
void installOS(String os);
Computer getComputer();
}
// 구체 빌더 - 게이밍 컴퓨터
public class GamingComputerBuilder implements ComputerBuilder {
private Computer computer = new Computer();
@Override
public void buildCPU(String cpu) {
computer.setCpu("High-end " + cpu);
}
@Override
public void buildRAM(String ram) {
computer.setRam("32GB " + ram);
}
// 다른 메소드 구현...
@Override
public Computer getComputer() {
return computer;
}
}
// 디렉터 클래스
public class ComputerDirector {
private ComputerBuilder builder;
public ComputerDirector(ComputerBuilder builder) {
this.builder = builder;
}
public Computer buildComputer() {
builder.buildCPU("Intel i9");
builder.buildRAM("DDR4");
builder.buildStorage("2TB SSD");
builder.buildGPU("RTX 3080");
builder.installOS("Windows 11");
return builder.getComputer();
}
}
설정 항목이 많은 객체, 불변 객체, SQL 쿼리 빌더처럼 조립 절차를 명확히 드러내야 하는 경우에 유용하다. 생성 단계를 세밀하게 제어하고 복잡한 생성 코드를 분리할 수 있다.
하지만 단순한 객체에까지 적용하면 별도 빌더 클래스와 코드량만 늘어날 수 있다.
기존 객체를 복제하는 프로토타입
프로토타입 패턴은 이미 준비된 객체를 복제해 새 객체를 만든다. 초기화 비용이 큰 객체를 반복 생성해야 하거나, 초기 상태가 비슷한 변형을 여러 개 만들어야 할 때 적합하다.
문서 템플릿을 보관해 두고 요청 유형에 따라 복제하는 구현은 다음과 같다.
// 프로토타입 인터페이스
public interface Cloneable {
Object clone();
}
// 구체 프로토타입
public class Document implements Cloneable {
private String content;
private String formatting;
private List<String> images;
// 깊은 복사 구현
@Override
public Document clone() {
Document clone = new Document();
clone.content = this.content;
clone.formatting = this.formatting;
clone.images = new ArrayList<>(this.images);
return clone;
}
// 기타 메소드...
}
// 클라이언트 코드
public class DocumentProcessor {
private Map<String, Document> templates = new HashMap<>();
public void loadTemplates() {
Document report = new Document();
report.setContent("보고서 기본 내용");
report.setFormatting("회사 표준 포맷");
templates.put("report", report);
Document letter = new Document();
letter.setContent("안내문 기본 내용");
letter.setFormatting("공식 레터 포맷");
templates.put("letter", letter);
}
public Document createDocument(String type) {
return templates.get(type).clone();
}
}
DB 쿼리나 네트워크 요청처럼 객체 생성 비용이 높은 경우, 여러 변형에 공통 초기 상태가 필요한 경우, 캐싱 메커니즘에서 활용할 수 있다. 런타임에 객체 타입을 추가하거나 제거하기 쉽고, 상속 대신 복제를 통한 확장도 가능하다.
순환 참조가 있는 복잡한 객체는 복제가 어려울 수 있으며, 깊은 복사를 구현하는 부담도 고려해야 한다.
선택 기준은 생성 책임의 위치다
객체가 하나만 필요하다면 싱글턴을, 함께 호환되어야 하는 객체군을 만든다면 추상 팩토리를 검토할 수 있다. 복잡한 객체의 조립 절차를 분리하려면 빌더가 맞고, 생성 책임을 하위 클래스로 넘기려면 팩토리 메서드가 적합하다. 기존 객체를 바탕으로 새 인스턴스를 만들어야 한다면 프로토타입을 사용한다.
패턴은 함께 쓸 수도 있다. 팩토리 인스턴스를 싱글턴으로 관리하거나, 빌더와 추상 팩토리를 결합해 복잡한 객체 집합을 만드는 방식이 가능하다.
다만 싱글턴을 과도하게 쓰면 전역 상태 관리가 복잡해지고, 팩토리 계층을 지나치게 쌓으면 불필요한 추상화가 생긴다. 간단한 객체에 빌더를 적용하는 것도 오버엔지니어링이 될 수 있다.
DI와 서비스 구조에서의 생성 패턴
Spring, Dagger 같은 DI 프레임워크는 팩토리 패턴의 개념을 확장해 객체 생성 책임을 컨테이너에 위임한다. 이 방식은 객체를 사용하는 코드와 생성 코드를 분리해 결합도를 낮춘다.
함수형 프로그래밍의 영향으로는 불변 객체 생성을 위한 빌더 활용과 팩토리 함수를 통한 객체 생성 방식이 나타난다. 마이크로서비스 아키텍처에서는 서비스 인스턴스 관리에 싱글턴 패턴을 활용하거나, 서비스 생성에 추상 팩토리 패턴을 적용할 수 있다.
생성 패턴의 목적은 특정 패턴을 많이 쓰는 데 있지 않다. 객체를 누가 만들고, 어떤 조건에서 바뀌며, 생성 방식의 변경이 어디까지 영향을 미치는지를 분명히 하는 데 있다.