Effective Java - Chapter 8

Updated:

Effective Java

[Effective Java 3/E] 8장 메서드

이번 장에서는 메서드를 설계할때 주의할점, 그리고 관례에 대하여 다루었습니다. 그 중

  1. 매개변수와 반환값 처리
  2. 메서드 시그니처 설계
  3. 문서화
    에 대해 자세히 다루어 보았습니다.

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

메서드의 입력 매개변수가 특정 조건을 만족해야 할 시에, 검사를 가능한 빨리 하여 오류를 사전에 잡아야한다.
매개 변수 검사에 실패하면 실패 원자성을 어기는 결과를 낳게 된다.
따라서, 매개변수 제약을 문서화 하고, 이를 어기면 던지는 예외도 문서화를 해야한다.

매개변수가 유효한지 확인하는 방법

  • 자바의 null 검사 기능 사용
public void someMethod(Integer val) {
  Integer integer = Objects.requireNonNull(val, "매개변수가 null!");
  System.out.println(integer);
}
  • Objects에 범위 검사 기능

리스트와 배열 전용으로 설계된 범위 검사 가능이다.
checkFromIndexSize, checkFromToIndex, checkIndex을 사용한다.

List<String> list = List.of("a", "b", "c");
// Exception in thread "main" java.lang.IndexOutOfBoundsException: 
//      Index 4 out of bounds for length 3
Objects.checkIndex(4, list.size());
  • 단언문(assert) 사용

private로 공개되지 않은 메서드이면 assert를 사용하여 유효성을 검사한다.
넘어온 매개변수가 조건식을 참으로 만들지 않으면 AssertionError를 던진다.

private void someMethod(int arr[], int length) {
    assert arr != null;
    assert length >= 0 && arr.length == length;

    // do something
}

매개변수 유효성 검사를 하지 않는 경우

유효성을 검사하는 비용이 지나치게 큰 경우 또는 계산 과정에서 암묵적으로 유효성 검사가 진행될 때는 유효성 검사를 건너 뛸 수도 있다.
예를 들어 Collections.sort(List)처럼 리스트를 정렬할 때는 정렬 과정에서 모든 객체가 상호 비교된다.
만일 비교할 수 없는 타입의 객체가 있으면 ClassCastException이 발생할 것이기 때문에 비교하기에 앞서 모든 원소를 검증하는 것은 불필요한 과정이 된다.

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

클래스가 클라이언트로부터 받거나 클라이언트에게 반환하는 구성 요소가 가변적이라면 그 요소는 반드시 방어적으로 복사해야 한다.

생성자로 받은 가변 매개변수 각각을 방어적 복사 해야한다.

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 + " after " + end);
      }
      this.start = start;
      this.end = end;
  }
  public Date start() { return start; }
  public Date end() { return end; }
  // ... 생략
}
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end().setMonth(3);    // period의 내부를 수정했다.

Period 생성자의 매개변수로 주어진 end Date 객체를 외부에서 변경하면, Period 내부의 Date 객체도 같이 변경 된다. -> 불변성을 해치는 상황!

// 방어적 복사를 적용한 생성자
public Period(Date start, Date end) {
  this.start = new Date(start.getTime());
  this.end = new Date(end.getTime());

  // 유효성 검사 전에 복사해야 한다. 
  if(start.compareTo(end) > 0) {
      throw new IllegalArgumentException(start + " after " + end);
  }
}

new 키워드를 통하여 새로운 Date객체를 만들어 이를 Period의 인스턴스로 생성한다.
clone을 사용하지 않는 이유는 Date의 재정의한 clone 메서드가 외부에서 악용을 위해 정의하였을 수도 있다.

접근자로 주어지는 인스턴스를 방어적 복사하여 전달한다.

Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
// period의 인스턴스를 수정하여 불편식을 해치고 있다. 
period.end().setMonth(3);

