이펙티브 자바 총정리

Woody·2023년 9월 10일
0

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

why?

  1. 이름을 가질 수 있다

    • 반환될 객체의 특성을 쉽게 묘사 가능
    • 한 클래스에 시그니처가 같은 생성자가 여럿 필요할 것 같으면 생성자를 static factory method로 바꾸고 차이를 잘 드러내는 이름을 지어주기
    class Person {
      boolean isKorean;
    
      public Person(boolean isKorean) {
        this.isKorean = isKorean;
      }
    }

    이것보단

    class Person {
    	boolean isKorean;
    	
    	private Person(boolean isKorean) {
    	  this.isKorean = isKorean;
    	}
    	
    	public static Person createKorean() {
    	  return new Person(true);
    	}
    	
    	public static Person createForeign() {
    	  return new Person(false);
    	}
    }
  2. 호출될 때마다 인스턴스를 새로 만들 필요가 없음

    • 미리 만들거나 만들어서 캐싱, 재사용
    • 인스턴스를 통제할 수 있다
      • instance-controlled class
      • 상황에 따라 싱글톤으로도, noninstantiable…
    public class Database {
        static Connection connection;
    
        private Database() {}
    
        public static Connection getConnection() {
            if (connection == null) {
                connection = new Connection();
            }
            return connection;
        }
    }
  3. 반환 타입의 하위 타입 객체를 반환할 수 있음

    • 반환 객체에 대한 유연성
    public class Ramen extends Noodle {}
    
    public class RiceNoodle extends Noodle{}
    
    public class Noodle {
    
        public Noodle() {} // Noodle만 return
    
        public static Ramen getRamen() {}
    
        public static RiceNoodle getRiceNoodle() {}
    }
  4. 매개변수에 따라 매번 다른 클래스의 객체 반환 가능

    • 반환타입의 하위 타입이기만 하면 어떤 클래스든 상관 없음
    • 클라 입장에서는 반환 클래스가 어떤 클래스의 인스턴스인지 상관없음
    public class Food {
    
        public static Food getRepresentativeNoodle(String country) {
            if (country.equals("vietnam")) {
                return new RiceNoodle();
            } else if (country.equals("italy")) {
                return new Pasta(); // 나중에 Pizza를 return하도록 바꿔도 됨
            } else {
                //...
            }
        }
    }
  5. 메서드 작성 시점에는 반환 객체 클래스가 존재하지 않아도 됨

    public interface Book {}
    
    public class Library {
        public static List<Book> getLibrary() {
            return new ArrayList<>();
        }
    }
    • 여기서 BookImpl은 나중에 구현해도 됨. Library 작성 시점에서는 없어도 됨
    • 프레임워크를 만드는 근간 (???)
      • 위의 예시로 따지면 사용자가 Book 구현체인 Essay든 Magazine이든 채워넣으면 된다는 이야기인가?

단점

  1. static factory method만 제공하면 하위 클래스를 만들 수 없음
    • 상속하려면 public이나 protected 생성자가 필요하기 때문
  2. 프로그래머가 찾기 어려움
    • 생성자보다 명확히 드러나지는 않음
    • 사용자는 인스턴스화할 방법을 일일이 찾아야 함
    • 문서와 잘하고 이름도 컨벤션 잘 지켜서 짓기

conventions

  • from
    • 매개변수 하나 받아서 해당 타입 인스턴스 반환. 형변환
    • Date.from
  • of
    • 여러 매개변수 받아서 적합한 타입의 인스턴스 반환
    • EnumSet.of
  • valueOf
  • instance/getInstance
    • 매개변수로 명시한 인스턴스를 반환
    • 하지만 같은 인스턴스임을 보장하지 않음
  • create/newInstance
    • instance/getInstance와 같지만
    • 매번 새로운 인스턴스를 생성해 반환함을 보장
  • [get|new]{Type}
    • [get|new]Instance와 같지만
    • 생성할 클래스가 아니라 다른 클래스에 factory method를 정의할 때 씀
  • type
    • [get|new]{Type}의 간결한 버전

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

매개변수가 많을 경우 할 수 있는 짓들

  1. 점층적 생성자 패턴
    • 필드가 n개면 매개변수가 (단순히 생각하면) 0~n개까지인 생성자를 만들 수 있음
    • 불편함
      • 설정 원치 않는 값도 넣어줘야 함
    • 이게 반복되면 클라 코드를 작성하고 읽기 어려움
      • 순서가 바뀌거나 이 값은 무슨 값이지…
  2. JavaBeans 패턴
    • 매개변수 없는 생성자로 객체 생성 수 setter 사용
    • 단점
      • 객체 하나 만들려면 메서드를 여러 개 호출
      • 객체 생성 완료까지 inconsistent
        • 매개변수의 유효성을 어떻게 확인할 것인가? 앞에선 생성자에서 한 번에 확인할 수 있는데?
        • 쓰레드 안정성을 위한 추가 작업 필요
      • immutable하게 만들 수 없음

해결 방안: 빌더 패턴

  • 클라가 필요로 하는 객체를 직접 만들지 않음
  • 순서
    • 필요 매개변수로 생성자나 static factory를 호출해 빌더 객체를 얻음
    • 빌더 객체가 제공하는 setter 통해 원하는 매개변수 설정
    • 매개변수가 없는 build 메서드 호출
    • 필요한 객체 등장! (보통 immutable)
public class Person {
  private final int age;
  private final String name;

	private Person(Builder builder) {
		// 필드 = builder.필드
	}

  public static class Builder {
		// 필수 매개변수
		// 선택 매개변수

		public Builder(필수_매개변수) {
			// 필수 매개변수 설정
	  }

		public Builder age(int age) { ... }

		public Person build() {
			return new Person(this);
		}
  }
}

장점

  • 계층적으로 설계된 클래스와 함께 쓰기 좋음
    • abstract class에서는 추상 빌더
    • 구체에서는 구체 빌더
    • 피자 예시
      • abstract class Pizza
      • class NyPizza extends Pizza
      • class Calzone extends Pizza
    • build 메서드는 구체인 하위 클래스 반환
      • 클라는 형변환에 신경쓸 필요 없음
      • covariant return typing
  • 가변인수 매개변수를 여러 개 사용 가능
    • 메서드를 나누거나
    • 여러 번 호출하도록 하고 내부에서 모을 수도 있음

단점

  • 빌더 만들어야 함
    • 성능 민감한 상황에선 부적합
  • 코드가 길어짐
    • 그러나 대개 API는 시간 지날수록 어차피 매개변수 많아짐

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

싱글턴: 인스턴스를 오직 하나만 생성할 수 있는 클래스

문제점

사용하는 클라 테스트하기가 어려워질 수 있음

만드는 방식

  • 공통
    • 생성자를 private으로 감추고
    • 유일한 인스턴스에 접근할 수 있는 수단 = public static 멤버
      • 첫 번째 방식에서는 final 필드
      • 두 번째 방식에서는 static factory 메서드
  1. public final 필드 + private constructor
    • private 생성자 뿐이므로 클래스가 초기화될 때 만들어지는 인스턴스가 시스템에서 유일
    • 예외: reflection API
      • 생성자에서 두 번째 생성 시 예외 처리
    • 장점
      • 싱글턴 패턴임이 명확
      • 간결함
  2. public static 팩터리 메서드 + private final 필드
    • 이 팩터리 메서드는 항상 같은 객체 참조 반환
    • reflection API 예외는 동일
    • 장점
      • API는 그대로 두고 싱글턴 패턴 아니게 변환 가능. 유연함
      • 제네릭 싱글 팩토리로 만들 수 있음(??)
      • 메서드 참조를 supplier로 사용 가능(??????)
  3. 원소가 하나인 enum type 선언
    • 위 두가지의 문제점
      • 역직렬화할 때 새로운 인스턴스가 만들어질 수 있음
      • 해결 방안: transient 선언 + readResolve 메서드 제공
    • 반면 enum 타입으로 선언하면 직렬화 문제 해결
    • reflection 공격도 막아줌
    • 대개 이 방식이 가장 좋은 방식
      • 하지만 Enum 외 클래스를 상속해야 하는 경우 사용 불가

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

  • static method와 필드만 담은 유틸리티 클래스를 만들고 싶은 경우
    • 쓰임새
      • Math, Arrays처럼 기본 타입 값이나 배열 관련 메서드 모아놓기
      • Collections처럼 특정 인터페이스 구현하는 객체를 생성해주는 static method
      • final class 관련 메서드 모아놓기

문제점

  • 이런 유틸 클래스는 인스턴스 만들어 쓰는 게 목적이 아님
  • 하지만 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자 만들어줌.
    • 하위 클래스를 인스턴스화 할 수도 있음

해결 방법

private 생성자를 추가

  • 부가적으로 상속 불가능하게 하는 효과도 있음

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

  • 많은 클래스가 하나 이상의 자원에 의존
  • static util classs나 singleton으로 구현하는 경우가 있음
    • 하지만 사용하는 자원에 따라 동작이 달라지는 클래스에서는 부적합
      • 자원을 교체하는 메서드를 사용할 수 있지만
      • 오류 나기 쉽고
      • 멀티스레드 환경에서 사용 불가

해결 방안: 인스턴스 생성 시 생성자에 필요한 자원을 넘겨주기

  • 의존 객체 주입의 한 형태
  • 변형: 자원 factory 넘겨주기
    • factory: 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체

단점

  • 큰 플젝에서는 코드 복잡해짐
  • 해결 방안: Spring 등 DI 프레임웤 사용

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

  • 같은 기능의 객체를 매번 생성하는 것보다 객체 하나를 재사용하는 것이 나을 때가 많음
    • 예: String literal
  • Static factory 메서드 제공하는 immutable class에서는 static factory 메서드 사용하면 피할 수 있음
    • 왜냐면 생성자는 매번 새로 만들지만 static factory 메서드는 그렇지 않기 때문
  • 생성 비용이 비싼 객체도 캐싱해 재사용하기
    • 예를 들어 Pattern 인스턴스는 Pattern.compile 사용하기
    • String.matches는 내부적으로 Pattern 객체를 생성한다
    • lazy initialization을 사용할 수도 있겠지만 비추. 성능 향상 그닥…
  • Adapter (view)
    • 작업은 뒷단에 맡기고 자신은 제2의 인터페이스 역할만
    • 예: Map의 keySet
      • Map의 Set 뷰 반환
      • 매변 새로운 set 인스턴스 만들 것 같지만 같은 set 인스턴스를 반환할 지도 모른다(모른다는 무슨 말이지??????)
      • 반환된 객체를 수정하면 다른 모든 객체가 바뀜. 같은 Map 인스턴스를 대변하기 때문
      • 따라서 뷰 객체를 하나만 재사용하는 게 이득임
  • 오토박싱
    • 예: Long에 long을 n번 더하면 불필요한 Long 인스턴스가 n개 만들어짐
    • 되도록 기본 타입을 사용하고 의도치 않은 오토박싱이 없도록 하자

유의사항

  • 요즘의 JVM은 작은 객체의 생성/회수가 큰 부담이 아님
  • 프로그램의 명확성, 간결성, 기능을 위해 만드는 객체는 오히려 좋아
  • 아주 무거운 객체 아니고서야 객체 pool을 만들지 말자
    • 많은 경우 코드가 복잡, 메모리 사용량 증가, 성능 저하

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

  • 자바는 메모리 관리에 신경 안써도 된다는 오해 → 사실이 아님
  • 책의 Stack 예시
    • pop 된 요소
  • 객체 하나를 회수 못하면 걔가 참고하고 또 걔가 참조하는 객체들까지 모두 회수 못함

해법

  • 참조를 다 썼을 때 명시적으로 null 처리하기
    • Stack에서는 pop했을 때 해당 위치의 요소를 null로
    • 장점: 나중에 잘못 접근할 일이 없다 (NPE 처리)
    • 그러나 null 처리 하는 건 예외적인 경우여야 함
  • 가장 좋은 방법은 변수를 유효 scope 밖으로 밀어내는 것
    • 변수의 범위를 최소가 되게 정의한다면 자연스레 해결
  • Stack의 문제점
    • 스택이 자기 메모리를 직접 관리하기 때문
    • GC는 원소가 비활성인지 알 방법이 없음
  • 자기 메모리를 직접 관리하는 클래스는 항상 mem 누수에 주의해야 함
  • 캐시도 누수 주범
    • WeakHashMap: 키를 참조하는 동안만 엔트리를 살릴 수 있음
    • 주기적으로 entry 청소 (ScheduledThreadPoolExecutor 이용)
  • listener, callback
    • 클라가 등록만 하고 명확히 해지하지 않는 경우
    • weak ref로 저장

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

그냥 쓰지 마라

cpp의 destructor와 다르다

  • cpp에서는 특정 객체와 관련된 자원을 회수하는 보편적인 방법이지만
  • java에선 gc가 해줌, 우리에게 작업 요구하지 않음

문제점들

즉시 수행을 보장 x

  • 실행될 때까지 얼마나 걸릴지 알 수 없음
  • gc에 달린 문제, 천차만별

  • 파일 닫기를 여기서 하면 원하는 것보다 많은 파일이 열려 있을 수 있다
  • 특히 finalizer 스레드는 다른 app 스레드보다 우선순위 낮음

실행 여부조차 보장 x

  • (접근할 수 없는 객체에 딸린) 종료 작업을 전혀 실행 못하고 프로그램이 중단될 수 있음
  • db 락을 여기서 관리하면 죽을 수도…

finalizer 동작 중 예외는 무시됨

  • 처리할 작업이 남았어도 그 순간 종료
  • 마무리 덜 된 상태로 마무리 될수도
  • 그리고 이렇게 훼손된 객체를 누군가 사용하려 한다면?

성능 문제

  • AutoClosable, try-with-resources보다 훨씬 느림

보안 문제

  • finalizer 공격
  • 생성자나 직렬화에서 예외 → finalizer 수행…!
    • finalizer 안에서 필드에 자기 참조를 할당하면?
    • gc가 회수 못함
  • 생성자에서 예외만 던지면 되는데 finalizer 있으면 그렇지 않음

그러면 어떻게 할까?

