본문 바로가기

나(다)/책

이펙티브 자바 요약(아이템1~90)

반응형

2장 객체 생성과 파괴

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

전통적으로 public 생성자를 사용하고 더 나아가 정적 팩터리 메서드를 제공할 수 있다.

무조건 public 생성자를 만들기보다 정적 팩터리를 사용하는게 유리한 경우가 많으니 잘 고려해보자.

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

API는 시간이 지날수록 매개변수가 많아지는 경향이 있으니 애초에 빌더로 시작하는게 나을때가 많다.

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

싱글턴은 함수와 같은 무상태 객체나 설계상 유일해야 하는 시스템 컴포넌트를 들 수 있다. 하지만 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.

  • 타입을 인터페이스로 정의하고 인터페이스를 구현 한 싱글턴이 아니면 mock으로 대체 할 수 없다.

    대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.

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

객체 지향적 개발 방식과 다르게 정적 메서드와 정적 필드를 담은 클래스를 만드는 경우가 있는데 예외적으로 쓰임새가 있는 경우가 있다.

인스턴스를 막으려면 생성자에 private을 하자

아이템 5 : 자원을 직접 명시하지 말고 의존 객체 주임을 사용하라

하나 이상의 자원에 의존하는 클래스의 경우 의존 객체 주입을 사용해야 한다.
맞춤법 검사기는 여러 언어별, 특수 어휘용 등의 사전에 의존하는 경우를 예를 들 수 있다. 사전 하나로 모든 상황에 대응할 수 없다.

  • 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.

  • 의존 객체 주입은 딱 하나의 자원만 사용하지만, 자원이 몇 개든 의존 관계가 어떻든 관계 없이 잘 작동하고 불변을 보장한다.

  • 생성자, 정적 팩터리, 빌더에 똑같이 응용할 수 있다.
    ⇒ 스프링에서는 생성자 주입을 사용하면 junit에 용이하다.

    하나 이상에 의존하고 자원에 영향을 받으면 의존 객체 주입 패턴으로 구현하는 방법으로 구현하면 유연성, 재사용성, 테스트 용이성을 기막히게 개선해준다.

아이템 6 : 불필요한 객체 생성을 피하라

똑같은 기능의 객체는 하나를 재사용하는 편이 나을 때가 많고 불변 객체는 언제든 재사용 할 수 있다.

String s = new String("bikini"); // 사용 하지 말자
String ss = "bikini"; // 하나의 인스턴스를 사용하는 방법

Boolean b = new Boolean("true"); // 비권장 API
Boolean bb = Boolean.valueOf("true"); // 권장 API

방어적 복사(아이템50)와 대조적인 아이템6이다. 기존 객체를 재사용 하되 객체를 만들어야 할 때는 만들어라이다. 방어적 복사에 실패하면 언제 터져 나올지 모르는 버그와 보안 구멍으로 이어지지만, 불필요한 객체 생성은 그저 코드 형태와 성능에만 영향을 준다.

아이템 7 : 다 쓴 객체 참조를 해제하라

c와 c++처럼 메모리를 직접 관리 하지 않아도 되는 gc가 좋지만 그래도 해줘야 할 경우가 있다.

메모리 누수는 수년간 잠복할 수도 있어서 발견을 하려면 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 동원하면 발견되기도 하지만 예방법이 가장 중요하다.

아이템 8 : finalizer와 cleaner 사용을 피하라

finalizer는 가급적 지양하고 cleaner는 안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자

아이템 9 : try-finally보다는 try-with-resources를 사용하라

InputStream, OutputStream, java.sql.Connection 등 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많고 닫지 못하거나 않으면 성능 이슈로 미칠 수 있다.

꼭 회수 할 경우에는 예외 없이 try-with-resources를 사용하자.

3장 모든 객체의 공통 메서드

아이템 10 : equals는 일반 규약을 지켜 재정의하라

eqals 메서드 재정의하기 쉬워 보이지만 자칫하면 함정에 빠져 문제를 일으킬 수 있고 문제를 회피하는 가장 쉬운 길은 재정의를 하지 않는 방법이다.

꼭 필요한 경우가 아니면 equals를 재정의하지 말자. 할꺼면 규약을 준수하고 핵심 필드를 빠짐없이 비교하자.

아이템 11 : equals를 재정의하려거든 hashCode도 재정의하라

equals를 재정의할 때는 hashCode도 반드시 재정의해야 한다.

아이템 12 : toString을 항상 재정의하라

규약에는 '간결하면서 사람이 읽기 쉬운 형태의 유익한 정보'를 반환해야 한다고 하고 '모든 하위 클래스에서 이 메서드를 재정의 하라'라고 한다.

모든 구체 클래스에서 Object의 toString을 재정의하자.

아이템 13 : clone 재정의는 주의해서 진행하라

clone은 복제해도 되는 클래스인 믹스인 인터페이스이지만 의도한 목적을 제대로 이루지 못했다.

  • 가장 큰 문제는 clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이면서 protected이기에 외부 객체에서 호출할 수 없다.

  • Cloneable에 메서드가 하나도 없지만 Object의 clone의 동작 방식을 결정한다.

    • Cleanable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다.

    • 이는 인터페이스에서 상당히 이례적으로 사용한 예이니 따라하지 말자.

      새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 구현해서는 안된다. 기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는게 최고'라는 것이며 배열만은 clone 메서드 방식이 가장 깔끔한, 합당한 예외라 할 수 있다.

아이템 14 : Comparable을 구현할지 고려하라

compareTo는 Object 메서드가 아니지만 단순 동치성 비교에 순서까지 비교할 수 있고 제네릭한 성격만 빼면 Object의 equals와 같다.

순서를 고려해야 하는 상황에는 꼭 Comparable을 구현하고 비교 할 경우 '<' 와 '>'를 사용하지 말고 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.

4장 클래스와 인터페이스

아이템 15 : 클래스와 멤버의 접근 권한을 최소화하라

내부 구현을 잘 숨기고 구현과 API를 깔끔히 분리하여 정보 은닉, 혹은 캡슐화가 얼마나 잘 되어 있느냐에 따라 잘 설계된 컴포넌트라고할 수 있다.

프로그램 요소의 접근성은 가능한 한 최소한으로 하라. 꼭 필요한 것만 골라 최소한의 public API를 설계하고 그 외에는 클래스, 인터페이스, 멤버가 의도치 않게 API를 공개되는 일이 없도록 해야 한다. public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안 된다. public static final 필드가 참조하는 객체가 불변인지 확인하라.

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

public 클래스는 절대 가변 필드를 직접 노출해서는 안 된다.

아이템 17 : 변경 가능성을 최소화하라

불변 클래스란 인스턴스의 내부 값을 수정할 수 없는 클래스로 간직된 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다.
예로는 String, 기본 타입의 박싱된 클래스들, biginteger, BigDecimal이 있다.
불변 클래스의 이점으로는 가변 클래스보다 설계와 구현하기 쉬우며 오류가 생길 여지도 줄어들어 훨씬 안전하다.

정리

  1. 클래스는 꼭 필요한 경우가 아니라면 불변이어야 하므로 무조건 setter를 만들지 말자
  2. 단점은 특정 상황에서의 잠재적 성능 저하가 있다.
  3. String과 BigInteger처럼 무거운 값 객체도 불변으로 만들 수 있는지 고심하고 성능으로 어쩔 수 없다면 불변 클래스와 쌍을 이루는 가변 동반 클래스를 public 클래스로 제공한다.
  4. 불변으로 만들수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이면 객체가 가질 수 있는 상태가 줄어들어 예측이 쉽고 오류가 생길 가능성이 줄어든다.
  5. 다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
  6. 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
  7. 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도 public으로 제공해서는 안된다.
  8. 객체를 재활용 할 목적으로 상태를 다시 초기화하는 메서드도 복잡성만 커지고 성능 이정이 거의 없다.
  • java.util.concurrent 패키지의 CountDownLatch 클래스가 이상의 원칙을 잘 방증하는데 비록 가변 클래스이지만 가질 수 있는 상태의 수가 많지 않고 인스턴스를 생성해 한번 사용하면 파괴된다.

아이템 18 : 상속보다는 컴포지션을 사용하라

이 책에서는 '상속'을 구현 상속을 말하며 인터페이스 상속과는 무관하다.

상속은 강력하지만 캡슐화를 해치므로 순수한 is-a 관계일경우에만 사용하고 그 외에는 컴포지션과 전달을 사용하자.

아이템 19 : 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라.

클래스 내부에서 스스로 어떻게 사용하는지 모두 문서로 남겨야 하고 문서를 반드시 지켜야 한다. 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 낫다.

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

추상 클래스를 구현한 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 되기에 확장에 어려움이 있으니 인터페이스를 규약에 지켜 사용하면 어떤 클래스를 상속하든 같은 타입으로 취급한다.

일반적으로는 다중 구현용 타입으로는 인터페이스가 가장 적합하고 가능 한 디폴트 메서드도 제공하여 구현한 모든 곳에서 활용 되도록하는게 좋지만 인터페이스에 걸려 있는 구현상의 제약 때문에 골격 구현을 추상 클래스로 제공하는 경우가 더 흔하기 때문이다.

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

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

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

인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다.
클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에 이야기 해주는것이다.

인터페이스는 타입을 정의하는 용도로만 사용하고 상수 공개용 수단으로 사용하지 말자.

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

태그 달린 클래스를 써야 하는 상황은 거의 없다. 태그를 없애고 계층구조로 대체하는 방볍을 생각해보자. 기존 클래스가 태그 필드를 사용하고 있다면 계층구조로 리팩터링을 고민하자.

아이템 24 : 멤버 클래스는 되도록 static으로 만들라

중첩 클래스는 클래스 안에 클래스가 정의 된 클래스로 바깥 클래스에서만 쓰여야 하며, 그 외에 쓰인다면 톱레벨 클래스로 만들어야 한다.
중첩 클래스의 정류는 정적 멤버 클래스, (비정적) 멤버 클래스, 익명 클래스, 지역 클래스가 있으며 정적 멤버 클래스를 제외 한 클래스는 내부 클래스에 해당한다.
중첩 클래스를 언제, 왜 사용해야하는지 이야기한다.

메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 긷라면 멤버 클래스로 만들고 멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적으로, 그렇지 않으면 정적으로 만든다.
중첩 클래스가 한 메서드 안에서만 쓰이면서 인스턴스를 생성하는 지점이 단 한곳이고 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다면 익명 클래스로 만들고 그렇지 않으면 지역 클래스로 만든다.

아이템 25 : 톱레벨 클래스는 한 파일에 하나만 담으라

소스 파일 하나에 톱레벨 클래스를 여러 개 선언하더라도 문제는 없지만 심각한 위험을 감수해야 한다.

소스 파일 하나에는 반드시 톱레벨(혹은 톱레벨 인터페이스)를 하나만 담자.

5장 제네릭

제네릭은 자바 5부터 사용 가능하고 컬렉션이 담을 수 있는 타입을 컴파일러에게 알려주어 더 안전하고 명확한 프로그래밍을 할 수 있지만 코드가 복잡해진다는 단점이 있다.

이번 장에서는 제네릭의 이점을 최대로 살리고 단점을 최소화하는 방법을 이야기한다.

아이템 26 : 로 타입은 사용하지 말라

클래스와 인터페이스 선언에 타입 매게변수가 쓰이면 이를 제네릭 클래스 혹은 제네릭 인터페이스라고 이를 총칭해 제네릭 타입이라고 한다.
제네릭 타입을 정의하면 로타입(raw type-타입 매개변수가 없는 제네릭 타입)도 함께 정의하며 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다.
<> 정의하여 타입이 무엇인지 필히 적시하자.

// static int numElementsInCommon(Set s1, Set s2) { // 잘못 된 예 Set이 무엇인지 모른다.
static int numElementsInCommon(Set<?> s1, Set<?> s2) { // 와일드 카드 타입을 사용하자.
        int result = 0;
        for (Object o1 : s1)
            if(s2.contains(o1))
                result++;
        return result;
    }

로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안 된다. 로 타입은 제네릭이 도입 도기 이전 코드와의 호환성을 위해 제공될 뿐이며 Set

와 Set<?>는 안전하지만, 로 타입인 Set은 안전하지 않다.

참고 용어

한글 용어 : 영문 용어 : 예시 : 아이템
매개변수화 타입 : parameterized type : List : 아이템26
실제 타입 매개변수 : actual type parameter : String : 아이템26
제네릭 타입 : generic type : List : 아이템26,29
정규 타입 매개변수 : formal type parameter : E : 아이템26
비한정적 와일드카드 타입 : unbounded wildcard type : List<?> : 아이템26
로 타입 : raw type : : 아이템29
재귀적 타입 한정 : recursive type bound : : 아이템30
한정적 와일드카드 타입 : bounded wildcard type : List<? extends Number> : 아이템31
제네릭 메서드 : generic method : static List asList(E[] a) : 아이템30
타입 토큰 : type token : String.class : 아이템 33
<> : 다이아몬드 연산자

아이템 27 : 비검사 경고를 제거하라

  • 제네릭을 사용하다 보면 많은 컴파일러 경고들이 있다. 쉽게 제거할 수 있는 경고들은 할 수 있는 한 모든 비검사 경고를 제거하라.
  • 경고를 제거할 수는 없지만 타입 안전하다고 확신할 수 있다면 @SuppressWarings("unchecked") 애너테이션을 달아 경고를 숨기자.
    • @suppressWarings 애너테이션은 항상 가능한 한 좁은 범위에 적용하자.
    • 절대로 클래스에는 하면 안되며 한 줄이 넘는 메서드나 생성자에 달려있으면 지역변수 선언쪽으로 옮기자.
