Effective Java

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

이진유진 2024. 3. 27. 15:48
반응형
Effective Java 3/E 판을 읽고 정리한 기록입니다. 

"생성자에 매개변수가 많다면, 빌더를 고려하라"는 것은

자바에서 객체를 생성할 때, 매개변수가 많은 경우, 가독성이 떨어지고 실수를 범하기 쉬워지며 코드 유지보수가 어려워질 수 있다는 의미입니다. 

따라서 많은 매개변수를 가진 생성자 대신 객체를 생성하기 위한 빌더 디자인 패턴을 고려해야 한다는 것을 나타냅니다. 

 

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

점층적 생성자 패턴은 객체를 생성할 때 선택적 매개변수를 포함한 여러 개의 생성자를 제공하는 방식입니다. 

이 패턴은 생성자의 매개변수 수가 늘어날수록 코드의 가독성과 유지보수가 저하될 수 있어, 

선택적으로 사용되는 생성자를 제공해 불필요한 복잡성을 줄이는 것을 목표로 합니다. 

예제코드

public class Person {
    private String name;
    private int age;
    private String address;
    private String phoneNumber;

    // 최소한의 필수 매개변수만을 받는 생성자
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 주소 정보까지 추가한 생성자
    public Person(String name, int age, String address) {
        this(name, age);
        this.address = address;
    }

    // 전화번호까지 추가한 생성자
    public Person(String name, int age, String address, String phoneNumber) {
        this(name, age, address);
        this.phoneNumber = phoneNumber;
    }

    // 각 필드에 대한 getter 메서드
    // setter 메서드는 이 예제에서는 생략하였습니다.
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getAddress() {
        return address;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }
}

 

위의 코드에서 Person 클래스는 생성자를 통해 객체를 생성하는데, 

필수적인 매개변수(name, age)는 최소한의 생성자로 받고, 

선택적인 매개변수(address, phoneNumbrer)를 추가로 받는 생성자를 구현하고 있습니다. 

이렇게 함으로써 사용자는 필요한 정보 만들 전달 하여 객체를 생성할 수 있습니다. 

 

빌더 패턴과 비교했을 때, 점층적 생성자 패턴은 간단하고 직관적이며, 

객체 생성 시 필요한 매개변수를 명시적으로 전달할 수 있어 유연성이 있지만, 

선택적 매개변수가 많아질수록 생성자의 수가 급격하게 증가하고, 

이로써 가독성 저하와 유지보수의 어려움이 생길 수 있습니다. 

 

이렇게 선택 매개변수가 많을 때 쓸 수있는 자바빈즈(Javabeans pattern)도 알아보겠습니다.

자바빈즈 패턴(JavaBeans Pattern) 

자바빈즈 패턴은 객체의 속성을 private로 설정하고, 이에 대한 접근자(getter)와 수정자(setter) 메서드를 공개하는 방식을 말합니다. 

이를 통해 객체의 상태를 캡슐화하고, 외부에서 안전하게 접근하여 속성을 변경하거나 읽을 수 있습니다. 

선택적 매개변수가 많을 때, 자바빈즈 패턴을 사용하면 가독성이 높아지고 코드 유지보수가 용이해집니다. 

 

public class Person {
    private String name;
    private int age;
    private String address;
    private String phoneNumber;

    public Person() {
        // 기본 생성자
    }

    // 이름 설정 메서드
    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 setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    // 이름 반환 메서드
    public String getName() {
        return name;
    }

    // 나이 반환 메서드
    public int getAge() {
        return age;
    }

    // 주소 반환 메서드
    public String getAddress() {
        return address;
    }

    // 전화번호 반환 메서드
    public String getPhoneNumber() {
        return phoneNumber;
    }
}

 

위의 코드와 같이 선택적 매개변수를 가진 객체를 생성할 수 있습니다. 

객체를 생성한 후에는 각 속성에 대해 set메서드를 호출해 값을 설정, get메서드를 호출해 값을 읽을 수 있습니다. 

자바빈즈 패턴의 단점

자바빈즈 패턴에서는 점층적 생성자 패턴의 단점이 보이지 않지만, 심각한 단점을 지니는데, 

그것은 자바빈즈 패턴에서는 객체 하나를 만들려면, 메서드를 여러 개 호출해야 하고, 

