점층적 생성자 패턴?

개발을 하다 보면 특정 클래스의 상태, 선택적인 매개변수가 많아질수록 점층적인 모양의 생성자들이 만들어지게 되는 경우가 있습니다. 그렇기에 코드에서 필수적인 매개변수 생성자부터 선택적인 변수 1.... N개 생성자까지 5개 이상의 생성자를 보게 될 수도 있습니다.

public Sample(Long id, String name) {
//...
}

public Sample(Long id, String name, Int age) {
//...
}

public Sample(Long id, String name, Int age, String profile) {
//...
}

// ~~~

public Sample(Long id, String name, Int age, String profile, String email, LocalDateTime createBy, LocalDateTime updateBy) {
//...
}

이러한 클래스를 비즈니스 로직, 전달되는 값에 따라 상황에 맞게 생성하기 위해 생성자 중 하나를 선택하게 되거나 의미를 위해 Item1에서 설명된 정적 팩토리 메서드를 사용할 수도 있습니다.

 

개인적으로 다양한 생성자와 (개발자를 위해) 의미를 부여하기 위한 정적 팩토리 메서드가 클래스에 많이 존재하는 것도 코드를 보는 입장에서는 보기 좋지는 않다고 생각합니다.

 

 

자바 빈즈 패턴?

이러한 문제의 대안으로 자바 빈즈 패턴이 존재합니다. 해당 방식은 No-arg 생성자를 통해 인스턴스를 생성하고 각각의 필드를 Setter를 이용해 값을 저장하는 것인데, 이 패턴도 코드를 작업하다 보면 필드를 누락할 수 있고, 그로 인해 발생하는 예외 지점을 찾는 것은 생각보다 쉽지 않습니다. 세터 자체가 어떠한 의미를 지니지도 않기 때문에 별로 좋아하지 않습니다.

 

추가적으로 해당 객체를 불변으로 만들 수 없고. 상황에 따라 스레드 안정성도 고려하여야 합니다.

Sample sample = new Sample();
sample.setId(1L);
sample.setName("Lob");
sample.setProfile("Serrl");
sample.setEmail("Example@hello.world");
sample.setCreateBy(LocaldateTime.now());
sample.setUpdateBy(LocaldateTime.now());

 

빌더 패턴?

이러한 상황에서 점층적인 생성자 패턴의 일관성, 안정성과 가독성을 챙기는 대안이 나오게 되는데 그것이 바로 빌더 패턴입니다.

public static class Builder {

    private Long requestId;
    private String requestCode;
    private Long userId;
    private String hrOrgan;
    private String username;
    private String password;
        private LocalDateTime createDate;

    public Builder requestId(Long val){
        requestId = val;
        return this;
    }

    public Builder requestCode(String val){
        requestCode = val;
        return this;
    }

    public Builder userId(Long val){
        userId = val;
        return this;
    }

    public Builder createDate(LocalDateTime val){
        LocalDateTime = val;
        return this;
    }

    public Builder hrOrgan(String val){
        hrOrgan = val;
        return this;
    }

    public Builder username(String val){
        username = val;
        return this;
    }

    public Builder password(String val){
        password = val;
        return this;
    }

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

protected HostRequest() {
}

public HostRequest(Builder builder){
    requestId   = builder.requestId;
    requestCode = builder.requestCode;
    userId      = builder.userId;
    createDate  = builder.createDate;
    hrOrgan     = builder.hrOrgan;
    username    = builder.username;
    password    = builder.password;
}

//....................

HostRequest request = new HostRequest.Builder()
        .requestId(1L)
        .requestCode("code")
        .userId(1L)
        .hrOrgan("platform service")
        .username("Lob")
        .password("examplePassword")
        .createDate(LocalDateTime.now())
        .build();

해당 예시의 경우 클래스 선언부에 빌더 코드가 많이 생성되지만, 사용하는 지점에서는 메서드의 이름으로 가독성이 향상되고 메서드 체이닝을 통해 인스턴스 자체가 원자적으로 생성되는 것처럼 보이는 효과를 지닙니다.

 

빌더 패턴을 사용하면서 클래스 래밸에서는 이러한 코드를 감추기 위해 Lombok의 Builder를 사용합니다.

해당 기능의 경우, Type래밸에서 Lombok의 No-Arg, Required 생성자와 같이 사용할 때 이슈가 합니다. 그렇기에 아래와 같이 생성자를 정의하고 해당 어노테이션을 사용하는 것을 선호합니다.

@Builder
public HostRequest(Builder builder){
    requestId   = builder.requestId;
    requestCode = builder.requestCode;
    userId      = builder.userId;
    createDate  = builder.createDate;
    hrOrgan     = builder.hrOrgan;
    username    = builder.username;
    password    = builder.password;
}

추가적으로, 계층적인 상속 구조에서 제네릭 빌더를 통해 하위 타입 객체에도 빌더를 상속하는 등 유연성을 지닐 수 있다고 합니다. 개인적으로 계층 상속 구조를 자주 사용하지는 않기 때문에.. 해당 부분은 넘어가도록 하겠습니다.

 

 

참고 자료

  • Effective Java 3판

+ Recent posts