[Java] 빌더 패턴 (Builder Pattern)

진예·2024년 2월 11일
0

JAVA

목록 보기
10/10
post-thumbnail
post-custom-banner

💡 Builder Pattern

객체의 생성과 표현을 분리하여 다양한 구성의 인스턴스를 만드는 생성 패턴


📒 점층적 생성자 패턴

필수 입력 필드 + 선택 입력 필드를 하나씩 늘려가며 생성자를 오버로딩하는 방식

public class Member {

    // 필수 입력 정보
    private String name;
    private int age;
    
    // 선택 입력 정보
    private String phoneNumber;
    private String email;
}

클래스를 생성하다 보면 위 경우처럼 값이 무조건 있어야 하는 필수 입력 필드와, 값이 있어도, 없어도 되는 선택 입력 필드가 존재하게 된다.

public class Member {
    ...

    public Member() {}

    // 필수 입력 정보 생성자
    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // 필수 입력 정보 + 전화번호 생성자
    public Member(String name, int age, String phoneNumber) {
        this.name = name;
        this.age = age;
        this.phoneNumber = phoneNumber;
    }
    
    // 필수 입력 정보 + 이메일 + 전화번호 생성자
    public Member(String name, int age, String phoneNumber, String email) {
        this.name = name;
        this.age = age;
        this.phoneNumber = phoneNumber;
        this.email = email;
    }
}

생성자를 통해 값을 입력받는 경우, 필수 정보를 입력받는 생성자, 필수 입력 정보 + 선택 정보 1을 입력받는 생성자, 필수 입력 정보 + 선택 정보 2를 입력받는 생성자, ... 와 같이 경우에 따른 생성자별도로 생성해야 한다. 이는 한 클래스 내의 필드 수가 증가할수록 생성자의 수 또한 증가하여 가독성이 떨어지게 된다.

Member member1 = new Member("member1", 10, "010-1234-5678", "member1@naver.com");
Member member2 = new Member("member2", 20, "member2@naver.com", "010-9876-5432");

또한, 각 생성자의 매개변수 순서를 기억해야 한다. 타입이 같은 필드가 여러 개인 경우 순서가 바뀌어도 타입만 일치하면 컴파일 에러가 발생하지 않고 정상적으로 실행되기 때문에 이후 해당 데이터를 사용하는 로직 처리 시 문제가 될 수 있다.


📒 자바 빈즈 패턴

기본 생성자 + setter()를 통한 값 입력

public class Member {
    // 필수 정보
    private String name;
    private int age;

    // 선택 정보
    private String phoneNumber;
    private String email;

    public Member() {}

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

