[Effective Java 3/E] 2장 객체 생성과 파괴

대연.

·

2021. 2. 22. 00:25

ITEM 1 Consider Static Factory Methods

  • Static factory method(정적 팩터리 메서드)

    • 생성자 없이 메서드를 통해 객체를 생성하는 것

    • ! : 디자인 패턴에서 일치하는 패턴은 없다. 책의 고유한 패턴.

    •  //example : valueOf in Boolean box class
       public static Boolean valueOf(boolean b){
             return b ? Boolea.TRUE : Boolean.FALSE;
       }
    • Advantages

      1. 생성자와 다르게 이름을 가질 수 있다.

        • 생성자의 파라미터로는 반환될 객체의 특성을 코드 작성자가 알기 힘들다.
        • 그러나 정적 팩터리 메서드는 이름을 잘 지어놓으면 코드를 읽는 사람이 반환될 객체의 특성을 쉽게 이해할 수 있다.
        • DONOT : 하나의 시그니처로 하나의 생성자를 만들 수 있는데, 이를 극복하기 위해 파라미터 순서를 바꾼 생성자를 추가하는 것을 권하지 않는다. 기억하기 어려워 개발자가 엉뚱한 생성자를 호출하도록 실수를 유도한다.
        • DO : 정적 팩터리 메서드의 이름을 잘 지어 생성자의 역할을 대신하도록 한다. 개발자는 정적 팩터리 메서드를 통해서 어떤 특징의 객체를 반환받을 지 조금 더 명확하게 알 수 있다.
      2. 정적 팩터리 메서드를 호출하기 위해서 객체를 만들 필요가 없다.

      3. 반환 타입의 하위 타입 객체를 반환할 수 있다.

      4. 입력 매개변수에 따라 다른 클래스의 객체를 반환할 수 있다.

        • 다형성(Polymorphism)에 기반하여, 해당 클래스를 상속하여 하위 클래스를 구현하고 이를 리턴할 수 있다.

        • 유지보수 측면에서, 아래의 예시에서 만약 트럭 종류를 새로 추가하거나, 혹은 뺀다고 하더라도 호출하는 입장에선 어떤 타입의 객체가 반환되는지 신경쓰지 않기 때문에, 더 좋다.

          //1-3, 1-4 example
          public abstract class Truck {
          
            abstract void turnOn();
          
            static public Truck getInstance(boolean isRich) {
                        Car ret;
                if(isRich) {
                                ret = new CyberTruck();
                } else {
                    ret = new Porter();
                }
                return staticFactoryMethod;
                        //depend on an argument, type of returning instance
                        //can vary.
            }
          }
          class CyberTruck extends Truck{
            public void turnOn() {
                System.out.println("위이이이잉");
            }
          }
          
          class Porter extends Truck{
            public void turnOn() {
                System.out.println("푸르르르르러러ㅓㅓㄺ");
            }
          }
          
      5. 정적 팩터리 메서드를 작성하는 시점에서 반환할 객체의 클래스가 존재하지 않아도 된다.

        → 잘 모르겠어요..

    • disadvantage

      1. 상속을 하려면 public이나 protected 생성자가 필요합니다.

        • 만약 클래스 내에 어떠한 member없이 정적 팩터리 메서드만 달랑 있다면, 상속을 통해 하위 클래스를 만들 수 없습니다.
      2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.

        • 정적 팩터리 메서드는 JavaDoc에서 찾을 수 없어, 개발자가 직접 코드를 보거나 혹은 API 개발자가 별도의 문서작성을 해야 합니다.

        • 어떤 클래스를 사용하고자 할 때, API User는 일반적으로 new를 통해 인스턴스를 할당하고자 할 것이지만, 정적 팩터리 메서드는 new 대신에 메서드를 통해서 인스턴스를 반환하기 때문입니다.

        • 이러한 문제를 방지하기 위해 아래의 명명규칙을 따를 것을 책에서는 권장합니다.

            // from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
            Date d = Date.from(instant);
          
            //of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
            Set faceCards = EnumSet.of(JACK, QUEEN, KING);
          
            //valueOf : from과 of의 더 자세한 버전
            BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
          
            //Instance or getInstance : 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴임을 보장하지 않는다.
            StackWalker luke = StackWalker.getInstance(options);
          
            //create or newInstance : 위의 Inst/getInst와 같지만 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
            Object newArray = Array.newInstance(classObject, arrayLen);
          
            //getType : getInstance와 같으나 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다.
            FileStore fs = Files.getFileStore(path);
          
            //newType : newInstance와 같으나 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다.
            BufferedReader br = Files.newBufferedReader(path);
          
            //type : getType과 newType의 간결한 버전
            List litany = Collections.list(legacyLitany);

생성자 없이 메서드를 통해 객체를 생성하는 것

