반응형
Jake Seo
제이크서 위키 블로그
Jake Seo
전체 방문자
오늘
어제
  • 분류 전체보기 (715)
    • 일상, 일기 (0)
    • 백준 문제풀이 (1)
    • 릿코드 문제풀이 (2)
    • 알고리즘 이론 (10)
      • 기본 이론 (2)
      • 배열과 문자열 (8)
    • 데이터베이스 (15)
      • Planet Scale (1)
      • MSSQL (9)
      • 디비 기본 개념 (1)
      • SQLite 직접 만들어보기 (4)
    • 보안 (7)
    • 설계 (1)
    • 네트워크 (17)
      • HTTP (9)
      • OSI Layers (5)
    • 회고 (31)
      • 연간 회고 (2)
      • 주간 회고 (29)
    • 인프라 (52)
      • 도커 (12)
      • AWS (9)
      • 용어 (21)
      • 웹 성능 (1)
      • 대규모 서비스를 지탱하는 기술 (9)
    • 깃 (7)
    • 빌드 도구 (7)
      • 메이븐 (6)
      • 그레이들 (0)
    • Java (135)
      • 이펙티브 자바 (73)
      • 자바 API (4)
      • 자바 잡지식 (30)
      • 자바 디자인 패턴 (21)
      • 톰캣 (Tomcat) (7)
    • 프레임워크 (64)
      • next.js (14)
      • 스프링 프레임워크 (28)
      • 토비의 스프링 (6)
      • 스프링 부트 (3)
      • JPA (Java Persistence API) (5)
      • Nest.js (8)
    • 프론트엔드 (48)
      • 다크모드 (1)
      • 노드 패키지 관리 매니저 (3)
      • CSS (19)
      • Web API (11)
      • tailwind-css (1)
      • React (5)
      • React 새 공식문서 요약 (1)
      • HTML (Markup Language) (5)
    • 자바스크립트 (108)
      • 모던 자바스크립트 (31)
      • 개념 (31)
      • 정규표현식 (5)
      • 코드 스니펫 (1)
      • 라이브러리 (6)
      • 인터뷰 (24)
      • 웹개발자를 위한 자바스크립트의 모든 것 (6)
      • 팁 (2)
    • Typescript (49)
    • 리눅스와 유닉스 (10)
    • Computer Science (1)
      • Compiler (1)
    • IDE (3)
      • VSCODE (1)
      • IntelliJ (2)
    • 세미나 & 컨퍼런스 (1)
    • 용어 (개발용어) (16)
      • 함수형 프로그래밍 용어들 (1)
    • ORM (2)
      • Prisma (2)
    • NODEJS (2)
    • cypress (1)
    • 리액트 네이티브 (React Native) (31)
    • 러스트 (Rust) (15)
    • 코틀린 (Kotlin) (4)
      • 자바에서 코틀린으로 (4)
    • 정규표현식 (3)
    • 구글 애널리틱스 (GA) (1)
    • SEO (2)
    • UML (2)
    • 맛탐험 (2)
    • 리팩토링 (1)
    • 서평 (2)
    • 소프트웨어 공학 (18)
      • 테스팅 (16)
      • 개발 프로세스 (1)
    • 교육학 (1)
    • 삶의 지혜, 통찰 (1)
    • Chat GPT (2)
    • 쉘스크립트 (1)
    • 컴파일 (2)
    • Dart (12)
    • 코드팩토리의 플러터 프로그래밍 (4)
    • 플러터 (17)
    • 안드로이드 스튜디오 (1)
    • 윈도우즈 (1)
    • 잡다한 백엔드 지식 (1)
    • 디자인 패턴 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 자료구조
  • 메이븐 페이즈
  • 도커공식문서
  • 자바스크립트 인터뷰
  • 이펙티브자바
  • 자바스크립트 면접
  • NEXT JS
  • 이펙티브 자바
  • 자바 검증
  • 이펙티브 자바 item9
  • 메이븐 라이프사이클
  • 작업기억공간
  • 자바스크립트
  • 객체복사
  • item7
  • bean Validation
  • pnpm
  • rust
  • 토비의 스프링
  • 추상 팩터리 패턴
  • Javadoc 자바독 자바주석 주석 Comment
  • 외래키 제약조건
  • serverless computing
  • 빈 검증
  • 알고리즘
  • 자바 디자인패턴
  • 프로그래머의 뇌
  • Next.js
  • 자바
  • 디자인패턴
  • 메이븐 골
  • 참조 해제
  • try-with-resources
  • 서버리스 컴퓨팅
  • Java
  • prerendering
  • 싱글톤
  • item9
  • 플라이웨이트패턴
  • 슬로우 쿼리
  • 싱글톤 패턴
  • MSSQL
  • 러스트
  • next js app
  • 팩터리 메서드 패턴
  • item8
  • 느린 쿼리
  • 스프링 검증
  • Pre-rendering
  • 싱글턴

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Jake Seo

