점층적 생성자 패턴 vs 자바빈즈 패턴 vs 빌더 패턴

박영준·2023년 7월 26일
0

Java

목록 보기
109/111

점층적 생성자 패턴, 자바빈즈 패턴, 빌더 패턴 은 '빌더 패턴'의 우수성을 설명하기 위해 자주 비교된다.

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

1) 정의

public class Student {
    private final String name;	// 필수 매개변수			
    private final int age;	// 필수 매개변수
    private final String gradeClassNumber;	// 필수 매개변수

    private final String homeAddress;	// 선택 매개변수
    private final String phoneNumber;	// 선택 매개변수

	// 필수 매개변수를 가지는 생성자
    public Student(String name, int age, String gradeClassNumber) {
        this(name, age, gradeClassNumber, null, null);
    }	

	// 필수 매개변수에 선택 매개변수 homeAddress 가 추가된 생성자
    public Student(String name, int age, String gradeClassNumber, String homeAddress) {
        this(name, age, gradeClassNumber, homeAddress, null);
    }	

	// 모든 매개변수(필수 + 선택 + α)를 가지는 생성자
    public Student(String name, int age, String gradeClassNumber, String homeAddress, String phoneNumber) {
        this.name = name;
        this.age = age;
        this.gradeClassNumber = gradeClassNumber;
        this.homeAddress = homeAddress;
        this.phoneNumber = phoneNumber;
    }	
}
  • 마치 생성자가 점층적으로 성장하는 생성자를 가지도록 한 디자인 패턴이 점층적 생성자 패턴
    (필수 매개변수 → 필수 매개변수 + 선택 매개변수 → 모든 매개변수(필수 + 선택 + α))

  • 필수 매개변수를 가지는 생성자를 필두로,
    선택 매개변수를 가지는 생성자를 추가하여 여러 생성자를 가지는 디자인 패턴

2) 장단점

장점

  • 객체의 불변성 유지

    • 자비빈즈 패턴의 경우: freeze() 함수 등... 을 통해, 객체의 불변성을 유지해줘야 함
  • 여러 개의 가변 인자(varargs)를 받을 수 있음

  • 유연해서, 하나의 빌더 객체로 여러 객체를 만들 수 있음

  • 제네릭을 이용하여 인자가 설정된 빌더는 훌륭한 추상화가 가능

    • 훌륭한 추상적 팩토리를 만듦 + Class.newInstance 의 단점을 보완

단점

  • 코드 多

    • 매개변수가가 多 경우, 조합도 多
      • 생성자 수의 많아지면서 클래스가 복잡해 질 수 있음
  • 해당 값의 필드가 정확히 어떤 데이터를 지칭하는지 헷갈릴 수 있다

    Student student = new Student("이진혁", 19, "3417", "경남 김해시")
    • 해당 생성자만 보고선 19가 나이인지, 팔 길이인지 판단할 수 없다.
      • 이를 알기 위해서는 Student 클래스의 생성자를 직접 확인해야 한다.
  • 매개변수의 타입이 동일할 경우, 생성자를 만들 수 없는 경우가 생긴다.

    • Student 클래스에서의 address 를 받는 생성자 & phoneNumber 를 받는 생성자를 동시에 추가 X
  • 빌더 객체를 생성하는 오버헤드가 하드웨어와 밀접한 프로그램 or 성능이 중요한 상황에서는 큰 문제가 될 가능성

2. 자바빈즈 패턴(JavaBeans Pattern)

기존에 내가 사용하던 방식이다.

1) 정의

Student 엔티티

public class Student {
    private final String name;	// 필수 매개변수
    private final int age;	// 필수 매개변수
    private final String gradeClassNumber;	// 필수 매개변수

    private String homeAddress;	// 선택 매개변수
    private String phoneNumber;	// 선택 매개변수
    
    public Student(String name, int age, String gradeClassNumber) {
        this.name = name;
        this.age = age;
        this.gradeClassNumber = gradeClassNumber;
    }
}

service

// setter로 선택 매개변수(homeAddress, phoneNumber)를 추가로 저장할 수도 있다.
Student student = new Student("이진혁", 19, "3417");
student.setHomeAddress("경남 김해시");
student.setPhoneNumber("010-1111-1111");
  • 자바빈(Java Bean)처럼, 클래스에 setter를 추가함으로써 생성자 이후 setter로 필드 값을 채워넣으며 객체를 완성하는 디자인 패턴

  • 인자 없는 생성자를 호출하여 기본 객체를 만든 다음, setter 를 이용하여 필드 값을 채워나가는 패턴

  • 생성자에 전달되는 인자 수가 많을 때 적용 가능

2) 장단점

장점

  • (점층적 생성자 패턴에 비해) 기본값으로 초기화가 이루어진 상태에서 원하는 필드만 채워나가기 때문에,
    설정할 필요가 없는 필드에 인자를 전달할 필요 X

  • (점층적 생성자 패턴에 비해) 각각의 필드의 값을 설정하는 setter 를 이용하기 때문에, 인자가 헷갈릴 일 X

