Skip to content

foreverfl/study-effective-java

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Effective Java

  • Effective Java를 보고 정리한 자료입니다.
  • '핵심 정리'는 각 아이템에 있는 경우만 적어두었습니다.
  • 각 아이템에 있는 '예제 코드'는 Claude3 Opus 및 GPT 4o를 이용해서 만들었습니다.

목차

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

정적 팩터리 메서드가 생성자보다 좋은 점

  • 이름을 가질 수 있음.
  • 호출될 때마다 인스턴스를 새로 생성하지 않아도 됨.
  • 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있음.
  • 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있음
  • 정적 팰터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 됨.

정적 팩터리 메서드가 생성자보다 나쁜 점

  • 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없음.
  • 정적 팩터리 메서드는 프로그래머가 찾기 어려움.

정적 팩터리 메서드에 흔히 사용하는 명명 방식들

  • from: 하나의 매개변수를 받아 해당 타입의 인스턴스를 생성하는 메서드. 예를 들어, Date.from(instant)는 Instant를 받아 Date 객체를 생성함.
  • of: 여러 매개변수를 받아 적절한 타입의 인스턴스를 생성하는 메서드. 예를 들어, Set.of(elements)는 여러 요소를 받아 Set을 생성함.
  • valueOf: from과 비슷하지만 더 명시적인 의미를 가지고, 주어진 매개변수를 사용하여 인스턴스를 생성함. 예를 들어, Integer.valueOf(String)는 문자열을 받아 Integer 객체를 생성함.
  • instance / getInstance: 인스턴스를 반환하는 메서드. 보통 싱글톤 패턴에서 사용됨. 예를 들어, Calendar.getInstance()Calendar의 인스턴스를 반환함.
  • create / newInstance: 새로운 인스턴스를 생성하는 메서드. 예를 들어, MyClass.create()MyClass의 새로운 인스턴스를 생성함.
  • getType: 특정 타입의 인스턴스를 반환하는 메서드. getInstance와 같으나, 새성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 씀. 예를 들어, FileStore.getFileStore(path)FileStore 인스턴스를 반환함.
  • newType: 새로운 타입의 인스턴스를 생성하는 메서드. 예를 들어, BufferedReader.newBufferedReader(reader)BufferedReader의 새로운 인스턴스를 생성함.
  • type: 인스턴스를 반환하는 일반적인 메서드. 예를 들어, TypeFactory.type()은 특정 타입의 인스턴스를 반환함.

핵심 정리

  • 정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋음. 그렇다고 하더라도 정적 팩터리를 사용하는게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있으면 고칠 것.

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

점층적 생성자 패턴의 단점

  • 점층적 생성자 패턴도 쓸 수는 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어려움.

자바빈즈 패턴

  • 자바빈즈(JavaBeans)는 재사용 가능한 소프트웨어 컴포넌트를 만들기 위한 표준으로, 주로 GUI 애플리케이션에서 사용됩니다. 자바빈즈 패턴은 다음과 같은 특징을 가짐
    • 모든 필드는 private으로 선언.
    • 각 필드에 대한 public gettersetter 메서드를 제공.
    • 기본 생성자(매개변수가 없는 생성자)를 제공.

자바빈즈 패턴의 단점

  • 자바빈즈 패턴에서는 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 됨.
  • 자바빈즈 패턴에서는 클래스를 분변으로 만들 수 없음.

빌더패턴의 특징

  • 빌더 패턴은 파이썬과 스칼라에 있는 명명된 선택적 매개 변수(named optional parameters)를 흉내낸 것.
class Car:
    def __init__(self, make, model, year=2020, color='black'):
        self.make = make
        self.model = model
        self.year = year
        self.color = color

    def __str__(self):
        return f'{self.year} {self.color} {self.make} {self.model}'

# 기본값을 사용하여 Car 객체 생성
car1 = Car('Toyota', 'Camry')
print(car1)  # 출력: 2020 black Toyota Camry

# 선택적 매개변수를 사용하여 Car 객체 생성
car2 = Car('Honda', 'Civic', year=2019, color='blue')
print(car2)  # 출력: 2019 blue Honda Civic
  • 빌더 패턴은 게층적으로 설계된 클래스와 함께 쓰기에 좋음.

핵심 정리

  • 생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 것이 나음. 매개 변수 중 다수가 필수가 아니거나 같은 타입이면 더 그러함. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바 빈즈보다 안전함.

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

예제 코드

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

예제 코드

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

핵심 정리

  • 클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋음. 이 자원들을 클래스가 직접 만들게 해서도 안됨. 대신 필요한 자원을 (혹은 그 자원을 만들어주는 팩터리를) 생성자에 (혹은 정적 팩터리나 빌더에) 넘겨줄 것. 의존 객체 주입이라 하는 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 기막히게 개선해줌.

예제 코드

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

예제 코드

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

핵심 정리

  • 메모리 누수는 겉으로 잘 드러나지 않아 시스템에 수년간 잠복하는 사례도 있음. 이런 누수는 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야만 발견되기도 함. 그래서 이런 종류으 ㅣ문제는 예방법을 익혀두는 것이 매우 중요함.

예제 코드

아이템 8. finalizercleaner 사용을 피하라

내용 정리

  • finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요함.
  • cleanerfinalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요함.
  • finalizercleaner로는 제때 실행되어야 하는 작업은 절대 할 수 없음.
  • 상태를 영구적으로 수정하는 작업에서는 절대 finalizercleaner에 의존해서는 안됨.
  • finalizercleaner는 심각한 성능 문제도 동반함.
  • finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있음.
  • final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무일도 하지 않는 finalizer를 만들고 final로 선언할 것.
  • finalizercleaner를 대신해서 AutoClosable을 쓸 것.

finalizer

  • Java 9 이전에는 객체가 가비지 컬렉션 대상이 될 때 정리 작업을 수행하는 방법으로 finalize 메서드를 사용했음.
  • finalize 메서드는 객체가 더 이상 참조되지 않을 때 가비지 컬렉터에 의해 호출되지만, 호출 시점이 명확하지 않으며 성능에 큰 영향을 미칠 수 있음.

Cleaner

  • Java 9에서 도입된 Cleanerfinalize 메서드를 대체하는 더 안전하고 성능이 좋은 방법.
  • Cleaner는 백그라운드 스레드를 사용하여 등록된 객체가 가비지 컬렉션 대상이 될 때 정리 작업을 수행함.
  • Cleaner는 명시적으로 객체를 정리할 수 있는 방법을 제공하여, 정리 작업의 타이밍을 좀 더 제어할 수 있음.

finalizer와 Cleaner 사용을 피해야 하는 이유

  • Unpredictable Timing: finalize 메서드는 가비지 컬렉터에 의해 호출되므로, 호출 시점이 예측할 수 없음. 이는 리소스 해제 작업이 지연되어 리소스 누수 문제를 일으킬 수 있음.
  • Performance Overhead: finalize 메서드는 성능에 큰 영향을 줄 수 있음. 가비지 컬렉션 과정에서 추가적인 오버헤드가 발생하기 때문임.
  • Unreliability: finalize 메서드가 실행되지 않을 수도 있음. 가비지 컬렉터가 객체를 수집하기 전에 프로그램이 종료될 수 있기 때문임.
  • Security Risks: finalize 메서드를 잘못 사용하면 보안 문제가 발생할 수 있음. 특히, finalize 메서드를 오버라이드하여 객체를 재생성하면 예기치 않은 동작을 유발할 수 있음.

핵심 정리

  • cleaner(자바 8까지는 finalizer)는 안전망 역할이나 중요하지 않은 네이티브 자원 회수로용으로만 사용할 것. 물론 이런 경우라도 불확실성과 성능 저하게 주의해야 함.

예제 코드

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

try-finally

  • try 블록에서 리소스를 사용하고, finally 블록에서 리소스를 명시적으로 해제해야 함.
  • 코드가 길어지고 가독성이 떨어질 수 있음.
  • 리소스 해제 로직을 개발자가 직접 작성해야 함.
  • 예외 발생 시 리소스 해제 로직이 실행되지 않을 수 있음.

