와드의 블로그

도메인 객체 생성 전략 본문

스터디/개발

도메인 객체 생성 전략

Ward 2026. 3. 30. 11:12

도메인 객체를 설계하고 생성할 때, 개발자는 객체의 일관성을 유지하고 코드의 가독성을 높이기 위해 다양한 생성 전략을 선택할 수 있습니다. 대표적으로 생성자(Constructor), 빌더 패턴(Builder Pattern), 정적 팩토리 메서드(Static Factory Method) 세 가지 방식이 있으며, 각각의 장단점과 적합한 사용 상황이 다릅니다.

1. 생성자 (Constructor)

가장 기본적이고 전통적인 객체 생성 방식입니다. new 키워드와 함께 호출되어 객체를 초기화합니다.

사용법

public class User {
    private String name;
    private int age;
    private String email;

    // 필수 값을 받는 생성자
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 모든 값을 받는 생성자 (오버로딩)
    public User(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
}

// 호출
User user = new User("홍길동", 30, "hong@example.com");

장점

  • 단순성: 객체 지향 언어에서 기본적으로 제공하는 방식으로, 구현이 직관적이고 단순합니다.
  • 표준화: 모든 개발자가 익숙한 방식이므로 별도의 학습이 필요 없습니다.

단점

  • 매개변수 순서 오류 위험 (휴먼 에러): 매개변수의 타입이 같은 경우(예: String name, String email), 호출 시 순서를 뒤바꾸어 입력해도 컴파일 에러가 발생하지 않아 데이터 무결성이 훼손될 수 있습니다.
  • 가독성 저하: 매개변수가 많아지면 각 인자가 어떤 필드를 의미하는지 파악하기 어렵습니다. (예: new User("홍", "김", 30, "M", ...)의 경우 순서가 헷갈릴 수 있음)
  • 유연성 부족: 선택적 매개변수가 많을 경우, 경우의 수만큼 생성자를 오버로딩해야 하는 '점층적 생성자 패턴(Telescoping Constructor Pattern)' 문제가 발생하여 코드가 지저분해집니다.

사용 시기 (언제 써야 하는가?)

  • 매개변수의 개수가 적을 때 (일반적으로 3개 이하).
  • 모든 매개변수가 객체 생성에 필수적인 값일 때.
  • 객체 생성의 의도가 클래스 이름 자체로 충분히 설명될 때.

2. 빌더 패턴 (Builder Pattern)

객체의 생성 과정과 표현 방법을 분리하여, 복잡한 객체를 단계별로 유연하게 생성할 수 있도록 도와주는 패턴입니다.

사용법

public class User {
    private String name;
    private int age;
    private String email;

    private User(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.email = builder.email;
    }

    public static class Builder {
        private String name; // 필수
        private int age;     // 필수
        private String email; // 선택

