Builder — 복잡한 객체를 단계별로 생성할 수 있는 생성 패턴입니다. 수십 개의 매개변수를 가진 생성자와 달리 Builder는 호출 체인을 통해 객체를 조립하며, 각 호출이 하나의 필드를 구성합니다. 이 패턴은 네트워크 클라이언트 구성, 데이터베이스 설정, 알림 및 탐색 빌더 등 많은 선택적 매개변수가 있는 객체에 특히 유용합니다. 자세한 내용은 Refactoring Guru: Builder를 참조하세요.
핵심 포인트
Builder — 복잡한 객체의 생성을 그 표현에서 분리하는 GoF 생성 패턴입니다. 동일한 생성 프로세스가 다른 표현을 만들 수 있습니다. Builder는 객체에 많은 선택적 매개변수가 있고 10개의 필드를 가진 생성자가 읽기 어렵고 유연성이 떨어질 때 유용합니다. 이 패턴은 생성자 오버로드 수가 기하급수적으로 증가하는 Telescoping Constructor 안티패턴도 해결합니다.
Builder 구조는 메인 클래스의 필드를 미러링하는 필드를 가진 내부 정적 Builder 클래스를 포함합니다. 각 set-메서드는 fluent chaining을 위해 Builder(this)를 반환합니다. 최종 build() 메서드는 필드 값을 프라이빗 생성자에 전달하여 대상 객체를 생성합니다. 메인 클래스에는 Builder를 받는 프라이빗 생성자가 있습니다. 클라이언트: Object.builder().setField1(val1).setField2(val2).build().
Builder 사용 시기 — 5+ 필드가 있는 객체 중 필수 필드가 2-3개만 있는 경우. 구성 객체(RequestConfig, DatabaseConfig). 생성 시 복잡한 유효성 검사 로직이 있는 객체. 생성 후 불변(immutable)해야 하는 객체. Android에서 Builder는 SDK에서 적극적으로 사용됩니다: AlertDialog.Builder, Retrofit.Builder, OkHttpClient.Builder, NotificationCompat.Builder.
Kotlin Builder에는 두 가지 접근 방식이 있습니다: 클래식 Java 스타일 Builder(중첩 클래스 사용)와 Kotlin 스타일 DSL builder(리시버가 있는 람다 사용). Java 스타일 Builder는 Android 호환성 및 Java 코드와 함께 사용할 때 선호됩니다. DSL builder는 Kotlin의 관용적인 방식입니다: 함수가 람다를 받아들이고, 그 내부에서 this가 필드를 직접 할당할 수 있는 Builder 컨텍스트가 됩니다.
// 클래식 Builder
data class HttpConfig private constructor(
val baseUrl: String,
val timeout: Long = 30_000,
val retries: Int = 3,
val headers: Map<String, String> = emptyMap()
) {
class Builder {
private var baseUrl: String = ""
private var timeout: Long = 30_000
private var retries: Int = 3
private var headers: MutableMap<String, String> = mutableMapOf()
fun baseUrl(url: String) = apply { this.baseUrl = url }
fun timeout(ms: Long) = apply { this.timeout = ms }
fun retries(n: Int) = apply { this.retries = n }
fun header(key: String, value: String) = apply { headers[key] = value }
fun build(): HttpConfig {
require(baseUrl.isNotBlank()) { "baseUrl is required" }
return HttpConfig(baseUrl, timeout, retries, headers)
}
}
}
// 사용법
val config = HttpConfig.Builder()
.baseUrl("https://api.example.com")
.timeout(15_000)
.header("Authorization", "Bearer token")
.build()
Kotlin DSL builder — 중첩 클래스가 없는 대안입니다. 빌더 함수는 빌더 객체의 컨텍스트에서 람다를 받아들입니다. 이것은 Kotlin에 관용적이며 write-필드가 필요하지 않습니다. DSL builders는 Ktor Client, kotlinx.serialization, Compose(Modifier)에서 적극적으로 사용됩니다. DSL builder는 Java와 호환되지 않으며 Java API가 있는 라이브러리에 적합하지 않습니다.
Swift Builder — Swift에는 내장 Builder 패턴이 없지만, Self를 반환하는 메서드를 통해 fluent interface를 쉽게 구현할 수 있습니다. 각 메서드가 속성을 구성하고 self를 반환합니다. Kotlin과 달리 Swift는 별도의 Builder 클래스가 필요하지 않습니다 — 조립 중에 객체가 mutable이면 객체 자체를 반환할 수 있습니다. 불변 객체의 경우 Kotlin과 유사하게 중첩된 Builder 클래스가 사용됩니다.
struct NetworkRequest {
let url: String
let method: HTTPMethod
let headers: [String: String]
let body: Data?
let timeout: TimeInterval
final class Builder {
private var url: String = ""
private var method: HTTPMethod = .get
private var headers: [String: String] = [:]
private var body: Data? = nil
private var timeout: TimeInterval = 30
func withURL(_: String) -> Self { /* self */ }
func withMethod(_: HTTPMethod) -> Self { /* self */ }
func withHeader(key: String, value: String) -> Self { /* self */ }
func withBody(_: Data) -> Self { /* self */ }
func withTimeout(_: TimeInterval) -> Self { /* self */ }
func build() throws -> NetworkRequest {
guard !url.isEmpty else { throw BuilderError.missingURL }
return NetworkRequest(
url: url, method: method, headers: headers,
body: body, timeout: timeout
)
}
}
}
Result Builders — Swift 5.4에서 @resultBuilder가 도입되었습니다 — 선언적 구조 생성을 위한 언어 메커니즘입니다. SwiftUI, AttributedString, SceneBuilder가 result builders를 사용합니다. 이것은 클래식 Builder의 대안입니다: set-메서드 체인 대신, result builder는 컴파일러가 배열이나 트리로 조립하는 요소가 있는 코드 블록을 사용합니다. SwiftUI의 @ViewBuilder가 가장 유명한 예입니다: body 내에서 if, switch, ForEach를 작성할 수 있고, 컴파일러가 조건에서 View를 만듭니다.
Telescoping Constructor — 클래스에 다양한 매개변수 세트를 가진 여러 오버로드된 생성자가 있는 안티패턴입니다. 예를 들어, 세 개의 생성자: HttpConfig(url), HttpConfig(url, timeout), HttpConfig(url, timeout, retries). 매개변수가 증가함에 따라 생성자 수가 기하급수적으로 증가합니다 — n개의 선택적 필드에 n!개의 조합이 필요합니다. Builder는 필요한 필드만 지정할 수 있게 하여 이 문제를 해결합니다.
| 특성 | Telescoping Constructor | Builder | Kotlin named args |
|---|---|---|---|
| 코드 양 | 기하급수적 증가 | 선형 증가 | 최소 |
| 가독성 | 낮음(어떤 매개변수가 무엇인가?) | 높음(메서드 + 이름) | 높음(이름 = 값) |
| 불변성 | 불변 | 불변 | 불변 |
| Java 호환성 | 완전 | 완전 | 없음(Kotlin만) |
| 유효성 검사 | 각 생성자에서 | build()에서 — 한 번 | init()에서 |
Kotlin named arguments + 기본값 — 순수 Kotlin 프로젝트에서 Builder의 우아한 대안입니다. 생성자 매개변수에 기본값이 있으며, 클라이언트는 필요한 것만 전달합니다: HttpConfig(baseUrl = url, timeout = 15_000). 단점은 컴파일 타임에 필수 필드를 검증할 수 없다는 것입니다. Builder는 Builder 생성자를 통해 필수 필드를 제공합니다(baseUrl은 필수). Java 라이브러리의 경우 Builder가 사실상의 표준으로 남아 있습니다.
Android SDK의 Builder — 표준 라이브러리에서 가장 일반적인 패턴 중 하나입니다. AlertDialog.Builder: new AlertDialog.Builder(context).setTitle().setMessage().setPositiveButton().create(). Retrofit.Builder: new Retrofit.Builder().baseUrl().addConverterFactory().build(). OkHttpClient.Builder: new OkHttpClient.Builder().connectTimeout().addInterceptor().build(). NotificationCompat.Builder: setContentTitle().setContentText().setSmallIcon().build().
Google이 Builder를 사용하는 이유 — 하위 호환성입니다. Builder에 새 메서드를 추가해도 기존 코드가 깨지지 않습니다. Google이 20개 매개변수의 생성자를 사용했다면 각 새 필드마다 새 오버로드가 필요했을 것입니다. Builder는 breaking changes 없이 수년간 set-메서드를 추가할 수 있게 합니다. 예를 들어, NotificationCompat.Builder는 Android 11에서 setBubbleMetadata()를 추가했으며 기존 코드에 영향을 주지 않았습니다.
Kotlin 라이브러리의 Builder — Ktor(HttpClientBuilder), Coil(ImageRequest.Builder), Room(Room.databaseBuilder(context, AppDatabase.class, "db").fallbackToDestructiveMigration().build()), Navigation(NavOptionsBuilder). Kotlin 프로젝트에서 Builder는 종종 DSL과 결합됩니다: Room.databaseBuilder(context, AppDatabase.class, "db").fallbackToDestructiveMigration().build(). 이 패턴은 하위 호환성과 Java 상호 운용성이 중요한 공개 API에서 계속 관련성이 있습니다.
자주 묻는 질문
Builder는 1-3개 필드의 객체에는 과잉입니다 — 일반 생성자나 data class가 더 명확합니다. Java 상호 운용성이 없는 Kotlin 프로젝트에서도 과잉이며, named arguments + 기본값이 동일한 작업을 더 간단하게 해결합니다. Builder는 5+ 필드, 복잡한 검증 또는 named arguments를 사용할 수 없는 Java API에 정당화됩니다.
Builder는 하나의 복잡한 객체를 단계별로 생성하고(필드 구성), Factory는 유형이나 매개변수에 따라 전체 객체를 생성합니다. Builder는 «어떻게 조립할까?»라는 질문에 답하고, Factory는 «무엇을 만들까?»에 답합니다. Builder는 종종 Factory와 결합됩니다: Factory가 유형을 선택하고 Builder가 필드를 구성합니다.
SwiftUI에서 Builder의 역할은 result builders(@ViewBuilder, @SceneBuilder)와 View 수정자(.font(), .padding())가 수행합니다. SwiftUI는 선언적 접근 방식과 fluent modifiers를 사용하므로 클래식 Builder가 필요하지 않습니다. UIKit 구성 요소의 경우 Builder가 유용합니다: UIAlertController, URLRequest, NSAttributedString.
Builder는 일반적으로 단일 스레드에서 객체를 조립하는 데 사용되므로 스레드 안전성이 필요하지 않습니다. Builder가 멀티스레드 환경(드문 경우)에서 사용되는 경우 각 set-메서드와 build()를 동기화하세요. 대안 — Immutable Builder: 각 set-메서드가 수정된 필드로 새 Builder 인스턴스를 반환합니다.
Retrofit.Builder는 DI 컨테이너 없이 작동해야 하는 공개 라이브러리 API입니다. Builder는 Dagger 또는 다른 DI 프레임워크에 대한 의존성 없이 구성 유연성(baseUrl, 변환기, 인터셉터, 사용자 정의 콜 어댑터)을 제공합니다. 애플리케이션 내에서 DI는 Builder를 통해 Retrofit을 한 번 생성할 수 있지만, Builder 자체는 Retrofit 공개 API의 일부로 남아 있습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.