Period의 인스턴스를 반환하는 end() getter 메서드를 통하여 인스턴스에 접근하였다.
근데 클라이언트가 setMonth(3)를 통하여 내부를 수정하는 결과를 초래하였다.

따라서, getter로 객체의 인스턴스를 전달 할 때에도 방어적 복사를 이용하여 수정을 하지 못하게 막는다.

public Date start() { 
  return new Date(start.getTime()); // 새로운 Date 객체를 만들어 반환
}
public Date end() { 
  return new Date(end.getTime());
}

접근자 메서드에서는 clone을 통하여 방어적 복사를 사용해도 된다.

방어적 복사를 하지 않아도 될 때

방어적 복사 비용이 너무 크거나, 클라이언트가 컴포넌트 내부를 수정하지 않는다고 신뢰할 경우에는 생략해도 된다.
또한, 구성요소를 수정 하였을 때 영향이 그 클라이언트에만 국한되어있다면 생략해도 된다.
대신 문서화를 하여 명시를 해야하는 것을 잊지말자.

Item 51 - 매서드 시그니처를 신중히 설계하라

이번 장은 다양한 설계 요령들을 정리 하였다.

메서드 이름을 신중히 짓자

항상 표준 명명 규칙을 따르자.
같은 패키지에 속한 다른 이름과 일관되게 짓자.
널리 쓰이는 이름이나 자바 라이브러리의 API 가이드를 참고하여 작명하자.

편의 메서드를 너무 많이 만들지 말자

클래스 혹은 인터페이스의 메서드가 많아지면 유지보수 하기에 어렵다.
아주 자주 쓰이는 것들만 별도의 약칭을 두어 메서드로 사용하자.

매개변수 목록은 짧게 유지하자

매개변수는 4개 이하가 좋다.
매개변수가 많아지면, 사용자들이 API를 사용하는데 혼돈이 오며 특히, 같은 타입의 매개변수가 여러개가 연달아 나오는 등의 경우 잘못 된 인자로 전달하여 의도와 다르게 동작할 수가 있다.
매개변수를 줄이는 방법은 다음과 같이 3가지가 있다.

  1. 여러 메서드로 나누어 본다.

독립된 기능을 하는 메서드로 구분을 하자.

List<String> list = Lists.of("a", "b", "c", "d");
// 전체가 아닌 지정된 범위의 부분 리스트에서 인덱스를 찾는 경우
List<String> newList = list.subList(1, 3);  // 부분 리스트 추룰
int index = newList.indexOf("b");           // 추출한 리스트에서 인덱스 찾기
  1. 도우미(Helper) 클래스를 만들자
// 기존 메서드
public void someMethod(String a, String b, String c, String d) {
  // do something
}

// Helper 클래스 적용
class HelperClass {
  String a;
  String b;
  String c;
  String d;
}

public void someMethod(HelperClass someHelper) {
  // do something
}
  1. 빌더 패턴을 이용하자

Item 2. 생성자에 매개변수가 많다면 빌더를 고려하라

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();

매개변수의 타입으로는 클래스보다는 인터페이스가 더 낫다

예를 들어 HashMap 보다는 Map을 사용하는 편이 좋다.
그렇게 되면, HashMap 뿐만 아니라, TreeMap, ConcurrentHashMap, 기타 Map 구현체도 인수로 건넬 수 있다.
클래스를 사용하는 것은 클라이언트에게 특정 구현체만 사용하도록 제한하는 것이다.

또한 boolean 보다는 원소 2개짜리 열거 타입이 낫다.
코드가 더 명확해지며 의미가 분명해 진다. 또한 또다른 선택지를 열거타입에 상수를 추가하는 방법으로 간단히 구현이 가능해 진다.

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

재정의(overriding): 런타임에 동적으로 선택
다중 정의(overloading): 컴파일 타임에 호출 여부가 정해진다.

class ColectionClassifier {
  public static String classify(Set<?> set) {
    return "집합";
  }

