[Java] Builder를 직접 구현해보자!

David Lee·2023년 11월 13일
0
post-thumbnail

포스트를 작성하게된 계기

자바 프로젝트를 진행하면서 Lombok의 @Builder 를 매우 애용하고 있는데, 퍼뜩 Builder 방식을 직접 구현해본다면 좀 더 이해하면서 사용할 수 있지 않을까? 라는 생각에 작성하게 되었다.

TODO

  • Builder 방식을 아무 자료를 참고하지 말고 직접 구현해보자!

Goal

public class Company {
    private final String name;
    private final String address;
    private final String description;

    public Company(String name, String address, String description) {
        this.name = name;
        this.address = address;
        this.description = description;
    }
}

위 클래스에 대한 Builder를 직접 만들어 보자!

구상하기

먼저 Lombok에서 @Builder를 선언한 경우를 생각해보니

Company company = Company.builder()
    .name("이름")
    .address("주소")
    .build();

위와 같이 .builder() static method를 시작으로 .build()로 마무리 된 것을 확인할 수 있었다.
위 코드를 기반으로 구현 방식을 생각해보니 아래 그림처럼 나타낼 수 있었다.

Company Class에서 .builder()를 호출하면 CompanyBuilder를 반환하고 CompanyBuilder의 .name(), .address(), .description()을 통해서 값을 지정한 후 .build()를 통해 Company 객체를 생성 후 반환하는 방식으로 Builder를 구상했다.

구현하기

먼저 CompanyBuilder 클래스를 생성하고 각 메서드를 생성했다.

public class CompanyBuilder {
    private String name;
    private String address;
    private String description;

    protected CompanyBuilder() {}

    public void name(String name) {
        this.name = name;
    }

    public void address(String address) {
        this.address = address;
    }

    public void description(String description) {
        this.description = description;
    }

    public Company build() {
        return new Company(name, address, description);
    }
}

CompanyBuilder class에 대한 직접 생성을 막기 위해 생성자 접근을 protected로 설정하였다.
하지만 위와 같이 코드를 작성하면 .name().address().build() 처럼 메서드가 이어지지 않는다. 그래서 값 지정 메서드에 대해서 객체 자신을 반환하도록 코드를 수정했다.

public class CompanyBuilder {
    private String name;
    private String address;
    private String description;

    protected CompanyBuilder() {}

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

    public CompanyBuilder address(String address) {
        this.address = address;
        return this;
    }

    public CompanyBuilder description(String description) {
        this.description = description;
        return this;
    }

    public Company build() {
        return new Company(name, address, description);
    }
}

위와 같이 코드를 작성하면 .name().address().build()와 같이 메서드를 이어서 활용할 수 있다.
이제 Company로 돌아가서 코드를 마무리해보자

public class Company {
    private final String name;
    private final String address;
    private final String description;

    protected Company(String name, String address, String description) {
        this.name = name;
        this.address = address;
        this.description = description;
    }

    public static CompanyBuilder builder(){
        return new CompanyBuilder();
    }

    public String getName() {
        return name;
    }

    public String getAddress() {
        return address;
    }

    public String getDescription() {
        return description;
    }
}

생성자 생성 방식대신 Builder를 사용할 것이기에 CompanyBuilder class와 마찬가지로 생성자 접근을 protected로 설정하였다.
이제 테스트를 통해서 Builder가 제대로 Company를 생성하는지 테스트 해보자

Test

테스트는 아래와 같이 작성되었다.

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

@DisplayName("Test Company Builder")
class CompanyTest {
    private static final String NAME = "이름";
    private static final String ADDRESS = "주소";
    private static final String DESCRIPTION = "설명";

    @DisplayName("이름, 주소, 설명이 주어졌을때 Company 생성")
    @Test
    void testCreateCompanyWithNameAddressDescription() {
        // Act
        Company actualResult = Company.builder()
                .name(NAME)
                .address(ADDRESS)
                .description(DESCRIPTION)
                .build();
        // Assert
        assertEquals(NAME, actualResult.getName());
        assertEquals(ADDRESS, actualResult.getAddress());
        assertEquals(DESCRIPTION, actualResult.getDescription());
    }

    @DisplayName("이름, 주소가 주어졌을때 Company 생성")
    @Test
    void testCreateCompanyWithNameAddress(){
        // Act
        Company actualResult = Company.builder()
                .name(NAME)
                .address(ADDRESS)
                .build();
        // Assert
        assertEquals(NAME, actualResult.getName());
        assertEquals(ADDRESS, actualResult.getAddress());
        assertNull(actualResult.getDescription());
    }
}

결과

결론

Builder를 직접 구상하고 구현해보니 왜 Lombok의 @Builder를 class에서 사용할때 @AllArgsConstructor를 요구하는지에 대해서 이해할 수 있었다.
포스트에서 구현하는 방식은 생성자의 모든 args를 필요로 했지만 만약 @Builder를 생성자에 사용하는 것처럼 Builder에서 사용하고 싶은 args를 제한하고자 하는 경우에는 Builder Class의 값들을 제한하면 될 것이다.
어노테이션에 대해서는 공부를 더 진행해서 Lombok처럼 @Builder 어노테이션을 직접 만들어 보는것도 앞으로 이해하는데 더 도움이 될 것같다!

profile
쌓아가기

0개의 댓글

관련 채용 정보