BOOK 6 - 이펙티브 자바(8)

8장 메서드

매개변수가 유효한지 검사하라

  • 메서드와 생성자는 대체로 입력 매개변수의 값이 특정 조건을 만족하기를 바란다.
  • 이런 제약은 반드시 문서화해야 하며, 메서드 몸체가 시작하기 전에 검사해야 한다.
    • “오류는 가능한 한 발생한 곳에서 가까이 빨리 잡아야 한다”는 원칙의 사례이다.

매개변수 검사를 제대로 하지 못했을 때 문제점

  • 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.
  • 메서드가 잘 수행되지만 잘못된 결과를 반환할 수 있다.
  • 메서드는 문제없이 수행됐지만, 어떤 객체를 이상한 상태로 만들어 놓아서 미래의 알 수 없는 시점에 이 메서드와는 관련 없는 오류를 발생시킬 수 있다.
  • 즉, 실패 원자성 을 어기는 결과를 낳을 수 있다.

null 검사

  • 매개변수가 null일 때 메서드는 NullPointerException 을 던질 수 있다.

  • @Nullable 이나 이와 비슷한 어노테이션을 사용해 특정 매개변수는 null이 될 수 있다고 알려줄 수도 있지만, 표준적인 방법은 아니다.

  • 자바 7에 추가된 java.util.Objects.requireNonNull 메서드는 유연하고 사용하기도 편하다.

    • 직접 null 검사를 수동으로 하지 않아도 된다.
    public static <T> T requireNonNull(T obj) {
    	if (obj == null)
      	throw new NullPointerException();
    	return obj;
    }
    
    • 이 메서드는 명시성과 빠른 실패를 위해 사용한다.
    • 참고 : https://velog.io/@rockpago/Objects.requireNonNull

범위 검사

  • 자바 9에서 Objects 에 범위 검사 기능도 더해졌다.
  • checkFromIndexSize , checkFromToIndex , checkIndex
  • null 검사 메서드만큼 유연하지는 않다.
    • 리스트와 배열 전용으로 설계되었고, 닫힌 범위는 다루지 못한다.
  • 이런 제약이 걸림돌이 되지 않는 상황에서는 유용하고 편하게 쓸 수 있다.

Asserts

  • 공개되지 않은 메서드라면 해당 메서드가 호출되는 상황을 직접 통제할 수 있다.
  • 따라서 오직 유효한 값만이 메서드에 넘겨지리라는 것을 보증할 수 있고, 그렇게 해야 한다.
  • 즉, public 이 아닌 메서드라면 assert 단언문을 사용해 매개변수 유효성을 검증할 수 있다.
private static void sort(long a[], int offset, int length) {
  assert a != null;
  assert offset >= 0 && offset <= a.length;
  assert length >= 0 && length <= a.length - offset;
  ...
}
  • 단언문들은 자신이 단언한 조건이 무조건 참이라고 선언한다.

일반적인 유효성 검사와 다른 점

  • 실패하면 AssertionError 를 던진다.
  • 런타임에 아무런 효과도, 아무런 성능 저하도 없다.
    • 디버깅 용도로 사용되기 때문에, 최종 실행환경에서는 제외된다.
    • 참고 : https://offbyone.tistory.com/294