public class Item27 {
    private int size;
    private Object[] elements;
    public <T> T[] toArray(T[] a ){
        if (a.length < size) {
            // 생성한 배열과 매개변수로 받은 배열의 타입이 모두 T[]로 같으므로 올바른 형변환이다.
            @SuppressWarnings("unchecked")
            T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
            return result;
        }
        System.arraycopy(elements, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }
}
  • @SuppressWarings("uinchecked") 애너테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 한다.

비검사 경고는 중요하니 무시하지 말자. 안전성이 보장 되면 @SuppressWarings("unchecked") 애너테이션으로 경고를 숨기고 근거를 주석으로 남겨라

아이템 28 : 배열보다는 리스트를 사용하라

배열과 제네릭 타입의 차이점

  • 배열은 공변(같이 변한다)이다. Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다.

  • 제네릭은 불공변(같이 변하지 않는다)이다. 서로 다른 타입 Type1과 Type2가 있을 때, List은 List의 하위 타입도 아니고 상위 타입도 아니다.

    배열은 공변, 제네릭은 불공변이어서 같이 쓰기 쉽지 않다. 배열은 런타임에는 타입이 안전하지만 컴파일런타임에는 그렇지 않고 제네릭은 반대다. 배열을 리스트로 바꾸는 방법을 적용해보자.

아이템 29 : 이왕이면 제네릭 타입으로 만들어라

제공되는 제네릭은 사용하기 쉽지만 만드는건 조금 더 어려우니 연습해보자.

보통 를 많이 사용한다.

  • 1번 방법은 가독성이 좋고 확실하게 E타입의 배열을 선언하여 한번만 형변환을 해주면 되고 코드가 짧다.
  • 2번 방법은 데이터를 꺼낼 때마나 형변환을 해주어야 한다.
  • 1번과 2번 방법 중 1번 방법을 선호하지만 배열 런타임 타입이 컴파일 타임 타입과 달리 힙 오염을 일으켜서 힙 오염이 걸리는 프로그래머는 2번 방법을 고수한다.
  • 배열보다는 리스트를 우선하라는 아이템 28과는 모순돼 보이지만 제네릭 타입 안에서 리스트를 사용하는게 항상 가능하지도, 꼭 더 좋은것도 아니다.
    • 자바가 리스트를 기본 타입으로 제공하지 않으므로 ArrayList 같은 제네릭 타입도 결국은 기본 타입인 배열을 사용해 구현해야 하고 HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 한다.

클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하므로 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 제네릭 타입으로 만들자. 그러면 기존 클라이언트에는 아무 영향을 주지 않으면서, 새로운 사용자를 휠씬 편하게 해주는 길이다.


아이템 30 : 이왕이면 제네릭 메서드로 만들어라

클래스와 마찬가지로 메서드도 제네릭으로 만들 수 있으며 매개변수화 타입을 받는 정적 유틸리티 메서드는 보통 제네릭이다.

  • 제네릭은 런타임에 타입 정보가 소거되므로 하나의 객체를 어떤 타입으로든 매개변수화할 수 있도록 정적 팩터리를 만들어야 하며 이를 제네릭 싱글턴 팩터리라고 한다.
  • 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있는데 재귀적 타입 한정이며 이는 주로 타입의 자연적 순서를 정하는 Comparable 인터페이스와 함께 쓰인다.

메서드보다는 제네릭 메서드가 더 안전하며 사용하기도 쉽다. 타입과 마찬가지로 메서드도 형변환 없이 사용할 수 있는 편이 좋으며, 많은 경우 그렇게 하라면 제네릭 메서드가 되어야 한다.

아이템 31 : 한정적 와일드카드를 사용해 API 유연성을 높이라

매개변수화 타입은 불공변이다. 예를 들어 List은 List

의 하위 타입이 아니라는 의미로 List은 List가 하는 일을 제대로 수행하지 못하니 하위 타입이 될 수 없다.(리스코프 치환법칙 위배)

  • 어떤 와일드카드를 써야 하는지 도움이 되는 방법
    • 펙스(PECS) : producer-extends, consumer-super , 나프탈린과 오들러는 이를 겟풋 원칙이라 부른다.
    • 매개변수화 타입 T가 생산자라면 <? extends T>를 사용한다.
      • 예를 들어 push All의 src 매개변수는 Stack이 사용할 E 인스턴스를 생산하므로 Iterable<? extends E>이다
    • 매개변수화 타입 T가 소비자라면 <? super T>를 사용한다.
      • 예를 들어 popAll의 dst 매개변수는 Stack으로부터 E 인스턴스를 소비하므로 Iterable<? super E>이다.
  • 제대로 사용하면 클라이언트는 와일드카드 타입이 쓰인지 의식하지 않아도 되며 클래스 사용자가 와일드카드 타입을 신경 써야 한다면 그 API에 무슨 문제가 있을 가능성이 크다.
  • 매개변수는 메서드 선언에 정의한 변수이고, 인수는 메서드 호출 시 넘기는 '실제값'이다.
    • void add(int value) { .. } : 매개변수, add(10) : 인수
    • class Set { .. } : 매개 변수, Set : 인수

와일드카드 타입을 적용하면 API가 훨씬 유연해진다. 널리 쓰일 라이브러리라면 반드시 와일드카드 타입을 적절히 사용해야한다.
PECS 공식 : 생산자(producer)는 extends를, 소비자(consumer)는 super를 사용한다.
Comparable과 Comparator는 모두 소비자이다.

아이템 32 : 제네릭과 가변인수를 함께 쓸 때는 신중하라

가변 인수 메서드와 제네릭은 자바 5 때 함께 추가되어 잘 어우러질거 같지만 그렇지 않다.

//    @SafeVarargs
//    static <T> List<T> flatten(List<? extends T>... lists) {
    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;
    }
  • 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 달자
  • 다음 두 조건을 모두 만족하는 제네릭 varargs 메서드는 안전하다.
    • varargs 매개변수 배열은 아무것도 저장하지 않는다.
    • 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.
  • @SafeVarargs 애너테이션은 재정의할 수 없는 애너테이션에 사용해야 한다.

가변인수와 제네릭은 궁합이 좋지 않은데 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭의 타입 규칙이 서로 다르기 때문이다.

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

  • 제네릭은 Set, Map<K,V> 등의 컬렉션과 ThreadLocal, AtomicReference 등의 단일 원소 컨테이너에도 흔히 쓰인다.
    • 이런 모든 쓰임에서 매개변수화되는 대상은 (원소가 아닌) 컨테이너 자신이다.
    • 예를 들어 Set에는 원소의 타입을 뜻하는 단 하나의 탕비 매개변수만 있으면 되며, Map에는 키와 값의 타입을 뜻하는 2개만 필요한 식이다.
  • 컨테이너 대신 키를 매개변수화 한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장하며 이런 설계 방식을 타입 안전 이종 컨테이너 패턴이라고 한다.
    • 각 타입의 Class 객체를 매개변수화한 키 역할로 사용하면 되는데 이 방식이 동작하는 이유는 class의 클래스가 제네릭이기 때문이다.
    • 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴을 타입 토큰이라 한다.

컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있는데 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다. 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다. 또한, 직접 구현한 키 타입도 쓸 수 있다. 예컨대 데이터베이스의 행(컨테이너)을 표현한 DatabaseRow 타입에는 제네릭 타입인 Column를 키로 사용할 수 있다.

6장 열거 타입과 애너테이션

  • 자바에서는 특수한 목정의 참조 타입이 있는데 하나의 클래스의 일종인 열거 타입이고, 인터페이스의 일종인 애너테이션이다.
  • 인스턴스 필드에 저장하여 값 자체를 넘기자.

Enum의 API 문서에 ordinal에 대해서 "대부분 프로그래머는 이 메서드를 쓸 일이 없고 EnumSet과 EnumMap 같이 열거 타입 기반의 범용 자료 구조에 쓸 목적으로 설계되었다." 따라서 이런 용도가 아니라면 ordinal 메서드는 절대 사용하지 말자.


아이템 36 : 비트 필드 대신 EnumSet을 사용하라.

열거한 값들이 주로(단독이 아닌) 집합으로 사용될 경우, 예전에는 각 상수에 서로 다른 2의 거듭제곱 값을 할당한 정수 열거 패턴을 사용해왔다.

  • EnumSet보다 인터페이스인 Set으로 받아 다른 Set 구현체를 넘기더라도 처리할 수 있도록 하는게 좋다.

정수 열거 패턴(아이템34), 이왕이면 인터페이스로 받는게 일반적으로 좋은 습관이다.(아이템64)

열거할 수 있는 타입을 한데 모아 집합 형태로 사용한다고 해도 비트 필드를 사용할 이유는 없다. EnumSet 클래스로 더 좋은 코드를 작성할 수 있고 단점으로는 불변 EnumSet을 만들수 없다는 것이다. Collections.unmodifiableSet으로 EnumSet을 감싸 사용할 수 있지만 명확성과 성능이 저하된다.


아이템 37 : ordinal 인덱싱 대신 EnumMap을 사용하라.

  • Map<Phase, Map<Phase, Transition>>은 "이전 상태에서 '이후 상태에서 전이로의 맵'에 대응시키는 맵"이라는 뜻이다.

ordinal 메서드(아이템35), 배열은 제네릭과 호환되지 않으니(아이템28), 런타임 제네릭 타입 정보를 제공한다.(아이템33), 스트림(아이템45)

배열의 인덱스를 얻기 위해 ordinal을 쓰는것은 일반적으로 좋지 않으니, 대신 EnumMap을 사용하라. Enum.ordinal을 (웬만해서는) 사용하지 말아야 한다.


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

인터페이스를 이용해 확장 가능한 열거 타입을 흉내내는 방식에서의 사소한 문제

  • 열거 타입끼리 구현을 상속할 수 없다.
  • 아무 상태에도 의존하지 않는 경우에는 디폴트 구현을 이용해 인터페이스를 추가하는 방법이 있다.

class 리터럴은 한정적 타입 토큰(아이템33), 와일드카드 타입(아이템31), EnumSet(아이템36), EnumMap(아이템37), 디폴트 구현(아이템20)

열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 함께 사용해 같은 효과를 낼 수 있다.


아이템 39 : 명명 패턴보다 애너테이션을 사용하라

  • 위 방식들로는 코드의 가독성을 개선할 수 있다면 이 방법을 사용해도 좋지만 애너테이션을 선언하고 처리하는 부분에서는 코드 양이 늘어나며 특히 처리 코드가 복잡해져 오류가 날 가능성이 커짐을 명심하자.

예외의 이름을 테스트 메서드 이름에 덧붙이는 방법도 있지만, 보기도 나쁘고 깨지기도 쉽다.(아이템62), 한정적 타입 토큰(아이템33), 애너테이션 타입들은 사용해야한다.(아이템40)

도구 제작자를 제외하고는, 일반 프로그래머가 애너테이션 타입을 직접 정의할 일은 거의 없지만 자바 프로그래머라면 예외 없이 자바가 제공하는 애너테이션 타입들은 사용해야 한다.


아이템 40 : @Override 애너테이션을 일관되게 사용하라

@Override는 메서드 선언에만 달 수 있으며 상위 타입의 메서드를 재정의했음을 의미하고 일관되게 사용하면 여러 가지 악명 높은 버그들을 예방해준다.

equals 메서드를 재정의하려 한 것으로(아이템10), hashCode도 함께(아이템11), 다중 정의(아이템52)

재정의한 모든 메서드에 @Override 애너테이션을 의식적으로 달면 여러분이 실수했을 때 컴파일러가 바로 알려줄것이다. 예외는 구체 클래스에서 상위 클래스의 추상 메서드를 재정의한 경우엔 달지 않아도 되며 달아도 해로울 것이 없다.


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

  • 마커 인터페이스 : 아무 메서드도 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스
    • Serializable 인터페이스가 가장 좋은 예로 직렬화를 할 수 있다고 알려주며 ObjectOutputStream을 통해 사용할 수 있다.
  • 마커 애너테이션이 등장하면서 마커 인터페이스는 구식이 되었다다고 하지만 두 가지 면에서 마커 애너테이션보다 낫다.
    1. 마커 인터페이스는 이를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있으나, 마커 애너테이션은 그렇지 않다.
      • 마커 인터페이스는 타입이기에 컴파일 오류 수준에서 잡을 수 있다.
    2. 적용 대상을 더 정밀하게 지정할 수 있다.
      • 적용 대상(@Target)을 ElementType.TYPE으로 선언한 애너테이션은 모든(클래스, 인터페이스, 열거타입, 애너테이션)에 달 수 있어 세밀하게 제어하지 못하지만 특정 인터페이스를 구현한 클래스에만 적용하고 싶은 마커가 있드면 인터페이스를 구현하면 된다.
        • 한 예로 Set을 들 수 있는데 Collection의 하위 타입이지만 Collection이 정의한 메서드 외에는 새로 추가한 것이 없다.
  • 마커 애너테이션이 마커 인터페이스보다 나은 점으로는 거대한 애너테이션 시스템의 지원을 받는다는 점을 들 수 있다.
    • 애너테이션을 적극활용하는 프레임워크는 일관성을 지키는데 유리하다.

언제 마커 애너테이션을, 언제 마커 인터페이스를 사용해여 하는가

  • 클래스와 인터페이스 외의 프로그램 요소(모듈, 패키지, 필드, 지역변수 등)에 마킹해야 할 경우에는 애너테이션을 쓸 수밖에 없다.
    • 클래스와 인터페이스만이 인터페이스를 구현하거나 확장할 수 있기 때문이다.
  • 마커를 클래스나 인터페이스에 적용해야 한다면 "이 객체를 매개변수로 받는 메서드를 작성할 일이 있을까?" 라고 자문하고 "그렇다"라고 한다면 마커 인터페이스를 써야 한다.
  • 반대로 메서드를 작성할 일은 절대 없다고 확신한다면 아마도 마커 애너테이션이 나은 선택일 것이다.
  • 추가로 애너테이션을 활발히 활용하는 프레임워크에서 사용하는 마커라면 마커 애너테이션을 사용하는 편이 좋을 것이다.

마커 애너테이션(아이템39)

새로 추가하는 메서드 없이 단지 타입 정의가 목적이라면 마커 인터페이스를 선택하자. 적용 대상이 ElementType.TYPE인 마커 애너테이션을 작성하고 있다면, 잠시 여유를 갖고 정말 애너테이션으로 구현하는게 옳은지, 혹은 마커 인터페이스가 낫지는 않을지 곰곰히 생각해보자.


람다와 스트림

아이템 42 : 익명 클래스보다는 람다를 사용하라

특정 함수나 동작에 사용하는 인터페이스를 함수 객체라하며 메서드를 하나만 담은 인터페이스를 의미한다.

  • 익명 클래스 방식은 코드가 너무 길어서 자바는 함수형 프로그래밍에 적합하지 않았다.
  • 람다식이 나오면서 문백을 살펴 타입을 추론해주어 한결 간략한 코드가 된다.

람다 자리에 바교자 생성 메서드를 사용하면 더 간결한 코드를 만들 수 있다.(아이템14,43), 함수 인터페이스(아이템44), private 정적 중첩 클래스(아이템24)

익명 클래스는(함수형 인터페이스가 아닌) 타입의 인스턴스를 만들 때만 사용하라. 람다는 작은 함수 객체를 아주 쉽게 표현할 수 있어 함수형 프로그래밍의 지평을 열었다.


아이템 43 : 람다보다는 메서드 참조를 사용하라

람다는 익명 클래스보다 간결한 특징이 있는데 메서드 참조를 이용하면 더 간결하게 작성할 수 있다.

비한정적 참조는 주로 스트림 파이프라인에서의 매핑과 필터 함수로 쓰인다.(아이템45)

메서드 참조는 람다의 간단명료한 대안이 될 수 있다. 메서드 참조 쪽이 짧고 명확하다면 메서드 참조를 쓰고, 그렇지 않을 때만 람다를 사용하라.


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

  • 람다를 지원하면서 API 작상 모범 사례도 변화되어 기본 메서드를 재정의하는 템플릿 메서드 패턴의 매력이 크게 줄었고 함수 객체를 받는 정적 팩터리나 생성자를 제공하는 방법이 현대적 해법이다.

    • 함수 객체를 매개변수로 받는 생성자와 메서드를 더 많이 만들어야 한다.
  • 위 두 가지 방식 중 편한 코드로 작성해도 되는데 스트림이 더 나아보이니 동료들도 괜찮다면 스트림으로 하라.

효과를 볼 수 있는 상황은 많지 않다.(아이템48), 이 맵은 단어들을 아나그램끼리 묶어놓은 것으로(아이템46)

스트림을 사용해야 멋지게 처리할 수 있는 일이 있고, 반복 방식이 더 알맞은 일도 있으며 잘 조합했을 때 가장 멋지게 해결된다. 스트림과 반복문 중 어느 쪽이 더 나은지 확신하기 어렵다면 둘 다 해보고 더 나은쪽을 택하라.


아이템 46 : 스트림에서는 부작용 없는 함수를 사용하라

  • 스트림은 함수형 프로그래밍에 기초한 패러다임으로 처음봐서는 이해하기 어려울 수 있지만 핵심은 계산을 일련의 변환으로 재구성하는 부분으로 각 변환 단계는 가능한 이전 단계의결과를 받아 처리하는 순수 함수여야 한다.
    • 순수 함수란 입력만이 결과에 영향을 주는 함수를 의미한다.
    • 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다.
    • 스트림 연산에 건네는 함수 객체는 모두 부작용(side effetc)이 없어야 한다.

forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 말자.

comparing 메서드는 키 추출 함수를 받는 비교자 생성 메서드다.(아이템14)

스트림 파이프라인 프로그래밍의 핵심은 부작용 없는 함수 객체에 있고 스트림뿐 아니라 스트림 관련 객체에 건네지는 모든 함수 객체가 부작용이 없어야 한다. 가장 중요한 수집기 팩터리는 toList, toSet, toMap, groupingBy, joining이다.


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

  • 기본은적으로 반환 타입으로 컬렉션 인터페이스를 사용하고 for-each문에서만 쓰이거나 반환된 원소 시퀀스가(주로 contains(Object) 같은) 일부 Collection 메서드를 구현할 수 없을 때는 Iterable 인터페이스를 사용했다. 반환 원소들이 기본 타입이거나 성능에 민감한 상황이라면 배열을 사용했지만 스트림이 자바 8에서 들어오면서 선택이 복잡한 일이 되어버렸다.
  • 스트림은 반복을 지원하지 않기에 반환 할 때 스트림은 적합하지 않으므로 스트림과 반복문을 조합해야 좋은 코드가 나온다.
  • Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함할 뿐아니라, Iterable 인터페이스가 정의한 방식대로 동작하지만 for-each로 스트림을 반복할 수 없는 이유는 Stream이 iterable을 확장하지 않아서이다.

스트림은 반복을 지원하지 않는다(아이템45)

원소 시퀀스를 반환하는 메서드를 작성할 때는, 스트림으로 처리하기를 원하는 사용자와 반복으로 처리하길 원하는 사용자가 모두 있을 수 있음을 떠올리고 양쪽을 모두 만족시키려고 노력하자.
컬렉션을 반환할 수 있다면 그렇게 하라. 반환 전부터 이미 원소들을 컬렉션에 담아 관리하고 있거나 컬렉션을 하나 더 만들어도 될 정도로 원소 개수가 젃다면 ArrayList 같은 표준 컬렉션에 담아 반환하라. 그렇지 않으면 앞서의 멱집합 예처럼 전용 컬렉션을 구현할지 고민하라.
컬렉션을 반환하는 게 불가능하면 스트림과 Iterable 중 더 자연스러운 것을 반환하라. 만약 나중에 Stream 인터페이스가 Iterable을 지원하도록 자바가 수정된다면, 그때는 안심하고 스트림을 반환하면 될 것이다.(스트림 처리와 반복 모두에 사용할 수 있으니)


아이템 48 : 스트림 병렬화는 주의해서 적용하라

동시성 프로그래밍에서는 안전성과 응답 가능상태를 유지하기 위해서 애써야 하는데 병렬 스트림 파이프라인 프로그래밍에서도 다를 바 없다.

성능을 테스트하여 병렬화를 사용할 가치가 있는지 확인해야 한다.(아이템67)

계산도 올바로 수행하고 성능도 빨라질거라는 확신 없이는 스트림 파이프라인 병렬화는 시도조차 하지 말라.


메서드

아이템 49 : 매개변수가 유효한지 검사하라

  • 생성자와 메서드의 입력 매개변수의 값은 보통 인덱스 값은 음수이면 안되며, 객체 참조는 null이 아니어야 한다.
  • 이런 제약은 반드시 문서화해야 하며 메서드 몸체가 시작되기 전에 검사해야 한다.

매개변수 검사에 실패하면 실패 원자성(아이템76), @throws 자바독 태그를 사용하면 된다.(아이템74), Expcetion(아이템72), 예외번역(아이템73)

메서드나 생성자를 작성할 때면 그 매개변수들에 어떤 제약이 있을지 생각해야 한다. 그 제약들을 문서화하고 메서드 코드 시작 부분에서 명시적으로 검사해야 한다.


아이템 50 : 적시에 방어적 복사본을 만들라

  • 자바는 c, c++보다 안전한 언어로 자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 불변식이 지켜진다.
  • 클라이언트가 여러분의 불변식을 꺠드리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.
    • 악의적인 의도를 가진 사람들이 시스템의 보안을 뚫으려는 시도가 늘고 있다.
    • 평범한 프로그래머도 순전히 실수로 여러분의 클래스를 오작동하게 만들 수 있다.
  • 어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하지만 주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다.

방어적 복사를 하지 않아야 하는 경우

  • 방어적 복사에는 성능 저하가 따르고, 또 항상 쓸 수 있는 것도 아니다.
    • (같은 패키지에 속하는 등의 이유로) 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략할 수 있고 해당 매개변수나 반환 값을 수정하지 말아야 함을 명확히 문서화하는게 좋다.
  • 다른 패키지에서 사용한다고 해서 넘겨받은 가변 매개변수를 항상 방어적 복사로 저장해야 하는건 아니다.
    • 때로는메서드나 생성자의 매개변수로 넘기는 행위가 그 객체의 통제권을 명백히 이전함을 뜻하기도 한다.
    • 이처럼 통제권을 이전하는 메서드를 호출하는 클라이언트는 해당 객체를 더 이상 직접 수정하는 일이 없다고 약속해야 하고 확실히 문서에 기재해야 한다.
  • 통제권을 넘겨 받기로 한 메서드나 생성자는 악의적인 공격에 취약하므로 불변식이 까지더라도 영향이 오직 호출한 클라이언트로 국한되도록 한정해야 한다.
    • 래퍼 클래스 패턴을 들 수 있는데 클라이언트는 래퍼에 넘긴 객체에 직접 접근할 수 있어 래퍼의 불변식을 쉽게 파괴할 수 있지만 영향은 오직 클라이언트 자신만 받게 된다.

매개변수의 유효성을 검사(아이템49), 되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다.(아이템17)

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


아이템 51 : 메서드 시그니처를 신중히 설계하라

  1. 메서드 이름을 신중하게 짓자
    • 항상 표준 명명 규칙을 따라야 쉽게 이해할 수 있고, 같은 패키지에 속한 다른 이름들과 일관되게 짓는 게 최우선 목표다.
  2. 편의 메서드를 너무 많이 만들지 말자
    • 모든 메서드는 각각 자신의 소임을 다해야 하는데 메서드가 너무 많은 클래스는 익히고, 사용하고, 문서화하고, 테스트하고, 유지보수하기 어렵다.
  3. 자주 쓰일 경우에만 별도의 약칭 메서드를 두고 확신이 서지 않으면 만들지 말자.
  4. 매개 변수 목록은 짧게 유지하라.
    • 4개 이하가 좋으며 4개가 넘어가면 매개변수를 전부 기억하기가 쉽지 않다.
  5. 같은 타입의 매개변수 여려 개가 연달아 나오는 경우가 특히 해롭다.

표준 명명 규칙(아이템68), 정적 멤버 클래스(아이템24), 빌더 패턴(아이템2), 매개변수의 타입으로는 클래스보다 인터페이스가 더 낫다(아이템64), 열거 타입 상수의 메서드 안으로 리팩터링해 넣을 수도 있다.(아이템34)

  1. 메서드 이름을 신중히 짓자.
  2. 편의 메서드를 너무 많이 만들지 말자.
  3. 확신이 서지 않으면 별도의 약칭 메서드를 만들지 말자.
  4. 매개변수 목록을 짧게 유지히자.
  5. 같은 타입의 매개변수 여러개가 연달아 나오는 경우가 특히 해롭다.
  6. 매개변수의 타입으로는 클래스보다는 인터페이스가 더 낫다.
  7. boolean보다는 원소 2개짜리 열거 타입이 낫다.

아이템 52 : 다중정의는 신중히 사용하라

  • 자바 8에서 람다와 메서드 참조도 안전하지 않다.

메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수를 받아서는 안 된다.

  • 서로 다른 함수형 인터페이스라도 서로 근본적으로 다르지 않다는 뜻이다.
  • 컴파일할 때 명령줄 스위치로 -Xlint:overloads를 지정하면 다중정의 경고를 준다.

자바 라이브러리는 이번 아이템의 정신을 지키려고 애쓰지만 실패한 클래스도 몇 개 있다.

  • String 클래스의 valueOf(char[])와 valueOf(Object)는 같은 객체를 건네더라도 다르게 수행한다.

가변인수(아이템53), 정적 팩터리(아이템1)

다중정의가 허용된다고 해서 꼭 활용하란 의미는 아니며 일반적으로 매개변수 수가 같을 때는 다중정의를 피하는게 좋다.


아이템 53 : 가변인수는 신중히 사용하라

  • 길이가 0인 배열을 반환하는 코드
  • 단순히 성능을 개선할 목적이라면 toArray에 넘기는 배열을 미리 할당하는건 추천하지 않는다.
    • toArray는 원소가 하나라도 있다면 배열을 새로 생성하고 0개이면 전달받은 배열은 반한한다.

성능 저하의 주범(아이템67), 불변 객체(아이템17)

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


아이템 55 : 옵셔널 반환은 신중히 하라

  • 자바 8 이전에는 메서드가 특정 조건에서 값을 반환할 수 없을 때 예외를 던지거나, (반환 타입이 객체 참조라면) null을 반환하는 것이었지만 허점이 있다.

    • 예외는 진짜 예외적인 상황에서만 사용해야 하고 예외 생성 시 스택 추적 전체를 캡처하므로 비용도 만만치 않다.
    • null을 반환하면 null이 반환 되지 않는다고 확신하지 않는 한 별도의 null 처리 코드를 추가해야 한다.
  • 자바 8이 되면서 Optional라는 대안이 생겼는데 null이 아닌 T타입 참조를 하나 담거나 혹은 아무것도 담지 않을 수 있다.

    • 아무것도 담지 않은 옵셔널은 '비었다'고 말하고 어떤 값을 담으면 '비지 않았다'고 한다.
    • 원소를 최대 1개 가질 수 있는 '불변' 컬렉션이다.
  • 인스턴스 필드에 저장해두면 해당 클래스를 확장해 하위 클래스를 따로 만들어야 함을 암시하는 '나쁜 냄새'가 난다.

진짜 예외적인 상황(아이템80), 검사 예외(아이템71), stream의 flatMap(아이템45), 빈컬렉션을 반환하자(아이템54), 세심한 측정(아이템67)

값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환 값이 없을 가능성을 염두에 둬야 하는 메서드라면 옵셔널을 반환해야 할 상활일 수 있다.
하지만 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있다.


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

  • 문서화 주석을 작성하는 규칙은 공식 언어 명세에 속하진 않지만 자바 프로그래머라면 응당 알아야 하는 업계 표준 API라 할 수 있고 이 규칙은 문서화 주석 방법(How to Write Doc Comments) 웹페이지에 기술되어 있다.
    • 자바 4이후로는 갱신되지 않은 페이지지만, 그 가치는 여전하다.
  • 자바 버전이 올라가며 추가 된 중요한 자바독 태그로는 자바 5의 @literal, @code 자바 8의 @implSpec, 자바 9의 @index를 꼽을 수 있다.

정말 잘 쓰인 문서인지를 확인하는 유일한 방법은 자바독 유틸리티가 생성한 웹페이지를 읽어보는 길뿐이다.

직렬화 형태(아이템87), 상속(아이템19), 모든 예외(아이템74), 모듈 시스템(15), 스레드 안전 수준(아이템82), 직렬화 형태(아이템87)

문서화 주석은 여러분 API를 문서화하는 가장 휼룡하고 효과적인 방법이다.

일반적인 프로그래밍 원칙

지역변수, 제어규ㅜ조, 라이브러리, 데이터 타입, 그리고 리플렉션과 네이티브 메서드를 다루고 최적화와 명명 규칙을 논한다.

아이템 57 : 지역변수의 범위를 최소화하라

이번 아이템은 기본적으로 "클래스와 멤버의 접근 권한을 최소화하라"라고 했던 아이템15와 취지가 비슷하다.

지역 변수의 범위를 최소화 하는 방법

  1. 지역 변수의 범위를 줄이는 가장 강력한 기법은 '가장 처음 쓰일 때 선언하기'다.
    • 미리 선언해두면 코드가 어수선해지고 가독성이 떨어지고 사용 시점에는 초기 값이 기억나지 않을 수도 있다.
  2. 거의 모든 지역변수는 선언과 동시에 초기화해야 한다.
    • 초기화에 필요한 정보가 충분하지 않다면 충분해질 때까지 선언을 미뤄야 한다.
    • 검사 예외를 던질 가능성이 있으면 try 안에서 초기화 해야 하고 try 바깥에서도 사용해야 한다면 try 블록 앞에서 선언해야 한다.
  3. 반복 변수가 계속 필요하지 않다면 while보다는 for문을 쓰는게 낫다.
    • 반복자를 사영해야 하는 상황에는 for-each문 보다 전통적인 for문을 쓰는게 낫다.
    • while문에서는 복사해 붙여넣기 오류가 발생 할 수 있다. int i를 첫 번째 while에서 쓰고 두 번째 while문에서 다시 사용하는 실수를 할 수도 있다.
    • for문은 while문보다 짧아서 가독성이 좋다.
  4. 메서드를 작게 유지하고 한 가지 기능에 집중하는것이다.
    • 한 메서드에서 여러 가지 기능을 처리한다면 그 중 한 기능과만 관련된 지역변수라도 다른 기능을 수행하는 코드에서 접근할 수 있게되는 문제가 발생한다.
  1. 지역 변수는 가장 처음 쓰일 때 작성한다.
  2. 선언과 동시에 초기화 해야 한다.
  3. while보다는 for문을 사용하자.
  4. 메서드를 작게 유지하고 한 가지 기능에 집중한다.

아이템 58 : 전통적인 for문보다는 for-each문을 사용하라

스트림이 제격인 작업이 있고, 반복이 제격인 작업이 있다.

for-each문을 사용할 수 없는 상황 세 가지

아래 세 가지 경우에 해당하면 일반적인 for문을 사용하되 이번 아이템에 언급 된 문제들을 경계해야 한다.

  1. 파괴적인 필터링(clestructive : 컬렉션을 순회하면서 선택된 원소를 제거해야 한다면 반복자의 remove메서드를 호출해야 한다. 자바 8부터는 Collection의 removeIf 메서드를 사용해 컬렉션을 명시적으로 순회하는 일을 피할 수 있다.
  2. 변형(transforming) : 리스트나 배열을 순회하면서 그 원소와 값 일부 혹은 전체를 교체해야 한다면 리스트의 반복자나 배열의 인덱스를 사용해야 한다.
  3. 병렬 반복(parallel iteration) : 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야 한다.

for-each문은 컬렉션과 배열은 물론 Iterable 인터페이스를 구현한 객체라면 무엇이든 순회할 수 있다. Iterable 인터페이스는 다음과 같은 메서드가 단 하나뿐이다.

  • 라이브러리가 너무 방대하여 모든 API 문서를 공부하기에는 벅차지만 자바 프로그래머라면 적어도 java.lang, java.utul, java.io와 그 하위 패키지들에는 익숙해져야 한다.
  • 어떤 영역의 기능을 제공하는지 살펴보고, 여러분이 원하는 기능이 아니라 판단되면 대안을 사용하자.
  • 어떤 라이브러리든 제공하는 기능은 유한하므로 항상 빈 구멍이 있기 마련이고 라이브러리에서 원하는 기능을 찾지 못하면 고품질의 서드파티 라이브러리가 될 것이며 대표적으로 구글의 구아바 라이브러리가 있다.

컬렉션 프레임워크와 스트림 라이브러리(아이템45~48), java.util.concurrent의 고수준 개념(아이템80,81)

바퀴를 다시 발명하지 말자. 아주 특별한 나만의 기능이 아니라면 누군가 이미 라이브러리 형태로 구현해놓았을 가능성이 크다.


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

float와 double 타입은 과학과 공학 게산용으로 설계되어 이진 부동소수점 연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 '근사치'로 계산하도록 세심하게 설계되었기 때문에 정확한 결과가 필요할 때는 사용하면 안 된다.

  • 기본 타입 계산방식으로 다룰 수 있는 값의 크기가 제한되고 소수점을 직접 관리해야 한다.

정확한 답이 필요한 계산에는 float나 double을 피하라.
소수점 추적은 시스템에 맡기고, 코딩 시에 불편함이나 성능 저하를 신경쓰지 않겠다면 BigDecimal을 사용하라.
반면 성능이 중요하고 소수점을 직접 추적할 수 있고 숫자가 너무 크지 않다면 int나 log을 사용하라. 숫자를 아홉자리 십진수로 표현할 수 있다면 int를 열여덟 자리 십진수로 표현할 수 있다면 log을 사용하라.
열여덟 자리를 넘어가면 BigDecimal을 사용해야 한다.


아이템 61 : 박싱된 기본 타입보다는 기본 타입을 사용하라

자바의 데이터 타입은 크게 int, double, boolean같은 기본 타입과 String, List같은 타입으로 나눌 수 있다.

박싱된 기본 타입은 언제 사용해야 하는가

  1. 컬렉션의 원소, 키, 값으로 쓴다. 컬렉션은 기본 타입을 담을 수 ㅇ벗으므로 어쩔 수 없이 박싱된 기본 타입을 써야 한다.
  2. 매개변수화 타입이나 매개변수화 메서드의 타입 매개변수로는 박싱된 기본 타입을 써야 한다.
  3. 리플렉션을 통해 메서들를 호출할 때도 박싱된 기본 타입을 사용해야 한다.

오토박싱과 오토언박싱(아이템6), 정적 compare 메서드를 사용해야 한다(아이템14), 리플렉션(아이템65)

기본 타입과 박싱된 기본 타입 중 하나를 선택해야 한다면 가능하다면 기본 타입을 사용하라.
기본 타입은 간단하고 빠르며 박싱된 타입을 써야 한다면 주의를 기울이자.
오토박싱이 박싱된 기본 타입을 사용할 때의 번거로움을 줄여주지만, 그 위험까지 없애주지는 않는다.
언박싱 과정에서 NullPointerException을 던질 수 있다.


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

  • Object를 실제 탕비으로 형변환해서 써야 해서 타입 안전하지 않아 ThreadLocal을 매개변수화 타입으로 선언해준다.

정적 멤버 클래스(아이템 24), 매개변수화 타입(아이템29)

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


아이템 63 : 문자열 연결은 느리니 주의하라

문자열 연결 연산자로 문자열 n개를 잇는 시간은 n^2에 비례한다.

  • 문자열 연결 연산자(+)는 여러 문자열을 하나로 합쳐주는 편리한 수단으로 한 줄짜리 출력 값 혹은 작고 크기가 고정된 객체의 문자열 표현을 만들때라면 괜찮지만, 본격적으로 사용하기 시작하면 성능 저하를 감내하기 어렵다.
  • 문자열은 불변이라서 두 문자열을 연결할 경우 양쪽의 내용을 모두 복사해야 하므로 성능 저하는 피할 수 없다.
  • 자바 6 이후 문자열 연결 성능을 다방면으로 개선했지만 차이는 여전히 크다

문자열 불변(아이템 17)

원칙은 간단하다. 성능에 신경 써야 한다면 많은 문자열을 연결할 때는 문자열 연결 연산자(+)를 피하자. 대신 StringBuilder의 append 메서드를 사용하라. 문자 배열을 사용하거나, 문자열을(연결하지 않고) 하나씩 처리하는 방법도 있다.


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

적합한 인터페이스만 있다면 매개변수뿐 아니라 반환 값, 변수, 필드를 전부 인터페이스 타입으로 선언하라.

  • 객체는 클래스가 아닌 인터페이스로 참조하라는 의미이기도 하다.
  • 객체의 실제 클래스를 사용해야 할 상황은 '오직' 생성자로 생성할 때뿐이다.

인터페이스 타입으로 사용하는걸 습관화 하고 적합한 인터페이스가 없다면 클래스를 참조해야 한다.


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

적절한 인터페이스나 상위 클래스를 이용할 수 있을 것이다.(아이템 64)

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


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

  • 자바 네이티브 인터페이스[JNI(Java Native Interface)]는 자바 프로그램이 네이티브 메서드를 호출하는 기술로써 C나 C++같은 네이티브 프로그래밍 언어로 작성한 메서드를 호출함을 말한다.

네이티브 메서드의 단점

  1. 네이티브 메서드는 안전하지 않으므로 네이티브 메서드를 사용하는 애플리케이션도 메모리 훼손 오류로부터 더 이상 안전하지 않다.
  2. 자바보다 플랫폼을 많이 타서 이식성도 낮다.
  3. 디버깅도 어렵고 주의하지 않으면 속도가 오히려 느려질 수도 있다.
  4. 가비지 컬렉터가 네이티브 메모리는 자동 회수하지 못하고, 심지어 추적조차 할 수 없다.
  5. 자바 코드와 네이티브 코드의 경계를 넘나들 때마다 비용도 추가 된다.
  6. 네이티브 메서드와 자바 코드 사이의 '접착 코드'를 작성해야 하는데 귀찮은 작업이기도 하고 가독성도 떨어진다.

안전하지 않으므로(아이템50), 가비지컬렉터(아이템8)

네이티브 메서드를 사용하려거든 한번 더 생각하라. 네이티브 메서드가 성능을 개선해주는 일은 많지 않다. 저수준 자원이나 네이티브 라이브러리를 사용해야만 해서 어쩔 수 없더라도 네이티브 코드는 최소한만 사용하고 철저히 테스트 하라.


아이템 67 : 최적화는 신중히 하라

모든 사람이 마음 깊이 새겨야 할 최적화 격언 세 가지를 소개한다.

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

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

  • 성능 때문에 견고한 구조를 희생하지 말자.
  • 좋은 프로그램이지만 원하는 성능이 나오지 않는다면 그 아키텍처 자체가 최적화할 수 있는 길을 안내해줄 것이다.
  • 좋은 프로그램은 정보 은닉 원칙을 따르므로 개별 구성요소의 내부를 독립적으로 설계할 수 있어서 시스템의 영향을 주지 않고도 각 요소를 다시 설계할 수 있다.
  • 프로그램을 완성할 때까지 성능 문제를 무시하라는 의미는 아니며 구현상의 문제는 나중에 최적화해 해결할 수 있지만, 아키텍처의 결함이 성능을 제한하는 상황이라면 시스템 전첼ㄹ 다시 작성하지 않고는 해결하기 불가능할 수 있다.
  • 완성된 설계의 기본 틀을 변경하려다 보면 유지보수하거나 개선하기 어려운 꼬인 구조의 시스템이 만들어지기 쉽기 때문에 설계 단계에서 성능을 반드시 염두에 두어야 한다.

성능을 제한하는 설계를 피하라.

  • 완성 후 변경하기가 가장 어려운 설계 요소는 바로 컴포넌트나 외부 시스템과의 소통 방식이다. API, 네트워크 프로토콜, 영구 저장용 데이터 포맷 등이 대표적이다.
  • 완성 후에는 변경하기 어렵거나 불가능할 수 있으며, 동시에 시스템 성능을 심각하게 제한할 수 있다.

API를 설계할 때 성능에 주는 영향을 고려하라.

  • public 타입을 가변으로 만들면 내부 데이터를 변경할 수 있게 만들어 불필요한 방어적 복사를 수 없이 유발할 수 있다.
  • 컴포지션으로 해결할 수 있음에도 상속 방식으로 설계한 public 클래스는 상위 클래스에 영원히 종속되며 그 성능까지도 물려받게 된다.
  • 인터페이스도 있는데 굳이 구현 타입을 사용하는 것 역시 좋지 않으며 특정 구현체에 종속되어 더 빠른 구현체가 나오더라도 이용하지 못하게 된다.

성능을 위해 API를 왜곡하는건 매우 안 좋은 생각이다.

  • 다행히 잘 설계된 API는 성능도 좋은 게 보통이다.
  • API를 왜곡하도록 만든 성능 문제는 해당 플랫폼이나 아랫단 소프트웨어의 다음 버전에서 사라질 수도 있지만 왜곡된 API와 이를 지원하는데 따르는 고통은 영원히 계속될 것이다.
  • 신중하게 설계하여 깨끗하고 명확하고 멋진 구조를 갖춘 프로그램을 완성한 다음에야 최적화를 고려해볼 차례가 되고 성능에 만족하지 못할 경우에는 한정된다.

잭슨이 소개한 최적화 규칙 두 가지에 하나를 더 추가한다면 '각각의 최적화 시도 전후로 성능을 측정하라'

  • 첫 번째 "하지 마라", 두 번째 "(전문가 한정) 아직 하지 마라" 세 번째 "각각의 최적화 시도 전후로 성능을 측정하라"가 되겠다.
  • 시도한 최적화 기법이 성능을 눈에 띄게 높이지 못하는 경우가 많고 심지어 더 나빠지게 할 때도 있는데 주요 원인은 프로그램에서 시간을 잡아먹는 부분을 추측하기 어려움에 있다.
  • 일반적으로 90%의 시간을 단 10%의 코등레서 사용한다는 사실을 기억해두자.
  • 프로파일링 도구(profiling tool)는 최적화 노력을 어디에 집중해야 할지 찾는데 도움을 준다.
    • 개별 메서드의 소비 시간과 호출 횟수 같은 런타임 정보를 제공하여, 집중할 곳은 물론 알고리즘을 변경해야 한다는 사실을 알려주기도 한다.
    • 그 외에 jmh도 언급할만한 도구로 자바 코드의 상세한 성능을 알기 수비게 보여주는 벤치마킹 프레임워크이다.

프로그래머가 작성한 코드와 CPU에서 수행하는 명령 사이의 '추상화 격차'가 커서 최적화로 인한 성능 변화를 일정하게 예측하기가 그만큼 어렵다.

  • 자바의 성능 모델은 정교하지 않을뿐더러 구현 시스템, 릴리스, 프로세서마다 차이가 있다.
  • 여러분의 프로그램을 여러 가지 자바 플랫폼이나 여러 하드웨어 플랫폼에서 구동한다면 최적화의 효과를 그 각각에서 측정하다보면 다른 구현 혹은 하드웨어 플랫폼 사이에서 성능을 타협해야 하는 상황도 마주할 것이다.

시스템의 나머지에 영향을 주지 않고도 각 요소를 다시 설계할 수 있다(아이템15), 방어적 복사(아이템50), 상속 방식(아이템18), 인터페이스를 사용하라(아이템 64), 불변(아이템17)

빠른 프로그램을 작성하려 안달하지 말자. 좋은 프로그램을 작성하다 보면 성능은 따라오기 마련이다. 하지만 시스템을 설계할 때, 특히 API, 네트워크 프로토콜, 영구 저장용 데이터 포맷을 설계할 때는 성능을 염두에 두어야 한다.
시스템 구현을 완료했다면 이제 성능을 측정해보라. 충분히 빠르면 그것으로 끝이고 그렇지 않다면 프로파일러를 사용해 문제의 원인이 되는 지점을 찾아 최적화를 수행하라.


아이템 68 : 일반적으로 통용되는 명명 규칙을 따르라

  • 철자 규칙은 패키지, 클래스, 인터페이스, 메서드, 필드, 타입 변수의 이름을 다루고 특별한 이유가 없는 한 반드시 따라야 한다.
  • 규칙을 어긴 API는 사용하기 어렵고, 유지보수 하기 어렵다
  • 철자 규칙이나 문법 규칙을 어기면 다른 프로그래머들이 코드를 읽기 번거로울 뿐 아니라 다른 뜻으로 오해할 수도 있고 그로 인해 오류까지 발생할 수 있다.

규칙

  1. 패키지와 모듈 이름은 각 요소를 점(.)으로 구분하여 계층적으로 짓는다.

  2. 요소들은 모두 소문자 알파벳 혹은 (드물게) 숫자로 이뤄진다.

  3. 조직 바깥에서도 사용될 패키지라면 조직의 인터넷 도메인 이름을 역순으로 사용하고 예외적으로 표준 라이브러리와 선택적 패키지들은 각각 java와 javax로 시작한다.

  4. 패키지 이름의 나머지는 해당 패키지를 설명하는 하나 이상의 요소로 이뤄진다.

    • 각 요소는 일반적으로 8자 이하의 짧은 단어로 한다.
    • utilities보다는 util처럼 의미가 통하는 약어를 추천한다.
    • 요소의 이름은 보통 한 단어 혹은 약어로 이뤄진다.
  5. 인터넷 도메인 이름 뒤에 요소 하나만 붙인 패키지가 많지만, 많은 기능을 제공하는 경우엔 계층을 나눠 더 많은 요소로 구성해도 좋다.

  6. (열거 타입과 애너테이션을 포함해) 클래스와 인터페이스의 이름은 하나 이상의 단어로 이뤄지며, 각 단어는 대문자로 시작한다.(List, FutherTask 등)

  7. 여러 단어의 첫 글자만 딴 약자는 max, min처럼 널리 통용되는 줄임말을 제외하고는 단어를 줄여 쓰지 않도록 한다.

  8. 약자의 경우 첫 글자만 대문자로 할지 전체를 대문자로 할지는 살짝 논란이 있다. 전체를 대문자로 쓰는 프로그래머도 있지만, 첫 글자만 대문자로 하는 쪽이 훨씬 많다.(HttpUrl)

  9. 메서드와 필드 이름은 첫 글자를 소문자로 쓴다는 점만 빼면 클래스 명명 규칙과 같다.(remove, ensureCapacity) 첫 단어가 약자라면 단어 전체가 소문자여야 한다.

  10. 상수 필드를 구성하는 단어는 모두 대문자로 쓰며 단어 사이는 밑줄로 구분한다.(VALUES, NEGATIVE_INFINITY 등) 이름에 밑줄은 상수 필드가 유일하다.

  11. 지역 변수에도 다른 멤버와 비슷한 명명 규칙이 적용된다. 약어를 써도 좋은데 변수가 사용되는 문맥에서 의미를 쉽게 유추할 수 있기 때문이다.

  12. 타입 매개변수 이름은 보통 한 문자로 표현되며 아래 다섯 가지 중 하나이다.

    1. 임의의 타입은 T (Type)
    2. 컬렉션 원소의 타입은 E (Element)
    3. 맵의 키와 값에는 K와 V (Key 와 Value)
    4. 예외에는 X (eXception)
    5. 메서드의 반환 타입에는 R (Return)
  13. 문법 규칙은 철자 규칙과 비교하면 더 유연하고 논란도 많은데 패키지에 대한 규칙은 따로 없다

  14. 객체를 생성할 수 있는 클래스(열거 타입 포함)의 이름은 보통 단수 명사나 명사구를 사용한다.(Thread, PriorityQueue, ChessPiece 등)

  15. 객체를 생성할 수 없는 클래스의 이름은 보통 복수행 명사로 짓는다.(Collectors, Collections 등)

  16. 인터페이스 이름은 클래스와 똑같이 짓거나(Collection, Comparator 등), able 혹은 ible로 끝나는 형용사로 짓는다.(Runnable, Iterable, Accessible)

  17. 애너테이션은 워낙 다양하게 활용되어 지배적인 규칙이 없이 명사, 동사, 전치사, 형용사가 두루 쓰인다.(BindingAnnotation, Inject, ImplementedBy, Singleton 등)

  18. 어떤 동작을 수행하는 메서드의 이름은 동사나(목적어를 포함한) 동사구로 짓는다.(append, drawImage)

  19. boolean 값을 반환하는 메서드라면 보통 is나 (드물게) has로 ㅅ작하고 명사나 명사구, 혹은 형용사로 기능하는 아무 단어나 구로 끝나도록 짓는다.(isDigit, isProbablePrime, isEmpty, isEnabled, hassSibling 등)

  20. 반환 타입이 boolean이 아니거나 해당 인스턴스의 속성을 반환하는 메서드의 이름은 보통 명사, 명사구, 혹은 get으로 시작하는 동사구로 짓는다.(size, hashCode, getTime 등)

  21. get으로 시작하는 형태만 써야 한다는 주장도 있지만, 근거가 빈약하다. 다음 코드에서 보듯 보통은 처음 두 형태를 사용한 코드의 가독성이 더 좋기 떄문이다.

    if (car.speed() > * SPEED_LIMIT)
       generateAudibleAlert("경찰 조심하세요!");
    • 한편 클래스가 한 속성의 게터와 세터를 모두 제공할 때도 적합한 규칙으로 이런 경우라면 보통 getAttribute와 setAttribute 형태의 이름을 갖게 될 것이다.
식별자 타입
패키지와 모듈 org.junit.jupiter.api, com.google.common.collect
클래스와 인터페이스 Stream, FutureTask, LinkedHashMap, HttpClient
메서드와 필드 remove, groupingBy, getCrc
상수 필드 MIN_VALUE, NEGATIBE_INFINITY
지역변수 i, denmon, houseNum
타입 매개변수 T, E, K, V, X, R, U, V, T1, T2

꼭 언급해야 하는 특별한 메서드 이름 몇가지

  1. 객체의 타입을 바꿔서 다른 타입의 또 다른 객체를 반환하는 인스턴스 메서드의 이름은 보통 toType 형태로 짓는다.(toString, toArray 등)
  2. 객체의 내용을 다른 뷰로 보여주는 메서드의 이름은 asType형태로 짓는다(asList 등)
  3. 객체의 값을 기본 타입 값으로 반환하는 메서드의 이름은 보통 typeValue 형태로 짓는다.(intValue 등)
  4. 정적 팩터리의 이름은 다양하지만 from, of, valueOf, instance, getInstance, newInstance, getType, newType을 흔히 사용한다.

필드 이름에 관한 문법 규칙은 클래스, 인터페이스, 메서드 이름에 비해 덜 명확하고 덜 중요하다.

  • API 설계를 잘 했다면 필드가 직접 노출될 일이 거의 없기 때문이다.
  • boolean 타입의 필드 이름은 보통 boolean 접근자 메서드에서 앞 단어를 뺀 형태다(initialized, composite 등)
  • 다른 타입의 필드라면 명사나 명사구를 사용한다.(height, digits, bodyStyle 등)
  • 지역변수 이름도 필드와 비슷하게 지으면 되나, 조금 더 느슨하다.

불변 참조 타입(아이템17), 객체를 생성할 수 없는 클래스(아이템4), 다른 뷰로 보여주는 메서드(아이템6), 정적팩터리(아이템1)

표준 명명 규칙을 체화하여 자연스럽게 베어 나오도록 하자.
철자 규칙은 직관적이라 모호한 부분이 적은 데 반해, 문법 규칙은 더 복잡하고 느슨하다.
자바 언어 명세의 말을 인용하면 "오랫동안 따라온 규칙과 충돌한다면 그 규칙을 맹종해서는 안 된다." 상식이 이끄는 대로 따르자.

예외

예외를 잘 활용하면 가독성, 신뢰성, 유지보수성이 높아지지만, 잘못 사용하면 반대의 효과가 나타난다.

아이템 69 : 예외는 진짜 예외 상황에만 사용하라

상태 검사 메서드, 옵셔널, 특정 값 중 하나를 선택하는 지침

  1. 외부 동기화 없이 여러 스레드가 동시에 접근할 수 있거나 외부 요인으로 상태가 변할 수 있다면 옵셔널이나 특정 값을 사용한다. 상태 검사 메서드와 상태 의존적 메서드 호출 사이에 객체의 상태가 변할 수 있기 때문이다.
  2. 성능이 중요한 상황에서 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행한다면 옵셔널이나 특정 값을 선택한다.
  3. 다른 모든 경우엔 상태 메서드 방식이 조금 더 낫다고 할 수 있다. 가독성이 살짝 더 좋고, 잘못 사용했을 때 발견하기가 쉽다. 상태 검사 메서드 호출을 깜빡 잊었다면 상태 의존적 메서드가 예외를 던져 버그를 확실히 드러낼 것이다. 반면 특정 값은 검사하지 않고 지나쳐도 발견하기가 어렵다.(옵셔널에는 해당하지 않는 문제다.)

직관적이지 않다는 사실(아이템67), 옵셔널(아이템55)

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


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

자바는 문제 상황을 알리는 타입으로 검사 예외, 런타임 예외, 에러 세 가지를 제공하는데 언제 무엇을 사용해야 하는지 헷갈려 하는 프로그래머들이 종종 있다. 100% 명확한건 아니지만 참고할 만한 지침을 제공한다.

예외를 사용해야 하는 상황

  1. 호출하는 쪽에서 복구하리라 여겨지는 상황이라면 검사 예외를 사용하라.
    • 검사와 비검사 예외를 구분하는 기본 규칙으로 검사 예외를 던지면 호출자가 그 예외를 catch로 잡아 처리하거나 더 바깥으로 전파하도록 강제하게 된다. 메서드 선언에 포함된 검사 예외 각각은 그 메서드를 호출했을 때 발생할 수 있는 유력한 결과임을 API 사용자에게 알려주는 것이다.
    • API 설계자는 API 사용자에게 검사 예외를 던져주어 그 상황에서 회복해내라고 요구하고 사용자는 예외를 잡기만 하고 별다른 조치를 취하지 않을 수도 있지만 이는 좋지 않는 생각이다.
    • 비검사 throwable은 두 가지로 런타임 예외와 에러이다.
      1. 동작 측면에서는 다르지 않고 프로그램에서 잡을 필요가 없거나 혹은 통상적으로는 잡지 말아야 한다.
      2. 프로그램에서 비검사 예외나 에러를 던졌다는 것은 복구가 불가능하거나 더 실행해봐야 득보다 실이 많다는 의미로 throwable을 잡지 않은 스레드는 적절한 오류 메시지를 내뱉으며 중단된다.
  2. 프로그래밍 오류를 나타낼 때는 런타임 예외를 사용하자.
    • 전제조건을 만족하지 못했을 때 발생하는 오류로 단순히 클라이언트가 해당 API의 명세에 기록된 제약을 지키지 못했다는 의미이다.
    • 복구할 수 있는 상황인지 프로그래밍 오류인지 명확히 구분되지는 않는 점이 있다.
      • 예를 들어 자원 고갈은 프로그래밍 오류일 수도 있고 진짜 자원이 부족해서 발생한 문제일 수 있다. 자원이 일시적으로만 부족하거나 수요가 순간적으로만 몰린 것이라면 충분히 복구할 수 있는 상황이다.
    • 복구 가능하다고 믿는다면 검사 예외를 그렇지 않다면 런타임 예외를 사용하자.
    • 확신하기 어렵다면 아마도 비검사 예외를 선택하는 편이 나을 것이다.
  3. 여러분이 직간접적으로구현하는 비검사 throwable은 모두 RuntimeException의 하위 클래스여야 한다.
    • 에러는 보통 JVM이 자원 부족, 불변식 깨짐 등 더 이상 수행을 계속할 수 없는 상황을 나타낼 때 사용하고 자바 언어 명세가 요구하는 것은 아니지만 널리 퍼진 규약이니, Error 클래스를 상속해 하위 클래스를 만드는 일은 자제해야 한다.
    • Error는 상속하지 말아야 할 뿐 아니라, throw문으로 직접 던지는 일도 없어야 한다.(AssertionError는 예외다.)
    • Exception, RuntimeException, Error를 상속하지 않는 throwable을 만들 수도 있고 자바 명세에서 직접 다루지 않지만 암묵적으로 일반적인 검사 예외(Exception의 하위 클래스 중 RuntimeException을 상속하지 않은)처럼 다룬다.
    • throwable은 이로울게 없으니 절대 사용하지 말자! throwable은 정상적인 검사 예외보다 나을게 하나도 없으면서 API 사용자를 헷갈리게 할 뿐이다.
  4. API 설계자들도 예외 역시 어떤 메서드라도 정의할 수 있는 완벽한 객체라는 사실을 잊곤한다.
    1. 예외의 메서드는 주로 예외를 일으킨 상황에 관한 정보를 코드 형태로 전달하는데 쓰이며 이런 메서드가 없다면 프로그래머들은 오류 메시지를 파싱해 정보를 빼내야 하는데, 대단히 나쁜 습관이다.
    2. throwable 클래스들은 대부분 오류 메시지 포맷을 상세히 기술하지 않는데, JVM이나 릴리스에 따라 포맷이 달라질 수 있다는 의미로 메시지 문자열을 파싱해 얻은 코드는 깨지기 쉽고 다른 환경에서 동작하지 않을 수 있다.
  5. 검사 예외는 일반적으로 복구할 수 있는 조건일 때 발생한다.
    1. 호출자가 예외 상황에서 벗어나는데 필요한 정보를 알려주는 메서드를 함께 제공하는 것이 중요하다.
    2. 예를 들어 물건을 구입하려는데 잔고가 부족하여 검사 예외를 발생했다고 했을 경우에 예외는 잔고가 얼마나 부족한지를 알려주는 접근자 메서드를 제공해야 한다.

예외를 잡기만 하고 별다른 조치를 하지 않을 수 있지만(아이템77), 비검사 예외(아이템71), 파싱해 정보를 빼내야 하는 나쁜 습관이다(아이템12)

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


아이템 71 : 필요 없는 검사 예외는 사용을 피해라

  • 검사 예외를 싫어하는 자바 프로그래머가 많지만 제대로 활용하면 API와 프로그램의 질을 높일 수 있다.
  • 결과를 코드로 반환하거나 비검사 예외를 던지는 것과 달리, 검사 예외는 발생한 문제를 프로그래머가 처리하여 안전성을 높이게끔 해준다.

스트림(아이템45~48), 옵셔널(아이템55), 예외(아이템70)

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


아이템 72 : 표준 예외를 사용하라

숙련된 프로그래머는 그렇지 못한 프로그래머보다 더 많은 코드를 재사용한다.

에외도 마찬가지로 재사용하는 것이 좋으며, 자바 라이브러리는 대부분 API에서 쓰기에 충분한 수의 예외를 제공한다.

표준 예외를 사용하면 좋은 점

  1. 다른 사람이 익히고 사용하기 쉬워진다.
  2. 많은 프로그래머에게 이미 익숙해진 규약을 그대로 따르기만 하면 된다.
  3. API를 사용한 프로그램도 낯선 예외를 사용하지 않게 되어 읽기 쉽게 된다는 장점도 크다.
  4. 예외 클래스 수가 적을수록 메모리 사용량도 줄고 클래스를 적재하는 시간도 적게 걸린다.

특정한 상황에는 재사용보다 알맞는 예외를 던지자.

가장 많이 재사용되는 예외는 IllegalArgumentException으로 호출자가 인수로 부적절한 값을 넘길 때 던지는 예외이다.

IllegalArgumentException(아이템49)

인수 값이 무엇이었든 어차피 실패했을거라면 IllegalStateException을, 그렇지 않으면 IllegalArgumentException을 던지자.


아이템 73 : 추상화 수준에 맞는 예외를 던지라

상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외를 바꿔 던져야 한다.

  • 이를 예외 번역이라고 한다.
  • 수행하려는 일과 관련 없어 보이는 예외가 튀어나오면 당황스러울 것이다. 메서드가 저수준 예외를 처리하지 않고 바깥으로 전파해버릴 떄 종종 일어나는 일이다.
  • 내부 구현 방식을 드러내어 윗 레벨 API를 오염 시키고 릴리스에서 구현방식을 바꾸면 다른 예외가 튀어나와 기존 클라이언트 프로그램을 깨지게 할 수도 있다.

AbstractSequentialList(아이템20), 고수준 예외를 던지면서 근분 원인도 알려준다(아이템75)

아래 계층 예외를 예방하거나 스스로 처리할 수 없고, 그 예외를 상위 계층에 그대로 노출하기 곤란하다면 예외 번역을 사용하라. 이때 예외 연쇄를 이용하면 상위 계층에는 맥락에 어울리는 고수준 예외를 던지면서 근본 우너인도 함께 알려주어 오류를 분석하기에 좋다.


아이템 74 : 메서드가 던지는 모든 예외를 문서화하라

메서드에서 던지는 예외는 메서드가 올바르게 사용되는 중요한 정보이므로 던지는 예외 하나하나를 문서화 하는데 충분한 시간을 쏟아야 한다.

문서화 해야 하는 경우

  1. 검사 예외는 항상 따로따로 선언하고, 각 예외가 발생하는 상황을 자바독의 @Throws 태그를 사용하여 정확히 문서화하자.
    • 공통 상위 클래스 하나로 뭉뚱그려 선언하는 일은 삼가자.
    • 극단적인 예로 Excetion이나 Throwable을 던진다고 선언해서는 안 되며 유일한 예외 메서드는 main뿐이다.
  2. 비검사 예외도 검사 예외처럼 정성껏 문서화해두면 좋다.
    • 잘 정비된 비검사 예외 문서는 사실상 그 메서드를 성공적으로 수행햐기 위한 전제조건이 된다.
  3. public 메서드라면 필요한 전제조건을 문서화해야 하며, 그 수단으로 가장 좋은 것이 바로 비검사 예외들을 문서화하는 것이다.
    1. 발생 가능한 비검사 예외를 문서로 남기는 일은 인터페이스 메서드에서 특히 중요하다.
  4. 메서드가 던질 수 있는 예외를 각각 @throws 태그로 문서화하되, 비검사 예외는 메서드 선언의 throws 목록에 넣지 말자.
  5. 검사냐 비검사냐에 따라 API 사용자가 해야 할 일이 달라지므로 확실히 구분하는게 좋다.
    1. 자바독 유틸리티는 메서드 선언의 throws 절에 등장하고 메서드 주석의 @throws 태그에도 명시한 예외와 @throws 태그에만 명시한 예외를 시각적으로 구분해준다.

메서드가 던진 가능성이 있는 모든 예외를 문서화하라. 검사 예외든 비검사 예외든, 추상 메서드든 구체 메서드든 모두 마찬가지다. 문서화에는 자바독 @throws 태그를 사용하면 되고 검사 예외만 메서드 선언의 throws문에 일일이 선언하고 비검사 예외 메서드 선언에는 기입하지 말자. 발생 가능한 예외를 문서로 남기지 않으면 다른 사람이 그 클래스나 인터페이스를 효과적으로 사용하기 어렵거나 심지어 불가능할 수도 있다.


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

예외를 잡지 하면 자바 시스템은 예외의 스택 추적 정보를 자동으로 출력하고 예외 객체의 toString메서드를 호출해 얻은 문자열로, 예외의 클래스 이름 뒤에 상세 메시지가 붙은 형태이다.

실패 순간을 포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 한다.

  • 사후 분석을 위해 실패 순간의 상황을 정확히 포착해 예외의 상세 메시지에 담아야 한다.
  • 상세 미시지에 비밀번호나 암호 키 같은 정보까지 담아서는 안 된다.
  • 문서와 소스코드에서 얻을 수 있는 정보는 길게 늘어놔봐야 군더더기가 될 뿐이다.
  • 예외의 상세 메시지와 최종 사용자에게 보여줄 오류 메시지를 혼동해서는 안 된다.
    • 최종 사용자에게는 친절한 안내 메시지를 보여줘야 하는 반면 예외 메시지는 가독성보다는 담긴 내용이 훨씬 중요하다.

예외는 실패와 관련한 정보를 얻을 수 있는 접근자 메서드를 적절히 제공하는 것이 좋다.

  • 포착한 실패 정보는 예외 상황을 복구하는데 유용할 수 있으므로 접근자 메서드는 비검사 예외보다는 검사 예외에서 더 빛을 발한다.
  • 'toString이 반환 값에 포함된 정보를 얻어올 수 있는 API를 제공하자'라는 일반 원칙을 따른다는 관점에서, 비검사 예외도 상세 정보를 알려주는 접근자 메서들르 제공하라고 권하고 싶다.

예외 하나하나를 문서화하는데(아이템56), 일반적으로 프로그래밍 오류(아이템70), public 메서드라면 필요한 전제조건을 문서화해야 하며(아이템56)

예외 상세 메시지에는 실패 관련 정보를 담고 코드에서 얻을 수 있는 정보는 피하자.


아이템 76 : 가능한 한 실패 원자적으로 만들라

일반화해 이야기하면, 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야 한다.

  • 이러한 특성을 실패 원자적이라고 한다.
  • 검사 예외를 던진 경우라면 호출자가 오류 상태를 복구할 수 있을테니 더 유용하다.

메서드를 실패 원자적으로 만드는 방법

  1. 불변 객체로 설계하면 태생적으로 실패 원자적이므로 메서드가 실패하면 새로운 객체가 만들어지지는 않을 수 있으나 기존 객체가 불안정한 상태에 빠지는 일은 결코 없다.

  2. 가변 객체의 메서드를 실패 원자적으로 만드는 방법은 작업 수행 전에 매개변수의 유효성을 검사하는 방법이며 객체의 내부 상태를 변경하기 전에 잠재적 예외의 가능성을 대부분을 걸러낼 수 있다.

     public Object pop() {
         if (size == 0) 
             throw new EmptyStackException();
         Object result = elements[--size];
         elements[size] = null; // 다 쓴 참조 해제
         return result;
     }
  3. 객체의 임시 복사본에서 작업을 수행한다 다음 작업이 성공적으로 완료되면 원래 객체와 교체하는 방법으로 데이터를 임시 자료구조에 저장해 작업하는 게 더 빠를 때 적용하기 좋은 방식이다.

    • 성능을 높이고자 취한 결정이지만, 기존 원본은 변하지 않는 효과를 덤으로 얻게 된다.
  4. 작업 도중 발생하는 실패를 가로채는 복구 코드를 작성하여 작업 전 상태로 되돌리는 방법이다.

    • 주로 (디스크 기반의) 내구성을 보장해야 하는 자료구조에 쓰이는데, 자주 쓰이는 방법은 아니다.

실패 원자성을 보장할 수 없는 경우

  • 두 스레드가 동기화 없이 같은 객체를 동시에 수정한다면 객체의 일관성이 깨질 수 있다.
  • 실패 원자적으로 만들 수 있어도 항상 그렇게 해야 하는건 아닌데 실패 원자성을 달성하기 위핸 비용이나 복잡도가 아주 큰 연산에서 해당한다.
  • 문제가 무엇인지 알고 나면 실패 원자성을 공짜로 얻을 수 있는 경우가 많다.

불변객체(아이템17), 매개변수의 유효성 검사(아이템49), 추상화 수준(아이템73)

메서드 명세에 기술한 예외라면 설혹 예외가 발생하더라도 객체의 상태는 메서드 호출 전과 똑같이 유지돼야 한다는 것이 기본 원칙이다. 이 규칙을 지키지 못한다면 실패 시의 객체 상태를 API 설명에 명시해야 한다.


아이템 77 : 예외를 무시하지 말라

API 설계자가 메서드 선언에 예외를 명시하는 까닭은, 그 메서드를 사용할 때 적절한 조치를 취해달라고 말하는 것으로 예외를 무시하지 말자.

검사와 비검사 예외에 똑같이 적용되며 예측할 수 있는 있는 예외 상황이든 프로그래밍 오류든, 빈 catch 블록으로 못 본척 지나치면 프로그램은 오류를 내재한 채 동작한다.
예외를 적절히 처리하면 오류를 완전히 피할 수도 있다. 무시하지 않고 바깥으로 전파되게만 돠둬도 최소한 디버깅 정보를 남긴 채 프로그램이 신속히 중단되게는 할 수 있다.


동시성

스레드는 여러 활동을 동시에 수행할 수 있게 해주지만 단일 스레드 프로그래밍보다 동시성 프로그래밍이 어렵고 잘못될 수 있는 일이 늘어나고 문제를 재현하기도 어려워진다.

하지만 오늘날 어디서나 쓰이는 멀티코어 프로세서의 힘을 제대로 활용하려면 반드시 내 것으로 만들어야 하는 기술이다.

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

동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다.

synchronized 키워드를 이용하면 매서드나 블록을 한번에 한 스레드씩 수행하도록 보장한다.

  • 객체를 하나의 일관된 상태에서 다른 일관된 상태로 변화시키고 동기화를 제대로 사용하면 어떤 메서드도 해당 객체가 일관되지 않은 순간을 볼 수 없을 것이다.
  • 동기화는 일관성이 깨진 상태를 볼 수 없게 하는 것과 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최정 결과를 보게 해준다.
  • "성능을 높이려면 원자적 데이터를 읽고 쓸 때는 동기화하지 말아야겠다"는 생각은 위험한 발생이다.
  • 자바 언어 명세는 스레드가 필드를 읽을 때 항상 '수정이 완전히 반영된' 값을 얻는다고 보장하지만, 한 스레드가 저장한 값이 다른 스레드에게 '보이는가'는 보장하지 않는다.

Thread.stop은 사용하지 말자

  • 다른 스레드를 멈추는 작업은 권장되지 않으며 자바 11에서 Thread.stop(Throwable obj)는 제거되었고 Thread.stop()은 아직 남아있다.

한 객체가 일관된 상태를 가지고 생성되고(아이템17), 동시성 컬렉션(아이템81)

여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화 해야 한다.
동기화하지 않으면 한 스레드가 수행한 변경을 다른 스레드가 보지 못할 수도 있다.
공유되는 가변 데이터를 동기화하는데 실패하면 응답 불가 상태에 빠지거나 안전 실패로 이어질 수 있다. 이는 디버깅 난이도가 가장 높은 문제에 속한다.
간헐적이거나 특정 타이밍에만 발생할 수도 있고, VM에 따라 현상이 달라지기도 한다. 배타적 실행은 필요 없고 스레드끼리의 통신만 필요하다면 volatile 한정자만으로 동기화할 수 있다. 다만 올바로 사용하기가 까다롭다.


아이템 79 : 과도한 동기화는 피하라

과도한 동기화는 성능을 떨어뜨리고, 교착상태에 빠뜨리고, 심지어 예측할 수도 없는 동작을 낳기도 한다.

응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안 된다.

  • 동기화 된 영역 안에서 재정의할 수 있는 메서드는 호출하면 안 되며, 클러이언트가 넘겨준 함수 객체를 호출해서도 안 된다.
  • 동기화된 영역을 포함한 클래스 고나점에서는 이런 메서드는 모두 바깥 세성에서 온 외계인이다.
  • 외계인 메서드가 하는 일에 따라 동기화된 영역은 예외를 일으키거나, 교착 상태에 빠지거나, 데이터를 훼손할 수도 있다.

함수 객체(아이템24), Biconsumer를 그대로 사용했더라도 별 무리는 없었을 것이다.(아이템44), 람다는 자기 자신을 참조할 수단이 없다.(아이템42), 실행자 서비스(ExecutorService, 아이템80), 스레드 안전한 클래스(아이템82), java.util,concurent(아이템81)

교착상태와 데이터 훼손을 피하려면 동기화 영역 안에서 외계인 메서드를 절대 호출하지 말자. 일반화해 이야기하면 동기화 영역 안에서의 작업은 최소한으로 줄이자. 가변 클래스를 설계할 때는 스스로 동기화해야 할지 고민하자. 멀티코어 세상인 지금은 과도한 동기화를 피하는 게 과거 어느 때보다 중요하다. 합당한 이유가 있을 때만 내부에서 동기화하고, 동기화했는지 여부를 문서에 명확히 밝히자.


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

java.util.concurrent 패키지가 등장했고 실행자 프레임워크라고 하는 인터페이스 기반의 유연한 태스크 실행 기능을 담고 있다.

ExecutorService exec = Executors.newSingleThreadExecutor(); // 작업 큐를 생성하는 방법
exec.execute(runnable); // 실행자에 실행할 테스트를 넘기는 방법
exec.shutdown(); // 우하하게 종료시키는 방법
  • 특정 태스크가 완료되기를 기다린다(get 메서드)
  • 태스크 모음 중 아무것 하나(invokeAny 메서드) 혹은 모든 태스크(invokeAll 메서드)가 완료되기를 기다린다.
  • 실행자 서비스가 종료하기를 기다린다(awaitTermination 메서드)
  • 완료된 태스크들의 결과를 차례로 받는다(ExecutorCompletionService 이용)
  • 태스크를 특정 시간에 혹은 주기적으로 실행하게 한다.(ScheduledThreadPoolExecutor 이용)

용도와 특징

  • 작은 프로그램이나 가벼운 서버에서는 Executors.newCashedThreadPool이 일반적으로 좋은 선택이다.
    • 무거운 서버에서는 좋지 못하는데 요청받은 태스크들을 큐에 쌓지 않고 즉시 스레드에 위임돼 실행 된다.
    • 가용한 스레드가 없다면 새로 생성하여 CPU 이용률이 100%에 치닫는 상황이 생길 수 있다.
  • 무거운 서버에서는 스레드 개수를 고정한 Executors.newFixedThreadPool을 선택하거나 와전히 통제할 수 있는 ThreadPoolExecutor를 직접 사용하는 편이 훨씬 낫다.
  • 작업 큐를 손수 만드는 일은 삼가야 하고, 스레드를 직접 다루는 것도 일반적으로 삼가야 한다.
    • 스레드를 직접 다루면 Thread가 작업 단위와 수행 메커니즘 역할을 모두 수행하게 되는데 실행자 프레임워크에서는 작업 단위와 실행 메커니즘이 분리된다.
  • 작업 단위를 나타내는 핵심 추상 개념이 태스크이고 두 가지가 있다.
    • Runnable과 Callable(Runnable과 비슷하지만 값을 반환하고 임의의 예외를 던질 수 있다)이다.
  • 태스크를 수행하는 일반적인 메커니즘이 바로 실행자 서비스로 태스크 수행을 실행자 서비스에 맡기면 우너하는 태스크 수행 정책을 선택할 수 있고, 생각이 바뀌면 언제든 변경할 수 있다. 핵심은 (컬렉션 프레임워크가 데이터 모음을 담당하듯) 실행자 프레임워크가 작업 수행을 담당해준다는 것이다.
  • 자바 7이 되면서 실행자 프레임워크는 포크-조인 태스크를 지원하도록 확장되었다. 포크-조인 태스크는 포크-조인 풀이라는 특별한 실행자 서비스가 실행해준다.
    • 포크-조인 태스크, 즉 ForkJoinTask의 인스턴스는 작은 하위 태스크로 나뉠 수 있고, ForkJoinPool을 구성하는 스레드들이 이 태스크들을 처리하며, 일을 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와 대신 처리할 수도 있다.
    • 이렇게 하여 모든 스레드가 바쁘게 움직여 CPU를 최대한 활용하면서 높은 처리량과 낮은 지연시간을 달성한다.
    • 이러한 포크-종니 태스크를 직접 작성하고 튜닝하기런 어려운 일이지만, 포크-조인 풀을 이용해 만든 병렬 스트림을 이용하면 적은 노력으로 그 이점을 얻을 수 있다.

병렬 스트림(아이템48)

작은 프로그램이나 가벼운 서버에서는 Executors.newCashedThreadPool이 일반적으로 좋은 선택이다. 무거운 서버에서는 좋지 못하는데 요청받은 태스크들을 큐에 쌓지 않고 즉시 스레드에 위임돼 실행 된다. 가용한 스레드가 없다면 새로 생성하여 CPU 이용률이 100%에 치닫는 상황이 생길 수 있다.
무거운 서버에서는 스레드 개수를 고정한 Executors.newFixedThreadPool을 선택하거나 와전히 통제할 수 있는 ThreadPoolExecutor를 직접 사용하는 편이 훨씬 낫다.


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

내부 동시성(아이템79), 디폴트메서드(아이템21), 실행자 서비스(아이템80)

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


아이템 82 : 스레드 안전성 수준을 문서화하라

한 메서드를 여러 스레드가 동시에 호출할 때 그 메서드가 어떻게 동작하느냐는 해당 클래스와 이를 사용하는 클라이언트 사이의 중요한 계약과 같다.

API문서에서 아무런 언급도 없으면 그 클래스 사용자는 나름의 가정을 해야만 하고 그 가정이 틀리면 클라이언트 프로그램은 동기화를 충분히 하지 못하거나 지나치게 한 상태일 것이며 두 경우 모두 심각한 오류로 이어질 수 있다.

동기화(아이템78), 지나친(아이템79), BigInteger(아이템7), 상속용으로 설계한 클래스(아이템19)

모든 클래스가 자신의 스레드 안전성 정보를 명확히 문서화해야 한다.
정확한 언어로 명확히 설명하거나 스레드 안전성 애너테이션을 사용할 수 있다. synchronized 한정자는 문서화와 아무런 관련이 없다. 조건부 스레드 안전 클래스는 메서드를 어떤 순서로 호출할 때 외부 동기화가 요구되고, 그 때 어떤 락을 얻어야 하는지도 알려줘야 한다. 무조건적 스레드 안전 클래스를 작성할 때는 synchronized 메서드가 아닌 비공개 락 객체를 사용하자. 이렇게 해야 클라이언트나 하위 클래스에서 동기화 메커니즘을 깨드리는 걸 예방할 수 있고, 필요하다면 다음에 더 정교한 동시성을 제어 메커니즘으로 재구현할 여지가 생긴다.


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

지연 초기화는 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법이다. 값이 전혀 쓰이지 않으면 초기화도 결코 일어나지 않기에 주로 최적화용도로 쓰이지만, 클래스와 인스턴스 초기화 때 발생하는 위험한 순환 문제를 해결하는 효과도 있다.

다른 모든 최적화와 마찬가지로 지연 초기화도 최선의 조언은 "필요할 때까지는 하지말라"이다.

  • 클래스 혹은 인스턴스 생성 시 초기화 비용은 줄지만 지연 초기화는 필드에 접근하는 비용은 커진다.
  • 지연 초기화하려는 필드 중 초기화가 이뤄지는 비율에 따라, 실제 초기화에 드는 비용에 따라, 초기화된 각 필드를 얼마나 빈번히 호출하느냐에 따라 지연 초기화가 성능이 느려지게 할 수도 있다.

"필요할 때까지는 하지말라"(아이템67), 심각한 버그(아이템78), final(아이템17), 동기화 비용(아이템79), volatile(아이템78)

대부분의 필드는 지연시키지 말고 곧바로 초기화해야 한다. 성능 때문에 혹은 위험한 초기화 순환을 막기 위해 꼭 지연 초기화를 써야 한다면 올바른 지연 초기화 기법을 사용하자. 인스턴스 필드에는 이중 검사 관용구를, 정적 필드에는 지연 초기화 홀더 클래스 관용구를 사용하자. 반복해 초기화해도 괜찮은 인스턴스 필드에는 단일검사 관용구도 고려 대상이다.


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

정확성이나 성능이 스레드 스케줄러에 따라 달라지는 프로그램이라면 다른 플랫폼에 이식하기 어렵다.

  • 정상적인 운영체제라면 이 작업을 공정하게 수행하지만 구체적인 스케줄링 정책은 운영체제마다 다를 수 있기 때문에 잘 작성된 프로그램이라면 이 정책에 좌지우지 돼서는 안된다.
  • 견고하고 빠릿하고 이식성 좋은 프로그램을 작성하는 가장 좋은 방법은 실행 가능한 스레드의 평균적인 수를 프로세서 수보다 지나치게 많아지지 않도록 하는 것이다.

실행자 프레임워크(아이템80)

프로그램의 동작을 스레드 스케줄러에 기대지 말자. 견고성과 이식성을 모두 해치는 행위다. 같은 이유로, Thread.yield와 스레드 우선순위에 의존해서도 안 된다. 이 기능들은 스레드 스케줄러에 제공하는 힌트일 뿐이다. 스레드 우선순위는 이미 잘 동작하는 프로그램의 서비스 품질을 높이기 위해 드물게 쓰일 수는 있지만, 간신히 동작하는 프로그램을 '고치는 용도'로 사용해서는 절대 안된다.


직렬화

객체 직렬화란 자바가 객체를 바이트 스트림으로 인코딩하고(직렬화) 그 바이트 스트림으로부터 다시 객체를 재구성하는(역직렬화) 메커니즘이다.

아이템 85 : 자바 직렬화의 대안을 찾으라

자바 직렬화는 취약점들로 인해 공격을 받는데 직렬화의 근본적인 문제는 공격 범위가 넓고 지속적으로 넓어져 방어하기가 어렵다.

ObjectStream의 readObject 메서드를 호출하면서 객체 그래프가 역질렬화되기 때문이다.

공격자와 보안 전문가들은 자바 라이브러리와 널리 쓰이는 서드파티 라이브러리에서 직렬화 기능 타입들을 연구하여 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드들으 찾아보았고 이런 메서드를 가젯 메서드라고 부른다.

역직렬화에 시간이 오래 걸리는 짦은 스트림을 역질렬화하는 것만으로도 서비스 거부 공격에 쉽게 노출될 수 있고 이런 스트림을 역질렬화 폭탄이라고 한다.

직렬화 가능 클래스를 올바르고 안전하고 효율적으로 작성하려면 상당한 주의가 필요하다. 다음 아이템부터는 이런분들을 위한 조언을 담았다.

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


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

어떤 클래스의 인스턴스를 직렬화할 수 있게 하려면 클래스 선언에 implements Serializable만 덧 붙이면 된다. 너무 쉽게 적용할 수 있기 때문에 프로그래머가 특별히 신경쓸 게 없다는 오해가 생길 수 있지만, 진실은 훨씬 더 복잡하다.

직렬화를 지원하기란 짧게 보면 손쉬워 보이지만, 길게 보면 아주 값비싼 일이다.

정보은닉(아이템15), 고품질의 직렬화(아이템87,90), Serializable 구현의 두 번째 문제는 버그와 보안 구멍이 생길 위험이 높아진다는 점이다.(아이템 85), 역직렬화를 사용하면 불변식 깨짐과 허가되지 않는 접근에 쉽게 노출된다는 뜻이다.(아이템88), 테스트 부담을 줄일 수 있다.(아이템 87, 90), 상속(아이템19), finalizer(아이템8), 직렬화 프록시 패턴(아이템90), 내부 클래스(아이템 24)

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


아이템 87 : 커스텀 직렬화 형태를 고려해보라

개발 일정에 쫓기는 상황에서는 이번 릴리즈에서는 그냥 동작 하도록만 만들고 다음 릴리즈에서 제대로 구현하기로 하는게 낫다. 하지만 Serializable을 구현하고 기본 직렬화 형태글 사용한다면 다음 릴리즈 때 버리려 한 현재의 구현을 영원히 발이 묶이게 된다.

-   직렬 버전 UID가 꼭 고유할 필요는 없다.
-   직렬 버전 UID가 없는 기존 클래스를 구버전으로 직렬화된 인스턴스와 호환성을 유지한 채 수정하고 싶다면, 구버전에서 사용한 자동 생성된 값을 그대로 사용해야 한다.
-   기존 버전과 호환성을 끊고 싶다면 단순히 직렬 버전 UID의 값을 바꿔주면 된다.
-   구버전으로 직렬화된 인스턴스들과의 호환성을 끊으려는 경우를 제외하고는 직렬 버전 UID를 절대 수정하지 말자

복원(아이템88), 초기화(아이템83), 스레드 안전(아이템82), 잠재적호환성(아이템86)

클래스를 직렬화하기로 했다면 어떤 직렬화 형태를 사용할지 심사숙고하기바란다.
자바의 기본 직렬화 형태는 직렬화한 결과가 해당 객체의 논리적 표현에 부합할 때만 사용하고, 그렇지 않으면 객체를 적절히 설명하는 커스텀 직렬화 형태를 고안하라. 직렬화 형태도 공개 메서드를 설계할 때에 준하는 시간을 들여 설계 해야 한다. 한번 공개된 메서드는 향후 릴리스에서 제거할 수 없듯이, 직렬화 형태에 포함된 필드도 마음대로 제거할 수 없다. 직렬화 호환성을 유지하기 위해 영원히 지원해야 하는것이다. 잘못된 직렬화 형태를 선택하면 해당 클래스의 복잡성과 성능에 영구히 부정적인 영향을 남긴다.


아이템 88 : readObject 메서드는 방어적으로 작성하라

  • 클래스 선언에 implements Serializable을 추가하는 것으로 모든 일을 끝낼 수 있을 것 같지만 불변식을 더 이상 보장 할 수 없다.
  • readObject 메서드가 실직적으로 또 다른 public 생성자이기 때문에 다른 생성자와 똑같은 수준으로 주의를 기울여야 한다.
  • readObject는 매개변수로 바이트 스트림을 받는 생성자라 할 수 있다.
    • 보통의 경우 바이트 스트림은 정상적으로 생성된 인스턴스를 직렬화해 만들어지지만 불변식을 깨뜨릴 의도로 임의 생성한 바이트 스트림을 건네면 문제가 생긴다. 정상적인 생성자로는 만들어 낼 수 없는 객체를 생성해 낼 수 있기 때문이다.

기본 직렬화(아이템87), 유효한지 검사하고(아이템49, 방어적 복사(아이템50)

readObecjt 메서드를 작성할 때는 언제나 public생성자를 작성하는 자세로 임해야 한다. readObject는 어떤 바이트 스트림이 넘어 오더라도 유효한 인스턴스를 만들어내야 한다. 바이트 스트림이 진짜 직렬화된 인스턴스라고 가정해서는 안 된다. 이번 아이템에서는 기본 직렬화 형태를 사용한 클래스를 예로 들었지만 직렬화를 사용하더라도 모든 문제가 그대로 발생할 수 있다. 이어서 안전한 readObject 메서드를 작성하는 지짐을 요약해보았다.

  • private이어야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적으로 복사하라. 불변 클래스 내의 가변 요소가 여기 속한다.
  • 모든 불변식을 검사하여 어긋나는 게 발견되면 InvalidObjectException을 던진다. 방어적 복사 다음에는 반드시 불변식 검사가 뒤따다라야 한다.
  • 역직렬화 후 객체 그래프 전체의 유효성을 검사해야 한다면 ObjectInputValidation인터페이스를 사용하라
  • 직접적이든 간접적이든, 재정의할 수 있는 메서드는 호출하지 말자.

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

  • 싱글턴 패턴도 implements Serializable을 추가하는 순간 더 이상 싱글턴이 아니게 된다.
  • 명시적으로 readObject를 제공하더라도 소용 없다.
  • readResolve 기능을 이용하면 readObject가 만들어낸 인스턴스를 다른 것으로 대체할 수 있다.
    • 역직렬화한 객체의 클래스가 readReseolve메서드를 적절히 정의해뒀다면, 역직렬화한 후 새로 생성된 객체를 인수로 이 메서드가 호출되고, 이 메서드가 반환한 객체 참조가 새로 생성된 객체를 대신해 반환한다. 대부분의 경우 이때 새로 생성된 객체의 참조는 유지 하지 않으므로 바로 가비지 컬렉션 대상이 된다.
  • 사실, readResolve를 인스턴스 통제 목적으로 사용한다면 객체 참조 타입 인스턴스 필드는 모두 transient로 선언해야 한다.
    • 싱글턴이 transient가 아닌 참조 필드를 가지고 있다면, 그 필드의 내용은 readResolve메서드가 실행되기 전에 역직렬화된다. 그렇다면 잘 조작된 스트림을 써서 해당 참조 필드의 내용이 역직렬화되는 시점에 그 역직렬화된 인스턴스의 참조를 훔쳐올 수 있다.
    • 더 자세히 알아보면 readResolve 메서드와 인스턴스 필드 하나를 포함한 '도둑' 클래스를 작성하고 이 인스턴스 필드는 도둑이 '숨길' 직렬화된 싱글턴을 참조하는 역할이 된다.
      • 직렬화된 스트림에서 싱글턴의 비휘발성 필드를 이 도둑의 인스턴스로 교체한다. 이제 싱글턴은 도둑을 참조하고 도둑은 싱글턴을 참조하는 순환고리가 만들어졌다.
      • 싱글턴이 도둑을 포함하므로 싱글턴이 역직렬화될 때 도둑의 readResolve메서드가 먼저 호출된다. 그 결과, 도둑의 readResolve 메서드가 수행될 때 도둑의 인스턴스 필드에는 역직렬화 도중인(그리고 readResolve가 수행되기 전인) 싱글턴의 참조가 담겨 있게 된다.
      • 도둑의 readResolve 메서드는 이 인스턴스 필드가 참조한 값을 정적 필드로 복사하여 readResolve가 끝난 후에도 참조할 수 있도록 하고 이 메서드는 도둑이 숨긴 transient가 아닌 필드의 원래 타입에 맞는 값을 반환한다.
  • readResolve를 사용하는 방식이 완전히 쓸모없는 것은 아니다.
    • 직렬화 가능 인스턴스 통제 클래스를 작성해야 하는데, 컴파일타임에는 어떤 인스턴스들이 있는지 알 수 없는 상황이라면 열거 타입으로 표현하는 것이 불가능하기 때문이다.
  • readResolve 메서드의 접근성은 매우 중요하다.
    • final 클래스에서라면 readResolve메서드는 private이어야 한다

기본 직렬화를 쓰지 않더라도(아이템87), 명시적인 readObject를 제공하더라도(아이템88), 열거타입(아이템 3)

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


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

  • 계속 이야기 했듯이 Serializable을 구현하기로 결정한 순간 언어의 정상 메커니즘인 생성자 이외의 방법으로 인스턴스를 생성할 수 있게 된다. 버그와 보안 문제가 일어날 가능성이 커진다는 의미이다.
  • 이 위험을 줄여줄 기법으로 직렬화 프록시 패턴이다.

직렬화 프록시 패턴 사용 방법

  1. 바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계해 private static으로 선언하고 이 중첩 클래스가 바로 바깥 클래스의 직렬화 프록시이다.

  2. 중첩 클래스의 생성자는 단 하나여야 하며, 바깥 클래스를 매개변수로 받아야 한다. 이 생성자는 단순히 인수로 넘어온 인스턴스의 데이터를 복사한다.

  3. 일관성 검사나 방어적 복사도 필요 없다.

  4. 설계상, 직렬화 프록시의 기본 직렬화 형태는 바깥 클래스의 직려화 형태로 쓰기에 이상적이다.

  5. 바깥 클래스와 직렬화 프록시 모두 Serializable을 구현한다고 선언해야 한다.

  6. 바깥 클래스와 논리적으로 동일한 인스턴스를 반환하는 readResolve 메서드를 SerializationProxy 클래스에 추가하고 역직렬화 시에 직렬화 시스템이 직렬화 프록시를 다시 바깥 클래스의 인스턴스로 변환하게 해준다.

  7. readResolve 메서드는 공개된 API만을 사용해 바깥 클래스의 인스턴스를 생성하는데, 이 패턴이 아름다운 이유이다.

  8. 직렬화는 생성자를 이용하지 않고도 인스턴스를 생성하는 기능을 제공하는데, 이 패턴은 직렬화의 이런 언어도단적 특성을 상당 부분 제거한다. 즉, 일반 인스턴스를 만들 때와 똑같은 생성자, 정적 팩터리, 혹은 다른 메서드를 사용해 역직렬화된 인스턴스를 생성하는 것이다.

  9. 역직렬화된 인스턴스가 해당 클래스의 불변식을 만족하는지 검사할 또 다른 수단을 강구하지 않아도 된다.

  10. 그 클래스의 정적 팩터리나 생성자가 불변식을 확인해주고 인스턴스 메서드들이 불변식을 잘 지켜준다면, 따로 더 해줘야 할 일은 없을 것이다.

방어적 복사처럼, 직렬화 프록시 패턴은 가짜 바이트 스트림 공격과 내부 필드 탈취 공격을 프록시 수준에서 차단해준다.

직렬화 프록시 패턴에는 두 가지 한계가 있다.

  1. 클라이언트가 멋대로 확장할 수 있는 클래스에는 적용할 수 없다.
  2. 객체 그래프에 순환이 있는 클래스에도 적용할 수 없다.]

불변(아이템17), EnumSet(아이템36), 확장(아이템19)

제 3자가 확장할 수 없는 클래스라면 가능한 한 직렬화 프록시 패턴을 사용하자. 이 패턴이 아마도 중요한 불변식을 안정적으로 직렬화해주는 가장 쉬운 방법일 것이다.


12장을 공부하면서 더 공부해야 하는 부분

  • SWAT 도구
반응형

'나(다) > ' 카테고리의 다른 글

[책] 달러구트 꿈 백화점  (0) 2021.04.17
[오디오북] 신경 끄기의 기술  (0) 2021.03.25
이펙티브 자바 - 12장 : 직렬화  (0) 2021.02.11
완벽한 공부하는 법  (2) 2021.02.07
이펙티브 자바 - 11장 : 동시성  (0) 2021.02.06