Java에서 Builder 패턴: 명확한 예제와 실제 적용

발행: (2025년 12월 31일 오전 02:02 GMT+9)
9 min read
원문: Dev.to

I’m happy to help translate the content, but I need the text you’d like translated. Could you please provide the article or the specific sections you want converted to Korean?

Builder Design Pattern in Java

Builder 디자인 패턴은 객체의 생성 과정을 그 표현과 분리하는 생성 패턴입니다. 다시 말해, 객체를 단계별로 생성할 수 있게 해 주며, 특히 복잡한 객체에 유용합니다.

Builder 패턴은 언제 사용해야 할까요?

8개의 속성을 가진 객체를 상상해 보세요. 8개의 인자를 받는 생성자를 만드는 것은 가능하지만, 모든 속성이 필요하지 않을 경우는 어떨까요? 사용되지 않는 매개변수에 null을 전달하는 것은 좋지 않은 관행이며, 가능한 모든 조합에 대해 생성자를 만들면 금세 관리하기 어려워집니다.

Solution: Builder 패턴을 적용하여 가독성이 좋고 유연한 방식으로 이러한 객체를 생성하세요.

빌더 패턴 사용의 장점

장점설명
유연성매개변수의 어떤 부분집합이든 자연스럽고 유창한 방식으로 객체를 생성합니다.
가독성생성 코드를 쉽게 따라하고 이해할 수 있습니다.
불변성한 번 생성된 객체는 불변일 수 있어, 원치 않는 수정으로부터 스레드 안전성을 보장합니다.

Builder 패턴은 Java에서 어떻게 구현되나요?

디자인 패턴은 추상적인 해결책이므로 구현은 상황에 맞게 조정될 수 있습니다. 아래에는 여러 구체적인 예시가 있습니다.

1. 클래식 Builder 디자인 패턴 (Java)

클래식 접근 방식은 제품 클래스의 필드와 동일한 구조를 가진 정적 내부 Builder 클래스를 정의합니다. 제품의 필드는 private final(불변)이며 getter만 제공합니다.

package com.funcionaenmimaquina.builder.classic;

public class Product {
    private final String name;
    private final String description;
    private final double price;
    private final String category;
    private final String imageUrl;
    private final String brand;
    private final String sku;

    private Product(Builder builder) {
        this.name = builder.name;
        this.description = builder.description;
        this.price = builder.price;
        this.category = builder.category;
        this.imageUrl = builder.imageUrl;
        this.brand = builder.brand;
        this.sku = builder.sku;
    }

    /** Builder ----------------------------------------------------------- */
    public static class Builder {
        private String name;
        private String description;
        private double price;
        private String category;
        private String imageUrl;
        private String brand;
        private String sku;

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

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

        public Builder price(double price) {
            this.price = price;
            return this;
        }

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

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

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

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

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

    /** Getters ----------------------------------------------------------- */
    public String getName()        { return name; }
    public String getDescription() { return description; }
    public double getPrice()       { return price; }
    public String getCategory()    { return category; }
    public String getImageUrl()    { return imageUrl; }
    public String getBrand()       { return brand; }
    public String getSku()         { return sku; }

    @Override
    public String toString() {
        return "Product{" +
                "name='" + name + '\'' +
                ", description='" + description + '\'' +
                ", price=" + price +
                ", category='" + category + '\'' +
                ", imageUrl='" + imageUrl + '\'' +
                ", brand='" + brand + '\'' +
                ", sku='" + sku + '\'' +
                '}';
    }
}

클래식 빌더 사용 예시

static void runClassicBuilder() {
    System.out.println("Classic Builder Pattern Example");
    Product product = new Product.Builder()
            .name("Laptop")
            .description("High performance laptop")
            .price(1200.00)
            .category("Electronics")
            .imageUrl("http://example.com/laptop.jpg")
            .brand("BrandX")
            .sku("SKU12345")
            .build();

    System.out.println(product);
}

2. 람다를 활용한 제네릭 Builder 디자인 패턴

람다를 사용하면 제네릭 빌더를 만들어 어떤 클래스에도 재사용할 수 있습니다.

import java.util.function.BiConsumer;
import java.util.function.Supplier;

public class Builder<T> {
    private final Supplier<T> supplier;
    private final T instance;

