ch8 메서드 - LenKIM/everyone-is-effective-java-study GitHub Wiki

메서드를 설계할 때 주의할 점

구체적으로는 매개변수와 반환값을 어떻게 처리하는지, 메서드 시그니처는 어떻게 설계해야 하는지? 문서화는 어떻게 해야하는지,

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

  • 제약조건은 반드시 문서화해야 하며 메서드 몸체가 시작되기 전에 검사해야 한다.
  • "오류는 가능한 한 빨리 (발생한 곳에서)잡아야 한다"

매개변수 검사를 제대로 하지 못하면 몇 가지 문제가 생길 수 있다.

  1. 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.
  • public과 protected 메서드는 매개변수 값이 잘못됐을 때 던지는 예외를 문서화해야 한다.
    • 보통은 IllegalArgumentException, IndexOutOfBoundsException, NullPointerException 중 하나
    • 매개변수의 제역을 문서화한다면 그 제약을 어겼을 때 발생하는 예외도 함께 기술
/**
* 항상 음이 아닌 BigInteger를 반환한다는 점에서 remainder 메서드와 다르다.
* 
* @param m 계수(양수여야 한다.)
* @return 현재 값 mod m
* @throws java.lang.ArithmeticException m이 0보다 작거나 같으면 발생한다.
*/
public BigInteger mode(BigInteger m){
if (m.signum() <= 0)
throw new ArithmeticException("계수(M)는 양수여야 합니다. " + m)
... // 계산 수행
}
  • java.util.Objects.requireNonNull 메서드는 유연하고 사용하기도 편하니, 더 이상 null검사를 수동으로 하지 않아도 된다. 원하는 예외 메시지도 지정할 수 있다. 또한 입력을 그대로 반환하므로 값을 사용하는 동시에 null 검사를 수행.
  • 생성자는 "나중에 쓰려고 저장하는 매개변수의 유효성을 검사하라"는 원칙의 특수한 사례. 생성자 매개변수는 유효성 검사는 클래스 불변식을 어기는 객체가 만들어지지 않게 하는 데 꼭 필요하다.
  • 메서드 몸체 실행 전에 매개변수 유효성을 검사해야 한다는 규칙에도 예외는 있다. 유효성 검사 비용이 지나치게 높거나 실용적이지 않을 때, 혹은 계산과정에서 암묵적으로 검사가 수행 될 때.
  • 메서드나 생성자를 작성할 때면 그 매개변수들에 어떤 제약이 있을지 생각해야 한다. 그 제약들을 문서화하고 메서드 코드 시작 부분에서 명시적으로 검사해야 한다. 이런 습관을 반드시 기르도록 하자. 그 노력은 유효성 검사가 실제 오류를 처음 걸러낼 때 충분히 보상받을 것.

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

  • 클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.
// 코드 50-1 기간을 표현하는 클래스 - 불변식을 지키지 못했다. (302-305쪽)
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;
    }
...

그렇지만, 이를 공격할 수 있다.

Date start = new Date(2020, Calendar.JUNE,21);
Date end = new Date(2020, Calendar.JUNE,23);
Period p = new Period(start, end);
end.setYear(2030);

System.out.println(p.end().getYear()); // 2030
  • Date는 낡은 API이니 새로운 코드를 작성할 때는 더 이상 사용하면 안된다.
  • 외부 공격으로부터 Period 인스턴스 내부를 보호라혀면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(defensive copy)해야 한다.
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 = new Date(start.getTime());
        this.end   = new Date(end.getTime());
    }
}

이렇게 할 경우 더 이상 Period에 위협이 되지 않는다. 매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고 이 복사본으로 유효성을 검사한 점에 주목하자.

이렇게 하더라도 멀티 스레드 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 반드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있다. 그러므로, 방어적 복사를 매개변수 유효성 검사전에 수행하면 이런 위험에서 해방될 수 있다.

