Effective Java 3rd - Ch08
포스트
취소

Effective Java 3rd - Ch08

Java 기초를 다지기 위해 효과적인 자바란 책을 공부 중입니다.

제8장 메서드

  • 메서드를 설계할 때 주의할 점들을 살펴본다
    • 매개변수와 반환값을 어떻게 처리해야 하는지
    • 메서드 시그니처는 어떻게 설계해야 하는지
    • 문서화는 어떻게
  • 사용성, 견고성, 유연성에 집중

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

메서드 몸체가 실행되기 전에 매개변수를 확인한다면 잘못된 값이 넘어왔을 때 즉각적이고 깔끔한 방식으로 예외를 던질 수 있다.

매개변수 검사를 제대로 하지 못하면 생기는 몇가지 문제

  • 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다
  • 더 나쁜 상황 : 메서드가 잘 수행되지만 잘못된 결과를 반환할 때
  • 심각히 나쁜 상황 : 메서드는 문제없이 수행됐지만, 어떤 객체를 이상한 상태로 만들어놓아서 미래의 알 수 없는 시점에 이 메서드와 관련 없는 오류를 낼 때

public과 protected 메서드는 매개변수 값이 잘못됐을 때 던지는 예외를 문서화해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
  * (현재값 mod m) 값을 반환한다. 이 메서드는
  * 항상 음이 아닌 BigInteger 를 반환한다는 점에서 remainder 메서드와 다르다.

  * @param m 계수 (양수여야 한다)
  * @return 현재값 mod m
  * @throws ArithmeticException m이 0보다 작거나 같으면 발생한다.
 **/
public BigInteger mod(BigInteger m) {
  if( m.signum() <= 0)
    throw new ArithmeticException("계수(m)는 양수여야 합니다. "+m);
  ... // 계산 수행
}

자바 7에 추가된 java.util.Objects.requireNonNull 메서드는 유연하고 사용하기도 편하니, 더 이상 null 검사를 수동으로 하지 않아도 된다.

코드 49-1 자바의 null 검사 기능 사용하기

1
2
// 반환값은 그냥 무시하고 필요한 곳 어디든 null 검사 목적으로 사용한다
this.strategy = Objects.requireNonNull(strategy, "전략");

:hand: 자바 9에서는 Objects 에 범위 검사 기능도 더해졌다.

  • checkFromIndexSize, checkFromToIndex, checkIndex 메서드들
  • 단, 예외 메시지를 지정할 수 없고, 리스트와 배열 전용으로 설계됐다
  • 또, 닫힌 범위(양 끝단값을 포함하는)는 다루지 못한다

코드 49-2 재귀 정렬용 private 도우미 함수

1
2
3
4
5
6
7
// 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;
  ... // 계산 수행
}

:bangbang:   이 단언문들은 자신이 단언한 조건이 무조건 참이라고 선언한다는 것이다.

단언문은 몇가지 면에서 일반적인 유효성 검사와 다르다.

  • 실패하면 AssertionError 를 던진다
  • 런타임에 아무런 효과도, 아무런 성능 저하도 없다
    • 단, java 실행시 명령줄에서 -ea 혹은 –enableassertions 플래스 설정하면 런타임에 영향을 준다

메서드가 직접 사용하지는 않으나 나중에 쓰기 위해 저장하는 매개변수는 특히 더 신경 써서 검사해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 코드 20-1 정적 팩토리 메서드
static List<Integer> intArrayAsList(int[] a) {
  // **NOTE: 만약 이 검사를 생략했다면
  // 새로 생성한 List 인스턴스가 사용되려 할 때 비로소 NullPointerException 이 발생한다
  // ==> 추적하기 어려워 디버깅이 상당히 괴로워질 수 있다
  Objects.requiredNonNull(a);

  return new AbstractList<>() {
    @Override public Integer get(int i) {
      return a[i];      // 오토 박싱
    }
    @Override public Integer set(int i, Integer val) {
      int oldVal = a[i];
      a[i] = val;       // 오토 언박싱
      return oldVal;    // 오토 박싱
    }
    @Override public int size() {
      return a.length;
    }
  };
}