AutoCloseable 을 구현하고 클라에서는 인스턴스 다쓰면 close 호출하기

  • 구체적으로 자신이 닫혔는지 추적하면서 close 내에서는 이 객체가 유효하지 않음 표기
  • 나중에 다시 불리면 예외 처리
  • 주의: 순환 참조 생기지 않게 할 것

cleaner와 finalizer는 왜 있는데?

  1. 자원 소유자가 close를 호출하지 않을 것에 대비한 안전망
  2. native peer
    • 일반 자바 객체가 native method 통해 기능을 위임한 native 객체 (?????)
    • 자바 객체가 아니라 gc가 그 존재를 모름
    • 이 때 cleaner, finalizer가 처리학기 좋음
    • 단 성능 저하 감당하고 심각한 자원을 갖고 있을 때만
    • 이 때도 자원을 즉시 회수해야 하면 close 메서드 쓸 것

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

  • java lib에는 close를 호출해 직접 닫아줘야 하는 게 많음
  • 하지만 앞에서 말했듯 finalizer는 별로임
  • 전통적으로 try-finally가 사용되었음
    • 자원 많아지면 코드 지저분해짐
    • 예외 던져지면 close도 실패
    • 그러면 close의 예외만 남고 앞의 예외 정보는 x

try-with-resources 사용하기

  • AutoCloseable 인터페이스 구현
    • void close() 만 존재
void function() {
	try (Resource1 rc1 = new ...
			 Resource2 rc2 = new ...) {
		// do smthing w/ rc1, rc2
	}
}
  • 이렇게 하면 원래 코드와 close 양쪽에서 모두 예외 발생하면 close 예외는 숨겨지고 원래 코드의 예외가 기록
    • 문제 진단 쉬워짐
  • catch도 사용 가능!

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

우선 하면 안되는 경우

  • 각 인스턴스가 본질적으로 고유한 경우 = 값 표현이 아니라 동작하는 개체를 표현하는 클래스
  • 인스턴스의 logical equality를 검사할 일이 없는 경우
  • 상위 클래스의 equals가 하위 클래스에도 맞는 경우
  • private class나 package-private class = equals 메서드를 호출할 일이 없는 경우

그러면 언제?

논리적 equality를 확인해야 하늗네 상위 클래스의 equals가 재정의되지 않은 경우. 즉 객체가 같은지가 아니라 값이 같은지 확인하고 싶은 경우

  • Map의 키, Set의 원소로도 쓸 수 있음
  • 아이템 1의 인스턴스 통제 클래스나 Enum이라면 재정의할 필요 x
    • 차피 논리적으로 같은 인스턴스가 2개 이상 만들어지지 않음

규약

  • 반사성: x ≠ null → x.equals(x) = true
  • 대칭성: x, y ≠ null && x.equals(y) = true → y.equals(x) = true
  • 추이성: x, y, z ≠ null && x.quals(y) = true && y.equals(z) = true → x.equals(z) = true
    • 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다
    • 추상 클래스라면 가능
    • 상속 대신 컴포지션을 사용하라
      • 추가되는 필드를 private으로 두고 부모 구체 클래스를 반환하는 view 메서드를 두자
      • 단 사용할 땐 주의하기
  • 일관성: x, y ≠null → x.equals(y)는 항상 true or false
    • 수정되지 않았을 경우
    • 불변 클래스라면 결과가 항상 같아야 함
    • equals 판단 기준에 신뢰할 수 없는 자원이 끼면 안됨 (e.g. IP 주소)
    • 메모리에 존재하는 객체만을 사용, deterministic한 계산만 해야 함
  • non-null: x≠null → x.equals(null) = false

좋은 equals 메서드 구현 방식

  • == 연산자로 자기 자신 참조인지 확인
    • 성능 최적화용
  • instanceof로 올바른 타입인지 확인
  • 입력을 올바른 타입으로 형변환
  • 핵심 필드들이 모두 일치하는지 하나씩 검사

주의할 점

  • 필드 비교 순서가 성능에 영향을 미칠 수 있음
    • 다를 가능성이 크거나 비교 cost가 싼 필드를 먼저 비교하자
  • 대칭성, 추이성, 일관성을 만족하는지 확인해보기
    • unit test
  • equals를 재정의한 경우 hashCode도 반드시 재정의
  • Object를 매개변수로 받자
    • 타입을 구체적으로 명시한 경우 이는 재정의(override)가 아니라 오버로딩임
    • 하위 클래스에서 FP를 뱉을 수 있고 보안도 안좋음

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

  • 안그러면 HashMap이나 HashSet에서 문제 생김

규약을 살펴보자

  • equals 비교에 사용되는 정보가 변경되지 않았다면 앱이 실행되는 동안 hashCode 메서드의 반환값은 같아야 함
  • equals가 두 객체 같다고 했으면 hashCode도 같아야 함
  • equals가 두 객체 다르다고 판단했어도 hashCode의 값이 다를 필요는 없음
    • 하지만 달라야 hash table의 성능이 좋아짐

좋은 hashCode 작성하기

  • 각 핵심 필드에 대해 다음 result를 계산

과정

  1. c를 계산
    • primitive 타입이면 Type.hashCode
    • 참조 타입 필드인 경우
      • 클래스의 equals 메서드가 필드의 equals를 재귀적으로 호출해 비교한다면 필드의 hashCode를 재귀적으로 호출
      • 복잡해지면 필드의 표준형을 만들어 표준형의 hashCode를 호출
      • 이 때 null이면 0
    • 배열인 경우 원소 각각을 별도 필드처럼 다루면서 위 두 가지 규칙 적용
      • 핵심 원소가 없는 경우 상수 사용
      • 모두 핵심 원소면 Arrays.hashCode
  2. result = 31 * result + c
  • 그리고 unit test
  • 파생 필드는 계산해서 제외해도 됨
  • equals 비교에 사용되지 않는 필드는 반드시 제거
  • 불변 클래스고 hash code 계산 비용이 크다면 캐싱 고려
  • 성능을 위해 핵심 필드를 생략하면 안됨
    • 해시 품질이 나빠져 hast table 성능 저하
  • 값 생성 규칙을 API 사용자에게 자세히 드러내지 말자
    • 클라가 값에 의지하면 계산 방식 바꾸기 힘듦

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

toString의 규약: 모든 하위 클래스에서 이 메서드를 재정의해라

  • 디버깅하기 쉬움!
  • println, printf, 문자열 +, assert 넘길 때 사용됨
  • 로깅도 쉬움
  • 객체가 가진 주요 정보를 모두 반환하는 게 좋음 (스스로를 완변히 설명하는 문자열)

반환값의 포맷을 문서화할지 정해야 함

  • 값 클래스면 추천 (e.g. 전화번호, 행렬…)
  • human-readable
  • 그대로 입출력에 사용하거나 csv 등으로 저장할 수도
  • 문자열 ↔ 객체 상호 전환할 수 있는 static factory나 생성자 같이 제공하면 좋다!
  • 단점: 시작하면 계속 얽매이게 됨
  • 하든 안하든 의도는 명확히 하자
  • 그리고 toString에 포함된 정보를 얻어올 수 있는 api는 제공하자
    • 안하면 toString 결과를 파싱해야 함…

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

  • Cloneable 인터페이스 → Object의 protected 메서드인 clone의 동작 방식을 결정
    • 이걸 구현, 재정의하는 클래스에서는 public + 반환 타입을 클래스 자신으로 변경하자
    • 먼저 super.clone 호출하고 필요한 필드를 적당히 수정
      • 여기서 적절히 = deep copy, side effect가 없도록
  • Cloneable을 구현한 클래스 사용자는 복제가 제대로 이뤄질 것을 기대한다

주의사항

  • stack

    • 배열의 element를 재귀적으로 clone
  • HashTable (버킷 내 원소들이 linked list)

    • 배열(bucket)뿐만 아니라 linked list도 복사해야 함
  • clone 내에서는 재정의될 수 있는 메서드 호출하면 안됨

    • 하위 클래스가 복제 과정 후 원본과 복제본의 상태가 달라질 수 있음 (흠…..?)
    • final이나 private
  • 재정의할 경우 exception 던지지 마라. 그래야 편함

  • 상속용 클래스에서는 clone 구현하지 말거나 final + throws Exception으로 막아놓기

  • 스레드 안전 클래스의 경우 동기화 필요

  • 근데 clonable은 문제점이 많으니 복사 생성자/팩토리를 제공하는 게 낫다

    • 자신과 같은 클래스 인스턴스를 받는 생성자
    • 인터페이스를 받아서 처리할 수도 있음

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

  • compareTo는 대부분 equals와 비슷하나 몇 가지 차이점
    1. 순서 비교 가능
    2. 제네릭
  • 규약은 거의 equals의 규약(아이템 10)이랑 비슷한데 지키면 좋을 점
    • x.compareTo(y) == 0 ↔ x.equals(y)
  • 규약을 지키지 못하면 비교를 제대로 활용할 수 없을 수 있다(e.g. TreeSet, TreeMap, Collections, Arrays)
    • 얘네는 동치성 비교할 때 compareTo를 써서 엇박 날수도 있다
  • <, > 대신 박싱된 기본 타입 클래스의 compare 사용
  • 핵심적인 필드부터 비교
  • 비교자 생성 메서드를 사용하면 코드가 간결해지지만 성능이 약간 나빠짐(선택)
  • 값의 차이를 그대로 return하면 안됨!
    • overflow
    • 부동소수점 관련 오류
    • 대신 static compare나 비교자 생성 메서드 사용

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

intro

  • 잘 설계된 컴포넌트 = 클래스 내부 테이터와 구현 정보를 외부로부터 잘 숨김
  • 오직 api로만 외부와 소통
  • 개발 속도, 관리 비용, 성능 최적화, 재사용성, 난이도 등에서 장점
  • 자바에서 접근성은 다음에 따라 결정
    • 선언 위치
    • 접근 제한자

원칙

모든 클래스와 멤버의 접근성을 최대한 좁혀야 함

  • top-level class, interface
    • public, package-private
    • 패키지 외부에서 쓸 일이 없다면 package-private
    • 한 번 public으로 가면 계속 호환성 관리해줘야 함
  • 접근 수준
    • private, package-private, protected, public
  • 권한을 자주 풀어줘야 한다면 컴포넌트 분해를 고려하자
  • 제약
    • 상위 클래스 메서드 재정의할 땐 접근 수준을 상위 클래스보다 좁게 설정할 수 없음
    • 리스코프 치환 원칙 때문 (상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체해 사용할 수 있어야 한다)
  • public 클래스의 필드는 되도록 public이 아니어야 함
    • 통제를 잃어버림
    • thread-safe하지 않음
  • 배열도 조심
    • 배열의 내용을 수정 가능하기 때문
    1. 배열은 private, public 불변 리스트를 추가
    2. 배열은 private, 복사본 반환하는 메서드 공개
  • 자바9에선 모듈 시스템 추가
    • 모듈 = 패키지의 묶음 (where 패키지 = class의 묶음)
    • 공개할 패키지 정의 가능
    • protected나 public이어도 패키지를 공개 안했으면 모듈 외부에서 접근 불가능
    • 조심해서 쓰세요

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

  • public 클래스라면 getter, setter 쓰는 게 맞음
  • 근데 package-private 클래스나 private 중첩 클래스는 필드 노출해도 문제 없음
    • 표현하려는 추상 개념만 잘 표현해주면 됨
    • 차피 클라이언트는 패키지 내부(혹은 private 중첩 클래스라면 이 클래스를 포함하는 클래스) 코드이므로 통제 가능
  • public 클래스의 필드가 불변이어도 직접 노출하는 건 나쁜 생각
    • 표현 방식 바꾸려면 api 변경 필요

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

불변 클래스 = 내부 값을 수정할 수 없는 클래스

적용한 패턴 = 함수형 프로그래밍

만드는 규칙

  1. setter 미제공
  2. 클래스 확장 불가능
    • 클래스를 private이나 package-private으로 만들고 public 정적 팩터리 메서드 제공
  3. 모든 필드를 final로
  4. 모든 필드를 private으로
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없게
    • 클라에서 제공한 객체 참조를 가리키거나 이를 그대로 반환 X

장점

  • 단순
  • thread safe
    • 안심하고 공유할 수 있어 재사용 적극 권장
  • 방어적 복사가 필요 없음
    • clone 메서드나 복사 생성자 제공 안하는 게 좋음
  • 불변 객체끼리는 내부 데이터 공유 가능
  • 구조가 복잡해도 불변식을 유지하기 수월
    • Map key나 Set 원소로 쓰기 좋음
  • 실패 원자성 제공
    • 메서드에서 예외가 발생한 후에도 객체는 메서드 호출 전화 같이 유효한 상태여야 함

단점

  • 값이 다르면 무조건 독립된 객체로 만들어야 함
    • 간단한 연산에도 엄청난 시간, 공간복잡도 있을 수도
    • 다단계 연산을 쓰면 해결 가능

대안

  • 모든 클래스를 불변으로 만들 수는 없으니
  • 변경할 수 있는 부분을 최소한으로 줄이자. 가능하다면 private final로

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

상속이 항상 최선은 아니다

  • 대개 코드를 재사용하기 쉽지만 (확장할 목적으로 설계되었거나 문서화도 잘 된 클래스가 아닌) 일반적인 구체 클래스를 패키지 경계를 넘어 상속하면 위험
  • 왜냐면 캡슐화를 깨뜨리기 때문
    • 상위 클래스의 구현 방식에 따라 하위 클래스의 동작에 이상 생길 수 있음
    • 다음 릴리즈에서 상위 클래스 변경되면 갑자기 하위에서 뭔가 깨질 수 있음
    • 그러면 하위 클래스도 수정해야 함…
  • 특히 메서드 재정의
  • 클라가 상위 클래스 메서드를 직접 호출해 불변식 해칠 수 있음

해결 방법: 컴포지션

기존 클래스가 새로운 클래스의 구성 요소로 쓰인다

  • 확장 대신
    • 새로운 클래스를 만들고 (Wrapper class)
    • private 필드로 기존 클래스의 인스턴스를 참조
  • 그럼 새로운 클래스의 메서드는 private 필드에 있는 인스턴스의 메서드 호출
    • forwarding
  • decorator pattern

장점

  • 기존 클래스 내부 구현 방식에 구애받지 않음
    • 메서드가 추가되더라도
  • 상위 클래스 api의 결함이 전파되지 않음
    • 새로운 api 설계하면 그만