  public static String classify(List<?> list) {
    return "리스트";
  }

  public static String classify(Collection<?> collection) {
    return "그 외"
  }

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

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

컴파일 타임에서는 for 문 안의 c는 항상 Collection<?> 타입이기 때문에, 위 코드의 출력 결과는 “그 외” 만 연달아 세 번 출력한다.

이처럼 오버로딩이 혼란을 일으키지 않도록 프로그래밍해야 한다.
특히, 매개변수 수가 같은 다중정의는 피해야 한다.
더불어 가변인수를 사용하는 메서드는 다중정의를 아예 하면 안된다.

따라서, 다중정의를 하는 대신에 메서드 이름을 다르게 짓는 방법을 사용하자.
ObjectOutputStream 클래스를 보면 다중정의 대신에 메서드의 이름을 다르게 지었다.
writeBoolean(boolean), writeInt(int),writeLong(long) 처럼 말이다.

위의 경우, 짝 클래스인 ObjectInputStream은 readBoolean(), readInt() 처럼 짝에 맞는 메서드를 가지고 있으므로, 사용자가 더 이해하기 쉽다.

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

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

인수가 1개 이상이어야 할 때는 아래와 같이 가변인수 앞에 필수 매개변수를 받도록한다.

static int min(int firstArg, int... remainingArgs) {
  int min = firstArg;
  for (int arg : remainingArgs) {
    if (arg < min) {
      min = arg;
    }
  }
  return min;
}

가변인수 메서드가 호출될 때마다 배열을 새로 할당하고 초기화한다는 이유로, 성능에 해가 될 수 있기 때문에, 성능이 걱정된다면 아래와 같은 패턴으로 해결한다.

public void foo() {}
public void foo(int arg1) {}
public void foo(int arg1, arg2) {}
public void foo(int arg1, arg2, arg3) {}  // 메서드 호출의 95% 이상이 3개 이하의 인수를 사용
public void foo(int arg1, arg2, arg3, int... restArg) {}

Item 54 - Null이 아닌, 빈 컬렉션이나 배열을 반환하라

어떤 클래스의 List 인스턴스를 반환하는 getter가 있다고 하자.

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

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

만약 인스턴스가 비어있다면, null을 반환한다.
이는 위 API를 사용하는 클라이언트에서 NULL을 방어하는 코드를 추가해주어야하는 번거로움이 있다.

NULL 반환과 빈 컨테이너의 반환의 성능차이는 큰 차이가 없어, 굳이 null을 반환하지 않아도 된다.

public List<Cheese> getCheeses() {
  // cheesesInStock의 값이 없다면 빈 컨테이너 반환
  return new ArrayList<>(cheesesInStock); 
}

빈 컬렉션의 할당이 성능을 저하 시킬수 있다고 판단되면, 매번 같은 불변 컬렉션 을 반환하면 된다.

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

배열의 경우, null 대신에 길이가 0인 배열을 반환하면 된다.
성능이 걱정된다면, 길이 0짜리 배열을 미리 선언하고 그 배열을 반환한다.

// 매번 새로 할당하지 않게 하는 방법
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

public Cheese[] getCheeses() {
  return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
  // 다음과 같이 미리 할당하는 것은 성능을 저하한다.
  // return cheesesInStock.toArray(new Cheese[cheesesInStock.size()]);
}

반환하는 배열의 길이가 0이상이면, Cheese[] 배열을 새로 만들어 그 안에 원소를 담고, 원소가 0개면 EMPTY_CHEESE_ARRAY을 반환하게 된다.

Item 55 - 옵셔널 반환은 신중히 하라

메서드가 특정 조건에서 값을 반환 할 수 없을 때, 자바 8 전에는