생성자는 “나중에 쓰려고 저장하는 매개변수의 유효성을 검사하라” 는 원칙의 특수한 사례이다.
생성자의 매개변수 유효성 검사는 클래스 불변식을 어기는 객체가 만들어지지 않게 하는데 꼭 필요하다

메서드 몸체 실행전에 매개변수 유효성을 검사해야 한다는 규칙에도 예외가 있다.

  • 유효성 검사 비용이 지나치게 높거나 실용적이지 않을 때
  • 계산 과정에서 암묵적으로 검사가 수행될 때 (반면에 실패 원자성도 고려해야 한다)
    • ex: Collections.sort(List) 처럼 객체 리스트를 정렬하는 메서드
      • 모든 객체가 비교될 수 있는지 검사해봐야 별다른 실익이 없다

:bangbang:   메서드는 최대한 범용적으로 설계해야 한다.

  • 메서드가 건네 받은 값으로 무언가 제대로 된 일을 할 수 있다면 매개변수 제약은 적을수록 좋다
  • 반드시 매개변수에 제약을 두는게 좋다는 뜻으로만 해석하지는 말라

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

자바는 안전한 언어다. 메모리 전체를 하나의 거대한 배열로 다루는 언에서는 누릴 수 없는 강점이다. 하지만 아무리 자바라 해도 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있는건 아니다.

클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.

예컨데 기간을 표현하는 다음 클래스는 한번 값이 정해지면 변하지 않도록 할 생각이었다.

코드 50-1 기간을 표현하는 클래스 - 불변식을 지키지 못했다. (302-305쪽)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public final class Period {
    private final Date start;
    private final Date end;

    /**
     * @param  start 시작 시각
     * @param  end 종료 시각. 시작 시각보다 뒤여야 한다.
     * @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
     * @throws NullPointerException start나 end가 null이면 발생한다.
     */
    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;
    }
}

‘불변’인 Period의 내부를 공격하는 두 가지 예

코드 50-2 Period 인스턴스의 내부를 공격해보자. (첫번째 공격)

1
2
3
4
5
6
7
// **NOTE: Date 는 낡은 API 이니 더 이상 사용하면 안된다
//         자바 8 이후로는 'Instant'를 사용하면 된다.
Date start = new Date();    // Date 는 가변이라는 점을 이용
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78);  // p의 내부를 변경했다!
System.out.println(p);

외부 공격으로부터 Period 인스턴스의 내부를 보호하려면
생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(depensive copy)해야 한다.

코드 50-3 수정한 생성자 - 매개변수의 방어적 복사본을 만든다.

1
2
3
4
5
6
7
8
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 + "보다 늦다.");
}

매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자.

  • 순서가 부자연스러워 보이겠지만 반드시 이렇게 작성해야 한다
    • 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
    • 컴퓨터 보안 커뮤니티에서는 이를 검사시점/사용시점 공격 혹은 영어로 TOCTOU 공격이라 한다.

코드 50-4 Period 인스턴스를 향한 두 번째 공격 (305쪽)

1
2
3
4
5
start = new Date();
end = new Date();
p = new Period(start, end);
p.end().setYear(78);  // p의 내부를 변경했다!
System.out.println(p);

두번째 공격을 막아내려면, 접근자가 가변 필드의 방어적 복사본을 반환하면 된다.

코드 50-5 수정한 접근자 - 필드의 방어적 복사본을 반환한다. (305쪽)

1
2
3
4
5
6
7
public Date start() {
    return new Date(start.getTime());   // 복사본 반환
}

public Date end() {
    return new Date(end.getTime());     // 방어적 복사에 clone 을 사용해도 된다
}

변경될 수 있는 객체라면 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스가 문제없이 동작할지를 따져보라.

