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

    정적 팩터리와 생성자에 선택적 매개변수가 많을 때 고려할 수 있는 방안

    public class NutritionFacts {
        private final int servingSize; // Required
        private final int servings; // Required
        private final int calories; // Option
        private final int fat; // Option
        private final int sodium; // Option
        private final int carbohydrate; // Option
    }

    위 클래스처럼 많은 필드를 가지는 클래스가 있다고 가정해보자. 이 중 어떤 필드는 필수지만, 어떤 필드는 선택적이다. 이런 클래스가 있을 때, 인스턴스를 생성한다면 어떤 형태로 생성하는 것이 좋을까?  

    • 대안1 : 점층적 생성자 패턴 또는 생성자 체이닝 (나이브한 선택 방법)
      • 매개변수가 늘어나면 클라이언트 코드를 작성하거나 읽기 어렵다. 
    • 대안2 : 자바빈즈 패턴
      • 완전한 객체를 만들려면 메서드(Setter)를 여러 번 호출해야 한다. (일관성이 무너진 상태가 될 수도 있다.)
      • 클래스를 불변으로 만들 수 없다.  (Setter가 열려있기 때문임) 

    인스턴스를 생성할 때, 필수적으로 필요한 필드는 생성자를 통해서 만드는 것이 좋다. 필수적인 필드를 강제하기 때문에 적절한 상태가 셋팅된다. 생성자를 이용해서 인스턴스를 생성할 때 옵셔널한 필드도 줄 수 있지만, 이 경우 코드가 굉장히 지저분해진다.        

     

    대안 1 : 생성자 체이닝 사용하기

    지저분한 코드를 줄이기 위해서 사용하는 방법 중 하나는 생성자 체이닝(점층적 생성자)가 있다.

    • 장점 : 코드의 반복을 줄여준다.
    • 단점 :
      • 매개변수가 많아진다면 인텔리제이의 도움 없이는 어떤 값이 어디로 가는지 정확히 알지 못한다. 

     

    // After 코드 (생성자 체이닝 사용)
    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }
    
    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0);
    }
    
    public NutritionFacts(int servingSize, int servings, int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    }
    
    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }
    
    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }
    
    public NutritionFacts(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
            this.calories = 0;
            this.fat = 0;
            this.sodium = 0;
            this.carbohydrate = 0;
        }
    
    // Before 코드
    public NutritionFacts(int servingSize, int servings, int calories) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = 0;
        this.sodium = 0;
        this.carbohydrate = 0;
    }
    
    public NutritionFacts(int servingSize, int servings, int calories, int fat) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = 0;
        this.carbohydrate = 0;
    }
    
    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = 0;
    }

     

     

    자바빈즈 패턴 - 일관성이 깨지고, 불변으로 만들 수 없다. 

    Java Beans는 자바 표준 스펙 중 하나다. Getter / Setter를 정의하고 Getter / Setter 이름이 어떠하게 표현되는지를 정의한 것이다. 이 규약을 통해서 개발자는 잘 알려진 방법으로 인스턴스를 생성하고 얻고 값을 설정할 수 있다. 많은 매개변수가 있을 때 Java Beans를 도입하면 객체 생성이 굉장히 간단해진다는 장점이 있다. 왜냐하면 인텔리제이의 기능만 사용해도 자동으로 필요한 Java Beans 코드가 생성되기 때문이다. 

    public class NutritionFacts {
        private int servingSize = -1; // Required
        private int servings = -1; // Required
        private int calories = 0; // Option
        private int fat = 0; // Option
        private int sodium = 0; // Option
        private int carbohydrate = 0; // Option
    
        public NutritionFacts() {
        }
    
        public void setServingSize(int servingSize) {
            this.servingSize = servingSize;
        }
    
    	...
    
        public int getServingSize() {
            return servingSize;
        }
    
        public int getServings() {
            return servings;
        }
        ...
    }

    Java Beans의 단점은 두 가지가 존재한다.

    • 필요한 모든 상태를 적절히 셋팅하지 못할 수 있다.
    • 필수 필드가 존재할 때, Setter를 이용해서 어떤 필드까지 셋팅해줘야 하는지 알 수 없다. 
    // Required : sodium, Fat, calories
    NutritionFacts nutritionFacts = new NutritionFacts();
    nutritionFacts.setServings(1);

    예를 들어서 다음과 같이 생성자로 객체를 생성하고 필요한 값을 셋팅하는 경우를 상상해보자. 이 때 필수 필드가 sodium, Fat, Calories라고 상상해보자. 그런데 setter를 이용해서 객체를 생성하던 도중 나머지 필드를 깜빡했다고 하면, 이 부분은 런타임에 올 때 까지 잡아낼 수가 없게 된다. 

     

    대안1 + 대안 2 : 필수 생성자 + Setter 조합

    기본 생성자 + Setter 조합으로 할 경우, 필수적인 필드를 빼먹을 수 있다는 단점이 존재한다. 이 부분을 보완하기 위해 필수적인 필드들은 생성자로 전달하고, 선택적인 값은 Setter를 이용해서 셋팅하는 방법도 존재한다. 예를 들면 아래 코드처럼 할 수 있다.

    // Required : sodium, Fat, calories
    NutritionFacts nutritionFacts = new NutritionFacts(1, 1, 1);
    nutritionFacts.setServings(1);

    하지만 이것 역시 한 가지 단점이 존재한다. 불변(immutable) 객체를 만들기 어렵다는 것이다. 불변 객체는 한번 셋팅한 값이 변하지 않는 객체다. 그렇지만 Setter가 public으로 열려있기 때문에 손쉽게 값을 수정할 수 있다. 따라서 이 방법은 불변 객체를 생성하기 어렵다. 

     

    빌더 패턴 

    많은 매개변수를 가진 클래스가 있다면 인스턴스를 생성하기 위해서 빌더를 고려하는 방법도 존재한다. 수동으로 빌더 패턴을 이용하려고 할 때는 다음 형태로 구현하면 좋다.

    • 빌더의 생성자에는 필수값을 받는다. 
    • 선택적인 값은 메서드 체이닝 기능을 제공해서 처리한다.  메서드 체이닝을 위해서 빌더 자신을 리턴한다. 
    • build() 메서드를 이용해서 인스턴스를 생성한다. 

    빌더 패턴의 장단점을 살펴보면 다음과 같다.

    • 장점 
      • 인스턴스를 생성할 때, 필수 필드값을 지정할 수 있다.
      • Builder로 인스턴스를 생성할 때만 값을 설정할 수 있게 된다. 즉, 불변 객체를 손쉽게 만들 수 있다.
    • 단점
      • 빌더는 코드를 이해하기 어렵게 만든다.
      • 빌더를 만들기 위해서 필요한 코드의 양도 많고 중복되는 양도 많다.

    빌더 패턴은 위와 같이 장단점이 명확하기 때문에 항상 빌더를 사용하는 것이 좋은 것은 아니다. 따라서 필요한 시점에만 빌더 패턴을 사용하는 것을 추천한다.  그렇다면 빌더를 사용하면 좋은 시점은 언제일까?

    필수적 / 선택적인 필드가 존재함 &&  매개변수가 너무 많아서 생성자가 늘어남  && 불변 객체를 생성하고 싶을 때

    위와 같은 상황일 때 빌더를 사용하는 것을 추천한다. 

     

    아래는 빌더를 이용한 예시다.

    public class NutritionFacts {
        private final int servingSize; // Required
        private final int servings; // Required
        private final int calories; // Option
        private final int fat; // Option
        private final int sodium; // Option
        private final int carbohydrate; // Option
    
        
        // 빌더 클래스
        public static class Builder {
            private final int servingSize; // Required
            private final int servings; // Required
            private int calories = 0; // Option
            private int fat = 0; // Option
            private int sodium = 0; // Option
            private int carbohydrate = 0; // Option
    
    		// 필수값은 빌더 생성자에 전달한다. 
            public Builder(int servingSize, int servings) {
                this.servings = servings;
                this.servingSize = servingSize;
            }
    
            public Builder calories(int calories) {
                this.calories = calories;
                return this; // 메서드 체이닝을 위해서 빌더 자신을 리턴한다.
            }
    
            
    		...
    
    		// 인스턴스 생성 → 빌더 자신을 넘겨준다. 
            public NutritionFacts build() {
                return new NutritionFacts(this);
            }
        }
    
    	// 인스턴스 생성자는 빌더 객체를 받아서 인스턴스 값을 셋팅하고 인스턴스를 생성한다. 
        private NutritionFacts(Builder builder) {
            this.calories = builder.calories;
            this.fat = builder.fat;
            this.sodium = builder.sodium;
            this.servingSize = builder.servingSize;
            this.servings = builder.servings;
            this.carbohydrate = builder.carbohydrate;
        }
    
        public static void main(String[] args) {
            NutritionFacts cocaCola = new Builder(240, 8)
                    .calories(100)
                    .sodium(35)
                    .carbohydrate(27)
                    .build();
        }
    
    
    }

     

     

    빌더 패턴 - Lombok의 @Builder 어노테이션

    Lombok은 어노테이션 프로세서를 이용해서 컴파일 시점에 손쉽게 Builder 패턴과 관련된 코드를 생성해준다. 하지만  Lombok은 단점도 함께 제공한다.

    • @builder가 붙으면 Lombok은 모든 매개변수가 있는 생성자를 기본적으로 생성한다. 
      • 이 부분은 @AllArgsConstructor(access = private)로 극복할 수 있다.
    • Lombok으로 Builder를 생성하면, 생성 시에 필수값을 셋팅할 수 없다.
      • 이 부분은 극복할 수 없다. 따라서 이런 상황에서는 Lombok을 사용하지 않거나, 잘 알고 사용해야한다. 

     

     


    계층형 빌더

    계층형 빌더는 self()라는 재귀적 장치를 만들어 Builder Factory 계층 구조를 재활용 할 수 있는 구조다. 계층형 빌더는 크게 다음과 같은 형태로 작성한다.

    • 추상 클래스는 추상 빌더를 가지고, 구체 클래스는 구체 빌더를 갖게 한다.
    • self()라는 재귀적 장치를 만들어 구체 빌더를 반환해 메서드 체이닝 할 수 있도록 한다. 

    self() 메서드는 추상 메서드로 만들고, 구체 클래스에서 이 메서드를 구현한다. 구체 클래스에서는 이 메서드를 통해 자기 자신(하위 계층의 빌더)를 반환하도록 한다. 이런 형태로 작성하게 된다면 다음이 기대된다.

    • 공통적으로 사용하는 로직은 추상 클래스 + 추상 빌더에서 처리하며 코드 중복을 줄일 수 있다.
    • self()를 통해서 클라이언트는 형변환을 신경 쓰지 않고 구체 클래스를 사용할 수 있다. 
    public abstract class Pizza {
        public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
    
        final Set<Topping> toppings;
    
        // Generic T 선언. 이 때 T는 Builder를 상속받은 클래스임. 
        abstract static class Builder<T extends Builder<T>> {
            EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
    
            // 공통 로직을 가짐. T를 반환함. 즉, 구체 Builder를 반환함.
            public T addTopping(Topping topping) {
                toppings.add(topping);
                return self();
            }
    
            // Pizza를 반환함. 구체 클래스 타입 반환으로 오버라이딩 가능. 
            abstract Pizza build();
            
            // 이 장치를 통해 T(구체 빌더)를 반환함. 
            protected abstract T self();
        }
    
        public Pizza(Builder<?> builder) {
            toppings = builder.toppings.clone();
        }
    }
    • 추상 Builder는 제네릭 T를 선언한다. 이 때, T는 추상 Builder를 상속받은 클래스다. 즉, 구체 빌더가 된다.
    • addTopping()의 결과로 T(구체 빌더)를 반환한다.
    • build()의 결과로 Pizza 타입을 반환한다. 구체 클래스 타입(NyPizza 등)의 반환으로 오버라이딩 가능하다.
    • self() 메서드로 T(구체 빌더)를 반환한다. 

    먼저 Pizza 추상 클래스를 선언했다. 공통된 부분은 Pizza 추상 클래스가 사용하고, 구현체는 각각의 특수한 부분만 구현한다. 아래에는 Pizza 추상 클래스를 구현한 CalzonePizza 클래스다. 

    public class CalzonePizza extends Pizza{
        private final boolean sauceInside;
    
        // static 클래스로 선언해야 사용가능함.
        // Builder는 자기 자신, Pizza.Builder는 Pizza 클래스에 선언되 Builder 클래스. 
        public static class Builder extends Pizza.Builder<Builder> {
    
            // 구체 클래스만의 필드
            private boolean sauceInside;
    
            // 구체 클래스만의 메서드
            // Self로 자기 자신(구체 Builder) 반환해서 메서드 가능함.
            public Builder sauceInside() {
                this.sauceInside = true;
                return self();
            }
    
            // CalzonePizza를 생성하도록 한다. 그래야 Pizza 대신 가능해짐. 
            @Override
            CalzonePizza build() {
                return new CalzonePizza(this);
            }
    
            // 자기 자신(구체 빌더)를 반환함.
            @Override
            protected Builder self() {
                return this;
            }
        }
    
        public CalzonePizza(Builder builder) {
            super(builder);
            this.sauceInside = builder.sauceInside;
        }
    }
    
    • 구체 빌더를 Builder로 선언한다. Pizza.Builder는 추상 빌더를 의미한다.
    • sauceInside(), 추상 클래스의 addToppings()는 self()를 반환한다. 이 때, self()는 구체 Builder 자기 자신이다. 즉, 추상 클래스의 addTopping이 호출되든, sauceInside()가 호출되든 하위 구체 빌더가 반환되기 때문에 메서드 체이닝이 가능하다. 
    • build()할 때 Pizza 대신 하위 구현체인 CalzonePizza를 반환한다. 이 덕분에 클라이언트(이 클래스의 사용자)는 Builder.build()의 타입을 염두하지 않고(NyPizza 든, CalzonePizza든 build 메서드 호출 시점에 자동으로 강제됨) 사용할 수 있다. 

    따라서 계층형 빌더를 구현하면 아래와 같이 사용할 수 있게 된다.

    public static void main(String[] args) {
        NyPizza pizza1 = new NyPizza.Builder(NyPizza.Size.M)
                .addTopping(Pizza.Topping.MUSHROOM)
                .addTopping(Pizza.Topping.ONION)
                .build();
    
        CalzonePizza pizza2 = new CalzonePizza.Builder()
                .addTopping(Pizza.Topping.ONION)
                .addTopping(Pizza.Topping.HAM)
                .sauceInside()
                .build();
    }
    • 계층형 빌더를 사용하고, 최종 build() 하더라도 어떤 타입인지 신경 쓰지 않아도 된다.
    • 공통된 기능은 하위 빌더에서 재선언하지 않아도 된다. 

    계층형 빌더 구조에서는 self()를 이용해서 자기 자신을 리턴한다. 계층형 빌더가 아닐 때는 self()가 아닌 this를 리턴해서 메서드 체이닝을 사용했었다. 하지만 상속구조(계층형 빌더)가 되면, self()를 이용해서 구체 빌더를 리턴하는 방식이 된다. this(PizzaBuilder)를 반환하게 되면 문제가 있다. 

    CalzonePizzaBuilder는 sauceInside 필드를 가진다. NyPizzaBuilder는 Size 필드를 가진다. 만약 this를 반환해서 PizzaBuilder가 반환된다면, 하위 빌더(구체 빌더)의 Private한 필드를 사용하지 못한다는 문제가 있다. 이 문제를 해결하기 위해서 self() 메서드를 만들어 하위 타입(구체 빌더)를 리턴해서 사용하는 것이다. 


    완벽공략 : 생성자에 매개변수가 많다면 빌더를 고려하라. 

    • p15. 자바빈즈, 게터, 세터
    • p17. 객체 얼리기 (freezing)
    • p17. 빌더 패턴
    • p19. IllegalArgumentException
    • p20. 재귀적인 타입 한정을 이용하는 제네릭 타입
    • p21. 가변인수 (varargs) 매개변수를 여러 개 사용할 수 있다.

     

     

    완벽 공략 6. 자바빈(JavaBean) 이란?

    재사용 가능한 GUI를 제공하기 위한 스펙을 의미한다. GUI를 사용하는 것이 아니라면 굳이 자바빈 규약을 준수할 필요는 없다.

    자바빈이 지켜야 할 규약

    Argument 없는 기본 생성자

    Getter / setter 메서드 이름 규약

    자바빈 스펙에 준하는 이름 규약으로 앞으로 getter / setter를 만들게 될 것이다. GUI를 위한 것은 아니다. 다만 요즘에 자주 사용되는 JPA, 스프링 등이 리플렉션을 통해 객체의 값을 조회 / 수정할 때 자바빈 규약으로 주로 접근하기 때문이다. 

    Serializable 인터페이스 구현

    하지만 실제로 오늘 날 자바빈 스펙 중에서도 getter와 setter가 주로 쓰는 이유는?

    JPA, 스프링 같은 여러 프리엠워크에서 리플렉션을 통해 특정 객체의 값을 조회하거나 설정하기 때문이다. 여러가지 툴들이 각각 접근하기 때문에 공통된 규약으로 getter / setter가 만들어져야 한다. 

    기본 생성자는 왜 있어야 할까? 그래야 객체를 만들기 편하기 때문이다. 리플렉션을 통해서 특정 객체를 만들고 값을 주입하는데, 리플렉션은 생성자를 통해서 객체를 만든다. 그런데 생성자에 argument가 있는 경우 만들기가 굉장히 번거로워진다. 따라서 기본 생성자로 객체를 만들고 값을 셋팅하는 형태가 된다. 

    Serializable 인터페이스를 왜 구현해야할까? 나중에 다시 읽어서 수정하거나 재사용하는 객체가 있다. 그럴려면 객체를 저장 가능한 형태로 만들어야 한다. 객체를 그 상태 그대로 저장했다가, 그 상태 그대로 복원되는 기능이 필요할 것이다. 그래서 Serializable을 구현하라고 한다. 그래야 직렬화 / 역직렬화가 잘 될 것이기 때문이다.

     

     

    boolean 타입의 getter / setter는 어떻게 만들어져야 자바빈 규약에 준하는 것일까? isHealthy() 같은 형태로 사용된다.

     

     

     

    완벽 공략 6. 빌더패턴?

    빌더를 디자인 패턴의 시각으로 바라보면 다음과 같이도 사용할 수 있다. 

    디자인 패턴은 디자인의 목적을 주로 바라본다. 그렇다면 빌더 패턴의 목적은 무엇일까?

    빌더 패턴의 목적
    어떠한 객체를 만드는 과정이 복잡한 프로세스를 별도의 클래스로 분리시키는 것이다. 원래 가지고 있던 코드의 양도 분리를 해서 줄일 수 있고, 단일 책임 원칙(SRP)을 적용해서 객체를 생성하는 과정을 별도의 클래스로 분리시킬 수 있는 장점이 있는 패턴이다.

    빌더 패턴을 구현하는 수단으로서는 빌더 인터페이스 / 빌더 구현체 / Director라는 개념이 있을 수 있다. 디자인 패턴은 목적이 중요한 것으로 수현과 구현 형태는 다를 수 있다는 점을 미리 참고하자. 

    디렉터 클래스의 역할은 다음과 같다. 빌더를 통해 만들어지는 객체들 중에 자주 만들어지는 객체들이 존재한다면, 디렉터는 빌더를 내부적으로 가지고 이것을 Deligation해서 생성해주는 역할을 한다.

    Director가 존재하면서 Builder 객체들의 메서드 체이닝의 코드를 줄이는 역할을 할 수가 있다. 예를 들어 아래의 cancunTrip()이 여러 군데에서 사용된다고 가정해보자. 그리고 cancuTrip에 addPlan() 메서드가 하나 더 들어온다면 여러군데에서 전부 수정해야한다. 하지만 cancunTrip을 빌더로 생성하는 곳을 하나로 정해둔다면 변경점도 적어진다. 

    public class TourDirector {
    
        private TourPlanBuilder tourPlanBuilder;
    
        public TourDirector(TourPlanBuilder builder) {
            this.tourPlanBuilder = builder;
        }
    
        public TourPlan cancunTrip() {
            return tourPlanBuilder.title("칸쿤 여행")
                    .nightsAndDays(2, 3)
                    .startDate(LocalDate.of(2020, 12, 9))
                    .whereToStay("리조트")
                    .addPlan(0, "체크인하고 짐 풀기")
                    .addPlan(0, "저녁 식사")
                    .getPlan();
        }
    
        public TourPlan longBeachTrip() {
            return tourPlanBuilder.title("롱비치")
                    .startDate(LocalDate.of(2021, 7, 15))
                    .getPlan();
        }
    }

    나머지 구조는 다음과 같이 정리할 수 있다.

    // 인터페이스
    public interface TourPlanBuilder {
    
        TourPlanBuilder nightsAndDays(int nights, int days);
    
        TourPlanBuilder title(String title);
    
        TourPlanBuilder startDate(LocalDate localDate);
    
        TourPlanBuilder whereToStay(String whereToStay);
    
        TourPlanBuilder addPlan(int day, String plan);
    
        TourPlan getPlan();
    
    
    }

    위의 코드는 빌더 클래스의 인터페이스다. 이 클래스의 구체 빌더를 만들어야 한다.

    public class DefaultTourBuilder implements TourPlanBuilder{
    
    
        private int nights;
        private int days;
        private String title;
    
        private LocalDate startDate;
        private String whereToStay;
        private List<DetailPlan> plans;
    
    
        @Override
        public TourPlanBuilder nightsAndDays(int nights, int days) {
            this.nights = nights;
            this.days = days;
            return this;
        }
    
        @Override
        public TourPlanBuilder title(String title) {
            this.title = title;
            return this;
        }
    
        @Override
        public TourPlanBuilder startDate(LocalDate localDate) {
            this.startDate = localDate;
            return this;
        }
    
        @Override
        public TourPlanBuilder whereToStay(String whereToStay) {
            this.whereToStay = whereToStay;
            return this;
        }
    
        @Override
        public TourPlanBuilder addPlan(int day, String plan) {
            if (this.plans == null) {
                this.plans = new ArrayList<>();
            }
    
            this.plans.add(new DetailPlan(day, plan));
            return null;
        }
    
        @Override
        public TourPlan getPlan() {
            return new TourPlan(title, startDate, nights, days, plans, whereToStay);
        }
    }

    빌더 인터페이스의 구현체다.

    public class TourPlan {
    
        private String title;
        private LocalDate startDate;
    
    
        private int nights;
        private int days;
    
        private List<DetailPlan> detailPlans;
        private String whereToStay;
    
        public TourPlan(String title, LocalDate startDate, int nights, int days, List<DetailPlan> detailPlans, String whereToStay) {
            this.title = title;
            this.startDate = startDate;
            this.nights = nights;
            this.days = days;
            this.detailPlans = detailPlans;
            this.whereToStay = whereToStay;
        }
    }

    Tour와 관련된 도메인 클래스다. 빌더 클래스는 Tour 도메인 클래스를 '잘' 생성하기 위해서 빌더 클래스를 도입했다. 

     

     


    완벽공략 9. IllegalArguementException

    IllegalArgumentException은 잘못된 인자를 넘겨 받았을 때 사용할 수 있는 기본 런타임 예외다. 이런 런타임 예외는 프로그래밍적인 에러고 복구할 수 있는 방법이 없기 때문에 CheckedException이 아닌 것이다. 

     

    질문1. Checked Exception과 unchecked Exception의 차이

    Unchecked Exception은 예외를 위로 던지거나 잡아서 처리하는 것이 강제되지 않는다. 경우에 따라서 메서드에 표현할 수도 있긴 하지만 기본적으로는 프로그래머가 던지지 않아도 되고, 던져지는 것을 신경쓰지 않아도 된다. 왜냐하면 프로그래밍적 문제이기 때문에 복구할 수 있는 방법이 없기 때문이다.

    CheckException은 예외를 위로 던지거나 잡아서 처리를 해줘야한다. 그렇게 하지 않으면 컴파일 할 수 없다. CheckedException은 복구가 가능한 상황이라고 가정을 하기 때문에 컴파일 단계에서 체크를 하라고 한다.

    IllegalArguementException을 던진다면, 최소한 어떠한 Argument가 잘못되었는지를 함께 알려주는 것이 좋다. 예외만 던지면 '왜' 잘못되었는지를 알 수 없다. 

    public void updateDeliveryDate(LocalDate deliveryDate) {
        if (deliveryDate.isBefore(LocalDate.now())) {
            // TODO : 과거로 배송을 해달라고?
            throw new IllegalArgumentException("deliveryDate can't be earlier than" + LocalDate.now());
        }
    }

     

     

    질문 2. 간혹 메서드 선언부에 unchecked Exception을 선언하는 이유?

    public void updateDeliveryDate(LocalDate deliveryDate) throws IllegalArgumentException {
    ...
    }

    RuntimeException은 굳이 Throw를 하지 않아도 되는데 메서드에 Throw를 하는 경우가 있다. 이것은 굳이 명시하지 않아도 되지만 API를 사용하는 클라이언트에게 명시적으로 직접 알려주고 싶을 때 명시한다. 

    그렇다면 수많은 UnchekedException을 모두 메서드에 표시하지 않는 이유는 무엇일까? 그 많은 Exception을 메서드에 표현하는 것 자체가 코드의 가독성을 떨어뜨린다. 따라서 일반적으로는 필수적으로 표기해야하는 CheckedException만 표기한다. 

     

    질문3. Checked Exception은 왜 사용할까?

    런타임 예외는 원하면 캐치를 해서 작업을 할 수도 있고, 원하지 않으면 무시할 수도 있다. 그래서 사용하기 편한데, CheckedException은 강제적으로 되어 사용하기 어렵다. 그런데 왜 CheckedException을 사용하는 것일까?  Checked Exception을 사용하는 것은 '이 에러가 발생했을 때, 클라이언트가 이에 맞은 후속 작업을 해주기를 원할 때' 사용한다.

    즉, CheckedException을 잡아서 Retry를 하는 로직을 사용할 수 있다. 혹은 클라이언트에게 적절한 에러 메세지를 내려줄 수도 있다. 이런 형태로 CheckedException은 복구 가능한 에러거나 이 에러를 가지고 무언가를 대응해주길 원할 때 사용한다. 

     


    완벽공략 10. 가변인수

    생성자로 누릴 수 없는 사소한 이점으로 빌더를 이용하면 가변 인수(varargs) 매개변수를 여러 번에 나눠서 사용할 수 있다. 생성자는 한번의 가변 인수만 사용할 수 있는 대신, 메서드로 체이닝하는 빌더는 여러 메서드에 여러번 가변 인수를 매개변수로 넘겨주는 작업을 할 수 있다는 것이다. 

    가변인수는 여러 인자를 받을 수 있는 가변적인 argument (Var + args)

    • 가변인수는 메서드에 오직 하나만 선언할 수 있다.
    • 가변인수는 메서드의 가장 마지막 매개변수가 되어야 한다.
    public void printNumbers(int... numbers) {
    
        // 실제 어떤 타입인지를 출력한다. (numbers가 어떤 타입인지)
        System.out.println(numbers.getClass().getCanonicalName());
    
        // 배열이 가지고 있는 타입을 의미한다. 
        System.out.println(numbers.getClass().getComponentType());
        Arrays.stream(numbers).forEach(System.out::println);
    }
    • 가변인수는 기본적으로 위와 같이 사용된다. 

    책에는 빌더를 이용하면 가변인수 파라메터를 여러 개 사용할 수 있다고 한다. 이것은 빌더가 가진 많은 메서드에서 가변인수를 각각 사용할 수 있다는 것을 의미한다. 사실 내가 생각했을 때는 빌더에 가변 인수를 나누는 것이 크게 의미는 없어보이지만, 아래 의도를 가진다고 한다. 

    public class Nutritions{
        public Builder calories(int... calories) {
            ...
        }
    
        public Builder fat(int... fat) {
            ...
        }
    
        public Builder sodium(int... sodium) {
            ...
        }
    
    }

     

     

     

     

     

     

    과제

     

     

     

    Serializable

    객체의 직렬화 / 역질렬화 하는 이유는 다른 곳에 저장하고 다시 읽어서 쓰겠다는 의도가 있다.

    댓글

    Designed by JB FACTORY