본문 바로가기
☕️ JAVA/🦋 Effective Java

생성자에 매개변수가 많다면 빌더 패턴을 써볼까?! (HTTPClient와 lombok @Builder) 🧰

by kukim 2022. 2. 11.

이 글은 책 이펙티브 자바 3판 Item 2와 하단 Reference 참고했습니다. 잘못된 내용이 있다면 편하게 말씀해주세요 🙏🏻

 

목차

1. 점층적 생성자 패턴(Telescoping Constructor pattern)

2. 자바빈즈 패턴(JavaBeans pattern)

3. 빌더 패턴(Builder pattern)

- 빌더 패턴 사용 예 : java.net

+a) lombok의 @Builder를 사용한 빌더 패턴 사용

+a) 다른 언어의 빌더 패턴


한 줄 요약 : 생성자에 매개변수가 많다면 빌더 패턴(Builder pattern)을 써보자.

이전 글 객체 생성할 때 '생성자' 대신 '정적 팩토리 메서드'를 써볼까? 🏭 에서 생성자 대신 정적 팩토리 메서드를 사용하면 좋은 이유를 알아봤다. 그렇다면 생성자에 매개변수가 많은 경우는 어떨까?  매개변수 경우의 수 만큼 팩토리 메서드를 만들어야 하는 대참사가 발생한다.

 

인스턴스를 만들 때 넘겨줄 매개변수가 많다면 어떻게 해야 할까?

3가지 방법을 알아보자.

1. 점층적 생성자 패턴(Telescoping constructor pattern)

Telescoping(이하 텔리스코핑) 생성자 패턴생성자를 여러 개 만드는 것이다. 필수 초기화할 멤버 변수를 기본으로 만든다. 아래 예는 NutritionFacts 클래스의 텔리스코핑 생성자 패턴이다.

// 1. 텔레스코핑 패턴

public class NutritionFacts {
    private final int servingSize;  // (mL, 1회 제공량)     필수
    private final int servings;     // (회, 총 n회 제공량)  필수
    private final int calories;     // (1회 제공량당)       선택
    private final int fat;          // (g/1회 제공량)       선택
    private final int sodium;       // (mg/1회 제공량)      선택
    private final int carbohydrate; // (g/1회 제공량)       선택

    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings,
                          int calories) {
        this(servingSize, servings, calories, 0);
    }

    public NutritionFacts(int servingSize, int servings,
                          int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    }

    public NutritionFacts(int servingSize, int servings,
                          int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }
    public NutritionFacts(int servingSize, int servings,
                          int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize  = servingSize;
        this.servings     = servings;
        this.calories     = calories;
        this.fat          = fat;
        this.sodium       = sodium;
        this.carbohydrate = carbohydrate;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola =
                new NutritionFacts(240, 8, 100, 0, 35, 27);
    }
    
}

단점

- 매개변수가 많아지면 코드 작성이 어렵고 가독성이 떨어진다.

- 잘못된 매개변수 입력할 수 있다.

- 확장성이 떨어진다.

2. 자바빈즈 패턴(JavaBeans pattern)

자바빈즈 패턴이란 멤버 변수를 초기화하고 세터(Setter) 메서드들을 호출해 원하는 멤버 변수 값을 설정한다.

// 2. 자바빈즈 패턴

public class NutritionFacts {

    private int servingSize = -1;  // 필수
    private int servings = -1;     // 필수
    private int calories = 0;
    private int fat = 0;
    private int sodium = 0;
    private int carbohydrate = 0;

    public NutritionFacts() {
    }

    public void setServingSize(int val) {
        servingSize = val;
    }

    public void setServings(int val) {
        servings = val;
    }

    public void setCalories(int val) {
        calories = val;
    }

    public void setFat(int val) {
        fat = val;
    }

    public void setSodium(int val) {
        sodium = val;
    }

    public void setCarbohydrate(int val) {
        carbohydrate = val;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts();
        cocaCola.setServingSize(240);
        cocaCola.setServings(8);
        cocaCola.setCalories(100);
        cocaCola.setSodium(35);
        cocaCola.setCarbohydrate(27);
    }
}