그 외 유의사항

  • 메서드가 직접 사용하지는 않으나 나중에 사용하기 위해 저장하는 매개변수는 특히 더 신경써서 검사해야 한다.
    • 생성자의 경우가 특수한 사례이다.
    • 생성자 매개변수의 유효성 검사는 클래스 불변식을 어기는 객체가 만들어지지 않게 하는 데 꼭 필요하다.
  • 메서드 몸체 실행 전에 매개변수 유효성을 검사해야 한다는 규칙도 예외는 있다.
    • 유효성 검사 비용이 지나치게 높거나 실용적이지 않을 때, 혹은 계산 과정에서 암묵적으로 검사가 수행될 때다.
    • 예를 들어, Collections.sort(List) 의 경우, 리스트 안의 객체들은 모두 상호 비교될 수 있어야 하며, 비교될 수 없는 타입의 객체가 들어있다면 ClassCastException 을 던진다.
    • 따라서 비교 전에 리스트 안의 모든 객체가 비교 가능한지 검사할 필요가 없다.
    • 하지만 암묵적 유효성 검사에 너무 의존하면 실패 원자성을 해칠 수 있으니 주의해야 한다.
  • 매개변수에 제약을 두는 게 좋다는 의미가 아니다. 메서드는 최대한 범용적으로 설계해야 한다.
    • 메서드가 건네받은 값으로 무언가 제대로 된 일을 할 수 있다면 매개변수 제약은 적을수록 좋다.

적시에 방어적 복사본을 만들라

  • 자바는 C, C++ 같이 버퍼 오버런, 배열 오버런, 와일드 포인터같은 메모리 충돌 오류에서 안전하다.
  • 하지만, 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있는 건 아니다.
  • 클라이언트가 해당 클래스의 불변식을 깨뜨릴 수 있다고 가정하고 방어적으로 프로그래밍 해야 한다.

불변식을 지키지 못한 경우 - Period 예제

  • 어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다.
  • 하지만 주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다.
public final class Period {
  private final Date start;
  private final Date end;
  
  public Period(Date start, Date end) {
    if (start.compareTo(end) > 0) {
      throw new IllegalArgumentException(start + "가 " + end + "보다 늦다.");
    }
    
    this.start = start;
    this.end = end;
  }
  
  public Date start() {
    return start;
  }
  
  public Date end() {
    return end;
  }
}
  • 이 클래스는 불변처럼 보이고, 시작 시각이 종료 시각보다 늦을 수 없다는 불변식이 지켜질 것 같다.
  • 하지만 Date 가 가변이라는 사실을 이용하면 불변식을 깨뜨릴 수 있다.

첫번째 공격

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78);
해결방법 1 - Instant 사용
  • Date 대신 자바 8 이후로 추가된 불변인 Instant 를 사용한다.
    • 혹은 LocalDateTime 이나 ZonedDateTime 을 사용해도 된다.
해결방법 2 - 방어적 복사
  • 많은 API와 내부 구현에 Date 같은 낡은 값 타입을 사용한 잔재가 남아있다.
  • 이 경우에는 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사하여 Period 인스턴스 내부를 보호한다.
  • 그런 다음 Period 인스턴스 안에서는 원본이 아닌 복사본을 사용한다.
public Period(Date start, Date end) {
  this.start = new Date(start.getTime());
  this.end = new Date(end.getTime());
  
  if (this.start.compareTo(this.end) > 0) {
      throw new IllegalArgumentException(this.start + "가 " + this.end + "보다 늦다.");
    }
}
주의점

1. 매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사했다.

  • 멀티스레딩 환경에서 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
    • 이를 검사시점/사용시점 (time-of-check/time-of-use) 공격 혹은 TOCTOU 공격이라 한다.
  1. 방어적 복사에 Dateclone 메서드를 사용하지 않았다.
  • Datefinal 이 아니므로 cloneDate 가 정의한 게 아닐 수 있다.
    • 즉, clone 이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다.
  • 따라서 매개변수가 제 3자에 의해 확장될 수 있는 타입이라면, 방어적 복사본을 만들 때 clone 을 사용해서는 안된다.

두번째 공격

  • 접근자 메서드가 Period 인스턴스 내부의 가변 정보를 직접 드러내기 때문에 Period 인스턴스는 아직 변경 가능하다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78);
해결 방법
  • 접근자가 가변 필드의 방어적 복사본을 반환하도록 한다.
public Date start() {
  return new Date(start.getTime());
}