확신할 수 없다면 복사본을 만들어 저장해야 한다.
ex) 클라이언트가 건네준 객체를 내부의 Set 인스턴스에 저장하거나 Map 의 키로 사용한다면?

되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다

  • Date 대신에 Instant (혹은 LocalDateTime 이나 ZonedDateTime)를 사용
  • 방어적 복사에는 성능저하가 따르고 또 항상 쓸 수 있는 것도 아니다
    • 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 생략해도 된다 (상호 신뢰)
    • 대신 변경해서는 안된다고 문서화를 해라 (책임은 클라이언트에 있다고)

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

:hand: 참고 : 메서드 시그니처(method signature)

자바에서 메서드 정의에서의 메서드 이름과 매개변수 리스트의 조합을 말한다.
자바 컴파일러는 메서드 시그니처를 통해 메서드간의 차이를 식별하고 오버로딩을 처리한다.

:bangbang:   API 설계 요령들

  • 메서드 이름을 신중히 짓자 (명명규칙)
    • 커뮤니티에서 널리 받아들여지는 이름을 사용
    • 긴 이름은 피하자
  • 편의 메서드를 너무 많이 만들지 말자
    • 너무 많은 클래스는 익히고 사용하기 어렵다 (고통스럽다)
    • 확신이 서지 않으면 만들지 말자
  • 매개변수 목록은 짧게 유지하자 (4개 이하가 좋다)
    • 같은 타입의 매개변수 여러 개가 연달아 나오는 경우가 특히 해롭다

:hand: 과하게 긴 매개변수 목록을 짧게 줄여주는 기술 소개

  • 여러 메서드로 쪼갠다
    • 직교성을 높여 오히려 메서드 수를 줄여주는 효과도 있다
  • 매개변수 여러 개를 묶어주는 도우미 클래스를 만든다
    • 일반적으로 정적 멤버 클래스로 둔다
  • 앞서 두 기법을 혼합한 것으로 빌더 패턴을 메서드 호출에 응용
    • 매개변수가 많을 때, 특히 그 중 일부는 생략해도 괜찮을 때 도움이 된다
      • 모든 매개변수를 하나로 추상화한 객체로 정의하고
      • 클라이언트에서 이 객체의 세터(setter) 메서드를 호출해 필요한 값을 설정하게 하는 것

그 외 팁!

  • 매개변수의 타입으로는 클래스보다는 인터페이스가 더 낫다.
    • 예를 들어, HashMap 을 넘기기 보다는 Map 을 사용하자
    • 그러면 어떤 Map 구현체도 인수로 건넬 수 있다
  • boolean 보다는 원소 2개짜리 열거 타입이 낫다
    • 코드를 읽고 쓰기가 쉬워진다. 나중에 선택지를 추가하기도 쉽다.
    • ex) 온도계 클래스의 단위를 열거타입으로 화씨온도와 섭씨온도로 정의하여 사용
      • 개별 메소드 정의로 의존성 확장도 편해진다

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

아래 예제는 컬렉션을 집합, 리스트, 그 외로 구분하고자 만든 프로그램이다.

코드 52-1 컬렉션 분류기 - 오류! 이 프로그램은 무엇을 출력할까? (312쪽)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CollectionClassifier {
    // classify 함수들의 다중정의
    public static String classify(Set<?> s) {
        return "집합";
    }
    public static String classify(List<?> lst) {
        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));
    }
}

수정된 컬렉션 분류기 (314쪽)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FixedCollectionClassifier {
    public static String classify(Collection<?> c) {
        return c instanceof Set  ? "집합" :
                c instanceof List ? "리스트" : "그 외";
    }

    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));
    }
}

재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택되기 때문이다.

  • 메서드 재정의란 상위 클래스가 정의한 것과 똑같은 시그니처의 메서드를 하위 클래스에서 다시 정의한 것을 말한다.
  • 컴파일 타임에 그 인스턴스의 타입이 무엇이었냐는 상관없다.

