java-squid / effective-java

effective java 3e study
105 stars 38 forks source link

[아이템 02] 생성자에 매개변수가 많으면 빌더를 고려하라 #2

Closed 102092 closed 4 years ago

david215 commented 4 years ago

abstract static class Builder<T extends Builder<T>> 에서 보이는 recursive type parameter에 대해서 간단하게 설명해주시면 감사하겠습니다. 빌더 패턴이 상속 관계에 얽혀있을 때 구현하기 위해서는 기본적인 개념이나 문법을 이해해야 할 듯 하네요.

wooody92 commented 4 years ago

비슷한 질문으로 추상 클래스로 abstract static class Builder<T extends Builder<T>>를 만들고 상속하여 사용하는 과정이 잘 이해가 안가는데요.. 저희가 이해하기 쉽도록 간단한 예제를 만들어서 이야기 해보면 좋을거같아요.

102092 commented 4 years ago

p16 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태가 된다..

여기서 객체의 일관성이란 어떤걸 의미하는 걸까요....? -> 아마도 생성된 객체가 setter를 통해 변화가능함을 의미하지 않나 싶네요. 그 다음 문단에서 javabeans setter, final class등이 나오는 걸 보니까..

kses1010 commented 4 years ago

@david215 Recursive type parameter 즉, 재귀적 타입 한정이라고 부릅니다. 재귀적 타입 한정을 이용하는 제네릭 타입을 사용하면 추상메서드 self를 지원하여 하위클래스에서도 형변환 하지 않고도 상위타입에서 구현한 메서드를 연쇄적으로 호출할 수 있다고 합니다.

아이템30에서 Comparable 인터페이스를 소개하면서 재귀적 타입 한정을 소개하고 있습니다.

public interface Comparable<T> {
    int compareTo(T o);
}

public static <E extends Comparable<E>> E max(Collection<E> c);

// 재귀적 타입 한정 빌더
abstract static class Builder<T extends Builder<T>> {

}

타입 한정인 <E extends Comparable<E>> 는 "모든 타입 E는 자신과 비교할 수 있다" 라고 읽을 수 있습니다. 그렇다면 Builder<T extends Builder<T>> 는 "모든 타입 T는 자신과 빌더를 사용할 수 있다." 라고 읽을 수도 있겠죠.

하위 클래스에서 타입 캐스팅없이 사용할 수 있습니다. 제가 정확하게 재귀적 타입 한정을 이해를 한 건지 모르겠군요. 그러나, 재귀적 타입 한정이 이보다 훨씬 복잡해질 가능성이 있으나, 다행히 그런일은 잘 일어나지 않는다고 합니다.


상속에 관하여

빌더 패턴에서 상속은 사실 다른 상속관계와 크게 다르지 않습니다. 피자 예제처럼 피자 → NY피자, 칼초네피자로 상속하고 오버라이딩할 메서드는 build()(인스턴스 생성), self()를 구현하면 됩니다. 여기서 self는 시뮬레이트한 셀프 타입 관용구라하여 파이썬, 스칼라에서 있는 self타입을 구현을 해야합니다. Java에선 self타입이 없거든요.

실제 빌더를 구현하지 않고 롬복을 사용할 때는 다릅니다. 롬복에서의 @Builder 는 필수매개변수가 없고 선택적 매개변수만 존재합니다. 그 뿐만 아니라 재귀적 타입한정을 이용한 빌더 생성을 하지 않습니다. 최소한의 기능만을 사용을 하겠다는 겁니다.

롬복을 이용한 빌더 상속은 부모 클래스에서 자식 클래스를 넘겨줄 때는 큰 문제는 없습니다.

@Getter
@AllArgsConstructor
public class Parent {
    private final String parentName;
    private final int parentAge;
}

@Getter
public class Child extends Parent {
    private final String childName;
    private final int childAge;

    @Builder
    public Child(String parentName, int parentAge, String childName, int childAge) {
        super(parentName, parentAge);
        this.childName = childName;
        this.childAge = childAge;
    }

그러나, 만약 부모 클래스에서 빌더를 생성한다면,

@Getter
@AllArgsConstructor
public class Parent {
    private final String parentName;
    private final int parentAge;

      @Builder
    public Parent(String parentName, int parentAge) {
        this.parentName = parentName;
        this.parentAge = parentAge;
    }
}

@Getter
public class Child extends Parent {
    private final String childName;
    private final int childAge;

    @Builder
    public Child(String parentName, int parentAge, String childName, int childAge) {
        super(parentName, parentAge);
        this.childName = childName;
        this.childAge = childAge;
    }

이런 경우에는 빌더이름이 중복이 되어 컴파일 에러가 발생합니다. 이름이 중복되어 나는 컴파일 에러 같은경우 자식클래스에서 빌더의 이름을 바꿔주면 됩니다. 클라이언트 코드에선 자식클래스를 생성 시 builderMethodName 에서 지정한 빌더를 사용합니다.

@Getter
public class Child extends Parent {
    private final String childName;
    private final int childAge;