객체가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태기 때문입니다. 

 

자세하게 단점들을 나열해보면,

 

  1.  불변 객체 지원이 어렵습니다
    1. 자바빈즈 패턴에서는 객체의 속성을 수정자(setter) 메서드를 통해 변경할 수 있어, 불변객체(immutable object)를 쉽게 구현하기 어렵습니다. 
    2. 불변 객체는 객체의 상태가 변경되지 않아야 하므로, 수정자 메서드를 제공하지 않아야 합니다.
  2. 코드 중복
    1. 객체의 속성이 많은 경우, 각 속성에 대한 접근자(getter)와 수정자(setter) 메서드를 구현해야 합니다. 
    2. 이로 인해 코드의 중복이 발생할 수 있으며, 유지보수가 어려워질 수 있습니다.
  3. 무결성 유지의 어려움
    1. 자바빈즈 패턴에서는 수정자(setter) 메서드를 통해 객체의 속성을 변경할 수 있기 때문에, 속성에 대한 무결성을 보장하기 어렵습니다. 즉 잘못된 값이 설정될 수 있으며, 이로 인해 예기치 않은 동작이 발생할 수 있습니다. 
  4. 스레드 안정성의 어려움
    1. 자바빈즈 패턴에서는 멀티스레드 환경에서의 안전성을 보장하기 어렵습니다. 
    2. 여러 스레드가 동시에 수정자(setter) 메서드를 호출 시 , 예기치 않은 결과가 발생할 수 있습니다. 
  5. 불변성을 위한 보일러플레이트 코드 
    1. 불변 객체를 구현하기 위해 필요한 보일러플레이트 코드가 많이 필요합니다. 
    2. 예를 들어, 불변 객체의 속성을 설정하는 생성자를 추가하거나, 속성을 변경하지 않는 수정자(setter) 메서드를 제공해야 합니다.                                                                                                               

이러한 단점들은 객체지향 설계 원칙 중 하나인 캡슐화(encapsulation)를 위반할 수 있으며, 

코드의 유연성과 유지보수성을 저하시킬 수 있습니다. 

 

그러므로, 우리는 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴을 고려할 수 있습니다.

빌더 디자인 패턴(Builder Design Pattern) 

객체를 생성하기 위한 복잡한 생성자 대신, 여러 개의 메서드를 이용하여 객체를 조립하는 방식을 제공합니다. 

이를 통해 객체를 생성할 때 매개변수를 하나씩 지정하는 대신, 메소드 체이닝을 사용하여 가독성이 높고 유연한 코드를 작성할 수 있습니다. 

예제코드

public class Person {
    private String name;
    private int age;
    private String address;
    private String phoneNumber;

    public Person(String name, int age, String address, String phoneNumber) {
        this.name = name;
        this.age = age;
        this.address = address;
        this.phoneNumber = phoneNumber;
    }
}

 

위와 같은 Person 클래스가 있다고 가정할 때, 

이 클래스는 생성자에 매개변수가 많아, 객체를 생성할 때 코드가 복잡해집니다. 

 

그러므로 빌더 패턴을 사용하여 다음과 같이 변경할 수 있습니다. 

public class Person {
    private String name;
    private int age;
    private String address;
    private String phoneNumber;

    private Person(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.address = builder.address;
        this.phoneNumber = builder.phoneNumber;
    }

    public static class Builder {
        private String name;
        private int age;
        private String address;
        private String phoneNumber;

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

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

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

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

        public Person build() {
            return new Person(this);
        }
    }
}

 

위의 예제에서 Builder 클래스를 사용하여 Person 객체를 생성하면, 

매개변수의 순서를 신경 쓰지 않고 객체를 생성할 수 있으며, 가독성이 향상되고 실수를 줄일 수 있습니다. 

 

빌더 클래스를 사용하여 객체 생성 방법

  1. 빌더 객체를 생성
  2. 필요 속성 값 설정
  3. build() 메서드 호출로, 최종적으로 객체 생성 
Person person = new Person.Builder("John")
                        .age(30)
                        .address("123 Main St")
                        .phoneNumber("555-1234")
                        .build();

 

위와 같은 Person 빌더 클래스는 위와 같이 객체를 생성할 수 있습니다. 

 