        // 필수 값은 빌더의 생성자로 강제하여 누락을 방지하는 것이 좋습니다.
        public Builder(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public Builder email(String email) {
            this.email = email;
            return this;
        }

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

// 호출
User user = new User.Builder("홍길동", 30)
                    .email("hong@example.com")
                    .build();

장점

  • 가독성 향상: 메서드 체이닝을 통해 어떤 값을 설정하는지 명확하게 알 수 있습니다.
  • 유연성: 선택적 매개변수를 처리하기 매우 좋으며, 생성자 오버로딩을 방지합니다.
  • 불변성 확보: Setter 메서드를 열어두지 않고도 객체를 완성된 상태로 한 번에 생성(불변 객체)할 수 있습니다.

단점

  • 필수 값 누락 가능성: 객체 생성에 반드시 필요한 값이 세팅되지 않은 상태로 build() 메서드가 호출될 위험이 있습니다. 컴파일 타임에 이를 완전히 막기 어려워 런타임 에러로 이어질 수 있습니다. (따라서 예시처럼 빌더의 생성자에서 필수 값을 강제하는 설계가 필요합니다.)
  • 구현 비용: 빌더 클래스를 별도로 만들어야 하므로 코드량이 증가합니다.
  • 성능(미미함): 객체를 생성하기 위해 빌더 객체를 먼저 생성해야 하므로, 성능에 극도로 민감한 상황에서는 문제가 될 수 있습니다.

사용 시기 (언제 써야 하는가?)

  • 매개변수가 많을 때 (보통 4개 이상).
  • 선택적 매개변수가 다수 존재할 때.
  • 불변 객체(Immutable Object)를 안전하게 생성하고 싶을 때.

3. 정적 팩토리 메서드 (Static Factory Method)

생성자 대신, 객체를 반환하는 static 메서드를 제공하여 객체 생성을 캡슐화하는 방식입니다.

사용법

public class User {
    private String name;
    private int age;
    private String role;

    private User(String name, int age, String role) {
        this.name = name;
        this.age = age;
        this.role = role;
    }

    // 정적 팩토리 메서드 (의도 명시)
    public static User createAdmin(String name, int age) {
        return new User(name, age, "ADMIN");
    }

    public static User createGuest(String name) {
        return new User(name, 0, "GUEST");
    }
}

// 호출
User admin = User.createAdmin("관리자", 35);
User guest = User.createGuest("방문자");

장점

  • 이름을 가질 수 있음: createAdmin, from, of 등 메서드에 이름을 부여하여 객체 생성의 의도를 명확하게 표현할 수 있습니다.
  • 인스턴스 통제 가능: 호출될 때마다 반드시 새로운 객체를 생성할 필요가 없습니다. (싱글톤 패턴, 캐싱 등을 적용하여 메모리 최적화 가능)
  • 하위 타입 반환 가능: 반환 타입의 하위 클래스 객체를 유연하게 반환할 수 있어 인터페이스 기반 프레임워크를 만드는 데 유용합니다.

단점

  • 상속의 제약: 정적 팩토리 메서드만 제공하고 생성자를 private으로 막아두면, 해당 클래스를 상속(상속을 위한 public이나 protected 생성자 필요)할 수 없습니다. (하지만 이는 상속 대신 컴포지션을 유도하므로 단점이 아닐 수도 있습니다.)
  • 탐색의 어려움: 일반적인 정적 메서드와 구별하기 어려워, 사용자가 생성 방법을 찾기 위해 문서를 확인해야 할 수 있습니다. (따라서 of, from, getInstance, create 등의 명명 규칙을 철저히 따라야 합니다.)

사용 시기 (언제 써야 하는가?)

  • 생성 목적이나 상태에 따라 이름 부여가 필요할 때 (생성자만으로는 의도를 표현하기 부족할 때).
  • 입력 값에 따라 다른 구현체(하위 타입)를 반환해야 할 때.
  • 미리 만들어둔 인스턴스를 재활용하여 성능을 높여야 할 때.

4. 요약 및 권장 가이드

실무에서는 도메인 객체를 생성할 때 매개변수의 개수와 복잡도에 따라 전략을 나누어 사용하는 것을 권장합니다.

  • 매개변수가 적을 때 (1~3개): 생성자 기반의 정적 팩토리 메서드를 사용합니다. 생성자만으로는 표현하기 힘든 객체 생성의 의도를 메서드 이름으로 명확히 드러낼 수 있으며, 파라미터가 적어 순서가 뒤바뀔 위험이 적습니다.
  • 매개변수가 많고 복잡할 때 (4개 이상): 빌더 패턴을 클라이언트에게 직접 노출합니다. 정적 팩토리 메서드로 수많은 매개변수를 받는 것은 생성자와 동일한 휴먼 에러(매개변수 순서 바뀜)를 유발하므로, 클라이언트가 명시적으로 필드를 매핑할 수 있도록 빌더를 제공해야 합니다.
  • 필수 값 누락 방지 (안전한 빌더): 빌더 패턴을 적용할 때는 롬복(Lombok)에 전적으로 의존하기보다는, 정적 내부 클래스(Static Nested Class)를 활용한 전통적인 빌더 패턴을 직접 구현하고 빌더의 생성자로 필수 값을 강제하여 데이터 누락을 컴파일 시점에 원천 차단하는 것이 가장 견고합니다.
  • 주의사항 및 실무 팁: 같은 타입의 매개변수(String, int 등)가 연속으로 선언될 경우 컴파일러가 순서 오류를 잡아주지 못합니다. 실무에서는 이러한 휴먼 에러를 막기 위해 원시 타입이나 문자열을 의미 있는 객체로 한 번 더 포장하는 값 객체(Value Object, VO) 패턴을 도입하기도 합니다.

적용 예시

// 1. 매개변수가 적을 때: 정적 팩토리 메서드 + 내부 생성자
public static User createGuest(String name, int age) {
    return new User(name, age, "GUEST"); 
}

// 2. 매개변수가 많을 때: 전통적인 빌더 패턴 직접 노출 (필수 값 강제)
// 클라이언트가 필드명을 직접 확인하며 조립 (휴먼 에러 차단 및 필수 값 강제)
User user = new User.Builder("홍길동", 30) // 필수 값 강제
                    .email("hong@email.com") // 선택 값 체이닝
                    .address("서울특별시 강남구")
                    .build();

'스터디 > 개발' 카테고리의 다른 글

정렬 알고리즘 정리  (0) 2026.05.18
DTO 사용 전략  (1) 2026.03.30