생성자에 매개변수가 많다면 빌더를 고려하라.

younk·2023년 11월 18일
0

이펙티브자바

목록 보기
1/1

Builder

빌더패턴은 객체의 생성과정과 표현방법을 분리하여, 다양한 구성의 인스턴스를 만드는 생성 패턴이다.
생성자에 들어갈 변수를 각각 메소드로 받고, 마지막에 통합 빌드하여 객체를 생성한다.

빌더패턴의 탄생 배경

1) 점층적 생성자 패턴

점층적 생성자 패턴은 필수 매개변수와 선택 매개변수를 점층적으로 늘려가며 받는 형태이다. 이 방식은 생성자를 여러개 오버로딩하여 만들어두고, 객체 생성시 필요한 변수가 포함된 생성자를 통해서 객체를 생성한다.

public class Student {

    private int id;               //필수
    private String name;          //필수
    private String grade;         //필수
    
    private String phoneNumber;   //선택
    private String email;		  //선택

    //필수멤버 생성자
    public Student(int id, String name, String grade) {
        this.id = builder.id;
        this.name = builder.name;
        this.grade = builder.grade;
    }
    
    //필수멤버 + 선택1 생성자
    public Student(int id, String name, String grade, String phnNum) {
        this.id = builder.id;
        this.name = builder.name;
        this.grade = builder.grade;
        this.phoneNumber = builder.phnNum;
    }
    
    //필수멤버 + 선택2 생성자
    public Student(int id, String name, String grade, String phnNm, String email){
        this.id = builder.id;
        this.name = builder.name;
        this.grade = builder.grade;
        this.phoneNumber = builder.phnNm;
        this.email = email;
    }

}
public static void main(String[] args) {
	Student stdnt1 = new Student(1, "kim", 3);
    
    Student stdnt2 = new Student(2, "Han", 2, "010-0000-0000");
    
    Student stdnt3 = new Student(3, "Park", 1, "010-1111-1111", "park@gmail.com");
}

이 방식의 문제점은 매개변수가 많아질수록 커진다. 외부에선 몇번째 파라미터가 어떤 변수를 뜻하는지 전혀 구분할 수 없기 때문이다. 물론 IDE가 친절하게 설명을 해주긴 하지만, 코드 자체로 봤을때 가독성 있는 코드라고 할 수는 없다. 게다가 멤버변수가 많아질수록 만들어야 하는 생성자 수가 증가하기 때문에 유지보수 측면에서도 좋지 않다.

2) 자바 빈 패턴

점층적 생성자 패턴의 단점을 보완하기 위해, setter 메소드를 사용한 자바빈 패턴이 고려되었다.
매개변수가 없는 기본 생성자로 객체를 생성한 후, setter 메소드를 이용해 변수를 세팅해주는 방법이다.

public class Student {

    private int id;               //필수
    private String name;          //필수
    private String grade;         //필수
    
    private String phoneNumber;   //선택
    private String email;		  //선택

    public void setId(int id) {
    	this.id = id;
    }
    public void setName(String name) {
    	this.name = name;
    } 
    public void setGrade(String grade) {
    	this.grade = grade;
    }
    public void setPhoneNumber(String phoneNumber) {
    	this.phoneNumber = phoneNumber;
    }
    public void setEmail(String email) {
    	this.email = email;
    }
    

}
public static void main(String[] args) {
	Student stdnt1 = new Student();
    stdnt1.setId(1);
    stdnt1.setName("kim");
    stdnt1.setGrade(3);
    
    Student stdnt2 = new Student();
    stdnt2.setId(2);
    stdnt2.setName("Han");
    stdnt2.setGrade(2);
    stdnt2.setPhoneNumber("010-0000-0000");
    
    Student stdnt3 = new Student();
    stdnt3.setId(3);
    stdnt3.setName("Park");
    stdnt3.setGrade(1);
    stdnt3.setPhoneNumber("010-1111-1111");
    stdnt3.setEmail("email@gmail.com");
    
}

