Language/Java

자바(JAVA) 빌더(builder) 패턴

potatoCompletion 2023. 1. 5. 21:12
목차

1. 빌더(builder) 패턴이란?


2. 구현 코드 및 사용 예시


3. 정리 및 결론

 

 

1. 빌더(builder) 패턴이란?

 

- 빌더 패턴이란, 디자인 패턴 중 생성 패턴의 한 종류로 다양한 구성의 인스턴스를 만드는데 유용하다.

예를 들어, 아래와 같은 class가 있다고 가정해보자.

 

@AllArgsConstructor 
public class User { 
    private String name;
    private int age;
    private String address;
    
}

 

이 때, 만약 요구사항이 바뀌어 "address 없이 User객체 만들 수 있게 해주세요!"

라는 요청이 들어오게 된다면? 아.. 귀찮아

 

@AllArgsConstructor 
public class User { 
    private String name;
    private int age;
    private String address;
    
    public User(String name, int age) {
    	this.name = name;
        this.age = age;
    }
    
}

 

결국 직접 원본 클래스에 address를 뺀 생성자를 만들어 줄 수 밖에 없다.

그런데 또 "어.. 죄송한데 age도 없이 만들 수 있게 해주세요 ㅎㅎ"

아..

 

이럴 때 미리 빌더패턴을 이용해 구현해 놓았다면?

 

User user1 = User.builder()
            .name("김씨")
            .age("21")
                .build()
                
                
User user2 = User.builder()
            .name("이씨")
                .build()

 

원본 클래스의 수정없이 간단하게 요구사항에 대응할 수 있다.

또한 코드의 가독성 또한 훨씬 뛰어나다.

 

 

2. 구현 코드 및 사용 예시

 

- Member.java(lombok 미사용/직접구현)

public class Member {

    private String name;
    private int age;
    private String address;
    private String phone;

    private Member(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.address = builder.address;
        this.phone = builder.phone;
    }

    // 빌더 호출, 외부에서 Member.builder() 으로 접근할 수 있도록 static 메소드로 생성
    public static Builder builder() {
        return new Builder();
    }

    // static 형태의 inner class 생성
    public static class Builder {
        private String name;
        private int age;
        private String address;
        private String phone;

        private Builder() {};

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

        public Builder age(int age) {
            this.age = age;
            return this;
        }

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

        public Builder phone(String phone) {
            this.phone = phone;
            return this;
        }

        // 마지막에 build 메소드를 실행하면 this가 return 되도록 구현
        public Member build() {
            return new Member(this);
        }
    }
}

출처 : https://wildeveloperetrain.tistory.com/30

 

빌더를 별다른 어노테이션 없이 순수하게 구현한 코드이다. innerClass를 만들어 사용한다.

하지만 코드가 길고 구현이 번거로워 보통 이렇게 직접 구현해 사용하진 않고, 아래의 코드처럼 사용한다.

 

-Member.java(lombok 사용)

@Builder
@AllArgsConstructor
public class Member {

    private String name;
    private int age;
    private String address;
    private String phone;
}

롬복을 사용해 간결하게 빌더 패턴을 이용할 수 있다.

 

그런데 만약 필수 파라미터가 존재하는 경우라면?

 

- Member.java

@NoArgsConstructor
@Entity
@Table(name="memberinfo_tb")
public class Member implements UserDetails {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "user_id", nullable = false)
    private String userId;

    @Column(name = "user_password", nullable = false)
    private String userPassword;

    @Column(name = "user_ip")
    private String userIp;

    @Column(name = "last_login")
    private String lastLogin;

    @Column(name = "create_date", nullable = false)
    private String createDate;  // create_date 컬럼은 자동생성
    
    private String roles;

    // 필수 파라미터 설정 && 빌더 패턴 사용
    @Builder
    public Member(String userId, String userPassword, String roles) throws IllegalArgumentException {

        // 안전한 객체 생성을 위한 검증 (빈 값이 들어올 시 에러내기 위하여)
        Assert.hasText(userId, "userId mut not be empty!");
        Assert.hasText(userPassword, "userPassword mut not be empty!");
        Assert.hasText(roles, "roles mut not be empty!");

        // Enum Roles 에 정의된 값만 가짐
        String role = Roles.valueOf(roles).toString();

        // 현재 일자를 형식에 맟춰 생성일자로 삽입
        SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date time = new Date();
        this.createDate = dateTimeFormat.format(time);

        this.userId = userId;
        this.userPassword = userPassword;
        this.roles = role;
    }

 

위 코드는 @Builder 어노테이션을 생성자 위에 붙여서 사용하였는데, 이는 필수 파라미터를 정의하기 위해서이고,

만약 필수 파라미터가 필요없는 경우라면 클래스 위에 @AllArgsConstructor와 함께 @Builder를 사용해 구현 가능하다.

 

 

- MemberTest.java(테스트코드)

public class MemberTest {

    @Test
    public void 아이디없이멤버생성() {
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            Member.builder()
                    .userId("")
                    .userPassword("password")
                    .roles(Roles.USER)
                    .build();
        });
    }  // IllegalArgumentException이 발생해야 테스트 통과

    @Test
    public void 패스워드없이멤버생성() {
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            Member.builder()
                    .userId("kiki")
                    .userPassword("")
                    .roles(Roles.USER)
                    .build();
        });
    }  // IllegalArgumentException이 발생해야 테스트 통과

    @Test
    public void 정상적으로멤버생성() {
        Member member = Member.builder()
                .userId("kiki")
                .userPassword("password")
                .roles(Roles.USER)
                .build();

        Assertions.assertEquals("kiki", member.getUserId());
        Assertions.assertEquals("password", member.getPassword());
        Assertions.assertEquals("USER", member.getRoles().toString());
    }
}

 

 

3. 정리 및 결론

 

나는 클래스를 생성할 때 Builder 패턴을 꼭 사용하는 편이다.

가독성, 유연성이 굉장히 좋아지고, lombok을 사용하지 못하는 특수한 경우가 아닌 이상 구현도 간단하기 때문이다.

다만, 필수 파라미터가 존재하는 클래스에서 사용할 경우 좀 더 주의가 필요하다.