public Date end() {
  return new Date(end.getTime());
}
  • 이렇게 하면 모든 필드가 객체 안에 완벽하게 캡슐화되었다.

그 외 유의사항

  • 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항상 그 객체가 잠재적으로 변경될 수 있는지를 체크해야 한다.
    • 변경될 수 있는 객체라면 그 객체가 임의로 변경되어도 그 클래스가 문제없이 동작할지를 따져본다.
    • 확신할 수 없다면 복사본을 만들어 저장해야 한다.
  • 길이가 1이상인 배열은 무조건 가변이다.
    • 따라서 내부에서 사용하는 배열을 클라이언트에 반환할 떄는 항상 방어적 복사를 수행해야 한다.
  • 방어적 복사에는 성능 저하가 따르고, 또 항상 쓸 수 있는 것도 아니다.
    • 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략할 수 있다.
  • 방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때로 한정해야 한다.

메서드 시그니처를 신중히 설계하라

API 설계 요령

  • 메서드 이름을 신중히 짓자.

    • 이해할 수 있고, 같은 패키지에 속한 다른 이름들과 일관되게 짓는게 최우선이다.
  • 편의 메서드를 너무 많이 만들지 말자.

    • 모든 메서드는 각각 자신의 소임을 다해야 한다.
    • 메서드가 너무 많은 클래스는 사용하고 테스트하고 유지보수하기 어렵다.
    • 클래스나 인터페이스는 자신의 각 기능을 완벽히 수행하는 메서드로 제공해야 한다.
  • 매개변수 목록은 짧게 유지하자.

    • 4개 이하가 좋다.
    • 같은 타입의 매개변수 여러개가 연달아 나오는 경우가 특히 해롭다.
    • 매개변수 순서를 기억하기 어렵고, 실수로 순서를 바꿔 입력해도 그대로 컴파일되고 실행된다.
  • 매개변수의 타입으로는 클래스보다는 인터페이스가 낫다.

    • 인터페이스 대신 클래스를 사용하면 클라이언트에게 특정 구현체만 사용하도록 제한하는 꼴이며, 입력 데이터가 다른 형태로 존재한다면 명시한 특정 구현체의 객체로 옮겨담느라 복사 비용을 치러야 한다.
  • 매개변수 타입 boolean 보다는 원소 2개짜리 열거 타입이 낫다.

    • 코드를 읽고 쓰기가 더 쉬워진다.
    • 아래와 같이 온도 열거 타입이 있을 때, boolean 타입 매개변수를 전달하는 것보다 열거 타입 매개변수를 전달하는 게 명확하다.
    public enum TemperatureScale { FAHRENHEIT, CELSIUS }
      
    Thermometer.newInstance(true);  											// boolean 타입 
    Thermometer.newInstance(TemperatureScale.CELSIUS);  	// 열거 타입			
    
    • 나중에 KELVIN 값을 추가해 캘빈온도도 지원할수도 있다.