단점

  • 콜백 프레임웤과는 상성 안맞음
    • 콜백 내부에서는 wrapper를 모르니 내부 객체를 참조로 넘기고 호출하게 됨

생각해보자

  • (컴포지션 대신) 상속은 하위 클래스가 상위 클래스의 진짜 하위 타입인 상황에서만 쓰여야 함

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

  • 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남기기
    • api 메서드에서 재정의 가능한 메서드를 호출한다면?
      • 순서, 호출 결과가 미치는 영향 등
    • @implSpec
  • 내부 동작 중간에 끼어들 수 있는 hook을 잘 선별해 protected 메서드로 공개
    • 성능 향상 등을 기대할 수 있고 간단해짐
    • 상속의 효과 극대화
    • 실제 하위 클래스를 만들어 시험해보기
      • 제3자가 만들어보기
    • 너무 많지도, 적지도 않게
  • 상속용 클래스 생성자에서는 재정의 가능 메서드를 호출하면 안됨
    • 오작동 가능
    • 왜냐면 상위 클래스 생성자가 하위 클래스 생성자보다 먼저 호출되기 때문
      • 재정의 메서드가 하위 클래스 생성자에서 초기화하는 값에 의존하면 문제!
  • clone, readObject는 재정의 가능 메서드를 호출하면 안됨
    • 역직렬화, 복제를 올바르게 하기 전에 재정의 메서드를 호출하게 됨
  • 상속용으로 설계하지 않은 클래스는 상속 금지하기
    • 금지까지는 아니어도 피하자
    • 인터페이스 쓰자
    • 그래도 해야겠다면 클래스 내부에서 재정의 가능 메서드 호출하지 말고 문서화

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

여러 장점이 있음

  • 추상 클래스는 단일 상속, 인터페이스는 선언된 메서드들을 모두 정의하기만 하면 같은 타입 취급
  • 기존 클래스에 쉽게 구현해넣을 수 있음
    • implements
    • 추상 클래스 끼워넣긴 어려움
  • mixin 정의에 안성맞춤
    • 대상 타입의 주 기능에 더해 선택적 기능을 혼합했다는 뜻
    • 반대로 클래스 계층구조에는 믹스인 삽입할 합리적 위치 없음
  • 계층구조 없는 타입 프레임웤 만들 수 있음
    • 현실에서는 계층을 엄격히 구분하기 어려운 경우도 있음 (가수, 작곡가, 싱어송라이터의 계층을 나눌 수 있나?)
    • 인터페이스들을 조합해 또다른 인터페이스를 만들 수도. 조합의 가능성이 넓어짐!
  • 기능 중 명백한 건 default 메서드로 제공해 편의 제공 가능

템플릿 메서드 패턴

  • 인터페이스와 skeletal impl 클래스를 함께 제공하는 방식으로 두 방식의 장점 모두 취함
  • 역할 분담
    • 인터페이스: 타입 정의, 필요시 default 메서드 (XInterface)
      • 추상 클래스가 타입 정의할 때의 제약으로부터 자유롭게 해줌
    • skeletal impl 클래스: 나머지 메서드 구현 (XAbstractInterface)
      • 구현을 도와줌
  • 이후 skeletal impl 클래스 확장
  • 개발하기 편리
  • 골격 구현 작성
    • (인터페이스) 보고 다른 메서드 구현에 사용되는 기반 메서드 선정
      • 골격 구현에서는 추상 메서드
    • (인터페이스) 기반 메서드를 사용해 직접 구현할 수 있는 걸 모두 default 메서드로
      • equals, hashCode같은 애들 말고
    • (골격 구현 클래스) 기반, default로 만들지 못한 메서드 추가
      • equals, hashCode 정의 필요하면 여기서
    • 이후 골격 구현 상속해서 사용

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

  • java8부터는 default method 추가
  • 하지만 불변식을 해치지 않게 작성하긴 어려움
    • 예를 들어 Synchronized collection
  • 컴파일에 성공해도 기존 구현체에 런타임 오류를 일으킬 수 있음
  • 그러니 기존 인터페이스에 default 메서드 추가는 웬만하면 없어야 함
    • 새로운 인터페이스라면 ㄱㅊ
  • 메서드 제거나 기존 시그니처 수정 용도가 아님
  • 문제 방지법
    • 서로 다른 방식으로 최소 3개는 구현해보기
    • 클라이언트도 여러 개 만들어보기
    • 릴리즈 전에 문제 잡자

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

인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다.

  • 인터페이스의 구현 = 이 인스턴스로 뭘 할 수 있는지 알려주는 것
  • 상수 인터페이스는 안티패턴
    • 상수는 본디 내부 구현인데 이를 노출하게 됨
  • 특정 클래스나 인터페이스에 강하게 연관된 상수라면 그 클래스나 인터페이스 자체에 추가해야 함
    • 열거 타입이나
    • 인스턴스화할 수 없는 유틸 클래스에 담기

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

예시로 사각형이나 원이 될 수 있는 Figure class

  • 태그 달린 클래스엔 쓸데 없는 코드가 많음
    • 가독성도 별로고
    • 메모리도 낭비되는 결과
  • 컴파일러는 별 도움 안되고 런타임에 에러 가능성 농후

대신 subtyping을 활용하자

  • root 추상 클래스 정의
    • 태그 따라 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언
    • 같은 메서드는 루트 클래스의 일반 메서드로 선언
    • 공통 데이터도 루트 클래스로

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

nested class의 종류, 언제 쓸까?

  • static 멤버 클래스
    • 다른 클래스 안에 선언
    • 바깥 클래스의 private 멤버에도 접근할 수 있음
    • 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스
    • 바깥 인스턴스에 접근할 일이 없으면 무조건 static으로 만들자
      • 그렇지 않으면 외부 인스턴스로 숨은 외부 참조를 가지고 시간, 공간 낭비
      • mem leak 가능성
  • (non static) 멤버 클래스
    • 바깥 클래스 인스턴스와 암묵적으로 연결
    • 정규화된 this(OuterClass.this)로 바깥 인스턴스의 인스턴스 메서드나 참조를 가져올 수 있음
    • 바깥 클래스 없이 생성할 수 없음
    • 어댑터 정의할 때 자주 쓰임
      • 인스턴스를 감싸 다른 클래스 인스턴스처럼 보이게
      • 예를 들어 컬렉션 뷰
  • 익명 클래스
    • 쓰이는 시점에 선언과 동시에 인스턴스 생성
    • 비정적 문맥에서만 외부 클래스 인스턴스 참조 가능
    • 길면 가독성 떨어짐
    • 정적 팩토리 메서드 구현할 때
  • 지역 클래스
    • 지역 변수 선언할 수 있는 곳이면 선언 가능 (유효범위도 같음)

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

  • 소스파일 하나에 톱레벨 클래스를 여러 개 선언할 수는 있다
  • 하지만 그럴 이유가 없음
    • 한 클래스를 여러 방식으로 정의할 수 있고
    • 컴파일러에 소스파일 전달하는 순서에 따라 사용하는 클래스가 달라짐

해결 방법

  • 톱레벨 클래스를 다른 소스 파일로 분리
  • 혹은 정적 멤버 클래스 사용하기

참고: 제네릭 관련 용어 정리

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

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

용어 정리

  • 제네릭 클래스/인터페이스 (제네릭 타입)
    • 클래스/인터페이스 선언에 타입 매개변수가 쓰인 것
  • 매개변수화 타입
    • 클래스/인터페이스 이름 다음 꺽쇠 괄호 안에 나오는 타입 매개변수들
    • List<String>String
  • raw 타입
    • 제네릭 타입에서 타입 매개변수를 사용하지 않은 것
    • List<E>List

raw 타입 단점

  • 타입 안정성, 표현력이 사라짐
  • 컴파일할 때 오류가 발생하지 않고 런타임에 오류 발생
  • 그러면 오류가 어디서 발생했는지 알아차리기 힘들 수 있음

특징

  • 컴파일러가 보이지 않는 형변환을 추가

그럼 왜 있나?

  • 호환성
  • raw 타입은 안되지만 List<Object>와 같은 사용은 ㄱㅊ
    • 모든 타입을 허용함을 컴파일러에 명확히 전달
    • 제네릭 하위 규칙 때문이기도
      • List<String>List의 하위 타입
      • List<Object>List 의 하위 타입 X
  • 원소의 타입을 몰라도 되는 경우에는?
    • 비한정적 와일드카드 타입
    • Set<?>

예외

  • class literal
    • List<String>.class 는 안됨
  • instanceof
    • 비한정적 와일드카드 타입 외에는 적용 불가
      • 이것도 굳이? 코드만 지저분해진다
    • 이 경우 명시적 형변환 필요

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

  • 쉬우니까 웬만하면 제거하기
    • 모든 비검사 경고는 런타임에 ClassCastException을 일으킬 수 있음
  • 타입 Safe 하다고 정말정말 확신할 수 있으면 @SupressWarnings("unchecked")
    • 다만 그 범위는 최소로
    • 변수 선언 → 메서드 선언 → 클래스 선언
    • 그리고 안전한 이유를 주석으로 남기기

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

배열과 제네릭 타입의 차이

  1. 배열은 공변, 제네릭은 불공변
    • Sub가 Super의 하위 타입인 경우
    • Sub[]는 Super[]의 하위 타입
    • 하지만 List와 List은 서로 하위 타입도, 상위 타입도 아님
      • 정확히는 어떤 타입 T1, T2에 대해서도 List과 List는 상/하위 타입 관계에 있지 않음
    • 리스트를 사용하면 타입 관련 실수를 컴파일시 알 수 있다
  2. 런타임시 타입 정보
    • 배열은 런타임에도 원소의 타입을 확인
    • 제네릭은 런타임에는 타입 정보가 소거됨
      • 호환성은 good
  • 결론: 배열과 리스트는 상성이 안맞음
  • 제네릭 배열은 생성 불가
    • type-safe하지 않음

    • 구체적 예시, 아래 코드가 실행될 수 있다면? (실제로는 컴파일 안됨)

      List<String>[] stringLists = new List<String>[1];
      List<Integer> intList = List.of(42); // 런타임에 제네릭은 소거, List[]가 되고
      Object[] objects = stringLists;
      objects[0] = intList; // 이게 가능해짐
      String s = stingLists[0].get(0); // ClassCastException!
  • 배열 E[] 대신 컬렉션 List<E>를 사용하자
    • 코드가 좀 길어지고 성능도 좀 나빠질 순 있지만
    • 타입 안정성과 상호 운용성이 좋아진다!

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

  • 제네릭을 안쓴다면 타입 관련 런타임 오류를 마주할 수 있음
    • 클라가 형변환을 해서 써야 한다거나…

어떻게?

elements(배열)를 갖는 스택의 예시

  • 클래스 선언에 타입 매개변수 추가하고 클래스 내부에서 이 타입 사용
    • 보통 E
  • 그런데 타입 매개변수(여기선 E)는 실체화 불가 타입
    • E 타입으로 배열을 만들 수 없음
    • new E[length]는 불가능

해결법

  1. 제네릭 배열 생성 금지 제약을 우회하기
    • Object 배열 생성 → 제네릭 배열로 형변환
      • 그러면 컴파일러 경고
    • 비검사 형변환이 안전하다면 @SuppressWarnings로 경고 숨기기
      • 필드가 private인지
      • 클라나 다른 메서드로 전달되는 일은 없는지 등등
    • 코드상 elementsE[]이지만 런타임 타입은 Object[]
      • 단점: 이 2개가 달라 힙 오염 발생
    • 보통 현업에선 이거 많이 씀
  2. 필드 타입을 Object[]로, 원소를 사용할 때는 E로 형변환
    • 이 경우 API에서 반환값을 형변환할 때 오류 발생
    • 형변환이 안전하다면 @suppressWarnings로 숨기기
    • 단점: 원소를 사용할 때마다 형변환해줘야 함

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

  • 매개변수화 타입을 받는 static util 클래스는 보통 제네릭
  • 예: Set::union
  • 불변 객체를 여러 타입으로 활용할 수 있으려면
    • 객체 타입 바꿔주는 정적 팩터리 만들어야 함
    • 제네릭 싱글턴 팩터리
    • 쓸 일이 있을까…..?
  • 재귀적 타입 한정 (recursive type bound)
    • 자기 자신이 들어간 표현식을 사용해 타입 매개변수의 허용 범위를 한정
    • 예: Comparable 인터페이스
    • String은 Comparable을, Integer는 Comparable를 구현
    • <E extends Comparable> = 모든 타입 E는 자신과 비교할 수 있다
  • 장점: 클라가 형변환해줄 필요가 없음

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

  • 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입 사용하기
class Stack<E> {
	public boolean isEmpty();
	public void push(E e);
	public E pop();

	// 생산자
	public void pushAll(Iterable<? extends E> src) {
		for (E e: src) {
			push(e);
		}
	}
	
	// 소비자
	public void popAll(Collection<? super E> dst) {
		while (!isEmpty()) {
			ds.add(pop());
		}
	}
}

PECS: Producer-Extends, Consumer-Super

// 이렇게 하면 Chooser<Number>의 생성자에 List<Integer>를 넘길 수 있다
public Chooser(Collection<? extends T> choices)
// A->B->C 상속 구조의 클래스이고 Comparable은 B가 구현했고 E=C인 경우를 위해
public static <E extends Comparable<? super E>> E max(List<? extends E> list)
  • 반환 타입에는 한정적 와일드카드 타입 사용하지 말기
    • 클라에서도 와일드카드 써야 함
  • 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하는 것 추천
    public void swap(List<?> list, int i, int j); // public api라면 이 방식이 낫다
    // 얘는 아래로 대체 가능
    public <E> void swap(List<E> list, int i, int j);
    • 단 첫 번재 코드는 helper 함수 필요로 함

      public void swap(List<?> list, ...) {
      	// 이 방식은 컴파일 에러, List<?>에는 null 외의 값을 넣을 수 없기 때문
      	// list.set(i, list.set(j, list.get(i)));
      	
      	swapHelper(list, i, j);
      }
      
      // 이 함수는 와일드카드 타입의 실제 타입을 알려줌
      private <E> void swapHelper(List<E> list, int i, int j) {
      	list.set(i, list.set(j, list.get(i)));
      }
    • (근데 이렇게 할거면 차라리 애초에 타입 매개변수 쓰는 게 낫지 않나??)

      OKKY - [JAVA] 제네릭 질문

      • 즉 메서드 내부에서 타입을 알아야하는경우는 타입파라미터를 쓰고, 타입을 몰라도 되는경우는 와일드카드를 쓰는걸 권합니다.
        • 가령 List를 받아서 그 size를 리턴하는경우엔 List가 어떤 요소를 담고있든 중요하지않죠. 이럴땐 타입파라미터가 아니라 와일드카드를 권합니다.
        • swap도 그러한 경우. 타입이 뭔지는 중요하지 않음
      • 개념적으로는 와일드카드가 맞는데 실제로 와일드카드로 구현하면 기술적 한계때문에 메서드 구현이 안됨
        • 기술쪽을 택하면 타입 매개변수로 구현
        • 개념을 지키면서 기술의 한계를 넘을때 저렇게 헬퍼 메서드를 만들어서 해결