    public void setAge(int age) {
        this.age = age;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

점층적 생성자 패턴의 단점을 해결하기 위해 자바 빈즈 패턴을 활용할 수 있다. 자바 빈즈 패턴은 생성자를 일일이 만드는 대신 기본 생성자 하나만 만들고, 각 필드에 대한 setter() 메서드를 열어두어 값을 입력받는 방식이다.

Member member = new Member();
        
member.setName("member");
member.setAge(10);
member.setEmail("member@naver.com");
member.setPhoneNumber("010-1234-5678");

자바 빈즈 패턴을 사용하면 필드의 입력 순서를 기억할 필요 없이, setXXX()를 통해 각 필드에 맞는 값을 입력할 수 있다. 하지만, 이 방식 또한 setter()를 모두 정의해야 하기 때문에 가독성이 좋은 편은 아니며 (해당 문제는 Lombok의 @Setter로 해결 가능), 필수 입력 필드에 대한 값 입력을 필수로 두지 않기 때문에, 누락할 가능성이 존재한다.

또한, setter()의 가장 큰 문제는 해당 메서드를 통해 어디서든 객체의 데이터를 변경할 수 있다는 것이다. 여러 메서드에서 특정 인스턴스를 호출하여 setter()로 값을 변경하다 보면, 이후 변경에 대한 추적이 힘들어 유지보수성이 떨어지게 된다. 따라서, setter()를 무작위로 열어두는 것은 위험을 초래할 수 있다.


📒 빌더 패턴

별도의 Builer 클래스의 메서드 통해 값을 입력받은 후, build()를 통해 인스턴스 생성


📝 Builder 클래스 구현

  1. 기준 클래스같은 필드를 가지는 Builder 클래스 구현
  2. 값을 입력받기 위한 생성자 + 메서드 생성 → Builder 객체를 리턴하여 체이닝 기법 적용
  3. 최종 인스턴스를 반환하는 build() 메서드 생성
public class Member {
    // 필수 정보
    private String name;
    private int age;

    // 선택 정보
    private String phoneNumber;
    private String email;

	// 빌더 클래스
    static class MemberBuilder {
        private String name;
        private int age;
        private String phoneNumber;
        private String email;

		// 필수 입력 필드 : 생성자
        public MemberBuilder(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public MemberBuilder setPhoneNumber(String phoneNumber) {
            this.phoneNumber = phoneNumber;
            return this;
        }

        public MemberBuilder setEmail(String email) {
            this.email = email;
            return this;
        }
        
        // 최종 인스턴스 생성
        public Member build() {
            Member member = new Member();
            
            member.name = name;
            member.age = age;
            member.email = email;
            member.phoneNumber = phoneNumber;
            
            return member;
        }
    }
}
Member member = new Member
                        .MemberBuilder("member", 10)
                        .setPhoneNumber("010-1234-5678")
                        .build();

생성자를 통해 필수 입력 필드nameage의 값을 입력받고, 선택 입력 필드phoneNumberemail메서드를 통해 선택적으로 값을 입력할 수 있다. 최종적으로 build() 메서드를 호출해야 입력값이 적용된 Member 인스턴스가 정상적으로 반환된다.


📝 @Builder

Lombok에서 제공하는 빌더 클래스 생성 어노테이션 : 클래스생성자 단위에 적용 가능

✔️ 클래스

  • 클래스 단위에 적용 시, 모든 필드를 포함하는 빌더 클래스 생성
@Builder
public class Member {
    // 필수 정보
    private String name;
    private int age;

    // 선택 정보
    private String phoneNumber;
    private String email;
}

Member member = Member.builder()
				.name("member")
				.age(10)
				.email("member@naver.com")
				.phoneNumber("010-1234-5678")
				.build();

클래스 단위에 @Builder@NoArgsConstructor함께 사용하면 에러 발생 : @Builder생성자가 없는 경우에만 생성자를 자동으로 생성한다. 따라서, @NoArgsConstructor에 의해 기본 생성자가 생성되거나, 클래스 내에 모든 필드를 가지는 생성자가 아닌 다른 생성자가 있는 경우 모든 필드를 가지는 생성자수동으로 생성해주어야 한다. (@AllArgsConstructors 추가)

✔️ 생성자

  • 생성자 단위에 적용 시, 해당 생성자의 매개변수에 포함된 필드만을 사용하여 빌더 클래스 생성
public class Member {
    // 필수 정보
    private String name;
    private int age;

    // 선택 정보
    private String phoneNumber;
    private String email;

    @Builder
    public Member(String name, int age, String phoneNumber) {
        this.name = name;
        this.age = age;
        this.phoneNumber = phoneNumber;
    }
}

Member member = Member.builder()
				.name("member")
				.age(10)
				//.email("member@naver.com") // 에러 발생 : 생성자에 미포함된 필드
				.phoneNumber("010-1234-5678")
				.build();

🙇🏻‍♀️ 참고 1 : [JAVA] 빌더패턴 (Builder Pattern) , @Builder
🙇🏻‍♀️ 참고 2 : Builder 패턴, @NoArgsConstructor, @AllArgsConstructor

profile
백엔드 개발자👩🏻‍💻가 되고 싶다
post-custom-banner

0개의 댓글