  1. null 반환
  2. throw Exception

의 2가지 방법이 있었다. 하지만 예외는 진짜 예외적인 경우에만 사용해야 하며, null은 NullPointerException과 null 처리 코드를 만들게 한다.

Optional

Optional<T> : null이 아닌 T 타입 참조를 하나 담거나 또는 아무것도 담지 않는 객체이다. 원소를 최대 1개 가질 수 있는 불변 컬렉션이며, null-safe를 보장한다.

// 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);   // null을 넣으면 안된다.
}

// 옵셔널 + 스트림
public static <E extends Comparable<E>>
  Optional<E> max(Collection<E> c) {
  return c.stream().max(Comparator.naturalOrder());
}

public static void main(String[] args) {
  List<Integer> comp = Arrays.asList(1, 5, 23, 6, 7);
  System.out.println(max(comp));  // output : Optional[23]
}

다양한 Optional Method 들

  • Optional.empty()
    내부 값이 비어있는 Optional 객체 반환

  • Optional.of(T value)
    내부 값이 value인 Optional 객체 반환 만약 value가 null인 경우 NullPointerException 발생

  • Optional.ofNullable(T value)
    가장 자주 쓰이는 Optional 생성 방법 value가 null이면, empty Optional을 반환하고, 값이 있으면 Optional.of로 생성

  • T get()
    Optional 내의 값을 반환
    만약 Optional 내부 값이 null인 경우 NoSuchElementException 발생

  • boolean isPresent()
    Optional 내부 값이 null이면 false, 있으면 true
    Optional 내부에서만 사용해야하는 메서드라고 생각

  • void ifPresent(Consumer<? super T> consumer)
    Optional 내부의 값이 있는 경우 consumer 함수를 실행

  • Optional<T> filter()
    Optional에 filter 조건을 걸어 조건에 맞을 때만 Optional 내부 값이 리턴
    조건이 맞지 않으면 Optional.empty를 리턴

  • Optional<U> map()
    Optional 내부의 값을 Function을 통해 가공

  • T orElse(T other)
    Optional 내부의 값이 있는 경우 그 값을 반환
    Optional 내부의 값이 null인 경우 other을 반환

  • T orElseGet(Supplier<? extends T> supplier)
    Optional 내부의 값이 있는 경우 그 값을 반환
    Optional 내부의 값이 null인 경우 supplier을 실행한 값을 반환

  • T orElseThrow(Supplier<? extends X> exceptionSupplier)
    Optional 내부의 값이 있는 경우 그 값을 반환
    Optional 내부의 값이 null인 경우 exceptionSupplier을 실행하여 Exception 발생
    인자를 주어지지 않을시, Optional 내부의 값이 null인 경우 NoSuchElementException 발생

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

API에는 반드시 잘 작성된 문서가 존재해야 한다.
자바에서 제공하는 javadoc은 소스 코드 파일에서 문서화 주석(document comment)을 취합해서 API 문서로 만들어준다.

태그 용도 설명
@param 모든 매개변수 매개 변수에 대한 설명을 표시한다.
@return void가 아닌 반환 반환 값에 대한 설명(데이터 유형 및 범위 등)을 표시 할 때 사용한다.
@throws 발생할 가능성 있는 모든 예외 태그는 발생할 수 있는 예외에 대한 설명을 표시 할 때 사용한다.
@exception 발생할 가능성 있는 모든 예외 @throws 태그와 동일하게 사용한다
@code 코드용 폰트로 렌더링 Javadoc에 예제 코드 작성시 사용된다, HTML 요소나 다른 자바독 태그를 무시한다.
@literal HTML 요소 무시 @code와 다르게 코드용 폰트로 렌더링하지 않는다.
@implSpec 구현스펙 안내 해당 메서드와 하위 클래스 사이의 계약 설명
@index 색인화 지정한 용어를 색인화할 수 있다.
@inheritDoc 상속 상위 타입의 문서화 주석 일부를 재사용한다.
@summary 요약 설명 해당 설명에 대한 요약

출처

Joshua Bloch. Effective Java 3/E. n.p.: 인사이트, 2018년 11월 1일.

Reference