코드 52-2 재정의된 메서드 호출 메커니즘 - 이 프로그램은 무엇을 출력할까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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) {
        // 저장하는 컬렉션이 Wine 타입이라 해도 호출될 함수는 생성시 인스턴스에 의해 결정된 사항
        List<Wine> wineList = List.of(
                new Wine(), new SparklingWine(), new Champagne());

        for (Wine wine : wineList)
            System.out.println(wine.name());
    }
}

안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자.

  • 다중정의가 혼동을 일으키는 상황을 피해야 한다
  • 다중정의하는 대신 메서드 이름을 다르게 지어주는 길도 항상 열려있다
    • ObjectOutputStream 클래스의 메서드들은 writeBoolean(boolean), writeInt(int), writeLong(long) 같은 식이다

한편, 생성자는 이름을 다르게 지을 수 없으니 두번째부터는 무조건 다중정의가 된다.

자바 5에서 오토박싱이 도입되면서 평화롭던 시대가 막을 내렸다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 이 프로그램은 무엇을 출력할까? (315-316쪽)
public class SetList {
    public static void main(String[] args) {
        // Integer 타입으로 생성
        Set<Integer> set = new TreeSet<>();
        List<Integer> list = new ArrayList<>();

        // add : { -3, -2, -1, 0, 1, 2 }
        for (int i = -3; i < 3; i++) {
            set.add(i);
            list.add(i);
        }
        // remove { 0, 1, 2 }
        for (int i = 0; i < 3; i++) {
            set.remove(i);      // ==> { -3, -2, -1 }
            list.remove(i);     // ==> { -2, 0, 2 }??
        }

        // List 의 경우 다중정의된 remove(int index)가 실행되었다
        System.out.println(set + " " + list);
    }
}
1
2
3
4
5
// 올바르게 수정된 코드
for( int i=0; i<3; i++){
  set.remove(i);
  list.remove((Integer) i);   // 혹은 remove(Integer.valueOf(i))
}

자바 8에서 도입한 람다와 메서드 참조 역시 다중정의 시의 혼란을 키웠다.

1
2
3
4
5
6
7
// 1번: Thread 의 생성자 호출
new Thread(System.out::println).start();

// Compile Error!!
// 2번: ExecutorService 의 submit 메서드 호출
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);   // Runnable<T>, Callable<T> 두가지가 있음

:hand: 참고1 : Java - (멀티쓰레딩 2) Executor 기본
:hand: 참고2 : A Guide to the Java ExecutorService

원인은 바로 submit 다중정의 메서드 중에는 Callable<T>를 받는 메서드도 있다는데 있다.
(Callable<T> 는 Future 객체를 반환한다)

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

  • 인수 위치가 같으면 혼란이 생긴다
  • 컴파일할 때 -Xlint:overloads 를 지정하면 이런 종류의 다중정의를 경고해줄 것이다

코드 52-3 인수를 포워드하여 두 메서드가 동일한 일을 하도록 보장한다

1
2
3
4
5
6
// String.contentEqauls 메소드나 CharSequence.contentEquals 메소드는 완전히 같은 작업을 수행한다
// 어떤 다중정의 메서드가 불리더라도 신경쓸게 없다
// <== 상대적으로 더 특수한 메서드에서 더 일반적인 다중정의 메서드로 일을 넘겨버린다 (forward)
public boolean contentEquals(StringBuffer sb){
    return contentEquals((CharSequence) sb);
}

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

가변인수 메서드는 명시한 타입의 인수를 0개 이상 받을 수 있다.

코드 53-1 간단한 가변인수 활용 예

1
2
3
4
5
6
7
8
// sum(1,2,3) 또는 sum() 등으로 사용됨
// 인수 개수는 런타임에 배열의 길이로 알 수 있다
static int sum(int... args) {
    int sum = 0;
    for (int arg : args)
        sum += arg;
    return sum;
}

코드 53-2 인수가 1개 이상이어야 하는 가변인수 메서드 - 잘못 구현한 예!

