Computer Science

[객체 생성 패턴] 점층적 생성자 패턴 → 자바빈 패턴 → Builder 패턴, lombok @Builder 사용방법 및 주의점

연_우리 2022. 1. 29. 21:26
반응형

 

 

객체를 생성하는 방법에는 3가지 패턴이 있다.

 

점층적 생성자 패턴 : 파라미터 별로 생성자를 만든다

public class Member {
    private Long id;
    private String name;
    private int age;
    private Team team;

    public Member(Long id) {
        this.id = id;
    }

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

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

    public Member(Long id, String name, int age, Team team) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.team = team;
    }
}
Member member1 = new Member("1");
Member member2 = new Member("2", "AA");
Member member3 = new Member("3", "AA", 20);
Member member4 = new Member("4", "AA", 30, teamA);

단점 

  : 객체를 생성할 때, 파라미터 순서가 중요하기때문에 순서를 알고있어야 값을 넣을 수 있다.

  : member가 생성되는건 알겠지만 어떤 필드에 어떤 값이 들어가는지는 코드를 보고 알 수 없다. (Member클래스를 봐야지만 이해할 수 있다)

  : 파라미터의 조합 만큼 생성자를 정의해야한다

 

 

자바빈 패턴 : 빈 생성자( )와 Getter & Setter를 생성한다

public class Member {
    private Long id;		//선택
    private String name;	//필수
    private int age;		//필수
    private Team team;		//필수

    public Member() {
    }

    public Long getId() {  return id;  }
    public void setId(Long id) {  this.id = id;  }

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

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

    public Team getTeam() {   return team;   }
    public void setTeam(Team team) {   this.team = team;   }
}

 

Member member1 = new Member();
member1.setTeam(teamA);
member1.setName("AA");
member1.setAge(20);

Member member2 = new Member();
member2.setTeam(teamB);
member2.setAge(30);

개선

  : 객체 생성 시 어떤 필드에 어떤 값을 넣는지 명시되어 코드 이해가 빠르다

  : 파라미터 순서에 상관없이 값을 넣을 수 있다

 

단점

  : 빈 생성자( )만 만들고 setter를 호출하지 않아도 컴파일러에서 오류로 잡히지 않는다!!

  : Member가 값이 모두 필요한 객체라면.. 갑자기 어디선가NullPointerException이 발생될 수 있다

  : setter가 호출되기 전까지는 객체의 일관성이 깨져버린다.

  : 필수로 들어가야하는 필드를 알고있어야 setter로 값을 넣어줄 수 있다. 

 

 

Builder 빌더 패턴 - 1 : 필수 필드를 생성자로

점층적 생성자 패턴의 장점은 안정성, 자바빈 패턴의 장점은 가독성이였다.

필수 매개변수는 생성자로 안정성을 높이고, 선택 매개변수는 setter로 설정하여 가독성을 높인 패턴이 빌더패턴이다.

public class Member {    
    private Long id;		//선택
    private String name;	//필수
    private int age;		//필수
    private Team team;		//필수
    
    //private Member(Builder builder) {
    //    this.id = builder.id;
    //    this.name = builder.name;
    //    this.age = builder.age;
    //    this.team = builder.team;
    //}
    
    public static class Builder{
        private Long id;		//선택
        private String name;		//필수
        private int age;		//필수
        private Team team;		//필수
        
        //필수 필드 생성자로 생성
        public Builder(String name, int age, Team team) {
            this.name = name;
            this.age = age;
            this.team = team;
        }
        
        //선택 필드 setter 생성 후 void를 Builder로 변경
        //return this; 추가
        public Builder setId(Long id) {
            this.id = id;
            return this;		//체이닝 가능해짐
        }
        
        public Member build(){
            Member member = new Member();
            member.id = this.id;
            member.name = this.name;
            member.age = this.age;
            member.team = this.team;
            return member;
            
            //return new Member(this);
            //이렇게 넘긴 후 Member에 주석으로 된 private 생성자를 만드는 방법도 있다
        }
    }
}
Member member1 = new Member.Builder("hello", 20, teamA).build();

Member member2 = new Member.Builder("world", 10, teamA).setId(2L).build();

개선

  : 필수 필드의 경우엔 값을 보장받을 수 있다. 필수 필드에 값이 없다면 컴파일러에서 오류로 잡힌다

  : 선택 필드의 경우엔 순서에 상관없이 값을 넣을 수 있다

 

단점

  : Member처럼 필수 필드가 많은 경우엔 생성자를 사용하니 가독성이 떨어진다 ( = 선택 필드가 많을 때 사용하면 유용하다!)

 

 

 

