이번 일정 관리 과제 때 구글링을 하면서 정말 많이 본게 builder 패턴이었다.
사람들이 자주 사용하는 데에는 이유가 있을 거라고 생각하고, 나도 머리에 넣고 다음에 사용해보려고 한다.
빌더 패턴은 복잡한 객체를 생성하는 방법 중 하나로, 객체의 생성 코드와 객체의 사용 코드를 분리하여 코드의 가독성과 유지 보수성을 향상 시키는 패턴이다.
더불어 디자인 패턴 중 생성 패턴에 속한다.
빌더 패턴은 많은 Optional한 멤버 변수나 파라미터 혹은 지속성 없는 상태 값들에 대해 처리해야 하는 문제를 해결한다.
객체를 생성할 때 필요한 매개변수들을 모두 포함하는 생성자를 사용하게 되는데 이 때, 매개변수의 순서를 혼동하기 쉽고 일부 매개변수에 대해 null 값 또는 기본값을 할당 해야할 때 그 의미를 파악하기 어렵게 만들 수 있다.
빌더 패턴은 객체의 불변성을 유지하는데 도움이 된다.
객체가 생성되고 난 후에는 객체의 상태를 변경할 수 없게 만들면, 다중 스레드 환경에서 해당 객체를 안전하게 사용할 수 있다.
Builder 패턴은 별도의 Builder 클래스를 만들어 필수 값에 대해서는 생성자를 사용하고, 선택적인 값들에 대해서는 메소드를 통해 필요한 값들을 입력받으며
build()메소드를 이용해 인스턴스를 리턴 받게 한다.
빌더 클래스를 선언하고, 생성할 객체의 속성에 대한 setter 메소드를 구현한다.
이 메소드들은 빌더 객체 자신을 반환하므로 메소드 체이닝이 가능하다.
build() 메소드를 통해 최종적인 객체를 생성한다.
public class User {
private String name;
private int age;
public static class Builder {
private String name;
private int age;
public Builder withName(String name) {
this.name = name;
return this;
}
public Builder withAge(int age) {
this.age = age;
return this;
}
public User build() {
User user = new User();
user.name = this.name;
user.age = this.age;
return user;
}
}
public static void main(String[] args) {
User user = new User
.Builder()
.withName("Henry")
.withAge(30)
.build();
}
}
그러나 더 쉬운 방법이 있다. 바로 어노테이션이다.
@Builder
@Getter
@Setter
public class User {
private String name;
private int age;
}
public static void main(String[] args) {
User user = new User
.Builder()
.withName("Henry")
.withAge(30)
.build();
}
🌞 이렇게 복잡한 객체의 생성 과정을 단계별로 나누어 클라이언트가 이해하기 쉽도록 인터페이스를 제공하는 것이 빌더 패턴의 핵심이며, DTO 같은 복잡한 객체 생성에 사용하면 많은 이점이 있다!
어떻게 생성하는지는 자유지만, 다음 같은 상황을 보자.
예를 들어 User 객체를 생성해야 하는데 age라는 파라미터가 필요 없다고 가정해보자. 생성자나 정적 메소드를 이용하게 되면, 개발자는 age 변수에 더미 값(0 또는 null)을 넣어주거나 age가 없는 생성자를 새로 만들어줘야 한다.
// 더미 값
User user = new User("김수한무", 0, "가수", "서울");
// age가 없는 생성자
public User(String name, String job, String address) {
this.name = name;
this.job = job;
this.address = address;
}
// age가 없는 정적 메소드
public statis User of(String name, String job, String address) {
return new User(name, 0, job, address);
}
이러한 작업이 빈번해지면 효율이 급격히 떨어지게 된다.
이런 상황에서 빌더 패턴을 사용하면 어떻게 될까?
User user = User.builder()
.name("김수한무")
.job("가수")
.address("서울")
.build();
이렇게 필요한 데이터만 설정할 수 있다는 큰 장점 덕에, 생성자 또는 정적 메소드와 비교했을 때 이점이 있다고 볼 수 있겠다.
또 다른 이점이 있다.
예를 들어, User 클래스에 성별을 나타내는 변수 gender를 추가해야 하는 상황을 생각해보자.
심지어 이미 생성자로 다음과 같이 객체를 만드는 코드가 존재하고 있다.
User user = new User("김수한무", 30, "가수", "서울");
이렇게 되면 새롭게 추가되는 변수 때문에 기존의 코드를 수정해야 하는 상황에 놓이게 된다.
그런데 만약 기존의 코드가 몇 백줄, 몇 천줄이라면?
당연히 감당하기 힘들다.
이럴 때 빌더 패턴을 이용하면 새로운 변수가 추가되는 등의 상황이 생겨도 기존 코드에 영향을 최소화 할 수 있다.
가독성 면에서도 뛰어나다.
User user = new User("김수한무", 0, "가수", "서울");
User user = User.builder()
.name("김수한무")
.age(30)
.job("가수")
.address("서울")
.build();
위 아래 코드를 비교해봐도, 빌더 패턴을 사용하면 각 값이 어떤 뜻을 나타내는지 한 눈에 알 수 있다.