긴 매개변수 목록을 줄여주는 기술

  1. 여러 메서드로 나눈다.
  • 쪼개진 메서드 각각은 원래 매개변수 목록의 부분집합을 받는다.
  • 직교성을 높여 오히려 메서드 수를 줄여주는 효과도 있다.
    • 직교성이 높다는 의미는 ‘공통점이 없는 기능들이 잘 분리되어 있다’, ‘기능을 원자적으로 쪼개 제공한다’ 로 해석할 수 있다.
    • 기능을 원자적으로 쪼개다보면, 중복이 줄고 결합성이 낮아져 코드를 수정, 테스트하기 쉬워진다.
    • 일반적으로 직교성이 높은 설계는 가볍고 구현하기 쉽고 유연하고 강력하다.
    • 특정 조합의 패턴이 상당히 자주 사용되거나 최적화하여 성능을 크게 개선할 수 있다면 직교성이 낮아지더라도 편의 기능으로 제공하는 편이 나을 수도 있다.
  • 예를 들어, List 인터페이스는 subListindexOf 메서드를 제공한다.
    • 이 두 메서드를 조합해 지정된 범위의 부분리스트에서의 인덱스를 찾을 수 있다.
  1. 매개변수 여러개를 묶어주는 도우미 클래스를 만든다.
  • 일반적으로 이런 도우미 클래스는 정적 멤버 클래스로 둔다.
  • 특히 매개변수 몇 개를 독립된 하나의 개념으로 볼 수 있을 때 추천하는 기법이다.
  • 예를 들어, 카드게임 예제에서 숫자(rank)와 무늬(suit)를 뜻하는 두 매개변수를 묶어 도우미 클래스를 만들어 하나의 매개변수로 주고 받을 수 있다.
  1. 객체 생성에 사용한 빌더 패턴을 메서드 호출에 응용한다.
  • 매개변수가 많고, 그 중 일부는 생략해도 괜찮을 때 도움이 된다.
  • 모든 매개변수를 하나로 추상화한 객체를 정의하고, 클라이언트에서 이 객체의 setter 메서드를 호출해 필요한 값을 설정하게 하는 것이다.
  • 클라이언트는 먼저 필요한 매개변수를 다 설정한 다음, execute 메서드를 호출해 앞서 설정한 매개변수들의 유효성을 검사한다.
  1. 매개변수 여러개를 묶어주는 도우미 클래스를 만든다.

다중정의는 신중히 사용하라

컬렉션 분류기 예제 - 다중정의 오류

public class CollectionClassifier {
  public static String classify(Set<?> s) {
    return "집합";
  }
  
  public static String classify(List<?> list) {
    return "리스트";
  }
  
  public static String classify(Collection<?> c) {
    return "그 외";
  }
  
  public static void main(String[] args) {
    Collection<?>[] collections = {
      new HashSet<String>(),
      new ArrayList<BigInteger>(),
      new HashMap<String, String>().values()
    };
    
    for (Collection<?> c : collections) {
      System.out.println(classify(c));
    }
  }
}
  • 위 코드를 실행하면 "집합", "리스트", "그 외" 를 출력할 것 같지만 실제로는 "그 외" 만 세 번 연달아 출력된다.
  • 다중정의된 세 classify 중 어느 메서드를 호출할지가 컴파일 타임에 정해지기 때문이다.
  • for 문 안의 c 는 런타임에는 타입이 매번 달라지지만, 컴파일타임에는 항상 Collection<?> 타입이다.
    • 따라서 항상 세번째 메서드인 classify(Collection<?>) 만 호출되는 것이다.

재정의 VS 다중정의 메서드 호출 메커니즘

  • 재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택된다.
  • 즉, 메서드를 재정의했다면 해당 객체의 런타임 타입이 어떤 메서드를 호출할지의 기준이 된다.
class Wine {
  String name() { return "포도주"; }
}

class SparklingWine extends Wine {
  @Override
  String name() { return "발포성 포도주"; }
}

class Champagne extends SparklingWine {
  @Override
  String name() { return "샴페인"; }
}

public class Overriding {
  public static void main(String[] args) {
    List<Wine> wineList = List.of(new Wine(), new SparklingWine(), new Champagne());
    
    for (Wine wine : wineList) {
      System.out.println(wine.name());
    }
  }
}
  • 위 코드를 실행하면 "포도주", "발포성 포도주", "샴페인" 을 출력한다.
  • 다중정의 메서드 사이에서는 객체의 런타임 타입은 전혀 중요하지 않고, 오직 매개변수의 컴파일타임 타입에 의해 선택된다.

  • 이처럼 재정의한 메서드와 다중정의한 메서드의 호출 메커니즘이 다르기 때문에 헷갈릴 수 있는 코드는 작성하지 않는 게 좋다.
  • 특히나 공개 API라면 사용자가 매개변수를 넘기면서 어떤 다중정의 메서드가 호출될지 모른다면 프로그램이 오동작하기 쉽다.