Builder 빌더 패턴 - 2 : build() 시 필수 필드가 없다면 예외처리

public class Member {    
    private Long id;		//선택
    private String name;	//필수
    private int age;		//필수
    private Team team;		//필수

    
    public static class Builder{
        private Long id;		//선택
        private String name;		//필수
        //private int age;		//필수	int는 값이 안들어와도 기본값이 0이다. null 체크를 위해 Integer를 사용하자
        private Integer age;	//필수
        private Team team;		//필수
        
        //필수 필드 생성자로 생성
        public Builder(String name, Integer age, Team team) {
            this.name = name;
            this.age = age;
            this.team = team;
        }
        
        //선택 필드 setter 생성 후 void를 Builder로 변경
        //return this; 추가
        public Builder setId(Long id) {
            this.id = id;
            return this;		//체이닝 가능해짐
        }
        
        public Member build(){
            //필수 필드에 값이 없다면 예외발생
            if(name == null || age == null || team == null){
                throw new IllegalArgumentException("Member의 필수값이 할당되지 않았습니다.");
            }
            
            //Assert를 이용하는 방법도 있다
            //Assert.notNull(name, "Member의 name이 null입니다.");
            //Assert.notNull(age, "Member의 age가 null입니다.");
            //Assert.notNull(team, "Member의 team이 null입니다.");
        
            Member member = new Member();
            member.id = this.id;
            member.name = this.name;
            member.age = this.age;
            member.team = this.team;
            return member;
        }
    }
}
Member member1 = new Member.Builder()
                            .setAge(20)
                            .setName("hello")
                            .setTeam(teamA)
                            .build();

Member member2 = new Member.Builder()
                            .setName("world")
                            .setAge(15)
                            .setTeam(teamB)
                            .setId(3L)
                            .build();

개선

  : 필수 필드도 순서에 상관없이 값을 넣을 수 있다.

  : 예외처리로 값을 보장받을 수 있다. 필수 필드에 값이 없다면 컴파일러에서 오류로 잡힌다

 

단점

  : 프로그램을 실행해봐야만 필수 필드에 값이 있는지 확인할 수 있다 (없다면 예외가 발생될것이니..)

 

 

 

 

Lombok의 @Builder 어노테이션 : Builder를 편하게 써보자

해당 어노테이션을 사용하면 바이트코드를 조작해서 Builder패턴을 알아서 만들어주는데,

클래스에 붙일때와 생성자에 붙일때의 동작방식이 다르다.

 

 

@Builder를 클래스에 붙인 경우

@Builder를 클래스에 붙이면 모든 필드의 생성자가 추가된다.

 

Member member1 = Member.builder()
                        .age(20)
                        .name("hello")
                        .team(teamA)
                        .build();
//주의! new 키워드가 없다

=> @Builder를 클래스에 붙이면 사실 setter를 사용하는 것과 다르지 않다

=> 객체를 생성할때 연관으로 뜨는 메서드들중에 어떤 것이 필수인지 모르게된다

 

 

@Builder를 적용하는 클래스에 커스텀하게 정의한 생성자가 있다면  @AllArgsConstructor를 사용해야한다

@Builder
@AllArgsConstructor 	//커스텀하게 정의한 생성자가 있다면 @AllArgsConstructor를 붙여주어야 
			//@Builder와 함께 사용가능
public class Member {
    private Long id;
    private String name;
    private int age;
    private Team team;
    
    public Member() {
    }

    public Member(String name, int age, Team team) {
        this.name = name;
        this.age = age;
        this.team = team;
    }
}

 

만약 내가 정의한 생성자가 있는 경우, @AllArgsConstructor를 붙여주어야 에러가 발생하지 않는다.

 

 

 

@Builder를 생성자에 붙인 경우

@Builder를 생성자에 붙이면 지정된 생성자대로 Builder패턴이 만들어진다.

클래스에 붙인것과 다르게 @AllArgsConstructor가 추가되지 않는다!

 

Member member1 = Member.builder()
                        .age(20)
                        .name("hello")
                        .team(teamA)
                        .build();
//주의! new 키워드가 없다

 => 객체 생성 시 선택 필드에 관한 메서드는 더이상 나오지 않게되었다!

 => 꼭 필요한 필수 필드에 대해서만 메서드가 나오게 되었지만 아직도 setter를 사용하는 것과 같다

 => name이 없어도 Member는 생성된다.

 

 

 

@Builder 필수 파라미터 보장

예외발생

public class Member {
    private Long id;
    private String name;
    private int age;
    private Team team;
    
