메서드 설계 시 주의할 점


49. 매개변수가 유효한지 검사하자.

매개변수의 불완전한 검사로 생기는 문제점은 다음과 같다.

매개변수의 검사에 실패하면 실패원자성을 어기는 결과를 낳을 수 있다.
실패 원자성이란, 호출된 메서드가 실행에 실패하더라도 객체 상태는 메소드 호출 전과 같아야 함을 뜻함.

매개변수 유효 검사할 때



50. 적시에 방어적 복사본을 만들자.

자바는 네이티브 메서드를 사용하지 않으니 C, C++ 같이 안전하지 않은 언어에서 흔히 보는 버퍼 오버런, 배열 오버런, 와일드 포인터 같은 메모리 충돌 오류에서 안전하다.
자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하더라도 그 불변식이 지켜진다.
하지만 아무런 노력 없이 다 막을 수 있는 것은 아니다.
클라이언트가 불변식을 깨뜨리려 한다고 가정하고 방어적으로 프로그래밍해야 한다.

기간을 표현하는 클래스: 불변식을 지키지 못한 경우

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

    public String toString() {
        return start + " - " + end;
    }
    
    . . .
    

이 클래스가 불변처럼 보이지만, Date가 가변이라는 사실을 이용하면 불변식을 깨뜨릴 수 있다.

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부를 수정했다. 

자바 8 이후로 다행히 Date 대신 불변인 Instant를 사용하면 된다.
(혹은 LocalDateTime, ZonedDateTime 사용 가능)
Date는 낡은 API 이므로 새로운 코드를 작성할 때 더 이상 사용하면 안 된다.

외부 공격으로부터 Period 인스턴스의 내부를 보호하려면
생성자에서 받은 가변 매개변수 각각을 방어적으로 복사(defensive copy)해야 한다.
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 + "보다 늦다.");
    }
}

매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성 검사를 했다.
순서가 부자연스러워보여도 반드시 이렇게 작성해야 한다.
멀티스레딩 환경에서 원본 객체의 유효성을 검사한 후 복사본을 만들면 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.

방어적 복사에 Date의 clone 메서드를 사용하지 않는데,
Date는 final이 아니기 때문에 clone이 Date가 정의한 게 아닐 수 있다.
즉, clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다.
이런 공격을 막기 위해 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.

복사본까지 만들었지만 아직 접근자 메서드가 있다.

public Date start() {
    return new Date(start.getTime());
}

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

생성자와 달리 접근자 메서드(getter)에서는 방어적 복사에 clone을 사용해도 된다.
Period가 가지고 있는 Date 객체가 java.util.Date임이 확실하기 때문이다.
하지만 인스턴스 복사에는 일반적으로 생성자나 정적 팩터리를 쓰는게 좋다.

모든 필드가 객체 안에 완벽하게 캡슐화되었다.


● 클래스의 가변, 불변 여부와 상관없이 가변인 내부객체를 클라이언트에 반환할 때는 반드시 심사숙고 하자.

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

● 방어적 복사를 생략해도 되는 상황은 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때, 혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때로 한정해야 한다.



51. 메서드 시그니처를 신중히 설계하자.



52. 다중정의는 신중히 사용하자.

오류가 있는 코드

public class CollectionClassifier {
    public static String classify(Set<?> s) {
        return "Set";
    }

    public static String classify(List<?> lst) {
        return "List";
    }

    public static String classify(Collection<?> c) {
        return "Unknown Collection";
    }

    public static void main(String[] args) {
        Collection<?>[] collections = {
                new HashSet<>(),
                new ArrayList<>(),
                new HashMap<String, String>().values()
        };

        for (Collection<?> c : collections)
            System.out.println(classify(c));
    }
}

이 코드에는 오류가 있다 무슨 오류가 있을까?

Set 혹은 List를 출력할것 같지만, 실제로 수행해보면 Unkown Collection만 출력한다.
그 이유는 다중정의(overloading)된 세 classify 중 어느 메서드를 호출할지가 컴파일타임에 결정되기 때문이다.

따라서 for문 안의 c는 항상 Collection 타입이다. 런타임에는 타입이 매번 달라지지만,
호출할 메서드를 선태하는 데에는 영향을 주지못한다.

따라서 컴파일타임의 매개변수 타입을 기준으로 항상 세 번째 메서드인 Classify(Collection<?>)만 호출한다.


직관과 어긋나는 이유

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


