Computer Science

[CS / DesignPattern] 빌더 패턴 (with Lombok)

마볼링 2023. 7. 27. 17:48

📕 빌더 패턴

📗 빌더 패턴이란?

  • 복잡한 객체들을 단계별로 생성할 수 있도록 하는 GoF 디자인 패턴 중 생성 패턴에 해당하는 패턴이다.
  • 객체를 생성하는 클래스와 표현하는 클래스를 분리하여, 동일한 절차에서도 서로 다른 표현을 생성하는 방법을 제공한다.

 

📗 왜 나왔을 까?

객체를 생성시 생성자(Constructor)만 사용할 때 발생할 수 있는 문제를 개선하기 위해 나왔다.

 

예시로 다음과 같은 Member 클래스가 있다고 하자.

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member extends BaseTimeEntity {

    /**
     * 회원 목록 테이블
     */

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private long id;

    @Column(unique = true, nullable = false, length = 1000)
    private String apiKey;

    @Column(unique = true)
    private String username;

    private String password;

    @Enumerated(EnumType.STRING)
    private Role role;

    @OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
    private List<Character> characters = new ArrayList<>();

    /**
     * 연관 관계 메서드
     */

    // 회원가입 생성자
    public Member(MemberSignupDto signupDto) {
        this.apiKey = signupDto.getApiKey();
        this.username = signupDto.getUsername();
        this.password = signupDto.getPassword();
        this.role = Role.USER;
    }

    // user 엔티티에 character 리스트 저장
    public Character addCharacter(Character character) {
        characters.add(character);
        character.setMember(this);
        return character;
    }

}

 

[회원가입 메소드]

public Member signup(MemberSignupDto signupDto) {
    return memberRepository.save(new Member(signupDto));
}

 

Spring Boot와 Lombok을 쓰고 있어 Getter, Setter등의 코드는 따로 존재하지 않고,

간단한 토이 프로젝트에서는 사용할 수 있지만, 대형 프로젝트에서는 사용하면 여러 이슈가 발생한다.

  • 다른 매개변수를 받게되면 그에 따른 생성자를 만들어야한다.
  • 필요 없는 파라미터들이 생기면 일일이 NULL값을 넘겨줘야한다.

 

또한 만약 여기서 여러 매개변수를 받는 생성자를 그냥 만들면 어떻게 될까?

public Member(long id, String apiKey, String username, String password, Role role, List<Character> characters) {
    this.id = id;
    this.apiKey = apiKey;
    this.username = username;
    this.password = password;
    this.role = role;
    this.characters = characters;
}

이렇게 되면 순서 또한 기억해서 생성자를 만들어야한다.

 

이러한 문제는 위에 보이는 코드 뿐만 아니라

팩토리 메소드 패턴이나 정적 팩토리 메소드 패턴등 다른 디자인 패턴에서도 발생하는 이슈들이다.

 

 

📗 어떻게 할까?

빌더 패턴은 이러한 문제들을 해결하기 위해 별도의 Builder 클래스를 만들어 필수 값에 대해서는 생성자를 통해,
선택적인 값들에 대해서는 메소드를 통해 step-by-step으로 값을 입력받은 후에 build() 메소드를 통해
최종적으로 하나의 인스턴스를 return하는 방식이다.

 

여기서 Spring boot 환경이라면 Lomok 어노테이션으로 쉽게 할 수 있다.

 

아래는 Builer 패턴을 반영한 코드이다

 

[Member]

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Member extends BaseTimeEntity {

    /**
     * 회원 목록 테이블
     */

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private long id;

    @Column(unique = true, nullable = false, length = 1000)
    private String apiKey;

    @Column(unique = true)
    private String username;

    private String password;

    @Enumerated(EnumType.STRING)
    private Role role;

    @OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
    private List<Character> characters = new ArrayList<>();
    
}

 

 

기본적인 @NoArgsConstructor@AllArgsConstructor만 추가하고,

@Builder 어노테이션을 추가한다.

 

보시다시피 코드상에 다른 어떠한 생성자도 존재하지 않는다.

 

[회원가입 메소드]

public Member signup(MemberSignupDto signupDto, List<Character> characterList) {
    Member member = Member.builder()
            .username(signupDto.getUsername())
            .password(passwordEncoder.encode(signupDto.getPassword()))
            .apiKey(signupDto.getApiKey())
            .characters(characterList)
            .role(Role.USER)
            .build();

    return memberRepository.save(member);
}

회원가입 메소드에서 빌더 패턴을 이용하여 필요한 객체를 생성한다.

이렇게 코드를 작성하면 위에서 얘기한 이슈들이 모두 해결된다.