본문 바로가기

Effective Java/객체 생성과 파괴

#03 #02의 보충설명 - 계층적으로 설계된 클래스와 빌더패턴

지난 시간에 공부했던 builder 패턴중 마지막에 뇌정지로 인한 계층적으로 설계된 클래스와 빌더패턴 에 대해서 정리하는 시간이다. 자바로 몇년동안 개발을 해오면서 이런 소스도 분석할 줄 모르는 내 자신이 너무 한심하기도 했다.

그래서 해당 클래스의 계층적 구조화 동작 방식을 하나하나 뜯어볼 계획이다.

개인적으로 공부하면서 집중력이 많이 필요한 부분 이였기에 해당 포스팅을 보면서 이해하고 싶다면... 꼭 컨디션이 좋을때 보길 추천한다.

 

알아두기 

어떤 클래스를 상속 받으면 상속받은 자식 클래스의 생성자에서 부모 클래스를 호출해야만 하는데 만약 부모가

기본생성자를 가지고 있다면 컴파일러는 자동으로 super()를 호출함으로써 부모의 생성자를 호출하게 된다. 이 부분을 이해 하고 있다면 Pizza 클래스의 생성자는 기본 생성자는 없고 내부 클래스인 Builder 받아서 어떤 작업을 하고있다.

그렇기 때문에 Pizza를 상속받은 자식클래스는 꼭 자신의생성자에서 Pizza를 호출하는 super(Builder) 를 사용하게 된다.

Index

  • NyPizza / Calzone / Pizza   Class다이어그램 및 소스 
  • 실제 인스턴스 생성과정 하나씩 짚어보기

NyPizza / Calzone / Pizza   Class다이어그램 및 소스

Pizza

 Pizza Class diagram

import java.util.*;
 
public abstract class Pizza{
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
    final Set<Topping> toppings;
    
    abstract static class Builder<extends Builder<T>> {
       EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
       
       // 하위 클래스 에서 공통으로 사용할 토핑 
       public T addTopping(Topping topping) {
          toppings.add(Objects.requireNonNull(topping));
          return self();
       }
 
       // Pizza 를 상속받은 클래스 ( NyPizza , Calzone ) return 
       // 메서드를 구현할 때 NyPizza 와 Calzome의 생성자를 호출하게 된다.
       // new NyPizza(this) , new Calzone(this);
       abstract Pizza build();
 
       // 하위 클래스는 이 메서드를 재정의하고 this를 반환하도록 해야 한다.
       protected abstract T self();
    }
 
    // 저장된 토핑이 build()를 호출할 때 각각 생성자에서 super(builer)를 호출하고
    // super는 Pizza의 생성자 이므로 아래의 Pizza가 호출 되면서 토핑을 clone()해서
    // 저장한다.
    Pizza(Builder<?> builder) {
       toppings = builder.toppings.clone();
    }
    
    // 실제 토핑값을 찍어보기 위한 toString() 재정의
    @Override
    public String toString() {
      Iterator iterator = toppings.iterator();
      String returnStr = "";
      while(iterator.hasNext()) {
         returnStr += iterator.next() + ", ";
      }
      return returnStr;
    }
 }
 

Pizza 는 추상 클래스로서 해당 클래스를 상속 받으면 토핑을 추가하고 빌더 패턴으로 인스턴스를 생성할 수 있는 abstract static class Builder<T extends Builder<T>> 와 해당 Builder를 인자로 받는 생성자를 가지고 있다.

Pizza를 상속받는 클래스에서 어쩔수 없이 지키게 되는 규칙들

1. Pizza의 생성자는 기본생성자가 없다. Builder<?>를 인자로 넘겨주는 생성자만 존재한다.

2. 1번의 이유로 Pizza를 상속받은 클래스는 자기 자신의 생성자에서 꼭 부모인 Pizza의 Builder 클래스를 전달해야된다.

3. 2번의 이유로 Pizza를 상속받은 자식 클래스 에서는 부모의 Builder 클래스를 상속받는 또다른 Builder클래스를 각각 만들게 된다.

NyPizza

NyPizza Class diagram

import java.util.Objects;
 
public class NyPizza extends Pizza {
    public enum Size { SMALL, MEDIUM, LARGE }
    private final Size size;
 
    public static class Builder extends Pizza.Builder<Builder> {
       private final Size size;
 
       // NyPizza 는 size를 받는 생성자만 존재함.
       public Builder(Size size) {
          super();
          this.size = Objects.requireNonNull(size);
       }
 
       @Override public NyPizza build() {
          return new NyPizza(this);
       }
 
       @Override protected Builder self() { return this; }
    }
 
    private NyPizza(Builder builder) {
       super(builder);
       size = builder.size;
    }
 }

NyPizza 는 Pizza 를 상속받고 내부에 static Builder 클래스는 부모(Pizza) 의 abstract static Builder를 상속받아서 self() 와 build() 를 @Override 한다.

