본문 바로가기
공부/Spring

[Spring] 빌더 패턴에 대한 이해와 사용법

by 웅대 2023. 5. 12.
728x90
반응형
member = new Member(1L, "chulsoo", "seoul");

어떤 클래스에 대한 인스턴스를 생성할 때 종종 빌더(builder) 패턴을 사용하곤 했다.

 

지금까지는 단순히 setter로 필드 값을 세팅하는 것보다 좋다고만 알고 있었는데 한 번쯤 제대로 짚고가면 좋을 것 같아서 공부한 내용을 정리해보려고 한다.

 

우선 다음과 같은 클래스가 있다고 해보자.

public class Member {
    private Long id;
    private String name;
    private String address;
}

이 클래스의 인스턴스를 생성할 때 필드 값을 정의하는 방법 중에 생성자를 사용한 방법이 있을 것이다.

 

<생성자>

public class Member {
    private Long id;
    private String name;
    private String address;

    public Member(Long id, String name, String address) {
        this.id=id;
        this.name = name;
        this.address = address;
    }
}

<필드 값 세팅>

Member member = new Member(1L, "chulsoo", "seoul");

지금은 필드 값의 종류가 적기 때문에 큰 문제가 없다.

 

그런데 만약 필드 값의 종류가 매우 많다면 어떨까? 

Member member = new Member(1L, "chulsoo", "seoul", "...", "...", ......)

생성자 방식은 필드 값을 세팅할 때 순서가 정해져있기 때문에 필드 값의 종류가 많으면 헷갈리기 시작할 것이다.

 

이를 방지하려면 어떤 방법이 있을까? 우선 setter 방식을 사용하면 이러한 문제를 어느정도 해결할 수 있다.

<setter>

public class Member {
    private Long id;
    private String name;

    private String address;

    public void setId(Long id) {
        this.id=id;
    }
    public void setName(String name) {
        this.name=name;
    }
    public void setAddress(String address) {
        this.address=address;
    }
}

<필드 값 세팅>

Member member = new Member();
member.setId(1L);
member.setName("chulsoo");
member.setAddress("seoul");

setName과 같이 어떤 필드를 세팅하는지 쉽게 알 수 있게 되었다.

 

그런데 웬만해서는 setter 방식을 사용하지 않는 편이 좋다.

 

setter를 사용하게 되면 필드 값에 대해서 불변성이 보장되지 않기 때문에 특별한 이유가 없는 이상 사용을 자제하는 편이 좋다.

 

이제 빌더 패턴을 사용해보자.

 

<Builder>

public class Member {
    private Long id;
    private String name;
    private String address;

    public Member(Builder builder) {
        this.id=builder.id;
        this.name=builder.name;
        this.address=builder.address;

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

    public static class Builder{
        private Long id;
        private String name;
        private String address;

        public Builder id(Long id) {
            this.id=id;
            return this;
        }
        public Builder name(String name) {
            this.name=name;
            return this;
        }
        public Builder address(String address) {
            this.address=address;
            return this;
        }

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

    }

}

사용법을 먼저 보고 코드를 이해해보면 더욱 쉬울 것이다.

 

Member member = Member.builder()
      .id(1L)
      .name("chulsoo")
      .address("seoul").build();

보다싶이 필드의 순서에 상관없이 원하는 값을 세팅할 수 있다.

 

코드를 단계별로 살펴보자.

 

1.

public class Member {
    private Long id;
    private String name;
    private String address;

    public static class Builder{
        private Long id;
        private String name;
        private String address;
    }
}

 

먼저 Member 클래스 내부에 static class은 Builder 클래스가 들어있다.

 

이 Builder 클래스는 Member 클래스의 필드 값을 가지고 있다.

 

Builder가 모든 필드 값을 가지고 있을 필요는 없다. 

 

만약 jpa를 적용한다면 id 값을 우리가 세팅할 일은 없을 것이다.

 

그런 경우 builder가 필드 id를 가지고 있지 않으면 된다.

 

지금은 테스트 환경이므로 id를 직접 세팅하도록 하였다.

 

2.

public class Member {
    private Long id;
    private String name;
    private String address;
    public static class Builder{
        private Long id;
        private String name;
        private String address;