    @Builder(builderMethodName = "childBuilder")
    public Child(String parentName, int parentAge, String childName, int childAge) {
        super(parentName, parentAge);
        this.childName = childName;
        this.childAge = childAge;
    }
}

// 클라이언트 코드
Child child = Child.childbuilder()
  .parentName("Cloud")
  .parentAge(345)
  .childName("Sunny")
  .childAge(29)
  .build();

만약 자신이 롬복 1.18버전을 쓰신다면 @SuperBuilder 를 사용하는것도 나쁘지 않습니다.

@SuperBuilder는 부모, 자식클래스 구분없이 빌더를 만들어 클라이언트 코드에 사용이 가능합니다.

@Getter
@SuperBuilder
public class Parent {
    // same as before...

@Getter
@SuperBuilder
public class Child extends Parent {
   // same as before...

// 클라이언트 코드
Child child = Child.builder()
  .parentName("Cloud")
  .parentAge(345)
  .childName("Sunny")
  .childAge(29)
  .build(); 

단, @Builder 와 같이 쓸 수없기 때문에 주의 해야 합니다.

kses1010 commented 4 years ago

@102092 객체의 일관성은 클라이언트가 일관된 접근 방식을 이용하여 일관된 접근 방식을 제공하는걸 의미합니다. 일관성이 없으면 접근방식(변수에 직접 사용, 엑세스 함수 사용)에 주의력을 소진하게 되어, 뒤에 있는 버그가 있을 경우에 찾기 힘들어집니다.

자바가 멀티쓰레드 환경이라 setter를 쓰면 일관성이 깨지는 게 개발자들 이야기입니다. 객체가 유효하지 않은 상태를 가질 수 없도록 하는 것이 바람직한 설계이고, 생성자 주입으로 유효성 검사와 함께 필수값을 모두 제공하고, 추후에 불필요한 값 변경을 금지하도록 하고 있습니다.

그 중 빌더 패턴은 매개변수가 많을 경우에 사용하는게 좋을 듯 합니다.

kses1010 commented 4 years ago

@102092 일관성에 대하여 아이템17을 보면 변경 가능성을 최소화하라고 합니다. 클래스를 불변으로 만들라고 정합니다. 여기서 클래스 불변으로 만드는 다섯가지 규칙이 있습니다.

  1. 객체의 상태를 변경하는 메서드를 제공하지 않는다. -> 아마 setter를 의미하는 것 같습니다.
  2. 클래스를 확장할 수 없도록 한다.
  3. 모든 필드를 final로 선언한다.
  4. 모든 필드를 private로 선언한다.
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. (여기서 무슨뜻인지 몰라서 넘겼습니다.)
kses1010 commented 4 years ago

@wooody92 A: 자바봄에서 본 예제에서는 카드를 예시로 하여 표현했는데 간단한 예제는 어떻게 구현을 해야할지 모르겠습니다. EnumSet존재와 재귀적 타입 한정이 어려운 관계로 구현이 어렵네요. 여기에 대해선 스터디할 때 같이 이야기를 나누었으면 좋겠습니다.

자바봄을 바탕으로한 구현은 다음과 같습니다.

PayCard.java

public abstract class PayCard {
    public enum Benefit {
        POINT("포인트"), SALE("할인"), SUPPORT("연회비지원");
        Benefit(String benefit) {

        }
    }

    final Set<Benefit> benefits;

    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Benefit> benefits = EnumSet.noneOf(Benefit.class);

        public T addBenefit(Benefit benefit) {
            this.benefits.add(benefit);
            return self();
        }

        abstract PayCard build();

        protected abstract T self();
    }

    PayCard(Builder<?> builder) {
        benefits = builder.benefits.clone();
    }

}

KakaoCard.java

public class KakaoCard extends PayCard {
    public enum Sale {
        GAME, KAKAO_STORE
    }

    private final Sale sale;

    public static class Builder extends PayCard.Builder<Builder> {
        private final Sale sale;

        public Builder(Sale sale) {
            this.sale = sale;
        }

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

        @Override
        protected Builder self() {
            return this;
        }
    }

    KakaoCard(Builder builder) {
        super(builder);
        sale = builder.sale;
    }
}

// 클라이언트

KakaoCard kakaoCard = new KakaoCard.Builder(GAME)
                .addBenefit(POINT)
                .build();
102092 commented 4 years ago

@kses1010

자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. (여기서 무슨뜻인지 몰라서 넘겼습니다.)

아마도 멤버 변수의 접근 제어자를 private하게 선언하라 라는 의미 같아요. 맥락상 가변 컴포넌트라는 게, 멤버 변수보다 더 넓은 의미라고도 보여지긴 하는 데.. 제 생각은 그렇습니다.

102092 commented 4 years ago

Q) 재귀적 한정 제어자(item30) 에 대해

102092 commented 4 years ago

Q) Lombok Builder, Builder에 대해

102092 commented 4 years ago

Q) 일관성에 대해서