추가

이펙티브 자바, 쉽게 정리하기 - item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라

  • 클래스가 가진 컬렉션 필드에 수용할 때는 많은 정보 중 필요한 정보만 취사선택 하면 된다. (생산자 관점)
    • 정보가 더 많아서 나쁠 게 없다. (상위 타입은 하위 타입을 수용할 수 있다. 상속 덕에 필요한 정보가 이미 다 있다.)
  • 클래스가 가진 컬렉션 필드를 꺼내올 때는 해당 컬렉션이 가진 정보를 받아줄 객체가 필요하다. (소비자 관점)
    • 상위 객체만이 하위 객체를 받아줄 수 있다. (하위 타입은 상위 타입을 수용할 수 없다. 더 많은 정보가 필요하다.)

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

  • 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않음. 예)
    static void dangerous(List<String>... stringLists) {
    	List<Integer> intList = List.of(42);
    	Object[] objects = stringLists;
    	objects[0] = intList; // !!!!
    	String s = stringLists[0].get(0);
    }
  • 근데 앞에서 봤듯이 아래 코드는 컴파일 에러
    List<String>[] stringLists = new List<String>[1];
  • 왜?
    • 제네릭이나 매개변수화 타입의 varargs 매개변수 받는 메서드가 유용하기 때문
  • @SafeVarargs annotation을 사용하면 메서드 작성자가 타입 안정성 보장
    • 정말 확실할 때만, which is
      1. 메서드가 varargs 제네릭 배열에 아무것도 저장하지 않고
      2. 배열 혹은 복제본이 밖으로 노출되지 않는다면 = 신뢰할 수 없는 코드가 배열에 접근 못하면
    • 요약해서 인수 전달의 역할만 한다면
// 위험한 예시
static <T> T[] toArray(T... args) {
	return args; // 힙 오염 발생 지점.
	// 반환하는 배열 타입은 컴파일타임에 결정되는데, 그 시점에는 컴파일러의 정보가 부족해 타입 잘못 판단할 수 있음
}

// 얘의 반환형은 Object[]
static <T> T[] pickTwo(T a, T b, T c) {
	switch(ThreadLocalRandom.current().nextInt(3)) {
		case 0: return toArray(a, b);
		case 1: return toArray(b, c);
		case 2: return toArray(c, a);
	}
	throw new Error();
}

public static void main(String[] args) {
	String[] attributes = pickTwo("이것", "저것", "그것");
	// ClassCaseException 발생. Object[] -> String[]의 형변환 발생하기 때문
}
  • varargs 매개변수 배열을 다른 메서드가 접근해도 되는 경우
    1. @SafeVarargs 로 제대로 annotated된 varargs 메서드에 넘기는 경우
    2. 배열 내용의 일부 함수를 호출만하는 일반 메서드(varargs 받지 않는)에 넘기는 경우
  • 대안: varargs 매개변수 대신 List 매개변수로
    • 그럼 위 예시는 다음과 같이 됨

      static <T> List<T> pickTwo(T a, T b, T c) {
      	switch(ThreadLocalRandom.current().nextInt(3)) {
      		case 0: return List.of(a, b);
      		case 1: return List.of(b, c);
      		case 2: return List.of(c, a);
      	}
      	throw new Error();
      }
      
      public static void main(String[] args) {
      	List<String> attributes = pickTwo("이것", "저것", "그것");
      }

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

public class Favorites {
	// Class<?> => 모든 키가 서로 다른 매개변수화 타입일 수 있다
	// Object => 키와 값 사이의 타입 관계를 보증하지 않음
	private Map<Class<?>, Object> favorites = new HashMap<>();

	public <T> void putFavorite(Class<T> type, T instance) {
		favorites.put(Objects.requireNonNumm(type), instance);
	}

	// favorites에서 꺼낸 직후에는 잘못된 컴파일 타입을 갖고 있음
	// Object -> T로의 타입 변환을 cast 메서드로 동적 변환
	public <T> T getFavorites(Class<T> type) {
		return type.cast(favorites.get(type));
	}
}

f.putFavorite(String.class, "Java");
String favoriteString = f.getFavorite(String.class);
  • 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하는 방식

제약

  • Class 객체를 제네릭으로 넘겨야 함
    • 로 타입으로 넘기면 타입 안전성이 깨짐

      f.putFavorite((Class)Integer.class, "Integer 인스턴스가 아닌데요?"); // 이게 됨
      int favoriteInteger = f.getFavorite(Integer.class);
  • 실체화 불가 타입에 사용 불가
    • String이나 String[]은 저장할 수 있지만 List은 저장 못함
    • 왜냐면 List.class를 사용할 수 없기 때문

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

  • 열거 타입 = 일정 상수 값들을 정의한 다음 그 외의 값은 허용하지 않는 타입
  • int 기반 열거 타입은 단점이 많음
    • 타입 안전 보장 x
    • 서로 다른 열거 타입을 비교해버릴 수 있음
    • 상수값이 바뀌면 클라도 바뀌어야 함
    • 디버깅하기 어려움
  • int 대신 string 쓰는 것도 별로
    • 오타
    • 문자열 비교 → 성능 저하
  • enum 타입을 쓰자!

작동 방식

  • 열거 타입 자체도 하나의 클래스
  • 상수 하나당 인스턴스를 만듦
  • 이걸 public static final 필드로 공개
  • 싱글턴

장점

  • 타입 안전성
  • 상수 추가나 순서 변경해도 ㄱㅊ
  • 디버깅도 쉬움
  • 메서드나 필드 추가 가능, 인터페이스 구현까지도 가능
  • Object 메서드, Comparable, Serializable 구현되어 있음
  • 데이터와 연결 가능
    • 생성자에서 데이터 받고 인스턴스 필드에 저장
  • 상수별 메서드 구현
    • 열거 타입에 추상 메서드 선연

    • 각 상수별 body에서 재정의

      enum Operation {
      	PLUS { public double apply(double x, double y) { return x + y ;} },
      	// MINUS, TIMES ...
      
      	public abstact double apply(double x, double y);
      }
  • 전략 열거 타입 패턴
    • 상수에 따라 전략을 선택하도록 함

    • 예: 요일(상수)별 임금 정산 계산식(전략)

      enum PayrollDay {
      	SUNDAY(WEEKEND), MONDAY(WEEKDAY),
      	// ...
      
      	int pay(int time, int payRate) {
      		return payType.pay(time, payRate);
      	}
      
      	enum PayType {
      		WEEKDAY {
      			int overtimePay(int time, int payRate) { ... }
      		}
      		
      		abstract int overtimePay(int time, int payRate);
      		
      		int pay(int time, int payRate) {
      			return base + overtimePay(time, payRate);
      		}
      	}
      }

결론

  • 필요한 원소들을 컴파일타임에 알 수 있다면 항상 열거 타입을 사용하자

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

  • 열거 타입 상수에 연결된 정수를 얻고 싶을 때 사용하고 싶겠지만
  • 유지보수하기 어려움
    • 상수 선언 변경
    • 같은 값 추가 불가
    • 중간에 값을 비울 수 없음
  • 원래 목적은 EnumSet, EnumMap같이 열거 타입 기반 범용 자료구조에 사용하기 위함
  • ordinal 사용하지 말고 인스턴스 필드에 저장하자

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

  • 비트로 상수들을 관리하면 연산은 효율적으로 수행할 수 있지만 단점이 많음
    • 정수 열거 상수 사용할 때의 문제점에 더해
    • 비트만으로는 해석하기 어려움
    • 순회하기 어려움
    • 최대 몇 비트가 필요한지 알아야 함
  • 대신 EnumSet을 사용하자
  • 열거 타입 상수 값으로 구성된 집합을 잘 표현
  • 다른 Set 구현체와 함께 사용 가능
  • 내부적으로 비트 벡터를 사용해 원소가 64개 이하면 비트 필드에 대등한 성능

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

  • 아이템 35와 같은 이유로 ordinal indexing 사용하면 문제가 많음
    • 비검사 형변환
    • index 자체로는 의미가 없으니 index → label로 변환하는 작업 필요
    • 정수는 type-safe하지 않음 (ArrayIndexOutOfRange…)

EnumMap 을 사용하자

  • enum 타입을 키로 사용
  • (다차원 관계) ordinal 2번? → EnumMap 2개를 중첩해라
    • 예: 물질 상태와 상전이
    • ordinal 쓴다면 상태 하나 추가하려면 배열을 한참 고쳐야 함

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

  • 대개 열거 타입의 확장은 좋은 생각은 아님
  • 근데 어울리는 경우가 하나 있는데 바로 opcode
  • how?
    • Operation interface 정의
    • XOperation 열거 타입이 위 인터페이스를 구현
    • 사용할 때는 Operation 인터페이스를 사용하면 여러 Operation 타입 대체 가능
      • 사용 방법 1

        private static <T extends Enum<T> & Operation> void func(Class<T> opEnumType) { ... }
        
        public static void main() {
        	test(ExtendedOperation.class)
        }
      • 사용 방법 2

        private static void test(Collection<? extends Operation> opSet) {
        	for (Operation op: opSet) { ... }
        }
        
        public static void main() {
        	test(Arrays.asList(ExtendedOperation.values()))
        }
  • 단점
    • 열거타입끼리 구현을 상속할 수 없음
      • 상태에 의존하지 않는 경우라면 default 사용
      • 공유 부분이 많다면 별도 helper class나 static helper 메서드로 분리를 고려

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

  • 명명 패턴은 단점이 많다. 예: 테스트 작성
    • 오타나면 안됨
      • tsetX처럼 오타나면 테스트 X
    • 원하는 프로그램 요소에 대해 동작하지 않을 수 있음
      • 메서드가 아닌 class의 이름에 Test를 붙인다면 동작 안할 것
    • 프로그램 요소를 매개변수로 전달할 벙법이 없음
      • 예외가 던져져야 한다는 걸 메서드 이름에? 뭐가 예외인지 어떻게 알건가?
  • 메타 애너테이션: 애너테이션 선언에 다는 애너테이션
    • @Retention ⇒ 이 애너테이션이 유지되는 범위
    • @Target ⇒ 애너테이션을 달 수 있는 범위
  • marker annotation: 매개변수 없이 단순히 대상에 마킹한다는 뜻
  • 예시
    • @Test 애너테이션
      • 클래스의 의미에 영향을 주지 않음
      • 애너테이션에 관심 있는 프로그램(도구)에 추가 정보를 제공
      • args → testClass → getDeclaredMethods → isAnnotationPresent → invoke
    • 예외를 위한 테스트가 필요하다면 @ExceptionTest 애너테이션을 추가
      public @interface ExceptionTest {
      	Class<? extends Throwable> value(); // 모든 예외, 오류 수용
      }
      • 이제 invoke의 catch 절에서 InvocationTargetException의 cause가 value의 instance면 pass
    • 예외들 중 하나가 발생하는지 확인하고 싶다면 위 value를 배열로 수정
      • 자바 8부터는 @Repeatable 애너테이션과 컨테이너 애너테이션을 이용해 배열이 아니라 여러 번 애너테이션을 달 수도 있음
        • 이 경우 isAnnotationPresent는 컨테이너가 달렸기 때문에 false를 반환하기 때문에 조심
  • 결론: 애너테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다!

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

  • 메서드 선언에만
  • 의미: 상위 타입의 메서드를 재정의했다
  • interface 메서드 재정의할 때도 사용할 수 있음
    • default 메서드 생기면서 달면 좋음
  • 장점
    • 버그 예방
      • 잘못해서 overloading을 한다거나…
      • 이런 경우 Override annotation 달면 컴파일시 실수 잡아줌
  • 예외
    • 구체 클래스에서 상위 클래스 메서드를 재정의할 때는 꼭 달 필요까진 없다
      • 이 경우 구현하지 않은 게 남았다면 컴파일러가 알려줌
    • but, 추상 클래스나 인터페이스에서는 상위 클래스나 인터페이스의 메서드를 재정의하는 모든 곳에 Override 다는 게 좋음

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

마커 인터페이스: 아무 메서드도 담고 있지 않고 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스

  • 예: Serializable
  • 마커 인터페이스의 장점
    • 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있음
      • 컴파일타임에 오류 잡아낼 수 있음
    • 적용 대상을 더 정밀하게 지정할 수 있음
      • 특정 인터페이스를 구현한 클래스에만 적용하고 싶은 마커가 있는 경우
      • 마킹하고 싶은 클래스에서만 인터페이스를 구현/확장하면 됨
  • 단점
    • annotation 시스템의 지원을 받지 못함
    • 마커 annotation과의 차이
  • 그럼 언제 마커 annotation vs 인터페이스?
    • 마커 annotation
      • 모듈, 패키지, 필드, 지역변수 등 클래스/인터페이스 외의 프로그램 요소에 마킹해야 할 때
      • annotation을 활발히 사용하는 프레임웤을 사용 중인 경우
    • 마커 인터페이스
      • 마킹된 객체를 매개변수로 받는 메서드를 작성해야될 경우
        • 그래야 컴파일타임에 오류 잡을 수 있음

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

  • 자바 8 전까지는 함수 객체를 사용해야 했음
    Collections.sort(words, new Comparator<String>(){
    	public int compare(String s1, String s2) {
    		// ...
    	}
    })
    • Comparator 인터페이스는 정렬을 담당하는 추상 전략을 의미
    • 구체적 전략은 익명 클래스로 구현
  • 이제 람다식 쓰자!
    • 추상 메서드 하나짜리 인터페이스들의 인스턴스

      Collections.sort(words, (s1, s2) -> ...)
    • s1, s2, 반환값의 타입은 컴파일러가 타입 추론

      • 상황에 따라 타입 명시해야 할 때도 있음
      • 제네릭은 타입 추론에 사용되기 때문에 여기서 특히 중요
  • enum에서 람다를 인스턴스 필드에 저장해 상수별 동작을 구현할 수 있음
    // before
    public enum Operation {
    	PLUS("+") {
    		public double apply(double x, double y) {
    			return x + y
    		}
    	}
    
    	Operation(String symbol) {
    		...
    	}
    }
    
    ---
    
    // after
    public enum Operation {
    	PLUS("+", (x, y) -> x + y)
    
    	Operation(String symbol, DoubleBinaryOperator op) {
    		...
    	}
    }

