티스토리 뷰

JAVA

[JAVA] 빌더 패턴(Builder pattern)

CharlieZip 2021. 11. 9. 00:13
반응형

빌더 패턴

빌더 패턴은 복잡한 객체를 생성하는 방법을 정의하는 클래스와 표현하는 방법을 정의하는 클래스를 별도로 분리하여, 서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공하는 패턴입니다.

 

빌더 패턴은 생성 패턴으로 인스턴스를 만드는 절차를 추상화하는 패턴입니다. 특히 빌더 패턴은 많은 Optional한 멤버 변수(혹은 파라미터)나 지속성 없는 상태 값들에 대해 처리해야 할 때 큰 장점을 가지고 있습니다.

 

 

빌더 패턴 구현 방법

  1. 빌더 클래스를 Static Nested Class로 생성합니다. 이때, 관례적으로 생성하고자 하는 클래스 이름 뒤에 Builder를 붙입니다.
  2. 빌더 클래스의 생성자는 public으로 하며, 필수 값들에 대해 생성자의 파라미터로 받습니다.
  3. Optional한 값들에 대해서는 각각의 속성마다 메소드로 제공하며, 이때 리턴 값이 빌더 객체 자신이어야 합니다.
  4. 빌더 클래스 내에 build() 메서드를 정의하여 클라이언트 프로그램에게 최종 생성된 결과물을 제공합니다.

 

글로만 보면 이해가 어렵기 때문에 바로 코드를 보도록 하겠습니다.

@Getter
public class Student {

    private Long id;
    private String name;
    private Integer age;

    private Student(StudentBuilder builder) {
        this.id = builder.id;
        this.name = builder.name;
        this.age = builder.age;
    }

    //Nested Builder Class
    public static class StudentBuilder {

        private Long id;
        private String name;
        private Integer age;

        //필수값들은 생성자의 파라미터
        public StudentBuilder(Long id) {
            this.id = id;
        }

        public StudentBuilder setName(String name) {
            this.name = name;
            return this;
        }

        public StudentBuilder setAge(Integer age) {
            this.age = age;
            return this;
        }

        public Student build() {
            return new Student(this);
        }

    }
}

StudentBuilder 클래스를 보면 id값은 필수 값으로 생성자의 파라미터를 통해 받고 나머지 필드값들은 각각의 속성마다 메서드를 생성하였습니다.

 

빌더 패턴을 통해 얻을 수 있는 장점은 다음과 같습니다.

  1. 경우에 따라 필요 없는 파라미터들에 대해 일일이 null 값을 넘겨주지 않아도 됩니다.
  2. 객체를 생성할때 파라미터의 순서를 신경쓰지 않아도 됩니다.

 

추가로 Student 클래스를 보면 public 생성자가 없는것을 볼수있습니다. Student 객체를 생성하기 위해서는 오직 StudentBuilder 클래스를 통해서만 가능합니다.

 

 

Student 객체 생성

// new Student(1L, "학생A", 10);
Student student = new Student.StudentBuilder(1L)
            .setName("학생A")
            .setAge(10)
            .build();
  • StudentBuilder를 통해 Student 객체를 생성할때 Optional한 Name,Age에 대해서는 필요없는 값이면 메서드를 사용하지 않으면 됩니다.
  • Name, Age 메서드의 순서가 바뀌어도 가능합니다.

 

빌더 패턴은 객체를 생성하는데 장점이 있지만 위와같이 추가적인 많은 코드가 필요합니다. 위의 예시는 필드가 3개뿐이지만 만약 필드가 많아지면 그만큼 코드의 양도 증가하게 되고 객체가 student 뿐만아니라 여러개의 객체가 있다고 생각하면 필요한 코드의 양은 생각만 해도 아찔합니다.

 

 

그래서 이걸 해결하기 위해 @Builder라는 Lombok을 사용합니다.

@Getter
@Builder
public class Student {

    private Long id;
    private String name;
    private Integer age;
}

이와같이 @Builder 애노테이션을 사용하면 위의 Nested Builder Class를 생성한것과 똑같습니다.

 

 

Student 객체 생성 - @Builder 사용

Student student = Student.builder()
            .id(1L)
            .name("학생A")
            .age(10)
            .build();