        public Builder id(Long id) { //필드 id 세팅
            this.id=id;
            return this;
        }
        public Builder name(String name) { //필드 name 세팅
            this.name=name;
            return this;
        }
        public Builder address(String address) { //필드 address 세팅
            this.address=address;
            return this;
        }

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

    }

}

이제 빌더의 메소드를 정의한다. 이 메소드들은 Builder의 필드 값을 세팅하고 Builder를 반환하는 메소드이다.

 

필드 name과 address를 세팅하고 싶다면 name, address 메소드를 통해 세팅하면 된다.

 

원하는 메소드를 호출하고 나면 그에 해당하는 필드 값들이 Builder에 세팅되어 있을 것이다.

 

이제 이 Builder를 Member로 변환해야 한다.

 

이를 위해 Member의 생성자 및 build 메소드를 구현한다.

 

public class Member {
    private Long id;
    private String name;
    private String address;

    public Member(Builder builder) { //생성자. Member로 변환하는 build 메소드에서 사용
        this.id=builder.id;
        this.name=builder.name;
        this.address=builder.address;

    }
    public static Builder builder() { //외부에서 Builder를 생성
        return new Builder();
    }

    public static class Builder{
        private Long id;
        private String name;
        private String address;

        public Builder id(Long id) {
            this.id=id;
            return this;
        }
        public Builder name(String name) {
            this.name=name;
            return this;
        }
        public Builder address(String address) {
            this.address=address;
            return this;
        }

        public Member build() { //Member로 변환 
            return new Member(this);
        }

    }

}

이렇게 빌더 패턴이 무엇인지 알아보았다.

 

빌더 패턴을 사용하면 원하는 필드 값을 세팅할 수 있기 때문에 가독성이 좋아진다.

 

또한 setter 방식에 비해서 필드 값 변경의 위험을 덜 수 있다.

 

이제는 빌더 패턴의 구체적인 사용법을 알아보려고 한다.

 

Builder 패턴 사용법

 

그런데 Builder 패턴을 사용할 때마다 긴 코드를 작성하면 오히려 가독성이 떨어질 수 있다.

 

다행히 Lombok에서 Builder 어노테이션을 제공해준다.

 

이를 사용하면 코드의 양을 줄일 수 있다.

 

위에서 사용했던 Member 클래스를 예시로 들 예정인데 id 값은 자동으로 생성되기 때문에 우리가 세팅할 필요는 없다고 가정하자. (이번 포스팅에서는 id 값 자동 생성 코드는 편의상 작성하지 않았다.)

 

우선 Builder 어노테이션은 생성자에 적용한다.

 

클래스에 적용을 해도 되지만 모든 필드 값에 적용되기 때문에 만약 id와 같이 우리가 세팅하는 값이 아닌 필드가 있을 수 있기 때문에 생성자에 적용한다.

 

public class Member {
    private Long id;
    private String name;
    private String address;
    @Builder
    public Member(String name, String address) {
        this.name=name;
        this.address=address;
    }
}

 

name과 address는 필수 값이라고 한다면 이는 생성할 때 유효성 검증을 진행하는 편이 좋다.

 

Assert를 사용하면 좋다.

public class Member {
    private Long id;
    private String name;
    private String address;
    @Builder
    public Member(String name, String address) {
        Assert.hasText(name, "name은 필수");
        Assert.hasText(address, "address은 필수");
        this.name=name;
        this.address=address;
    }
}

hasText, notNull 등등 여러 메소드가 있으므로 적절한 것을  사용한다.

 

hasText를 적용했는데 만약 name과 address가 빈 값이거나 null이면 IllegalArgumentException 오류가 발생한다.

 

 

728x90
반응형

댓글