아래 Calzone 도 NyPizza와 결국에는 똑같이 Builder를 상속 받아야 된다. (다만 재정의 하는 매소드의 내용은 당연히 this로서 자기 자신을 사용한다.)

그리고 자기 자신에게 맞게 각자 만들어서 사용하면 된다. 예) 자신의 맴버인 Size를 선언해서 set하고 있다.

Calzone

Calzone Class diagram

public class Calzone extends Pizza {
    private final boolean sauceInside;
 
    public static class Builder extends Pizza.Builder<Builder> {
       private boolean sauceInside = false;
 
       // Calzone 은 기본 생성자만 있음
       public Builder() {
          super();
       }
 
       // sauceInside 는 옵션 
       public Builder sauceInside() {
         sauceInside = true;
         return this;
      }
 
       @Override public Calzone build() {
          return new Calzone(this);
       }
 
       @Override protected Builder self() { return this; }
    }
 
    private Calzone(Builder builder) {
       super(builder);
       sauceInside = builder.sauceInside;
    }
 
 
 }

Calzone 은 Pizza 를 상속받고 내부에 static Builder 클래스는 부모(Pizza) 의 abstract static Builder를 상속받아서 self() 와 build() 를 @Override 한다.

 

NyPizza와 다른점은 생성자는 기본 생성자를 사용하고 Size가 아닌 sauceInside 라는 boolean 변수를 가지고 sauceInside() 를 호출하면 true로 변경된다.

 

실제 인스턴스 생성과정 하나씩 짚어보기

객체 생성예제

public class Test {
 
    public static void main(String[] args) {
      
        NyPizza pizza = new NyPizza.Builder(NyPizza.Size.SMALL)
                .addTopping(Pizza.Topping.SAUSAGE)
                .addTopping(Pizza.Topping.ONION)
                .build();
  
        Calzone calzone = new Calzone.Builder()
                .addTopping(Pizza.Topping.HAM)
                .sauceInside()
                .build();
                
                System.out.println(pizza.toString());
                System.out.println(calzone.toString());
    }
}
 

1. new NyPizza.Builer(NyPizzaSize.SMALL) : NyPizza의 생성자는 private이므로 꼭 내부 Builder를 호출하고 build() 메소드를 호출할 때 자기 자신을 생성하는 new NyPizza(this)를 호출할 수 있기 때문에 NyPizza.Builer(NyPizzaSize.SMALL) 를 호출해서 NyPizza를 만들 준비를 한다.

 

2. .addTopping(Pizza.Topping.SAUSAGE) : 해당 메서드는 부모에서 가지고 있는 공통 메서드로서 모든 피자는 토핑이라는 기능이 필요하기 때문에 미리 미리 구현을 해놨고 재정의 할 필요 없이 사용하고 있다. 

 

Pizza.Builder abstract static 클래스의 메소드 addTopping

// 하위 클래스 에서 공통으로 사용할 토핑 
       public T addTopping(Topping topping) {
          toppings.add(Objects.requireNonNull(topping));
          return self();
       }

T는 각각 상속받는 자식 클래시의 내부 Builder가 된다.

(NyPizza의 내부 static class Builder extends Pizza.Builder<Builder>   <== 제네릭에 들어가는 Builder 는 NyPizza의 Builder 이다. 헷갈리지 말자.

 

addTopping은 return self()를 하고 있는데 잘보면 return 타입이 T로 되어있기 때문에 결국 상속받은 자기 자신의 Builder를 return 하도록 구현해야 된다. 그래서 아래처럼 구현됨.

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

 

이렇게 this(여기에 this는 NyPizza.Builder 클래스 이다.)를 return 하기 때문에 객체를 생성할 때 우리는 체이닝 기법을 사용해서 추가하고 싶은 토핑을 연달아서 추가할 수 있는 것이다.

 

3. .build() : NyPizza를 생성할 수 있는 new NyPizza(this) 를 호출해서 자기 자신의 생성자를 호출한다. 이때 this는 NyPizza의 내부 static class Builder이고 우리는 Builder에 Size와 사용할 토핑을 체이닝 방식으로 저장해 뒀기 때문에 this를 넘겨서 자신의 NyPizza() 생성자에서 super(this); 를 통해서 토핑을 저장할 수 있게 된다.

 

조금 어렵다면 Pizza 클래스를 다시 보길 바란다. Pizza는 아래의 enum 과 toppings를 가지고 있다. 

public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }

final Set<Topping> toppings;

 

우리가 super(this) 를 하게되면 세팅된 토핑들이 Pizza의 toppings로 들어가게 된다. 그래서 우리가 각각 toString()사용할 때 저장된 토핑들이 return되고 Print되는 부분을 확인할 수 있다.

 

Calzone의 생성 과정은 각자 알아서 하나씩 따라가 보자! (NyPizza와 매우 비슷하다)

마무리

 

정리한다고 해봤는데... 과연 누군가를 이해시킬수 있을지 의문이다. 시간이 된다면 조금더 쉽게 설명할 수 있도록 여러가지 방법응 찾아서 시도해 봐야겠다.