단점

  • 객체의 일관성 보장 X

    • 해당 함수를 실행하기 전까진 완성된 객체가 아니다
  • 객체의 불변성 유지 X

    • 객체 완성 후에도 setter 를 통해 내용 변경이 가능하기 때문
      • 단, 객체 완성 후 freeze() 함수로 객체를 얼리면 불변성 유지가 가능하다
  • 객체 하나를 완성하기 위해, 여러 메서드를 호출해야 함

    A a = new A();
    a.setA("A");
    a.setB(2);
    a.freeze();

3. 빌더 패턴

빌더 패턴은 매우 자주 사용된다.

1) 정의

  • '점층적 생성자 패턴의 안정성' + '자바빈 패턴의 가독성' 을 결합한 대안

  • 작게 분리된 인스턴스를 건축 하듯이 조합하여 객체를 생성

  • 필요한 객체를 직접 생성하는 대신, 필수 인자들을 생성자에 전부 전달하여 빌더 객체를 만든다.
    그런 다음, 빌더 객체에 정의된 설정 메소드들을 호출하여 선택적 인자들을 추가해 나간다.
    필요한 인자를 모두 적용하고 나면, build 메소드를 이용하여 변경 불가능(immutable) 객체를 만드는 패턴

  • 인자가 많은 생성자나 정적 팩토리가 필요한 클래스를 설계할 때 유용.

    • 특히, 대부분의 인자가 선택적 인자인 상황에 유용.
  • setter 대신에 사용할 수 있다.
    참고: Entity에서 @Setter 사용을 지양하자

2) 사용법

방법 1

Car 엔티티

class Car {
    private String brand;
    private String engine;
    private String name;
    private String tire;
    private int capacity;
    private int price;

    Car (Builder builder) {
        this.brand = builder.brand;
        this.engine = builder.engine;
        this.name = builder.name;
        this.tire = builder.tire;
        this.capacity = builder.capacity;
        this.price = builder.price;

    }

	// Builder 클래스 : Car 클래스 안에, 정적 클래스인 Builder 를 만든다
    public static class Builder {
        private String brand;
        private String engine;
        private String name;
        private String tire;
        private int capacity;
        private int price;

        // 필수적인 필드 : brand
        public Builder(String brand) {
            this.brand = brand;
        }

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

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

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

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

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

		// Builder 클래스는 build 메서드를 통해서 새로운 Car 객체를 생성
        public Car build() {
            return new Car(this);
        }
    }
}

service

// Builder 클래스는 build 메서드를 통해서 새로운 Car 객체를 생성
Car myCar = new Car.Builder("현대")
		.engine("독일엔진")
		.name("제네시스")
		.tire("한국타이어")
		.capacity(4)
		.price(70000000)
		.build();

방법 2

① 빌더 패턴을 적용할 객체(엔티티)에 @Builder 달기

@Builder
public class Bag {
	private String name;
    private int money;
    private STring memo;
}

② 어노테이션만 달면 빌더가 생기고, 빌더를 통해 객체를 생성할 수 있다.

Bag bag = Bag.builder()
	.name("name")
    .money(1000)
    .memo("memo")
    .build();

방법 3 : @Builder.Default

  • 기본값 설정을 위한 어노테이션

@Builder.Default 사용 X
해당 필드의 값이 없다.

@Builder
@ToString
public class Member {
    String id;
    int age;
}

@Builder.Default 사용 O
age 필드에서 설정해둔 초기값이 출력된다.

@Builder
@ToString
public class Member {
    String id;
    
    @Builder.Default
    int age = 10;
}

3) 장단점

장점

  • 객체의 생성 과정과 표현 방법을 분리하여,
    동일한 객체 생성에서도 서로 다른 결과를 만들어 낼 수 있다.(복잡한 객체 생성 가능)

  • 여러 개의 가변 인자(varargs)를 받을 수 있음

    • 빌더 패턴은 유연하므로, 하나의 빌더 객체로 여러 객체를 만들 수 있음
  • 제네릭을 이용하여 인자가 설정된 빌더는 훌륭한 추상화가 가능

    • 훌륭한 추상적 팩토리를 만듦 + Class.newInstance 의 단점 보완
  • (다른 패턴에 비해) 생성자 파라미터가 많아도 가독성이 좋음

    // 빌더 패턴 X
    Bag bag = new Bag("name", 1000, "memo", "abc", "what", "is", "it", "?");
    
    // 빌더 패턴 O
    Bag bag = Bag.builder()
    		.name("name")
    		.money(1000)
    		.memo("memo")
    		.letter("This is the letter")
    		.box("This is the box")
    		.build();
  • 빌더의 필드 이름으로 값을 설정하기 때문에, 파라미터를 순서대로 두지 않아도 됨

    // 빌더 패턴 X
    public Bag(String name, int money, String memo) {
            this.name = name;
            this.money = money;
            this.memo = memo;
    }
    
    // 빌더 패턴 O
    Bag bag = Bag.builder()
    		.name("name")
    		.memo("memo")	// memo를 money 대신 먼저해도 무방하다
    		.money(1000)
    		.build();

단점

  • 빌더 객체를 생성하는 오버헤드가

    • 하드웨어와 밀접한 프로그램
    • 성능이 중요한 상황에서는 성능에 큰 문제가 될 가능성 있음
  • (점층적 생성자 패턴에 비해) 코드 多 필요


참고: [Java] Builder 어노테이션 기본값 사용하기(Builder.Default)

profile
개발자로 거듭나기!

0개의 댓글