1
2
3
4
5
6
7
8
9
10
11
12
static int min(int... args) {
    // args 유효성 검사를 명시적으로 해야 한다
    if (args.length == 0)
        throw new IllegalArgumentException("인수가 1개 이상 필요합니다.");

    // min 의 초기값을 Integer.MAX_VALUE 로 설정해야 안전하게 사용 가능
    int min = args[0];
    for (int i = 1; i < args.length; i++)
        if (args[i] < min)
            min = args[i];
    return min;
}

코드 53-3 인수가 1개 이상이어야 할 때 가변인수를 제대로 사용하는 방법

1
2
3
4
5
6
7
8
// 첫번째 인수가 반드시 존재해야 함을 매개변수 정의시 명시
static int min(int firstArg, int... remainingArgs) {
    int min = firstArg;
    for (int arg : remainingArgs)
        if (arg < min)
            min = arg;
    return min;
}

성능에 민감한 상황이라면 가변인수가 걸림돌이 될 수 있다. (배열 할당과 초기화)
예를 들어 해당 메서드 호출의 95%가 인수를 3개 이하로 사용한다고 해보자.

1
2
3
4
5
6
public void foo() { }
public void foo(int a1) { }
public void foo(int a1, int a2) { }
public void foo(int a1, int a2, int a3) { }
// 매개변수 4개째부터 가변인수 사용 : 5% 이하이므로 괜찮다
public void foo(int a1, int a2, int a3, int... rest) { }

EnumSet 의 정적 팩토리도 이 기법을 사용해 열거타입 집합 생성비용을 최소화한다.

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

코드 54-1 컬렉션이 비었으면 null 을 반환한다 - 따라하지 말 것!

1
2
3
4
5
6
7
8
9
private final List<Cheese> cheesesInStock = ...;

/**
 * @return 매장 안의 모든 치즈 목록을 반환한다.
 *      단, 재고가 하나도 없다면 null 을 반환한다.
 **/
public List<Cheese> getCheeses() {
  return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}

재고가 없다고 해서 특별히 취급할 이유는 없는데도 이처럼 null 을 반환한다면
클라이언트는 null 상황을 처리하는 코드를 추가로 작성해야 한다.

1
2
3
4
List<Cheese> cheeses = shop.getCheeses();
// null 방어용 코드
if( cheeses != null && cheeses.contains(Cheese.STILTON))
  System.out.println("좋았어, 바로 그거야.");

빈 컨테이너 할당이 null 보다 비용이 드니 null 이 낫다는 주장에 대해

  • 이정도의 성능 차이는 신경쓸 수준이 못된다
  • 빈 컬렉션과 배열은 굳이 새로 할당하지 않고도 반환할 수 있다

코드 54-2 빈 컬렉션을 반환하는 올바른 예

1
2
3
public List<Cheese> getCheeses() {
    return new ArrayList<>(cheesesInStock);
}

집합이 필요하면 Collections.emptySet 을, 맵이 필요하면 Collections.emptyMap 을 사용하면 된다.

:hand: 참고 : Collections.emptyList() vs. New List Instance

코드 54-3 최적화 - 빈 컬렉션을 매번 새로 할당하지 않도록 했다

1
2
3
4
5
public List<Cheese> getCheeses() {
    // List<Cheese> 타입으로 자동 형변환 된다 (신기!)
    return cheesesInStock.isEmpty() ? Collections.emptyList()
            : new ArrayList<>(cheesesInStock);
}

배열을 쓸 때도, 절대 null 을 반환하지 말고 길이가 0인 배열을 반환하라.

코드 54-4 길이가 0 일 수도 있는 배열을 반환하는 올바른 방법

1
2
3
public Cheese[] getCheeses() {
    return cheesesInStock.toArray(new Cheese[0]);
}

코드 54-5 최적화 - 빈 배열을 매번 새로 할당하지 않도록 했다

1
2
3
4
5
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

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

코드 54-6 나쁜 예 - 배열을 미리 할당하면 성능이 나빠진다