주의할 점

  • 이름이 없고 문서화 불가능
    • 따라서
      • 코드 자체로 동작이 명확히 설명되지 않거나
      • 코드 줄 수가 많아지는 경우
    • 지양하기
  • 사용할 수 없는 곳도 있다
    • 람다는 함수형 인터페이스에서만 사용
      • 추상 클래스의 인스턴스를 만들 때 람다를 쓸 수 없음
    • 이런 경우 익명 클래스 사용
  • 자신 참조 불가능
    • 람다에서의 this는 바깥 인스턴스를 가리킴
  • 직렬화는 웬만하면 피하기

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

  • 메서드 참조는 익명 클래스, 람다보다도 간결
  • 매개변수가 늘어날수록 메서드 참조로 줄어드는 코드도 늘어남
  • 때론 람다가 더 간결할 때도 있음
    • 메서드와 람다가 같은 클래스에 있는 경우

참조 유형들

  1. 정적
    • before: map.merge(key, 1, (count, incr) -> count + incr)
    • after: map.merge(key, 1, Integer::sum)
  2. bound 인스턴스 메서드 참조: 수신 객체를 특정
    • before: Instant then = Instant.now(); t -> then.isAfter(t);
    • after: Instant.now()::isAfter
  3. unbound 인스턴스 메서드 참조: 수신 객체를 특정하지 않음
    • before: str -> str.toLowerCase()
    • after: String::toLowerCase
  4. 클래스 생성자
    • before: () -> new TreeMap<K, V>()
    • after: TreeMap<K, V>::new
  5. 배열 생성자
    • before: len -> new int[len]
    • after: int[]::new

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

  • 웬만한 건 표준 함수형 인터페이스에 다 있다
    • java.util.function
    • UnaryOperator, Predicate, Function…
  • type, input, output에 따른 변형들까지 있음
  • 기본 함수형 인터페이스에 primitive type을 boxing해서 넣지 말자
    • 성능 저하
  • 전용 함수형 인터페이스를 만들어야 할 때
    • 자주 쓰이며 이름 자체가 용도를 명확히 설명
    • 반드시 따라야 하는 규약이 있음
    • 유용한 디폴트 메서드 제공 가능
  • 작성할 때는 주의해서 설계하고 interface 임을 명심
    • 애너테이션 (@FunctionalInterface) 잘 붙이기
      • 인터페이스가 람다용으로 설계된 것임을 알려줌
      • 추상 메서드를 오직 하나만 가져야만 컴파일
      • 유지보수할 때 누가 메서드 추가할 가능성 배제
  • 주의할 점: 다중정의
    • 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중정의하면 안됨
      • 클라에서 모호해짐

추가

함수형 인터페이스와 람다


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

  • 스트림 api는 다량의 데이터 처리 작업을 도움
  • 핵심 추상 개념
    • stream = 데이터 원소의 유한/무한 시퀀스
    • stream pipeline: 이 원소들로 수행하는 연산 단계
  • 구성
    • source stream
    • 중간 연산 (intermediate op)
      • 스트림을 변환
    • 종단 연산 (terminal op)
      • 컬렉션에 담기, 특정 원소 선택 등등
      • lazy evaluation
        • 종단 연산 없으면 아무 일도 하지 않음 (== no-op)
  • 병렬 처리 가능하나 효과 볼 수 있는 경우 거의 없음
  • 주의
    • 잘못(과다) 사용하면 읽기 어렵고 유지보수하기 어려워짐
    • 람다 매개변수명을 잘 지어야 이해하기 쉽다
    • 사실상 final인 변수만 읽을 수 있고 지역변수 수정은 불가능
    • return/break/continue, exception 처리 등이 불가능
  • 쓰기 좋은 경우
    • 시퀀스를 일관되게 변환
    • 필터링
    • 하나의 연산을 사용해 결합
    • 컬렉션에 모으기
    • 특정 조건 만족하는 원소 찾기
  • 쓰기 힘든 경우
    • stage별 각 단계에서의 값에 동시 접근해야 하는 경우
  • 뭐가 나은지 모르겠으면 둘 다 써보고 괜찮은 걸로

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

  • 스트림의 핵심 = 계산을 transformation으로 재구성하는 것
    • 각 변환 단계는 순수 함수여야 함
    • 즉, 스트림 연산에 건네진 함수 객체는 side effect가 없어야 함
  • forEach연산은 스트림 계산 결과를 보고할 때만 사용하고 계산에는 사용하지 말기
  • collector는 스트림에 필수적인 개념
    • toList, toSet, toCollection, toMap, groupingBy, joining…
  • toMap의 옵션들
    • 각 원소가 고유한 key에 매핑된 경우
    • 병합 함수 제공
    • 연관 원소들 중 하나 선택
    • 마지막 값 선택
  • groupingBy
    • 입력으로 classifier
    • 출력으로 원소들을 카테고리별로 모은 맵을 담은 수집기
    • 리스트 외의 값을 갖는 맵을 생성하려면 downstream 수집기 명시
      • 해당 카테고리의 모든 원소를 담은 스트림으로부터 값을 생성

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

  • 스트림은 iteration을 지원하지 않아서 스트림과 반복을 잘 조합해야 함
  • Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함하고 동작하지만
    • Iterable을 확장하지 않는 게 문제
  • 어댑터 메서드 사용할 수 있음 (1)
    public static <E> Iterable<E> iterableOf(Stream<E> stream) {
    	return stream::iterator;
    }
    
    for (ProcessHandle p: iterableOf(ProcessHandle.allProcesses())) {
    	// do smthing
    }
  • 원소 시퀀스를 반환하는 public API의 반환 타입은 Collection 및 그 하위 타입으로 하는 게 최선
    • Collection 인터페이스는 Iterable의 하위 타입이고 stream 메서드도 제공하기 때문
  • 하지만 큰 시퀀스를 메모리에 올리면 안됨
    • 간결히 할 수 있다면 전용 컬렉션 구현을 검토해보기 (2)
      • AbstractCollection 등 활용

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

  • java는 동시성 프로그래밍 잘 지원하지만
    • 병렬 스트림 파이프라인을 사용하려면 주의해야 함
  • 다음의 경우 병렬화로 성능 개선 기대 못함
    • 데이터 소스가 stream.iterate이거나
    • 중간 연산으로 limit 쓰는 경우
      • 이 경우 필요없는 연산 할 수 있음 (일단 해놓고 버림)
  • 언제 좋은가?
    • 스트림 소스가 다음일 경우
      • ArrayList, HashMap, HashSet, ConcurrentHashMap, 배열, int, long
    • 데이터를 원하는 크기로 쉽게 나눌 수 있기 때문
    • 또 locality 높음
  • 종단 연산도 중요
    • 종단 연산 작업량의 비중이 높다면 성능 향상 기대하기 힘듦
    • 가장 적합한 건 reduction
  • 잘못된 병렬화는
    • 성능 저하, 잘못된 결과, 예상치 못한 동작 야기
    • 함수 객체의 규약을 잘 지켜야 함
  • 성능 향상 확인법
    • 원소 수 * 원소당 수행되는 코드 줄 수 > 수십만인가?
  • 검증
    • 변경 전후 운영 시스템과 비슷한 환경에서 성능 테스트해보기
    • 시스템과 같은 스레드 풀을 사용하기에 다른 부분에 영향 가지 않는지도 봐야 함

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

  • 메서드와 생성자 매개변수의 제약은 반드시 문서화하고 메서드 몸체 시작 전에 검사하기
    • 잘못된 값에 대해 깔끔히 예외 던질 수 있음

검사 못했을 경우 문제점

  • 메서 수핸 중간에 모호한 예외를 던져야 함

  • 메서드가 수행되고 잘못된 결과 반환

  • 특정 객체를 이상한 상태로 만들어 나중에 메서드와 관련 없는 오류를 만듦

  • public, protected 메서드는 매개변수의 제약과 잘못됐을 때 던지는 예외를 문서화 해야함

    • @throws 태그
  • requireNonNull 메서드도 유용함

  • public, protected가 아니면 우리가 매개변수 잘 넘겨줘야 함

    • 이 때는 assert 사용
      • 런타임에 효과, 성능 저하 없음
  • 나중에 사용하기 위한 매개변수는 더욱 신경쓰기

    • 디버깅 어려워질 수 있음
  • 클래스 생성자의 경우 클래스 불변식을 위해 필요

  • 예외

    • 검사 비용이 높을 때
    • 계산과정에서 암묵적으로 수행될 때
      • e.g. Collections.sort

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

  • 클라이언트를 믿지 말고 방어적으로 프로그래밍하기
  • 흔한 실수: 필드 자체는 private final이지만 외부에서 설정한 필드 인스턴스를 직접 수정하는 경우
    • 이 경우 매개변수들을 방어적 복사하기
    • 그리고 이 복사본으로 유효성 검사
  • 매개변수가 외부에서 확장될 수 있는 타입이면 복사본 만들 때 clone 사용하면 안됨
  • getter의 경우 방어적 복사본 반환하기
    • 이 경우 clone 사용해도 무방
    • 혹은 불변 뷰를 반환
  • 클라가 제공한 참조를 내부에 보관하는 경우 이 참조(객체)가 잠재적으로 변경될 수 있는지 따져보기
    • 확신할 수 없다면 복사본을 저장하자
  • 고로, 불변 객체들을 조합해 객체를 구성해야 방어적 복사 할 일이 줄어든다
  • 복사를 생략해도 될 때
    • 클래스와 클라가 상호 신뢰할 수 있을 때
    • 불변식이 깨져도 영향이 호출한 클라로 한정될 때

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

API 설계 요령들!

  • 메서드 이름 신중히 짓기
    • 표준 명명 규칙
      • 이해할 수 있고 같은 패키지 내 다른 메서드와 일관되게
    • 커뮤니티에서 널리 받아들여지는 이름
    • 긴 이름 피하기
  • 편의 메서드를 너무 많이 만들지 않기
    • 많으면 구현측도, 클라측도 어려워짐
  • 매개변수 목록은 짧게 유지
    • 4개 이하 추천
    • 같은 타입 매개변수가 연달아 나오는 건 피하기
      • 실수로 순서 바꾸기 좋음
    • how?
      1. 여러 메서드로 쪼개기
      2. 매개변수 여러 개를 묶는 도우미 클래스
        • 보통 정적 멤버 클래스
      3. 빌더 패턴 응용
        • setter 메서드로 매개변수 설정
        • execute 메서드에서 유효성 검사
        • 계산 수행
  • 매개변수의 타입으로는 클래스보다는 인스턴스
    • 유연해짐
    • 클래스를 사용하면 클라가 제한됨
    • 또 특정 구현체로 옮겨담느라 비용 증가할 수 있음
  • boolean보다는 원소 2개짜리 enum 쓰기
    • 가독성 증가
    • 나중에 변경 쉬움

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

  • 재정의한 메서드는 동적으로 선택되고 다중정의한 메서드는 정적으로 선택됨
  • 고로 다중정의가 혼동을 일으키는 상황은 피하기
  • 매개변수 수가 같은 다중정의는 피하기
    • varargs를 사용한다면 아예 다중정의 하지 말기
  • 다중정의보다는 메서드 이름 다르게 짓는 게 낫다
  • 생성자의 경우 2개 이상인 경우 무조건 다중정의가 됨
    • 하지만 정적 팩터리라는 대안이 있음
  • 매개변수 중 하나 이상이 근본적으로 다른 경우(=서로 형변환 불가능한 경우) 헷갈릴 일이 없음
    • 이러면 런타임 타입으로면 결정 가능
  • 참조 메서드와 호출 메서드 다 다중정의 되어있으면 대환장파티
    • resolution 알고리즘에 따라 결정
  • 헷갈릴만한 매개변수는 형변환해서 정확한 다중정의 메서드 사용하도록 하기
  • 혹은 같은 객체를 입력받는 다중정의 메서드들이 모두 동일하게 동작하게 하기

추가

005. LSP와 Generic, 오브젝트 4/6


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

  • varargs 메서드는 명시한 타입의 인수를 0개 이상 받을 수 있음
    • 배열로 전달됨
  • 인수의 개수를 제한(1개 이상이라던가)해야할 수도 있는데 length로 확인하는 방법은 문제가 있음
    • 컴파일타임이 아닌 런타임에 실패하게 됨
  • 이보다는 매개변수 2개 받기
    • int min(int firstArg, int... remainingArgs)
  • 성능에 민감하다면 다중정의 사용하는 걸 고려

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

  • 보통 값이 없으면 null을 반환할 때가 많음
    • 하지만 이러면 클라가 null을 처리하는 로직 추가해야 함
  • 틀린 속설: null 말고 빈 컨테이너 반환하면 성능이 저하된다?
    • 이는 보통 수준에서 신경쓸 수준이 못됨
    • 빈 컬렉션/배열은 굳이 새로 할당하지 않고도 반환할 수 있음
  • (가능성은 적지만) 성능 저하가 있다면?
    • 불변인 빈 컬렉션/배열 선언해두고 반환
    • 이 경우 실제 성능 개선이 있는지 확인하기

