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

soluinoon·2023년 9월 3일
0

Effective Java

목록 보기
2/2
post-thumbnail
post-custom-banner

매개변수가 많아진다면?

필드가 많아지고 생성자에 매개변수가 많아진다면 코드가 복잡해집니다.
이럴 때 개발자들은 다양한 방법을 사용하곤 했습니다.
예제를 설명하기 위해, 제가 플레이했던 중세 시뮬레이션 게임인 '마운트앤블레이드'의 캐릭터를 만든다고 가정하겠습니다.

이렇게 생긴 게임입니다. 플레이어 외형을 정하기 위해 성별은 필수값으로 받는다고 가정하겠습니다.

플레이어의 나이, 체력, 마나, 공격력, 방어력은 선택변수 입니다.

점층적 생성자 패턴


필요한 매개변수를 점층적으로 받는 생성자입니다. 위 같은 코드는 단점이 명확합니다.

  1. age, hitPoint 처럼 자료형이 같고 연속적으로 나타난다면, 순서를 다르게 주면 오류를 잡을 수 없다.
  2. 외부에서 코드를 읽기 어렵다. 어떤 생성자가 필요한지 파악하는게 어렵다.

자바 빈즈 패턴

이번 예시에선 나이도 필수값으로 지정해보겠습니다.

public 생성자로 먼저 생성한 뒤에, setter를 통하여 값을 초기화하는 방식 입니다.

Player jihong = new Player();
jihong.setGender = true;
jihong.setAge = 26;
jihong.setHitPoint = 300;
...

점층적 생성자 패턴에서 엄청나게 많은 생성자를 만든 것에 비하면 코드가 정말 깔끔하고 헷갈리지도 않습니다.

하지만, 가장 중요한 불변이 아닙니다.
거기다가 필수값인 나이와, 성별을 주지 않는다면 제대로 동작하지 않고, 디버깅이 어렵게됩니다.

빌더 패턴

이런 단점들을 해소하고자 나온게 빌더 패턴입니다.

public class Player {
    private final boolean gender; // 필수 true면 male, false면 female
    private final int age; // 필수
    private final int hitPoint;
    private final int manaPoint;
    private final int strikingPower;
    private final int defensivePower;
    
    public static class Builder {
    	// 필수값들
        private final boolean gender;
        private final int age;
        
        // 필수 아닌 값, final이 아니고 기본값이 설정되어 
        // 있기 때문에 입력하지 않는다면 기본값으로 설정된다.
        private int hitPoint = 100;
        private int manaPoint = 100;
        private int strikingPower = 30;
        private int defensivePower = 30;

		// 필수값 입력
        public Builder(boolean gender, int age) {
            this.gender = gender;
            this.age = age;
        }
        // 메서드 이름을 필드명으로 줘서 가독성이 좋아진다!
        public Builder hitPoint(int hitPoint) {
            this.hitPoint = hitPoint;
            // 자기 자신을 반환하기 때문에 메서드 체이닝이 가능
            return this;
        }

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

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

        public Builder defensivePower(int defensivePower) {
            this.defensivePower = defensivePower;
            return this;
        }
        
        // 최종적으로 build()를 통해 본 클래스의 생성자를 호출
        public Player build() {
            return new Player(this);
        }
    }
    
	// 본 클래스의 생성자는 빌더로 생성 가능하다.
    public Player(Builder builder) {
        this.gender = builder.gender;
        this.age = builder.age;
        this.hitPoint = builder.hitPoint;
        this.manaPoint = builder.manaPoint;
        this.strikingPower = builder.strikingPower;
        this.defensivePower = builder.defensivePower;
    }
}

---사용---

Player jihong = new Player.Builder(true, 26) // 필수값으로 빌더를 얻는다.
                .hitPoint(300)
                .manaPoint(150)
                .defensivePower(30) // 순서가 바뀌어도 상관 x
                .strikingPower(30) // 순서가 바뀌어도 상관 x
                .build();

위의 코드를 통해 불변성과 가독성을 모두 얻을 수 있었습니다.

여담으로, 이런 패턴은 파이썬과 스칼라에 있는 네임드 옵셔널 파라미터를 흉내낸 것이라고 합니다.

정적 팩터리 메서드 사용

정적 팩터리 메서드를 사용하면 Spring의 @Builder 어노테이션의 기본 형태와 같은 방식으로 빌더를 사용할 수 있습니다.

public class Player {
    private final boolean gender; // 필수 true면 male, false면 female
    private final int age; // 필수
    private final int hitPoint;
    private final int manaPoint;
    private final int strikingPower;
    private final int defensivePower;

    public static class Builder {
        private final boolean gender;
        private final int age;

        private int hitPoint = 100;
        private int manaPoint = 100;
        private int strikingPower = 30;
        private int defensivePower = 30;

        public Builder(boolean gender, int age) {
            this.gender = gender;
            this.age = age;
        }

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

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

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

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

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


    }

    public Player(Builder builder) {
        this.gender = builder.gender;
        this.age = builder.age;
        this.hitPoint = builder.hitPoint;
        this.manaPoint = builder.manaPoint;
        this.strikingPower = builder.strikingPower;
        this.defensivePower = builder.defensivePower;
    }
    
	// 추가
    public static Builder builder(boolean gender, int age) {
        return new Builder(gender, age);
    }
}

---사용---
Player jihong2 = Player.builder(true, 26)
                .hitPoint(300)
                .manaPoint(150)
                .defensivePower(30)
                .strikingPower(30)
                .build();

계층적 클래스로 설계된 곳에서의 빌더 패턴

빌더 패턴은 계층적으로 설계된 클래스에서 사용하기 좋습니다.
(30 읽고 다시 작성)

Reference

http://www.gamtoon.com/new/ecm/usernews/view.gam?num=73

profile
수박개 입니다.
post-custom-banner

0개의 댓글