1
2
3
// 성능을 위해 미리 배열을 할당해 놓는다는 이런 코드는 바람직하지 않다
// 오히려 성능이 떨어진다는 연구 결과도 있다
return cheesesInStock.toArray(new Cheese[cheesesInStock.size()]);

55) Optional 반환은 신중히 하라

자바 8 전에는 메서드가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지가 두가지 있었다.

  • 예외를 던지거나
    • 예외는 진짜 예외적인 상황에서만 사용해야 한다
  • null 을 반환하는 것
    • 별도의 null 처리 코드를 추가해야 한다

또 하나의 선택지로 자바 8 버전에서 Optional<T> 이 생겼다.

  • Optional 은 원소를 최대 1개 가질 수 있는 ‘불변’ 컬렉션이다
  • Optional 을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며
  • null 을 반환하는 메서드보다 오류 가능성이 작다

코드 55-1 컬렉션에서 최대값을 구한다 - 컬렉션이 비었으면 예외를 던진다

1
2
3
4
5
6
7
8
9
10
11
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;
}

코드 55-2 컬렉션에서 최대값을 구해 Optional로 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
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);

    // **주의: Optional.of(null) 을 던지면 NullPointerException 발생
    return Optional.of(result);
}

Optional 을 반환하는 메서드에서는 절대 null 을 반환하지 말자.
:arrow_right: 필요한 경우 Optional.ofNullable(value) 를 사용하면 된다.

스트림의 종단 연산 중 상당수가 Optional 을 반환한다.

코드 55-3 컬렉션에서 최댓값을 구해 Optional로 반환한다. - 스트림 버전

1
2
3
4
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
    // stream.max 메서드가 Optional<E> 를 반환
    return c.stream().max(Comparator.naturalOrder());
}

그렇다면 null 을 반환하거나 예외를 던지는 대신 Optional 반환을 선택해야 하는 기준은 무엇인가?

  • 반환값이 없을 수도 있음을 API 사용자에게 명확히 알려준다.
    • Optional 은 검사 예외와 취지가 비슷하다
  • 기본값을 설정하는 방법
  • 상황에 맞는 예외를 던질 수 있다
  • 항상 값이 채워져 있다고 가정한다

코드 55-4 옵셔널 활용 1 - 기본값을 정해둘 수 있다

1
String lastWordInLexicon = max(words).orElse("단어 없음...");

코드 55-5 옵셔널 활용 2 - 원하는 예외를 던질 수 있다

1
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

코드 55-6 옵셔널 활용 3 - 항상 값이 채워져 있다고 가정한다

1
2
// 없을 경우 NoSuchElementException 이 발생
Element lastNobleGas = max(Elements.NOBLE_GASES).get();

이따금 기본값을 설정하는 비용이 아주 커서 부담이 될 때,
Supplier 를 인수로 받는 orElseGet 을 사용하면 초기 설정 비용을 낮출 수 있다.

그외 쓰임새에 대비한 메서드들 : filter, map, flatMap, ifPresent

다음 코드는 부모 프로세스의 프로세스 ID를 출력하거나, 부모가 없다면 “N/A”를 출력한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 불필요하게 사용한 Optional의 isPresent 메서드를 제거하자. (329쪽)
// **NOTE: ProcessHandle 클래스는 자바 9에서 소개되었다
public class ParentPid {
    public static void main(String[] args) {
        ProcessHandle ph = ProcessHandle.current();

        // isPresent를 적절치 못하게 사용했다.
        Optional<ProcessHandle> parentProcess = ph.parent();
        System.out.println("부모 PID: " + (parentProcess.isPresent() ?
                String.valueOf(parentProcess.get().pid()) : "N/A"));

        // 같은 기능을 Optional의 map를 이용해 개선한 코드
        System.out.println("부모 PID: " +
            ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));
    }
}

스트림을 사용한다면 Optional 들을 Stream<Optional<T>> 로 받아서,
그중 채워진 Optional 들에서 값을 뽑아 Stream<T> 에 건네 담아 처리하는 경우