이 방식은 생성자 오버로딩시 나타났던 가독성의 문제점이 사라졌다. 어떤 변수에 어떤 값이 들어가는지 잘 드러나고, setter 메소드로 유연한 객체 생성이 가능해졌다. 다만 객체 생성 시점에 필수 멤버가 세팅된다는 보장이 없다. 객체가 불안정하게 일관성이 깨진 상태로 사용이 가능해진다는 점이 문제점이 된다. 또한 setter메소드로 인해 객체의 불변성을 보장할 수 없어진다. 물론 setter를 사용한 후 freezing을 사용하여 불변으로 만들 수 있지만, freezing은 권장되지 않는 방식이다.

3) 빌더 패턴

빌더 패턴은 객체 생성시 Builder 클래스를 통해 변수를 하나씩 입력 받은후, build() 메소드로 인스턴스를 최종 생성한다.
Builder 클래스는 생성자가 많은 경우, 혹은 변경 불가능한 불변객체가 필요한 경우에 주로 사용되며, 코드의 가독성과 일관성, 불변성을 유지하는 것에 집중한다.
이펙티브자바에서는 빌더클래스를 static inner class로 구현하도록 한다.
그 이유로는

  • 하나의 빌더 클래스는 하나의 대상 객체만을 위해 사용됨
  • 대상 객체는 오로지 빌더 객체에 의해 초기화됨
  • static을 통해 객체 생성 전 빌더를 사용할 수 있도록 함

등이 있다.

아래 코드를 통해 빌더 패턴을 구현해 보았다.

public class Student {
    //setter 메소드를 사용하지 않기때문에 객체를 freezing 시킬 수 있다. --> final로 선언하여 값을 불변으로 사용가능
    private final int id;               //필수
    private final String name;          //필수
    private final String grade;         //필수
    private final String phoneNumber;   //선택
    private final String email;         //선택
    
    // inner Builder 클래스 생성, static으로 선언해야 객체 생성전에 메소드를 사용할 수 있다.
    public static class Builder {
        private final int id;
        private final String name;
        private final String grade ;
        private String phoneNumber = "";
        private String email = "";

        //필수 멤버를 사용한 빌더
        public Builder(int id, String name, String grade) {
            this.id = id;
            this.name = name;
            this.grade = grade;
        }

        // 선택멤버 set (setter는 void를 리턴하지만, 빌더는 자신을 리턴한다 --> 메소드 체이닝이 가능해짐)
        Builder phoneNumber(String phoneNumber) {
            this.phoneNumber = phoneNumber;
            return this;
        }

        // 선택멤버 set
        Builder email(String email) {
            this.email = email;
            return this;
        }
    }

    //생성자는 외부에 노출되지 않고, 빌더 객체에 의해서만 초기화된다
    private Student(Builder builder) {
        this.name = builder.name;
        this.id = builder.id;
        this.grade = builder.grade;
        this.phoneNumber = builder.phoneNumber;
        this.email = builder.email;
    }

}
public static void main(String[] args) {
	Student stdnt = new Student.Builder(1, "Kim", 3)
    						   .phoneNumber("010-0000-0000")
                               .email("email@gmail.com")
                               .build();
}

빌더패턴의 단점은 코드가 많아지고, 멤버가 중복된다는 점이다.
그래서 실무에서 빌더패턴을 사용할때는 주로 롬복의 @Builder 어노테이션을 사용해서 코드를 간결하게 줄일 수 있다.

@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Student {
    //멤버변수
    private final int id;               //필수
    private final String name;          //필수
    private final String grade;         //필수
    private final String phoneNumber;   //선택
    private final String email;         //선택
}
public static void main(String[] args) {
	Student stdnt = new StudentBuilder()
    				.id(1)
                    .name("Kim")
                    .grade(3)
    				.phoneNumber("010-0000-0000")
                    .email("email@gmail.com")
                    .build();
}

다만 롬복을 사용해서 빌더를 만들면, 객체생성시 어디까지가 필수멤버이고 어디까지가 선택멤버인지 가늠하기 어렵다는 문제점이 있다.
또한 롬복의 빌더는 모든 멤버가 파라미터인 생성자가 자동으로 생성되는데, 이렇게되면 외부에서 빌더를 통하지 않고 객체를 생성할수 있게 된다.
그래서 빌더로만 인스턴스 생성을 허용하고 싶으면
@AllArgsConstructor(access = AccessLevel.PRIVATE)
를 통해 모든 파라미터를 가진 생성자의 access레벨을 private로 바꿔주면 된다.

0개의 댓글