Lombok @Builder는 builder 라는 클래스 명을 사용하며 메서드의 이름기존의 필드명을 그대로 사용합니다.

 

사실 @Builder 애노테이션을 class 레벨에 사용하는 것은 추천드리지 않습니다.

  • @Builder를 Class에 적용시키면 생성자의 접근 레벨이 default이기 때문에, 동일 패키지 내에서 해당 생성자를 호출 할 수 있는 문제
  • 모든 멤버 필드에 대해서 매개변수를 받는 기본 생성자를 만듭니다.
    • 마치 @AllArgsConstructor 와 같은 효과를 발생시킵니다.
    • Student 객체에서 id 값이 데이터베이스 PK 생성전략에 의존하고 있다고 가정한다면 객체를 생성할 때 id값을 넘겨받지 않아야 합니다.
    • Class 레벨에 적용하면 객체생성에 제한을 두기가 어렵습니다.

 

따라서 private 생성자를 구현해서 @Builder를 지정하는 방법을 추천드립니다.

@Getter
public class Student {

    private Long id;

    private String name;

    private Integer age;

    @Builder
    private Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

 

Student 객체 생성

Student student = Student.builder()
            //.id(1L)  error 발생
            .name("학생A")
            .age(10)
            .build();

@Builder 생성자가 name, age 파라미터만 가지고 있으므로 Student 객체를 생성할 때 id값을 넘겨받지 못하게 제한할 수 있습니다.



@Builder 기능 

@Builder 애노테이션은 몇가지 기능을 제공합니다. 기능들에 대해 알아보겠습니다.

 

public enum Gender {
    MAN, FEMALE;
}

Enum 값에대해서도 테스트하기 위해 Gender를 추가하였습니다.



@Builder
@Getter
@ToString
public class Custom {

    private String name;
    private Integer age;
    private LocalDateTime time;
    private Boolean check;
    private List<String> jobs;
    private Gender gender;
}

Custom 객체를 정의해줍니다. 클래스레벨에 @Builder를 선언하고 다양한 타입의 필드를 정의하였습니다.

 

 

Custom 객체 생성

@Test
public void builderTest() {
    Custom custom = Custom.builder()
            .name("Custom")
            .age(20)
            .time(LocalDateTime.now())
            .check(true)
            .jobs(Arrays.asList("직업"))
            .gender(Gender.MAN)
            .build();

    System.out.println("custom = " + custom);
    assertThat(custom.getName()).isEqualTo("Custom");
    assertThat(custom.getAge()).isEqualTo(20);
    assertThat(custom.getCheck()).isEqualTo(true);
}

실행결과
custom = Custom(name=Custom, age=20, time=2021-10-31T23:17:14.533483500, check=true, jobs=[직업], gender=MAN)

 



Builder.Default

빌더는 왜 좋은걸까??

빌더는 개발자가 좀 더 능동적이게 객체를 생성할 수 있게 도와준다.

다음 코드를 보며 좀 더 생각해보자.

@Test
public void builder() {
    Custom custom = Custom.builder()
            .age(20)
            .jobs(Arrays.asList("직업"))
            .gender(Gender.MAN)
            .time(LocalDateTime.now())
            .build();

    System.out.println("custom = " + custom);
    assertThat(custom.getAge()).isEqualTo(20);
}

위의 코드를 보면 처음 Custom 객체를 생성할때와 달리 namecheck 필드를 제외하고 생성을 하였다.

그러면 우리가 추가하지 않은 namecheck필드에는 어떤 값이 들어가게 될까??

 

바로 null값이 들어가게 된다.

 

이게 위의 코드의 실행결과이다.
custom = Custom(name=null, age=20, time=2021-10-31T23:35:04.204326400, check=null, jobs=[직업], gender=MAN)



그러면 다른 필드는 어떻게 되는지 한번 테스트 해보자.

@Test
public void builderNull() {
    Custom custom = Custom.builder()
            .build();

    System.out.println("custom = " + custom);
}

실행결과
custom = Custom(name=null, age=null, time=null, check=null, jobs=null, gender=null)

 

Builder는 값을 설정하지 않으면 자동으로 null을 채워주는것을 확인할 수 있다.

여기서 저는 Integer, Boolean과 같이 Wrapper 타입으로 선언하여 모두 null이 채워졌지만 만약 int, boolean 으로 Primitive 타입으로 선언한다면 int는 0 boolean는 false를 기본값으로 채워줍니다.

 

즉, Builder는 0/null/false 3가지 값중 하나로 값을 채워줍니다.

 

만약 우리가 직접 기본값을 설정해주고 싶다면 Lombok 1.16.16 이후의 버전에 추가된 @Builder.Default를 사용하여 기본값을 정해줄 수 있습니다.

@Builder
@Getter
@ToString
public class Custom {