1
2
3
4
// 자바 8에서 구현할 수 있는 형식
streamOfOptionals
    .filter(Optional::isPresent)
    .map(Optional::get)

자바 9에서는 Optional 에 stream() 메서드가 추가되었다.
이 메서드는 Optional 을 Stream 으로 변환해주는 어댑터다.

1
2
3
// 자바 9에서 stream 함수로 구현하면
streamOfOptionals
    .flatMap(Optional::stream)

컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안된다.

  • 반환값으로 옵셔널을 사용한다고 해서 무조건 득이 되는 건 아니다
  • 빈 Optional<List<T>> 를 반환하기 보다는 빈 List<T> 를 반환하는게 좋다
    • 빈 컨테이너를 그대로 반환하면 클라이언트에 옵셔널 처리 코드를 넣지 않아도 된다

:bangbang:   그렇다면 어떤 경우에 메서드 반환 타입을 T 대신 Optional<T> 로 선언해야 할까?

  • 결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional<T> 를 반환한다
  • 성능이 종요한 상황에서는 옵셔널이 맞지 않을 수 있다

박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수밖에 없다. (값을 두 겹이나 감싸기 때문)
:arrow_right: int, long, double 전용 옵셔널 클래스들 : OptionalInt, OptionalLong, OptionalDouble
그러니 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자.

:bangbang:   Optional 을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는게 적절한 상황은 거의 없다.
예컨데, 옵셔널을 맵의 값으로 사용하면 절대 안된다.

:bangbang:   Optional 을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.
옵셔널을 인스턴스 필드에 저장해 두는게 필요할 때가 있을까? 대부분 불필요하지만 간혹 있다.

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

여러분의 API를 올바로 문서화 하려면

  • 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야 한다.
  • 메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 명료하게 기술해야 한다.
  • 한 클래스(혹은 인터페이스) 안에서 요약 설명이 똑같은 멤버가 둘 이상이면 안된다.
  • 클래스 혹은 정적 메서드가 스레드 안전하든 그렇지 않든, 스레드 안전 수준을 반드시 포함해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// 문서화 주석 예 (333-341쪽)
public class DocExamples<E> {
    // 메서드 주석 (333-334쪽)
    /**
     * Returns the element at the specified position in this list.
     *
     * <p>This method is <i>not</i> guaranteed to run in constant
     * time. In some implementations it may run in time proportional
     * to the element position.
     *
     * @param  index index of element to return; must be
     *         non-negative and less than the size of this list
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException if the index is out of range
     *         ({@code index < 0 || index >= this.size()})
     */
    E get(int index) {
        return null;
    }
    // 한글 버전 (334쪽)
    // /**
    //  * 이 리스트에서 지정한 위치의 원소를 반환한다.
    //  *
    //  * <p>이 메서드는 상수 시간에 수행됨을 보장하지 <i>않는다</i>. 구현에 따라
    //  * 원소의 위치에 비례해 시간이 걸릴 수도 있다.
    //  *
    //  * @param  index 반환할 원소의 인덱스; 0 이상이고 리스트 크기보다 작아야 한다.
    //  * @return 이 리스트에서 지정한 위치의 원소
    //  * @throws IndexOutOfBoundsException index가 범위를 벗어나면,
    //  * 즉, ({@code index < 0 || index >= this.size()})이면 발생한다.
    //  */
    // E get(int index) {
    //     return null;
    // }


    // 자기사용 패턴 등 내부 구현 방식을 명확히 드러내기 위해 @implSpec 사용 (335쪽)
    /**
     * Returns true if this collection is empty.
     *
     * @implSpec This implementation returns {@code this.size() == 0}.
     *
     * @return true if this collection is empty
     */
    public boolean isEmpty() {
        return false;
    }
    // 한글 버전 (335쪽)
    // /**
    //  * 이 컬렉션이 비었다면 true를 반환한다.
    //  *
    //  * @implSpec 이 구현은 {@code this.size() == 0}의 결과를 반환한다.
    //  *
    //  * @return 이 컬렉션이 비었다면 true, 그렇지 않으면 false
    //  */
    // public boolean isEmpty() {
    //     return false;
    // }