다중정의를 해도 괜찮은 경우

다중정의로 인한 혼란을 줄이기위한 방법들이 있다.


생성자

생성자는 이름을 다르게 지을 수 없으니 두 번째 생성자부터는 무조건 다중정의가 된다.
하지만 이는 정적 팩터리라는 대안을 활용할 수 있는 경우가 많다.


안전하게 다중정의 하기

매개변수 수가 같은 다중정의 메서드가 많더라도, 그중 어느 것이 주어진 매개변수 집합을 처리할지가 명확히 구분된다면, 헷갈릴 일은 없을 것이다. 즉, 매개변수 중 하나 이상이 “근본적으로 다르다(radically different)”면 가능하다.

근본적으로 다르단 말은 두 타입의 값을 어느쪽으로든 형변환이 불가능하다는 말이다.
ArrayList에는 int를 받는 생성자와 Collection을 받는 생성자가 있는데, 어떤 상황에서든 두 생성자가 헷갈릴 일은 없을것이다. (근본적으로 다름)


오토박싱으로 인한 오류

자바 1.5부터는 오토박싱과 오토 언박싱 이라는 개념이 추가 되었다.

Integer a = 3; // 내부적으로는 Integer a = new Integer(3); 으로 변환하여 동작
Object o = 3;  // 마찬가지로 Object o = new Integer(3); 으로 동작하여 다형성 적용

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);
        }
        System.out.println(set + " " + list);
    }
}

이 프로그램은 잘못된 결과를 출력한다.

list.remove()는 Object를 받는 경우와 index(int)를 받는 경우 두가지 형태로 다중정의 되어있는데,
여기서 넘긴 i값을 integer로 해석해서 인덱스로 삭제하는 일이 발생한다.

이처럼 제네릭과 오토박싱의 등장으로 이러한 피해를 입은 경우가 있다.


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

public boolean contentEquals(StringBuffer sb) {
	return contentEquals((CharSequence) sb);
}



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

컬렉션이 비었으면 null을 반환한다.

private final List<Cheese> cheesesInStock = new ArrayList<>();

public List<Cheese> getCheeses() {
    return cheesesInStock.isEmpty() ? null : new ArrayList<>();
}

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

List<Cheese> cheeses = shop.getCheeses();
if(cheeses != null && cheeses.contains(Chesses.STILTON)) {
    . . .
}

컬렉션이나 배열 같은 컨테이너(container)가 비었을 때 null을 반환하는 메서드를 사용할 때
항상 이와 같은 방어 코드를 넣어줘야 한다. 방어 코드를 빼면 오류가 발생할 수도 있다.


빈 컨테이너를 할당 비용 vs null 반환

빈 컨테이너를 할당하는 데도 비용이 드니 null을 반환하는 쪽이 낫다는 주장도 있는데 틀린 주장이다.

  1. 성능 분석 결과 이 할당이 성능 저하의 주범이라고 확인되지 않는 한,
    이 정도의 성능 차이는 신경 쓸 수준이 못 된다.

  2. 빈 컬렉션과 배열은 굳이 새로 할당하지 않고도 반환할 수 있다.


public List<Cheese> getCheeses() {
    return new ArrayList<>();
}

가능성은 작지만 사용 패턴에 따라 빈 컬렉션은 할당 하는 것은 성능을 떨어뜨릴 위험이 있다.

해법은 매번 똑같은 빈 불변 컬렉션을 반환하는 것이다.

Collections.emptyList 메서드나 Collections.emptySet 등을 사용하면 된다.


public List<Cheese> getCheeses() {
    return cheesesInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>();
}


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

길이 0짜리 배열을 미리 선언해두고 매번 그 배열을 반환하면 된다.
길이 0인 배열은 모두 불변이기 때문이다.


private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

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


return cheesesInStock.toArray(new Cheese[cheesesInStock.size()]);

단순히 성능을 개선할 목적이라면 toArray()에 넘기는 배열을 미리 할당하는 건 추천하지 않는다.
오히려 성능이 떨어 질 수 있다.

List.toArray(T[] a) 메서드는 주어진 배열 a가 충분히 크면 a 안에 원소를 담아 반환하고,
그렇지 않으면 T[] 타입 배열을 새로 만들어 그 안에 원소를 담아 반환한다.

따라서 원소가 하나라도 있다면 Cheese[] 타입의 배열을 새로 생성해 반환하고, 원소가 0개면 EMPTY_CHEESE_ARRAY를 반환한다.