이렇게 하면, 생성자에게 매개변수를 하나씩 지정하는 대신, 

각 속성을 메서드를 통해 설정하여 가독성이 향상되고 , 실수를 줄일 수 있습니다. 

또한 객체를 생성할 때 필요한 속성만 설정할 수 있으며, 다른 속성은 기본값으로 설정되도록 할 수도 있습니다. 

 

빌더 클래스의 기본값 설정법 예시 코드 

public class Person {
    private String name;
    private int age;
    private String address;
    private String phoneNumber;

    private Person(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.address = builder.address;
        this.phoneNumber = builder.phoneNumber;
    }

    public static class Builder {
        private String name;
        private int age = 18; // 기본값으로 18세를 설정
        private String address = "Unknown"; // 기본값으로 "Unknown" 주소를 설정
        private String phoneNumber = "Unknown"; // 기본값으로 "Unknown" 전화번호를 설정

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

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

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

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

        public Person build() {
            return new Person(this);
        }
    }
}

 

위와 같은 코드로, 빌더 패턴을 사용하여 필요한 설정만 설정하고 기본값을 설정할 수 있습니다. 

 

빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋다.

빌더 패턴은 객체를 생성할 때 매개변수가 많거나, 복잡한 계층적 구조를 가진 클래스와 함께 사용하기 좋습니다. 

계층적으로 설계된 클래스는 여러 수준의 구조로 이뤄져 있으며, 

각 수준은 다른 클래스의 인스턴스를 포함하거나 상속 관계를 가집니다. 

 

계층적으로 설계된 Person 클래스를 빌더 패턴을 사용하여 구현한 예제입니다. 

public class Person {
    private String name;
    private int age;
    private Address address;

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

    public static class Builder {
        private String name;
        private int age;
        private Address address;

        public Builder(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public Builder address(String street, String city, String state, String zip) {
            this.address = new Address(street, city, state, zip);
            return this;
        }

        public Person build() {
            return new Person(this);
        }
    }

    // Address 클래스는 내부 클래스로 정의되어 있음
    public static class Address {
        private String street;
        private String city;
        private String state;
        private String zip;

        public Address(String street, String city, String state, String zip) {
            this.street = street;
            this.city = city;
            this.state = state;
            this.zip = zip;
        }

        // 각 속성에 대한 getter 메서드
        // setter 메서드는 이 예제에서는 생략하였습니다.
        public String getStreet() {
            return street;
        }

        public String getCity() {
            return city;
        }

        public String getState() {
            return state;
        }

        public String getZip() {
            return zip;
        }
    }

    // Person 클래스의 나머지 코드...
}

 

위의 코드에서 Person 클래스는 내부 클래스인 Address 객체를 포함하며, 

Person 객체를 생성할 때 주소 정보를 설정하기 위해 빌더 패턴을 사용하였습니다. 

이렇게 함으로써 계층적으로 구조화된 클래스를 쉽게 생성할 수 있으며, 

가독성이 높고 유연성이 있는 코드를 작성할 수 있습니다. 

 

// 추상 클래스 Animal
public abstract class Animal {
    protected String name;

    public String getName() {
        return name;
    }

    // 추상 빌더 클래스
    public static abstract class AnimalBuilder<T extends Animal> {
        protected String name;

        public AnimalBuilder<T> name(String name) {
            this.name = name;
            return this;
        }

        // 추상 build 메서드
        public abstract T build();
    }
}

// 구체 클래스 Dog
public class Dog extends Animal {
    private String breed;

    private Dog(Builder builder) {
        this.name = builder.name;
        this.breed = builder.breed;
    }

    public String getBreed() {
        return breed;
    }

    // 구체 빌더 클래스
    public static class Builder extends AnimalBuilder<Dog> {
        private String breed;

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

        @Override
        public Dog build() {
            return new Dog(this);
        }
    }
}

// 구체 클래스 Cat
public class Cat extends Animal {
    private String color;

    private Cat(Builder builder) {
        this.name = builder.name;
        this.color = builder.color;
    }

    public String getColor() {
        return color;
    }

    // 구체 빌더 클래스
    public static class Builder extends AnimalBuilder<Cat> {
        private String color;

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