다중정의 혼란을 피하는 법

  • 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 않는게 좋다.
    • 가변인수를 사용하는 메서드라면 다중정의를 아예 하지 말아야 한다.
  • 다중정의 대신 메서드 이름을 다르게 지어주는 방법이 있다.
    • 예를 들어, ObejctOutputStream 클래스의 write 메서드는 모든 기본 타입과 일부 참조 타입용 변형을 가지고 있다.
    • 하지만 다중정의가 아닌 모든 메서드에 다른 이름을 지어주는 방법을 택했다.
    • writeBoolean , writeInt , writeLong 같은 식이다.
  • 생성자는 이름을 다르게 지을 수 없기 때문에 두번째 생성자부터는 무조건 다중정의가 된다.
    • 하지만 정적 팩터리라는 대안을 활용할 수 있다.
  • 매개변수 수가 같은 다중정의 메서드가 많더라도, 매개변수 중 하나 이상이 ‘근본적으로 다르다’면 헷갈릴 일이 없다.

근본적으로 다른 매개변수

  • 근본적으로 다르다는 것은 두 타입의 값을 서로 어느 쪽으로든 형변환할 수 없다는 뜻이다.
  • 이 조건만 충족하면 어느 다중정의 메서드를 호출할지가 매개변수의 런타임 타입만으로 결정된다.
  • 예를 들어, ArrayList 에는 int 를 받는 생성자와 Collection 을 받는 생성자가 있는데, 이 두 타입은 서로 형변환이 불가능하므로 어떤 상황에서든 두 생성자 중 어느 것이 호출될지 헷갈릴 일은 없을 것이다.
오토박싱
  • 자바 4까지는 모든 기본 타입이 모든 참조 타입과 근본적으로 달랐지만, 자바 5에서 오토박싱이 도입되면서 더는 근본적으로 다르지 않게 되었다.
public class SetList {
  public static void main(String[] args) {
    Set<Integer> set = new TreeSet<>();
    List<Integer> list = new ArrayList<>();
    
    for (int i = -3; i < 3; i++) {
      set.add(i);
      list.add(i);
    }
    
    for (int i = 0; i < 3; i++) {
      set.remove(i);
      list.remove(i);
    }
  }
}
  • 프로그램을 실행했을 때 두 컬렉션의 원소가 모두 [-3, -2, -1] 이리라고 기대하지만, 실제로는 set 의 원소는 [-3, -2, -1] 이고, list 의 원소는 [-2, 0, 2] 가 된다.
  • List 인터페이스가 remove(Object)remove(int) 를 다중정의했기 때문이다.
  • set.remove(i) 의 메서드 시그니처는 remove(Object) 이다.
    • 다중정의된 메서드가 없으니 기대한대로 동작하여 집합에서 입력받은 값의 원소를 제거한다.
  • list.remove(i) 는 다중정의된 remove(int index) 를 선택한다.
    • 이 메서드는 ‘지정한 위치’의 원소를 제거하는 기능을 수행한다.
    • 따라서 차례로 0번, 1번, 2번 원소가 제거되어 [-2, 0, 2] 가 남게 된다.
  • 이 문제는 list.remove 의 인수를 Integer 로 형변환하여 올바른 다중정의 메서드를 선택하게 하면 해결된다.

  • 자바 언어에 오토박싱과 제네릭을 더한 결과 List 인터페이스가 취약해졌다.
람다와 메서드참조
  • 자바 8에서 도입한 람다와 메서드 참조 역시 다중정의 시의 혼란을 키웠다.
// 1번
new Thread(System.out::println).start();