아이템 55: Optional 반환은 신중히 하라

  • Optional 등장 이전 반환이 불가능한 경우 가능했던 방법
    • 예외 → 진짜 예외적인 경우가 아님, 예외 생성 비용 문제
    • null 반환 → 별도 처리해줘야 하고 안할 경우 NPE
  • Optional을 반환하는 메서드에서는 절대 null 반환하지 말기
  • Optional 반환 선택 기준
    • 검사 예외의 취지 생각
    • 반환값이 없음을 API 사용자에게 알려줌
  • 결과가 없을 수 있으며 클라이언트가 이 상황을 특별하게 처리해야 할 경우 사용하자

주의할 점

  • isPresent 사용은 신중히
    • 웬만한 작업을 할 수 있지만 대부분 적당한 메서드로 대체 가능
  • 컨테이너 타입(컬렉션, 스트림, 배열, 옵셔널…)은 옵셔널로 감싸면 안됨
    • 이보다는 빈 컨테이너를
  • 박싱된 기본 타입을 담은 옵셔널보다는 전용 옵셔널 (OptionalInt, OptionalLong, OptionalDouble…)
  • 옵셔널을 컬렉션의 키/값, 배열의 원소로 사용하는 게 적합한 경우는 거의 없음
  • 인스턴스 필드로 사용?
    • 이보다는 선택적 필드를 추가한 하위 클래스를 분리

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

  • Javadoc은 설명을 추려 API 문서로 변환해줌

  • API를 올바로 문서화하려면 공개된 모든 class, interface, 메서드, 필드 선언에 문서화 주석을 달아야 함

    • contructor에는 못 단다
    • 공개되지 않은 친구들도 주석 달면 좋다
  • HTML 태그도 알아들음

  • 첫 문장은 해당 요소의 summary로 간주

    • 주어가 없는 동사구로
  • 메서드 문서화 주석

    • 메서드와 클라 사이의 규약을 명료히 기술해야 함
    • 상속용이 아니라면 무엇을 하는지를 기술
      • 상속용인 경우 올바로 재정의하는 방법 명시
    • precondition, postcondition
    • 부작용
  • 제네릭 타입이나 제네릭 메서드를 문서화할 때는 모든 타입 매개변수에 주석 달기

  • 열거 타입을 문서화할 때는 상수들에도 주석 달기

  • 애너테이션 타입을 문서화할 때는 멤버들에도 주석 달기

  • 스레드 안전 수준을 API 설명에 반드시 포함

  • 직렬화할 수 있는 클래스라면 직렬화 형태도 기술

  • 메서드 주석을 삭속시킬 수도 있음

    • 이 때 인터페이스가 우선
  • 복잡한 경우 설명 문서 링크를 제공할 수도


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

  • 가장 좋은 방법: 가장 처음 쓰일 때 선언하기
  • 거의 모든 지역변수는 선언과 동시에 초기화
    • 초기화에 필요한 정보가 부족하다면 충분해질 때까지 선언 미루기
  • for문은 범위 최소화를 잘해줌
    • while보다는 for문 쓰자
  • 메서드를 작게 유지하고 한 가지 기능에만 집중하는 것도 좋은 방법 == 기능별 메서드 분리

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

  • iterator나 인덱스 변수가 의미 없는 경우가 있음 (원소만 필요한 경우)
  • for (Element e: elements)
  • Iterable 인터페이스를 구현한 객체라면 모두 사용 가능

하지만 사용할 수 없는 경우

  • 파괴적 필터링(destructive filtering)
    • 컬렉션을 순회하며 선택된 원소를 제거하는 경우
    • 자바 8부터는 removeIf 메서드 제공
  • 변형(transorming)
    • 리스트나 배열을 순회하며 원소의 값 일부나 전체를 교체하는 경우
  • 병렬 반복(parallel iteration)
    • 여러 컬렉션을 병렬로 순회하는 경우

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

  • (예) 랜덤 숫자 생성기를 직접 구현할 경우 문제가 많다
    • 이럴 때 쓰라고 Random.nextInt가 준비되어 있다
  • 장점
    • 표준 라이브러리를 쓰면 전문가의 지식과 선배 프로그래머의 경험을 활용할 수 있다!!
    • 핵심 로직 개발에 집중 가능
    • 따로 노력하지 않아도 성능이 지속해서 개선됨
    • 기능의 추가
    • 다른 개발자들이 쉽게 이해할 수 있음
  • 특히 java.lang, java.util, java.io 및 하위 패키지들

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

  • float, double은 근사치로 계산하도록 설계됨
  • 금융 관련 계산이랑은 안맞음
    • 대신 BigDecimal, int, long 사용하기
  • BigDecimal 단점
    • 쓰기 불편, 느림

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

  • 기본 타입과 참조 타입을 크게 구분하지 않고 사용할 수 있게 됐지만 차이가 없는 것은 아님
    1. 박싱 타입에는 identity가 있음
    2. 박싱 타입은 nullable
    3. 기본 타입이 시간과 메모리 사용에 있어 효율적
  • 주의할 점
    • 박싱 타입에 == 쓰면 오류 발생
    • 기본, 박싱 타입 홍뇽하면 박싱이 자동으로 풀리고
      • 이 때 null을 언박싱하면 NPE
    • 박싱, 언박싱이 자주 일어날수록 성능 저하
  • 박싱 타입은 언제 쓰나?
    • 컬렉션의 원소, 키, 값
    • 매개변수화 타입이나 매개변수와 메서드의 타입 매개변수
    • 리플렉션으로 메서드 호출할 때

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

  • 다른 값 타입을 대신하기 부적합
    • 예를 들어 키보드 입력을 받을 때 숫자, boolean 등이라면 적절하게 타입 변환해서 사용해라
  • 열거 타입 대신하기 부적합
  • 혼합 타입을 대신하기 부적합
    • delimeter로 join한다거나…
    • delimeter가 값에 포함된 경우 문제
    • 파싱→ 느림, 귀찮음, 오류 가능성
  • 권한 표현에 부적합
    • 예: 한 스레드에서 사용해야 하는 value의 key
      • 실수로 같은 값을 사용한다거나 할 수 있음
      • 보안 취약

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

  • 문자열 연결 연산자로 문자열 n개를 이으면 n^2의 시간
    • 양쪽 모두를 복사해야 하기 때문
  • 성능 저하가 우려되면 StringBuilder 사용하기

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

  • 앞서 아이템 51에서 매개변수 타입으로 클래스 말고 인터페이스 쓰라고 했는데 이를 확장해서,
  • 적합한 인터페이스만 있다면 매개변수는 물론 반환값, 변수, 필드를 모두 인터페이스 타입으로 선언해라
    • 실제 클래스 사용은 only when 생성자로 생성할 때
  • 유연해짐!
    • 생성자나 정적 팩터리만 갈아끼우면 됨
  • 주의할 점
    • 클래스가 interface 규약 외에 특별한 기능을 제공하고 이를 쓴다면 바꿀 애도 이걸 제공해야 함
  • 적합한 게 없다면 클래스로…
    • cases
      • String, BigInteger 등
      • 프레임워크 객체
        • e.g: java.io.OutpuStream
      • interface에 없는 특별한 메서드를 제공하는 클래스들
        • PriorityQueue::comparator
    • 하지만 이렇다 하더라고 가장 덜 구체적인, 상위의 클래스 타입을 사용해라

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

  • 리플렉션을 이용하면
    • 프로그램에서 임의의 클래스에 접근 가능
    • Class 객체의 Constructor, Method, Field 인스턴스를 가져올 수 있고
    • 클래스의 멤버 이름, 필드 타입, 메서드 시그니처까지도 가져올 수 있음
    • 심지어 조작도 가능
      • 인스턴스 생성, 메서드 호출, 필드 접근
    • 컴파일시 존재하지 않던 클래스도 이용 가능

단점

  • 컴파일타임 타입 검사 이점 못누림
    • 예외 검사 포함
    • 잘못하다 런타임 오류
  • 지저분한 코드
  • 성능 떨어짐

장점 누리기

  • 아주 제한된 형태로만 사용해야 이점 취할 수 있음
    • 컴파일타임에 이용할 수 없는 클래스를 사용해야 하는 경우
      • 명령줄 인수로 정해진다거나…
    • 이 경우 생성에만 리플렉션 사용하고
    • 인터페이스나 상위 클래스로 참조해 사용해라
    • Set의 정확한 클래스를 인수로 받고 인수들을 이 set에 추가하는 경우
    • 이 예시에서 리플렉션의 단점들 확인 가능
      • 런타임에 별의 별 예외 던질 수 있음
      • 장황한 코드
        • 인스턴스 생성을 위해 무려 25줄의 코드가 필요…
  • 또다른 유용한 경우
    • 런타임에 존재하지 않을 수 있는 다른 클래스, 메서드, 필드와의 의존성 관리
      • 버전이 여러 개 존재하는 외부 패키지 다룰 때
      • 가장 낮은 버전 기준으로 컴파일하고 이후 버전의 클래스나 메서드는 리플렉션으로 접근
    • 다만 이 경우에도 런타임에 존재하지 않을 수 있다는 걸 고려해야 함

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

  • JNI: Java Native Interface
    • C, cpp처럼 네이티브 PL로 작성된 메서드를 호출할 수 있게 해줌
  • 쓰임새
    • 레지스트리같은 플랫폼 특화 기능 사용
      • 점점 자바에 흡수되는 중
    • 네이티브 코드로 작성된 기존 라이브러리 사용
      • 대체할 만한 게 없으면 쓸 수밖에…
    • 성능에 영향 주는 부분만 성능 개선을 위해
      • 권장하지 않음
  • 성능 개선?
    • 대부분의 작업에서 자바는 이미 충분한 성능 보여줌
    • 다만 아닐 수도 있음 (e.g. BigInteger는 GMP의 성능이 꾸준이 이뤄졌다 함)
  • 단점
    • 메모리 훼손 오류
    • 낮은 이식성
    • 어려운 디버깅
    • GC가 회수 못하고 추적도 못함
    • context switch(라고 해도 되려나?)마다 cost
    • 네이티브와 자바 사이의 glue code
      • 귀찮고 가독성 떨어짐

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

  • 전문가가 아니라면 웬만하면 하지 마라
    • 섣불리 하다가 해로운 결과로 이어지기 쉬움
  • 차라리 견고한 구조를 위해 노력해라
  • 설계 단계에서 성능을 염두에 두긴 해야 함
    • 성능을 제한하는 설계 피하기
      • 컴포넌트끼리, 외부 시스템과 소통 방식
    • API 설계시 성능에 주는 영향을 고려하기
      • public 가변 데이터는 불필요한 방어적 복사를 유발
      • 컴포지션 대신 상속을 쓰면 성능 제약까지 물려받음
      • 인터페이스 대신 구현 타입을 사용하면 나중에 성능 개선된 구현체가 나와도 갈아끼우기 어려움
      • 그렇다고 성능을 위해 API를 왜곡하면 안좋은 생각
  • 잘 설계한 프로그램을 완성한 후에나 최적화를 고려해볼만
  • 최적화 시도한다면 전후 성능을 측정해라
    • profiling tool

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

  • 대부분 자바 언어 명세에 잘 기술되어 있음
  1. 철자
    • 패키지, 클래스, 인터페이스, 메서드, 필드, 타입 변수의 이름을 다룸
    • 패키지, 모듈
      • 각 요소를 점으로 구분해 계층적으로
      • 소문자나 숫자
      • 외부 공개 패키지라면 도메인 이름 역순
      • 짧은 단어나 약어
    • 클래스, 인터페이스
      • 대문자 시작
      • 널리 통용되는 줄임말(e.g. min, max…) 아니라면 줄여 쓰지 않도록 함
    • 메서드, 필드
      • 대문자 시작만 빼면 클래스/인터페이스와 동일
      • 첫 단어가 약자라면 단어 전체가 소문자
      • 단, 상수 필드는 모두 대문자+밑줄 구분 (e.g. NEGATIVE_INFINITY)
    • 지역 변수
      • 약어 써도 됨
    • 타입 매개변수
      • 한 문자
      • T (general), E (Collection), K/V (맵의 키/값), X (예외), R (메서드 반환 타입)
      • 그 외에는 T, U, V 등
  2. 문법
    • 클래스
      • 단수 명사/명사구(e.g. PriorityQueue)
      • 단 객체를 생성할 수 없는 클래스는 복수형 명사로
    • 인터페이스
      • 클래스와 비슷하거나
      • ~able, ~ible의 형용사
    • 메서드
      • 동사구 (e.g. append)
      • boolean 값 반환하면 is~, has~
      • 특별 case
        • 객체 타입을 바꿔서 또다른 객체를 반환하는 인스턴스 메서드의 경우
          • to*Type* 형태로
        • 객체 내용을 다른 뷰로 보여주는 경우
          • as*Type* 형태

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

  • 예외 기반의 프로그래밍 하지 마라…
    • 예: ArrayIndexOutOfBoundsException 뜰 때까지 순회하기(;;;;)
      • 오히려 더 느림
    • 가독성 떨어지고 성능을 떨어뜨림
    • 제대로 동작하지 않을 수 있음
  • 잘 설계된 API는 클라가 정상적인 제어 흐름에서 예외를 사용할 일이 없어야 함
  • 상태 의존적 메서드를 제공하는 클래스는 상태 검사 메서드도 함께 제공해야 함
    • e.g. Iterator 인터페이스의 nexthasNext
  • 혹은 올바르지 않은 상태일 때 빈 옵셔널이나 null 반환
  • 언제 무엇을?
    • 옵셔널이나 특정 값
      • 여러 스레드가 동시 접근 가능 or 외부 요인으로 상태 변화 가능한 경우
        • 상태 검사와 메서드 사이에 변화 가능하기 때문
      • 성능이 중요한 상황에서 상태 검사와 메서드 사이에 중복된 일을 수행하는 경우
    • 상태 검사 메서드
      • 위 경우를 제외한 대부분의 경우

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

  • 자바는 throwable로 3가지 제공: 검사 예외, 런타임 예외(비검사), 에러(비검사)

검사 예외

  • 호출하는 쪽에서 복구할 것(복구 가능한 조건)이라 여겨지는 상황
  • 검사 예외를 던지면 caller가 catch로 처리하거나 밖으로 전파하도록 강제
  • API 사용자에게 회복을 요구하는 셈