    // 문서화 주석에 HTML이나 자바독 메타문자를 포함시키기 위해 @literal 태그 사용 (336쪽)
    /**
     * A geometric series converges if {@literal |r| < 1}.
     */
    public void fragment() {
    }
    // 한글 버전 (336쪽)
    // /**
    //  * {@literal |r| < 1}이면 기하 수열이 수렴한다.
    //  */
    // public void fragment() {
    // }

    // 문서화 주석 첫 '문장'에 마침표가 있을 때 요약 설명 처리 (337쪽)
    /**
     * A suspect, such as Colonel Mustard or {@literal Mrs. Peacock}.
     */
    public enum Suspect {
        MISS_SCARLETT, PROFESSOR_PLUM, MRS_PEACOCK, MR_GREEN, COLONEL_MUSTARD, MRS_WHITE
    }
    // 한글 버전 (337쪽)
    // /**
    //  * 머스타드 대령이나 {@literal Mrs. 피콕} 같은 용의자.
    //  */
    // public enum Suspect {
    //     MISS_SCARLETT, PROFESSOR_PLUM, MRS_PEACOCK, MR_GREEN, COLONEL_MUSTARD, MRS_WHITE
    // }


    // 자바독 문서에 색인 추가하기 - 자바 9부터 지원 (338쪽)
    /**
     * This method complies with the {@index IEEE 754} standard.
     */
    public void fragment2() {
    }
    // 한글 버전 (338쪽)
    // /**
    //  * 이 메서드는 {@index IEEE 754} 표준을 준수한다.
    //  */
    // public void fragment2() {
    // }


    // 열거 상수 문서화 (339-340쪽)
    /**
     * An instrument section of a symphony orchestra.
     */
    public enum OrchestraSection {
        /** Woodwinds, such as flute, clarinet, and oboe. */
        WOODWIND,

        /** Brass instruments, such as french horn and trumpet. */
        BRASS,

        /** Percussion instruments, such as timpani and cymbals. */
        PERCUSSION,

        /** Stringed instruments, such as violin and cello. */
        STRING;
    }
    // 한글 버전 (340쪽)
    // /**
    //  * 심포니 오케스트라의 악기 세션.
    //  */
    // public enum OrchestraSection {
    //     /** 플루트, 클라리넷, 오보 같은 목관악기. */
    //     WOODWIND,
    //
    //     /** 프렌치 호른, 트럼펫 같은 금관악기. */
    //     BRASS,
    //
    //     /** 탐파니, 심벌즈 같은 타악기. */
    //     PERCUSSION,
    //
    //     /** 바이올린, 첼로 같은 현악기. */
    //     STRING;
    // }


    // 애너테이션 타입 문서화 (340쪽)
    /**
     * Indicates that the annotated method is a test method that
     * must throw the designated exception to pass.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface ExceptionTest {
        /**
         * The exception that the annotated test method must throw
         * in order to pass. (The test is permitted to throw any
         * subtype of the type described by this class object.)
         */
        Class<? extends Throwable> value();
    }
    // 한글 버전 (341쪽)
    // /**
    //  * 이 애너테이션이 달린 메서드는 명시한 예외를 던져야만 성공하는
    //  * 테스트 메서드임을 나타낸다.
    //  */
    // @Retention(RetentionPolicy.RUNTIME)
    // @Target(ElementType.METHOD)
    // public @interface ExceptionTest {
    //     /**
    //      * 이 애너테이션을 단 테스트 메서드가 성공하려면 던져야 하는 예외.
    //      * (이 클래스의 하위 타입 예외는 모두 허용된다.)
    //      */
    //     Class<? extends Throwable> value();
    // }
}

 
 

끝!   읽어주셔서 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.