Search
Duplicate

아이템 02 - 생성자에 매개변수가 많다면 빌더를 고려하라

작성자
챕터
2장 - 객체 생성과 파괴
최종 편집
2023/09/13 12:39
생성 시각
2023/07/11 00:53

점층적 생성자 패턴

필수 매개변수만 받는 생성자, 필수 매개변수와 선택 매개변수 1개를 받는 생성자, ... 필수 매개변수와 선택 매개변수 n개를 받는 생성자와 같은 방식으로 생성자를 만들어 나가는 방식
단점
매개변수가 많아지면 클라이언트 코드를 작성하거나 읽기 어려워진다.
매개변수가 몇 개이고 각 값의 의미가 무엇인지를 항상 파악해야 한다.
매개변수의 순서를 실수로 바꿔도 컴파일러는 알아채지 못하고 런타임에서야 발견할 수 있다.
예제 코드
public class Job { private final String name; private final int salary; private final int workingHours; private final int vacationDays; public Job(String name, int salary) { this(name, salary, 0); } public Job(String name, int salary, int workingHours) { this(name, salary, workingHours, 0); } public Job(String name, int salary, int workingHours, int vacationDays) { this.name = name; this.salary = salary; this.workingHours = workingHours; this.vacationDays = vacationDays; } }
Java
복사

자바 빈즈 패턴