try-with-resources

  • Java 7부터 도입된 기능으로, 리소스를 자동으로 해제해주는 구문.
  • try 블록에서 사용할 리소스를 소괄호 안에 선언함.
  • 리소스 객체는 AutoCloseable 인터페이스를 구현해야 함.
  • try 블록이 종료되면 자동으로 리소스의 close() 메서드가 호출되어 리소스가 해제됨.
  • 코드가 간결해지고 가독성이 좋아짐.
  • 예외 발생 여부와 관계없이 항상 리소스가 해제됨.

핵심 정리

  • 꼭 회수해야 하는 자원을 다룰 때는 try-finally가 아닌 try-with-resources를 사용할 것. 예외는 없음. 코드는 더 짧고 분명해지고, 만들어지는 예외 정보도 훨씬 유용함. try~finally로 작성하면 실용적히지 못할 만큼 코드가 지저분해지는 경우라도, try-with-resources로는 정확하고 쉽게 자원을 회수할 수 있음.

예제 코드

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

다음 상황에 해당한다면 equals를 재정의하지 말 것.

  • 각 인스턴스가 본질걱으로 고유함. 예를 들어, 대부분의 Thread 클래스 인스턴스는 본질적으로 고유함. 두 개의 Thread 인스턴스가 논리적으로 같다고 비교하는 것은 의미가 없음. 각 스레드는 고유한 실행 흐름을 나타내기 때문임.
Thread thread1 = new Thread();
Thread thread2 = new Thread();
System.out.println(thread1.equals(thread2)); // false, 본질적으로 고유
  • 인스턴스의 '논리적 동치성(logical equality)'를 검사할 일이 없음. 예를 들어, 유틸리티 클래스(특정 기능이나 작업을 수행하는 정적 메서드들을 모아놓은 클래스)나 상수 클래스의 경우 논리적 동치성을 검사할 일이 없음.
public class UtilityClass {
    // 이 클래스는 논리적 동치성을 검사할 필요가 없음
    private UtilityClass() {}
    public static void utilityMethod() {
        // ...
    }
}
  • 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어 맞음. 예를 들어, AbstractList 클래스는 List 인터페이스를 구현하는 모든 하위 클래스에 대해 적절한 equals 메서드를 제공함. 따라서 ArrayListLinkedList에서 equals를 재정의할 필요가 없음.
List<Integer> list1 = new ArrayList<>(Arrays.asList(1, 2, 3));
List<Integer> list2 = new LinkedList<>(Arrays.asList(1, 2, 3));
System.out.println(list1.equals(list2)); // true, 상위 클래스의 equals가 적절히 동작
  • 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없음. 예를 들어, 내부적으로만 사용되는 private 클래스는 외부에서 접근할 수 없으므로 equals 메서드를 호출할 일이 없음.
class PackagePrivateClass {
    private int value;

    PackagePrivateClass(int value) {
        this.value = value;
    }

    // equals를 재정의할 필요 없음
}

public class Main {
    public static void main(String[] args) {
        PackagePrivateClass obj1 = new PackagePrivateClass(1);
        PackagePrivateClass obj2 = new PackagePrivateClass(1);

        // obj1.equals(obj2) 호출할 일이 없음
    }
}

equals 메서드를 재정의할 때 따라야하는 일반 규약

  • 반사성(reflexivity): null이 아닌 모든 참조 값 x에 대해, x.equals(x)true임.
  • 대칭성(symmetry): null이 아닌 모든 참조 값 xy에 대해, x.equals(y)true이면 y.equals(x)true임.
    • equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없음.
  • 추이성(transitivity): null이 아닌 모든 참조 값 x, y, z에 대해, x.equals(y)true이고 y.equals(z)true이면 x.equals(z)true임.
    • 구체 클래스를 확장해 새로운 ㄱ밧을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않음.
  • 일관성(consistency): null이 아닌 모든 참조 값 xy에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환함.
    • equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안 됨.
  • null 아님(non-nullity): null이 아닌 모든 참조 값 x에 대해, x.equals(null)false임.

equals 메서드 구현 방법

  1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인할 것.
  2. instanceof 연산자로 입력이 올바른 타입인지 확인할 것.
  3. 입력을 올바른 타입으로 형변환할 것.
  4. 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사할 것.

equals 메서드 구현 시 주의사항

  • equals를 재정의할 땐, hashCode도 반드시 재정의 할 것.
  • 너무 복잡하게 해결하려 하지 말 것.
  • Object 외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말 것.

핵심 정리

  • 꼭 필요한 경우가 아니면 equals를 재정의하지 말 것. 많은 경우에 Objectequals가 여러분이 원하는 비교를 정확히 수행함. 재정의해야 할 때는 그 클래스의 핵심 필드를 모두 빠짐없이, 다섯가지 규약을 확실히 지켜가며 비교해야 함.

예제 코드

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

hashCode

  • hashCode 메서드는 객체의 해시 코드를 반환하는 메서드로, 해시 기반 컬렉션(HashMap, HashSet 등)에서 객체를 빠르게 검색하거나 저장하는 데 사용됨. 자바의 기본 hashCode 메서드는 객체의 메모리 주소를 기반으로 해시 코드를 생성함.