    private String name;
    @Builder.Default
    private Integer age = 0;

    @Builder.Default
    private LocalDateTime time = LocalDateTime.now();

    @Builder.Default
    private Boolean check = false;
    private List<String> jobs;

    @Builder.Default
    private Gender gender = Gender.MAN;
}

 

 

@Builder.Default를 사용하는 방법은 필드에 어노테이션을 추가한뒤 필드에 기본값을 직접 지정해주면 됩니다.

@Builder.Default
private Integer age = 0;

time, check, gender 필드에도 추가로 기본값을 추가해줬습니다.

 

 

객체 생성 - Default 적용

@Test
public void builderDefault() {
    Custom custom = Custom.builder()
            .name("Custom")
            .jobs(Arrays.asList("직업"))
            .build();

    System.out.println("custom = " + custom);
    assertThat(custom.getName()).isEqualTo("Custom");
    assertThat(custom.getAge()).isEqualTo(0);
    assertThat(custom.getCheck()).isEqualTo(false);
    assertThat(custom.getGender()).isEqualTo(Gender.MAN);
}

실행결과
custom = Custom(name=Custom, age=0, time=2021-10-31T23:59:39.079739800, check=false, jobs=[직업], gender=MAN)

위와같이 @Builder.Default로 설정해준 기본값이 모두 잘 입력되어 있는것을 확인할 수 있습니다.



 

Builder Singular

우리가 생성한 Custom 객체의 job 필드를 보면 컬렉션으로 생성되어 있습니다.
private List<String> jobs;

그리고 .jobs(Arrays.asList("직업1", "직업2")) 라는 방법을 통해 값을 설정할수있었습니다.

 

이것을 @Singular를 사용하면 한번에 하나씩 값 목록을 작성할 수 있습니다.

 

Singular 추가

@Singular
private List<String> jobs;

 

 

객체 생성 - Singular 적용

@Test
public void builderSingular() {
    Custom custom = Custom.builder()
            .name("Custom")
            .job("직업1")
            .job("직업2")
            .build();

    System.out.println("custom = " + custom);
    assertThat(custom.getName()).isEqualTo("Custom");
    assertThat(custom.getJobs().size()).isEqualTo(2);
}

 

실행결과
custom = Custom(name=Custom, age=0, time=2021-11-01T01:04:05.271057900, check=false, jobs=[직업1, 직업2], gender=MAN)

여기서는 @Singular가 java.util.List로 작업을 했지만 Set,Map 자료구조로도 가능합니다.

여기서 주의할점은 @Singular를 사용하면 인수가 단수형식으로 전달된다는 점입니다. 위의경우 jobs이므로 job의 형태로 전달됩니다. 만약 단수형이 애매한경우 직접 이름을 지정해 줄 수도 있습니다. @Singular("job") List<String> jobs 라고 명시적으로 지정해주면 됩니다.



마무리

Builder패턴은 쉽게 객체를 개발자가 편하게 생성할 수 있게 도와주는 패턴입니다. 객체를 생성할때 간단한 경우이면 생성자나 정적 팩토리 메서드를 사용하여도 문제가 없지만 객체가 조금은 복잡해질경우 Builder 패턴을 고려해보면 좋을것 같습니다.

추가로 Default 기능의 경우 null이라는 기본값이외에도 지정된 값을 넘길수 있어 매우 유용하지만 Singular의 경우 실제 객체를 생성할때 컬렉션의 값을 일일이 넣어주는 경우보다는 컬렉션 객체를 넘겨받아 넣어주기 경우가 더 많을거 같고 편리할거 같아 사용하실 때 편의성에 대해 고려해보면 좋을 것 같습니다.

 

 

 

참고자료

반응형

'JAVA' 카테고리의 다른 글

[JAVA] Sort() 정렬하기  (1) 2021.07.13
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함