study/effective java

[item2] 생성자에 매개변수가 많다면 빌더 패턴을 고려해라

Green-Flamingo 2023. 4. 24. 02:19

매개변수가 많으면 발생하는 문제점

코드를 작성하다보면 어쩔 수 없이 매개변수가 많은 객체를 만들어야 하는 상황이 있을 때도 있다. 이럴 때 생성자나 정적 팩토리 메소드로 매개변수를 사용하는 방법은 매개변수의 순서를 기억하기 어렵고 일부가 누락될 수 있는 문제점이 있다. 이 문제를 해결하기 위해 매개변수가 많은 객체를 생성할 때는 빌더 패턴을 이용해 매개변수의 누락을 방지하도록 할 수 있다.

기존의 생성자를 이용한 인스턴스 생성

생성자를 통해 만들려면 다음과 같은 코드를 이용한다.

public class Person {
    private final String name;
    private final int age;
    private final String address;
    private final String phoneNumber;
    private final String email;
    
    public Person(String name, int age, String address, String phoneNumber, String email) {
        this.name = name;
        this.age = age;
        this.address = address;
        this.phoneNumber = phoneNumber;
        this.email = email;
    }
    // ...
}

5개의 속성을 선언해야하며 이중 누락할 수도 있다. 심지어 age 를 제외한 나머지 속성들은 모두 같은 타입을 가지기에 순서를 잘못쓰면 이름에 주소가 들어가거나 핸드폰 번호에 이메일이 들어갈 수 있는 실수를 범할 수 있다.

빌더 패턴을 통하면 이 문제를 해결할 수 있다.

public class Person {
    private final String name;
    private final int age;
    private final String address;
    private final String phoneNumber;
    private final String email;
    
    private Person(PersonBuilder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.address = builder.address;
        this.phoneNumber = builder.phoneNumber;
        this.email = builder.email;
    }
    
    public static class PersonBuilder {
        private final String name;
        private final int age;
        private String address = "";
        private String phoneNumber = "";
        private String email = "";
        
        public PersonBuilder(String name, int age) {
            this.name = name;
            this.age = age;
        }
        
        public PersonBuilder address(String address) {
            this.address = address;
            return this;
        }
        
        public PersonBuilder phoneNumber(String phoneNumber) {
            this.phoneNumber = phoneNumber;
            return this;
        }
        
        public PersonBuilder email(String email) {
            this.email = email;
            return this;
        }
        
        public Person build() {
            return new Person(this);
        }
    }
    // ...
}

코드는 길지만 객체 생성은 가독성이 높아진다. 아래의 코드는 위에서 작성한 빌더 패턴을 통해 객체를 생성하는 코드이다.

Person.PersonBuilder builder = new Person.PersonBuilder("alice", 25);
builder.address("Seoul")
       .phoneNumber("010-1234-5678")
       .email("a@a.com");
Person person = builder.build();

이 코드를 보면 다음의 이점을 얻을 수 있다.

  1. 필수적인 값과 선택적인 값을 분리시킬 수 있다.
    • 빌더를 만들 때 이름과 나이를 받고 시작한다. 이는 이름과 나이값을 사람을 식별하는 필수 요소로 볼 수 있기에 이 값을 넣지 않는한 빌더를 통한 객체 생성을 시작할 수 없다.
    • 나머지를 선택 속성으로 넣어서 필수적인 요소와 선택적인 요소를 분리시킬 수 있다.
  2. 가독성이 높아진다.
    • address, phoneNumber, email 메소드를 통해 인자로 들어오는 값이 어떤 의미를 가지는지 표현할 수 있다.
  3. 계층형 빌더: 계층적으로 설계된 클래스에 용이하다.
    • 계층형 빌더란 빌더를 계층형 구조로 설계한 것이다.
    • Person을 확장해서 직업을 가진 객체를 만든다고 가정해보자. 직업을 가진 객체들도 빌더 패턴을 이용하도록 하여 Person이 가지는 속성들을 모두 가지게 할 수 있다. 빌더 자체를 계층적으로 가지도록 하여 Person의 기본정보 + 직업 정보를 하나의 빌더가 아닌 부모 객체인 Person의 빌더를 재사용하여 유연하게 객체를 생성할 수 있다.

빌더 패턴은 매개변수가 많을 때 유용한 패턴이다. 매개변수가 적으면 빌더 패턴으로 작성하는 것은 오히려 낭비이다. 또한 빌더는 성능 이슈가 나올 수 있다. 매개변수를 받자마자 즉시 생성하는 것이 아닌 build() 라는 명시적인 호출이 있을 때까지 메모리에 정보를 저장해놓고 있기 때문이다. 그러나 이에 대한 차이는 생각보다 크지 않기에 성능에 민감한 내용이 아니라면 무관하다.


결론

사실상 Gof의 빌더 사용을 권장하는 내용이므로 굳이 자바가 아니더라도 다른 언어에서도 사용가능한 패턴이다. Lombok에서 빌더를 만들어주는 어노테이션도 있으니 매개변수가 많은 객체를 만들 때는 이 방법을 사용해보는 것도 좋을 것이다.


Reference