테마.log/Effective Java

[Effective Java] 2. 생성자에 매개변수가 많다면 빌더를 고려하라

_2J 2022. 4. 15. 23:00

Effective Java 3/E

 

정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋겠다. 그렇다고 하더라도 정적 팩터리를 사용하는게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자!

 

 

아이템2: 생성자에 매개변수가 많다면 빌더를 고려하라

 

 

 

Class 설계시 필수, 선택적으로 받을 매개변수가 구분됩니다.

이렇게 설계된 다향한 형태의 클래스들을 객체화하는 대표적인 3가지 패턴이 존재합니다.

 

 

1. 점층적 생성자 패턴 ( Telescoping Constructor Pattern )

2. 자바 빈즈 패턴 ( Java Beans Pattern )

3. 빌더 패턴 ( Builder Pattern )

 

 

 

 

 

 

 

1. 점층적 생성자 패턴 ( Telescoping constructor pattern )

필수 매개변수만 받는 생성자부터 추가적으로 선택 매개변수를 1개, 2개... N개 형태로 선택 매개변수를 전부 받는 생성자까지 늘려가는 방식이며, 마치 생성자가 점층적으로 많아지는 생성자를 가지도록 한 디자인 패턴입니다.

 

public class Member {
	
    private String name;				// 필수
    private int age;				   	// 필수
    private String address;				// 선택
    private String phone;				// 선택
    private String email;  				// 선택
    
    // 필수 매개변수를 가지는 생성자
    public Member(String name, int age) {
    	this(name, age, null, null, null);
    }
    
    // 선택 매개변수 address가 추가된 생성자
    public Member(String name, int age, String address) {
    	this(name, age, address, null, null);
    }
    
    // 선택 매개변수 phone이 추가된 생성자
   	public Member(String name, int age, String address, String phone) {
    	this(name, age, address, phone, null);
    }
    
    // 모든 매개변수를 가지는 생성자
    public Member(String name, int age, String address, String phone, String email) {
    	this.name = name;
        this.age = age;
        this.address = address;
        this.phone = phone;
        this.email = email;
    }
}

 

 

점층적 생성자 패턴의 단점

  • 매개변수가 많아질수록 많은 조합이 만들어지고, 생성자의 수가 많아집니다. 이는 코드 작성 효율과 가독성이 저하되는 영향을 줍니다.
  • 클래스의 생성자를 호출하는 입장에서 해당 매개변수가 맞는지, 매개변수의 개수는 제대로 입력한 것인지 확인해야 하는 불편함이 있습니다.
  • 매개변수의 타입이 같은 경우 생성자를 만들 수 없습니다.
Member member = new Member("jan", 99, "KOR", "01012345678");

Test test = new Test(12, 10, 1, 330, 9);

Member의 경우 간단한 생성자라서 들어가는 값만 봐도 어떤 내용인지 유추할 수 있습니다.

하지만 예를 들어 Test 같은 생성자에 인자로 모두 int 값만 들어가는 경우가 있다면 12가 무슨 값인지, 330이 무슨 값인지 알기 위해서는 생성자를 찾아봐야 하는 경우가 생깁니다.

 

 

    // 선택 매개변수 address가 추가된 생성자
    public Member(String name, int age, String address) {
    	this(name, age, address, null, null);
    }
    
    // 선택 매개변수 phone이 추가된 생성자
    public Member(String name, int age, String phone) {
        this(name, age, null, phone, null);
    }

마지막 매개변수 타입이 같은 생성자를 만들 수 없는 경우입니다. 다음과 같은 생성자는 인자로 받는 타입이 같기 때문에 이렇게 두 개의 생성자를 동시에 생성할 수 없습니다.

 

 

 

 

 

 

2. 자바 빈즈 패턴 ( Java Beans Pattern ) 

매개변수가 없는 생성자로 객체를 만든 후 Setter 메소드를 호출해 원하는 매개변수의 값을 설정하는 방식의 패턴이다.

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

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

public void setAddress(String address) {
    this.address = address;
}

public void setPhone(String phone) {
    this.phone = phone;
}

public void setEmail(String email) {
    this.email = email;
}
Member member = new Member();
member.setName("jan");
member.setAge(99);
member.setAddress("KOR");
member.setPhone("01012345678");

 

 

자바빈즈 패턴의 장단점

 

코드가 길지만 인스턴스를 만들기 쉽고, 가독성이 좋다는 장점이 있습니다.

단점으로는 하나의 객체를 완성하기까지 메서드를 여러 번 호출해야 하고, setter 메서드가 있기 때문에 일관성이 무너진 상태.

 

앞선 점층적 생성자 패턴에서는 매개변수들이 유효한지 생성자에서만 확인하면 일관성 유지가 가능했지만, 자바빈즈 패턴의 경우에는 불가능하다. 때문에 불변의 객체(immutable)를 만들 수 없고, 쓰레드 안정성을 위해 추가적 작업(수동으로 freeze)이 필요하다.

 

 

 

 

 

 

 