        @Override
        public Cat build() {
            return new Cat(this);
        }
    }
}

 

위의 코드에서는 Animal 클래스를 추상 클래스로 정의하고, 

이 클래스에서 해당하는 빌더를 추상 클래스로 구현하였습니다. 

이후 각각의 구체 클래스인 Dog과 Cat 클래스에서는 해당 클래스에 대한 구체적인 정보를 포함한 빌더 클래스를 구현하였습니다. 

 

이렇게 함으로써 상위 클래스와 하위 클래스 간의 관계도 유지하며, 각 클래스에 관련된 정보를 생성할 수 있습니다. 

ex) 개에 대한 필터 - Dog.Builder 클래스에 구체적 정의 / 고양이에 대한 빌더 - Cat.Builder 클래스에 구체적 정의 

 

위와 같은 개와 고양이를 나타낸 클래스의 사용 예시 코드입니다.

public class Main {
    public static void main(String[] args) {
        // 개 객체 생성
        Dog dog = new Dog.Builder()
                .name("멍멍이")
                .breed("시베리안 허스키")
                .build();

        // 고양이 객체 생성
        Cat cat = new Cat.Builder()
                .name("야옹이")
                .color("흰색")
                .build();

        // 생성된 개와 고양이 객체 정보 출력
        System.out.println("개 이름: " + dog.getName());
        System.out.println("개 종: " + dog.getBreed());
        System.out.println("고양이 이름: " + cat.getName());
        System.out.println("고양이 색상: " + cat.getColor());
    }
}

 

생성자로는 누릴 수 없는 이점으로, 빌더를 이용하면 가변인수 매개변수를 유용하게 사용할 수 있다.

가변인수(varargs) 매개변수는 메서드나 생성자의 매개변수로 배열 형태의 인수를 받을 수 있는 특별한 형태의 매개변수입니다. 

 

이것을 빌더 패턴에서 객체를 생성할 때, 가변인수 매개변수로 여러 개의 값을 전달하고자 할 때 유용하게 사용됩니다. 

public class Product {
    private String name;
    private double price;
    private String[] tags;

    private Product(Builder builder) {
        this.name = builder.name;
        this.price = builder.price;
        this.tags = builder.tags;
    }

    public static class Builder {
        private String name;
        private double price;
        private List<String> tagList = new ArrayList<>();

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

        public Builder price(double price) {
            this.price = price;
            return this;
        }

        public Builder tag(String tag) {
            this.tagList.add(tag);
            return this;
        }

        public Builder tags(String... tags) {
            this.tagList.addAll(Arrays.asList(tags));
            return this;
        }

        public Product build() {
            return new Product(this);
        }
    }

    // Getter methods...
}

 

위의 코드에서 Product 클래스를 빌더 패턴으로 구현하고 있고, 

Builder 클래스에서는 tag 메서드를 사용해 하나의 태그를 추가하거나, 

tags 메서드를 사용해 여러 개의 태그를 한 번에 추가할 수 있습니다. 

public class Main {
    public static void main(String[] args) {
        // 빌더 클래스를 사용하여 제품 객체를 생성
        Product product = new Product.Builder()
                .name("Notebook")
                .price(1500.00)
                .tag("Electronics")
                .tag("Office")
                .build();

        // 생성된 제품 객체의 정보 출력
        System.out.println("제품 이름: " + product.getName());
        System.out.println("제품 가격: $" + product.getPrice());
        System.out.println("제품 태그: " + Arrays.toString(product.getTags()));
    }
}

 

위와 같이 사용될 수 있습니다. 

핵심정리
생성자나 정적 팩토리가 처리해야 할 매개변수가 많다면, 
빌더 패턴을 선택하는 게 낫습니다. 
매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇습니다. 
빌더는 점층적으로 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 
자바빈즈보다 안전합니다. 

오늘은 빌더 패턴에 대하여 자세히 알아보았습니다. 
생각보다 제가 모르는 것이 많아, 
하루빨리 이펙티브 자바를 정복해야겠다는 생각이 드는 오늘이네요..

다들 즐거운 개발되세요 :> 

+ 이펙티브 자바라는 좋은 책 선물해 주신 산군의 강준혁 CTO님 너무너무 감사드려요 :D
    더 열심히 공부해서 빠르게 성장하겠습니다 :D

 

반응형