Effective Java 3/E
정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋겠다. 그렇다고 하더라도 정적 팩터리를 사용하는게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자!
아이템2: 생성자에 매개변수가 많다면 빌더를 고려하라
Class 설계시 필수, 선택적으로 받을 매개변수가 구분됩니다.
이렇게 설계된 다향한 형태의 클래스들을 객체화하는 대표적인 3가지 패턴이 존재합니다.
1. 점층적 생성자 패턴 ( Telescoping Constructor Pattern )
2. 자바 빈즈 패턴 ( Java Beans Pattern )
3. 빌더 패턴 ( Builder Pattern )
1. 점층적 생성자 패턴 ( Telescoping constructor pattern )
필수 매개변수만 받는 생성자부터 추가적으로 선택 매개변수를 1개, 2개... N개 형태로 선택 매개변수를 전부 받는 생성자까지 늘려가는 방식이며, 마치 생성자가 점층적으로 많아지는 생성자를 가지도록 한 디자인 패턴입니다.
public class Member {
private String name; // 필수
private int age; // 필수
private String address; // 선택
private String phone; // 선택
private String email; // 선택
// 필수 매개변수를 가지는 생성자
public Member(String name, int age) {
this(name, age, null, null, null);
}
// 선택 매개변수 address가 추가된 생성자
public Member(String name, int age, String address) {
this(name, age, address, null, null);
}
// 선택 매개변수 phone이 추가된 생성자
public Member(String name, int age, String address, String phone) {
this(name, age, address, phone, null);
}
// 모든 매개변수를 가지는 생성자
public Member(String name, int age, String address, String phone, String email) {
this.name = name;
this.age = age;
this.address = address;
this.phone = phone;
this.email = email;
}
}
점층적 생성자 패턴의 단점
- 매개변수가 많아질수록 많은 조합이 만들어지고, 생성자의 수가 많아집니다. 이는 코드 작성 효율과 가독성이 저하되는 영향을 줍니다.
- 클래스의 생성자를 호출하는 입장에서 해당 매개변수가 맞는지, 매개변수의 개수는 제대로 입력한 것인지 확인해야 하는 불편함이 있습니다.
- 매개변수의 타입이 같은 경우 생성자를 만들 수 없습니다.
Member member = new Member("jan", 99, "KOR", "01012345678");
Test test = new Test(12, 10, 1, 330, 9);
Member의 경우 간단한 생성자라서 들어가는 값만 봐도 어떤 내용인지 유추할 수 있습니다.
하지만 예를 들어 Test 같은 생성자에 인자로 모두 int 값만 들어가는 경우가 있다면 12가 무슨 값인지, 330이 무슨 값인지 알기 위해서는 생성자를 찾아봐야 하는 경우가 생깁니다.
// 선택 매개변수 address가 추가된 생성자
public Member(String name, int age, String address) {
this(name, age, address, null, null);
}
// 선택 매개변수 phone이 추가된 생성자
public Member(String name, int age, String phone) {
this(name, age, null, phone, null);
}
마지막 매개변수 타입이 같은 생성자를 만들 수 없는 경우입니다. 다음과 같은 생성자는 인자로 받는 타입이 같기 때문에 이렇게 두 개의 생성자를 동시에 생성할 수 없습니다.
2. 자바 빈즈 패턴 ( Java Beans Pattern )
매개변수가 없는 생성자로 객체를 만든 후 Setter 메소드를 호출해 원하는 매개변수의 값을 설정하는 방식의 패턴이다.
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setAddress(String address) {
this.address = address;
}
public void setPhone(String phone) {
this.phone = phone;
}
public void setEmail(String email) {
this.email = email;
}
Member member = new Member();
member.setName("jan");
member.setAge(99);
member.setAddress("KOR");
member.setPhone("01012345678");
자바빈즈 패턴의 장단점
코드가 길지만 인스턴스를 만들기 쉽고, 가독성이 좋다는 장점이 있습니다.
단점으로는 하나의 객체를 완성하기까지 메서드를 여러 번 호출해야 하고, setter 메서드가 있기 때문에 일관성이 무너진 상태.
앞선 점층적 생성자 패턴에서는 매개변수들이 유효한지 생성자에서만 확인하면 일관성 유지가 가능했지만, 자바빈즈 패턴의 경우에는 불가능하다. 때문에 불변의 객체(immutable)를 만들 수 없고, 쓰레드 안정성을 위해 추가적 작업(수동으로 freeze)이 필요하다.
3. 빌더 패턴 ( Builder Pattern )
점층적 생성자 패턴의 안전성 + 자바빈즈 패턴의 가독성 두가지 패턴의 장점을 가진 패턴이다.
public class Member {
private String name;
private int age;
private String address;
private String phone;
private Member(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.address = builder.address;
this.phone = builder.phone;
}
// 빌더 호출, 외부에서 Member.builder() 으로 접근할 수 있도록 static 메소드로 생성
public static Builder builder() {
return new Builder();
}
// static 형태의 inner class 생성
public static class Builder {
private String name;
private int age;
private String address;
private String phone;
private Builder() {};
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
// 마지막에 build 메소드를 실행하면 this가 return 되도록 구현
public Member build() {
return new Member(this);
}
}
}
Build Pattern을 사용할 클래스는 위와 같이 구현할 수 있으며,
Member member = Member.builder()
.name("jan")
.age(99)
.address("jan")
.phone("010")
.build();
다음과 같이 사용할 수 있습니다.
* Member 클래스에는 setter 메서드와 public 생성자가 없기 때문에 Member 객체를 얻기 위해서는 오직 Builder 클래스를 통해서만 가능합니다.
각 매개변수를 넣어주는 메서드 체이닝 기법을 적용하고, 필요한 매개변수를 세팅한 후 build() 메서드로 객체를 생성합니다.
이렇게 빌더 패턴을 사용하면 어느 필드에 어떤 값을 채워야 하는지 명확하게 알 수 있으며, 데이터 일관성, 객체 불변성 등을 만족시키며 코드의 가독성 또한 향상됩니다.
하지만 빌드 패턴은 구현시 많은 코드양이 필요하고, Builder라는 객체를 하나 더 만드는 것이기 때문에 사용하는 코드에 따라 성능이 낮아질 수 있습니다.
따라서 클래스를 설계할 때 필수, 선택 인자들이 많은 경우 Builder 패턴을 사용하는 것이 효율적입니다.
+ Lombok 라이브러리를 활용한 빌더 패턴
앞서 생성한 빌더 패턴을 @Builder 어노테이션 하나로 구현할 수 있다.
구현한 객체
@Builder
public class Member {
private String name;
private int age;
private String address;
private String phone;
}
컴파일된 클래스파일
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.example.demo;
public class Member {
private String name;
private int age;
private String address;
private String phone;
Member(String name, int age, String address, String phone) {
this.name = name;
this.age = age;
this.address = address;
this.phone = phone;
}
public static Member.MemberBuilder builder() {
return new Member.MemberBuilder();
}
public static class MemberBuilder {
private String name;
private int age;
private String address;
private String phone;
MemberBuilder() {
}
public Member.MemberBuilder name(String name) {
this.name = name;
return this;
}
public Member.MemberBuilder age(int age) {
this.age = age;
return this;
}
public Member.MemberBuilder address(String address) {
this.address = address;
return this;
}
public Member.MemberBuilder phone(String phone) {
this.phone = phone;
return this;
}
public Member build() {
return new Member(this.name, this.age, this.address, this.phone);
}
public String toString() {
return "Member.MemberBuilder(name=" + this.name + ", age=" + this.age + ", address=" + this.address + ", phone=" + this.phone + ")";
}
}
}
@Builder를 Class위에 선언하면 @AllArgsConstrutor를 붙인 것과 같은 효과를 보는데, Builder Pattern을 사용할 경우 필수 매개 변수를 최소화 해주는 것이 좋다.
@Builder
public class Member {
@NonNull
private final String name;
@NonNull
private final Integer age;
private String address;
private String phone;
}
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.example.demo;
import lombok.NonNull;
public class Member {
@NonNull
private final String name;
@NonNull
private final Integer age;
private String address;
private String phone;
Member(@NonNull String name, @NonNull Integer age, String address, String phone) {
if (name == null) {
throw new NullPointerException("name is marked non-null but is null");
} else if (age == null) {
throw new NullPointerException("age is marked non-null but is null");
} else {
this.name = name;
this.age = age;
this.address = address;
this.phone = phone;
}
}
public static Member.MemberBuilder builder() {
return new Member.MemberBuilder();
}
public static class MemberBuilder {
private String name;
private Integer age;
private String address;
private String phone;
MemberBuilder() {
}
public Member.MemberBuilder name(@NonNull String name) {
if (name == null) {
throw new NullPointerException("name is marked non-null but is null");
} else {
this.name = name;
return this;
}
}
public Member.MemberBuilder age(@NonNull Integer age) {
if (age == null) {
throw new NullPointerException("age is marked non-null but is null");
} else {
this.age = age;
return this;
}
}
public Member.MemberBuilder address(String address) {
this.address = address;
return this;
}
public Member.MemberBuilder phone(String phone) {
this.phone = phone;
return this;
}
public Member build() {
return new Member(this.name, this.age, this.address, this.phone);
}
public String toString() {
return "Member.MemberBuilder(name=" + this.name + ", age=" + this.age + ", address=" + this.address + ", phone=" + this.phone + ")";
}
}
}
Null 체크
@NonNull 어노테이션을 변수에 붙이면 자동으로 null 체크를 해줍니다. 즉, 해당 변수가 null로 넘어온 경우, NullPointerException 예외를 일으켜 줍니다.