3. 빌더 패턴 ( Builder Pattern )

점층적 생성자 패턴의 안전성 + 자바빈즈 패턴의 가독성 두가지 패턴의 장점을 가진 패턴이다.

 

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);
        }
    }
}

Build Pattern을 사용할 클래스는 위와 같이 구현할 수 있으며,

 

Member member = Member.builder()
                .name("jan")
                .age(99)
                .address("jan")
                .phone("010")
                .build();

다음과 같이 사용할 수 있습니다.

* Member 클래스에는 setter 메서드와 public 생성자가 없기 때문에 Member 객체를 얻기 위해서는 오직 Builder 클래스를 통해서만 가능합니다.

 

 

각 매개변수를 넣어주는 메서드 체이닝 기법을 적용하고, 필요한 매개변수를 세팅한 후 build() 메서드로 객체를 생성합니다.

이렇게 빌더 패턴을 사용하면 어느 필드에 어떤 값을 채워야 하는지 명확하게 알 수 있으며, 데이터 일관성, 객체 불변성 등을 만족시키며 코드의 가독성 또한 향상됩니다.

 

하지만 빌드 패턴은 구현시 많은 코드양이 필요하고, Builder라는 객체를 하나 더 만드는 것이기 때문에 사용하는 코드에 따라 성능이 낮아질 수 있습니다.

 

따라서 클래스를 설계할 때 필수, 선택 인자들이 많은 경우 Builder 패턴을 사용하는 것이 효율적입니다.

 

 

 

 

 

 

 

+ Lombok 라이브러리를 활용한 빌더 패턴

앞서 생성한 빌더 패턴을 @Builder 어노테이션 하나로 구현할 수 있다.

 

구현한 객체

@Builder
public class Member {
    private String name;
    private int age;
    private String address;
    private String phone;
}

 

컴파일된 클래스파일

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.demo;

public class Member {
    private String name;
    private int age;
    private String address;
    private String phone;

    Member(String name, int age, String address, String phone) {
        this.name = name;
        this.age = age;
        this.address = address;
        this.phone = phone;
    }

    public static Member.MemberBuilder builder() {
        return new Member.MemberBuilder();
    }

    public static class MemberBuilder {
        private String name;
        private int age;
        private String address;
        private String phone;

        MemberBuilder() {
        }

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

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

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

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

        public Member build() {
            return new Member(this.name, this.age, this.address, this.phone);
        }

        public String toString() {
            return "Member.MemberBuilder(name=" + this.name + ", age=" + this.age + ", address=" + this.address + ", phone=" + this.phone + ")";
        }
    }
}

 

@Builder를 Class위에 선언하면 @AllArgsConstrutor를 붙인 것과 같은 효과를 보는데, Builder Pattern을 사용할 경우 필수 매개 변수를 최소화 해주는 것이 좋다.

 

@Builder
public class Member {
    @NonNull
    private final String name;
    @NonNull
    private final Integer age;
    private String address;
    private String phone;
}

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.demo;

import lombok.NonNull;

public class Member {
    @NonNull
    private final String name;
    @NonNull
    private final Integer age;
    private String address;
    private String phone;

    Member(@NonNull String name, @NonNull Integer age, String address, String phone) {
        if (name == null) {
            throw new NullPointerException("name is marked non-null but is null");
        } else if (age == null) {
            throw new NullPointerException("age is marked non-null but is null");
        } else {
            this.name = name;
            this.age = age;
            this.address = address;
            this.phone = phone;
        }
    }

    public static Member.MemberBuilder builder() {
        return new Member.MemberBuilder();
    }

    public static class MemberBuilder {
        private String name;
        private Integer age;
        private String address;
        private String phone;

        MemberBuilder() {
        }

        public Member.MemberBuilder name(@NonNull String name) {
            if (name == null) {
                throw new NullPointerException("name is marked non-null but is null");
            } else {
                this.name = name;
                return this;
            }
        }

        public Member.MemberBuilder age(@NonNull Integer age) {
            if (age == null) {
                throw new NullPointerException("age is marked non-null but is null");
            } else {
                this.age = age;
                return this;
            }
        }

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

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

        public Member build() {
            return new Member(this.name, this.age, this.address, this.phone);
        }

        public String toString() {
            return "Member.MemberBuilder(name=" + this.name + ", age=" + this.age + ", address=" + this.address + ", phone=" + this.phone + ")";
        }
    }
}

 

Null 체크

@NonNull 어노테이션을 변수에 붙이면 자동으로 null 체크를 해줍니다. 즉, 해당 변수가 null로 넘어온 경우, NullPointerException 예외를 일으켜 줍니다.