매개변수가 없는 생성자로 객체를 만든 후, setter 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식
단점
객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.
클래스를 불변으로 만들 수 없으며, 스레드 안전성을 얻으려면 프로그래머가 추가 작업을 해줘야 한다.
질문
객체가 완전히 생성되기 전까지 일관성이 무너진다는 것이 무슨 의미인지 잘 모르겠다.
→ 생성자로 객체를 생성한 후부터 setter 메서드들을 호출해서 원하는 매개변수의 값을 설정한 최종 객체를 만드는 과정에서 해당 객체는 불완전한 상태에 있다는 의미로 해석됨.
예제 코드
public class Job { // (선택) 매개변수들은 (필요하다면) 기본값으로 초기화 private String name; private int salary; private int workingHours = 0; private int vacationDays = 0; public Job() { } public void setName(String name) { this.name = name; } public void setSalary(int salary) { this.salary = salary; } public void setWorkingHours(int workingHours) { this.workingHours = workingHours; } public void setVacationDays(int vacationDays) { this.vacationDays = vacationDays; } }
Java
복사

빌더 패턴

필수 매개변수만으로 생성자를 호출해 빌더 객체를 만든 후, 빌더 객체가 제공하는 setter 메소드들로 원하는 선택 매개변수의 값을 설정하고, 매개변수가 없는 build 메소드로 최종적으로 (일반적으로 불변인) 객체를 생성하는 방식
빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어두는 것이 일반적이다.
빌더의 setter 메소드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다. - fluent API or method chaining 이라 부른다.
빌더 패턴은 명명된 선택적 매개변수(named optional parameters)를 흉내 낸 것이다.
빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.
빌더 패턴에서의 유효성 검사
빌더의 생성자와 setter 메서드에서 입력 매개변수를 검사하고, build 메서드가 호출하는 생성자에서 여러 매개변수에 대한 불변식을 검사한다.
공격에 대비해 이런 불변식을 보장하려면 빌더로부터 매개변수를 복사한 후 해당 객체 필드들도 검사해야 한다.
불변식(invariant) : 객체의 생성과 파괴가 일어날 때까지 항상 참인 조건
변경을 허용할 수는 있으나 주어진 조건 내에서만 허용한다는 의미
ex) 리스트의 크기는 반드시 0 이상이어야 한다.
ex) 기간을 표현하는 Period 클래스에서 start 필드의 값은 반드시 end 필드의 값보다 앞서야(작아야) 한다.
공변 반환 타이핑 (covariant return typing)
하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능
변성(variance) - 타입 간에 서로 어떤 관계가 있는지를 나타내는 개념
java generic에서는 3가지 가변성 성질을 제공한다.
공변(covariance) : 타입의 계층 관계가 그대로 유지되는 성질 ex) A가 B의 하위 타입일 때, T<A>는 T<B>의 하위 타입이다.
반공변(contravariance) : 타입의 계층 관계가 반대로 유지되는 성질 ex) A가 B의 하위 타입일 때, T<B>는 T<A>의 하위 타입이다.
무공변(invariance) : 타입의 계층 관계가 유지되지 않는 성질 ex) A가 B의 하위 타입이라고 해도, T<A>와 T<B>는 아무 관계가 없다.
이러한 3가지 공변들로 메소드의 인자로 들어오는 파라미터의 타입에 제한을 걸 수 있다.
공변은 upperbound로 그 객체의 하위 타입만 허용하는 것이고, 반공변은 lowerbound로 그 객체의 상위 타입만 허용하는 것이다.
이러한 타입 관계는 객체지향 프로그래밍 원칙 중 하나인 "리스코프 치환 원칙"에 해당하며, 이 원칙은 상위 타입이 사용되는 곳에는 하위 타입의 인스턴스를 넣어도 이상 없이 동작해야 함을 의미한다.
빌더 패턴의 장점
빌더를 이용하면 가변인수(varargs) 매개변수를 여러개 사용할 수 있다. (가변인수는 타입이 같은 매개변수를 0개 이상 받을 수 있게 해준다.) 각각을 적절한 메서드로 나눠 선언하거나 같은 메서드를 여러 번 호출하면서 넘겨진 매개변수들을 하나의 필드로 모을 수도 있다.
유연하다 -> 빌더 하나로 여러 객체를 순회하면서 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 만들수도 있다.
빌더 패턴의 단점
객체를 생성하려면 먼저 그 객체를 만들 빌더부터 생성해야 하기 때문에 빌더 생성 비용이 클 경우, 성능 저하의 원인이 될 수 있다.
매개변수가 일정 개수 이상 (보통 4개) 되어야 코드가 길어지는 비용을 감수하고 사용하는 가치가 있다. - API는 시간이 지날수록 매개변수가 많아지는 경향이 있기 때문에 builder pattern을 사용하는 것이 좋다.
질문
빌더의 생성자와 setter 메서드에서 입력 매개변수에 대한 불변식을 검사하면 되지 않나? 왜 1차적으로 검사하고, 이후에 다시 한번 build 메서드가 호출하는 생성자에서 불변식을 검사해야 하는지 모르겠다.
→ 생성자와 setter 메서드를 호출하고 마지막으로 build 메서드를 호출해서 객체를 생성하는 과정에서 입력 매개변수에 대한 값이 바뀔 여지가 있어서?
공변 반환 타이핑은 결국 하위 클래스의 메서드가 상위 클래스의 메서드를 overriding 하였다는 의미 아닌가? 이 외의 추가적인 의의가 있는가?
→ Yes
빌더 하나로 여러 객체를 순회하면서 만들 수도 있다는 것이 어떤 의미인지 잘 모르겠다.
→ 상위 클래스에서 정의된 빌더를 하위 클래스의 객체를 생성하기 위해서 사용가능 하다는 의미로 보임.
예제 코드
public class Job implements AutoCloseable { private final String name; private final int salary; private final int workingHours; private final int vacationDays; public static class Builder { // 필수 매개변수 private final String name; private final int salary; // 선택 매개변수 - 기본값으로 초기화 private int workingHours = 0; private int vacationDays = 0; public Builder(String name, int salary) { this.name = name; this.salary = salary; } public Builder setWorkingHours(int workingHours) { this.workingHours = workingHours; return this; } public Builder setVacationDays(int vacationDays) { this.vacationDays = vacationDays; return this; } public Job build() { return new Job(this); } } private Job(Builder builder) { name = builder.name; salary = builder.salary; workingHours = builder.workingHours; vacationDays = builder.vacationDays; } @Override public void close() throws Exception { System.out.println("Job is closed."); }
Java
복사