이를 검사시점/사용시점(time-of-check/time-of-use) 공격 혹은 영어 표기를 줄여서 TOCTOU 공격

  • 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안된다.

    Date start = new Date(2020, Calendar.JUNE,21);
            Date end = new Date(2020, Calendar.JUNE,23);
            Period p = new Period(start, end);
            p.end().setYear(2030);

    이렇게도 공격할 수 있기 때문에 막아내려면 단순히 접근자가 가변 필드의 방어적 복사본을 반환하면 된다.

    public Date start() {
            return new Date(start.getTime());
        }
        public Date end() {
            return new Date(end.getTime());
        }
  • 매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위해서만은 아니다. 메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다.

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

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

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

  • 메서드 이름을 신중히 짓자. (명명 규칙은 아이템 68)

  • 편의 메서드를 너무 많이 만들지 말자. 아주 자주 쓰일 경우에만 별도의 약칭 메서드를 두기 바라며, 확신이 서지 않으면 만들지 말자.

  • 매개변수 목록은 짧게 유지하자. 4개 이하가 좋다. 같은 타입의 매개변수 여러 개가 연달아 나오는경우가 특히 해롭다.

  • 과하게 긴 매개변수 목록을 짧게 줄여주는 기술 세 가지

    1. 여러 메서드를 쪼갠다.
    2. 매개변수 여러 개를 묶어주는 도우미 클래스를 만드는 것. 일반적으로 이런 도우미 클래스는 정적 멤버 클래스로 둔다. 특히 잇따른 매개변수 몇 개를 독립된 하나의 개념으로 볼 수 있을 때 추천하는 기법.
    3. 두 기법을 혼합한 것. 객체 생성에 사용한 빌더 패턴을 메서드 호출에 응용한다고 보면 된다. 이 기법은 매개변수가 많을 때, 특히 그중 빙부가 생략해도 괜찮을 때 도움.
  • 매개변수의 타입으로는 클래스보다는 인터페이스가 더 낫다. 매개변수로 적합한 인터페이스가 있다면 (이를 구현한 클래스가 아닌) 그 인터페이스를 직접 사용하자. (HashMap 대신 Map 을 사용.)

  • boolean 보다는 원소 2개짜리 열거 타입이 낫다.(이름상 boolean을 받아야 의미가 더 명확할 때는 예외)

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

// 코드 52-1 컬렉션 분류기 - 오류! 이 프로그램은 무엇을 출력할까? (312쪽)
public class CollectionClassifier {
    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));
    }
}

결과는 "집합", "리스트", "그 외"를 차례로 출력할 것 같지만, 실제로 수행해보면 "그 외"만 세 번 연달아 호출된.

다중정의된 세 classify 중 어느 메서드를 호출할지가 컴파일타임에 정해지기 때문. 타입이 결정되는 건 런타임에 달려있다.

컴파일 타입에는 Collection<?> 에서 ? 이므로 무조건 그외 호출

이처럼 직관과 어긋나는 이유는 재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택되기 때문.

메서드를 재정의했다면 해당 객체의 런타임 타입이 어떤 메서드를 호출할지의 기준이 된다. 모두 알다시피, 메서드 재정의란 상위 클래스가 정의한 것과 똑같은 시그니처의 메서드를 하위 클래스에서 다시 정의한 것을 말한다.

class Wine {
    String name() { return "포도주"; }
}

// 재정의된 메서드 호출 메커니즘 (313쪽, 코드 52-2의 일부)
class SparklingWine extends Wine {
    @Override String name() { return "발포성 포도주"; }
}

// 재정의된 메서드 호출 메커니즘 (313쪽, 코드 52-2의 일부)
class Champagne extends SparklingWine {
    @Override String name() { return "샴페인"; }
}

// 재정의된 메서드 호출 메커니즘 (313쪽, 코드 52-2의 일부)
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());
    }
}

이 프로그램은 "포도주", "발포성 포도주", "샴페인"을 차례로 출력.

위 프로그램은, for 문에서의 컴파일 타임 타입이 모두 Wine인 것에 무관하게 항상 '가장 하위에서 정의한'재정의 메서드가 실행되는 것.

첫번째 코드의 문제점을 해결하려면?

public static String classify(Collection<?> c) {
        return c instanceof Set  ? "집합" :
                c instanceof List ? "리스트" : "그 외";
    }

와 같은 방식을 시도하자.

그러나, 다중정의는 위 예시의 이유에서든 API 사용자가 혼동을 일으키므로, 그런 상황을 최대한 피해야 한다.

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

가변인수를 사용하는 메서드라면 다중정의를 아예 하지말아야 한다.

  • 다중정의하는 대신 메서드 이름을 다르게 지어주는 길도 항상 열려 있으니 말이다.

  • 가변인수(vars)를 사용하는 메서드라면 다중정의를 아예 하지 말아야 한다. 이 규칙만 잘 따르면 어떤 다중정의 메서드가 호출될지 헷갈릴 일은 없을 것이다.

