위 책을 보면서 정리한 글입니다.
복잡하게 존재하는 생성자는 객체 생성과 초기화를 혼란스럽게 할 수 있다.
필드를 초기화하기 위해 호출하는 생성자가 복잡하게 선언되어 있으면 잘못된 객체 생성이 생길 수 있다. 또한, 객체의 필드가 추가 또는 삭제되거나 초기값들의 변경에 대한 요구사항이 생기면 생성자의 수정이 불가피하며, 생성자가 호출된 모든 코드의 수정이 필요하다.
↪ 명확하지 않고 수정에 유연하지 못한 코드는 객체 생성과 초기화를 독립적으로 책임지는 클래스로 분리하여 개선할 필요가 있다.
사용자의 추가 및 정보 수정을 할 때 반드시 초기화되어야 하는 User 객체 필드와 선택적으로 초기화되어야 하는 User 객체 필드를 구분 짓는다.
↪ 필드의 추가 변경이 기존 코드에 영향을 주지 않는 구조로 변경하는데, 이를 위해 빌더 패턴을 활용
빌더 패턴을 활용하여 객체 생성 시에도 정확한 호출이 가능하여 런타임 오류를 방지해주고 요구사항 변경에도 효과적으로 대응 가능
빌더 패턴이란
- 복잡한 객체의 생성 과정과 표현 방법을 분리하여 생성 절차가 동일하더라도 서로 다른 표현 결과를 만들 수 있는 패턴
- 복잡한 객체의 생성을 빌더에 위임 -> 내부 구조를 몰라도 각각의 요소를 조합 --> 완성된 객체
빌더 패턴의 장점
- 매개변수에 대한 필수값과 선택값을 명확하게 판단 가능
- 객체의 필드 변경에 대해서 기존 코드에 영향 없이 대응 가능
빌더 패턴의 단점
- 기존 생성 방식을 이용한 개발자라면 빌더 구현 자체가 생산 비용
- 하지만 유지보수를 생각하면 오히려 효율적으로 생각할 수 있다.
빌더 패턴을 언제 이용하는가
1. 생성자 초기화 시 필수 또는 선택해야 하는 값이 존재할 때
2. 생성자 오버로딩이 많아져서 사용이 혼동될 때
3. 매개변수가 4개 이상일 때
4. 오버로딩 방식과 Setter 방식이 혼용되어 사용될 때
5. 초기화한 객체를 불변(만들어진 객체는 수정 불가)으로 만들고 싶을 때
public class User {
private String id; /** 아이디 : 필수 **/
private String password; /** 비밀번호 : 필수 **/
private String name; /** 이름 : 필수 **/
private String email; /** 이메일 : 선택 **/
private String address; /** 주소 : 선택 **/
private String mobile; /** 전화번호 : 추가 **/
private String passport; /** 여권번호 : 추가 **/
public User(String id, String password, String name) {
this(id, password, name, null);
}
public User(String id, String password, String name, String email) {
this(id, password, name, email, null);
}
public User(String id, String password, String name, String email, String address) {
this.id = id;
this.password = password;
this.name = name;
this.email = email;
this.address = address;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public void setPassport(String passport) {
this.passport = passport;
}
}
public class Account {
private UserService userService = new UserService();
public User createUser(String id, String password, String address) {
User user = new User(id, password, address);
return userService.create(user);
}
public User updateUser(String id, String password, String email, String name, String address, String mobile, String passport) {
User user = new User(id, password, name, email, address);
user.setMobile(mobile);
user.setPassport(passport);
return userService.update(user);
}
}
public class UserService {
public User create(User user) {
// 중략
return user;
}
public User update(User user) {
// 중랼
return user;
}
}
클라이언트에서 전달받은 매개변수로 User 객체를 생성하고, UserService 클래스의 메서드를 호출합니다. 사실 흔히 보는 스프링에서의 MVC 패턴과 유사하게 구성되어 있다.
위의 코드에서의 문제점은 객체를 생성하기 위해서는 생성자를 호출해야 하지만, 여러 생성자가 오버로드되어 있습니다. 따라서, 어떤 생성자로 어떤 매개변수를 전달하여 인스턴스를 생성할 수 있는지 명확하게 나타내지 못한다.
또, 부가적인 필드의 초기화는 Setter 메서드를 이용하므로, 해당 필드에 대해서는 객체 호출 시점에서 초기화를 보장할 수 없다.
초기화 방식이 일관되지 않아 가독성도 나쁘다.
public class AccountTest {
private Account account = new Account();
@Test
public void testCreateUser_사용자_계정_생성() {
//GIVEN
//WHEN
User user = account.createUser("id", "password", "name");
//THEN
Assert.assertNoNull(user);
}
@Test
public void testUpdateUser_사용자_계정_수정() {
//GIVEN
//WHEN
User user = account.updateUser("id", "password", "name", "email", "address", "mobile", "passort");
//THEN
Asser.assertNotNull(user);
}
}
public class UserTest {
private User account = new User();
@Test
public void testUserBuilder_사용자_빌더_객체_생성() {
//GIVEN
//WHEN
User user = new Uesr.Builder("id", "password", "name").build();
//THEN
Assert.assertNoNull(user);
}
}
public class User {
private String id; /** 아이디 : 필수 **/
private String password; /** 비밀번호 : 필수 **/
private String name; /** 이름 : 필수 **/
private String email; /** 이메일 : 선택 **/
private String address; /** 주소 : 선택 **/
private String mobile; /** 전화번호 : 추가 **/
private String passport; /** 여권번호 : 추가 **/
public static class Builder {
private String id; /** 아이디 : 필수 **/
private String password; /** 비밀번호 : 필수 **/
private String name; /** 이름 : 필수 **/
private String email; /** 이메일 : 선택 **/
private String address; /** 주소 : 선택 **/
private String mobile; /** 전화번호 : 추가 **/
private String passport; /** 여권번호 : 추가 **/
// 필수값
public Builder(String id, String password, String name) {
this.id = id;
this.password = password;
this.name = name;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder mobile(String mobile) {
this.mobile = mobile;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public Builder passport(String passport) {
this.passport = passport;
return this;
}
public User build() {
return new User(this);
}
}
private User(Builder builder) {
this.id = builder.id;
this.password = builder.password;
this.name = builder.name;
this.email = builder.email;
this.address = builder.address;
this.mobile = builder.mobile;
this.passport = builder.passport;
}
}
Getter, Setter를 이용하는 자바빈즈 패턴을 사용하면 여러 명이 진행하는 프로젝트에서 누군가 의도하지 않게 Setter를 이용하여 중간에 값을 변경할 수 있다.
↪ 이런 이유로 해당 필드값의 변경으로 발생하는 문제를 파악하는 데 시간이 많이걸린다. 빌더 패턴을 이용하면 이러한 문제를 사전에 방지 할 수 있다.
빌더 패턴은 매개변수의 선택값과 필수값이 존재하고, 기존 코드에 영향 없이 매개 변수 변화에 대응해야 할 때 강점이 있다.
빌더 객체를 생성해야 하므로 기존 코드 생성 방식보다 코드와 비용은 늘어날 수 있지만 클라이언트의 유지보수 비용까지 생각한다면 의미있는 작업.