• 2 아이템 목록
      1. 생성자 대신 정적 팩토리 메서드를 고려하라
      2. 생성자에 매개변수가 많다면 빌더를 고려하라
      3. private 생성자나 열거 타입으로 싱글턴임을 보증하라
      4. 인스턴스화를 막으려거든 private 생성자를 사용하라
      5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
      6. 불필요한 객체 생성을 피하라
      7. 다 쓴 객체 참조를 해제하라
      8. finalizer와 cleaner 사용을 피하라
      9. try-finally보다는 try-with-resources를 사용하라

 

  • 아이템 1. 생성자 대신 정적 팩토리 메서드를 고려하라
    Class 인스턴스를 어떻게 얻냐? 라고 했을때 바로 떠오르는 것이 public생성자다.
    하지만, 생성자 대신 static factory method 사용하면 아래와같은 장점들이 있다.

 

  • 1-1 장점
      • 이름을 가질 있다. New Redboy() vs Redbody.getRedboyInstance()
        다른 예시로는 BigInterger(int,int Random) vs BigInteger.probblePrime() 오른쪽이 소수인 BigInteger 반환한다는 의미가 명확함을 있다.
      • 시그니처(생성자 외다른 함수로 생성하게 있음) 대해 제약이 없다. 생성자는 이름을 가질 없기 떄문에 오로지 파라미터만으로 시그니처를 다르게 하여 만들 있다.Static factory method  파라미터를 받으면서도 생성자를 다르게 생성할 있다.
      • 반드시 새로운 인스턴스를 안만들어도 된다. New Redboy() 필연적으로 인스턴스를 생성한다. 인스턴스를 계속 만들지 않아도 되는 상황에선 static factory method 사용함으로써, 인스턴스 *통제 클래스 static factory method 클래스로 인스턴스를 관리할 있다. 그로인한 장점은 아래와 같다.
        • 인스턴스 통제 클래스는 싱글턴과 같이 인스턴를 1 혹은 N개로 제어하는 클래스이다.
          예시로는 public static Boolean valueOf(boolean b) { return b? Boolean.TRUE : Boolean.FALSE; }
          • -> valueOF 클래스는 static 함수여서 계속 재사용 할 있다.
      • 반환 타입의 하위 타입 인스턴스를 만들 있다. 생성자의 경우에는 반환형 클래스가 정해져있지만정해져 있지만,
        static factory method
        하위타입을 반환할 있다. 이말인 즉슨  정적 펙터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다는 것이다. 이런 유연함은 서비스 제공자 프레임워크의 근간이 된다. JDBC 대표적이다. 서비스 제공자(Provider) 프레임워크에서 제공자는 서비스의 구현체다. 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여 , 클라이언트를 구현체로부터 분리해준다.
  • 서비스 제공자 프레임워크는 3개의 핵심 컴포넌트로 이뤄진다. 인터페이스, 등록api, 인스턴스를 얻을때 사용하는 서비스 접근 api 있다.
    자바 8부터는 인터페이스에 public static 추가할수 있게되었다.
      • 사용자(클라이언트) 입장 에선 해당 api 반환하는지 신경쓰지 않아도 된다.
      • 이를 응용하여 parameter 하위타입의 반환 클래스를 바꿀 있다, 예시로는 EnumSet 있다.
        • 원소가 64 이하면 long 변수 하나로 관리하는 RegularEnumSet 반환하고 65 이상이면 long배열로 관리하는 JumboEnumSet 반호나한다.
  • 1-2 단점
      • Static public 메소드만 제공하는 클래스는 상속할 없다. 상속을 하려면 public이나 protected 생성자가 필요하기 떄문이다.
      • 생성자는 java docs 명확히 나오지만, static factory method 일반 메소드일 docs에서 특별하게 취급하지 않다.
      • Static factory 메소드명의 관례는 아래와 같다.
        • From: 매개변수 하나 받아서 인스턴스화 ex. Date.from()
        • Of: 여러 매개변수를 받아서 인스턴스화 ex. Enumset.of()
        • valueOF: from of 자세한 버전 ex. BigInteger.valueOf()
        • getInstance: 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는다.
        • Create, newInstance: 매번 새로운 인스턴스를 생성해 반환한다.
      • 결론: 무조건 public 생성자를 사용하기보단 static factory method 통해 인스턴스를 관리하자.
  • 아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라
      • 생성자와 static factory method 모두 똑같은 단점이 있다, optional parameter 많다면 불편해진다.
        예를 들어 NutritionFacts(영양정보) 라는 class 있다고 가정해보자. 2 필수 인자와 4개의 선택인자가 있을때 (점층적 생성자 패턴) 아래와 같다.
  • 2-1. 점층적 생성자 패턴
    • NutritionFacts salmon = new NutritionFacts(123, 2, 34, 4, 5555, "salmon");
      NutritionFacts rice = new NutritionFacts(null, 2, null, 4, 1234, "rice");
    • N번째 인자에 넘기는것이 무엇인지 생성자 시그니처(생성자 명의 파라미터까지) 봐야한다봐야 한다.
      책에서처럼 점측정 생자로 대응하는것은 명시적으로 좋지않다.
    • 매개변수가 많아질 수록 많은 생성자를 작성해야해서 복잡해진다.

 

  • 2-2. 자바 빈 패턴
    • NutritionFacts salmon = new NutritionFacts();
      salmon.setXX(123);
      salmon.setYY("2");
    • 인자 없이 생성자로 인스턴스를 만든 setter 메소드로 값을 주입하는 방식이다.
    • 필수 인자를 강제하고 싶으면 생성자와 섞어 쓸 수도 있다.
    • NutritionFacts salmon = new NutritionFacts(123, 2);
      salmon.setXX(1234);
      salmon.setYY("salmon");
    • 그러나 자바빈 패턴에서는 클래스를 불변으로 만들 없는 치명적인 단점이 존재한다.
      어디서나 setter 호출될 있다. 인스턴스가 중간에 다른 쓰레드에 의해 사용되어 버리는 경우, 안정적이지 않은 상태로 사용될 있다. 따라서 쓰레드 안정성까지 보당(locking, synchronized ) 해줘야 한다.

 

  • 2-3. 빌더(Builder) 패턴
    위의 점층적 생성자와 자바빈으 장점만 모아둔 가장 좋은방법이 빌더 패턴이다. 필요한 객체를 직접 만드는 대신 Builder 객체를 얻은 setter들을 호출한 build() 통해 필요한 객체를 얻는다.
    • NutritionFacts salmon = new NutritionFacts.Builder()
                                      .calories(123)
                                      .sodium(2)
                                      .carbohydrate(5555)
                                      .build();
    • 필수 인자라면 new NutritionFacts.Builder("필수", "인자")처럼 쓸 수도 있다. build()가 호출되는 시점에서 검, 불변화,불변화 있다.
      단점으로는 builder함수를 추가해서 사용하는 메모리 낭비 정도 밖에 없다.
  • 2-4. 실무에 유용! lombok의 @Builder 어노테이션을 쓰자
    • 위에서 빌더 패턴의 유일한 단점은 builder를 짜야한다는 것이다. 코드량이 많다 보니 한 스크롤은 그냥 먹을 것이다. 안 그래도 override 할 것 많은데 이런 boilerplate 코드들이 많으면 좋지 않다. 이럴 때 lombok의 @Builder 어노테이션을 쓰면 유용하다. class 위에 어노테이션만 달면 바로 클래스명.Builder().build(); 를 사용 가능해진다.
    • 단점으로는 위처럼 필수 인자를 쓸 수 없으며, build() 호출 시점에 검증 등을 통해 예외를 던지는 커스터마이징을 할 수 없다.
    • 아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라
    • 싱글턴(singleton)이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 일단 생성자를 private으로 감춘다.
  • 3-1. public static final 멤버 변수
    • public static final Redboy INSTANCE = new Redboy(); 은 static 영역이 로딩될 때 딱 1번만 호출된다.
    • 따라서 Redboy.INSTANCE 는 싱글턴이 보장된다.
  • 3-2. static factory method
    • private static final Redboy INSTANCE = new Redboy();
      public static Redboy getInstance() {
          return INSTANCE;
      }
    • 방식이 방식에 비해 갖는 장점은 API변경에 유연하며, 메소드 레퍼런스로 Redboy::getInstance처럼 사용이 가능하다. (생성자 퍼런스 Redboty::new 싱글턴이 아니다.) 유연한 이유를 부연 설명하자면 생성한다는 함수를 명시적으로 사용하고, 지연 초기화 ,불필요한 복사 생성 방지를 있다.
    • 역직렬화 시 싱글턴이 깨지는 이슈
    • 방법 모두, 역직렬화 같은 타입의 인스턴스가 여러 생길 있다. 예를들어 이미 직렬화된 Redboy 다시 역직렬화할 , private static final Redboy INSTANCE이라면이라면 여러 인스턴스가 생길 있는것이다. 따라서 책에서는 transient 키워드를 추가하고, readResovle() 구현해서 어떤값이 들어오든 버리고 transient Redboy INSTANCE를를 반환하도록 하였다.
    • public class Redboy implements Serializable {
              private static final transient Redboy INSTANCE = new Redboy();
    • public static Redboy getInstance() {
                      return INSTANCE;
              }
              
              public Object readResolve() {
                      return INSTANCE;
              }
      }
  • 직렬화/역직렬화 알고 가자!
      • 직렬화(Serialize) : JVM 메모리 영역에 존재하는 인스턴스를 byte형태로 구워버림
      • 역직렬화(Deserialize) : byte형태를 다시 JVM에 올리는 것
      • 웹, 앱 개발자는 익숙하겠지만 json, xml로 많이들 직렬화/역직렬화 한다.
      • transient는 Serialize 하는 과정에 제외하고 싶은 경우 선언하는 키워드
      • readResolve() : 역직렬화시 호출된다.
      • 정확히는 클래스의 멤버 변수(레퍼런스 타입)가 serializable 하지 않을 경우, 이 멤버 변수를 직렬화/역직렬화 해주기 위해 호출된다.
      • writeReplace() : readResolve의 반대로 직렬화 시 호출된다.
  • 3-3. enum
    • public enum Redboy {
          INSTANCE;
      }
    • 3-1번과 비슷하지만 매우 간결하고, 직렬화 걱정이 없다. 하지만 이것은 javap 로 디컴파일 해보면 3-1번과
    • 비슷하다는 것을 알 수 있다.
    • C:\Users\Redboy\Desktop> javac Redboy.java
    • C:\Users\Redboy\Desktop> javap Redboy.class
      Compiled from "Redboy.java"
      public final class Redboy extends java.lang.Enum <Redboy> {
          public static final Redboy INSTANCE;
          public static Redboy [] values();
          public static Redboy valueOf(java.lang.String);
          static {};
      }
  • 아이템 4. 인스턴스화를 막으려거든 private 생성자를 사용하라
    • 유틸 성 클래스는 보통 인스턴스 화해서 쓰기보단, static method 들로 구성하고 많이 쓴다.
    • public class JacksonUtils {
          public static <T> T convertObject(...)
          public static String writeValueAsString(...)
      }
    • 그러나 자바 컴파일러는 JacksonUtils.java 컴파일할 떄 default 생성자(public JacksonUtils) 추가해버린다. 사용자가 스턴스화 있는 여지를 주는것이다. 이것을 방지하기 위해 abstract class 만들 있지만, 상속해서 인스턴스화하라는 뜻으로 오해할 수도 있다.
      따라서 가장 좋은 해법으로는 생성자의 접근제어자를 prviate으로 하는것이다. 또한 이것은 상속을 막는 효과도 있다.
    • (유틸 성 클래스를 상속하진 않으니..)
    • public class JacksonUtils {
          private JacksonUtils() {}
          public static <T> T convertObject(..) {...}
          public static String writeValueAsString(..) {...}
      }
  • 아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
    • 많은 클래스가 하나 이상의 자원(bean)에 의존한다. 이럴 때 정적 유틸 성 클래스나 싱글턴을 사용하게 되면 문제가 생긴다. 아래는 잘못된 구현의 예제이다.
    • // 한국어사전에 기반한 맞춤법 검사기 유틸이다. 이러면 문제는 다른 언어 사전으로 어떻게 갈아 끼울 것인가?
      public class SpellChecker {
          private static final Lexicon dictionary = new KoreanDic();
          private SpellChecker() {}
          public static boolean isValid(String word) {
              // dictionary를 이용한 검증 로직..
          }
          public static List <String> suggestions(String typo) {
              // dictionary를 이용한 제안 로직..
          }
      }
    • // 한국어사전에 기반한 맞춤법 검사기 싱글톤이다. 이러면 문제는 다른 언어 사전으로 어떻게 갈아 끼울 것인가?
      public class SpellChecker {
          private final Lexicon dictionary = new KoreanDic();
          private SpellChecker() {}
          public boolean isValid(String word) {
              // dictionary를 이용한 검증 로직..
          }
          public List <String> suggestions(String typo) {
              // dictionary를 이용한 제안 로직..
          }
      }
    • 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글톤 방식이 적합하지 않다. 이럴때는 인스턴스를 생성할 생성자, static factory method, builder 필요한 자원을 넘겨주는 방식이 좋다. 이것을 의존객체 주입 패턴이라고 한다.
       
    • private final Lexicon dictionary;
      public SpellChecker(Lexicon dictionary) {
          this.dictionary = dictionary;
      }
    • 의존 객체를 통째로 넘겨주는 것이 아니라, 한 단계 더 들어가서 의존객체를 생성하는 Factory를 넘겨주게 할 수도 있다.자바 8에서는 특히 이것을 명확하게 나타낼 수가 있는데, Supplier를 아래처럼 이용하는 것이다.
    • public SpellChecker(Supplier <? extends Lexicon> dicFactory) {
          this.dictionary = dicFactory.get();
      }
  • 아이템 6. 불필요한 객체 생성을 피하라
    • 가장 쉬운 예제는 문자열인데, new String("hello"); 보단 "hello"; 가 극단적으로 좋다. 전자는 새로운 인스턴스가 만들어져 heap영역에 올라가고, 후자의 문자열 리터럴은 상수 풀에 올라가기 때문이다. 비슷한 이유로 new Boolean(String) 대신 Boolean.valueOf(String)이 좋다.
  • 6-1. 비싼 객체
    • Pattern 같은 비싼 객체들은 한번 쓰고 버리는 것보단 캐싱해서 쓰는 것이 좋다.
    • // AS-IS
      static boolean isRomanNumeral(String s) {
          return s.matches("^(?=.) M*(C [MD]|D? C {0,3})(X [CL]|L? X {0,3})(I [XV]|V? I {0,3})$");
      }
    • // TO-BE
      private static final Pattern ROMAN = Pattern.compile("^(?=.) M*(C [MD]|D? C {0,3})(X [CL]|L? X {0,3})(I [XV]|V? I {0,3})$");
    • static boolean isRomanNumeral(String s) {
          return ROMAN.matcher(s). matches();
      }
  • 6-2. 어댑터
    • 어댑터는 실제 작업은 뒷단 객체에 위임하고 자신은 제2의 인터페이스 역할을 해주는 객체이다. 어댑터는 뒷단 객체만 관리하면 되므로, 뒷단 객체 하나 당 하나씩만 만들어지면 된다.
    • Map의 keySet()은 Set 어댑터를 반환하므로, 아래와 같은 경우 혼동이 생길 수 있다. (아래 예제 코드는 백기선 님 포스팅을 참조했다.)
    •     Map<String, Integer> serviceSinceMap = new HashMap <>();
          serviceSinceMap.put("Kakao", 2010);
          serviceSinceMap.put("Naver", 1999);
    • Set <String> test1 = serviceSinceMap.keySet();
          Set<String> test2 = serviceSinceMap.keySet();
    • test 1.remove("Kakao");
          System.out.println(test1 == test2); // true
          System.out.println(test1.size()); // 1
          System.out.println(test2.size()); // 1
          System.out.println(serviceSinceMap.size()); // 1
    • serviceSinceMap.keySet(); 할 때마다 새로운 Set이 만들어지는 게 아니라 같은 Set인스턴스이다.
  • 6-3. 오토 박싱
    • 오토 박싱은 기본 타입과 레퍼런스 타입 간에 자동으로 상호 변환해주는 기술이다. 개발자 입장에서 구분을 흐리게 해서 편하게 사용 가능하게 해 주지만, 그 경계가 완전히 없어진 것은 아니다.
    • Long sum = 0l;
      for (long i = 0 ; i <= Integer.MAX_VALUE ; i++) {
          sum += i;
      }
    • 이 코드는 매우 느리며, 쓸데없이 Long 객체를 2의 31 제곱 개나 만든다. 단순히 기본 타입 Long-> long으로 바꾸기만 해도 매우 개선된다. 따라서 특별한 이유가 없다면, 박싱 된 기본 타입보다는 기본 타입을 사용하자.
    • 아이템 7. 다 쓴 객체 참조를 해제하라
    • java는 GC가 자동으로 다 쓴 객체를 회수해주긴 하지만, 아래의 몇몇 경우에 개발자가 직접 메모리를 관리함으로써 GC가 회수를 하지 않아,Out Of Memory(OOM)등 문제가 발생할 수 있다.
  • 7-1. 메모리를 직접 관리하는 예제
    • public class Stack {
          private Object [] elements;
          ...
          public Object pop() {
              if (size == 0) throw new EmptyStackException();
              return elements [--size]; // 문제점
          }
          ...
      }
    • Object []은 Arrays.copyOf()를 통해 길이 조절이 되는 상황이다. pop() 메서드에서 size를 감소시키나, 해당 값은 그대로 두고 있으므로 메모리 누수가 발생하는 것을 볼 수 있다.
    • 이 경우 명시적으로 null을 할당해줌으로써 참조 해제를 통해 GC를 돌릴 수 있다.
    • Object result = elements [--size];
      elements [size] = null;
      return result;
  • 7-2. cache
    • 캐시를 직접 구현할 경우, 객체를 캐시에 넣어두고 잊는 등 문제의 소지가 있다. WeakHashMap, LinkedHashMap.removeEldestEntry, 백그라운드 스레드를 돌리며 캐시 해제 등 방법을 책에서 제시하고 있다. (캐시 라이브러리 쓰자..)
    • WeakHashMap
    • Weak Reference를 알아야 한다. java에서는 3가지 참조 유형이 있다.
    • Strong Reference : Integer value = 1; GC대상이 아니다.
    • Soft Reference : SoftReference <Integer> key = new SoftReference <Integer>(value); value 가 null 이 되어 참조되지 않을 때 GC대상이 된다. 그러나 Weak Reference와 다르게 메모리가 부족하지 않으면 굳이 GC하지 않는다.
    • Weak Reference : WeakReference <Integer> key = new WeakReference <Integer>(value); value 가 null 이 되어 참조되지 않을 때 GC대상이 된다. 무조건 다음 GC 때 사라진다.
    • WeahHashMap은 Weak Reference의 특성을 구현한 HashMap이다.
    • WeakHashMap <Integer, String> map = new WeakHashMap <>();
      Integer key1 = 129;
      Integer key2 = 130;
      map.put(key1, "value1");
      map.put(key2, "value2");
      System.out.println(map.keySet()); // [130, 129]
      key1 = null;
      System.gc();
      System.out.println(map.keySet()); // [130]
    • 위에서 GC가 돌면 map에는 130:"value2"130:"value2"만 남게 된다.
  • 7-3. 콜백
    • 7-2번과 마찬가지로 put 되었는데 계속 내버려두면 쌓인다. Weak Reference를 사용한 WeakHashMap에 저장해두면 좋다.
  • 아이템 8. finalizer와 cleaner 사용을 피하라
      • https://docs.oracle.com/javase/9/docs/api/java/lang/ref/Cleaner.html
      • Finalizer는 예측 불가능하고 위험하며, 대부분 불필요하다. 성능도 안 좋아진다. (자바 9부터는 deprecated가 되었고, 대안으로 cleaner를 소개했다.) 그러나 cleaner 역시 finalizer보다 덜 위험하지만 여전히 문제는 비슷하다.
      • 책에서는 Finalizer를 까고 있는 내용을 요약하면..
    • 언제 실행될지 알 수 없다. 실행을 보장 X
      • 자바 스펙에 실행 시점을 명확히 하지 않음.
        • 인스턴스가 finalization 큐에 들어간 후 언제 실행될지 알 수 없다. 아예 안될 수도..
    • 성능 저하
    • 예외 발생 시 무시 : 보통 예외가 발생하면 stack trace 가 출력되지만, finalize 내에선 무시되고 처리한다.
    • finalizer와 cleaner를 쓰는 적절히 쓰는 곳
    • 자원 반납에 쓸 close 메서드를 클라이언트가 호출하지 않았다는 가정 하에, 물론 실제로 Finalizer나 Cleaner가 호출될지 안될지 언제 호출될지도 모르긴 하지만, 안 하는 것 보다는 나으니까. 실제로 자바에서 제공하는 FileInputStream, FileOutputStream, ThreadPoolExecutor 그리고 java.sql.Connection에는 안전망으로 동작하는 finalizer가 있다.
  • 아이템9. try-finally보다는 try-with-resources를 사용하라
    • 자바 라이브러리에는 close 메서드를 통해 닫아야하는 자원들이 있다. 자바 7이전에서는 try-finally를 이용해 close()를 호출했다. 그러나 2개 이상의 자원을 사용할 때 이것은 복잡해진다.
    • InputStream in = new FileInputStream(src);
      try {
          OutputStream out = new FileOutputStream(dst);
          try {
              ...
          } finally {
              out.close();
          }
      } finally {
          in.close();
      }
    • 이것은 복잡함의 문제뿐만 아니라 finally에서 예외가 터지면 다른 예외가 덮힌다는 복잡한 문제까지 있다. 이럴 경우 디버깅이 힘들어진다.
    • public class Redboy implements AutoCloseable {
              public void doWork() throws RuntimeException {
              throw new RuntimeException();
          }
    • @Override
          public void close() throws RuntimeException {
              throw new RuntimeException();
          }
      }
    • // RedboyClient main 메소드
      Redboy redboy = null;
      try {
          redboy = new Redboy();
          redboy.doWork();
      } finally {
          if (redboy != null) {
              redboy.close();
          }
      }
    • // 실행결과, close() 메소드에서 발생한 예외만 잡혔다.
      Exception in thread "main" java.lang.RuntimeException
              at effective.Redboy.close(Redboy.java:10)
              at effective.Client.main(RedboyClient.java:50)
    • 자바7부터는 try-with-resources 가 새로 나왔다. 이것은 AutoCloseable를 구현하고 아래와 같이 써주면, 자동으로 닫아준다. 물론 자원 여러개도 된다. (AutoCloseable를 구현하지 않았다면, 사용할 수 없다)
    • try (InputStream in = new FileInputStream(src);
           OutputStream out = new FileOutputStream(dst)) {
          ...
      } catch (..) {}
    • 위와는 다르게 close에서 발생한 예외를 숨겨주고 stacktrace에서 suppressed 라는 태그를 달고 출력된다. 즉, 뒤에 발생한 에러는 첫번째 발생한 에러 뒤에다 쌓아두고(suppressed) 처음 발생한 에러를 중요시 여긴다. 그리고 Throwable의 getSuppressed 메소드를 사용해서 뒤에 쌓여있는 에러를 코드에서 사용할 수도 있다.

 

 

참조: https://sjh836.tistory.com/168

 

 

'IT 도서 정리' 카테고리의 다른 글

자바 네트워크 프로그래밍 4판  (0) 2020.10.01
읽었던 책들 하나씩 정리하기  (0) 2020.09.29
소프트웨어 장인  (0) 2020.07.10
MySQL 퍼포먼스 최적화  (0) 2020.07.10

+ Recent posts