인프콘, 유튜브를 보다가 이런 질문을 만나게 되었다
lombok이 없다면? mock 없이 테스트를 진행해야한다면?
Lombok 없이 builder를 직접 구현해보자.
오늘은 고객을 등록하는 코드를 리팩토링 해보자.
고객은 이름(Name), 생년월일(Birthday), 전화번호(PhoneNumber)를 갖는다.
기존 코드는 JavaBeans Pattern으로 구현되어있다.
👏 JavaBeans Pattern? 👏
👉 매개변수가 없는 생성자로 객체를 만든 후 Setter 메서드를 호출하여 매개변수 값을 설정하는 패턴
public class CustomerWithJavaBeans {
String name;
String birthday;
String phoneNumber;
// JavaBeans패턴(setter)
public void setName(String name) {
this.name = name;
}
public void setBirthday(String birthday) {
this.birthday = birthday;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
}
JavaBeans Pattern을 활용하여 고객을 등록해보자.
public class RegisterCustomerWithJavaBeans {
public static void main(String[] args) {
CustomerWithJavaBeans customer1 = new CustomerWithJavaBeans();
customer1.setName("아무개");
customer1.setBirthday("19951005");
customer1.setPhoneNumber("01012345678");
}
}
JavaBeans Pattern은 다음과 같은 문제를 갖는다.
1) 일관성
문제 : 개발자가 실수🤦♀️🤦♂️로 필수 매개변수를 set 해주지 않는다면, 유효하지 않은 객체가 생성되고 다른 곳에서 호출 시 런타임 예외가 발생할 수 있다
2) 불변성
문제 : 객체를 생성했음에도 여전히 Setter가 외부적으로 노출되어 있어 다른 개발자가 함부로 객체를 조작할 수 있게 된다.
이번에는 빌더 패턴을 직접 구현해보자
👏 Builder Pattern?? 👏
👉별도의 Builder 클래스를 생성하여 순차적으로 값을 입력받은 후 최종적 build() 메소드로 하나의 인스턴스를 생성하는 패턴
빌더 패턴을 직접 구현하기 전에 Customer를 먼저 선언한다
public class Customer {
String name;
String birthday;
String phoneNumber;
public Customer(String name, String birthday, String phoneNumber){
this.name = name;
this.birthday = birthday;
this.phoneNumber = phoneNumber;
}
}
빌더 패턴의 핵심은 return this
다. 여기서 this는 빌더 객체 자신으로서, 자기 자신을 순차적으로 호출한다.
마지막으로, build()를 호출하여 최종적으로 Customer 객체를 생성한다.
public class CustomerWithBuilder {
String name;
String birthday;
String phoneNumber;
public CustomerWithBuilder name(String name){
this.name = name;
return this;
}
public CustomerWithBuilder birthday(String birthday){
this.birthday = birthday;
return this;
}
public CustomerWithBuilder phoneNumber(String phoneNumber){
this.phoneNumber = phoneNumber;
return this;
}
public Customer build(){
return new Customer(name, birthday, phoneNumber);
}
}
빌더 패턴을 활용하여, 고객을 등록해보자.
public class RegisterCustomerWithBuilder {
public static void main(String[] args) {
Customer customer1 = new CustomerWithBuilder()
.name("빌더왕")
.birthday("19951005")
.phoneNumber("01011112222")
.build();
}
}
이렇게만 보면, 왜 Builder 패턴을 써야하는지 감이 오지 않는다.
빌더 패턴을 써야하는 이유 3가지를 살펴보자.
고객의 정보를 받는데, 이름과 전화번호를 필수로 받고 생년월일은 선택적으로 받기로 하자.
이름과 전화번호를 builder()의 생성자로 받는다면 사용자는 필수멤버와 선택멤버를 구분하여 사용할 수 있다
public class CustomerWithBuilder {
// 필수멤버
String name;
String phoneNumber;
// 선택멤버
String birthday;
public CustomerWithBuilder(String name, String phoneNumber){
this.name = name;
this.phoneNumber = phoneNumber;
}
public CustomerWithBuilder birthday(String birthday){
this.birthday = birthday;
return this;
}
public Customer build(){
return new Customer(name, birthday, phoneNumber);
}
}
name과 phoneNumber가 builder() 생성자를 호출 시 필수로 들어가야 하기 때문에 해당 클래스를 이용하는 개발자는 다음과 같이 사용하게 된다.
public class RegisterCustomerWithBuilder {
public static void main(String[] args) {
Customer customer1 = new CustomerWithBuilder("빌더왕", "01012345678") // 필수멤버
.birthday("19951005") // 선택멤버
.build();
}
}
빌더 패턴을 사용해야하는 두 번째 이유.
build()를 호출할 때까지 객체 생성을 지연시켜 필요에 따라 객체를 완성시킬 수 있다.
public class RegisterCustomerDelayedBuild {
public static void main(String[] args) {
List<CustomerWithBuilder> builders = new ArrayList<>();
builders.add(
new CustomerWithBuilder("매실맨", "01012345678")
.birthday("19951005")
);
builders.add(
new CustomerWithBuilder("오미자", "01033334444")
.birthday("19920510")
);
// ... 일련의 로직 처리 ...
// 최종 빌더
for (CustomerWithBuilder builder : builders){
Customer customer = builder.build();
System.out.println(customer);
}
}
}
빌더 패턴을 사용해야하는 세 번째 이유가 가장 중요하다.
이펙티브자바에서는 빌더 패턴을 사용해야 하는 이유로 불변객체
를 제시한다.
JavaBeans Pattern의 가장 큰 문제점은 Setter가 외부에 노출되어 있어 이용자에 따라 객체가 변할 수 있다.
다음, 이펙티비 자바에서 제시한 코드를 분석해보며 불변객체
가 무엇이며, 어떻게 구현되는지 살펴보자.
class Customer {
// final로 필드 선언
private final String name;
private final String birthday;
private final String phoneNumber;
// 빌더 클래스에서만 호출(private 선언)
private Customer(Builder builder) {
this.name = builder.name;
this.birthday = builder.birthday;
this.phoneNumber = builder.phoneNumber;
}
// 정적 내부 빌더 클래스
public static class Builder {
// 필수 매개변수
private final String name;
private final String phoneNumber;
// 선택 매개변수
private String birthday;
// 필수 매개변수는 생성자로 받기
public Builder(String name, String phoneNumber) {
this.name = name;
this.phoneNumber = phoneNumber;
}
// 선택 매개변수는 자기 자신을 호출하며 반환
public Builder birthday(String birthday) {
this.birthday = birthday;
return this;
}
// build() 호출하여 최종 불변객체 획득
public Customer build() {
return new Customer(this);
}
}
}
불변객체
: 객체 생성 이후 내부의 상태가 변하지 않는 객체
불변 객체는 오로지 읽기(get) 메소드만을 제공하며 쓰기(set)는 제공하지 않는다.
클래스의 맴버 변수에 final로 선언하면 상수값이 되거나 write-once 필드가 된다.
불변 객체를 사용해야 하는 이유는
각 이유에 대한 설명까지 적기엔 길어질 것 같아, 정리를 잘 해놓으신 글이 있어 가져왔습니다.
-> 불변객체를 사용해야 하는 이유
참고자료