장점

텔리스코핑 생성자 패턴에 비해 확장하기 쉽다. 인스턴스 만들기 쉽다. 가독성이 좋아진다.

 

단점

- 객체 하나 만들려면 메서드 여러 개를 호출해야 한다.

- 텔리스코핑 생성자 패턴은 매개변수들이 유효한지 생성자에서만 확인하면 일관성 유지가 가능하지만 자바빈즈 패턴은 이는 객체가 완전히 생성되기 전까지 일관성(consistency)이 없는 상태가 된다. 문제 발생 시 디버깅하기 쉽지 않다.

- 이는 클래스 생성 시 불변으로 만들 수 없으며 스레드 안전성을 얻으려면 추가 작업을 해줘야 한다.

+) 불변 클래스 : 인스턴스의 내부 값을 수정할 수 없는 클래스 (인스턴스 생성 시 값이 고정되고 객체 파괴 순간까지 바뀌지 않음 e.g. String, 기본 타입의 박싱 된 클래스들(Integer, Float...), BigInteger, BigDecimal...)

 

3. 빌더 패턴(Builder pattern)

빌더 패턴은 텔리스코핑과 자바빈즈 패턴의 장점을 모았다. 방법은 다음과 같다.

1. 필수 매개 변수만으로 된 생성자(혹은 정적 팩토리)를 통해 빌더 객체를 만든다.

2. 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수를 설정한다.

3. 마지막으로 매개변수가 없는 build() 메서드를 호출해 원하는 객체를 얻는다.

// 3. 빌더패턴

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {
        // 필수 매개변수
        private final int servingSize;
        private final int servings;

        // 선택 매개변수 - 기본값으로 초기화한다.
        private int calories      = 0;
        private int fat           = 0;
        private int sodium        = 0;
        private int carbohydrate  = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings    = servings;
        }

        public Builder calories(int val)
        { calories = val;      return this; }
        public Builder fat(int val)
        { fat = val;           return this; }
        public Builder sodium(int val)
        { sodium = val;        return this; }
        public Builder carbohydrate(int val)
        { carbohydrate = val;  return this; }

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

    private NutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                .calories(100).sodium(35).carbohydrate(27).build();
    }
}

장점

- Builder의 세터 메서드들은 빌더 자신을 반환한다. 이는 메서드 연쇄(method chaining)가 가능하다.(fluent API라고도 함)

- 텔리스코핑 패턴보다 코드 읽고 쓰기 쉽고 자바빈즈 패턴보다 안전하다.

- API는 시간이 지날수록 매개변수가 많아지는 경향이 있으니 애초에 빌더로 시작하는 편이 나을 때가 많다.

 

단점

- 객체를 만들기 위해 Builder부터 만들어야 한다.

- 오히려 매개변수가 3개 이하라면 더 복잡할 수 있다.

 

빌더 패턴 사용 예 : Java 11의 HTTP Client 

HTTP Client는 자바 11부터 생긴 API로 HTTP의 클라이언트와 요청, 응답 기능을 제공하는 API이다. (java.net.*)

HttpClient나 HttpRequest 클래스는 정적 팩토리 메서드 + 빌더 패턴을 통해 인스턴스를 생성한다.

아래 코드는 HttpClient와 HttpRequest의 빌더 패턴 예이다.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;

public class HTTP_Client {
    public static void main(String[] args) {
        HttpRequest request = HttpRequest.newBuilder() // 1. 빌더 객체 생성
                .uri(URI.create("http://openjdk.java.net/")) // 2. 빌더 객체의 매개변수 셋팅
                .GET() // 2. 
                .header("Accept", "text/html") // 2. 
                .header("User-Agent", "Mozilla/5.0") // 2. 
                .build(); // 3. 빌더 객체를 가지고 HttpRequest 객체 생성 후 리턴


        HttpClient client = HttpClient.newBuilder()
                .version(Version.HTTP_2)
                .followRedirects(Redirect.SAME_PROTOCOL)
                .proxy(ProxySelector.of(new InetSocketAddress("www-proxy.com", 8080)))
                .authenticator(Authenticator.getDefault())
                .build();

    }
}