// 2번
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);
  • 1번과 2번이 모두 Runnable 을 받는 메서드가 다중정의되어 있는 비슷한 모습이지만 2번만 컴파일 오류가 발생한다.
  • submit 의 다중정의 메서드 중에는 Callable<T> 를 받는 메서드도 있다는 점이 원인이다.
  • 만약 println 이 다중정의 없이 단 하나만 존재했다면 이 코드는 제대로 컴파일이 됐을 것이다.
  • 지금은 참조된 메서드(println)와 호출한 메서드(submit) 양쪽 다 다중정의되어 다중정의 해소 알고리즘이 기대처럼 동작하지 않는다.
    • 기술적으로 부정확한 메서드 참조라고 한다.
    • 암시적 타입 람다식(implicitly lambda expression)이나 부정확한 메서드 참조 같은 인수 표현식은 목표 타입이 선택되기 전에는 그 의미가 정해지지 않기 때문에 적용성 테스트 때 무시된다.
  • 메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다.
    • 즉, 서로 다른 함수형 인터페이스라도 서로 근본적으로 다르지 않다는 뜻이다.

정리

  • 다중정의된 메서드 중 하나를 선택하는 규칙은 매우 복잡하며, 자바가 버전업될수록 더 복잡해지고 있어 이 모두를 이해하고 사용하는 프로그래머는 극히 드물 것이다.
  • 일반적으로 매개변수 수가 같을 때는 다중정의를 피하는 것이 좋다.
  • 불가능하다면, 헷갈릴만한 매개변수는 형변환하여 정확한 다중정의 메서드가 선택되도록 해야 한다.

가변인수는 신중히 사용하라

  • 가변인수 메서드는 명시한 타입의 인수를 0개 이상 받을 수 있다.
  • 가변인수 메서드를 호출하면 가장 먼저 인수의 개수와 길이가 같은 배열을 만들고, 인수들을 이 배열에 저장하여 메서드에 건네준다.

인수가 1개 이상이어야 하는 경우

  • 최솟값을 찾는 메서드의 경우, 인수 0개를 받을 수도 있도록 설계하는 것은 좋지 않다.
static int min(int... args) {
  if (args.length == 0) {
    throw new IllegalArgumentException("인수가 1개 이상 필요합니다.");
  }
  
  int min = args[0];
  for (int i = 1; i < args.length; i++) {
    if (args[i] < min) {
      min = args[i];
    }
  }
  
  return min;
}
  • args 유효성 검사를 명시적으로 해야하고, 인수를 0개 넣어 호출하면 컴파일타임이 아닌 런타임에 실패한다.

해결 방법

  • 매개변수를 2개 받도록 하면된다.
  • 즉, 첫 번째로 평범한 매개변수를 받고, 두 번째로 가변인수를 받으면 된다.
static int min(int firstArg, int... remainigArgs) {
  int min = firstArg;
  for (int arg : remainigArgs) {
    if (arg < min) {
      min = arg;
    }
  }
  return min;
}

가변인수 메서드 성능 최적화

  • 성능에 민감한 상황이라면 가변인수가 걸림돌이 될 수 있다.
  • 메서드가 호출될 때마다 배열을 새로 할당하고 초기화하는 비용을 감당할 수는 없지만 가변인수의 유연성이 필요할 때 선택할 수 있는 패턴이 있다.
  • 예를 들어, 해당 메서드 호출의 95%가 인수를 3개 이하로 사용한다고 해보자.
    • 그렇다면 아래처럼 인수가 0개인 것 부터 4개인 것 까지 총 5개의 메서드를 다중정의하자.
    • 마지막 다중정의 메서드가 인수 4개 이상인 5%의 호출을 담당하는 것이다.
public void foo() {}
public void foo(int a1) {}
public void foo(int a1, int a2) {}
public void foo(int a1, int a2, int a3) {}
public void foo(int a1, int a2, int a3, int... rest) {}

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

private final List<Cheese> chessesInStock = ...;

public List<Cheese> getCheeses() {
  return chessesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}
  • 위 코드처럼 null 을 반환하는 경우가 있다면, 클라이언트는 null 상황을 처리하는 코드를 추가로 작성해야 한다.