package net.slipp.franchise;

import net.minidev.json.JSONUtil;
import org.assertj.core.util.Strings;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collector;
import java.util.stream.Collectors;

// 이 프로그램은 무엇을 출력할까? (315-316쪽)
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);
        }
        list.forEach(System.out::println);
        for (int i = 0; i < 3; i++) {
            set.remove(i);
            list.remove(i);
        }
        System.out.println(set + " " + list);
    }
}


// list.remove 는 인덱스를 나타냄.

E remove(int index);

boolean remove(Object o);
new Thread(System.out::println).start();
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);

2번째에서 컴파일 오류가 발생.

  • 메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다. 이 말은 서로 다른 함수형 인터페이스라도 서로 근본적으로 다르지 않다는 뜻.

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

// 가변인수 활용 예 (320-321쪽)
public class Varargs {
    // 코드 53-1 간단한 가변인수 활용 예 (320쪽)
    static int sum(int... args) {
        int sum = 0;
        for (int arg : args)
            sum += arg;
        return sum;
    }
}

만약 인수가 1개 이상이여야 할 때도 있다. 예컨데 최솟값을 찾는 메서드인데 인수를 0개만 받을 수도 있도록 설계하는 건 좋지 않다

//    // 코드 53-2 인수가 1개 이상이어야 하는 가변인수 메서드 - 잘못 구현한 예! (320쪽)
//    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;
//    }
// 코드 53-3 인수가 1개 이상이어야 할 때 가변인수를 제대로 사용하는 방법 (321쪽)
static int min(int firstArg, int... remainingArgs) {
  int min = firstArg;
  for (int arg : remainingArgs)
    if (arg < min)
      min = arg;
  return min;
}

public static void main(String[] args) {
  System.out.println(sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
  System.out.println(min(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
}

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

https://johngrib.github.io/wiki/null-object-pattern/

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

메서드가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지는 두가지

  1. 예외를 던지거나
  2. null을 반환하는 것.

위 두가지 모두 허점이 있는데, 예외는 진짜 예외적인 상황에서만 사용해야 하며, 예외를 생성할 때 스택 추적 전체를 캡처하므로 비용 많이 듬.

그러나 자바 8부터는 하나의 옵션이 더 생겼는데, 그것은 바로 Optional<T> 를 반환하는 것.

// 코드 55-1 컬렉션에서 최댓값을 구한다. - 컬렉션이 비었으면 예외를 던진다. (327쪽)
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;
}

이 부분을 Optional<E>로 반환하는 값을 만들 경우

// 코드 55-2 컬렉션에서 최댓값을 구해 Optional<E>로 반환한다. (327쪽)
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);
}

주의 할점은 옵셔널을 반환하나는 메서드에서는 절대 null을 반환하지 말자. 옵셔널을 도입한 취지를 완전히 무시하는 행위.

// 코드 55-3 컬렉션에서 최댓값을 구해 Optional<E>로 반환한다. - 스트림 버전 (328쪽)
public static <E extends Comparable<E>>
  Optional<E> max(Collection<E> c) {
  return c.stream().max(Comparator.naturalOrder());
}

옵셔널은 검사 예외와 취지가 비슷하다. 즉 반환 값이 없을 수도 있음을 API 사용자에게 명확히 알려준다.

알려주는 방법으로 .get() .orElase(), .orElseThrow .isPresent() 등 있다.

// 불필요하게 사용한 Optional의 isPresent 메서드를 제거하자. (329쪽)
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"));
    }
}

자바 9에서는 Optional에 stream() 메서드가 추가되었다. 이 메서드는 Optioanl을 Stream으로 변환해주는 어탭터이다. 옵셔널에 값이 있으면 그 값을 원소로 담는 스트림으로, 값이 없다면 빈 스트림으로 변환. 이를 Stream의 flatMap 메서드와 조합하면 앞의 코드를 다음처럼 명료하게 바꿀수 있음

StreamofOptionals.flatMap(Optional::stream)

그리고, 반환값으로 옵셔널을 사용한다고 해서 무조건 득이 되는 건 아니다. 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안 된다. 빈 Optional<List<T>>를 반환하기보다는 빈 List<T> 반환하는 게 좋다.

어떤 경우에 옵셔널로 선언해야 될까?

  • 결과가 없을 수도 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional<T> 반환

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

⚠️ **GitHub.com Fallback** ⚠️