HttpRequest의 클래스의 빌더 패턴은 다음과 같이 객체를 생성하고 있다.

1. 빌더 객체 생성

HttpRequest.newBuilder() 정적 팩토리 메서드를 통해 빌더 객체 'HttpRequestBuilderImpl()'을 생성한다. 

 

2. 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수를 설정한다.

uri(), GET(), .header(), .header() 를 통해 빌더 객체의 매개변수를 설정한다.

 

3. build() 메서드 호출

최종적으로 HttpRequest 객체의 구현체 ImmutableHttpRequest()를 리턴한다.

 

 

자세한 소스코드

public static HttpRequest.Builder newBuilder() {
    return new HttpRequestBuilderImpl();
}


public class HttpRequestBuilderImpl implements HttpRequest.Builder {

    private HttpHeadersBuilder headersBuilder;
    private URI uri;
    private String method;
    
    // 중략
    
    // 빌더 생성
    public HttpRequestBuilderImpl() {
        this.headersBuilder = new HttpHeadersBuilder();
        this.method = "GET"; // default, as per spec
        this.version = Optional.empty();
    }
    
    // build()
    public HttpRequest build() {
        if (uri == null)
            throw new IllegalStateException("uri is null");
        assert method != null;
        return new ImmutableHttpRequest(this);
    }
    
    // uri() : 일종의 세터메서드
    @Override
    public HttpRequestBuilderImpl uri(URI uri) {
        requireNonNull(uri, "uri must be non-null");
        checkURI(uri);
        this.uri = uri;
        return this;
    }

	// setHeader() : 일종의 세터메서드
    @Override
    public HttpRequestBuilderImpl setHeader(String name, String value) {
        checkNameAndValue(name, value);
        headersBuilder.setHeader(name, value);
        return this;
    }
    
    
    // 생략

}

 


+a) lombok

 1. Java Lombok 라이브러리의 @Builder 에너테이션을 통해 간편하게 빌더를 만들 수 있다. (Builder inner class 만들지 않아도 자동으로 만들어준다.)

import lombok.Builder;

@Builder // 에노테이션만 작성해주면 builder()와 build() 메서드 자동 생성해준다.
public class NutritionFacts {

    private int servingSize = -1;  // 필수
    private int servings = -1;     // 필수
    private int calories = 0;
    private int fat = 0;
    private int sodium = 0;
    private int carbohydrate = 0;

    public NutritionFacts(int servingSize, int servings) {
        this.servingSize = servingSize;
        this.servings = servings;
    }


    public static void main(String[] args) {
        NutritionFacts cocaCola = NutritionFacts.builder()
                .calories(100).sodium(35).carbohydrate(27).build();
    }
}

+a) 다른 언어

코틀린이나 c# 등은 언어적 차원에서 named argument를 지원한다. 따라서 자바처럼 복잡하게 빌더 패턴 없이 생성자를 통해 손쉽게 만들 수 있다.

// 더북 c# 교과서 : https://thebook.io/006890/part02/ch19/09/
using System;

class NamedParameter
{
    static void Main()
    {
        sum(10, 20);                      //기본 형태
        sum(first: 10, second: 20); //① 매개변수 이름과 콜론(:) 기호를 사용하여 호출
        sum(second: 20, first: 10); //② 매개변수 이름을 지정하면 변수 위치 변경 가능
    }

    static void Sum(int first, int second) //명명된 매개변수
    {
        Console.WriteLine(first + second);
    }
}

⛓ Reference

https://kukim.tistory.com/81

https://openjdk.java.net/groups/net/httpclient/intro.html

https://github.com/WegraLee/effective-java-3e-source-code/tree/master/src/effectivejava/chapter2/item2

https://github.com/Meet-Coder-Study/book-effective-java

https://projectlombok.org/features/Builder

https://thebook.io/006890/part02/ch19/09/

 

댓글