런타임 예외

  • 프로그래밍 오류
  • 대부분 클라가 API 명세의 제약을 지키지 못했다는 뜻
  • “복구 가능한가?”는 API 설계자의 판단에 따라
    • 애매하다면 비검사 예외를 선택하는 쪽이 낫다

에러

  • JVM이 자원 부족, 불변식 깨짐 등으로 더이상 수행을 계속할 수 없는 상황

  • 직접 구현하는 비검사 throwable은 모두 RuntimeException의 하위 클래스여야 함

    • Error 상속하지 말기
  • 예외도 객체다!

    • 오류 복구를 위한 정보를 잘 담자
    • 안그러면 메세지를 파싱해야 하는데 매우 안좋은 습관…

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

  • 검사 예외를 제대로 사용하면 api와 프로그램의 질을 높일 수 있음
    • 발생한 문제를 프로그래머가 처리해 안전성을 높이게 해줌
  • 하지만 api 사용자에게 부담
    • 처리하거나 전파해야 함
    • 스트림 안에서 직접 사용할 수 없음
  • 잘 사용될 수 없는 경우라면 비검사 예외 사용하는 게 좋음
    • 잘 사용될 수 없는 경우란
      1. api를 제대로 사용해도 발생할 수 있거나
      2. 프로그래머가 의미 있는 조치를 취할 수 있는 경우
  • 기존에 검사 예외가 있던 경우에 추가하는 건 별 일 아니지만 단 하나의 검사 예외는 부담이 훨씬 큼
    • try블록 추가…
    • 스트림에서 사용 중인지…

회피 방법

  • 적절한 옵셔널 반환
    • 단점
      • 예외와 관련된 부가적인 정보를 담을 수 없음
  • 메서드를 2개로 쪼개 비검사 예외로 바꾸기

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

  • 재사용성을 프로그래밍에서 중요
    • 예외도 마찬가지
  • 표준 예외 재사용의 장점
    • 다른 사람이 익히고 사용하기 쉬움
    • 예외 클래스가 적을수록
      • 메모리 사용량이 적고
      • 클래스 적재 시간도 줄어듦

예시

  • IllegalArgumentException
    • caller가 인수로 부적절한 값을 넘겼을 때
  • IllegalStateException
    • 대상 객체의 상태가 호출된 메서드를 수행하기 적합하지 않을 때
    • e.g. 제대로 초기화되지 않은 객체를 사용하려 할 때
  • ConcurrentModificationException
    • 단일 스레드에서 사용하려 설계한 객체를 여러 스레드가 동시에 수정하려 할 때
    • 문제가 생길 “가능성” 정도를 알려줌
  • UnsupportedOperationException
    • 요청한 동작을 대상 객체가 지원하지 않을 때
  • 그 외에도 NullPointerException, IndexOutOfBoundsException, ArithmeticException, NumberFormatException 등등…

주의사항

  • Exception, RuntimeException, Throwable, Error는 직접 재사용하지 말기
    • 포괄적이라 안정적으로 테스트할 수 없음
  • api 문서를 참고해 어떤 상황에서 던져지는지 확인하기
  • 이름 뿐만 아니라 예외가 던져지는 맥락도 부합할 때 재사용하기
  • IllegalState, IllegalArgument 중 뭘 쓸지 헷갈린다면?
    • 인수 값이 무엇이었든 어차피 실패했을 것 → IllegalStateException
    • 그렇지 않은 경우 → IllegalArgumentException

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

  • 상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 함
    • 그렇지 않으면
      • 내부 구현 방식을 외부에 드러냄
      • 변경 후 릴리즈하면 클라 깨질 수 있음
  • 번역할 때 저수준 예외가 디버깅에 도움이 된다면 exception chaining 사용하기
    try {}
    catch(LowerLevelException cause) {
    	throw new HigherLevelException(cause);
    }
  • 최고의 방법은 저수준 메서드가 반드시 성공하게(예외가 발생하지 않게) 하는 것
  • 차선책: 저수준 예외를 피할 수 없다면 상위 계층에서 예외를 조용히 처리, caller에게 전파하지 앟게 하는 방법
    • 이 경우 로그 정도는 찍어 조치 취할 수 있게

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

  • 예외는 메서드를 올바르게 사용하는 데 매우 중요한 정보
    • 그러니까 문서화 잘하자
  • 검사 예외는 항상 따로따로 선언하고 각 예외가 발생하는 상황을 javadoc의 @throws 태그로 정확히 문서화하기
    • 공통 상위 클래스 하나로 묶어 선언하지 말기
      • e.g. Exception, Throwable을 던진다고 선언하지 말기
        • 근데 main은 ㄱㅊ (JVM만 호출하기 때문)
  • 비검사 예외도 문서화해두면 좋다
    • 특히 인터페이스 메서드
    • 조건이 인터페이스의 일반 규약에 속하게 되고 모든 구현체가 일관되게 동작하게 해줌
    • 단 비검사 예외는 메서드 선언의 throws 목록에 넣지 말기 (@throws 태그 문서화는 하되)
      • 사용자가 할 일이 달라지기 때문
      • 시각적으로 구분할 수도 있음 (”아 메서드 선언에 없고 @throws 문서에는 있으면 비검사 예외구나~”)
  • 클래스의 여러 메서드가 같은 이유로 같은 예외를 던진다면 클래스 설명에 추가하는 방법도 있음

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

  • 예외의 toString 메서드에 실패 원인에 대한 정보를 최대한 많이 담아야 함
    • 사후 분석, 수정을 위해
  • 실패 순간을 포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 함
  • 최종 사용자에게는 친절한 안내 메시지, 예외 메시지는 가독성보다는 담긴 내용이 더 중요
  • 메시지 템플릿 만들어놓고 예외 생성자에서는 필요한 정보만 받는 것도 생각해볼만 함
    • e.g. IndexOutOfBoundsException 예외의 경우 (int lowerBound, int upperBound, int index)

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

  • 호출된 메서드가 실패하더라도 해당 객체는 호출 전 상태를 유지해야 함
  • 예외 발생했을 때 객체 상태가 메서드 호출 전과 달라진다면 실패 시 객체 상태를 api 설명에 명시하기

how?

  • 불변 객체로 설계
  • 작업 수행 전 매개변수의 유효성 검사하기
  • 실패 가능성이 있는 코드는 객체의 상태를 바꾸는 코드보다 앞에 위치시키기
  • 객체의 임시 복사본에서 작업하고 성공하면 원래 객체와 교체하기
  • 실패를 가로채는 복구 코드 작성, 작업 전 상태로 되돌리기 (잘 안쓰임)
    • durability를 보장해야 할 경우

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

  • catch 블록을 비우지 말기
    • 검사/비검사 예외 모두 적용
  • 무시해야 할 때도 있긴 있음
    • FileInputStream 닫을 때
    • 읽기만 했으므로 ㄱㅊ
    • 로그를 남기는 정도?
  • 그러나 무시하는 경우라도 그 이유를 catch 절에 주석으로 남기기
    • 예외 변수의 이름을 ignored로 하는 것도 방법

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

  • synchronized 키워드는 해당 메서드나 블록을 한 번에 한 스레드씩 수행하도록 보장
  • 동기화의 역할
    • 일관성이 깨진 상태를 볼 수 없게 해줌
    • 같은 락의 보호 하에 수행된 이전 수정의 최종 결과를 보게 해줌

여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화해야 함

  • 언어 명세상 long, double 외 변수를 읽고 쓰는 것은 atomic
  • 하지만 한 스레드가 저장한 값이 다른 스레드에 보이는지는 보장하지 않음
    • 즉 동기화는 스레드 사이의 안정적 통신에 필요
  • 예시: 1초 후에 스레드 멈추는 방법
    • boolean 필드를 synchronized 메서드로 접근해 읽고 쓰기
    • boolean 필드를 volatile로 선언
  • Volatile은 주의해서 쓰자
    • volatile int에 ++ 연산자 써도 안전하지 않을 수 있음
      • 읽기, 쓰기 2번의 연산이 있기 때문
    • 접근, 변경하는 메서드를 synchronized로
      • 이 경우 필드 선언에 volatile은 제거
    • 대신 AtomicLong을 사용하는 것도 방법
      • volatile은 통신만 지원하지만 이건 atomicity까지 지원
      • getAndIncrement()
  • 하지만 웬만하면 가변 데이터는 단일 스레드에서 쓰자

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

  • 과도한 동기화는 단점들 불러옴
    • 성능 저하
    • deadlock
    • 예측할 수 없는 동작
  • 동기화 메서드나 블록 안에서는 제어를 클라에게 양도하면 안됨
    • 안그러면 응답 불가나 안전 실패
    • e.g. 재정의 가능한 메서드 호출, 클라가 넘겨준 함수 객체 호출
  • 기본적으로 동기화 영역에서는 최대한 일을 적게 하자
  • 가변 클래스라면 다음 2가지 방법 중 하나 쓰자
    • 동기화하지 말고 클래스를 동시에 사용하는 클래스에서 외부 동기화하도록 하기
    • 동기화를 내부에서 수행해 스레드 안전한 클래스로 만들기
      • 단 외부에서 객체 전체에 락 거는 것보다 동시성을 월등히 개선할 수 있을 때만

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

  • work queue는 다음과 같이 쉽게 생성 가능
    • ExecutorService exec = Executors.newSingleThreadExecutor()
    • exec.execute(runnable)
    • exec.shutdown()
  • ExecutorService의 주요 기능들
    • 특정 태스크 완료 대기
    • 태스크 중 하나 혹은 모든 태스크가 완료되기를 기대
    • 실행자 서비스 종료 대기
    • 완료된 태스크 결과 차례로 받기
    • 태스크를 특정 시간, 혹은 주기적으로 실행하게 함
  • 둘 이상의 스레드가 처리하게 하고 싶다면 스레드 풀 생성
  • 서버 종류에 따라 다른 스레드 풀 사용
    • 가벼운 서버라면 Executors.newCachedThreadPool
      • 하지만 이 경우 태스크들이 즉시 스레드에 위임, 가용 스레드가 없다면 새로 생성
      • 고로 서버가 무겁다면 스레드가 계속 생성됨. cpu 사용량 100%!
    • 무거운 서버라면 Executors.newFixedThreadPool (스레드 개수가 고정)
      • 혹은 ThreadPoolExecutor
  • 작업 큐를 직접 만들거나 스레드를 직접 다루는 것도 일반적으로 삼가자
    • 실행자 프레임웤은 작업 단위(태스크)와 실행 메커니즘이 잘 분리
  • 태스크의 종류
    • Runnable
    • Callable
      • Runnable과 비슷하나 값 반환, 예외 던지기 가능
  • ExecutorService = 태스크를 수행하는 일반적인 메커니즘
  • ForkJoinTask
    • fork-join 풀이라는 특별한 ExecutorService가 실행해줌
    • 작은 태스크들로 나뉘고 이 태스크들은 ForJoinPool을 구성하는 스레드들이 처리
    • 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와 처리
      • cpu를 최대로 활용 가능

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

  • wait과 notify는 제대로 사용하기 어려우니까 동시성 유틸을 쓰자
    • 크게 3가지 범주
      • 실행자 프레임웤
      • 동시성 컬렉션
      • 동기화 장치 (synchronizer)

동시성 컬렉션

  • 표준 컬렉션 인터페이스에 동시성을 가미한 것
    • 동기화를 내부에서 수행
    • 고로 외부에서 락 쓰면 더 느려짐
  • 내부의 락을 handle할 수 없기 때문에 여러 기본 동작을 atomic하게 수행해주는 메서드들도 있음
    • e.g. Map::puIfAbsent
  • Collections.synchronizedMapConcurrentHashMap
    • 훨씬 빠르다!
  • 일부 인터페이스는 작업 성공까지 block되도록 한 것도 있음
    • e.g. BlockingQueue
      • take했는데 원소 없으면 원소 추가될 때까지 기다림

동기화 장치

  • 스레드가 다른 스레드를 기다릴 수 있게 해줌
  • 예시
    • Phaser (가장 강려크)
    • CountDownLatch, Semaphore (많이 쓰임)
    • CyclicBarrier, Exchanger
  • CountDownLatch
    • 하나 이상의 스레드가 또다른 하나 이상의 스레드 작업이 끝날 때까지 대기
    • 일회성
    • 사용 예시: 여러 executor가 있고 전체 동작 시간을 알고 싶다면?
      CountDownLatch start = new CountDownLatch(1); // 시작하는 executor가 `countDown`
      CountDownLatch done = new CountDownLatch(concurrency); // 모든 executor가 `countDown`
      
      ...
      done.await();