Object 명세에서 발췌한 hashCode 관련 규약

  • 일관성: equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 함. 단, 애플리케이션을 다시 실행한다면 이 값이 달라져도 상관없음.
  • equals와의 관계: equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 함.
  • 해시 충돌 최소화: equals(Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode는 서로 다른 값을 반환할 필요는 없음. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아짐.
  • 서로 다른 두 객체가 같은 해시 코드를 가질 수 있음. 이는 해시 충돌이며, 해시 기반 자료구조는 이를 처리할 수 있는 방법을 가지고 있음. 예를 들어, HashMap은 같은 해시 코드를 가진 여러 객체를 처리할 수 있음. 해시 충돌을 처리하는 방법은 다양함. 대표적으로 두 가지가 있음.
    • 체이닝(Chaining): 동일한 해시 코드를 가진 객체들을 연결 리스트로 저장함. 해시 테이블의 각 버킷은 연결 리스트를 참조함.
    • 개방 주소법(Open Addressing): 충돌이 발생하면 다른 버킷을 찾아 객체를 저장함.

hashCode 사용시 주의할 점.

  • equals을 재정의한 클래스 모두에서 hashCode도 재정의해야함.
  • hashCode 재정의를 잘못했을 때 크게 문제가 되는 조항은, 논리적으로 같은 객체는 같은 해시 코드를 반환해야 한다는 것.

좋은 hashCode 작성 요령

  • 일관된 equals 구현: 두 객체가 equals 메서드에 의해 같다고 판단된다면, 두 객체는 반드시 같은 hashCode를 반환해야 함.
  • 다양한 필드 사용: 가능한 많은 중요한 필드를 포함하여 해시 코드를 계산함.
  • 31 사용: 소수인 31을 사용하여 각 필드를 조합함. 31은 홀수이고 소수로, 해시 충돌을 줄이는 데 도움이 됨.
  • 기본형 필드: 기본형 필드는 직접 포함함. 예를 들어, int 필드는 직접 해시 코드 계산에 사용함.
  • 참조형 필드: 참조형 필드는 해당 객체의 hashCode를 포함함. 만약 필드가 null일 수 있다면 null인 경우를 처리함.
  • 예제 코드
@Override
public int hashCode() {
    int result = 17; // 임의의 비-zero 정수로 시작
    result = 31 * result + (name != null ? name.hashCode() : 0); // name 필드의 해시코드 포함
    result = 31 * result + age; // age 필드의 해시코드 포함
    return result;
}

핵심 정리

  • equals를 재정의할 때는 hashCode도 반드시 재정의해야함. 그렇지 않으면 프로그램이 제대로 동작하지 않음. 재정의한 hashCodeObject의 API 문서에 기술된 이란 규약을 따라야 하며, 서로 다른 인스턴스라면 되도록 해시코드도 서로 다르게 구현해야 함. 이렇게 구현하기가 어렵지는 않지만 조금 따분함 일임. AutoValue 프레임워크를 사용하면 equalshashCode를 자동으로 만들어줌.

예제 코드

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

toString을 재정의해야 하는 이유

  • toString을 잘 구현한 클래스는 사용하기에 훨신 즐겁고, 그 클래스를 사용한 시스템은 디버깅하기 쉬움.
  • 실제로 사용할 때는 toString은 그 객체가 가진 주요 정보를 반환하는 것이 좋음.
  • 포맷을 명시하든 아니든 의도는 명확히 밝여야 함.
  • toString이 반환한 값에 포함된 정보를 얻어놀 수 있는 API를 제공할 것.

핵심 정리

  • 모든 구체 클래스에서 ObjecttoString을 재정의할 것. 상위 클래스에서 이미 알맞게 재정의한 경우는 예외임. toString을 재정의한 클래스는 사용하기도 즐겁고 그 클래스를 사용한 시스템을 디버깅하기 쉽게 해줌. toString은 해당 객체에 관한 명확하고 유용한 정보를 읽기 좋은 형태로 반환해야 함.

예제 코드

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

clone의 특징

  • 실무에서 Cloneable을 구현한 클래스는 clone 메서드를 public으로 제공하며, 사용자는 당연히 복제가 제대로 이뤄지리라 기대함.
  • Cloneable 아키텍처는 '가변 객체를 참조하는 필드는 final로 선언하라'는 일반 용법과 충돌함.
  • publicclone메서드에서는 throws 절을 없애야 함.
  • Cloneable을 이미 구현한 클래슥 아니라면, 복사 생성자와 복사 팩터리라는 더 나은 객체 복사 방식을 제공할 수 있음.

핵심 정리

  • Cloneable이 몰고 온 모든 문제를 되짚어봤을 때, 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스로 이를 구현해서는 안 됨. final 클래스라면 Cloneable을 구현해도 위험이 크진 않지만, 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 함. 기본 원칙은 '복제 기능은 생성자 팩터리를 이용하는 게 최고'라는 것. 단, 배열만은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있음.

예제 코드

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

CompareTo 일반 규약

  • 반사성 (Reflexive): x.compareTo(x) == 0이어야 함. 어떤 객체도 자신과 비교할 때 0을 반환해야 함.
  • 대칭성 (Symmetric): x.compareTo(y) == -y.compareTo(x)여야 함. 만약 xy보다 작으면 yx보다 커야 하고, 그 반대도 성립해야 함.
  • 추이성 (Transitive): x.compareTo(y) > 0이고 y.compareTo(z) > 0이면 x.compareTo(z) > 0이어야 함. 즉, xy보다 크고 yz보다 크면 xz보다 커야 함.
  • 일관성 (Consistency): x.compareTo(y) == 0이면, x.compareTo(z)y.compareTo(z)의 결과는 항상 같아야 함. 객체의 비교 결과가 일관성이 있어야 함.
  • Null 비교: compareTo 메서드는 null을 비교 대상으로 할 수 없음. NullPointerException을 던져야 함.

CompareTo를 사용할 때 주의할 점

  • compareTo 메서드에서 관계 연산자 <>를 사용하는 이전 방식은 거추장스럽고 오류를 유발하니, 이제는 추천하지 않음.

핵심 정리

  • 순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 함. compareTo 메서드에서 필드의 값을 비교할 때 <>연산자는 쓰지 말아야 함. 그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용할 것.

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

  • public 클래스는 절대 가변 피드를 직접 노출해서는 안 됨. 불변 필드라면 노출해도 덜 위험하지만 완전히 안심할 수는 없음. 하지만 package-private 클래스나 private 중첩 클래스에서는 종종 (불변이든 가변이든) 필드를 노출하는 편이 나을 때도 있음.

예제 코드

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

예제 코드

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

핵심 정리

  • 상속은 강력하지만 캡슐화를 해친다는 문제가 있음. 상속은 상위 클래스와 하위 클래스가 순수한 is-a 관계일때만 써야 함. is-a 관계일 때도 안심할 수만은 없는 게, 하위 클래스의 패키지가 상위 클래스와 다르고, 상위 클래스가 확장을 고려해 설계되지 않았다면 여전히 문제가 될 수 이씀. 상속의 취약점을 피하려면, 상속 대신 컴포지션과 전달을 사용할 것. 특히 래퍼 클래스로 구현할 적당한 인터페이스가 있다면 더욱 그러함. 래퍼 클래스는 하위 클래스보다 견고하고 강력함.

예제 코드

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

핵심 정리

  • 상속용 클래스를 설계하기란 결코 만만치 않음. 클래스 내부에서 스스로를 어떻게 사용하는지(자기사용 패턴) 모두 문서로 남겨야 하며, 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 함. 그렇지 않으면, 그 내부 구현 방식을 믿고 활용하던 하위 클래스를 오동작하게 만들 수 있음. 다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 proteted로 제공해야할 수도 있음. 그러니 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 나음. 상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록 만들면 됨.

예제 코드

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

핵심 정리

  • 일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합함. 복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 꼭 고려해볼 것. 골격 구현은 '가능한 한' 인터페이스의 디폴트 메서드로 제공하여 그 인터페이스를 구현한 모든 곳에서 활용하도록 하는 것이 좋음. '가능한 한'이라고 한 이유는, 인터페이스에 걸려있는 구현상의 제약 때문에 골격 구현을 추상 클래스로 제공하는 경우가 더 흔하기 때문임.

예제 코드

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

예제 코드

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

핵심 정리

  • 인터페이스는 타입을 정의하는 용도로만 사용해야 함. 상수 공개용 수단으로 사용하지 말 것.

예제 코드

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

태그 달린 클래스

  • 태그 달린 클래스(tagged class)는 하나의 클래스 안에 여러 가지 역할이나 상태를 나타내는 필드를 포함하고, 그 필드의 값에 따라 동작을 다르게 하는 클래스. 일반적으로 이러한 태그 필드(tag field)는 해당 클래스가 어떤 역할을 하는지를 나타내며, 그에 따라 클래스 내부의 다른 필드들이 사용되거나 무시됨. 이는 클래스 계층구조를 어설프게 흉내낸 것으로, 코드가 장황해지고 유지보수하기 어려워짐.

태그 달린 클래스의 단점

  • 태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적임.
  • 태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류일 뿐임.

핵심 정리

  • 태그 달린 클래스를 써야 하는 상황은 거의 없음. 새로운 클래스를 작성하는 데 태그 필드가 등장한다면 태그를 없애고, 계층 구조로 대체하는 방법을 생각해 볼 것. 기존 클래스가 태그 필드를 사용하고 있다면 계층구조로 리팩터링하는 것을 고민해볼 것.

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

핵심 정리

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

예제 코드

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

핵심 정리

  • 소스 파일 하나에는 반드시 톱레벨 클래스(혹은 톱레벨 인터페이스)를 하나만 담을 것. 이 규칙만 따른다면 컴파일러가 한 클래스에 대한 정의를 여러개 만들어 내는 일은 사라짐. 소스 파일을 어떤 순서로 컴파일하든 바이너리 파일이나 프로그램의 동작이 달라지는 일은 결코 일어나지 않을 것.

예제 코드

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

내용 정리

  • 로 타입을 쓰면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 됨.
  • List<Object>와 같은 매개변수화 타입을 사용할 때와 달리 List같은 로 타입을 사용하면 타입 안전성을 잃게 됨.
  • class 리터럴에는 로 타입을 써야함.
  • 다음은 제네릭 타입에 instanceof를 사용하는 올바른 예제.

제네릭 클래스 / 제네릭 인터페이스

  • 제네릭 클래스와 인터페이스는 클래스나 인터페이스를 정의할 때 타입 파라미터를 사용하는 것을 의미함. 이를 통해 코드의 재사용성을 높이고 타입 안전성을 보장할 수 있음.
public class GenericClass<T> {
    private T value;

    public GenericClass(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

public interface GenericInterface<T> {
    void doSomething(T t);
}

매개변수화 타입

  • 매개변수화 타입은 제네릭 클래스나 인터페이스를 인스턴스화할 때 구체적인 타입을 지정하는 것을 의미함. 이를 통해 제네릭 타입을 사용할 때 타입 안전성을 제공받을 수 있음.
GenericClass<String> stringInstance = new GenericClass<>("Hello");
GenericClass<Integer> integerInstance = new GenericClass<>(123);

로 타입

  • 로 타입(raw type)은 제네릭 타입에서 타입 매개변수를 지정하지 않은 경우를 의미함. 로 타입을 사용하면 제네릭이 제공하는 타입 안전성 검사가 무력화됨.
GenericClass rawInstance = new GenericClass("Raw Type");

제네릭 타입과 instanceof

  • 제네릭 타입에 대해 instanceof를 사용할 때는 로 타입을 사용해야 함. 이를 통해 제네릭 타입의 인스턴스를 안전하게 검사할 수 있음.
if (o instanceof GenericClass) {
    GenericClass<?> genericInstance = (GenericClass<?>) o;
}

와일드카드

  • 와일드카드는 제네릭 타입을 사용할 때 다양한 타입을 처리할 수 있도록 유연성을 제공하는 기법. 주로 ?를 사용하여 상위 또는 하위 타입 경계를 지정함.
public void printList(List<?> list) {
    for (Object elem : list) {
        System.out.println(elem);
    }
}

핵심 정리

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

예제 코드

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

내용 정리

  • 할 수 있는 모든 비검사 경고를 제거할 것.
  • 경고를 제거할 수는 없지만 타입 안전하다고 확신할 수 있다면 @SupressWarnings("unchecked") 애너테이션을 달아 숨길 것.
  • @SupressWarnings은 항상 가능한 좁은 범위에 적용할 것.
  • SupressWarnings("unchecked") 애너테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 함.

공변성과 실체화

공변성 (Covariance)

  • 배열의 공변성: 배열은 공변(covariant). 즉, 배열의 타입 간에 상속 관계가 있으면, 배열 타입 간에도 같은 상속 관계가 있음. 예를 들어, IntegerNumber의 서브타입이기 때문에 Integer[]Number[]의 서브타입. 이는 유연하게 보일 수 있지만, 타입 안전성 문제를 일으킬 수 있음.
public class CovariantExample {
    public static void main(String[] args) {
        Number[] numbers = new Integer[10]; // Integer[] is a subtype of Number[]
        numbers[0] = 1; // Valid
        numbers[1] = 1.5; // ArrayStoreException at runtime, because 1.5 is not an Integer
    }
}

실체화 (Reification)

  • 배열의 실체화: 배열은 런타임 시 실제 타입 정보를 유지함. 즉, 배열의 타입이 런타임에도 유지됨. 따라서 배열에 잘못된 타입의 요소를 저장하려고 하면 런타임에 예외가 발생할 수 있음.
  • 제네릭 타입의 타입 소거: 반면, 제네릭 타입은 컴파일 시 타입 검사를 수행하지만, 런타임에는 타입 정보가 지워짐. 이를 타입 소거(type erasure)라고 함. 따라서 제네릭 타입의 인스턴스는 런타임에 타입 정보를 유지하지 않음.
import java.util.ArrayList;
import java.util.List;

public class ReificationExample {
    public static void main(String[] args) {
        // 배열은 실체화됨
        Object[] objectArray = new Integer[1];
        objectArray[0] = "Hello"; // ArrayStoreException at runtime

        // 제네릭 타입은 실체화되지 않음 (타입 소거)
        List<Integer> integerList = new ArrayList<>();
        List rawList = integerList; // Raw type, no generic type information
        rawList.add("Hello"); // No compile-time error, but introduces type-safety issue
        Integer integer = integerList.get(0); // ClassCastException at runtime
    }
}

핵심 정리

  • 비검사 경고는 중요하니 무시하지 말 것. 모든 비검사 경고는 런타임에 ClassCastException을 일으킬 수 있는 잠재적 가능성을 뜻하니 최선을 다해서 제거할 것. 경고를 없앨 방법을 찾지 못하겠다면, 그 코드가 타입 안전함을 증명하고 가능한 한 범위를 좁혀 @SupperessWarnings("unchecked") 애너테이션으로 경고를 숨길 것. 그런 다음 경고를 숨기기로 한 근거를 주석으로 남길 것.

예제 코드

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

핵심 정리

  • 배열과 제네릭에는 매우 다른 타입 규칙이 적용됨. 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거됨. 그 결과 배열은 런타입에는 타입 안전하지만 컴파일 타임에는 그렇지 않음. 제네릭은 반대임. 그래서 둘을 섞어 쓰기른 쉽지 않음. 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 생각해야 함.

예제 코드

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

핵심 정리

  • 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편함. 그러니 새로운 타입을 설계할 때는, 형변환 없이도 사용할 수 있도록 할 것. 그렇게 하려면 제네릭 타입으로 만들어야 할 경우가 많음. 기존 타입 중 제네릭이었어야 하는 게 있다면 제네릭 타입으로 변경할 것. 기존 클라이언트에는 아무 영향을 주지 않으면서, 새로운 사용자를 훨신 편하게 해주는 길임.

예제 코드

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

핵심 정리

  • 제네릭 타입과 마찬가지로, 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉬움. 타입도 마찬가지로, 메서드도 형변환 없이 사용할 수 있는 편이 좋으며, 많은 경우 그렇게 하려면 제네릭 메서드가 되어야 함. 역시 타입과 마찬가지로, 형변환을 해줘야 하는 기존 메서드는 제네릭하게 만들 것. 기존 클라이언트는 그대로 둔 채 새로운 사용자의 삶을 훨신 편하게 만들어줄 것.

예제 코드

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

핵심 정리

  • 조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨신 유연해짐. 그러니 널리 쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용해줘야 할 것. PECS 공식을 기억할 것. 즉, 생산자(producer)는 extends를 소비자(consumer)는 super를 사용함. ComparableComparator는 모두 소비자라는 사실도 잊지 말 것.

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

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

예제 코드

아이템 34. int 상수 대신 열거 타입을 사용하라

핵심 정리

  • 열거 타입은 확실히 정수 상수보다 뛰어남. 더 읽기 쉽고 안전하고 강력함. 대다수 열거 타입이 명시적 생성자나 메서드 없이 쓰이지만, 각 상수를 특정 데이터와 연결짓거나 상수마다 다르게 동작하게 할 때는 필요함. 드물게는 하나의 메서드가 상수별로 다르게 동작해야 할 때도 있음. 이런 열거 타입에서는 switch문 대신에 상수별 메서드 구현을 사용할 것. 열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용할 것.

예제 코드

아이템 35. ordinal 메서드 대신 인스턴스 필드를 사용하라

예제 코드

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

핵심 정리

  • 열거할 수 있는 타입을 한데 모아 집합 형태로 사용한다고 해도 비트 필드를 사용할 이유는 없음. EnumSet 클래스가 비트 필드 수준의 명료함과 성능을 제공하고 아이템 34에서 설명한 열거 타입의 장점까지 선사하기 때문임. EnumSet의 유일한 단점이라면(자바 9까지는 아직) 불변 EnumSet을 만들 수 없다는 것. 그래도 향후 릴리스에서 수정되리라 봄. 그때까지는 (명확성과 성능이 조금 희생되지만) Collections.unmodifiableSet으로 EnumSet을 감싸 사용할 수 있음.

예제 코드

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

핵심 정리

  • 배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니, 대신 EnumMap을 사용할 것. 다차원 관계는 EnumMap<..., EnumMap<..>>으로 표현할 것. "애플리케이션 프로그래머는 Enum.ordinal을 (웬만해서는) 사용하지 말아야한다(아이템 35)"는 일반 원칙의 특수한 사례임.

예제 코드

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

핵심 정리

  • 열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 함께 사용해 같은 효과를 낼 수 있음. 이렇게 하면 클라이언트는 이 인터페이스를 구현해 자신만의 열거 타입(혹은 다른 타입)을 만들 수 있음. 그리고 API가 (기본 열거 타입을 직접 명시하지 않고) 인터페이스 기반으로 작성되었다면 기본 열거 타입의 인스턴스가 쓰이는 모든 곳을 새로 확장한 열거 타입으 인스턴스로 대체해 사용할 수 있음.

예제 코드

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

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

  • 마커 인터페이스와 마커 애너테이션은 각자의 쓰임이 있음. 새로 추가하는 메서드 없이 단지 타입 정의가 목적이라면 마커 인터페이스를 선택할 것. 클래스가 인터페이스 외의 프로그램 요소에 마킹해야 하거나, 애너테이션을 적극 활용하는 프레임워크의 일부로 그 마커를 편입시키고자 한다면 마커 애너테이션이 올바른 선택임. 적응 대상이 EnumType.TYPE인 마커 애니테이션으르 작성하고 있다면, 잠시 여유를 갖고 정말 애너테이션으로 구현하는 게 옳은지, 혹은 마커 인터페이스가 낫지는 않을지 곰곰히 생각해볼 것.

예제 코드

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

내용 정리

  • 람다는 이름이 없고 문서화도 못함. 따라서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 말아야 함.
  • 람다를 직렬화하는 일은 극히 삼가야 함.
  • 익명 클래스는 (함수형 인터페이스가 아닌) 타입의 인스턴스를 만들 때만 사용할 것.

핵심 정리

  • 자바가 8로 업데이트 되면서 작은 함수 객체를 구현하는데 적합한 람다가 도입됨. 타입을 명시해야 코드가 더 명확할 때만 제외하고는, 람다의 모든 매개변수 타입은 생략할 것.람다는 작은 함수 객체를 아주 쉽게 표현할 수 있어 (이전 자바에서는 실용적이지 않던) 함수형 프로그래밍으 지평을 열음.

예제 코드

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

메서드 참조 유형

  • 정적 메서드 참조 (Static Method Reference)
    • 형식: ClassName::staticMethodName
    • 설명: 클래스의 정적 메서드를 참조함.
  • 한정적 인스턴스 메서드 참조 (Bound Instance Method Reference)
    • 형식: instanceRef::instanceMethodName
    • 설명: 특정 객체의 인스턴스 메서드를 참조함.
  • 비한정적 인스턴스 메서드 참조 (Unbound Instance Method Reference)
    • 형식: ClassName::instanceMethodName
    • 설명: 클래스의 인스턴스 메서드를 참조하지만, 객체 인스턴스는 람다 표현식의 첫 번째 매개변수로 전달됨.
  • 클래스 생성자 메서드 참조 (Class Constructor Reference)
    • 형식: ClassName::new
    • 설명: 클래스의 생성자를 참조함.
  • 배열 생성자 메서드 참조 (Array Constructor Reference)
    • 형식: TypeName[]::new
    • 설명: 배열의 생성자를 참조함.

핵심 정리

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

예제 코드

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

내용 정리

  • 필요한 용도에 맞는 게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용할 것.
  • 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지 말 것.
  • 직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface를 활용할 것.

기본 함수형 인터페이스

  • UnaryOperator<T>
    • 개념: 같은 타입의 인수와 반환값을 가지는 함수형 인터페이스. 입력값을 연산하여 동일한 타입의 결과를 반환함.
    • 메서드: T apply(T t)
  • BinaryOperator<T>
    • 개념: 두 개의 같은 타입 인수를 받아 동일한 타입의 결과를 반환하는 함수형 인터페이스. 주로 두 값을 연산하는 데 사용됨.
    • 메서드: T apply(T t1, T t2)
  • Predicate<T>
    • 개념: 인수를 받아 boolean 값을 반환하는 함수형 인터페이스. 주로 조건 검사에 사용됨.
    • 메서드: boolean test(T t)
  • Function<T, R>
    • 개념: 인수를 받아 다른 타입의 결과를 반환하는 함수형 인터페이스. 주로 값을 변환하는 데 사용됨.
    • 메서드: R apply(T t)
  • Supplier<T>
    • 개념: 인수 없이 결과만 반환하는 함수형 인터페이스. 주로 값을 생성하는 데 사용됨.
    • 메서드: T get()
  • Consumer<T>
    • 개념: 인수를 받지만 결과를 반환하지 않는 함수형 인터페이스. 주로 값을 소비하는 데 사용됨.
    • 메서드: void accept(T t)

핵심 정리

  • 이제 자바도 람다를 지원함. 여러분도 지금부터는 API를 설계할 때, 람다도 염두에 두여야 한다는 뜻. 입력값과 반환값에 함수형 인터페이스 타입을 활용할 것. 보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 가장 좋은 선택임. 단, 흔치는 않지만 직접 새로운 함수형 인터페이스를 만들어 쓰는 편이 나을 수도 있음을 잊지 말 것.

예제 코드

아이템 45. 스트림은 주의해서 사용하라

핵심 정리

  • 스트림을 사용해야 멋지게 처리할 수 있는 일이 있고, 반복 방식이 더 알맞은 일도 있음. 그리고 수 많은 작업들이 이 둘을 조합했을 때 가장 멋지게 해결됨. 어느 쪽을 선택하는 확고부동한 규칙은 없지만 참고할 만한 지침 정도는 있음. 어느 쪽이 나은 지가 확연히 드러나는 경우가 많겠지만, 아니더라도 방법이 있음. 스트림과 반복 중 어느 쪽이 나은지 확신하기 어렵다면 둘 다 해보고 더 나은 쪽을 택할 것.

예제 코드

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

핵심 정리

  • 스트림 파이프라인 프로그래밍의 핵심은 부작용 없는 함수 객체에 있음. 스트림 뿐 아니라 스트림 관련 객체에 건네지는 모든 함수가 객체가 부작용이 없어야 함. 종단 연산 중 forEach는 스트림이 수행한 계산 결과를 보고할 때만 이용해야 함. 계산 자체에는 이용하지 말 것. 스트림을 올바로 사용하려면 수집기를 잘 알아둬야 함. 가장 중요한 수집기 팩터리는 toList, toSet, toMap, groupingBy, joining임.

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

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

예제 코드

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

메서드 값의 유효성을 검사하는 방법

  • java.util.Objects.requireNonNull을 활용할 것.
  • checkFromIndexSize, checkFromToIndex, checkIndex를 활용할 것.
  • assert를 활용할 것. VSCode 기준으로는 launch.json에 아래와 같이 vmArgs-ea를 추가해야 함.
    "version": "0.2.0",
    "configurations": [
        {
            "type": "java",
            "name": "Launch Main",
            "request": "launch",
            "mainClass": "chapter08.Item49_CheckParametersForValidity",
            "args": [],
            "vmArgs": "-ea"
        }
    ]

assert

개념

  • assert는 자바에서 제공하는 조건 검증 도구로, 주로 디버깅 및 테스트 중에 코드의 논리적 오류를 검출하기 위해 사용됨. assert 키워드는 특정 조건이 참인지 확인하며, 조건이 거짓이면 AssertionError를 던짐. 이 기능은 개발 중에만 활성화되고, 실제 운영 환경에서는 비활성화할 수 있음.

사용법

  • 기본 사용법: 이 구문은 conditiontrue인지 확인함. conditionfalse이면 AssertionError가 발생함.
assert condition;
  • 조건이 거짓일 때 메시지를 포함하는 방법:이 구문은 conditiontrue인지 확인하고, conditionfalse일 경우 지정한 "Error message"와 함께 AssertionError를 발생시킴.
assert condition : "Error message";

핵심 정리

  • 메서드나 생성자를 작성할 때면 그 매개변수들에 어떤 제약이 있을지 생각해야 함. 그 제약들을 문서화하고 메서드 코드 시작 부분에서 명시적으로 검사해야 함. 이런 습관을 반드시 기르도록 할 것. 그 노력은 유효성 검사가 실제 오류를 처음 걸러낼 때 충분히 보상받을 것.

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

방어적 본사본이란?

  • 방어적 복사본(defensive copy)은 객체의 불변성을 유지하기 위해 클라이언트가 제공한 객체의 복사본을 만들어 사용하는 프로그래밍 기법. 이는 객체의 상태를 외부에서 변경하지 못하게 방지하는 역할을 함. 특히, 클라이언트가 제공한 객체가 가변적(mutable)인 경우에는 방어적 복사본을 통해 원본 객체의 변경으로부터 안전하게 보호할 수 있음.

방어적 복사를 구현할 때 유의해야 할 점

  • 입력 매개변수 복사: 클라이언트가 제공한 가변 객체를 생성자나 메서드의 내부에 저장하기 전에 복사함.
  • 반환값 복사: 가변 객체를 반환할 때, 원본을 그대로 반환하지 않고 복사본을 반환함.
  • 클론 메서드 사용 자제: 매개변수가 제 3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone 메서드를 사용하지 않음. 이는 clone 메서드가 정확하게 구현되지 않은 경우가 많기 때문임.
  • 클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍 해야 함.
  • Date는 낡은 API이니 새로운 코드를 작성할 때는 더 이상 사용하면 안 됨.
  • 생성자에게 받은 매개변수 각각을 방어적으로 복사해야 함.
  • 매개변수가 제 3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 됨.

방어적 복사를 사용하는 케이스

  • 컬렉션 (Collection): 컬렉션(예: List, Set, Map)을 반환할 때 방어적 복사를 사용하여 원본 컬렉션이 외부에서 변경되지 않도록 보호할 수 있음.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class DefensiveCopyExample {
    private final List<String> items;

    public DefensiveCopyExample(List<String> items) {
        this.items = new ArrayList<>(items); // 방어적 복사
    }

    public List<String> getItems() {
        return Collections.unmodifiableList(new ArrayList<>(items)); // 방어적 복사 후 불변 리스트 반환
    }

    public static void main(String[] args) {
        List<String> original = new ArrayList<>();
        original.add("Item1");
        original.add("Item2");

        DefensiveCopyExample example = new DefensiveCopyExample(original);
        List<String> items = example.getItems();

        // items.add("Item3"); // 예외 발생: UnsupportedOperationException

        System.out.println(items);
    }
}
  • 배열 (Array): 배열을 반환할 때 방어적 복사를 사용하여 원본 배열이 외부에서 변경되지 않도록 보호할 수 있음.
public class DefensiveCopyArrayExample {
    private final int[] numbers;

    public DefensiveCopyArrayExample(int[] numbers) {
        this.numbers = numbers.clone(); // 방어적 복사
    }

    public int[] getNumbers() {
        return numbers.clone(); // 방어적 복사
    }

    public static void main(String[] args) {
        int[] original = {1, 2, 3, 4, 5};
        DefensiveCopyArrayExample example = new DefensiveCopyArrayExample(original);

        int[] numbers = example.getNumbers();
        numbers[0] = 100; // 원본에 영향 없음

        System.out.println(java.util.Arrays.toString(example.getNumbers()));
    }
}
  • 불변 객체 (Immutable Objects): 불변 객체를 반환할 때, 내부의 가변 필드를 방어적 복사하여 반환함.
public class DefensiveCopyImmutableExample {
    private final ComplexObject complexObject;

    public DefensiveCopyImmutableExample(ComplexObject complexObject) {
        this.complexObject = new ComplexObject(complexObject); // 방어적 복사
    }

    public ComplexObject getComplexObject() {
        return new ComplexObject(complexObject); // 방어적 복사
    }

    static class ComplexObject {
        private final String data;

        public ComplexObject(String data) {
            this.data = data;
        }

        public ComplexObject(ComplexObject other) {
            this.data = other.data; // 복사 생성자
        }

        @Override
        public String toString() {
            return data;
        }
    }

    public static void main(String[] args) {
        ComplexObject original = new ComplexObject("Hello");
        DefensiveCopyImmutableExample example = new DefensiveCopyImmutableExample(original);

        ComplexObject copy = example.getComplexObject();
        System.out.println(copy);
    }
}
  • 깊은 복사 (Deep Copy): 객체가 중첩된 가변 객체를 포함하고 있을 때, 깊은 복사를 통해 모든 중첩 객체를 복사함.
import java.util.HashMap;
import java.util.Map;

public class DeepCopyExample {
    private final Map<String, String> data;

    public DeepCopyExample(Map<String, String> data) {
        this.data = new HashMap<>(data); // 깊은 복사
    }

    public Map<String, String> getData() {
        return new HashMap<>(data); // 깊은 복사
    }

    public static void main(String[] args) {
        Map<String, String> original = new HashMap<>();
        original.put("key1", "value1");
        original.put("key2", "value2");

        DeepCopyExample example = new DeepCopyExample(original);
        Map<String, String> copy = example.getData();
        copy.put("key3", "value3"); // 원본에 영향 없음

        System.out.println(example.getData());
    }
}
  • 상태가 변경될 수 있는 객체 (Mutable Objects): 상태가 변경될 수 있는 객체를 반환할 때, 방어적 복사를 통해 외부에서 객체 상태를 변경하지 못하도록 함.
public class MutableObjectExample {
    private final Address address;

    public MutableObjectExample(Address address) {
        this.address = new Address(address); // 방어적 복사
    }

    public Address getAddress() {
        return new Address(address); // 방어적 복사
    }

    static class Address {
        private String street;

        public Address(String street) {
            this.street = street;
        }

        public Address(Address other) {
            this.street = other.street;
        }

        public String getStreet() {
            return street;
        }

        public void setStreet(String street) {
            this.street = street;
        }

        @Override
        public String toString() {
            return street;
        }
    }

    public static void main(String[] args) {
        Address original = new Address("123 Main St");
        MutableObjectExample example = new MutableObjectExample(original);

        Address copy = example.getAddress();
        copy.setStreet("456 Broadway"); // 원본에 영향 없음

        System.out.println(example.getAddress());
    }
}
  • 안전한 스레드 (Thread Safety): 스레드 간의 데이터 공유 시, 방어적 복사를 통해 안전하게 데이터를 전달함.
public class ThreadSafeExample {
    private final SharedData sharedData;

    public ThreadSafeExample(SharedData sharedData) {
        this.sharedData = new SharedData(sharedData); // 방어적 복사
    }

    public SharedData getSharedData() {
        return new SharedData(sharedData); // 방어적 복사
    }

    static class SharedData {
        private String data;

        public SharedData(String data) {
            this.data = data;
        }

        public SharedData(SharedData other) {
            this.data = other.data;
        }

        public String getData() {
            return data;
        }

        public void setData(String data) {
            this.data = data;
        }

        @Override
        public String toString() {
            return data;
        }
    }

    public static void main(String[] args) {
        SharedData original = new SharedData("Shared Data");
        ThreadSafeExample example = new ThreadSafeExample(original);

        SharedData copy = example.getSharedData();
        copy.setData("Modified Data"); // 원본에 영향 없음

        System.out.println(example.getSharedData());
    }
}

핵심 정리

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

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

오류 가능성이 적은 API를 설계하는 방법

  • 편의 메서드를 너무 많이 만들지 말 것. 확신이 없으면 만들지 말 것.
  • 매개변수 목록은 짦게 유지할 것. 4개 이하가 좋음. 같은 타입의 매개변수 여러 개가 연달아 나오는 경우가 특히 해로움. 다음은 매개변수 목록을 짧게 줄여주는 기술임.
    • 여러 메서드로 쪼개기.
    • 매개변수 여러 개를 묶어주는 도우미 클래스를 만들기.
    • 객체 생성에 사용한 빌더 패턴을 메서드 호출에 응용하기.
  • 매개변수의 타입으로는 클래스보다는 인터페이스가 더 나음.
  • boolean보다는 원소 2개짜리 열거 타입이 나음. 열커 타입을 사용하면 코드를 읽고 쓰기가 더 쉬워짐. 나중에 선택지를 추가하기도 쉬움.

아이템 52. 오버로딩(overloading)는 신중히 사용하라

오버로딩(overloading)의 단점

  • 어느 메서드를 호출할지가 컴파일 타임에 정해짐.
  • 오버라이딩(overriding)한 메서드는 동적으로 선택되고, 오버로딩(overloading)한 메서드는 정적으로 선택됨.

오버로딩(overloading)을 사용할 때 유의할 점

  • 오버로딩(overloading)이 혼동을 일으키는 상황을 피해야 함.
  • 안전하고 보수적으로 가려면 매개변수 수가 같은 오버로딩(overloading)은 피할 것. 오버로딩(overloading)하는 대신 메서드 이름을 다르게 지어주는 방법도 있음.
  • 메서드를 오버로딩(overloading) 할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안됨.

핵심 정리

  • 프로그래밍 언어가 오버로딩(overloading)을 허용한다고 해서 오버로딩(overloading)을 꼭 활용하란 뜻은 아님. 일반적으로 매개변수가 같을 때는 오버로딩(overloading)을 피하는 것이 좋음. 상황에 따라, 특히 생성자라면 이 조언을 따르기가 불가능할 수 있음. 그럴 때는 헷갈릴만한 매개변수는 형변환하여 명확한 오버로딩(overloading) 메서드가 선택되도록 해야 할 것. 이것이 불가능하면, 예컨대 기존 클래스를 수정해 새로운 인터페이스를 구현해야 할 때는 같은 객체를 입력받는 오버로딩(overloading) 메서드들이 모두 동일하게 동작하도록 만들어야 함. 그렇지 못하면 프로그래머들은 오버로딩(overloading)된 메서드나 생성자를 효과적으로 사용하지 못할 것이고, 의도대로 동작하지 않는 이유를 이해하지도 못할 것.

예제 코드

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

핵심 정리

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

예제 코드

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

null보다 빈 값을 반환해야 하는 이유

  • null을 사용하면, 항시 다시 사용할 경우 방어코드가 필요함.
  • 빈 컨테이너를 할당해서 반환하는 것과 null을 반환하는 것에는 성능 차이가 거의 없음.
  • 빈 컬렉션과 비열은 새로 할당하지 않고 반환 가능함.

핵심 정리

  • null이 아닌, 빈 배열이나 컬렉션을 반환할 것. null을 반환하는 API는 사용하기 어렵고 오류 처리 코드도 늘어남. 그렇다고 성능이 좋은 것도 아님.

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

옵셔널이란?

  • Optional은 Java8에서 도입된 class.
  • 값이 있을 수도 없을 수도 있는 상황에서 사용됨. 즉, NullPointerException을 방지하기 위해 사용할 수 있음.
  • Optional 객체는 값이 없는 상황에서는 Optional.empty()를 사용하여 생성할 수 있음.
  • Optional 객체는 isPresent() 메서드를 통해 값의 존재 여부를 확인할 수 있으며, get() 메서드를 통해 값을 가져올 수 있음. 단, get()은 값이 없는 경우 NoSuchElementException을 던짐.
  • Optional 객체의 orElse(), orElseGet(), orElseThrow() 메서드를 통해 값이 없는 경우에 대한 처리를 명시적으로 정의할 수 있음.
    • orElse(): Optional이 비어있는지 여부와 관계없이 항상 인자로 전달된 값을 평가함. 따라서 해당 값을 얻는 데 비용이 크거나 부작용이 있는 경우에는 주의해야 함.
    • orElseGet(): Optional이 비어있을 때만 인자로 전달된 함수형 인터페이스를 실행함. 따라서 값을 얻는 데 비용이 크거나 부작용이 있는 경우에 유용함.
    • orElseThrow(): Optional이 비어있을 때 인자로 전달된 예외 공급자(exception supplier)를 실행하여 예외를 던짐. 이를 통해 값이 없는 경우에 대한 사용자 정의 예외 처리를 할 수 있음.
  • Optionalnull 체크를 직접 하지 않고도 우아하게(elegantly) 처리할 수 있는 방법을 제공함.
  • Optional은 값이 없는 경우 기본값을 반환하거나, 원하는 예외를 던지는 등의 다양한 방법으로 활용할 수 있음.

옵셔널을 사용하는 상황

  • 결과가 없을 수 있으며, 클라이언트가 이 상황을 특별히 처리해야 할 상황에 Optional<T>를 반환함.

옵셔널을 사용할 때 주의할 점

  • 옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말 것. 옵셔널을 도입한 취지를 무시하는 행위임.
  • 옵셔널은 검사 예외와 취지가 비슷함. 즉, 반환 값이 없을 수도 있음을 API 사용자에게 명확하게 알려줌.
  • 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안됨. 옵셔널로 감싸기 보다는 빈 컨테이너를 반환하는 것이 좋음.
  • 박싱된 기본 타입을 담은 옵셔널을 반환해서는 안됨. 옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없음.

옵셔널 활용법

  • 기본값을 정해둘 수 있음.
  • 원하는 예외를 던질 수 있음.
  • 항상 값이 채워져 있다고 가정함.
  • 값이 처음에 필요할 때, Supplier<T>를 활용해서 초기 설정 비용을 낮출 수 있음.
  • 적합한 메서드를 찾지 못했다면, isPresent를 사용할 것. 하지만 대부분은 작업은 위의 작업으로 처리 가능함.

핵심 정리

  • 값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야 하는 메서드라면 옵셔널을 반환해야 할 상황일 수 있음. 하지만 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수도 있음. 그리고 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드묾.

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

핵심 정리

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

예제 코드

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

예제 코드

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

핵심 정리

  • 전통적인 for 문과 비교했을 때, for-each 문은 명료하고, 유연하고, 버그를 예방해 줌. 성능 저하도 없음. 가능한 모든 곳에서 for문이 아닌 for-each문을 사용할 것.

예제 코드

아이템 59. 라이브러리를 익히고 사용하라

핵심 정리

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

예제 코드

아이템 60. 정확한 답이 필요하다면 floatdouble은 피하라

핵심 정리

  • 정확한 답이 필요한 계산에는 floatdouble을 피할 것. 소수점 추적은 시스템에 맡기고, 코딩 시의 불편함이나 성능 저하를 신경쓰지 않겠다면 BigDecimal을 사용할 것. BicDecimal이 제공하는 여덟가지 반올림 모드를 이용하여 반올림을 완벽히 제어할 수 있음. 법으로 정해진 반올림을 수행해야 하는 비즈니스 계산에서 매우 편리한 기능임. 반면, 성능이 중요하고 소수점을 직접 추적할 수 있고 숫자가 너무 크지 않다면 intlong을 사용할 것. 숫자를 아홉자리 십진수로 표현할 수 있다면 int를 사용하고, 열여덟 자리 십진수로 표현할 수 있다면 long을 사용할 것. 열여덟 자리를 넘어가면 BigDecimal을 사용해야 함.

예제 코드

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

핵심 정리

  • 기본 타입과 박싱된 기본 타입 중 하나를 선택해야 한다면 가능하면 기본 타입을 사용할 것. 기본 타입은 간단하고 빠름. 박싱된 기본 타입을 써야 한다면 주의를 기울일 것. 오토박싱이 박싱된 기본 타입을 사용할 때의 번거로움을 줄여주지만, 그 위험까지 없애주지 않음. 두 박싱된 기본 타입을 == 연산자로 비교한다면 식별성 비교가 이뤄지는데, 이는 여러분이 원한 게 아닐 가능성이 큼. 같은 연산에서 기본 타입과 박싱된 기본 타입을 혼용하면 언박싱이 이뤄지며, 언박싱 과정에서 NullPointerException을 던질 수 있음. 마지막으로, 기본 타입을 박싱하는 작업은 필요 없는 객체를 생성하는 부작용이 나올 수 있음.

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

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

예제 코드

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

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

  • 네이티브 메서드를 사용하려거든 한번 더 생각할 것. 네이티브 메서드가 성능을 개선해 주는 일은 많지 않음. 저수준자원이나 네이티브 라이브러리를 사용해야만 해서 어쩔 수 없더라도 네이티브 코드는 최소한만 사용하고 철저히 테스트할 것. 네이티브 코드 안에 숨은 단 하나의 버그가 여러분 애플리케잇녀 전체를 훼손할 수도 있음.

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

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

예제 코드

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

내용 정리

  • 예외는 오직 예외 상황에서만 써야함. 절대로 일상적인 제어 흐름용으로 쓰여선 안 됨.
  • 잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 함.

핵심 정리

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

예제 코드

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

내용 정리

  • 호출하는 쪽에서 복구하리라 여겨지는 상황이라면 검사 예외를 사용할 것.
  • 프로그래밍 오류를 나타낼 때는 런타임 예외를 사용할 것.
  • 구현하는 비검사 throwable은 모두 RuntimeException의 하위 클래스여야 함.

검사 예외(Checked Exception)

  • 검사 예외는 Exception 클래스의 서브 클래스 중 RuntimeException을 상속하지 않는 예외.
  • 컴파일러가 프로그램의 컴파일 시점에 검사 예외 처리 여부를 검사함.
  • 검사 예외가 발생할 수 있는 메서드를 호출할 때는 반드시 try-catch 블록으로 예외를 처리하거나 throws 키워드로 예외를 선언해야 함.
  • 대표적인 검사 예외로는 IOException, SQLException 등이 있음.
  • 검사 예외는 일반적으로 복구 가능한 예외 상황을 나타내며, 예외 처리를 강제함으로써 안정성을 높임.
  • 컴파일 시점에 예외 처리 여부를 검사함.
  • 예외 처리를 강제함으로써 안정성을 높일 수 있음.

비검사 예외(Unchecked Exception)

  • 비검사 예외는 RuntimeException 클래스의 서브 클래스.
  • 컴파일러가 프로그램의 컴파일 시점에 비검사 예외 처리 여부를 검사하지 않음.
  • 비검사 예외는 try-catch 블록으로 예외를 처리하거나 throws 키워드로 예외를 선언하지 않아도 됨.
  • 대표적인 비검사 예외로는 NullPointerException, ArrayIndexOutOfBoundsException 등이 있음.
  • 비검사 예외는 일반적으로 프로그램의 오류나 잘못된 사용으로 인한 예외 상황을 나타내며, 개발자가 예외 처리를 선택적으로 할 수 있음.
  • 컴파일 시점에 예외 처리 여부를 검사하지 않음.
  • 예외 처리를 선택적으로 할 수 있어 유연성을 제공함.

Throwable

  • Throwable은 Java에서 모든 예외와 오류의 최상위 클래스.
  • Throwable은 크게 두 가지 하위 클래스로 나뉨
    • Exception: 일반적인 예외를 나타내는 클래스. 프로그램 실행 중에 발생할 수 있는 예외 상황을 나타냄.
    • Error: 심각한 오류를 나타내는 클래스. 일반적으로 프로그램으로 처리할 수 없는 시스템 레벨의 오류를 나타냄.
  • Throwable을 직접 사용하는 경우는 드뭄. 대부분의 경우 Exception이나 Error 클래스 또는 그 하위 클래스를 사용하여 예외와 오류를 처리함.
  • 하지만 Throwable은 예외와 오류의 공통적인 기능을 제공하며, 다음과 같은 상황에서 사용될 수 있음.
    • 모든 예외와 오류를 한 번에 처리해야 할 때
    • 예외와 오류에 대한 공통적인 로깅이나 보고 기능을 구현할 때
    • 사용자 정의 예외 클래스를 만들 때 (예: extends Exception 또는 extends Throwable)

핵심 정리

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

예제 코드

아이템 71. 필요 없는 검사 예외 사용은 피하라

핵심 정리

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

예제 코드

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

내용 정리

  • Exception, RuntimeException, Throwable, Error는 직접 재사용하지 말것.
  • 인수 값이 무엇이었든 어차피 실패했을 거라면 IllegalStateException을, 그렇지 않으면 IllegalArgumentException을 던질 것.

자주 사용하는 Exception

  • IllegalArgumentException: 메서드에 전달된 인수가 유효하지 않을 때 발생함. 예를 들어, 메서드가 특정 범위의 값을 요구하지만 잘못된 값이 전달된 경우에 발생함.
  • IllegalStateException: 객체의 상태가 메서드 호출에 적합하지 않을 때 발생함. 객체가 사용하기 전에 올바르게 초기화되지 않았거나, 이미 종료된 객체에 대해 작업을 시도하는 경우에 발생함.
  • NullPointerException: null 참조를 통해 객체의 메서드나 필드에 접근하려고 할 때 발생함. 예를 들어, null 객체의 메서드를 호출하거나 필드를 참조할 때 발생함.
  • IndexOutOfBoundsException: 배열이나 리스트에서 잘못된 인덱스를 사용할 때 발생함. 예를 들어, 배열의 범위를 벗어난 인덱스에 접근하려고 할 때 발생함.
  • ConcurrentModificationException: 컬렉션이 반복되는 동안 구조적으로 변경될 때 발생함. 예를 들어, 컬렉션의 요소를 반복하면서 요소를 추가하거나 제거할 때 발생함.
  • UnsupportedOperationException: 호출된 메서드가 지원되지 않는 작업을 시도할 때 발생함. 주로 변경할 수 없는 컬렉션에서 요소를 추가하거나 제거할 때 발생함.

예제 코드

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

내용 정리

  • 상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 함.
  • 무턱대고 예외를 전파하는 것보다야 예외 번역이 우수한 방법이지만, 남용해서는 곤란함.

예외 번역(Exception Translation)

  • 예외 번역은 낮은 수준의 예외를 더 높은 수준의 예외로 변환하는 것을 의미함. 이 방법은 API 계층 간의 예외 처리를 단순화하고, 특정 구현 세부 사항에 대한 의존성을 줄이는 데 도움이 됨. 예를 들어, 데이터베이스 계층에서 발생한 예외를 서비스 계층에서 처리하기 쉽도록 변환할 수 있음.

예외 연쇄(Exception Chaining)

  • 예외 연쇄는 하나의 예외가 다른 예외의 원인이 될 때 사용됨. 예외 연쇄를 통해 예외의 발생 원인을 추적할 수 있으며, 각 계층에서 예외를 감싸서 던질 때 원래의 예외를 원인(cause)으로 포함시켜 전달함.

핵심 정리

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

예제 코드

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

내용 정리

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

@throws

  • @throws 태그는 JavaDoc에서 사용되어 메서드가 던질 가능성이 있는 예외를 문서화하는 데 사용됨. 이 태그를 사용하면 메서드가 어떤 예외를 던질 수 있는지, 그리고 그 예외가 발생할 수 있는 상황을 명확하게 설명할 수 있음. 이를 통해 다른 개발자가 해당 메서드를 사용할 때 발생할 수 있는 예외 상황을 예측하고 적절히 처리할 수 있도록 도와줌.

JavaDoc 생성하기

javadoc -d docs -sourcepath src src/chapter10/Item74_DocumentAllThrownExceptions.java

핵심 정리

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

예제 코드

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

예제 코드

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

예제 코드

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

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

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

예제 코드

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

예제 코드

아이템 81. waitnotify보다는 동시성 유틸리티를 애용하라

핵심 정리

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

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

  • readObejct 메서드를 작성할 때는 언제나 public 생성자를 작성하는 자세로 임해야 함. readObject는 어떤 바이트 스트림이 넘어오더라도 유효한 인스턴스를 만들어내야 함. 바이트 스트림이 진짜 직렬화된 인스턴스라고 가정해서는 안 됨. 이번 아이템에서는 기본 직렬화 형태를 사용한 클래스를 예로 들었지만, 커스텀 직렬화를 사용하더라돠 모든 문제가 그대로 발생할 수 있음. 이어서 안전한 readObject 메서드를 작성하는 지침은 다음과 같음.
    • private이어야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적으로 복사할 것. 불편 클래스 내의 가변 요소가 여기 속함.
    • 모든 불변식을 검사하여 어긋나는 게 발견되면 InvalidObjectException을 던짐. 방어적 복사 다음에는 반드시 불변식 검사가 뒤따라야 함.
    • 역직렬화 후 객체 그래프 전체의 유효성을 검사해야 한다면 ObjectInputValidation 인터페이스를 사용할 것(이 책에서는 다루지 않음).
    • 직접적이든 간접적이든 재정의할 수 있는 메서드는 호출하지 말 것.

예제 코드

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

핵심 정리

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

예제 코드

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

핵심 정리

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

예제 코드

About

[Study] Effective Java(이펙티브 자바)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages