티스토리 뷰

읽은책

Effective Java

승가비 2021. 12. 31. 17:58
728x90

 

item.1: 생성자 대신 정적 팩터리 메서드를 고려하라

  • 전통: public 생성자
  • 별도: 정적 팩터리 메서드 (static factory method)
  • public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }

장점

  • 이름을 가질 수 있다.
  • 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
    • 인스턴스를 통제하면 클래스를 singleton)
    • 인스턴스화 불가
  • 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
    • 엄청난 유연성
  • 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환
  • 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

단점

  • 상속을 하려면 public 이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
  • 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
  • from of valueOf instance getInstance create newInstance getType newType type

item.2: 생성자에 매개변수가 만다면 빌더를 고려하라

  • 점층적 생성자 패턴도 쓸 수는 있지만,
  • 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
  • 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고,
  • 객체가 완전히 생성되기 전까지는 일관성 (consistency) 무너진 상태에 놓이게 된다.
  • 가독성을 겸비한 빌더 패턴(Builder pattern)
  • NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();
  • 코드는 쓰기 쉽고, 무엇보다도 읽기 쉽다.
  • 빌더 패턴은 (파이썬과 스칼라에 있는) 명명된 매개변수 (named optional parameters)를 흉내 낸 것이다.
  • 불변식 (invariant)을 검사하여, IllegalArgumentException을 던지면 된다.
    • 불변 (immutable; immutability)
      • 어떠한 변경도 허용하지 않는다는 뜻
      • 주로 변경을 허용하는 가변 (mutable) 객체와 구분하는 용도로 사용
  • 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋다.
  • public abstract class Pizza { public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE } final Set<Topping> toppings; abstract static class Builder<T extends Builder<T>> { EnumSet<Toppings> toppings = EnumSet.noneOf(Topping.class); public T addTopping(Topping topping) { toppings.add(Objects.requireNonNull(topping)); return self(); } abstract Pizza build(); protected abstract T self(); } Pizza(Builder<?> builder) { toppings = builder.toppings.clone(); } }
  • 여기에 추상 메서드인 self를 더해 하위 클래스에서는 형변환하지 않고도 메서드 여쇄를 지원할 수 있다.
  • self 타입이 없는 자바를 위한 이 우회 방법을 시뮬레이트한 셀프 타입(simmulated self-type) 관용구라 한다.
  • 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; } }
  • 공변 반환 타이핑(covariant return typing)
  • 형변환에 신경 쓰이지 않고도 빌더를 사용할 수 있다.
  • 생성자 패턴보다는 코드가 장황해서 매개변수 4개 이상은 되어야 값어치를 한다.
  • 하지만 API는 시간이 지날수록 매개변수가 많아지는 경향이 있음을 명심하자.
  • 생성자나 정적 패터리 방식으로 시작했다가 나중에 매개변수가 많아지면 빌더 패턴으로 전환할 수도 있지만,
  • 이전에 만들어둔 생성자와 정적 팩터리가 아주 도드라져 보일 것이다.
  • 애초에 빌더로 시작하는 편이 나을 때가 많다.

item.3: private 생성자나 열거 타입으로 싱글턴임을 보증하라

  • 싱글턴 (singleton): 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.
  • 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.
  • public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public void leaveTheBuilding() { ... } }
  • public 이나 protected 생성자가 없으므로 Elvis 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.
  • public class Elvis { private static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public static Elvis getInstance() { return INSTANCE; } public void leaveTheBuilding() { ... } }
  • Serializable을 구현한다고 선언하는 것만으로는 부족하다.
  • 일시적(transient)이라고 선언하고 readResolve 메서드를 제공해야 한다.
  • private Object readResolve() { return INSTANCE; }

열거 타입 방식의 싱글턴 - 가장 바람직한 방법

public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() { ... }
}
  • 추가 노력 없이 직력화할 수 있고,
  • 심지어 아주 복잡한 직렬화 상황이나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.
  • 대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.
  • 싱글턴이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다.
  • 열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있다.

item.4: 인스턴스화를 막으려거든 private 생성자를 사용하라

  • 추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다.
  • private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.

item.5: 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

  • 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.
  • 생성할 떄 생성자에 필요한 자원을 넘겨주는 방식
  • constructor, 의존 객체 의존성 주입

핵심 정리

  • 하나 이상의 자원에 의존하고, 자원이 클래스 동작에 영향을 준다면
    • 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다.
  • 필요한 자원을, 생성자(정적 패터리, 빌더)에 넘겨주자.
  • 유연성, 재사용성, 테스트 용이성을 개선해준다.

item.6: 불필요한 객체 생성을 피하라

  • 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다.
  • 재사용은 빠르고 세련되다.
  • 이 문장이 반복문이나 빈번히 호출되는 메서드 안에 있다면 쓸데없는 String 인스턴스가 수백만개 만들어 질 수 있다.
  • 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.
  • String s = "asdf";
  • Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.
  • 팩터리 메서드는 전혀 그렇지 않다.
  • 비싼 객체가 반복해서 필요하다면 캐싱하여 재사용하길 권한다.
  • // 6.5배 정도 빨라짐 // 코드도 더 명확해짐

public class RomanNumerals {
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();
}
}

- 객체가 불변이라면 재사용해도 안전함이 명백하다. (immutable - 순수함수
)

### 박싱된 기본 타입 vs 기본 타입
- 박싱된 기본 타입보다는 기본 타입을 사용하고,
- 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.
- "객체 생성은 비싸니 피해야한다."로 오해하면 안된다.
- 객체를 생성하고 회수하는 일이 크게 부담되지 않는다.
- 프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일이다.

### 방어적 복사
- 방어적 복사가 필요한 상황에서 객체를 재사용했을 때의 피해가,
- 필요 없는 객체를 반복 생성했을 때의 피해보다 크다는 것을 명심하자.
- 방어적 복사에 실패하면, 언제 터져 나올지 모르는 버그 & 보안 구멍으로 이어진다.
- 불필요한 객체 복사시, 그저 코드 형태와 성능에만 영향을 준다.

--- 

## item.7: 다 쓴 객체 참조를 해제하라
- 오래 실행하다 보면 점차 가비지 컬렉션 활동 & 메모리 사용량이 늘어남
- elements 배열의 활성 영역 밖의 참조들이 모두 여기에 해당
- 해당 참조를 다 썼을 때 null 처리(참조 해제)하면 된다.
```java
elements[size] = null
  • 프로그램의 오류는 가능한 한 조기에 발견하는 게 좋다.
  • 모든 객체를 다 쓰자마자 일일이 null 처리하는 데 현안이 되기도 한다.
  • 그 참조를 담은 변수를 유호 범위(scope) 밖으로 밀어내는 것이다.
  • 변수의 범위를 최소가 되게 정의
  • 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야 한다.
  • null 처리를 직접 해줘야 한다.
  • 캐시 역시 메모리 누수를 일으키는 주범이다.
  • 백그라운드 스레드
  • 콜백을 약한 참조(weak reference) 저장 하면 가비지 컬렉터가 즉시 수거해간다.

item.8: finalizer와 cleaner 사용을 피하라

  • finalizer는 예측할 수 없고
  • 상황에 따라 위험할 수 있어 일반적으로 불필요하다.
  • 기본적으로 쓰지말아야 한다.
  • cleaner는 finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.
  • GC: 접근할 수 없게 된 객체를 수거하는 일
  • 프로그래머에게는 아무런 작업도 요구하지 않는다.
  • 즉, finalizer와 cleaner로는 제때 실행되어야 하는 작업을 절대 할 수 없다.

가비지 컬렉터 구현마다 천차만별

  • JVM 자원 회수가 제멋대로 지연
  • OutOfMemoryError 객체 수천개가 finalizer 대기열에서 회수되기만을 기다림
  • finalizer를 사용하지 않는 방법뿐
  • 상태를 영구적으로 수정하는 작어벵서는 절대 finalizer나 cleaner에 의존해서는 안 된다.

지탄의 대상

System.runFinalizersOnExit
Runtime.runFinalizersOnExit

item.9: try-finally 보다는 try-with-resources 를 사용하라.

static void copy(String src, String dst) throws IOException {
    try (
        InputStream in = new FileInputStream(src);
        OutputStream out = new FileOutputStream(dst)
    ) {
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) >= 0) {
            out.write(buf, 0, n);
        }
    }
}
  • catch 절을 쓸 수 있다.
  • try 문을 더 중첩하지 않고도 다수의 예외를 처리할 수 있다.
  • static String firstLineOfFile(String path, String defaultVal) { try ( BufferedReader br = new BufferedReader(new FileReader(path)) ) { return br.readLine(); } catch (IOException e) { return defaultVal; } }

핵심 정리

  • try-with-resources 를 사용하자!
  • 더 짧고 분명해지고, 예외 정보도 훨씬 유용하다.

item.10: equals는 규약을 지켜 재정의하라

  • 재정의에 함정이 도사리고 있어서 자칫하면 끔찍한 결과를 초래한다.
  • 재정의하지 않는 것이 최선이다.

동치관계(equivalence relation)

  • 반사성(reflexivity): x.equals(x) == true
  • 대칭성(symmetry): x.equals(y) == true && y.equals(x) == true
  • 추이성(transitivity): x.equals(y) == true && y.equals(z) == true && z.equals(x)
  • 일관성(consistency): x.equals(y) == true (many times same)
  • null-아님: x.equals(null) == false
@Override public boolean equals(Object o) {
    if (o == this) {
        return true;
    }
    if (!(o instanceof A)) {
        return false;
    }

    A a = (A)o;
    return a.a == a && a.b == b && a.c == c;
}

item.11: equals를 재정의하려거든 hashCode도 재정의하라

  • equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다.
  • hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다.
  • equals(Object)가 두 객체를 같다고 판단했으면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.
  • 두 객체가 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다.
  • 단, 다른 객체에 대해서는 다른 값을 반환해야 해시 테이블 성능이 좋아진다.

@Override public int hashCode() { return 42; }

  • 모든 객체에게 똑같은 값
  • 해시 테이블의 버킷 하나에 담겨 마치 연결 리스트처럼 동작
  • 평균 수행시간이 O(1)인 해시테이블이 O(n)으로 느려져서, 객체가 많으면 도저히 쓸 수 없다.

좋은 해시 함수

  • 좋은 해시 함수라면 서로 다른 인스턴스에 다른 해시코드 반환
  • 인스턴스들을 32비트 정수 범위에 균일하게 배분해야 한다.
  • 클래스가 불변이고 해시코드를 계산하는 비용이 크다면, 매번 새로 계산하기 보다는 캐싱하는 방식을 고려

주의사항

  • 성능을 높인답시고 해시코드를 계산할 때 핵심 필드를 생략해서는 안 된다.
  • 영역의 수많은 인스턴스가 단 몇 개의 해시 코드로 집중되어 해시테이블의 속도가 선형으로 느려질 것이다.
  • equals를 재정의할 때는 hashCode도 반드시 재정의해야 한다.

Keyword

  • 비결정적(undeterministic)

item.12: toString을 항상 재정의하라

  • 클래스_이름@16진수로_표현한_해시코드
  • 간결하면서 사람이 읽기 쉬운 유익한 정보를 반환
  • 모든 하위 클래스에서 이 메서드를 재정의하라
  • toString을 잘 구현한 클래스는 사용하기에 훨씬 즐겁고
  • 그 클래스를 사용한 시스템은 디버깅하기 쉽다.
  • toString이 반환한 값에 포함된 정보를 얻어 올 수 있는 API를 제공하자

item.13: clone 재정의는 주의해서 진행하라

  • Cloneable은 복제해도 되는 클래스임을 명시하는 용도인 믹스인 인터페이스(mixin interface)
  • Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메서드를 호출할 수 없다.
  • Cloneable을 구현한 클래스의 이스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환
  • 그렇지 않으면 CloneNotSupportedException을 던진다.
  • clone 메서드는 사실상 생성자와 같은 효과를 낸다.
  • 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
  • Cloneable 아키텍처는 가변 객체를 참조하는 필드는 final로 선언하라는 일반 용법과 충돌

복사 생성자와, 복사 팩터리라는 더 나은 객체 복사 방식을 제공

public Yum(Yum yum) { ... };

public static Yum newInstance(Yum yum) { ... };
  • 복사 생성자와 그 변형인 복사 팩터리는 Cloneable/clone 방식보다 나은면이 많다.
  • 언어 모순적이고 위험천만한 객체 생성 메커니즘(생성자를 쓰지 않는 방식)을 사용하지 않으며,
  • 엉성하게 문서화된 규약에 기대지 않고,
  • 정상적인 final 필드 용법과도 충돌하지 않으며,
  • 불필요한 검사 예외를 던지지 않고,
  • 형변환도 필요치 않다.
  • 더 정확한 이름
    • 변환 생성자(conversion constructor)
    • 변환 팩터리(conversion factory)

item.14: Comparable을 구현할지 고려하라

  • Comparable.compareTo
  • 클래스의 인스턴스들에는 자연적인 순서(natural order)
  • 다음처럼 손쉽게 정렬 Arrays.sort(a);
  • public interface Comparable<T> { int compareTo(T t); }
  • 동치성 검사도, 반사성, 대칭성, 추이성을 충족해야 함
  • 작으면 음수
  • 같으면 0
  • 크면 양수
    static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());
  • static Comparator<Object> hashCodeOrder = new Comparator<>() { public int compare(Object o1, Object o2) { return Integer.compare(o1.hashCode(), o2.hashCode) } }

item.15: 클래스와 멤버의 접근 권한을 최소화하라

  • 정보 은닉, 혹은 캡슐화라고 하는 이 개념은 소프트웨어 설계의 근간이 되는 원리다.
  • 시스템 개발 속도를 높인다. (병렬로 개발)
  • 시스템 관리 비용을 낮춘다. (빠른 디버깅)
  • 정보 은닉 자체가 성능을 노평주지는 않지만, 성능 최적화에 도움을 준다.
  • 소프트웨어의 재사용성을 높인다.
  • 큰 시스템을 제작하는 난이도를 낮춰준다.

접근성

  • private: 멤버를 선언한 톱레벨 클래스에서만 접근
  • package-private: 멤버가 소속된 패키지 안의 모든 클래스에서 접근
  • protected: package-private 접근 범위를 포함, 선언한 하위 클래스에서도 접근
  • public: 모든 곳에서 접근

모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다.

  • 패키지 외부에서 쓸 이유가 없다면 package-private 로 선언
  • public 으로 선언한다면 API가 되므로 하위 호환을 위해 영원히 관리해줘야만 한다.
  • protected 멤버의 수는 적을수록 좋다.
  • 접근성 좁힐 수 없는 제약은 상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체해 사용할 수 있다는 규칙을 지키기 위해 필요
  • public 가변 필드를 갖는 클래스는 일반적으로 스레드 안전하지 않다.
  • 추상 개념을 완성하는 데 꼭 필요한 구성요소로써의 상수라면 public static final 필드로 공개해도 좋다.
  • 길이가 0이 아닌 배열은 모두 변경 가능하니 주의하자.
    • 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안된다.
    • // 보안상 허점이 있음 public static final Thing[] VALUES = { ... };

item.16: public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라

  • private 멤버변수를 추가하고,
  • public 접근자 (getter) 추가

item.17: 변경 가능성을 최소화하라

  • 오류가 생길 여지
  • 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
  • 클래스를 확장할 수 없도록 한다.
  • 모든 필드 final 로 선언한다.
  • 모든 필드를 private 로 선언한다.
  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

불변객체는 단순하다

  • 모든 생성자가 클래스 불변식 (class invariant)를 보장한다면
  • 불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요 없다.
  • 불변 객체는 안심하고 공유할 수 있다.
  • 재활용 방법은 자주 쓰이는 값들을 상수(public static final)로 제공하는 것
  • 불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
  • 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.
  • 아무리 복잡하더라도 불변식을 유지하기 훨씬 수월하기 때문
  • 불변 객체는 그 자체로 실패 원자성을 제공
  • 불변 클래스에도 단점은 있다.
  • 값이 다르면 반드시 독립된 객체로 만들어야 한다는 것이다.

클래스가 불변임을 보장하려면

  • 자신을 상속 못하게 해야 함
  • final class
  • constructor(private, package-private)
  • public 정적 팩터리를 제공하는 방법
  • 확장이 불가능하다.
  • 외부에 비치는 값을 변경할 수 없다.

핵심 정리

  • getter가 있다고해서 무조건 setter을 만들지는 말자
  • 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
  • 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.
  • 예측하기 쉬워지고 오류가 생길 가능성이 줄어든다.
  • 변경해야 할 필드를 뺀 나머지 모두를 final로 선언하자.
  • 다른 합당한 이유가 없다면 모든 필드는 private final 이어야 한다.
  • 생성자는 불변식 설정이 모두 완료된,
  • 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
  • 다시 초기화하는 메서드도 안 된다.
  • 복잡성만 커지고 성능 이점은 거의 없다.

Keyword

  • 가변 동반 클래스(companion class)

item.18: 상속보다는 컴포지션을 사용하라

  • 상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다.
  • 메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.
  • 자기사용(self-use)
  • 컴포지션(composition; 구성)
  • 이 방식을 전달(forwarding), 전달 메서드(forwarding method)

데코레이터 패턴

  • 기능을 덧씌운다는 뜻
  • 위임(delegation): 자신의 참조를 넘기는 경우만 위임에 해당한다.
  • 래퍼 클래스는 단점이 거의 없다.
  • 래퍼 클래스가 콜백(callback) 프레임워크와는 어울리지 않는다는 점만 유의
    • 자신을 감싸고 있는 래퍼의 존재를 모르니,
    • 대신 자신(this)의 참조를 넘기고,
    • 콜백 때는 래퍼가 아닌 내부 객체를 호출하게 된다.

상속

  • 진짜 하위 타입인 상황에만 쓰여야 한다.
  • is-a 관계
  • 그렇지 않으면 내부 구현을 불필요하게 노출
  • 확장하려는 클래스의 API에 아무런 결함이 없는가?
  • 결함이 있다면, 이 결함이 여러분 클래스의 aPI까지 전파돼도 괜찮은가?
  • 컴포지션으로는 이런 결함을 숨기는 새로운 API를 설계할 수 있지만, 상속은 상위 클래스의 결함을 승계한다.

핵심 정리

  • 상속은 캡슐화를 해친다.
  • 순수한 is-a 상태에서만
  • 확장성을 고려하지 않은 클래스의 상속은 위험
  • 취약점을 피하려면 컴포지션과 전달을 사용
  • 래퍼 클래스는 하위 클래스보다 견고하다.

item.19: 상속을 고려해 설꼐하고 문서화하라. 그러지 않았다면 상속을 금지하라

  • 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야 한다.
  • API 설명에 적시
  • public 과 protected 메서드 중 final이 아닌 모든 메서드를 뜻한다.
  • Implementation Requirements @implSpec

좋은 API 문서

  • 어떻게?가 아닌 무엇?을 하는지 설명해야 한다.
  • 상속이 캡슐화를 해치기 때문에 일어나는 안타까운 현실
  • 클래스를 안전하게 상속할 수 있도록 하려면, 내부 구현을 설명해야 한다.
    ```
  • tag "implSpec:a:Implementation Requirements:"
    ```
  • 클래스의 내부 동작 과정 주간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태롤 공개해야할 수도 있다.

상속 설계

  • 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.
  • 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다. (중복 호출 문제)
  • private, final, static 메서드는 재정의가 안되니 마음껏 호출해도 된다.
  • Cloneable, Serializable 을 구현한 클래스는 상속하지 않도록 한다. (엄청난 부담)
    • clone: 복제 전에 재정의한 메서드부터 호출
    • readObject: 역직렬화 되기 전에 재정의한 메서드부터 호출
    • readResolve: private 무시
    • writeReplace: private 무시

상속 금지

  • final class
  • constructor: private or package-private
    • public 정적 팩터리

item.20: 추상 클래스보다는 인터페이스를 우선하라

  • 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있다.
  • 인터페이스는 믹스인(mixin) 정의에 안성맞춤이다.
    • 이처럼 대상 타입의 주된 기능에 선택적 기능을 혼합(mixed in) 한다고 해서 믹스인이라 한다.
  • 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
  • n개라면 지원해야 할 조합의 수는 2^n개나 된다. 흔히 조합 폭발 (combinatorial explosion)
  • 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다.
  • 인터페이스와 추상 골격 구현(skeletal implementation) 클래스
  • 바로 템플릿 메서드 패턴
  • Interface AbstractInterfact AbstractCollection AbstractSet AbstractList AbstractMap

item.21: 인터페이스는 구현하는 쪽을 생각해 설계하라

  • 무작정 삽입
  • 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법이다.
  • 디폴트 메서드는 (컴파일에 성공하더라도) 기존 구현체에 런타임 오류를 일으킬 수 있다.
  • 인터페이스를 설계할 때는 여전히 세심한 주의를 기울여야 한다.
  • 인터페이스를 릴리스한 후라도 결함을 수정하는 게 가능한 경우도 있겠지만, 절대 그 가능성을 기대서는 안 된다.

item.22: 인터페이스는 타입을 정의하는 용도로만 사용하라

  • final 이 아니라면, 오염
  • import static Constants.*
  • public class Constants { // 인스턴스화 방지 private Constants() {} public static final int a = 1; public static final int b = 2; public static final int c = 3; }
  • 인터페이스는 타입을 정의하는 용도로만 사용해야 한다.
  • 상수 공개용 수단으로 사용하지 말자.

item.23: 태그 달린 클래스보다는 클래스 계층구조를 활용하라

  • 열거 타입 선언, 태그 필드, switch case 쓸데 없는 코드 제거
  • 필드들을 final로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화 해야
  • 태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다.
  • 계층 구조를 어설프게 흉내낸 아류일 뿐이다.

item.24: 멤버 클래스는 되도록 static으로 만들라

  • 내부 클래스(inner class)
  • 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다.
  • 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.
    • 메모리 누수 방지
    • 바깥 맵으로의 참조를 갖게되어 공간과 시간을 낭비

item.25: 톱레벨 클래스는 한 파일에 하나만 담으라

  • 서로 다른 소스 파일로 분리하면 그만

item.26: 로 타입은 사용하지 말라

  • 제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입(generic type)이라 한다.
  • 매개변수화 타입(parameterized type)
  • 실제(actual) 타입 매개변수

로 타입(raw type)

  • List<E>의 로타입은 List
  • 로 타입을 쓰면 제네릭이 안겨주는 안정성과 표현력을 모두 잃게 된다.
  • 바로 호환성 때문이다.
  • List<Object>처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮다.
  • List<Object>는 모든 타입을 허용한다는 의사를 컴파일러에 명확히 전달한 것이다.
  • 컴파일은 되지만 로 타입인 List를 사용하여 다음과 같은 경고가 발생한다.
  • ClassCastException을 던진다.

와일드카드 타입(unbounded wildcard type)

  • 물음표(?)를 사용하자. Set<?>
  • static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }
  • 특징을 간단히 말하자면 와일드카드 타입은 안전하고, 로 타입은 안전하지 않다.
  • Collection<?>에는 (null 외에는) 어떤 원소도 넣을 수 없다.
  • 제네릭 타입에 instanceof를 사용하는 올바른 예
  • if (o instanceof Set) { Set<?> s = (Set<?>) o; ... }

핵심 정리

  • 로 타입은 제네릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐이다.
  • 빠르게 훑어보자면, Set<Object>는 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입이고,
  • Set<?>는 모종의 타입 객체만 저장할 수 있는 와일드카드 타입이다.
  • 그리고 이들의 로 타입인 Set은 제네릭 타입 시스템에 속하지 않는다.
  • Set<Ojbect>Set<?>는 안전하지만, 로 타입인 Set은 안전하지 않다.

용어

한글 영문
매개변수화 타입 parameterized type List
실제 타입 매개변수 actual type parameter String
제네릭 타입 generic type List
정규 타입 매개변수 formal type parameter E
비한정적 와일드카드 unbounded List<?>
로 타입 raw type List
한정적 타입 매개변수 bounded type parameter <E extends Number>
재귀적 타입 한정 recursive type bound  
한정적 와일드카드 타입 bounded wildcard type List<? Extends Number>
제네릭 메서드 generic method static List asList(E[] a)
타입 토큰 type token String.class

item.27: 비검사 경고를 제거하라

  • 수많은 컴파일러 경고를 보게될 것이다.
  • 제네릭에 익숙해질수록 마주치는 경고 수는 줄겠지만 새로 작성한 코드가 한번에 깨끗하게 컴파일되리라 기대하지는 말자.
  • 할 수 있는 한 모든 비검사 경고를 제거하라.
  • new HashSet(); // warning new HashSet<>(); // ok
  • 경고를 제거할 수는 없지만 타입 안전하다고 확신할 수 있다면 @SuppressWarnings("unchecked") 에너테이션을 달아 경고를 숨기자.
  • 비검사 경고는 런타임에 ClassCastException을 일으킬 수 있는 잠재적 가능성을 뜻하니 최선을 다해 제거하라.

item.28: 배열보다는 리스트를 사용하라

  • 배열 Sub[] 배열 Super[]의 하위 타입이 된다.
    • 공변, 즉 함께 변한다는 뜻이다.
  • 제네릭은 불공변(invariant)이다.
    • List<Type1>List<Type2>의 하위타입도 아니고 상위 타입도 아니다.
  • ArrayStoreException: 호환되지 않는 타입
  • 리스트를 사용하면 컴파일할 때 바로 알 수 있다.
  • 여러분도 물론 컴파일 시에 알아채는 쪽을 선호할 것이다.
  • 배열은 실체화(reify) 된다.
  • 제네릭 타입은 실체화 불가 타입(non-reifiable type)
  • Long 배열에 String을 넣으려 하면 ArrayStoreExcpetion이 발생한다.
  • 애초에 경고의 원인을 제거하는 편이 훨씬 낫다.
  • 배열 대신 리스트
public class Chooser<T> {
    private final List<T> choiceList;

    public Chooser(Collection<T> choices) {
        choiceList = new ArrayList<>(choices);
    }

    public T choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceList.get(rnd.nextInt(choiceList.size()));
    }
}

핵심 정리

  • 제네릭은 불공변이고 타입 정보가 소거된다.
  • 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자.

item.29: 이왕이면 제네릭 타입으로 만들라.

  • E 와 같은 실체화 불가 타입으로는 배열을 만들 수 없다.
  • 자바가 리스트를 기본 타입으로 제공하지 않으므로 ArrayList 같은 제네릭 타입도 결국은 기본 타입인 배열을 사용해 구현해야 한다.
  • 또한 HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 한다.

핵심 정리

  • 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라.
  • 그렇게 하려면 제네릭 타입으로 만들어야 할 경우가 많다.
  • 기존 클라이언트에는 아무 영향을 주지 않으면서, 새로운 사용자를 훨씬 편하게 해주는 길이다.

item.30: 이왕이면 제네릭 메서드로 만들라.

  • 예컨대 Collections의 알고리즘 메서드는 모두 제네릭이다.
  • 코드에서 타입 매개변수 목록은 <E>이고 반환 타입은 Set<E>이다.
  • 매개변수의 명명 규칙은 제네릭 메서드나 제네릭 타입이나 똑같다.
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;

항등함수(identity function): Function.identity

  • 제네릭 싱글턴 팩터리 패턴
  • private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;

@SuppressWarnings("unchecked")
public static UnaryOperator identityFunction() {
return (UnaryOperator) IDENTITY_FN;
}

- 재귀적 타입 한정(recursive type bound)

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

- 형변환을 해줘야 하는 기존 메서드는 제네릭하게 만들자.

---

## item.31: 한정적 와일드카드를 사용해 API 유연성을 높이라
- 매개변수화 타입은 불공변(invariant)
- 하위 타입이 될 수 없다.(리스코프 치환 원칙에 어긋난다.)
- 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라.

### 펙스(PECS): producer-extends, consumer-super
- 매개변수화 타입 `T`가 생산자라면 `<? extends T>`를 사용하고,
- 소비자라면 `<? super T>`를 사용하라.
- 반환 타입에는 한정적 와일드 카드 타입을 사용하면 안된다.
- 클래스 사용자가 와일드카드 타입을 신경 써야 한다면 그 API에 무슨 문제가 있을 가능성이 크다.
- 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라.
- `List<?>`인데, `List<?>`에는 null 외에는 어떤 값도 넣을 수 없다는 데 있다.

### 핵심 정리
- 와일드카드 타입을 적용하면 API가 훨씬 유연해진다.
- 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용해줘야 한다.
- PECS
- 생산자(producer)는 extends
- 소비자(consumer)는 super를 사용한다.
- `Comparable`과 `Comparator`는 모두 소비자라는 사실도 잊지 말자.

---

## item.32: 제네릭과 가변인수를 함께 쓸때는 신중하라
- 제네릭과 varargs를 혼용하면 타입 안전성이 깨진다.
- 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.
- `@SafeVarargs` 애너테이션은 메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치다.
```java
static <T> List<T> flatten(List<List<? extends T>> lists) {
    List<T> result = new ArrayList<>();

    for (List<? Extends T> list : lists) {
        result.addAll(list);
    }

    return result;
}

핵심 정리

  • 가변인수와 제네릭은 궁합이 좋지 않다.
  • 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고,
  • 배열과 제네릭의 타입 규칙이 서로 다르기 때문이다.
  • 제네릭 varags 매개변수는 타입 안전하지는 않지만, 허용된다.
  • 메서드에 제네릭 (혹은 매개변수화된) varags 매개변수를 사용하고자 한다면,
  • 먼저 그 메서드가 타입 아전한지 확인한 다음 @SafeVarargs 애너테이션을 달아 사용하는 데 불편함이 없게끔 하자.

item.33: 타입 안전 이종 컨테이너를 고려하라.

  • 타입 안전 이종 (heterogeneous) 컨테이너
  • public class Favorites { private Map<Class<?>, Object> favorites = new HashMap<>(); public <T> void putFavorite(Class<T> type, T instance) { favorites.put(Objects.requireNonNull(type), instance); } public <T> T getFavorite(Class<T> type) { return type.cast(favorite.get(type)); } }
  • public class Favorites { public <T> void putFavorite(Class<T> type, T intance); public <t> T getFavorite(Class<T> type); }

슈퍼 타입 토큰 (super type token)

Favorites f = new Favorites();
List<String> pets = Arrays.asList("a", "b", "c");

f.putFavorite(new TypeRef<List<String>>(){}, pets);
List<String> listofStrings = f.getFavorite(new TypeRef<List<String>>(){});

item.34: int 상수 대신 열거 타입을 사용하라

  • 정수 열거 패턴 (int enum pattern) 기법에는 단점이 많다.
  • 타입을 보장할 방법이 없으며 표현력도 좋지 않다.
  • 정수 열거 패턴을 사용한 프로그램은 깨지기 쉽다.
  • 다시 컴파일하지 않은 클라이언트는 실행이 되더라도 엉뚱하게 동작할 것이다.
  • 문자열 비교에 따른 성능 저하 역시 당연한 것이다.
  • 대안을 제시: 열거 타입 (enum type)
  • public enum Apple { FUJI, PIPIN, GANNY_SMITH }
  • 인스턴스 통제된다.
  • 싱글턴을 일반화한 형태
public enum Operation {
    PLUS { public double apply(double x, double y) { return x + y; } },
    MINUS { public double apply(double x, double y) { return x - y; } },
    TIMES { public double apply(double x, double y) { return x * y; } },
    DIVIDE { public double apply(double x, double y) { return x / y; } },
    ;

    public abstract double apply(double x, double y);
}
  • switch 문은 열거 타입의 상수별 동작을 구현하는 데 적합하지 않다.
  • 기존 열거 타입에 상수별 동작을 혼합해 넣을 때는 switch 문이 좋은 선택이 될 수 있다.
  • 필요한 원소를 컴파일타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자.

전략 열거 타입 패턴

package effectivejava.chapter6.item34;

import static effectivejava.chapter6.item34.PayrollDay.PayType.*;

enum PayrollDay {
    MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
    THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
    SATURDAY(WEEKEND), SUNDAY(WEEKEND);

    private final PayType payType;

    PayrollDay(PayType payType) { this.payType = payType; }

    int pay(int minutesWorked, int payRate) {
        return payType.pay(minutesWorked, payRate);
    }

    enum PayType {
        WEEKDAY {
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked <= MINS_PER_SHIFT ? 0 :
                        (minsWorked - MINS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked * payRate / 2;
            }
        };

        abstract int overtimePay(int mins, int payRate);
        private static final int MINS_PER_SHIFT = 8 * 60;

        int pay(int minsWorked, int payRate) {
            int basePay = minsWorked * payRate;
            return basePay + overtimePay(minsWorked, payRate);
        }
    }

    public static void main(String[] args) {
        for (PayrollDay day : values())
            System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1));
    }
}

핵심 정리

  • 열거 타입은 확실히 정수 상수보다 뛰어나다.
  • 열거 타입에서는 switch 문 대신 상수별 메서드 구현을 사용하자.
  • 열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자.

item.35: ordinal 메서드 대신 인스턴스 피드를 사용하라

  • 상수 선언 순서를 바꾸는 순간 오동작하며,
  • 이미 사용 중인 정수와 값이 같은 상수는 추가할 방법이 없다.
  • 인스턴스 필드에 저장하자.
  • 열거 타입 기반의 범용 자료구조에 쓸 목적으로 설계되었다.
  • 따라서 이런 용도가 아니라면 ordinal 메서드는 절대 사용하지 말자.

item.36: 비트 필드 대신 EnumSet을 사용하라

  • 비트 필드 값이 그대로 출력되면 단순한 정수 열거 상수를 출력할 때보다 해석하기가 훨씬 어렵다.
  • 열거할 수 있는 타입을 한데 모아 집합 형태로 사용한다고 해도 비트 필드를 사용할 이유는 없다.

item.37: ordinal 인덱싱 대신 EnumMap을 사용하라

Map<Plant.LifeCycle, Set<Planet>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values())
  • 내부에서 배열을 사용하기 때문이다.
  • Map의 타입 안전성과 배열의 성능을 모두 얻어낸 것이다.
System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle)));
  • EnumMap을 써서 얻은 공간과 성능 이점이 사라진다는 문제가 있다.
System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle, () new EnumMap<>(LifeCycle.class), toSet())));
  • 스트림을 사용하면 EnumMap만 사용했을 때와는 살짝 다르게 동작한다.
  • EnumMap 버전은 언제나 식물의 생애주기당 하나씩의 중첩 맵을 만들지만,
  • 스트림 버전에서는 해당 생애주기에 속하는 식물이 있을 때만 만든다.
  • 예컨대 정원에 한해살이와 여러해살이 식물만 살고 두해살이가 없다면,
  • EnumMap 버전에서는 맵을 3개 만들고 스트림 버전에서는 2개만 만든다.
  • 실수로 잘못 수정하면 런타임 오류가 날것이다.
    • IndexOutOfBoundsException
    • NullPointerException
  • 다시 이야기하지만 EnumMap을 사용하는 편이 훨씬 낫다.
public enum Phase {
    SOLID, LIQUID, GAS;

    public enum Transition {
        MELT(SOLID, LIQUID),
        FREEZE(LIQUID, SOLID),
        ;

        private final Phase from;
        private final Phase to;

        Transition(Phase from, Phase to) {
            this.from = from;
            this.to = to;
        }

        private static final Map<Phase, Map<Phase, Transition>> m = Stream.of(values()).collect(groupingBy(t -> t.from, () -> new EnumMap<>(Phase.class), toMap(t -> t.to, t -> t, (x, y) -> y, () -> new EnumMap<>(Phase.class))));

        public static Transition from(Phase from, Phase to) {
            return m.get(from).get(to);
        }
    }
}

item.38: 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라.

  • 할 일은 Operation 인터페이스를 구현한 열거 타입을 작성하는 것뿐이다.
  • public static void main(String[] args) { double x = Double.parseDouble(args[0]); double y = Double.parseDouble(args[1]); test(ExtendedOperation.class, x, y); test2(Arrays.asList(ExtendedOperation.values()), x, y); }
  • public enum ExtendedOperation implements Operation { EXP("^") { public double apply(double x, double y) { return Math.pow(x, y); } }, REMAINDER("%") { public double apply(double x, double y) { return x % y; } }; private final String symbol; ExtendedOperation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } }

private static <T extends Enum & Operation> void test(Class opEnumType, double x, double y) {
for (Operation op : opEnumType.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}

private static void test2(Collection<? extends Operation> opSet, double x, double y) {
for (Operation op : opSet) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}

- 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 함께 사용해 같은 효과를 낼 수 있다.

---

## item.39: 명명 패턴보다 애너테이션을 사용하라
- 오타가 나면 안 된다.
- 무시하고 지나치기 때문에 개발자는 통과했다고 오해할 수 있다.
- 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다는 것이다.
- 매개변수로 전달할 마땅한 방법이 없다는 것이다.
- 아무 매개변수 없이 단순히 대상에 마킹(marking)한다는 뜻에서 마커(marker) 애너테이션이라 한다.
- 이를 처리하는 부분에서는 코드 양이 늘어나며, 특히 처리 코드가 복잡해져 오류가 날 가능성이 커짐을 명심하자.
- 애너테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다.
- 도구 제작자를 제외하고는, 일반 프로그래머가 애너테이션 타입을 직접 정의할 일은 거의 없다.
- 하지만 자바 프로그래머라면 예외 없이 자바가 제공하는 애너테이션 타입들은 사용해야 한다.
- 애너테이션을 사용하면 해당 도구가 제공하는 진단 정보의 품질을 높여줄 것이다.
- 단, 그 애너테이션들은 표준이 아니니 도구를 바꾸거나 표준이 만들어지면 수정 작업을 조금 거쳐야 할 것이다.

---

## item.40: @Override 애너테이션을 일관되게 사용하라
- 메서드 선언에만 달 수 있으며,
- 메서드를 재정의했음을 뜻한다.
- 일관되게 사용하면 여러 가지 악명 높은 버그들을 예방해준다.
- `Object.equals`를 재정의한다는 의도를 명시해야 한다.
```java
@Override 
public boolean equals(Object o) {
    if (!(o instanceof Bigram)) {
        return false;
    }
    Bigram b = (Bigram) o;

    return b.first == first && b.second == second;
}
  • 상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 애너테이션을 달자.
  • 구체 클래스에서 상위 클래스의 추상메서드를 재정의할 때는 굳이 @Override를 달지 않아도 된다.
  • 재정의 메서드 모두에 Override를 일괄로 붙여두는게 좋아 보인다면 그래도 상관없다.
  • 대부분의 IDE는 재정의할 메서드를 선택하면 @Override를 자동으로 붙여주니 참고하자.
  • 실수로 재정의했을때 경고해줄 것이다.
  • 추가했을 때 알려주는 컴파일 오류의 보완재 역할로 보면 되겠다.
  • IDE와 컴파일러 덕분에 우리는 의도한 재정의만 정확하게 해낼 수 있는 것이다.
  • 재정의한 모든 메서드에 @Override 애너테이션을 의식적으로 달면 여러분이 실수했을때 컴파일러가 바로 알려줄 것이다.
  • 예외는 한가지뿐이다. 구체 클래스에서 상위클래스의 추상 메서드를 재정의한 경우엔 이 애너테이션을 달지 않아도 된다.

item.41: 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라

  • 클래스가 특정 속성을 가짐을 표시해주는 인터페이스를 마커 인터페이스(marker interface)라 한다.
  • Serializable 마커 인터페이스를 보고 그 대상이 직력화할 수 있는 타입이지 확인한다.
  • 마커 애너테이션이 마커 인터페이스보다 나은 점으로는 거대한 애너테이션 시스템의 지원을 받는다는점을 들 수 있다.
  • 타입 정의가 목적이라면 마커 인터페이스를 선택하자.
  • 클래스나 인터페이스 외의 프로그램 요소에 마킹해야 하거나, 애너테이션을 적극 활용하는 프레임워크 일부로 그 마커를 편입시키고자 한다면 마커 애너테이션이 올바른 선택이다.
  • 적용 대상이 ElementType.TYPE인 마커 애너테이션을 작성하고 있다면, 잠시 여유를 갖고 정말 애너테이션으로 구현하는 게 옳은지, 혹은 마커 인터페이스가 낫지는 않을지 곰곰이 생각해보자.

item.42: 익명 클래스보다는 람다를 사용하라

  • 문자열을 길이순으로 정렬하는데, 정렬을 위한 비교 함수로 익명 클래스를 사용한다.
    // 익명 클래스의 인스턴스를 함수 객체로 사용 - 낡은 방법
    Collections.sort(words, new Comparator<String>() {
      public int compare(String s1, String s2) {
          return Integer.compare(s1.length(), s2.length());
      }
    });

람다식을 함수 객체로 사용 - 익명 클래스 대체

Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
  • 타입을 명시해야 코드가 더 명확할 때만 제외하고는,
  • 람다의 모든 매개변수 타입은 생략하자.
  • 컴파일러가 타입을 알 수 없다는 오류를 낼 때만 해당 타입을 명시하면 된다.
  • 반환값이나 람다식 전체를 형변환해야할 때도 있겠지만 아주 드물다.
    public enum Operation {
      PLUS ("+", (x, y) -> x + y),
      MINUS ("-", (x, y) -> x - y),
      ;
    
      private final String symbol;
      private final DoubleBinaryOperator op;
    
      Operation(String symbol, DoubleBinaryOperator op) {
          this.symbol = symbol;
          this.op = op;
      }
    
      @Override public String toString() {
          return symbol;
      }
    
      public double apply(double x, double y) {
          return op.applyAsDouble(x, y);
      }
    }
  • Collections.sort(words, comaringInt(String::length)); words.sort(comparingInt(String::length));
  • 클래스 몸체는 더 이상 사용할 이유가 없다고 느낄지 모르지만,
  • 람다는 이름이 없고 문서화도 못 한다.
  • 따라서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 말아야 한다.
  • 세 줄 안에 끝내는 게 좋다.
  • 세 줄을 넘어가면 가독성이 심하게 나빠진다.
  • 람다가 길거나 읽기 어렵다면 더 간단히 줄여보거나 람다를 쓰지 않는 쪽으로 리팩터링하길 바란다.
  • 람다의 시대가 열리면서 익명 클래스는 설 자리가 크게 좁아진 게 사실이다.
  • 하지만 람다로 대체할 수 없는 곳이 있다.
  • 람다는 함수형 인터페이스에서만 쓰인다.
    • 추상 클래스의 인스턴스는 익명 클래스를 사용해야 함
  • 람다에서의 this 키워드는 바깥 인스턴스를 가리킨다.
  • 익명 클래스에서의 this는 익명 클래스의 인스턴스 자신을 가리킨다.
  • 자신을 참조해야 한다면 반드시 익명 클래스를 써야 한다.
  • 직렬화 형태가 구현별로 다를 수 있다.
  • 람다를 직렬화하는 일은 극히 삼가야 한다.

item.43: 람다보다는 메서드 참조를 사용하라

  • 가장 큰 특징은 간결함
  • 메서드 참조(method reference)
  • 멀티셋(multiset)을 구현한 게 된다.
    map.merge(key, 1, (count, incr) -> count + incr);
  • 키, 값, 함수를 인수로 받으며,
  • 주어진 키가 맵 안에 아직 없다면 주어진 {키, 값} 쌍을 그대로 저장한다.
  • 반대로 키가 이미 있다면 함수를 현재 값과, 주어진 값에 적용한다음, 그 결과로 현재 값을 덮어쓴다.
  • count, incr은 크게 하는 일 없이 공간을 꽤 차지한다.
    map.merge(key, 1, Integer::sum);
  • 메서드 참조로 제거할 수 있는 코드양도 늘어난다.
  • 람다로 할 수 없는 일이라면 메서드 참조로도 할 수 없다.
  • 메서드 참조가 좋은 대안이 되어준다.
  • 새로운 메서드에 담은 다음, 람다 대신 그 메서드 참조를 사용하는 식이다.
  • 매핑과 필터 함수에 쓰인다.
    Integer::parseInt
    Instant.now()::isAfter
    String::toLowerCase
    TreeMap<K,V>::new
    int[]::new
  • 메서드 참조 쪽이 짧고 명확하다면 메서드 참조를 쓰고, 그렇지 않을 때만 람다를 사용하라.

item.44: 표준 함수형 인터페이스를 사용하라

protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return size() > 100;
}
  • 필요한 용도에 맞는 게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하라.
  • 표준 함수형 인터페이스 대부분은 기본 타입만 지원한다.
  • 그렇다고 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말자.
  • 특히 계산량이 많을 때는 성능이 처참히 느려질 수 있다.
  • 의도를 명시하는 것으로,
  • 인터페이스가 람다용으로 설계된 것임을 알려준다.
  • 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다.
  • 누군가 실수로 메서드를 추가하지 못하게 막아준다.
  • 직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 애너테이션을 사용하라.
  • 이제 자바도 람다를 지원한다.
  • 여러분도 지금부터 API를 설계할 때 람다도 염두에 두어야 한다는 뜻이다.
  • 입력과 반환값에 함수형 인터페이스 타입을 활용하라.
  • 보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 가장 좋은 선택이다.
  • 단, 흔치는 않지만 직접 새로운 함수형 인터페이스를 만들어 쓰는 편이 나을 수도 있음을 잊지 말자.

item.45: 스트림은 주의해서 사용하라

  • 다량의 데이터 처리 작업(순차적이든 병렬적이든)
  • 스트림(stream)은 데이터 원소의 유한 혹은 무한 시퀀스(sequence)를 뜻한다.
  • 스트림 파이프라인(stream pipelin)은 원소들로 수행하는 연산 단계를 표현하는 개념이다.
  • 컬렉션, 배열, 파일, 정규표현식, 패턴 매치(matcher), 난수 생성기, 혹은 다른 스트림
  • 종단 연산(terminal operation)으로 끝나며,
  • 그 사이 하나 이상의 중간 연산(intermediate operation)이 있을 수 있다.
  • 각 중간 연산은 스트림을 어떠한 방식으로 변환(transform)한다.
  • 파이프라인은 지연 평가(lazy evaluation)된다.
  • no-op과 같으니, 종단 연산을 빼먹는 일이 절대 없도록 하자.
  • 파이프라인은 순차적으로 수행된다.
  • parallel 메서드를 호출해주기만 하면 되나, 효과를 볼 수 있는 상황은 많지 않다.
  • 원소 수가 많은 아나그램(anagram) 그룹을 출력한다.
  • 알파벳이 같고 순서가 다른 단어를 말한다.
  • 스트림을 과용하면 프로그램이 읽거나 유지보수하기 어려워진다.
  • 람다에서는 타입 이름을 자주 생략하므로 매개변수 이름을 잘 지어야 스트림 파이프라인이 가독성이 유지된다.
  • 도우미 메서드를 적절히 활용하는 일의 중요성은 일반 반복 코드에서보다는 스트림 파이프라인에서 훨씬 크다.
    "Hello world!".chars().forEach(System.out::println);
  • 스트림으로 바꾸는 게 가능할지라도 코드 가독성과 유지보수 측면에서는 손해를 볼 수 있기 때문이다.
  • 스트림과 반복문을 적절히 조합하는 게 최선이다.
  • 기존 코드는 스트림을 사용하도록 리팩터링하되, 새 코드가 더 나아 볼 때만 반영하자.
  • 동시에 접근하기는 어려운 경우다.
  • 원래의 값은 잃는 구조이기 때문이다.
  • 저장하는 객체를 사용해 매핑하는 우회 방법도 있지만,
  • 이런 방식은 코드 양도 많고 지저분하여 스트림을 쓰는 주목적에서 완전히 벗어난다.
  • 가능한 경우라면, 앞 단계의 값이 필요할 때 매핑을 거꾸로 수행하는 방법이 나을 것이다.

평탄화(flattening)

  • as-is
    private static List<Card> newDeck() {
      List<Card> result = new ArrayList<>();
      for (Suit suit : Suit.values()) {
          for (Rank rank : Rank.values()) {
              result.add(new Card(suit, rank));
          }
      }
      return result;
    }
  • to-be
    private static List<Card> newDeck() {
      return Stream.of(Suit.values())
          .flatMap(suit -> Stream.of(Rank.values())
              .map(rank -> new Card(suit, rank)))
          .collect(toList());
    }
  • 조합했을 때 가장 멋지게 해결된다.
  • 스트림과 반복 중 어느 쪽이 나은지 확신하기 어렵다면 둘 다 해보고 더 나은 쪽을 택하라.

item.46: 스트림에서는 부작용 없는 함수를 사용하라

  • 스트림 패러다임의 핵심은 계산을 일련의 반환(transformation)으로 재구성하는 부분이다.
  • 순수 함수여야 한다.
    • 순수 함수란 오직 입력만이 결과에 영향을 주는 함수를 말한다.
    • 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다.
    • 모두 부작용(side effect) 이 없어야 한다.
  • forEach 연산은 스트림 계산 결과를 보고할 대만 사용하고, 계산하는 데는 쓰지 말자.
  • 이 코드는 수집기(collector)를 사용하는데, 스트림을 사용하려면 꼭 배워야 하는 새로운 개념이다.
    • toList, toSet, toCollection(collectionFactory)
      List<String> topTen = freq.keySet().stream()
      .sorted(comparing(freq::get).reversed())
      .limit(10)
      .collect(toList());
      private static final Map<String, Operation> stringToEnum = Stream.of(values()).collect(toMap(Object::toString, e -> e));

toMap은 충돌이 나면 마지막 값을 취하는(last-write-wins) 수집기를 만들 때도 유용하다.

toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal);
words.collect(groupingBy(word -> alphabetize(word)))

toCollection(collectionFactory)

Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));
  • summing, averaging, summarizing
  • filtering, mapping, flatMapping, collectingAndThen
  • minBy, maxBy

joining

  • 원소들을 연결하는 (concatenate)
  • 구분자 (delimiter)
  • 예컨대 구분문자로 쉼표(,)를 입력하면 CSV 형태의 문자열을 만들어준다.
    • (단, 스트림에 쉼표를 이미 포함한 원소가 있다면 구분문자와 구별되지 않으니 유념하자.)
  • 구분 문자에 더해 접두문자(prefix)
  • 접미문자(suffix)도 받는다.
  • 예컨대 접두, 구분, 접미문자를 각각 [ , ]로 지정하여 얻은 수집기는 [came, swa, conquered]처럼 마치 컬렉션을 출력한 듯한 문자열을 생성한다.

핵심 정리

  • forEach는 스트림이 수행한 계싼 결과를 보고할 때만 이용해야 한다.
  • 계산 자체에는 이용하지 말자.
  • 스트림을 올바로 사용하려면 수집기를 잘 알아둬야 한다.
  • 가장 중요한 수집기 팩터리는 toList, toSet, toMap, groupingBy, joining

item.47: 반환 타입으로는 스트림보다 컬렉션이 낫다

  • Iterable 인터페이스
  • 스트림과 반복을 알맞게 조합해야 좋은 코드가 나온다.
  • 사실 Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함할 뿐만 아니라,
  • Iterable 인터페이스가 정의한 방식대로 동작한다.
  • 그럼에도 for-each로 스트림을 반복할 수 없는 까닭은 바로 StreamIterable을 확장(extend)하지 않아서다.
  • 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안 된다.
  • 스트림으로 처리하기를 원하는 사용자와 반복으로 처리하길 원하는 사용자가 모두 있음을 떠올리고,
  • 양쪽을 다 만족시키려 노력하자.
  • 컬렉션을 반환할 수 있다면 그렇게 하라.

item.48: 스트림 병렬화는 주의해서 적용하라.

  • 동시성 프로그램 측면에서 자바는 항상 앞서갔다.
  • wait / notify

속도를 높이고 싶어 스트림 파이프라인의 parallel()을 호출하겠다는 순진한 생각을 했다고 치자.

  • CPU는 90%나 잡아먹는 상태가 무한히 계속된다.
  • 종국에는 완료될지도 모르겠으나,
  • 1시간 반이나 지나 강제 종료할 때까지 아무 결과도 출력하지 않았다.
  • 스트림 라이브러리가 이 파이프라인을 병렬화하는 방법을 찾아내지 못했기 때문이다.
  • 환경이 아무리 좋더라도 데이터 소스가 Stream.iterate거나 중간 연산으로 limit`를 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없다.
  • 스트림 파이프라인을 마구잡이로 병렬화하면 안 된다.
  • 성능이 오히려 끔찍하게 나빠질 수 있다.

대체로 스트림의 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스거나 배열, int 범위, long 범위일 때 병렬화의 효과가 가장 좋다.

  • 이 자료구조들은 모두 데이터를 원하는 크기로 정확하고 손쉽게 나눌 수 있어서 일을 다수의 스레드에 분배하기 좋다는 특징이 있다.
  • 나누는 작업은 Spliterator가 담당하며
    • Spliterator 객체는 Stream 이나 iterableSpliterator 메서드로 얻어올 수 있다.

참조 지역성 (locality of reference)이 뛰어나다는 것이다.

  • 참조들이 메모리에 연속해서 저장되어 있다는 뜻이다.
  • 하지만 참조들이 가리키는 실제 객체가 메모리에서 서로 떨어져 있을 수 있는데, 그러면 참조 지역성이 더 나빠진다.
  • 메모리로 전송되어 오기를 기다리며 대부분 시간을 멍하니 보내게 된다.
  • 따라서 참좆 지역성은 다량의 데이터를 처리하는 벌크 연산을 병렬화할 때 아주 중요한 요소로 작용한다.
  • 참조 지역성이 가장 뛰어난 자료구조는 기본 타입의 배열이다.
  • 기본 타입 배열에서는 (참조가 아닌) 데이터 자체가 메모리에 연속해서 저장되기 때문이다.

축소 (reduction)

  • min, max, count, sum
  • 가변축소(mutable reduction): anyMatch, allMatch, noneMatch
    • Streamcollect 메소드는 병렬화에 적합하지 않다.
    • 컬렉션들을 합치는 부담이 크기 때문이다.
  • 스트림을 잘못 병렬화하면 (응답 불가를 포함해) 성능이 나빠질 뿐만 아니라 결과 자체가 잘못되거나 예상 못한 동작이 발생할 수 있다.
  • 결과가 잘못되거나, 오동작: 안전 실패(safety failure)라 한다.
  • 만족하고(associative)
  • 간섭받지 않고(non-interfering)
  • 상태를 갖지 않아야(stateless)
  • 하지만 병렬로 수행하면 참혹한 실패로 이어지기 십상이다.
  • 순차 버전처럼 정렬하고 싶다면 forEach -> forEachOrdered

스트림 병렬화는 오직 성능 최적화 수단임을 기억해야 한다.

  • 테스트하여 병렬화를 사용할 가치가 있는지 확인해야 한다.
  • 조건이 잘 갖춰지면 parallel 메서드 호출 하나로 거의 프로세서 코어 수에 비례하는 성능 향상을 만끽할 수 있다.
    static long pi(long n) {
      return LongStream.rangeClosed(2, n)
          .mapToObj(BigInteger::valueOf)
          .filter(i -> i.isProbablePrime(50))
          .count();
    static long pi(long n) {
      return LongStream.rangeClosed(2, n)
          .parallel()
          .mapToObj(BigInteger::valueOf)
          .filter(i -> i.isProbablePrime(50))
          .count();
  • 하지만 n이 크다면 이 방식으로 계산하는 건 좋지 않다.
  • 래머의 공식(Lehmer's Formula)이라는 훨씬 효율적인 알고리즘이 있기 때문이다.
  • 무작위 수들로 이뤄진 스트림을 병렬화하려거든 ThreadLocalRandom 보다는 SplittableRandom 인스턴스를 이요하자.
  • 한편 ThreadLocalRandom은 단일 스레드에서 쓰고자 만들어졌다.
  • 병렬 스트림용 데이터 소스로도 사용할 수는 있지만 SplittableRandom만큼 빠르지는 않을 것이다.
  • 그냥 Random은 모든 연산을 동기화하기 때문에 병렬 처리하면 최악의 성능을 보일 것이다.

핵심 정리

  • 스트림을 잘못 병렬화하면 프로그램을 오작동하게 하거나 성능을 급격히 떨어뜨린다.
  • 병렬화하는 편이 낫다고 믿더라도,
  • 수정 후의 코드가 여전히 정확한지 확인하고 운영 환경과 유사한 조건에서 수행해보며 성능지표를 유심히 관찰하라.
  • 그래서 계산도 정확하고 성능도 좋아졌음이 확실해졌을 때, 오직 그럴때만 병렬화 버전 코드를 운영 코드에 반영하라.

item.49: 매개변수가 유효한지 검사하라

  • 입력 매개변수의 값이 특정 조건을 만족하기를 바란다.
  • 인덱스 값은 음수이면 안 되며, 객체 참조는 null이 아니어야 하는 식이다.
  • 메서드 몸체가 시작되기 전에 검사해야 한다.
  • 오류는 가능한 한 빨리 (발생한 곳에서) 잡아야 한다.
  • 오류를 발생한 즉시 잡지 못하면 해당 오류를 감지하기 어려워지고, 감지하더라도 오류의 발생 지점을 찾기 어려워진다.
  • 메서드 몸체가 실행되기 전에 매개변수를 확인한다면 잘못된 값이 넘어왔을 때 즉각적이고 깔끔한 방식으로 예외를 던질 수 있다.
  • 매개 변수 검사에 실패하면 실패 원자성(failure atomicity)

매개 변수 값이 잘못됐을 때 던지는 예외를 문서화해야 한다.(@throws 자바독 태그를 사용)

  • IllegalArgumentException
  • IndexOutOfBoundsException
  • NullPointerException
  • 매개 변수의 제약을 문서화 한다면 그 제약을 어겼을 때 발생하는 예외도 함께 기술
  • API 사용자가 제약을 지킬 가능성을 크게 높일 수 있다.

java.util.Objects.requireNonNull

this.strategy - Objects.requireNonNull(strategy, "전략");
  • 반환값은 그냥 무시하고 필요한 곳 어디서든 순수한 null 검사 목적

assert

private static void sort(log a[], int offset, int length) {
    assert a != null;
    assert offset >= 0 && offset <= a.length;
    assert length >= 0 && length <= a.length - offset;
    ...
  • 실패하면 AssertionError
  • 런타임에 아무런 효과도, 아무런 성능 저하도 없다.

주의

  • 메서드는 최대한 범용적으로 설계
  • 무언가 제대로 할 수 있다면 매개변수 제약은 적을 수록 좋다.
  • 하지만 구현이라는 개념 자체가 특정한 제약을 내재한 경우도 드물지 않다.

핵심 정리

  • 제약들을 문서화 하고 메서드 코드 시작 부분에서 명시적으로 검사해야 한다.
  • 유효성 검사가 실제 오류를 처음 걸러낼 때 충분히 보상받을 것이다.

item.50: 적시에 방어적 복사본을 만들라

  • 클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.
  • 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(defensive copy)해야 한다.
    public Period(Date start, Date end) {
      this.start = new Date(start.getTime());
      this.end = new Date(end.getTime());
  • 매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자
  • 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
  • 컴퓨터 보안: 검사시점/사용시점(time-of-check/time-of-use) 공격; TOCTOU 공격
  • 제 3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.
  • 방어적 복사에는 성능 저하가 따르고, 또 항상 쓸 수 있는 것도 아니다.

핵심 정리

  • 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야 한다.
  • 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면
  • 방어적 복사를 수행하는 대신 해당 구성 요소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하도록 하자.

item.51: 메서드 시그니처를 신중히 설계하라

  • 메서드 이름을 신중히 짓자
    • 이해, 일관, 널리 사용하는 이름(커뮤니티)
    • 긴 이름을 피하자
  • 편의 메서드를 너무 많이 만들지 말자.
    • 메서드가 너무 많은 클래스는 익히고, 사용하고, 문서화하고, 테스트하고, 유지보수하기 어렵다.
    • 구현하는 사람과 사용하는 사람 모두를 고통스럽게 한다.
    • 자주 쓰일 경우에만 별도의 약칭 메서드를 두기 바란다.
    • 확신이 서지 않으면 만들지 말자.
  • 매개변수 목록은 짧게 유지하자.
    • 4개 이하가 좋다.
    • 같은 타입의 매개변수 여러 개가 연달아 나오는 경우가 특히 해롭다.
    • 매개변수 순서를 기억하기 어려울뿐더러, 실수로 순서를 바꿔 입력해도 그대로 컴파일되고 실행된다.
    • 의도가 다르게 동작한다.

매개변수 목록 짧게 줄이기 1: 여러 메서드로 쪼갠다.

  • 쪼개진 메서드 각각은 원래 매개변수 목록의 부분집합을 받는다.
  • 잘못하면 메서드가 너무 많아질 수 있지만, 직교성(orthogonality)을 높여 오히려 메서드 수를 줄여주는 효과도 있다.
    • 서로 영향을 주는 성분이 전혀 없다는 뜻
    • 직교성이 높다.: 공통점이 없는 기능들이 잘 분리되어 있다.
    • 기능을 원자적으로 쪼개 제공한다.
    • 개별 메서드로 제공해야 직교성이 높다고 할 수 있다.
    • 직교성을 높여 오히려 메서드 수를 줄여주는 효과도 있다.
    • 원자적으로 쪼개다 보면, 자연스럽게 중복이 줄고 결합성이 낮아져 코드를 수정하기 수월해진다.
    • 테스트하기 쉬워짐
    • 가볍고 구현하기 쉽고 유연하고 강력하다.
  • 무한정 작게 나누는게 능사는 아니다.
  • 사용자의 눈높이에 맞게, 즉 API가 다루는 개념의 추상화 수준에 맞게 조절해야 한다.
  • 주 사용되거나 최적화하여 성능을 크게 개선할 수 있다면 직교성이 낮아지더라도 편의기능으로 제공하는 편이 나을 수도 있다.
  • 마이크로서비스(microservice) 아키텍처는 직교성이 높고
  • 모놀리식(monolithic) 아키텍처는 직교성이 낮다고 할 수 있다.

매개변수 목록 짧게 줄이기 2: 도우미 클래스

  • 매개변수의 타입으로는 클래스보다는 인터페이스가 더 낫다.
    • Map
  • 특정 구현체에 옮겨 담느라 비싼 복사 비용을 치르지 않아도 됨
  • boolean 보다는 원소 2개짜리 열거 타입이 낫다
  • 코드를 읽고 쓰기 더 쉬워진다.

item.52: 다중정의는 신중히 사용하라

  • 다중정의(overloading, 오버로딩)
  • 재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택되기 때문이다.
  • 가장 하위에서 정의한 재정의 메서드가 실행되는 것이다.
  • 다중정의가 혼동을 일으키는 상황을 피해야 한다.
  • 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자.
  • 다중정의하는 대신 메서드 이름을 다르게 지어주는 길도 항상 열려 있으니 말이다.
  • 오토박싱이 도입되면서 평화롭던 시대가 막을 내렸다.
  • 자바 언어에 제네릭과 오토박싱을 더한 결과 List 인터페이스가 취약해졌다.
  • 다행히 같은 피해를 입는 API는 거의 없지만, 다중 정의시 주의를 기울여야할 근거로는 충분하다.
  • 적용성 테스트(applicability test) 때 무시된다.
  • 메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안 된다.
  • forward 것이다.
  • 같은 객체를 건네더라도 전혀 다른 일을 수행한다.
  • 혼란을 불러올 수 있는 잘못된 사례로 남게 되었다.

핵심 정리

  • 언어가 다중정의를 허용한다고 해서 다중정의를 꼭 활용하란 뜻은 아니다.
  • 매개변수 수가 같을 대는 다중정의를 피하는 게 좋다.
  • 형변환하여 정확한 다중정의 메서드가 선택되도록 해야 한다.

item.53: 가변인수는 신중히 사용하라

static int sum(int... args) {
    int sum = 0;
    for (int arg : args) {
        sum += arg;
    }
    return sum;
}
static int min(int firstArg, int... remainingArgs) {
    int min = firstArg;
    for (int arg : remainingArgs) {
        if (arg < min) {
            min = arg;
        }
    }
    return min;
}

핵심 정리

  • 인수 개수가 일정하지 않은 메서드를 정의해야 한다면 가변 인수가 반드시 필요하다.
  • 메서드 정의할 때 필수 매개변수는 가변인수 앞에 두고, 가변인수를 사용할 때는 성능 문제까지 고려하자.

item.54: null이 아닌, 빈 컬렉션이나 배열을 반환하라

  • 사실 재고가 없다고 해서 특별히 취급할 이유는 없다.
  • null을 반환한다면, 클라이언트는 이 null 상황을 처리하는 코드를 추가로 작성해야 한다.
  • 상황을 특별히 취급해줘야 해서 코드가 더 복잡해진다.
  • 빈 컨테이너를 할당하는 데도 비용이 드니 null 을 반환하는 쪽이 낫다는 주장도 있다.
  • 하지만 이는 두 가지 면에서 틀린 주장이다.
    • 성능 분석 결과 이 할당이 성능 저하의 주범이라고 확인되지 않는 한 이정도의 성능 차이는 신경 쓸 수준이 못 된다.
    • 빈 컬렉션과 배열은 굳이 새로 할당하지 않고도 반활 할 수 있다.
      public List<Cheese> getCheeses() {
      return cheesesInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(cheesesInStock);
      }
  • 배열을 쓸 때도 마찬가지다.
  • 절대 null을 반환하지 말고 길이가 0인 배열을 반환하라.
    private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
    

public Cheese[] getCheeses() {
return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}


### 핵심 정리
- null이 아닌, 빈 배열이나 컬렉션을 반환하라.
- null을 반환하는 API는 사용하기 어렵고 오류 처리 코드도 늘어난다.
- 그렇다고 성능이 좋은 것도 아니다.

---

## item.55: 옵셔널 반환은 신중히 하라
- 예외를 던지거나, null을 반환하는 것이다.
- 예외는 진짜 예외적인 상황에서만 사용해야 하며, 예외를 생성할 때 스택 추적 전체를 캡처하므로 비용이 크다.
- null을 반환하면 문제가 생기지 않겠지만, 나름의 문제가 있다.
- null을 반환할 수 있는 메서드를 호출할 때는, 별도의 null 처리 코드를 추가해야 한다.
- null 처리를 무시하고 반환된 null 값을 어딘가에 저장해두면 언젠가 NullPointerException이 발생할 수 있다.

### `Optional<T>`
- 비었다, 비지 않았다. 불변 컬렉션
- 메서드보다 오류가능성이 작다.
- 빈 옵셔널: `Optional.empty()`
- 값 옵셔널: `Optional.of(value)`
  - `null` 을 넣으면 `NullPointerException` 발생시킴
- `Option.ofNullable(value)` null 가능
- 옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자.
- 옵셔널은 검사 예외와 취지가 비슷하다.
```java
max(words).orElse("단어 없음...");
max(toys).orElseThrow(TemperTantrumException::new);
max(Elements.NOBLE_GASES).get();
  • 옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.

핵심 정리

  • 옵셔널을 반환해야 할 상황일 수 있다.
  • 하지만 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있다.
  • 그리고 옵셔널을 반환값 이외의 용도로 쓴느 경우는 매우 드물다.

item.56: 공개된 API 요소에는 항상 문서화 주석을 작성하라

  • API를 쓸모 있게 하려면 잘 작성된 문서도 곁들여야 한다.
  • 자바독(javadoc)이라는 유틸리티가 귀찮은 작업을 도와준다.
  • 소스코드 파일에서 문서화 주석(doc comment; 주바독 주석)이라는 특수한 형태로 기술된 설명을 추려 API 문서로 변환해준다.
  • 문서화 주석 작성법(How to Write Doc Comments)
  • 여러분의 API를 올바로 문서화하려면 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야 한다.
  • 메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 명료하게 기술해야 한다.
  • 제네릭 타입이나 제네릭 메서드를 문서화할 때는 모든 타입 매개변수에 주석을 달아야 한다.
  • 열거 타입을 문서화할 때는 상수들에도 주석을 달아야 한다.
  • 애너테이션 타입을 문서화할 때는 멤버들에도 모두 주석을 달야아 한다.
  • 클래스 혹은 정적 메서드가 스레드 안전하든 그렇지 않든, 스레드 안전 수준을 반드시 API 설명에 포함해야 한다.
  • 잘 쓰인 문서인지 확인하는 방법은 자바독 유틸리티가 생성한 웹페이지를 읽어보는 길뿐이다.

핵심 정리

  • 문서화 주석은 여러분 API를 문서화하는 가장 훌륭하고 효과적인 방법이다.
  • 공개 API라면 빠짐없이 설명을 달아야 한다.
  • 표준 규약을 이관되게 지키자.
  • 문서화 주석에 임의의 HTML 태그를 사용할 수 있음을 기억하라.
  • 단 HTML 메타 문자는 특별하게 취급해야 한다.

item.57: 지역변수의 범위를 최소화하라

  • 클래스와 멤버의 접근 권한을 최소화하라
  • 지역변수의 유효 범위를 최소로 줄이면 코드 가독성과 유지보수성이 높아지고 오류 가능성은 낮아진다.
  • 지역변수의 범위를 줄이는 가장 강력한 기법은 역시 가장 처음 쓰일 때 선언하기
    • 미리 선언부터 해두면 코드가 어수선해져 가독성이 떨어진다.
    • 타입과 초깃값이 기억나지 않을 수도 있다.
  • 거의 모든 지역변수는 선언과 동시에 초기화해야 한다.
  • try 블록 안에서 초기화
    • try 블록 바깥에서도 사용해야 한다면 try 블록 앞에서 선언해야 한다.

for > while

  • 그리고 for 키워드와 몸체 사이의 괄호 안으로 제한된다.
  • 반복문이 종료된 뒤에도 써야 하는 사오항이 아니라면 while 문보다 for 문을 사용하는게 낫다.
  • while 문에는 복사해 붙여넣기 오류가 있다.
  • while 문보다 짧아서 가독성이 좋다.
  • 메서드를 작게 유지하고 한 가지 기능에 집주하는 것

item.58: 전통적인 for 문보다는 for-each 문을 사용하라

  • while 문보다는 낫지만 가장 좋은 방법은 아니다.
  • for-each 문을 사용하면 모두 해결된다.
  • 향상된 for 문 (enhanced for statement)
    for (Element e : elements) {
      ...
    }

사용하지 못하는 경우

  • 파괴적인 필터링(destructive filtering)
  • 변형(transforming)
  • 병렬반복(parallel iteration)

Iterable 인터페이스를 구현한 객체라면 무엇이든 순회할 수 있다.

public interface Iterable<E> {
    Iterator<E> iterator();
}
  • 구현해두면 그 타입을 사용하는 프로그래머가 for-each 문을 사용할 때마다 여러분에게 감사해할 것이다.

핵심 정리

  • 명료하고
  • 유연하고
  • 버그를 예방해준다.
  • 성능 저하도 없다.

item.59: 라이브러리를 익히고 사용하라

  • ThreadLocalRandom.nextInt(int)
    • 의사난수 생성기
    • 정수론
    • 2의 보수 계산
    • 직접 해결하지 않고 라이브러리르 쓰면됨
  • 20년 가까이 수백만의 개발자가 열심히 사용했지만 버그가 보고된 적이 없다.
  • 버그가 발견 되더라도 다음 릴리즈에서 수정될 것이다.
  • 표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 여러분보다 앞서 사용한 다른 프로그래머들의 경험을 활용할 수 있다.

핵심적인 일과 크게 관련 없는 문제를 해결하느라 시간을 허비하지 않아도 된다는 것이다.

  • 프로그래머들은 하부 공사를 하기보다는 애플리케이션 기능 개발에 집중하고 싶어 한다.

따로 노력하지 않아도 성능이 지속해서 개선된다는 점이다.

  • 사용자가 많고, 업계 표준 벤치마크를 사용해 성능을 확인하기 때문에
  • 표준 라이브러리 제작자들은 더 나은 방법을 꾸준히 모색할 수밖에 없다.

기능이 점점 많아진다.

  • 커뮤니티에서 이야기가 나오고 논의도니 후 다음 릴리즈에 해당 기능이 추가되곤 한다.

여러분이 작성한 코드가 많은 사람에게 낯익은 코드가 된다는 것이다.

  • 자연스럽게 다른 개발자들이 더 읽기 좋고, 유지보수하기 좋고, 재활용하기 쉬운 코드가 된다.

라이브러리가 방대하여 모든 API 를 공부하기는 벅차겠지만 자바 프로그래머라면 적어도 하위 패키지들에는 익숙해져야 한다.

java.lang
java.util
java.io
  • 매년 아주 빠르게 성장하고 있으니, 모든 기능을 요약하는 건 무리다.

핵심 정리

  • 바퀴를 다시 발명하지 마자.
  • 아주 특별한 나만의 기능이 아니라면, 누군가 이미 라이브러리 형태로 구현해놓았을 가능성이 크다.
  • 그런 라이브러리가 있다면, 쓰면 된다.
  • 있는지 잘 모르겠다면 찾아보라.
  • 일반적으로 라이브러리의 코드는 여러분이 직접 작성한 것보다 품질이 좋고, 점차 개선될 가능성이 크다.
  • 여러분의 실력을 폄하하는 게 아니다.
  • 코드 품질에도 규모의 경제가 적용된다.
  • 즉, 라이브러리 코드는 개발자 각자가 작성하는 것보다 주목을 훨씬 많이 받으므로 코드 품질도 그만큼 높아진다.

item.60: 정확한 답이 필요하다면 float와 double은 피하라

  • 금융 계산에는 BigDecimal , int, long을 사용해야 한다.
  • 기본 타입보다 쓰기가 훨씬 불편하고, 느리다. 단발성 계산이라면 느리다는 문제는 무시할 수 있지만,
  • 쓰기 불편하다는 점은 못내 아쉬울 것이다.
  • 값의 크기가 제한되고, 소수점을 직접 관리해야 한다.
  • 이번 예에서는 모든 계산을 달러 대신 센트로 수행하면 이 문제가 해결된다.
  • 다음은 이 방식으로 구현해본 코드다.

핵심 정리

  • 정확한 답이 필요한 계산에는 float나 double을 피하라.
  • BigDecimal 이 제공하는 여덟 가지 반올림 모드를 이용하여 반올림을 완벽히 제어할 수 있다.
  • 법으로 정해진 반올림을 수행해야 하는 비즈니스 계산에서 아주 편리한 기능이다.
  • 반면, 성능이 중요하고 소수점을 직접 추적할 수 있고 숫자가 너무 크지 않다면 int, long을 사용하라.
  • 숫자를 아홉 자리 십진수로 표현할 수 있다면 int를 사용하고,
  • 열여덟 자리 십진수로 표현할 수 있다면 long을 사용하라.
  • 열여덟 자리를 넘어가면 BigDecimal을 사용해야 한다.

item.61: 박싱된 기본 타입보다는 기본 타입을 사용하라

  • int, double, boolean
  • Integer, Double, Boolean
  • 박싱된 기본 타입은 값에 더해 식별성(identity) 속성을 갖는다.
  • 기본 타입의 두 인스턴스는 값이 같아도 서로 다르다고 식별될 수 있다.
  • 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 유효하지 않은 값, null 을 갖을 수 있다.
  • 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.
  • 기본 타입과 박싱된 타입을 호뇽한 연산에서는 박싱된 기본 타입의 박싱이 자동으로 풀린다.
  • 박싱과 언박싱이 반복해서 일어나 체감될 정도로 성능이 느려진다.
  • 리플렉션을 통해 메서드를 호출할 때도 박싱도니 기본 타입을 사용해야 한다.

핵심 정리

  • 기본 타입은 간단하고 빠르다.
  • 오토박싱이 박싱된 기본 타입을 사용할 때의 번거로움을 줄여주지만, 그 위험까지 없애주지는 않는다.
  • 언박싱 과정에서 NullPointerException을 던질 수 있다.
  • 기본 타입을 박싱하는 작업은 필요 없는 객체를 생성하는 부작용을 나을 수 있다.

item.62: 다른 타입이 적절하다면 문자열 사용을 피하라

  • 문자열은 다른 값 타입을 대신하기에 적합하지 않다.
  • 문자열은 열거 타입을 대신하기에 적합하지 않다.
  • 문자열은 혼합 타입을 대신하기에 적합하지 않다.
  • 문자열 파싱은 느리고, 귀찮고, 오류 가능성도 크다.
  • 차라리 전용 클래스를 새로 만드는 편이 낫다.
  • 문자열은 권한을 표현하기에 적합하지 않다.

핵심 정리

  • 더 적합한 데이터 타입이 있거나 새로 작성할 수 있다면,
  • 문자열을 쓰고 싶은 유혹을 뿌리쳐라.
  • 문자열은 잘못 사용하면 번거롭고, 덜 유연하고, 느리고, 오류 가능성도 크다.
  • 문자열을 잘못 사용하는 흔한 예로는 기본 타입, 열거 타입, 혼합 타입이 있다.

item.63: 문자열 연결은 느리니 주의하라

  • 문자열 연결 연산자(+)는 여러 문자열을 하나로 합쳐주는 편리한 수단이다.
  • 문자열 연결 연산자로 문자열 n개를 잇는 시간은 n^2에 비례한다.
  • 성능을 포기하고 싶지 않다면, StringBuilder를 사용하자.

item.64: 객체는 인터페이스를 사용해 참조하라

  • 적합한 인터페이스만 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라.
    Set<Son> sonSt = new LikedhashSet<>();
  • 인터페이스를 타입으로 사용하는 습관을 길러두면 프로그램이 훨씬 유연해질 것이다.
  • 코드는 전혀 손대지 않고 새로 구현한 클래스로의 교체가 완료됐다.
  • 순회 보장 vs 순회 미보장
  • 적헙한 인터페이스가 없다면 당연히 클래스로 참조해야 한다.
  • 적합한 인터페이스가 없다면 클래스의 계층구조 중 필요한 기능을 만족하는 가장 덜 구체적인 클래스를 타입으로 사용하자.

item.65: 리플렉션보다는 인터페이스를 사용하라

  • 리플렉션 기능(java.lang.reflet)을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있다.
  • Constructor, Method, Field
  • 인스턴스를 이용해 각각에 연결된 실제 생성자, 메서드, 필드를 조작할 수도 있다.

리플렉션을 이용하면 컴파일 당시에 존재하지 않던 클래스도 이용할 수 있는데, 물론 단점이 있다.

  • 컴파일 타입 검사가 주는 이점을 하나도 누를 수 없다.
    • 주의해서 대비 코드를 작성해두지 않았다면, 런타임 오류가 발생한다.
  • 리플렉션을 이용하면 코드가 지저분하고 장황해진다.
    • 지루한 일이고, 읽기도 어렵다.
  • 성능이 떨어진다.
    • 리플렉션을 통한 메서드 호출은 일반 메서드 호출보다 훨씬 느리다.
    • 고려해야 하는 요소가 많아 정확한 차이는 이야기하기 어렵지만,
    • 내 컴퓨터에서 입력 매개변수가 없고 int를 반환하는 메서드로 실험해 보니 느리다.

의존관계 주입 프레임워크처럼 리플렉션을 써야 하는 복잡한 애플리케이션

  • 리플렉션은 아주 제한도니 형태로만 사용해야 그 단점을 피하고 이점만 취할 수 있다.
  • 리플렉션은 인스턴스 생성에만 쓰고 이렇게 만든 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자.
  • 리플렉션은 런타임에 존재하지 않을 수도 있는 다른 클래스, 메서드, 필드와의 의존성을 관리할 때 적합하다.
  • 여러 개 존재하는 외부 패키지를 다룰 때 유용하다.
  • 이렇게 하려면 접근하려는 새로운 클래스나 메서드가 런타임에 존재하지 않을 수 있다는 사실을 반드시 감안해야 한다.
  • 즉 같은 목적을 이룰 수 있는 대체 수단을 이용하거나 기능을 줄여 동작하는 등의 적절한 조치를 취해야 한다.

핵심 정리

  • 리플렉션은 복잡한 특수 시스템을 개발할 때 필요한 강력한 기능이지만, 단점도 많다.
  • 컴파일 타임에는 알 수 없는 클래스를 사용하는 프로그램을 작성한다면 리플랙션을 사용해야 할 것이다.
  • 단, 되도록 객체 생성에만 사용하고,
  • 생성한 객체를 이용할 때는 적절한 인터페이스나 컴파닝ㄹ타임에 알 수 있는 상위 클래스로 형변환해 사용해야 한다.

item.66: 네이티브 메서드는 신중히 사용하라

  • 자바 네이티브 인터페이스(java Native Interface, JNI)는 자바 프로그램이 네이티브 메서드를 호출하는 기술
  • 성능을 개선할 목적으로 네이티브 메서드를 사용하는 것은 거의 권장하지 않는다.
  • 네이티브 메서드에는 심각한 단점이 있다.
  • 네이티브 언어가 안전하지 않으므로 네이티브 메서드를 사용하는 애플리케이션도 메모리 훼손 오류로부터 더 이상 안전하지 않다.
  • 이식성도 낮고, 디버깅도 어렵다.
  • 주의하지 않으면 속도가 오히려 느려질 수도 있다.
  • 가비지 컬렉터가 네이티브 메모리는 자동 회수하지 못하고, 심지어 추적조차 할 수 없다.
  • 자바 코드와 네이티브 코드의 경계를 넘나들 때마다 비용도 추가된다.
  • 마지막으로 네이티브 메서드와 자바 코드 사이의 접착 코드(glue code)를 작성해야 하는데, 귀찮고 작업이기도 하거니와 가독성도 떨어진다.

핵심 정리

  • 네이티브 코드는 최소한만 사용하고 철저히 테스트하라.
  • 네이티브 코드 안에 숨은 단 하나의 버그가 여러분 애플리케이션 전체를 훼손할 수도 있다.

item.67: 최적화는 신중히 하라

  • 맹목적인 어리석음을 포함해) 그 어떤 핑계보다 효율성이라는 이름 아래 행해진 컴퓨팅 죄악이 더 많다. - 윌리엄 울프
  • (전체의 97% 정도인) 자그마한 효율성은 모두 잊자. 섣부른 최적화가 만악의 근원이다. - 도널드 크누스
  • 최적화를 할때는 다음 두 규칙을 따르라. - M.A 잭슨
    • 첫번째, 하지마라
    • 두번째, (전문가 한정) 아직 하지마라. 다시 말해, 완전히 명백하고 최적화되지 않은 해법을 찾을 때까지는 하지 마라.

빠른 프로그램보다는 좋은 프로그램을 작성하라.

  • 좋은 프로그램은 정보 은닉 원칙을 따르므로 개별 구성요소의 내부를 독립적으로 설계할 수 있다.
  • 따라서 시스템의 나머지에 영향을 주지 않고ㅗ도 각 요소를 다시 설계할 수 있다.
  • 프로그램을 와성할 때까지 성능 문제를 무시하라는 뜻은 아니다.
  • 아키텍처의 결함이 성능을 제한하는 상황이라면 시스템 전체를 다시 작성하지 않고는 해결하기 불가능할 수 있다.
  • 완성된 설계의 기본 틀을 변경하려다 보면 유지보수하거나 개선하기 어려운 꼬인 구조의 시스템이 만들어지기 쉽기 때문이다.
  • 성능을 제한하는 설계를 피하라.
    • 컴포넌트끼리, 혹은 외부 시스템과의 소통 방식이다.
    • API 네트워크 프로토콜, 영구 저장용 데이터 포맷 등이 대표적이다.
  • API를 설계할 때 성능에 주는 영향을 고려하라
  • 성능을 위해 API를 왜곡하는 건 매우 안 좋은 생각이다.
  • 신중하게 설계하여 깨끗하고 명확하고 멋진 구조를 갖춘 프로그램을 완성한 다음에야 최적화를 고려해볼 차례가 된다.
  • 물론 성능에 만족하지 못할 경우에 한정된다.

각각의 최적화 시도 전후로 성능을 측정하라.

  • 기번이 성능을 눈에 띄게 높이지 못하는 경우가 많고,
  • 심지어 더 나빠지게 할 때도 있다.
  • 주요 원인은 여러분 프로그램에서 시간을 잡아먹는 부분을 추측하기가 어렵기 때문이다.
  • 느릴 거라 고 짐작한 부분이 사실은 성능에 별다른 영향을 주지 않는 곳이라면 여러분의 시간만 허비한 꼴이 된다.
  • 일반적으로 90%의 시간으 단 10%의 코드에서 사용한다는 사실을 기억해두자.
  • 프로파일링 도구(profiling tool)는 최적화 노력을 어디에 집중해야 할지 찾는 데 더움을 준다.

구현 혹은 하드웨어 플랫폼 사이에 성능을 타협해야 하는 상황도 마주할 것이다.

핵심 정리

  • 빠른 프로그램을 작성하려 안달하지 말자.
  • 좋은 프로그램을 작성하다 보면 성능은 따라오게 마련이다.
  • 하지만 시스템을 설계할 때, 특히 API, 네트워크, 프로토콜, 영구 저장용 데이터 포맷을 설계할 때는 성능을 염두에 두어야 한다.
  • 시스템 구현을 완료했다면 이제 성능을 측정해보라.
  • 충분히 빠르면 그것으로 끝이다.
  • 그렇지 않다면 프로파일러를 사용해 문제의 원인이 되는 지점을 찾아 최적화를 수행하라.
  • 가장 먼저 어떤 알고리즘을 사용했는지를 살펴보자.
  • 알고리즘을 잘못 골랐다면 다른 저수준 최적화는 아무리 해봐야 소용이 없다.
  • 만족할 때까지 이 과정을 반복하고, 모든 변경 후에는 성능을 측정하라.

item.68: 일반적으로 통용되는 명명 규칙을 따르라.

  • 패키지, 클래스, 인터페이스, 메서드, 필드, 타입 변수의 이름을 다룬다.
  • 이 규칙들은 특별한 이유가 없는 한 반드시 따라야 한다.
  • 패키지: 인터넷 도메인 이름을 역순으로 사용함
    • 일반적으로 8자 이하의 짧은 단어

타입 매개변수

  • T: random
  • E: element
  • K: key
  • V: value
  • X: exception
  • R: return
  • T, U, V: sequence

item.69: 예외는 진짜 예외 상황에만 사용하라

  • 예외를 써서 루프를 종료한 이유는 도대체 뭘까?
  • 예외를 사용한 반복문의 해악은 코드를 헷갈리게 하고 성능을 떨어뜨리는데서 끝나지 않는다.
  • 심지어 제대로 동작하지 않을 수도 있다.
  • 예외는 (그 이름이 말해주듯) 오직 예외 상황에서만 써야 한다. 절대로 일상적인 제어 흐름용으로 쓰여선 안 된다.
  • 잘 설계된 API 라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 한다.

핵심 정리

  • 예외는 예외 상황에서 쓸 의도로 설계되었다.
  • 정상적인 제어 흐름에서 사용해서는 안 되며,
  • 이를 프로그래머에게 강요하는 API를 만들어서도 안 된다.

item.70: 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라

  • 자바는 문제 상황을 알리는 타입(throwable)으로 검사 예외, 런타임 예외, 에러, 이렇게 세 가지를 제공하는데
  • 호출하는 쪽에서 복구하리라 여겨지는 사오항이라면 검사 예외를 사용하라.
  • 검사 예외를 던져주어 그 상황에서 회복해내라고 요구한 것이다.
  • 물론 사용자는 예외를 잡기만 하고 별다른 조치를 취하지 않을 수도 있지만, 이는 보통 좋지 않은 생각이다.
  • 프로그램에서 비검사 예외나 에러를 던졌다는 것은 복구가 불가능하거나 더 실행해봐야 득보다는 실이 많다는 뜻이다.
  • 프로그래밍 오류를 나타낼 때는 런타임 예외를 사용하자.
  • 여러분이 구현하는 비검사 throwable은 모두 RuntimeException의 하위 클래스여야 한다.

핵심 정리

  • 복구할 수 있는 상황이면 검사 예외를, 프로그래밍 오류라면 비검사 예외를 던지자.
  • 확실하지 않다면 비검사 예외를 던지자.
  • 검사 예외도 아니고 런타임 예외도 아닌 throwable 은 정의하지도 말자.
  • 검사 예외라면 복구에 필요한 정보를 알려주는 메서드도 제공하자.

item.71: 필요 없는 검사 예외 사용은 피하라

  • 검사 에외는 프로그램의 안전성을 높여주지만,
  • 남용하면 쓰기 고통스러운 API를 낳는다.
  • API 호출자가 예외 상황에서 복구할 방법이 없다면 비검사 예외를 던지자.
  • 복구가 가능하고 호출자가 그 처리를 해주길 바란다면,
  • 우선 옵셔널을 반환해도 될지 고민하자.
  • 옵셔널만으로는 상황을 처리하기에 충분한 정보를 제공할 수 없을 대만 검사 예외를 던지자.

item.72: 표준 예외를 사용하라

  • 숙련된 프로그래머는 그렇지 못한 프로그래머보다 더 많은 코드를 재사용한다.
  • 예외도 마찬가지로 재상요하는 것이 좋으며,

표준 예외를 재사용하면 얻는게 많다.

  • 그중 최고는 여러분의 API가 다른 사람이 익히고 사용하기 쉬워진다는 것이다.
  • 많은 프로그래머에게 이미 익숙해진 규약을 그대로 따르기 때문이다.
  • IllegalArgumentException
  • IllegalStateException
  • NullPointerException
  • IndexOutOfBoundsException
  • ConcurrentModificationException
  • UnsupportedOperationException
  • 동작을 대상 객체가 원하지 않을 때 던진다.

직접 재사용하지 말 것

  • Exception
  • RuntimeException
  • Throwable
  • Error
  • 직접 재사용하지 말자.
  • 클래스들은 추상 클래스라고 생각하길 바란다.
  • 예외들을 포괄하는 클래스이므로 안정적으로 테스트할 수 없다.

표준 예외를 확장해도 좋다.

  • 단, 예외는 직렬화할 수 있다는 사실을 기억하자
  • 인수의 값이 너무 크다고 본다면 IllegalArgumentException
  • 덱에 남은 카드 수가 너무 적다고 본다면 IllegalStateException
  • 인수 값이 무었이든 어차피 실패했을거라면 IllegalStateException
  • 그렇지 않으면 IllegalArgumentException

item.73: 추상화 수준에 맞는 예외를 던지라

  • 상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 한다.
    try {
      ...
    } catach (LowerLevelException e) {
      throw new HigherLevelException(...);
    }
  • 무턱대고 예외를 전파하는 것보다야 예외 번역이 우수한 방법이지만, 그렇다고 남용해서는 곤란하다.
  • java.util.logging 같은 적절한 로깅 기능을 활용하여 기록해두면 좋다.

핵심 정리

  • 예외 번역을 사용하라.
  • 이때 예외 연쇄 이용하면 상위 계층에는 맥락에 어울리는 고수준 에외를 던지면서 근본 원인도 함께 알려주어 오류를 분석하기에 좋다.

item.74: 메서드가 던지는 모든 예외를 문서화하라

  • 검사 예외는 항상 따로따로 선언하고, 각 예외가 발생하는 상황을 자바독의 @throws 태그를 사용하여 정확히 문서화하자.
  • 메서드가 던질 수 있는 예외를 각각 @throws 태그로 문서화하되, 비검사 예외는 메서드 선언의 throws 목록에 넣지 말자.
  • 한 클래스에 정의된 많은 메서드가 같은 이유로 같은 예외를 던진다면 그 예외를 (각각의 메서드가 아닌) 클래스 설명에 추가하는 방법도 잇다.
  • NullPointerException

핵심 정리

  • 모든 예외를 문서화하라.
  • 발생 가능한 예외를 문서로 남기지 않으면 다른 사람이 그 클래스나 인터페이스를 효과적으로 사용하기 어렵거나 심지어 불가능할 수도 있다.

item.75: 예외의 상세 메시지에 실패 관련 정보를 담으라

  • 예외를 잡지 못해 프로그래밍 실패하면 자바 시스템은 그 예외의 스택 추적(stack trace) 정보를 자동으로 출력한다.
  • 예외 객체의 toString 메서드를 호출해 얻는 문자열로,
  • 신뢰성 엔지니어(site reliability engineer, SRE)
  • 실패 순간의 상황을 정확히 포착해 예외의 상세 메시지에 담아야 한다.
  • 실패 순간을 포착하려면 발생한 예외에 관여한 모든 매개변수와 필드의 값을 실패 메시지에 담아야 한다.
  • IndexOutOfBoundsException
  • 최솟값과 최댓값, 그리고 그 범위를 벗어났다는 인덱스의 값
  • 현상을 보면 무엇을 고쳐야 할지를 분석하는 데 큰 도움이 된다.
  • 추적 정보를 많은 사람이 볼 수 있으므로 상세 메시지에 비밀번호나 암호 키 같은 정보까지 담아서는 안 된다.
  • 비검사 예외라도 상세 정보를 알려주는 접근자 메서드를 제공하라고 권하고 싶다.

item.76: 가능한 한 실패 원자적으로 만들라

  • 일반화해 이야기하면, 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야 한다.
  • 실패 원자적(failure-atomic)
  • 가장 간단한 방법은 불변 객체로 설계하는 것이다.
  • 내구성(durability)을 보장해야 하는 자료구조에 쓰이는데
  • 실패 원자성은 일반적으로 권장되는 덕목이지만 항상 달성할 수 있는 것은 아니다.
  • 실패 원자적으로 만들 수 있더라도 항상 그리 해야 하는 것도 아니다.
  • 실패 원자성을 달성하기 위한 비용이나 복잡도가 아주 큰 연산도 있기 때문이다.
  • 호출 전과 똑같이 유지돼야 한다는 것이 기본 규칙이다.
  • 지키지 못한 다면 실패 시의 객체 상태를 API 설명에 명시해야 한다.
  • 아쉽게도 지금의 API 문서 상당 부분이 잘 지키지 않고 있다.

item.77: 예외를 무시하지 말라

  • catch 블록을 비워두면 예외가 존재할 이유가 없어진다.
  • 비유하자면 화재경보를 무시하는 수준을 넘어 아예 꺼버려, 다른 누구도 화재가 발생했음을 알지 못하게 하는 것과 같다.
  • 머릿속에 사이렌을 울려야 한다.
  • 예외를 무시하기로 했다면 catch 블록 안에 그렇게 결정한 이유를 주석으로 남기고 예외 변수의 이름도 igrnoed로 바꿔놓도록 하자.
    int numColors = 4;
    try {
      numColors = f.get(1L, TimeUnit.SECONDS);
    } catch (TimeoutException | ExecutionException ignored) {
    }
  • 예외를 적절히 처리하면 오류를 완전히 피할 수도 있다.
  • 무시하지 않고 바깥으로 전파되게만 놔둬도 최소한 디버깅 정보를 남긴 채 프로그램이 신속히 중단되게는 할 수 있다.

item.78: 공유 중인 가변 데이터는 동기화해 사용하라

  • synchronized 키워드는 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장한다.
  • 동기화 없이는 한 스레드가 만든 변화를 다른 스레드에서 확인하지 못할 수 있다.
  • 원자적(atomic)
  • 동기화는 배타적 실행뿐 아니라, 스레드 사이의 안정적인 통신에 꼭 필요하다.
  • Thread.stop은 사용하지 말자.
  • boolean 필드를 폴링하면서 그 값이 true가 되면 멈춘다.
  • 이 필드를 false로 초기화해놓고, 다른 스레드에서 이 스레드를 멈추고자 할 때 true로 변경하는 식이다.

동기화가 빠지면 가상 머신이 다음과 같은 최적화를 수행할 수도 있다.

// as-is
while (!stopRequested) i++;

// to-be
if (!stopRequested)
while (true) i++;
  • 쓰기와 읽기가 모두 동기화되지 않으면 동작을 보장하지 않는다.
  • volatile 한정자는 배타적 수행과는 상관없지만 항상 가장 최근에 기록된 값을 읽게 됨을 보장한다.
  • 애초에 가변 데이터를 공유하지 않는 것이 좋다.
  • 가변 데이터는 단일 스레드에서만 쓰도록 하자.

핵심 정리

  • 여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓴느 동작은 반드시 동기화 해야 한다.
  • 필요 없고 스레드끼리의 통신만 필요하다면 volatile 한정자만으로 동기화할 수 있다.
  • 다만 올바로 사용하기가 까다롭다.

item.79: 과도한 동기화는 피하라

  • 응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안된다.
  • 교착상태에 빠지거나, 데이터를 훼손할 수도 있다.
  • 재진입(reentrant)
  • 기본 규칙은 동기화 영역에서는 가능한 한 일을 적게 하는 것이다.
  • 지연 시간이 진짜 비용이다.

핵심 정리

  • 동기화 영역 안에서 외계인 메서드를 절대 호출하지 말자
  • 동기화 영역 안에서의 작업은 최소한으로 줄이자.
  • 가변 클래스를 설계할 때는 스스로 동기화해야 할지 고민하자.
  • 과도한 동기화를 피하는 게 과거 어느 때 보다 중요하다.
  • 합당한 이유가 있을 때만 내부에서 동기화하고, 동기화했는지 여부를 문서에 명확히 밝히자.

item.80: 스레드보다는 실행자, 태스크, 스트림을 애용하라

  • 큐를 둘 이상의 스레드가 처리하게 하고 싶다면 간단히
  • 다른 정적 팩터리를 이용하여, 다른 종류의 실행자 서비스(스레드 풀)를 생성하면 된다.
  • 모든 스레드가 바쁘게 움직여 CPU를 최대한 활용하면서 높은 처리량과 낮은 지연시간을 달성한다.
  • 이러한 포크-조인 태스크를 직접 작성하고 튜닝하기란 어려운 일이지만,
  • 포크-조인 풀을 이용해 만든 병렬 스트림을 이용하면 적은 노력으로 그 이점을 얻을 수 있다.

item.81: wait와 notify보다는 동시성 유틸리티를 애용하라

  • wait와 notify는 올바르게 사용하기가 아주 까다로우니 고수준 동시성 유틸리티를 사용하자.
  • java.util.concurrent
  • 동시성 컬렉션(concurrent collection)
  • 동기화 장치(synchronizer)
  • Collections.synchronizedMap 보다는 ConcurrentHashMap이 좋다.
  • 카운트다운 래치(latch; 걸쇠)
  • System.currentTimeMillis 보다는 System.nanoTime이 좋다.

wait 메서드를 사용할 때는 반드시 대기 반복문 (wait loop) 관용구를 사용하라.

  • 반복문 밖에서는 절대 호출하지 말자.

notify vs notifyAll

  • 스레드를 깨우는 메소드
  • notifyAll 합리적이다.
  • 악의적인 wait 호출을 예방할 수 있다.

핵심 정리

  • waitnotify를 직접 사용하는 것을 동시성 어셈블리 언어로 프로그래밍 하는 것에 비유할 수 있다.
  • java.util.concurrent는 고수준 언어에 비유할 수 있다.
  • 코드를 새로 작성한다면 waitnotify를 쓸이유가 거의 없다.
  • 레거시 코드를 유지보수해야 한다면 wait는 항상 표준 관용구에 따라 while문 안에서 호출하도록 하자.
  • 일반적으로 notify 보다는 notifyAll을 사용해야 한다.
  • 혹시라도 notify를 사용한다면 응답 불가 상태에 빠지지 않도록 각별히 주의하자.

item.82: 스레드 안전성 수준을 문서화하라

  • 멀티스레드 환경에서도 API를 안전하게 사용하게 하려면 클래스가 지우너하는 스레드 안전성 수준을 정확히 명시해야 한다.
  • 불변(immutable)
  • 무조건적 스레드 안전(unconditionally thread-safe)
  • 조건부 스레드 안전(conditionally thread-safe)
  • 스레드 안전하지 않음(not thread-safe)
  • 스레드 적대적(thread-hostile)

item.83: 지연 초기화는 신중히 사용하라

  • 지연초기화(lazy initialization)
  • 값이 전혀 쓰이지 않으면 초기화도 결코 일어나지 않는다.
  • 필요할 때까지는 하지 마라.
  • 초기화 비용은 줄지만 그 대신 지연 초기화 하는 필드에 접근하는 비용은 커진다.
  • 대부분의 상황에서 일반적인 초기화가 지연 초기화보다 낫다.

지연 초기화가 초기화 순환성(initialization circularity)을 깨뜨릴 것 같으면 synchronized를 단 접근자를 사용하자.

private FieldType field;

private synchronized FieldType getField() {
    if (field == null)
        field = computeFieldValue();
    return field;
}
  • 정적 필드에도 똑같이 적용된다.
  • 물론 필드와 접근자 메서드 선언에 static 한정자를 추가해야 한다.
  • 성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스 (lazy initialization holder class) 관용구를 사용하자.
    private static class FieldHolder {
      static final FieldType field = computeFieldValue();
    }
    

private static FieldType getField() {
return FieldHolder.field;
}


### 성능 때문에 이스턴스 필드를 초기화해야 한다면 이중검사(double-check) 관용구를 사용하라.
```java
private volatile FieldType field;

private FieldType getField() {
    FieldType result = field;
    if (result != null) return resultl;

    synchronized(this) {
        if (field == null) field = computeFieldValue();
        return field;
    }
}
  • 지역변수를 사용하지 않을 때보다 1.4배 빠르게 동작한다.
  • 지연 초기화 홀더 클래스 방식이 더 낫다.

단일 검사 관용구

private volatile FieldType field;

private FieldType getField() {
    FieldType result = field;
    if (result == null)
        field = result = computeFieldValue();
    return result;
}
  • 스레드당 최대 한 번 더 이뤄질 수 있다.
  • 아주 이례적인 기법으로, 보통은 거의 쓰지 않는다.

핵심 정리

  • 대부분의 필드는 지연시키지 말고 곧바로 초기화해야 한다.
  • 인스턴스 필드에는 이중검사 고나용구를,
  • 정적 필드에는 지연 초기화 홀더 클래스 관용구를 사용하자.
  • 반복해 초기화해도 괜찮은 인스턴스 필드에는 단일 검사 관용구도 고려 대상이다.

item.84: 프로그램의 동작을 스레드 스케줄러에 기대지 말라

  • 정확성이나 성능이 스레드 스케줄러에 따라 달라지는 프로그램이라면 다른 플랫폼에 이식하기 어렵다.
  • 스레드는 당장 처리해야 할 작업이 업삳면 실행돼서는 안 된다.
  • 절때 바쁜 대기(busy waiting) 상태가 되면 안 된다.

핵심 정리

  • 프로그램의 동작을 스레드 스케줄러에 기대지 말자.
  • Thread.yield와 스레드 우선순위에 의존해서도 안된다.

item.85: 자바 직렬화의 대안을 찾으라

로버트 시커드(Robert Seacord)

자바의 역직렬화는 명백하고 현존하는 위험이다.
이 기술은 지금도 애플리케이션에서 직접 혹은, 자바 하부 시스템 RMI(Remote Mthod Invocation), JMX(Java Management Extension), JMS(Java Messaging System)을 통해 간접적으로 쓰이고 있기 때문이다.
신뢰할 수 없는 스트림을 역직렬화하면 원격 코드 실행(remote code execution, RCE), 서비스 거부(denial-of-service, DoS) 공격으로 이어질 수 있다.
잘못한 게 아무것도 없는 애플리케이션이라도 이런 공격에 취약해질 수 있다.
  • 역직렬화 폭탄(deserialization bomb)
  • 직렬화 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는 것이다.
  • 승리하는 유일한 길은 전쟁하지 않는 것이다.
  • 여러분이 작성하는 새로운 시스템에서 자바 직렬화를 써야 할 이유는 전혀 없다.
  • 신뢰할 수 없는 데이터는 절대 역직렬화하지 않는 것이다.
  • 블랙리스트 방식보다는 화이트리스트 방식을 추천한다.

핵심 정리

  • 직렬화는 위험하니 피해야 한다.
  • 시스템을 밑바닥부터 설계한다면 JSON이나 프로토콜 버퍼 같은 대안을 사용하자.
  • 신뢰할 수 없는 데이터는 역직렬화하지 말자.
  • 꼭 해야한다면 객체 역직렬화 필터링을 사용하되,
  • 이마저도 모든 공격을 막아줄 수는 없음을 기억하자.
  • 클래스가 직렬화를 지원하도록 만들지 말고,
  • 꼭 그렇게 만들어야 한다면 정말 신경써서 작성해야 한다.

item.86: Serializable을 구현할지는 신중히 결정하라

  • implements Serializable만 덧붙이면 된다.

Serializable을 구현하면 릴리스한 뒤에는 수정하기 어렵다.

  • 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API
  • 영원히 지원해야 하는 것이다.
  • serialVersionUID UID(serial version UID)
  • static final long
  • 명시하지 않으면 시스템이 런타임에 암호 해시 함수(SHA-1)를 적용해 자동으로 클래스 안에 생성해 넣는다.

버그와 보안 구멍이 생길 위험이 높아진다는 점이다.

신버전을 릴리스할 때 테스트할 것이 늘어난 다는 점이다.

가볍게 결정할 사안이 아니다.

상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안 되며, 인터페이스도 대부분 Serializable을 확장해서는 안된다.

내부 클래스는 직렬화를 구현하지 말아야 한다.

핵심 정리

  • Serializable은 구현한다고 선언하기는 아주 쉽지만,
  • 그것은 눈속임일 뿐이다.
  • 한 클래스의 여러 버전이 상호작용할 일이 없고 서버가 신뢰할 수 없는 데이터에 노출될 가능성이 없는 등,
  • 보호된 환경에서만 쓰일 클래스가 아니라면 구현은 신중하게 이뤄져야 한다.
  • 상속할 수 있는 클래스라면 주의사하이 더욱 많아진다.

item.87: 커스텀 직렬화 형태를 고려해보라

  • 먼저 고민해보고 괜찮다고 판단될 때만 기본 직렬화 형태를 사용하라.
  • 객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 무방하다.
  • 기본 직렬화 형태가 적합하다고 결정했더라도 불변식 보장과 보안을 위해 readObject 메서드를 제공해야 할 때가 많다.
  • 해당 객체의 논리적 상태와 무관한 필드라고 확신할 때만 transient 한정자를 생략해야 한다.
  • 객체의 전체 상태를 읽는 메서드에 적용해야 하는 동기화 매커니즘을 직렬화에도 적용해야 한다.
  • 어떤 직렬화 형태를 택하든 직렬화 가능 클래스 모두에 직렬 버전 UID를 명시적으로 부여하자.
  • 구버전으로 직렬화된 인스턴스들과의 호환성을 끊으려는 경우를 제외하고는 직렬 버전 UID를 절대 수정하지 말자.

기본 직렬화 형태를 사용하면 문제점 (물리적 표현과 논리적 표현의 차이가 클때)

  • 공개 API가 현재의 내부 표현 방식에 영구히 묶인다.
  • 너무 많은 공간을 차지할 수 있다.
  • 시간이 너무 많이 걸릴 수 있다.
  • 스택 오버플로를 일으킬 수 있다.

핵심 정리

  • 한번 공개된 메서드는 향후 릴리스에서 제거할 수 없듯이, 직렬화 형태에 포함된 필드도 마음대로 제거할 수 없다.
  • 직렬화 호환성을 유지하기 위해 영원히 지원해야 하는 것이다.
  • 잘못된 직렬화 형태를 선택하면 해당 클래스의 복잡성과 성능에 영구히 부정적인 영향을 남긴다.

item.88: readObject 메서드는 방어적으로 작성하라

  • 객체를 역직렬화할 때는 클라이언트가 소유해서는 안 되는 객체 참조를 갖는 필드를 모두 반드시 방어적으로 복사해야 한다.

item.89: 인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라.

  • 사실, readResolve를 인스턴스 통제 목적으로 사용한다면 객체 참조 타입 인스턴스 필드는 모두 transient로 선언해야 한다.
  • readResolve 메서드의 접근성은 매우 중요하다.

핵심 정리

  • 불변식을 지키기 위해 인스턴스를 통제해야 한다면 가능한 한 열거 타입을 사용하자.
  • 여의치 않은 상황에서 직렬화와 인스턴스 통제가 모두 필요하다면 readResolve 메서드를 작성해 넣어야 하고,
  • 그 클래스에서 모든 참조 타입 인스턴스 필드를 transient로 선언해야 한다.

item.90: 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라

728x90
댓글
댓글쓰기 폼