    @Builder
    public Member(String name, int age, Team team) {
        Assert.notNull(name, "name 없어요");
        Assert.notNull(age, "age 없어요");
        Assert.notNull(team, "team 없어요");
        
        this.name = name;
        this.age = age;
        this.team = team;
    }
}
Member member1 = Member.builder()
                        .age(20)
                        .team(teamA)
                        .build();
//실행하면 에러 발생

위에처럼 커스텀 생성자에 Assert문, 또는 If문으로 예외를 발생시켜서 필수 파라미터의 값을 보장하는 방법이 있다

 => 파라미터 순서에 상관없이 값을 넣을 수 있다.

 => 하지만 해당 메서드를 실행할때까지는 에러가 있는지 알 수 없다

 

 

builder() 미리 정의

public class Member {
    private Long id;
    private String name;
    private int age;
    private Team team;
    
    @Builder
    public Member(String name, int age, Team team) {
        this.name = name;
        this.age = age;
        this.team = team;
    }

    public static Member.MemberBuilder builder(String name, Integer age) {
        MemberBuilder memberBuilder = new MemberBuilder();
        memberBuilder.name(name);
        memberBuilder.age(age);
        return memberBuilder;
    }
}
Member member2 = Member.builder("hello", 20).team(teamA).build();

lombok이 만들어주는 builder()를 대신 미리 작성해준다! 그러면 lombok이 builder를 다시 만들지 않고 작성한대로 사용된다.

 => 실행 전에 컴파일 오류가 발생하게끔 되었다

 => 하지만 생성자처럼 파라미터의 순서가 중요해졌다..

 

 

 

@Builder를 여러개 작성하여 역할을 구분해주자

@Builder(builderMethodName = "") : builder 메소드명을 재정의함

@Builder(builderClassName = "") : builder 클래스명을 정의함

 

builderMethodName만 정의했을 때

=> MemberBuilder 내부 클래스가 서로 공유되고있다

=> caseDelete에는 team이 없어야하는데 team까지 나오고있다

 

 

builderClassName과 builderMethodName을 함께 사용했을 때

=> 클래스명을 지정해준대로 다른 inner 클래스가 생성되었다

     caseDelete에 team이 더이상 나오지 않게 되었다

 

 

builderClassName과 builderMethodName을 함께 사용했을 때 필수값을 지정하고 싶다면

public class Member {
    private Long id;
    private String name;
    private Integer age;
    private Team team;

    // builder를 미리 작성해주고자 하면 ClassName과 MethodName에 대소문자 구분해서 작성해주자
    @Builder(builderClassName = "CaseUpdate", builderMethodName = "caseUpdate")
    public Member(String name, Integer age, Team team) {
        this.name = name;
        this.age = age;
        this.team = team;
    }

    //lombok이 만들어줄 builder를 먼저 만들어준다
    //컴파일 에러로 잡을 수 있다. 하지만 매개변수의 순서가 중요해진다
    public static Member.CaseUpdate caseUpdate(String name, Integer age){
        CaseUpdate caseUpdate = new CaseUpdate();
        caseUpdate.name(name);
        caseUpdate.age(age);
        return caseUpdate;
    }


    @Builder(builderClassName = "CaseDelete", builderMethodName = "caseDelete")
    public Member(String name, Integer age) {
  
    	//예외 발생하게 처리
        //컴파일 에러로는 잡을 수 없다. 실행해야만 에러를 찾을 수 있다.
        Assert.notNull(name, "name 없어요");
        Assert.notNull(age, "age 없어요");
        
        this.name = name;
        this.age = age;
    }
}
Member member1 = Member.caseUpdate("hello", 20).team(teamA).build();
Member member2 = Member.caseDelete().name("world").build();	//에러 발생할 것

 

 

 

@Builder를 사용할 때 주의할 점 정리

1. @Builder 사용 위치?

되도록 생성자에 @Builder를 사용하자.

2. @Builder 필수 파라미터를 보장해주고 싶다면?
 - Assert나 If문으로 예외를 발생시키자. 실행 후에만 예외가 발생함을 알 수 있다.
 - lombok이 만들어줄 builder를 미리 만들어주자. 컴파일 에러로 찾을 수 있지만 매개변수의 순서가 중요해진다

3. @Builder를 여러개 사용하고 싶다면?
builderClassName과 builderMethodName을 반드시 지정해주자!

 

 

 

 

 

 

 

lombok으로 편리하게 사용할수는 있지만.. 

builder패턴은 직접 만들어주는 것도 크게 나쁘지 않다고 생각한다!

 

반응형
  • 네이버 블러그 공유하기
  • 페이스북 공유하기
  • 트위터 공유하기
  • 구글 플러스 공유하기
  • 카카오톡 공유하기