    priv

#### 간단한 POJO 사용 예시

```java
public class Person {
    private String firstName;
    private String lastName;
    private int age;

    // Setters (or any mutators) – they can be package‑private if you want
    public void setFirstName(String firstName) { this.firstName = firstName; }
    public void setLastName(String lastName)   { this.lastName = lastName; }
    public void setAge(int age)               { this.age = age; }

    @Override
    public String toString() {
        return firstName + " " + lastName + ", age " + age;
    }
}
static void runGenericBuilder() {
    Person person = Builder.of(Person::new)
            .with(Person::setFirstName, "John")
            .with(Person::setLastName,  "Doe")
            .with(Person::setAge,       30)
            .build();

    System.out.println(person);
}

User 클래스 예시

public class User {
    private String name;
    private String lastname;
    private String secondLastName;
    private String phone;
    private String email;

    // Getters and Setters needed
    public void setName(String name) { this.name = name; }
    public void setLastname(String lastname) { this.lastname = lastname; }
    public void setSecondLastName(String secondLastName) { this.secondLastName = secondLastName; }
    public void setPhone(String phone) { this.phone = phone; }
    public void setEmail(String email) { this.email = email; }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", lastname='" + lastname + '\'' +
                ", secondLastName='" + secondLastName + '\'' +
                ", phone='" + phone + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}
static void runGenericBuilder() {
    System.out.println("Generic Builder Pattern Example");
    User user = Builder.of(User::new)
            .with(User::setName, "John")
            .with(User::setLastname, "Doeh")
            .with(User::setSecondLastName, "Doeh")
            .with(User::setPhone, "555555")
            .with(User::setEmail, "mail@mail.com")
            .build();
    System.out.println(user.toString());
}

3. Java에서 Lombok을 사용한 Builder 디자인 패턴

Lombok은 어노테이션을 통해 보일러플레이트 코드를 제거해 주는 Java 라이브러리입니다. 다양한 기능 중에서도 Builder 패턴을 간단히 구현할 수 있는 방법을 제공합니다.

import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

@Builder
@Getter
@ToString
public class Videogame {
    private String name;
    private String platform;
    private String category;
}

사용법

static void runLombokBuilder() {
    System.out.println("Lombok Builder Pattern Example");
    Videogame videogame = Videogame.builder()
            .name("The Legend of Zelda")
            .platform("Nintendo Switch")
            .category("Action-Adventure")
            .build();
    System.out.println(videogame.toString());
}

Lombok은 레코드에 대한 @Builder 어노테이션도 지원합니다:

@Builder
public record SoccerTeam(String name, String country, String coach) { }

4. Java에서 레코드를 활용한 Builder 디자인 패턴

레코드는 Java 14(프리뷰)에서 도입되고 Java 16에서 정식화된 기능으로, 보일러플레이트 없이 불변 클래스를 만들 수 있게 해 줍니다. Lombok 없이 레코드에 대한 Builder가 필요하다면 직접 구현할 수 있습니다.

수동으로:

public record Smartphone(String model, String brand, String operatingSystem, double price) {

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private String model;
        private String brand;
        private String operatingSystem;
        private double price;

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

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

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

        public Builder price(double price) {
            this.price = price;
            return this;
        }

        public Smartphone build() {
            return new Smartphone(model, brand, operatingSystem, price);
        }
    }
}

사용법

static void runRecordBuilder() {
    System.out.println("Record Builder Pattern Example");
    Smartphone smartphone = Smartphone.builder()
            .model("iPhone 14")
            .brand("Apple")
            .operatingSystem("iOS")
            .price(999.99)
            .build();
    System.out.println(smartphone.toString());
}

결론

이제 우리는 Builder 디자인 패턴이 무엇을 위한 것인지 알고, Java에서 이를 구현하는 여러 방법을 살펴보았습니다. 올바른 접근 방식을 선택하는 것은 프로젝트의 상황과 구체적인 요구 사항(예: 불변성, 보일러플레이트 감소, 외부 라이브러리)에 따라 달라집니다.

관련 링크

Back to Blog

관련 글

더 보기 »