55. Optional 반환은 신중히 하자.

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

두 방법 모두 허점이 있다.


예외와 null

예외는 진짜 예외적인 상황에서만 사용해야 한다.
예외를 생성할 때 스택 추적 전체를 캡처하므로 비용이 크기 때문이다.

null을 반환하면 이런 문제가 생기진 않지만, 별도의 null 처리 코드를 추가해야 한다.

null 처리를 무시하면 언젠가는 NullPointerException이 일어날 수 있다.
그것도 근본적인 원인에서 멀리 떨어진 곳에서 말이다.


Optional

Optional<T> 은 null이 아닌 T타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있다.

아무것도 담지 않은 옵셔널은 비었다라고 말하고, 반대로 어떤 값을 담은 옵셔널은 비지 않았다라고 한다.

옵셔널은 원소를 최대 1개 가질 수 있는 불변 컬렉션이다.

보통은 T를 반환해야 하지만, 특정 조건에서는 아무것도 반환하지 않아야 할 때 T 대신 Optional을 반환하도록 선언하면 된다. 그러면 유효한 반환값이 없을 때는 빈 결과를 반환하는 메서드가 만들어진다.

옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, 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;
}
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
  if (c.isEmpty())
    throw Optional.empty();
  
  E result = null;
  for (E e : c)
    if (result == null || e.compareTo(result) > 0)
      result = Objects.requireNonNull(e);
  return Optional.of(result);
}

옵셔널을 만들기 위해 두 가지 정적 팩터리 메서드를 사용 했다.


옵셔널 반환의 기준

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

메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 한다.

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

이렇게 하면 예외가 실제로 발생하지 않는 한 예외 생성 비용은 들지 않는다.

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


캐싱

Optional 기본값을 설정하는 비용은 아주 커서 부담이 될 때가 있다.

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

public class Main {
    // 캐싱
    private static final Supplier<Cheese> defaultCheese = Cheese::new;

    public static void main(String[] args) {
        getCheese().orElseGet(defaultCheese);
    }

    public static Optional<Cheese> getCheese() {
        return Optional.empty();
    }

}

class Cheese {
    . . .
}


Optional을 사용해야 하는 곳

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

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

이 경우 Optional을 반환한다.

그런데 이렇게 하더라도 Optional을 반환하는 데는 대가가 따른다.
Optional도 새로 할당하고 초기화해야 하는 객체이고, 그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한 단계를 더 거치는 셈이다.
그래서 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있다.


원시타입 Optional

박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다는 값을 두 겹이나 감싸기 때문에 무거울 수 밖에 없다.
그래서 int, long, double 전용 옵셔널 클래스들인 OptionalInt, OptionalLong, OptionalDouble이 있다. 이 옵셔널들도 Optional 가 제공하는 메서드를 거의 다 제공한다.

이렇게 대체제까지 있으니 박싱된 기본 타입을 담은 옵셔널을 반환하는 일도 없도록 하자.
단 ‘덜 중요한 기본 타입’용인 Boolean, Byte, Character, Short, Float은 예외일 수도 있다.


Map의 키로 옵셔널?

지금까지 옵셔널을 반환하고 반환된 옵셔널을 처리하는 이야기를 했고, 다른 쓰임에 관해서는 논하지 않았다.
왜냐하면 대부분 적절치 않기 때문이다.

예를 들어 옵셔널을 맵의 값으로 사용하면 절대 안 된다.
만약 그렇게 하면 맵 안의 키가 없다는 사실을 나타내는 방법이 두 가지가 된다.

쓸데 없이 복잡성만 높이게 된다.


인스턴스 변수로 옵셔널?

옵셔널을 인스턴스 필드에 저장해두는게 필요할 때가 있을까?

이런 상황 대부분은 필수 필드를 갖는 클래스와, 이를 확장해 선택적 필드를 추가한 하위 클래스를 따로 만들어야 함을 암시하는 나쁜 냄새다.
이전에 살펴봤던 영양소 관련 클래스인 NutritionFacts 클래스의 필드 대부분은 필수 값이 아니다.
또한 그 필드들은 기본 타입이라 값이 없음을 나타낼 방법이 마땅치 않다.

이런 경우라면 선택적 필드의 게터 메서드들이 옵셔널을 반환하게 해주는 것도 좋은 방법이기도 하다.