waitnotify

  • 웬만하면 동시성 유틸리티 써야겠지만 레거시를 다뤄야 할수도 있음
  • wait 쓸 땐 wait loop를 사용하자
    • 여러 문제점들 막아줌 (불변식 깨짐, 응답 불가)

      synchronized(obj) {
      	while (조건)
      		obj.wait();
      
      	// do smthing
      }
  • 조건으로 한 스레드만 혜택을 받을 수 있다면 notifyAll 말고 notify를 쓰자

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

  • 한 메서드를 여러 스레드가 동시에 호출할 때 그 메서드가 어떻게 동작하는지 문서화
  • javadoc 기본 옵션으로는 synchronized 한정자가 포함 안됨
    • 단순 구현의 문제라서
  • 스레드 안전성의 순서
    • 불변 > unconditionally 스레드 안전 > conditionally 스레드 안전 > 스레드 안전하지 않음 > 스레드 적대적
    • 불변: 클래스 인스턴스는 상수와 같아 동기화 필요 없음
      • e.g. String, Long, BigInteger
    • unconditionally~ : 인스턴스는 수정될 수 있지만 내부에서 충실히 동기화, 외부에서 동기화 없이 동시에 사용해도 안전함
      • e.g. AtomicLong, ConcurrentHashMap
    • conditionally~: unconditionally와 같지만 일부 메서드는 동시에 사용하려면 외부 동기화 필요
      • e.g. Collections.synchronized 래퍼 메서드가 반환한 컬렉션
      • 이 경우 주의해서 문서화해야 함
        • 어떤 순서로 호출할 때 외부 동기화가 필요한가?
        • 그리고 어떤 락을 얻어야 하는가?
    • 안전하지 않음: 인스턴스는 수정될 수 있으며 동시에 사용하려면 각 메서드 호출을 클라가 선택한 외부 동기화 메커니즘으로 감싸야 함
      • e.g. ArrayList, HashMap같은 기본 컬렉션
    • 적대적: 모든 메서드 호출을 외부 동기화로 감싸도 안전하지 않음
  • 서비스 거부 공격
    • 클라가 클래스에서 공개한 락을 얻어 오래 놓지 않는 경우 발생 가능
    • 내부 비공개 락 객체 사용하기
      private final Object lock = new Object();
      
      public void api() {
      	synchronized(lock) { /* do smthing */ }
      }
    • 단 unconditional한 경우에만 사용가능
      • conditional의 경우 호출 순서에 따라 필요한 락이 무엇인지 알려줘야 하기 때문

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

  • lazy initialization: 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 방법
  • 쓰임새
    • 최적화
    • 클래스와 인스턴스 초기화 때 발생하는 순환 문제 해결
  • 하지만 필요할 때까지는 하지 말아라
    • 대부분의 경우 일반적인 초기화가 더 나음
  • 단점
    • 해당 필드에 접근하는 비용은 커짐
  • 그럼에도 필요한 경우
    • 필드를 사용하는 인스턴스 비율이 낮지만 초기화하는 비용이 큰 경우
    • 이 경우 성능 측정해보기
  • 멀티스레드 환경에서는 까다로움
    • 지연 초기화하는 필드를 두 개 이상의 스레드에서 공유하면? → 어떤 형태로든 동기화 해야함
  • 초기 순환성을 깨뜨릴 것 같다면 synchronized 접근자를 사용하자!
    private Type field;
    
    private synchronized Type getField() {
    	if (field == null) field = computeField();
    	return field;
    }
  • 성능으로 정적 필드를 지연 초기화해야 한다면?
    • 지연 초기화 홀더 클래스 / lazy initialization holder class

      private static class FieldHolder {
      	static final FieldType field = compusteFieldValue();
      }
      
      private static FieldType getField() { return FieldHolder.field; }
    • getField 처음 호출될 때 FieldHolder 클래스를 초기화

    • 동기화를 하지 않아 성능 유지

  • 성능으로 인스턴스 필드를 지연 초기화해야 한다면?
    • 이중 검사 / double-check 관용구

      private volatile FieldType field;
      
      private FieldType getField() {
      	FieldType result = field;
      	if (result != null) return result;
      
      	synchronized(this) {
      		if (field == null) field = computeFieldValue();
      		return field;
      	}
      }
    • 첫 번째 비교는 동기화 없이 검사

      • 이 때 result 쓰는 이유?
        • 필드가 초기화된 상황(즉 대부분의 경우)에서는 필드를 한 번만 읽도록 보장
        • 빠름
    • 두 번째 비교는 동기화해서 검사

    • 필드는 volatile

      • 초기화 이후로는 동기화하지 않기 때문
  • 반복해서 초기화해도 괜찮다면 single check로
    • 동기화 후 검사는 생략

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

  • 스케줄링 정책은 OS마다 다름 → 정확성/성능이 스케줄러에 따라 달리지는 프로그램은 이식성이 낮음
  • 가장 좋은 건 실행 가능한(전체 스레드 수가 아님) 평균 스레드 수를 프로세서 수보다 지나치게 많아지지 않게 하는 것
    • 실행 준비가 된 스레드는 작업 완료 때까지 계속 실행되게 만들기

유의점들 (실행 가능한 스레드 수를 적게 유지하는 법)

  • 스레드는 당장 처리할 작업이 없다면 실행돼서는 안됨
  • busy wait 상태가 없어야 함
  • Thread.yield 사용 피하기
    • 테스트도 못함

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

  • 직렬화는 편하긴 하지만 문제점이 있음
  • 근본적으로 공격 가능 범위가 너무 넓음
    • readObject는 클래스패스 내 거의 모든 타입의 객체를 만들어낼 수 있음
    • 역직렬화 과정에서 타입들 안의 모든 코드를 수행할 수 있음 ⇒ 타입 코드 전체가 공격 범위에 들어가게 됨
  • gadget: 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행할 수 있게 하는 메서드
    • 신중히 제작한 바이트 스트림만 역직렬화 해야함
  • 역직렬화에 시간이 오래 걸리는 짧은 스트림으로 서비스 거부 공격도 가능
  • 고로 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화(자바 직렬화)하지 않는 것
  • 대신 cross-platform structured-data representation 사용하자
    • JSON, protobuf…
    • 자바 직렬화보다 간단
    • 임의 객체 그래프를 자동으로 직렬화/역직렬화 하지 않음
    • 대신 기본, 배열 타입만 지원
  • JSON은 텍스트 기반이라 읽기 쉽고, protobuf는 이진 표현이라 빨라요
    • protobuf는 부가적으로 문서를 위한 스키마를 제공
  • 레거시로 인해 자바 직렬화를 해야 한다면?
    • 신뢰할 수 없는 데이터는 역직렬화 하지 말기
    • 확신도 할 수 없다면?
      • 객체 역직렬화 필터링 사용 (java.io.ObjectInputFilter)
      • 데이터 스트림이 역직렬화되기 전에 필터를 설치하는 기능
      • 클래스 단위의 블랙 리스트/화이트 리스트 방식 지원
        • 화이트리스트 방식 추천
    • 하지만 모든 걸 걸러주진 못함

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

  • 우선 결론: 직렬화 가능 클래스 만들 거면 설계를 제대로 해야 한다~

문제점들

  • 한 번 구현 후(자바 기본 방식) 릴리즈해버리면 수정하기 어려움
    • 첫 공개 당시 내부 구현에 묶임
      • 이후 내부 구현을 고치면 원래 직렬화 형태와 달라지게 됨, 프로그램 깨질 수도 있음
    • private, package-private 필드마저 공개하는 꼴 → 캡슐화 깨짐
    • 예) 자동 생성된 serialVersionUID 를 사용하는 경우
      • 클래스 변경되면 이 값도 달라져 호환성 깨짐
  • 버그, 보안
    • 역직렬화는 숨은 생성자
    • 불변식, 내부를 들여다볼 수 없다는 전제들을 싸그리 무시
  • 새로운 버전 릴리즈 할 때 테스트할 것이 늘어남
    • 호환성 검사 해야 함
      • 새로운 버전 → 직렬화 → 역직렬화 → 옛날 버전…
    • 고칠 때마다 테스트할 게 비례해서 증가

언제 무엇을 어떻게

  • 객체 전송/저장용이라면 어쩔 수 없음ㅠ
  • 값을 나타내거나 컬렉션 → 구현
    • e.g. BigInteger, Instant
  • 동작하는 객체 → 구현 X
    • e.g. 스레드 풀
  • 상속용으로 설계된 클래스, 인터페이스는 대부분 Serializable을 구현/확장해서는 안된다
    • 구현하는 쪽에 부담

주의할 점들

  • 클래스의 인스턴스 필드가 직렬화, 확장 모두 가능하다면?
    • 하위 클래스에서 finalize 메서드 재정의 못하게 해야 함
      • 자신이 재정의하면서 final로 선언
      • 그래야 불변식 보장 가능
    • 필드가 기본값으로 초기화될 경우 불변식이 깨진다면
      • readObjectNoData 메서드를 추가해라
        private void readObjectNoData() throws InvalidObjectException {
        	throw new InvalidObjectException("스트림 데이터가 필요합니당");
        }
  • Serializable 구현을 안한다면
    • 상속용 클래스인데 직렬화 지원 안하면 하위 클래스에서 직렬화 하려 할 때 부담
    • 이런 클래스 역직렬화 하려면 상위 클래스에서 매개변수 없는 생성자 제공해야 함
    • 안그러면 직렬화 프록시 패턴 써야 함(아이템 90)
  • 내부 클래스는 직렬화 구현하면 안됨
    • 내부 클래스에는 바깥 인스턴스의 참조 등을 저장하기 위해 컴파일러가 자동으로 일부 필드 추가
    • 얘네들이 클래스 정의에 어떻게 추가되는지는 언어 명세에 정의 x
      • 즉, 기본 직렬화 형태가 불분명

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

  • 고민 후 괜찮다고 판단(유연성, 성능, 정확성)될 때만 기본 직렬화 형태를 사용해라
  • 객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태도 괜찮다
    class Name implements Serializable {
    	private final String lastName;
    	private final String firstName;
    }
  • 그렇지 않은 경우의 예시: String linked list
    public final class StringList implements Serializable {
    	private int size = 0;
    	private Entry head = null;
    
    	private static class Entry implements Serializable {
    		String data;
    		Entry next;
    		Entry prev;
    	}
    }
    • 문제점
      • 공개 api(StringList.Entry)가 현재 내부 구현 방식에 묶임
        • 나중에 linked list 사용 안해도 관련 코드는 지원해야 함
      • 너무 많은 공간
        • 직렬화하면 모든 엔트리와 연결 정보까지 포함해 크기가 커짐
          • 근데 이는 내부 구현, 가치가 없음
        • 디스크 저장, 네트워크 전송 느려짐
      • 오래 걸림
      • stack overflow
        • 기본 직렬화는 객체 그래프를 재귀 순회
        • 그리 큰 객체 아니어도 stack overflow 가능
        • 근데 또 플랫폼마다 달라질 가능성
    • 이렇게 고치자
      public final class StringList implements Serializable {
      	private transient int size = 0;
      	private transient Etnry head = null;
      
      	private static class Entry {
      		String data;
      		Entry next;
      		Entry prev;
      	}
      
      	public final void add(String s) { /*...*/ }
      
      	/**
      	 * @serialData ...
      	 */
      	private void writeObject(ObjectOutputStream s) throws IOException{
      		s.defaultWriteObject();
      		s.writeInt(size);
      		for (Entry e = head; e != null; e = e.next) {
      			s.writeObject(e.data);
      		}
      	}
      
      	private void writeObject(ObjectOutputStream s) throws IOException, ClassNotFoundException{
      		s.defualtReadObject();
      		int numElements = s.readInt();
      
      		for (int i = 0; i < numElements; i++) {
      			add((String) s.readObject());
      		}
      	}
      }
      • defaultWriteObject, defualtReadObject 해줘야 나중에 transient 아닌 필드 추가돼도 호환
        • defaultWriteObject: 신 → 직렬화 → 역직렬화 → 구
          • 새로 추가된 필드 무시
      • writeObject의 문서화
        • 이 메서드는 private이지만 사실상 public api
  • 커스텀 직렬화의 경우 대부분의 인스턴스 필드를 transient로 선언하기
    • 그렇지 않으면 직렬화됨
    • 객체의 논리적 상태와 무관한 필드인 게 확실할 때만 transient 생략
    • transient로 선언된 필드들은 역직렬화하면 기본값으로 초기화
      • 원치 않는 동작인 경우 readObject 메서드에서 defaultReadObject 호출 후 원하는 값으로 복원
  • 동기화 메커니즘을 사용 중인 경우 직렬화에도 똑같이 적용해야 함
    • synchronized 메서드 등
  • 직렬화 가능 클래스에 모두 직렬 버전 UID를 명시적으로 부여하기

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

  • readObject는 또다른 public 생성자, 그래서 다른 생성자와 마찬가지로 다음을 고려해야 함
    • 인수 유효성 검사
    • 매개변수의 방어적 복사
  • readObject 메서드에서 defaultReadObject를 호출한 후 역직렬화된 객체가 유효한지 검사 필요
    • 유효하지 않은 경우 InvalidObjectException 던지기
  • 또 역직렬화 할 때는 클라가 소유해서는 안되는 객체 참조(private 가변 요소)는 방어적으로 복사하기
    • 안그러면 역직렬화 후 byte stream으로 참조, 수정 가능해짐
    • 단 final 필드는 방어적 복사가 불가능, 필요하다면 final 한정자 제거
      • 공격받는 것보다야 낫다
  • 순서: 방어적 복사 → 유효성 검사
  • readObject 내에서는 재정의 가능 메서드를 호출하면 안됨
    • 하위 클래스에서 역직렬화 마치기도(유효성 검사, 방어적 복사하기) 전에 재정의된 메서드 실행됨 → 오작동

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

  • readResolve 사용하면 readObject가 만든 인스턴스를 다른 것으로 대체 가능
    • e.g. 싱글턴 객체 반환하기
    • 이 경우 모든 객체 참조 필드는 transient로 선언해야 함
    • 안그러면 공격당하기 쉽다
  • transient가 아닌 필드 있을 경우
    • 그 필드는 readResolve 실행 전 역직렬화됨
    • 그러면 그 인스턴스 참조 훔쳐와 저장할 수 있음
  • 그럴 바에야 enum으로 구현하는 것이 낫다
    • Java가 보장해줌!

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

  • how? → 바깥 클래스의 논리적 상태를 정밀히 표현하는 중첩 클래스 설계
    • 얘가 직렬화 프록시
    • private static으로 선언
    • 생성자는 단 하나, 바깥 클래스를 매개변수로
      • 생성자는 단순히 매개변수 인스턴스 데이터 복사
    • 바깥 클래스, 직렬화 프록시 모두 Serializable 구현
class Period implements Serializable {
	private final Date start;
	private final Date end;

	private static class SerializationProxy implements Serializable {
		private static final long serialVersionUID = 123456789L;

		private final Date start;
		private final Date end;

		SerializationProxy(Period p) {
			this.start = p.start;
			this.end = p.end;
		}

		// 역직렬화 시 프록시를 다시 바깥 클래스의 인스턴스로 변환
		private Object readResolve() {
			return new Period(start, end);
		}
	}

	// 직렬화가 이뤄지기 전 바깥 클래스 인스턴스를 직렬화 프록시로 변환
	// -> 직렬화 시스템은 바깥 클래스의 직렬화 인스턴스를 생성할 수 없음
	private Object writeReplace() {
		return new SerializationProxy(this);
	}

	private void readObject(ObjectInputStream stream) throws InvalidObjectException {
		throw new InvalidObjectException("프록시가 필요합니다");
	}
}
  • 한계
    • 클라가 확장할 수 있는 클래스에는 적용 불가
    • 객체 그래프에 순환이 있는 클래스에 적용 불가
    • 좀 느려짐
profile
코드야 내가 잘못했어

0개의 댓글