제이크서 위키 블로그

Java/이펙티브 자바

이펙티브 자바, 쉽게 정리하기 - item2. 생성자에 매개변수가 많다면, 빌더 패턴을 고려하라

2021. 12. 23. 22:46

생성자에 매개변수가 많다면 빌더를 고려하라

생성자에 매개변수가 많다면?

영양 정보를 제공해야 하는데, 클래스 내부에 멤버 필드가 매우 많다고 가정하자.

static 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;
    ...
}
  • 위 상황에서 경우의 수 별로 생성자로 만드는 것은 무리가 있다.
    • 총 6개의 필드가 있고 이 중에 3개를 뽑는 것만 해도 경우의 수가 6*5*4/3*2가 나온다.
    • 혹여나 만든다해도 실제 객체를 생성할 때, 실수하기도 쉽고 어떤 생성자가 있는지 찾아보기도 매우 귀찮다.

자바빈즈 패턴으로 해결해보기

  • 자바빈즈 패턴이란 일단 빈 생성자로 객체를 만든 뒤에 setter를 통해 값을 설정하는 것이다.
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
...
  • 자바빈즈 패턴은 커다란 단점이 몇가지 있다.
    • 객체 하나를 만들려면 메서드 여러개를 호출해야 한다.
    • 객체가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태에 놓이게 된다.
    • 생성자에서 필드 값을 받을 때처럼 클래스 혹은 필드를 불변으로 만들 수 없다.
      • 불변 클래스가 아니기에 Threadsafe 하기 위해 프로그래머의 추가 작업이 필요하다.
      • 불변 클래스를 설명하는 좋은 글

빌더 패턴 적용해보기

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;
    }
}

일반 클래스 내부에 정적 클래스인 Builder 클래스를 만들어 구현한다. Builder의 생성자는 멤버 중 필수인 값을 받아둔다. 필수가 아닌 나머지 값은 빌더의 메서드 체인으로 받는다.

  • NutritionFacts 클래스 내부에 Builder 라는 정적 클래스를 둔다.
  • Builder 클래스를 초기화할 때 필요한 값은 필수 값이며, 불변 값이다.
  • build()를 통한 객체 생성 시에 불변식(invariant)을 통해 매개변수를 검사할 수 있다.
    • 잘못된 점을 발견하면 IllegalArgumentException으로 이유를 알려주면 된다.
  • build()에서는 this 즉, 현재의 Builder 객체를 넘겨서 최종 객체를 만든다.

계층적으로 설계된 Pizza 예제로 빌더 패턴 체험하기

계층적으로 설계된 클래스와 빌더 패턴은 함께 쓰기 매우 좋다.

Pizza 추상 클래스

public abstract class Pizza {
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
    final Set<Topping> toppingSet;

    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Topping> toppingSet = EnumSet.noneOf(Topping.class);

        public T addTopping(Topping topping) {
            toppingSet.add(Objects.requireNonNull(topping));
            return self();
        }

        abstract Pizza build();

        // 하위 클래스는 이 메서드를 재정의 해서 `this`를 반환하게 해야 한다.
        protected abstract T self();
    }

    Pizza(Builder<?> builder) {
        toppingSet = builder.toppingSet.clone();
    }
}
  • 추상 Pizza 클래스이다.
    • 토핑을 얹는 기능을 제공한다.
  • 일반적인 빌더 패턴과 같이 내부에 정적 빌더 클래스를 가지고 있다.
  • 다른 종류의 Pizza 구현체 Builder는 자신(Pizza.Builder)의 하위 타입 Builder를 제너릭으로 받게 될 것이다.
    • 이를 재귀적 타입 바운드(Recursive Type Bound)라고 한다.
    • 무언가를 비교할 때 T 라는 제너릭 타입을 받는다고 치면 <T extends Comparable<T>>와 같이 재귀적 타입 바운드를 해주면 비교할 수 있음이 확실하다.
  • self() 메서드는 형변환하지 않고도 해당 타입 그 자체로 메서드 연쇄를 지원할 수 있게 해준다.

NewYorkPizza 구현 클래스

public class NewYorkPizza extends Pizza {
    public enum Size { SMALL, MEDIUM, LARGE }
    private final Size size;

    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;

        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }

        @Override
        public NewYorkPizza build() {
            return new NewYorkPizza(this);
        }

        @Override
        protected Builder self() { return this; }
    }

    public NewYorkPizza(Builder builder) {
        super(builder);
        this.size = builder.size;
    }
}
  • super()를 통해 받은 토핑을 올린다.
  • size를 통해 받은 사이즈를 적용한다.
    • size는 null이 들어오지 않도록 Builder의 생성자에서 검증한다.

CalzonePizza 구현 클래스

public class CalzonePizza extends Pizza {
    private final boolean sauceInside;

    public static class Builder extends Pizza.Builder<Builder> {
        private boolean sauceInside = false; // 기본 값

        public Builder sauceInside() {
            sauceInside = true;
            return this;
        }

        @Override
        public CalzonePizza build() {
            return new CalzonePizza(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    private CalzonePizza(Builder builder) {
        super(builder);
        this.sauceInside = builder.sauceInside;
    }
}
  • 하위 클래스가 상위 클래스의 메서드가 정의한 반환 타입이 아닌 그 하위 타입을 반환하는 기능을 공변환 타이핑이라고 한다.
    • 추상 클래스에서 build() 메서드는 Pizza 인터페이스를 반환하게 되어 있으나, 현재 구현체에서는 CalzonePizza를 반환하고 있다.

학습 테스트 코드 작성하기

NewYorkPizza newYorkPizza = new NewYorkPizza
        .Builder(MEDIUM) // 필수로 사이즈를 정해야 함
        .addTopping(HAM)
        .addTopping(SAUSAGE)
        .addTopping(MUSHROOM)
        .build();

CalzonePizza calzonePizza = new CalzonePizza
        .Builder()
        .sauceInside()
        .addTopping(ONION)
        .build();
  • 생성자로는 누릴 수 없는 가변인수(varargs)라는 이점을 누릴 수 있다.

정리

  • 빌더 패턴은 상당히 유연하다.
    • 빌더 하나로 여러 객체를 순회하며 만들 수도 있다.
      • build() 메서드가 실행되는 시점에야 실제 객체가 만들어질 것이다.
    • 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수도 있다.
    • 특정 필드는 빌더가 알아서 채우도록 할 수도 있다.

단, 간혹 빌더라는 객체 하나를 더 생성하는 비용 자체가 민감한 경우도 있을 수 있다 이럴 땐 주의해야 한다.

생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면, 빌더 패턴을 선택하는게 더 낫다.

클라이언트 코드를 읽고쓰기가 간결하고, 자바 빈즈보다 훨씬 안전하다.

반응형

'Java > 이펙티브 자바' 카테고리의 다른 글

이펙티브 자바, 쉽게 정리하기 - item5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라  (0) 2021.12.25
이펙티브 자바, 쉽게 정리하기 - item4. 인스턴스화를 막으려면 private 생성자를 사용하라  (0) 2021.12.25
이펙티브 자바, 쉽게 정리하기 - item3. private 생성자나 열거 타입으로 싱글턴임을 보증하라  (0) 2021.12.24
이펙티브 자바, 쉽게 정리하기 - item1. 생성자 대신 정적 팩터리 메서드를 고려하라  (0) 2021.12.22
이펙티브 자바 - 들어가면서...  (0) 2021.12.22
    'Java/이펙티브 자바' 카테고리의 다른 글
    • 이펙티브 자바, 쉽게 정리하기 - item4. 인스턴스화를 막으려면 private 생성자를 사용하라
    • 이펙티브 자바, 쉽게 정리하기 - item3. private 생성자나 열거 타입으로 싱글턴임을 보증하라
    • 이펙티브 자바, 쉽게 정리하기 - item1. 생성자 대신 정적 팩터리 메서드를 고려하라
    • 이펙티브 자바 - 들어가면서...
    Jake Seo
    Jake Seo
    ✔ 잘 보셨다면 광고 한번 클릭해주시면 큰 힘이 됩니다. ✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.

    티스토리툴바