List<Cheese> cheeses = shop.getCheeses();
if (cheese != null && cheeses.contains(Cheese.STILTON)) {
  System.out.println("That's right");
}
  • 컬렉션이나 배열같은 컨테이너가 비었을 때 null을 반환하는 메서드를 사용하면 위와 같은 방어 코드를 항상 넣어줘야 한다.

빈 컬렉션 반환 예제

  • 아래와 같이 빈 컬렉션이나 배열을 굳이 새로 할당하지 않고 반환할 수 있다.
public List<Cheese> getCheeses() {
  return new ArrayList<>(cheesesInStock);
}
  • 또는 항상 똑같은 빈 불변 컬렉션을 반환하는 방법도 있다.
    • Collections.emptyListCollections.emptySet 이 그 예다.

길이가 0인 배열 반환 예제

  • 배열을 쓸 때도 절대 null을 반환하지 말고 길이가 0인 배열을 반환하는 것이 좋다.
public Cheese[] getCheeses() {
  return cheesesInStock.toArray(new Cheese[0]);
}
  • 길이 0짜리 배열을 미리 선언해두고 매번 그 배열을 반환하는 방법도 있다.
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

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

옵셔널 반환은 신중히 하라

  • 자바 8 이전에는 메서드가 특정 조건에서 값을 반환할 수 없을 때 두가지 선택지가 있었다.
    • 예외를 던지거나, null을 반환하는 것이다.
    • 하지만 예외는 진짜 예외적인 상황에서만 사용해야 하며, 예외를 생성할 때 stack trace 전체를 캡쳐하므로 비용도 많이 든다.
    • null을 반환하는 경우는 별도의 null 처리 코드를 추가해야 한다.
  • 자바 8에서 Optional<T> 이 생기면서 null 이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않는 선택지가 생겼다.
  • Optional은 원소를 최대 1개 가질 수 있는 ‘불변’컬렉션이다.
    • Optional<T>Collection<T> 을 구현하지는 않았지만, 원칙적으로 그렇다는 의미다.

Optional의 사용

  • 보통은 T 를 반환해야 하지만 특정 조건에서는 아무것도 반환하지 않아야 할 때 T 대신 Optional<T> 를 반환하도록 선언하면 된다.
  • 옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 작다.
public static <E extends Comparable<E>> E max(Collection<E> c) {
  if (c.isEmpty()) {
    throw new IllegalArgumentException("빈 컬렉션");
  }
  
  E result = null;
  for (E e : c) {
    if (result == null || e.compareTo(result) > 0) {
      result = Objects.requireNonNull(e);
    }
  } 
  return result;
} 
  • 위 메서드에 빈 컬렉션을 건네면 IllegalArgumentException 을 던진다.
  • 이를 Optional을 반환하도록 수정하면 다음과 같다.
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
  if (c.isEmpty()) {
    return Optional.empty();
  }
  
  E result = null;
  for (E e : c) {
    if (result == null || e.compareTo(result) > 0) {
      result = Objects.requireNonNull(e);
    }
  } 
  return Optional.of(result);
}
  • 적절한 정적 팩터리를 사용해 옵셔널을 생성해 반환해주기만 하면된다.
    • 빈 옵셔널은 Optional.empty() 로 만들고, 값이 든 옵셔널은 Optional.of(value) 로 생성한다.
    • Optional.of(value) 에 null을 넣으면 NullPointerException 을 던지니 유의하자.
    • null 값도 허용하는 옵셔널을 만들려면 Optional.ofNullable(value)
  • 옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자.
    • 이는 옵셔널의 취지를 완전히 무시하는 것이다.

