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패턴은 직접 만들어주는 것도 크게 나쁘지 않다고 생각한다!

     

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