ITEM 2. Consider a builder when faced with many constructor parameters

  • 식품의 영양정보를 표현하는 클래스를 상상해보자. 굉장히 많은 항목들이 있는데, 일반적으로 대다수의 값이 0이다. (영양제가 아닌 이상에야..)

  • 아래 코드는 칼로리, 단백질, 지방, 탄수화물만을 담고 있는 영양 정보 클래스를 예시로 들었다. 4개 정도의 파라미터 정도는 아래 구현하지 말아야할 패턴으로도 쉽게 구현이 가능하지만, 추후 확장성을 고려한다면 빌더 패턴을 쓰는 것이 바람직하다고 생각된다.

  • DONOT : 점층적 생성자 패턴(telescoping constructor pattern)

    • 생성자를 작성할 때

      public class NutritionFacts{
            // member variables...
            public NutritionFacts(int cal){...};
            public NutritionFacts(int cal, int protein){...};
            public NutritionFacts(int cal, int protein, int ...){...};
            public NutritionFacts(int cal, int protein, int ..., int...){...};
      
      }
    • 영양정보를 표현하는 클래스에 칼로리/탄/단/지 표기만 하고, 생성자의 파라미터 순서도 위와 같다고 가정해보자.

      • 모든 음식에는 칼로리가 있을테니 이를 필수로 지정하고, 파라미터 1개짜리 생성자는 칼로리만을 매개변수로 받는 생성자를 호출해 인스턴스를 받는다. NutritionFacts(int cal);
      • 근데 만약 탄수화물/단백질이 0인 올리브유의 영양정보 클래스를 만들고 싶다면? NutritionFacts(int cal, int protein, int carbohydrate, int fat);을 사용하여야 할 것이고, 불필요하게 탄수화물/단백질에 0을 입력하여야 한다.
      • 매개변수가 4개가 아닌 수십개가 된다면? 만약 중간 순서에 있는 파라미터가 빠진다면? 모든 생성자를 수정해주어야 한다. 호출할 때도 수십개의 파라미터 순서가 뒤바뀐다거나 하는 문제가 발생하지만, 컴파일러는 문제를 찾을 수없다.
      • 매개변수 갯수가 맞는지 확인하는 것도 아주 귀찮은 작업이 될 것이다.
  • DONOT : 자바빈즈 패턴(JavaBeans Pattern)

    • 생성자 예시 코드

      public class NutritionFacts{
            private int cal = -1; // mandatory field. no default value
            private int protein = 0; 
            private int carbohydrate = 0; 
            // and other member variables...
      
            public void setProtein(int val) {protein = val;}
            public void setCarbohydrate(int val) {carbohydrate = val;}
            public void setFat(int val) {fat = val;}
      }
    • 파라미터가 20개인 클래스의 생성자를 20개씩 안 만들어도 된다. 만세!

    • 그러나 치명적인 문제는, 하나의 객체를 완성하기 위해 API User가 여러 개의 세터 메서드를 호출하여야 한다.

    • 일관성, Consistency 의 문제

      • 필요한 세터 메서드를 다 호출하기 전 까지는, 일관성(Consistency)가 무너진 상황이 된다.
      • 점층적 생성자 패턴에서는, 매개변수들이 유효한지 여부를 생성자에서만 확인하면 됐었지만, 자바빈즈에서는 이를 검증할 방도가 없거나 매우 까다롭다.
      • 일관성이 깨진 객체 때문에 발생하는 각종 버그들을 디버깅하기 어렵다. Thread safe를 달성하기 위해서 객체를 freezing 과정이 필요하며, 다루기도 어렵고 freeze 메서드를 호출해줬는지 컴파일러가 보증할 방법이 없어 런타임 오류가 발생할 가능성이 높다.
      • 클래스를 불변으로 만들수도 없다.
  • DO : Builder

    • 자바빈즈 패턴의 가독성과 점층적 패턴의 안전성을 합친 패턴이다.

    • Builder 객체를 static으로 선언하고, User는 builder 객체를 얻어서 세터메서드를 통해 원하는 선택 매개변수만을 설정한 뒤에, build() 메서드를 호출하여 객체를 얻는다. 일반적으론 불변 객체이다.

      ```java
      public class NutritionFacts {
      // 불변 객체를 설정하기 위한 final keyword
      private final int cal;
      private final int protein;
      private final int carbohydrate;
      private final int fat;

      // static으로 지정해주어야 NutritionFacts 객체를 생성하지 않고
      // Builder 객체를 얻을 수 있다.
      public static class NutriFactsBuilder {

      // mandatory fields
      private int calories = 0;
      
      // optional fields
      private int fat = 0; // 별도로 지정하지 않는다면 default value를 가진다.
      private int sodium = 0;
      private int carbohydrate = 0;
      
      public Builder(int cal) { // mandatory field만 생성자를 통해 설정한다.
         this.servingSize = servingSize;
         this.servings = servings;
      }
      
      public Builder setCal(int val) { cal = val; return this; }
      public Builder setFat(int val) { fat = val; return this; }
      public Builder setCarbohydrate(int val) { carbohydrate = val; return this; }
      public NutritionFacts build() { return new NutritionFacts(this);  }

      / /*

                세터 메서드는 빌더 자기자신을 반환하므로, 
                연쇄적으로 세터 메서드를 호출할 수 있다.
                builder.setCal(150).setFat(200).setCarbohydrate(100);
                이를 Fluent API 혹은 Method Chaining으로 부른다.
    • */

      }

      private NutritionFacts(Builder builder) {
      cal = builder.cal;
      fat = builder.fat;
      protein = builder.protein;
      carbohydrate = builder.carbohydrate;
      }
      }

    • 유효성 검사 코드는 빌더의 생성자나 세터에서 검사하고, build 메서드가 호출하는 생성자(NutritionFacts())에서 여러 매개변수에 걸친 불변식(invariant)를 검사하자.

      • 불변식(invariant)?
        • 런타임 혹은 정해진 기간동안 만족해야 하는 조건을 의미함
        • ex) 리스트의 크기는 0 이상이어야 하니, 만약 음수가 된다면 불변식은 깨지는 것이다.
    • 만약 매개변수가 잘못되었다면 IllegalArgumentException을 던지면 된다.

  • 계층적인 클래스에 사용되는 빌더 패턴(HierarchicalBuilder)

    • 피자 클래스를 빌더 패턴을 이용해 구현하려면 어떻게 해야할까?

    • 피자 클래스는 토핑을 갖는다.

    • 피자의 하위클래스인 뉴욕피자에는 size를 필수로 받으며, 칼조네 피자에는 소스가 어디에 발렸는지 표시하는 boolean 타입의 sauceInside를 갖는다.

    • public abstract class Pizza {
        public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
        final Set<Topping> toppings;
            //Builder 패턴을 사용할 때는 public setter를 사용하여서는 안된다.
      
        abstract static class Builder<T extends Builder<T>> {
            EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
            public T addTopping(Topping topping) {
                toppings.add(Objects.requireNonNull(topping));
                return self();
            }
      
                    /*  이 build() 메서드는 해당하는 하위 클래스를 반환한다.
                          NyPizza.Builder는 Pizza가 아닌 NyPizza를 반환한다.
                            이렇게 구현하기 위해 상속받는 클래스는 build()를 반드시
                            오버라이딩 해야한다.
                    */
            abstract Pizza build();
      
            /* 하위 클래스는 이 메서드를 재정의(overriding)하여
                "this"를 반환하도록 해야 한다.
                        self 타입이 없는 자바에서 유사한 기능을 사용하기
                        위한 simulated self-type.
      
                        API User가 하위타입의 클래스를 받고도 method chaining을
                        형변환 없이 사용하게 하기 위해서 구현한다.
                        공변 반환 타이핑(covariant return typing)
                    */ 
      
            protected abstract T self();
        }
      
        Pizza(Builder<?> builder) {
            toppings = builder.toppings.clone(); 
                    // 아이템 50 참조. Shallow copy로 인한 문제점을 방지하고자
                    // clone()을 사용한다.
        }
      }
      
      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;
      
            public Builder(Size size) {
                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;
        }
      
        @Override public String toString() {
            return toppings + "로 토핑한 뉴욕 피자";
        }
      }
      
      public class Calzone extends Pizza {
        private final boolean sauceInside;
      
        public static class Builder extends Pizza.Builder<Builder> {
            private boolean sauceInside = false; // 기본값
      
            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;
        }
      
        @Override public String toString() {
            return String.format("%s로 토핑한 칼초네 피자 (소스는 %s에)",
                    toppings, sauceInside ? "안" : "바깥");
        }
      }
      
      // PizzaTest.java
      NyPizza pizza = new NyPizza.Builder(SMALL)
                    .addTopping(SAUSAGE).addTopping(ONION).build();
      Calzone calzone = new Calzone.Builder()
                    .addTopping(HAM).sauceInside().build();
    • Advantage

      • 빌더 하나로 여러 객체를 반복문 내에서 순회하면서 만들 수 있다. (method chaining!)
      • 빌더에 넘기는 매개변수에 따라서 다른 하위 클래스의 객체를 만들 수도 있다.
    • Disadvantage

      • 객체를 만들기 위해서 빌더부터 구현하여야 하고, 시간을 많이 잡아먹는다.
      • 성능이 중요한 상황에서는 문제가 될 수 있다.

ITEM 3. Enforce the singleton property with a private constructor or an enum type

  • Singleton pattern이란, 인스턴스를 오직 하나만 생성할 수 있는 클래스를 의미합니다.

  • Advantage

    • 객체를 생성하면 global한 특성을 띄며, static을 통한 한 번의 객체 생성으로 재사용을 통해 불필요한 객체 생성으로 인해 낭비되는 메모리가 없습니다.
    • 설계상 유일해야 하는 시스템 컴포넌트나 함수를 사용하기 위한 유틸리티 객체를 만들 때 integrity를 보장한다.
  • Disadvantage

    • 객체 상속이 불가능하고 global한 특성을 띄기 때문에, OOP 원칙과 부합하지 않는다.
    • 그러나 타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱글턴이 아니라면, 이를 사용하는 클라이언트를 테스트하기 어렵다.
  • 어떻게 구현할 수 있는가?

    • 어떻게 구현하던 생성자를 private로 감추고, 인스턴스에 접근하기 위한 방법이 두 가지로 나뉜다.

    • Method #1 : public static 멤버를 선언하기

      • 인스턴스를 담고 있는 reference type variable을 public static으로 선언하여 클래스 로딩 타임에 인스턴스를 생성한다. 외부에서 private() 생성자를 호출할 수 없고, static을 통해 클래스가 로드될 때 딱 한 번 인스턴스가 생성됨을 보장할 수 있다.

      • Disadvantage

        • 권한이 높은(previleged) 클라이언트는 AccessibleObject.setAccessible을 통해서 private 생성자를 호출 할 수 있다. (생성자 코드 참조)
        public class Elvis {
          public static final Elvis INSTANCE = new Elvis();
              //외부에선 이 public static 멤버 변수를 통해 인스턴스를 받는다.
          private Elvis() { 
                      if(INSTANCE != null) throw new RuntimeException("...");
                      // 두 번째 인스턴스 생성을 방지한다.    
              }
        
          public void leaveTheBuilding() {
              System.out.println("Whoa baby, I'm outta here!");
          }
        }
    • Method #2 : 정적 팩터리 메서드를 통해 인스턴스 반환하기

      • getInstance()를 통해 항상 같은 객체의 참조를 반환한다.

      • Advantage

        • API에 의해서 싱글턴 클래스임을 쉽게 확인할 수 있다.
          • private static필드가 final이기 때문!
        • 간결하다.
        • getInstance()를 수정함으로써, API를 바꾸지 않고도 싱글턴 패턴을 따르지 않도록 만들기 쉽다. (ex : 호출하는 쓰레드에 따라서 다른 인스턴스를 반환하길 원할 때)
        • 제네릭 싱글턴 팩터리로 만들 수 있다.
        • 정적 팩터리의 메서드 참조를 공급자(Supplier)로 사용할 수 있다. (아이템 43, 44 참조)
        public class Elvis {
          private static final Elvis INSTANCE = new Elvis();
              //Method #1과 다른 점은 access modifier가 private인 점이다.
          private Elvis() { }
          public static Elvis getInstance() { return INSTANCE; }
              //인스턴스에 접근하기 위해선 public getInstance()를 통해 접근한다.
              //항상 같은 객체의 참조를 반환하기 때문에, 제 2의 Elvis는 만들어지지 않는다.
        
          public void leaveTheBuilding() {
              System.out.println("Whoa baby, I'm outta here!");
          }
        }
  • Method #3 : 원소가 하나인 Enum 타입을 선언하기

    • 이 메서드를 사용하기 위한 동기는, 싱글턴 클래스를 직렬화할 때 생기는 문제가 있다.

    • 싱글턴 패턴을 직렬화하기 위해선 단순히 Serializable 인터페이스를 implements 하는 것으로는 부족하다.

    • 모든 인스턴스 필드를 transient로 선언하고 readResolve 메서드(Serializable 인터페이스의 abstract method)를 추가하여 만들어 두었던 인스턴스를 반환하고, 역직렬화된 이전의 가짜 인스턴스는 GC가 처리하도록 둔다. 만약 이런 조치를 취하지 않는다면 매번 역직렬화 할 때마다 가짜 elvis가 생긴다.

      public enum Elvis {
       INSTANCE;
      
       public void leaveTheBuilding() { }
      }
    • 직렬화가 필요한 싱글턴 패턴이라면, Enum 타입을 통해 선언하는 것을 고려하자.

      public enum Elvis {
       INSTANCE;
      
       public void leaveTheBuilding() { }
      }
    • Advantage

      • 복잡한 직렬화 상황이나 리플렉션 공격에서도 추가적인 인스턴스 생성을 막을 수 있다.
      • 더 간결하다.
    • Disadvantage

      • 만약 만들려는 singleton이 어떤 클래스를 상속받아야 한다면, Enum 클래스를 상속받을 수 없으므로 이는 사용하지 못한다.

ITEM 4. Enforce noninstantiability with a private constructor

  • static 메서드와 static 필드만을 담은 유틸리티 클래스를 만들고 싶을 때는 어떻게 해야할까?
    • 특정 클래스를
    • 추상 클래스로 만들어도 상속을 통해 하위 클래스를 만들어 인스턴스화 하면 그만이다. 추상 클래스의 본 목적을 생각해보면 API User가 이를 상속해서 쓰도록 유도하는 side-effect도 있다.
    • 구현
      • 생성자를 명시하지 않으면 컴파일러가 public 생성자를 자동으로 만들어준다. private 생성자를 만들어주면 컴파일러가 기본 생성자를 만들지 않으니 클래스의 인스턴스화를 막을 수 있다.
      • private 생성자 안에 throw new AssertionError();를 작성해주면 클래스 안에서 실수로 생성자를 호출하지 않도록 강제할 수 있다.
      • 생성자가 존재하는데 인스턴스를 못 만들면 직관적이지 않고 이상해보이니 주석을 달면 좋다.
    • 상속을 금지하는 효과도 있다. 하위 클래스는 상위 클래스의 생성자를 호출하여야 하는데, 이는 불가능하다.
      • 하위 클래스의 생성자의 첫 줄에 상위 클래스의 생성자를 explicit하게 호출하지 않으면, super()를 자동으로 호출하도록 컴파일러가 변경한다.

ITEM 5: Prefer dependency injection to hardwiring resources

  • 많은 클래스가 하나 이상의 자원에 의존한다. 맞춤법 검사기를 예로 들면, 문장이나 단어가 맞춤법에 맞는지 대조할 사전에 의존한다.

  • ex

    1. 정적 유틸리티로 구현

       public class SpellChecker {
           private static final Lexicon dictionary = ...;
           //Lexicon 타입의 사전에만 의존한다.
      
           private SpellChecker() {} 
               // private 생성자로 인스턴스화 방지
      
               //기타 메서드들
           public static boolean isVaild(String word) {...}
           public static List<String> suggestions(String typo) {...}
       }
    2. 싱글턴으로 구현

       public class SpellChecker {
           private final Lexicon dictionary = ...;
      
           private SpellChecker() {} 
               // private 생성자로 인스턴스화 방지
           public static SpellChecker INSTANCE = new SpellChecker(...);
           // 위의 코드에 이어 getInstance 혹은 static 변수를 통하여 유일 인스턴스 제공
      
           public static boolean isVaild(String word) {...}
           public static List<String> suggestions(String typo) {...}
       }
  • 위 두 가지의 구현은 코드에 명시적으로 선언된 사전만을 사용할 수 있다.

  • 만약 다른 사전을 가지고 맞춤법 검사를 하고자 하면, final 키워드를 떼고 새로운 사전으로 교체하는 메서드를 추가하는 방법은?

    • 멀티쓰레드 환경에선 문제를 일으키고, 좋은 코드가 아니다.
  • 맞춤법 검사기 클래스가 여러 자원 인스턴스를 지원하고, 클라이언트가 원하는 사전을 사용하길 원한다면 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방법을 생각할 수 있다. (의존 객체 주입 패턴, defendency )

      public class SpellChecker { 
          private final Lexicon dictionary; 
    
          private SpellChecker(Lexicon dictionary) { 
              this.dictionary = Objects.requireNonNull(dictionary); 
                  //SpellChecker를 생성할 때, 매개변수로 dictionary를 전달한다.    
           } 
    
        public static boolean isVaild(String word) {...}
          public static List<String> suggestions(String typo) {...}
      }
  • 만약 SpellChecker가 Lexicon 클래스로 생성된 인스턴스의 사전만을 이용한다면 위의 방법으로 충분하다. 그러나 자원 팩터리를 넘겨주는 방법도 고려해볼 수 있다.

  • 팩터리 메서드 패턴

    • 객체를 만들어내는 부분을 하위 클래스에 위임하는 패턴입니다. 자동차 회사가 SUV, Truck, Sedan 등을 생산할 때, 한 공장 라인에서 세 종류 모두를 생산하는 일은 굉장히 비효율적인 일이 될 것입니다.

        public abstract class Car {
            public abstract String drive();
            public abstract String getName();
            // drive를 abstract method로 선언함으로써 상속받는 
            // 하위 클래스가 의무적으로 구현하도록 강제한다.
        }
      
        public class Sedan extends Car {
            private String name;
            public String getName(){
                    return name;
            }    
      
            @Override
            public String drive() {
                return "(정숙한 소리)";
            }
        }
      
        public class Truck extends Car {
            private String name;
            public String getName(){
                    return name;
            }    
            @Override
            public String drive() {
                return "(털털털털터렅러털)";
            }
        }
      
        public class Suv extends Car {
            private String name;
            public String getName(){
                    return name;
            }    
            @Override
            public String drive() {
                return "(우우우웅ㅇㅇㅇ웅)";
            }
        }
      
        package pattern.factory;
      
        public class CarFactory {
                public Car createCar(String name){
                        if(name == "Suv") return Suv();
                        else if ...
                        //하나의 팩터리 메서드가 모든 차종 인스턴스를 리턴한다면,
                        //코드가 길어지고 의존성이 커지며 유지보수가 힘들어진다.
                }
        }
      
    • 그 대신, 각 차종마다 하나의 생산라인을 할당한다면 효율적인 생산이 가능할 것입니다.

        package pattern.factory;
      
        public abstract class CarFactory {
            abstract Car createCar(String name);
        }
      
        public class SedanFactory extends CarFactory {
            @Override
            Robot createCar(String name) {
                Sedan sedan = new Sedan();
                switch( name ){
                    case "genesis": sedan.setName("고급진 제네시스");
                    case "chairman": sedan.setName("회장님 차");
                }
                return ret;
            }
        }
      
        public class SedanFactory extends CarFactory { 
        {...}
        }
    • 위의 예제에서는 편의상 모든 차종이 같은 메서드와 멤버 변수를 가지고 있지만, 개발하다보면 각 하위 클래스가 독자적으로 갖는 메서드와 변수들이 점점 늘어날 일이 많습니다.

    • Advantage

      • 의존성을 제거하고 클래스간의 결합도(어떤 클래스를 변경하고자 할 때 다른 클래스를 얼마나 수정하여야 하는가)를 낮추기 위함입니다.
      • 유지보수가 용이합니다.
  • 정리하기

    1. 만약 클래스가 하나 이상의 자원에 의존하거나
    2. 그 자원이 클래스 동작에 영향을 준다면
    • 싱글톤 / 정적 유틸리티 클래스는 사용하지 않는 것이 좋다.
    • 이 자원들을 클래스가 직접 만들게 하지도 않는다.
    • 필요한 자원의 인스턴스나 혹은 팩터리 메서드를 생성자를 통해 전달한다.
  • 궁금한 것 : Supplier<>?

ITEM 6: Avoid creating unnecessary objects

  • 동일한 객체를 매번 생성하지 않고 재사용하는 편이 빠른 것은 모두가 알고 있는 사실이다.

      String s = new String("Hello!"); // DO NOT
    
      String a = "Hello!"; // DO
  • 단 한번의 실행이면 위와 아래의 차이는 없지만, new String()이 매번 호출될 때 마다 새 객체를 만드는 것에 반해, 아래의 코드는 "Hello!" 스트링을 String Constant Pool에 집어넣고 재사용한다.

  • 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피한다.

      Boolean(String); // DO NOT
    
      Boolean.valueOf(String); // DO
    • 정적 팩터리 메서드는 객체를 생성하지 않고 메서드를 사용할 수 있다는 장점을 적극 활용한다.
    • 위의 코드는 매번 생성자를 호출하고 매번 객체를 생성하기 때문에, 루프 안에서 사용하면 무수히 많은 Boolean 객체가 생성된다.
    • valueOf() 메서드는 정적 팩터리 메서드이기 때문에, static 메모리 영역에 저장되어 끝날 때 까지 단 하나의 메모리 공간을 갖고 재사용이 가능하다.
  • 다른 예 : String.matches()

      public static boolean isRomanNumeral(String s) {
          return s.matches("^(?=[MDCLXVI])M*D?C{0,4}L?X{0,4}V?I{0,4}$");
      }
      //DO NOT
    
      public class RomanNumerals {
          // Pattern 인스턴스를 미리 생성해서 캐싱
          private static final Pattern ROMAN = Pattern.compile(
              "^(?=[MDCLXVI])M*D?C{0,4}L?X{0,4}V?I{0,4}$");
    
          public static boolean isRomanNumeral(String s) {
              return ROMAN.matcher(s).matches();
          }
      }
      // DO
    • s.matches가 사용하는 패턴 인스턴스는 단순하게 String 인스턴스를 만드는 것을 넘어서서 입력받은 정규표현식에 해당하는 유한 상태 머신(Finite state machine)을 만들기 때문에, 굉장히 생성 비용이 높다.
    • 대신에 아래의 코드와 같이 패턴을 컴파일한 것을 캐싱하여 반복 사용하면 성능을 끌어올릴 수 있다.
    • 다른 부수적인 효과로는 static 변수에 이름을 붙여 효율적으로 관리할 수 있다.
    • 만약 이 RomanNumerial이 생성되고 단 한 번도 호출되지 않는다면, 쓸데없는 비용을 지불하게 되는 셈이다. 그러나 지연 초기화(lazy initialization)을 통해 처음 호출되는 순간에 패턴을 컴파일하게 할 수는 있지만, 코드가 복잡해지기 때문에 권하지는 않는다. 만약 패턴이 아닌 매우매우매우 비싼 객체를 재사용하고자 할 때는 고려해볼 법 하다.
  • 불변? 가변?

    • 불변 객체는 재사용해도 아무런 문제가 되지 않는다.
    • 가변 객체라고 무조건 재사용하면 안되는가?
      • 어댑터는 불변 객체가 아니지만, 실제 작업은 뒷단에 위치한 객체에 위임하고, 자신은 유사 인터페이스의 역할을 수행하기에 어댑터 객체는 한 번만 생성해도 충분하다.
      • Map의 KeySet 메서드는 Map 객체 안의 키 전부를 담은 Set 뷰를 반환한다. Set 인스턴스가 가변이기 때문에 KeySet을 호출할 때 마다 새로운 Set 인스턴스가 만들어진다고 생각하기 쉽지만, 그 모든 인스턴스는 기능적으로 똑같다.
  • 오토박싱 (auto boxing)

    • 자바에선 primitive type의 데이터를 객체로 포장해주는 래퍼 클래스(Wrapper Class)를 지원한다.

    • primitive type과 wrapper class를 섞어 쓸 때 자동으로 상호 변환해주는 것을 auto boxing이라 한다.

        Integer a = new Integer(4);
        int b = -4;
        System.out.println(a+b); // 0
    • 프로그래머에게 매우 편리한 기능이지만, 불필요한 객체생성이 큰 성능 오버헤드를 불러 일으키는 경우가 있다.

        public static long sum() {
            Long sum = 0L;
            for (long i = 0; i <= Integer.MAX_VALUE; i++) {
                sum += i; // 매번 새로운 Long 객체가 만들어진다.
            }
            return sum;
        }
    • long i 가 Long sum에 매번 더해질 때 마다, 객체가 생성되기에 2^31번의 객체 생성이 있을 것이다.

    • 가급적 primitive type을 사용하자.

  • 정리

    1. 객체 생성을 무조건 피하지 말자.
      • 최근의 개선된 JVM은 작은 객체를 reclaim하는 것이 큰 부담으로 다가오지 않는다.
    2. 아주 무거운 객체가 아닌 이상 각자의 custom pool을 만들지는 말자
      • 데이터베이스 연결은 생성비용이 비싸니 재사용하는 것이 맞다.
      • 그러나 자체 풀을 만드는 것은 코드를 복잡하게 만들고, 메모리 성능을 떨어뜨린다.
      • JVM의 GC는 직접 만든 객체 풀 보다 훨씬 빠르다.
    3. 방어적 복사 vs 필요없는 객체를 반복 생성하기
      • 방어적 복사는 "새로운 객체를 만들어야 한다면 기존 객체를 재사용하지 마라"
        • 새로운 객체를 사용할 때 재사용 하는 일은 디버그하기 어려운 버그와 보안 취약점으로 이어진다.
      • 이번 아이템에서 다루는 내용은 "기존 객체를 재사용해야 한다면 새로운 객체를 만들지 마라"
        • 단순히 코드형태와 성능에만 영향을 주지 프로그램의 integrity에는 영향을 주지 않는다.

ITEM 7: Eliminate obsolete object references

  • C/C++과는 다르게, JAVA에서는 다 쓴 메모리를 할당 해제 하는 번거로움 없이 GC가 reclaim한다.

  • 그러나 찾아내기 미묘한 메모리 누수가 있을 수도 있다.

      public class Stack {
          private Object[] elements;
          private int size = 0;
          private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
          public Stack() {
              elements = new Object[DEFAULT_INITIAL_CAPACITY];
          }
    
          public void push(Object e) {
              ensureCapacity();
              elements[size++] = e;
          }
    
          public Object pop() {
              if (size == 0)
                  throw new EmptyStackException();
              return elements[--size];
          }
    
          /**
           * 원소를 위한 공간을 적어도 하나 이상 확보한다.
           * 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
           */
          private void ensureCapacity() {
              if (elements.length == size)
                  elements = Arrays.copyOf(elements, 2 * size + 1);
          }
    
          public static void main(String[] args) {
              Stack stack = new Stack();
              for (String arg : args)
                  stack.push(arg);
    
              while (true)
                  System.err.println(stack.pop());
          }
      }
    • 위 스택 객체의 문제는, 만약 100개의 원소를 push했다가 pop하여 10개 정도로 유지하는 경우, 나머지 90개의 원소들은 여전히 elements[10] ~ elements[99]가 reference를 여전히 가지고 있다.

    • 0

      9까지는 여전히 유효범위 안에 있지만 10

      99는 논리적으로 추후에 재사용할 일도 없는데 참조가 살아있는 경우다. element 배열로 직접 저장소 풀을 만들어 관리하기 때문에 생기는 문제다.

    • 이런 디테일한 부분을 놓친다면, GC 활동빈도가 잦아지고, 메모리 사용량이 늘어나며 결국 성능이 저하될 것이다. 극단적인 경우는 디스크 페이징이나 OutOfMemoryError를 일으켜 종료되기도 한다.

    • 단 하나의 배열 객체 참조들이 매우 많은 객체가 reclaim되는 것을 막을 수 있다.

    • 이 다 쓴 참조(obsolete reference)를 할당 해제 하기 위해서는 , null로 참조를 없애주면 gc가 작동한다.

                public Object pop() {
                if (size == 0)
                    throw new EmptyStackException();
                Object result = elements[--size];
                elements[size] = null; // 다 쓴 참조 해제
                return result;
            }
      • 또 다른 소소한 장점은, 다 쓴 참조를 할당하는 버그가 있을 경우 NullPointerException이 발생할 것이다.
    • 자기가 직접 메모리를 관리하도록 작성한 클래스는 메모리 누수에 항상 주의해야 한다.

  • 캐시 : 또 다른 가능성

    • 객체 참조를 캐시에 넣고 한참을 그대로 놔두어 메모리 누수가 발생할 수 있다.\
    • 캐시 외부에서 ket를 참조하는 동안만 엔트리가 살아있도록 하려면, WeakHashMap을 사용해 캐시를 만드는게 좋지만 굉장히 예외적인 경우다.
    • 시간이 지날 수록 캐시 엔트리의 가치를 떨어뜨리는 방법으로도 해결할 수 있다. 특정 조건(마지막으로 참조된 지 특정 시간이 지난 캐시)을 만족하는 캐시를 청소하는 방법들.
      1. ScheduledThreadPoolExecutor 같은 방법으로 백그라운드 쓰레드를 활용하여 주기적으로 청소
      2. 캐시에 새 엔트리를 추가할 때 마다 부수적으로 청소를 수행하는 방법이 있다.(LinkedHashMap의 removeEldestEntry)
  • 리스너(Listener) 혹은 콜백(Callback)

    • 캐시의 경우와 유사하다. 클라이언트가 콜백을 등록하고 명확히 해지하지 않는다면 콜백은 쌓이지만, WeakHashMap을 통해 weak reference로 저장하면 된다.

ITEM 8: Avoid finalizers and cleaners

  • Finalizer
    • 클래스에 finalize() 메서드를 override하면 GC가 해당 클래스로 만들어진 인스턴스를 reclaim할 때 연관된 자원들을 정리하기 위한 목적으로 고안되었다. 그러나...
    • 쓰지 말아야 할 이유
      1. Finalizer와 Cleaner로는 즉각 실행되어야 하는 작업은 할 수 없다.
        • GC 구현체에 따라서 finalizer의 실행 시점은 알고리즘에 따라 다르다.
        • file close를 finalizer에서 수행한다면, 제 때 의도한 파일이 닫히지 않고 남아있는 상태에서 시스템이 동시에 열 수 있는 파일 개수의 한계를 초과할 수 있다.
        • finalizer 스레드의 우선순위가 낮아, finalizer의 명령들이 제대로 수행되지 못하는 경우도 있다.
      2. 자바 명세에는 finalizer의 수행 여부조차 보장하지 않는다.
        • 상태를 영구적으로 수정하는 작업(데이터베이스 수정) 등은 절대 finalizer/cleaner에 의존해서는 안된다. 공유 자원의 lock/semaphore 처리를 맡긴다면, deadlock의 가능성이 존재한다.
        • System.runFinalization, System.runFinalizationOnExit, Runtime.runFinalizersOnExit 등은 결함이 많아서 사용하기 어렵다.
      3. finalizer 실행 중 발생한 예외는 무시되며, 처리할 작업이 남았더라도 즉시 종료된다.
        • 마무리가 덜 된 객체를 다른 스레드에서 사용하면 대재앙이 될 것이다.
        • unhandled exception은 일반적으로 스레드를 중단시키고 StackTrace를 출력하겠지만 finalizer 안의 코드는 경고조차 출력하지 않는다.
      4. 심각한 성능 문제가 있다.
        • finalizer가 GC의 효율성을 떨어뜨리는 문제가 있다.
      5. finalizer 공격 취약점 때문에 보안 문제가 발생한다.
        • 생성자나 직렬화 과정에서 예외가 발생하면, 하위 클래스가 생성되다 만 객체에서 하위 클래스의 finalizer가 수행될 수 있는 허점이 있다. 해커가 악의적인 하위 클래스를 끼워넣는다면 문제는 심각해진다.
        • finalizer는 static field에 자신의 참조를 할당하여 GC가 이를 수거하지 못하도록 하기에 한 번 만들다 만 객체가 생성된다면 보안에 구멍이 뚫리게 된다.
        • 공격 방어: 아무 일도 하지 않는 finalize 메서드를 final로 선언하면 상속이 불가능하기 때문에 하위 클래스로 인해 생기는 취약점을 막을 수 있다.
    • 위와 같은 이유로 java 최신버젼에선 deprecated 되었다.
    • 파일이나 스레드 등 종료해야 할 자원을 담고있는 객체에서 finalizer와 cleaner를 대신해 줄 방법은?
      • AutoCloseable Inteface 구현하기
      • 클라이언트에서 명시적으로 close() 호출(+try-with-resources), (각 인스턴스는 자신이 닫혔는지 추적할 수 있는 필드를 유지하는 것이 좋다. 만약 닫혔는데 메서드 호출이나 멤버변수 접근을 하면 IllegalStateException 던지기)
    • 그럼에도 써야하는 극히 일부 예외
      1. 자원의 소유자가 close()를 호출하지 않는 것에 대한 안전장치
        • 즉시 호출되지 않는 finalizer/cleaner지만, 늦게나마 뒷정리를 해주는 것이 하지 않는 것에 비해서 좋기 때문이다. 그러나 이 역시 신중하게 작성하도록 한다.
        • FileInputStream, FileOutputStream, ThreadPoolExecutor는 이 구현을 따르고 있다.
        • 구현하고자 한다면 State inner class를 선언하는데 Room 객체를 참조하지 않도록 구현하여야 한다. 순환참조가 생기게 되면 GC가 Room 인스턴스를 reclaim하지 않는다.
        • 하지만 난 쓰지 않을래......
      2. 네이티브 피어와 연결된 객체
        • 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말한다. 그러나 GC는 네이티브 피어가 자바 객체가 아니기 때문에 회수하지 않는다.
        • 그러나 역시 close()가 좋다. -
        • 중요하지 않은 네이티브 피어 회수 용도로만 사용하자

ITEM 9: Prefer try-with-resources to try-finally

  • 전통적으로 자원이 제대로 close() 됨을 보장하기 위해 try-finally를 사용했다.

  • 그러나 여러 자원을 사용하면 2중, 3중, N중 중첩된 try-finally를 구현해야 하는 지저분한 문제가 있다.

      static void copy(String src, String dst) throws IOException {
          InputStream in = new FileInputStream(src);
          try { //1중
              OutputStream out = new FileOutputStream(dst);
              try { //2중
                  byte[] buf = new byte[BUFFER_SIZE];
                  int n;
                  while ((n = in.read(buf)) >= 0)
                      out.write(buf, 0, n);
              } finally {
                  out.close();
              }
          } finally {
              in.close();
          }
        }
      }
  • 또한 하드웨어 문제가 발생해서 read()가 예외를 던지게 되고, close()가 수행되면서 예외를 던진다. 하지만 앞서 발생한 read() 예외는 덮어지게 된다.

  • 대신에 해당 자원의 클래스에 AutoCloseable을 구현한다.

    • close() 메서드 하나면 구현하면 된다.

    • 해당 인터페이스를 구현한 클래스는 try-with-resources로 작성하면 깔끔하게 예외처리가 가능하다.

        //#1
        static String firstLineOfFile(String path) throws IOException {
                try (BufferedReader br = new BufferedReader(
                            new FileReader(path))) { 
                        //try 키워드 옆에 괄호를 열고 AutoCloseable을 구현한 
                        //인스턴스를 안에서 생성해주도록 하자.
                        //여러 인스턴스를 생성할 수도 있다.
                return br.readLine();
            }
        }
      
        //#2 try-with-resources를 catch와 함께 사용하기
        static String firstLineOfFile(String path, String defaultVal) {
                try (BufferedReader br = new BufferedReader(
                            new FileReader(path))) {
                    return br.readLine();
            } catch (IOException e) { // 메서드에서 throws하는 대신 직접 처리한다.
                    return defaultVal;
                }
        }
    • try-with-resources에서도 가려지는 exception이 존재하지만, 대신에 (suppressed)라는 꼬리표를 달고 출력이 된다. getSuppressed() 메서드를 사용하면 가려진 예외도 가져올 수 있다.

0개의 댓글