스트림과 옵셔널

  • 스트림의 종단 연산 중 상당수가 옵셔널을 반환한다.
  • 앞의 max 메서드를 스트림버전으로 다시 작성한다면 다음과 같다.
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
  return c.stream().max(Comparator.naturalOrder());
}
  • Streammax 연산이 우리에게 필요한 옵셔널을 생성해준다.

옵셔널 활용

  • null 을 반환하거나 예외를 던지는 대신 옵셔널 반환을 선택해야 하는 기준은 무엇일까?
  • 옵셔널은 검사 예외와 취지가 비슷하다. 즉, 반환값이 없을 수도 있음을 API 사용자에게 명확히 알려준다.
  • 메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 한다.

활용 1 - 기본값 설정

String lastWordInLexicon = max(words).orElse("단어 없음");
  • 기본값을 설정하는 비용이 커서 부담될 경우

    • Supplier<T> 를 인수로 받는 orElseGet 을 사용하면, 값이 처음 필요할 때 생성하므로 초기 설정 비용을 낮출 수 있다.

활용 2 - 예외 던지기

Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
  • 실제 예외가 아니라 예외 팩터리를 건넸다.
  • 이렇게 하면 예외가 실제로 발생하지 않는 한 예외 생성 비용이 들지 않는다.

활용 3 - 값 바로 꺼내기

Element lastNobleGas = max(Elements.NOBLE_GASES).get();
  • 옵셔널에 값이 채워져 있다고 확신한다면 곧바로 값을 꺼내 사용할 수 있다.
  • 잘못 판단해 옵셔널에 값이 없다면 NoSuchElementException 이 발생한다.

그 외 다양한 메서드

  • 더 특별한 쓰임에 대비한 메서드들도 준비되어 있다.

isPresent

  • 안전 밸브 역할의 메서드로, 옵셔널이 채워져 있으면 true를, 비어있으면 false를 반환한다.
  • isPresent 를 쓴 코드 중 상당수는 앞서 언급한 메서드들로 대체할 수 있으며 더 명확하고 용법에 맞는 코드가 되기 때문에 신중히 사용하는게 좋다.
Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("부모 PID: " + (parentProcess.isPresent() ? 
                                 String.valueOf(parentProcess.get().pid()) : "N/A"));

map

  • 위 코드는 map 메서드를 사용하여 아래와 같이 다듬을 수 있다.
System.out.println("부모 PID" + 
                   ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));

스트림에서의 활용

  • 스트림을 사용한다면 옵셔널들을 Stream<Optional<T>> 로 받아서 그 중 채워진 옵셔널들에서 값을 뽑아 Stream<T> 에 건네 담아 처리하는 경우도 있다.
streamOfOptionals
  .filter(Optional::isPresent)
  .map(Optional::get)
  • 자바 9에서는 Optionalstream() 메서드가 추가되었다.
    • OptionalStream 으로 변환해주는 어댑터이다.
    • 옵셔널에 값이 있으면 그 값을 원소로 담은 스트림으로, 없다면 빈 스트림으로 변환한다.
  • 이를 StreamflatMap 메서드와 조합하면 앞의 코드를 다음처럼 바꿀 수 있다.
streamOfOptionals.flatMap(Optional::stream)

그 외 유의사항

  • 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안된다.
    • Optional<List<T>> 를 반환하기보다는 빈 List<T> 를 반환하는 게 좋다.
  • 결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional<T> 을 반환한다.
  • Optional도 엄연히 새로 할당하고 초기화해야하는 객체이고, 그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한단계를 더 거치는 셈이다.
    • 따라서 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있다.
  • 박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수 밖에 없기 때문에 전용 옵셔널 클래스들이 존재한다.
    • OptionalInt , OptionalLong OptionalDouble 이 그것이다.
    • 이렇게 대체재까지 있으니 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 한다.
  • 옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는게 적절한 상황은 거의 없다.
    • 마찬가지로, 옵셔널을 인스턴스 필드에 저장해두는 것도 대부분 적절하지 않다.
    • 즉, 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.