BottleH Blog

이펙티브자바 3판 8장 정리

    Tags

  • Java
이펙티브자바 3판 8장 정리 thumbnail

8장 메서드

이번 장의 내용 중 상당부분은 메서드뿐 아니라 생성자에도 적용된다.

목차


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


매개변수 검사를 제대로 하지 못하는 경우 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다. 더 나쁜 상황은 메서드가 잘 수행되지만 잘못된 결과를 반환할 때다. 더더욱 나쁜 상황은 메서드는 문제없이 수행됐지만, 어떤 객체를 이상한 상태로 만들어놓아서 미래의 알 수 없는 시점에 이 메서드와는 관련 없는 오류를 낼 때다.

49-1. 문서화

publicprotected 메서드는 매개변수 값이 잘못됐을 때 던지는 예외를 문서화해야 한다.(@throws 자바독 태그 사용) 클래스 수준 주석은 그 클래스의 모든 public 메서드에 적용되므로 각 메서드에 일일이 기술하는 것보다 훨씬 깔끔한 방법이다.

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

49-2. 유효성검사 방법

  1. 자바 7에 추가된 java.util.Objects.requireNonNull
this.strategy=Objects.requireNonNull(strategy,"전략");

더이상 null 검사를 수동으로 하지 않아도 되며, 유연하다.

  1. 자바 9에서 추가된 Objects에 범위 검사 기능

checkFromIndexSize, checkFromToIndex, checkIndex 라는 메서드인데 null 검사 메서드만큼 유연하지는 않다. 예외 메시지를 지정할 수 없고 리스트와 배열 전용으로 설계됐다. 또한, 닫힌 범위(양 끝단 값을 포함하는)는 다루지 못한다.

  1. assert

공개되지 않은 메서드(private)라면 개발자가 직접 호출되는 상황을 통제할 수 있다. 따라서 오직 유효한 값만이 메서드에 넘겨지리라는 것을 여러분이 보증할 수 있고, 그렇게 해야 한다. 즉, 이럴 때는 assert를 사용하여 매개변수 유효성을 검사할 수 있다.

private static void sort(long a[], int offset, int length) { assert a != null; assert offset >=0 && offset <= a.length; ...// 계산수행 }

여기서 핵심은 위의 단언문들은 자신이 단언한 조건이 무조건 참이라고 선언한다는 것이다.

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

  • 실패하면 AssertionError를 던진다.
  • 런타임에 아무런 효과도, 아무런 성능 저하도 없다.
    • 단, -ea, --enableassertions 플래그 설정하면 런타임에 영향을 줌.

49-3. 유효성검사 예외

  1. 유효성검사 비용이 지나치게 높을 때
  2. 실용적이지 않을 때
  3. 계산과정에서 암묵적으로 검사가 수행될 때

✔ 예를 들어, Collections.sort(List)처럼 리스트를 정렬할 때는 정렬 과정에서 모든 객체가 상호 비교된다. 만일 비교할 수 없는 타입의 객체가 있으면 ClassCastException이 발생할 것이기 때문에 비교하기에 앞서 모든 원소를 검증하는 것은 불필요한 과정이 된다.

결론: 아이템49를 "매개변수에 제약을 두는 게 좋다"고 해석해서는 안 된다. 사실은 그 반대다. 메서드는 최대한 범용적으로 설계해야 한다.

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


자바는 네이티브(Native) 메서드를 사용하지 않아서 C, C++ 언어에서의 버퍼 오버런, 배열 오버런, 와일드 포인터 같은 메모리 충돌 오류에서 안전하다. 하지만 다른 클래스로부터의 침범을 아무런 노력없이 다 막을 수 있는 건 아니다. 따라서 클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.

50-1. 불변식을 지키지 못하는 경우

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가 가변이라는 사실을 이용하면 어렵지 않게 불변을 깨뜨릴 수 있다.

다행히 자바8 이후로는 쉽게 해결할 수 있다. Date 대신 불변인 Instant를 사용하자.(혹은 LocalDateTime, ZonedDateTime 사용) Date는 낡은 API이니 새로운 코드를 작성할 때는 더 이상 사용하면 안 된다. 하지만 Date처럼 가변인 낡은 타입을 쓰던 시절이 길어서 예전에 작성된 낡은 코드들을 대처해보자.

50-2. 방어적 복사

위의 Period 인스턴스의 내부를 보호하려면 외부의 공격으로부터 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수를 방어적으로 복사해

public Period(Date start, Date end) { this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (start.compareTo(this.end) > 0) { throw new IllegalArgumentException(start + "가 " + end + "보다 늦다."); } }

위와 같이 매개변수의 유효성을 검사하기 전에 복사본을 만들어야 한다. 멀티 스레드(Multi-Thread) 환경이라면 원본 객체의 유효성을 검사한 후에 복사본을 만드는 찰나의 순간에 다른 스레드가 원본 객체를 수정할 가능성이 있기 때문이다.

❗ 이와 같은 공격을 검사시점 / 사용시점(time-of-check / time-of-use) 공격 혹은 TOCTOU 공격이라고 한다.

50-2-1. 방어적 복사에서의 clone

매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다. 생성자에서와 다르게 접근자 메서드에서는 clone 메서드를 사용해도 된다. Date 객체가 반환될 것임이 확실하기 때문!

50-2-2. 접근자 메서드

위의 Period 메서드는 접근자(getter) 메서드가 내부의 가변 정보 직접 드러내기 때문에 공격이 가능하다.

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

방어적 복사는 성능 저하가 있을 수 있고 같은 패키지에 속하는 등의 이유로 클라이언트가 객체의 상태를 변경하지 않는 것이 확실하다면 방어적 복사본을 만들지 않아도 된다. 하지만 이러한 상황이라도 명확히 문서화하는 것이 좋다. 다른 패키지에서 사용한다고 해서 방어적 복사를 항상해야 하는 것도 아니다. 메서드 또는 생성자의 매개변수로 넘기는 행위 자체의 의미가 그 객체의 통제권을 명백히 이전함을 뜻하기도 한다.

결론: 클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야 한다. 되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다.

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


  1. 메서드 이름은 신중히 짓자.

    • 표쥰 명명 규칙에 따라야 한다.
    • 같은 패키지에 속한 다른 이름들과 일관되게 짓는 게 최우선 목표다.
    • 개발자 커뮤니티에서 널리 받아들여지는 이름을 사용하자.
    • 긴 이름은 피하자.
    • 애매하면 자바 라이브러리의 API 가이드를 참조하라.
  2. 편의 메서드를 너무 많이 만들지 말자.

    • 너무 많은 메서드는 그에 따른 문서화, 유지보수, 테스트를 요구한다.
    • 아주 자주 쓰일 경우에만 별도의 약칭 메서드를 두기 바란다.
    • 확신이 서지 않으면 만들지 말자.
  3. 매개변수 목록은 짧게 유지하자.

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

    • 예를 들어 HashMap 보다는 Map을 사용하는 편이 좋다.
    • 클래스를 사용하는 것은 클라이언트에게 특정 구현체만 사용하도록 제한하는 것이다.
  5. boolean 보다는 원소 2개짜리 enum이 더 낫다.

    • 메서드 이름상 boolean을 받아야 의미가 더 명확할 때는 예외다.
    • 열거 타입을 사용하면 코드를 읽고 쓰기가 더 쉬워지며, 나중에 선택지를 추가하기도 쉽다.

51-1. 매개변수 목록을 짧게 줄여주는 기술

51-1-1. 여러 메서드로 쪼갠다.

잘못하면 메서드가 너무 많아질 수 있지만 직교성을 높여 오히려 메서드 수를 줄여주는 효과도 있다.

ex) 리스트에서 특정 요소를 찾는다고 가정해보자. 이 기능을 하나의 메서드로 구현하려면 리스트의 시작과 끝 그리고 찾을 요소까지 총 3개의 매개변수가 필요하다. 하지만 List 인터페이스는 subListindexOf 메서드를 별개로 제공한다.

✔ 직교성: 소프트웨어적 관점에서의 직교성은 "공통점이 없는 기능들이 잘 분리되어 있다", "기능을 원자적으로 쪼개 제공한다."라는 뜻

51-1-2. 매개변수 여러 개를 묶어주는 도우미 클래스를 만들기

일반적으로 이런 도우미 클래스는 정적 멤버 클래스로 둔다.

51-1-3. 빌더 패턴

이 기법은 매개변수가 많을 때, 특히 그중 일부는 생략해도 괜찮을 때 도움이 된다.

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


다중 정의(overloading, 오버로딩)된 메서드의 호출 여부는 컴파일 타임에 정해진다.

52-1. 다중정의 예시

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

위 코드의 실제 출력 결과는 “그 외” 만 연달아 세 번 출력한다. 그 이유는 세 classify 중 어느 메서드를 호출할지가 컴파일 타임에 정해지기 때문이다. 컴파일 타임에서는 for 문 안의 c는 항상 Collection<?> 타입이다.

이처럼 직관과 어긋나는 이유는 재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택되기 때문이다. 만약 위 예시가 재정의가 되었다면 생각한대로 출력이 되었을 것이다. 반면, 다중정의된 메서드 사이에서는 객체의 런타임 타입은 전혀 중요치 않다. 선택은 컴파일타임에, 오직 매개변수의 컴파일타임 타입에 의해 이뤄진다.

52-2. 다중정의를 사용할 때 규칙

  1. 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자.
  2. 가변인수(varargs)를 사용하는 메서드라면 다중정의를 아예 하지 말아야 한다.
  3. 메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다.
    • 서로 다른 함수형 인터페이스라도 서로 근본적으로 다르지 않다는 뜻이다.

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

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


가변인수 메서드를 호출하면 인수의 개수와 길이가 같은 배열을 만들고 인수들을 만들어진 배열에 저장한 후에 가변인수 메서드에 전달해준다.

53-1. 인수 1개이상

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

인수가 1개 이상이어야 할 때는 평범한 매개변수를 추가함으로서, 코드를 깔끔히 만들자.

위와 같이 가변인수는 인수 개수가 정해지지 않았을 때 아주 유용하다.

53-2. 다중정의 활용

성능에 민감한 상황이라면 가변인수가 걸림돌이 될 수 있다.(호출될 때마다 배열 할당 및 초기화)

public void foo() {} public void foo(int arg1) {} public void foo(int arg1, arg2) {} public void foo(int arg1, arg2, arg3) {} public void foo(int arg1, arg2, arg3, int... restArg) {}

위와 같이 다중정의를 사용하면 성능이슈를 최대한 줄일 수 있다.

결론: 인수 개수가 일정하지 않은 메서드를 정의해야 한다면 가변인수가 반드시 필요하다. 메서드를 정의할 대 필수 매개변수는 가변인수 앞에 두고, 가변인수를 사용할 때는 성능 문제까지 고려하자.

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


다음은 주변에서 흔히 볼 수 있는 메서드다.(컬렉션이 비었으면 null 반환)

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

이렇게 작성을 하면, null 상황을 처리하는 방어코드를 추가로 작성해야 한다.

54-1. 빈 컨테이너 할당이 좋을까 null이 좋을까?

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

  • 성능분석 결과 이 할당이 성능 저하의 주범이라고 확인되지 않는 한, 이 정도의 성능 차이는 신경 쓸 수준이 못 된다.
  • 빈 컬렉션과 배열은 굳히 새로 할당하지 않고도 반환할 수 있다.

아래와 같이 사용하자.

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

가능성은 작지만, 사용 패턴에 따라 빈 컬렉션 할당이 성능을 눈에 띄게 떨어뜨릴 수도 있다.

✔ 이에 따른 해법은 아래와 같이 매번 똑같은 빈 '불변' 컬렉션을 반환하는 것이다.

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

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

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

이 방식이 성능을 떨어뜨릴 것 같다면 길이 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()]);

결론: null이 아닌, 빈 배열이나 컬렉션을 반환하라. null을 반환하는 API는 사용하기 어렵고, 오류 처리 코드도 늘어난다. 그렇다고 성능이 좋은 것도 아니다.

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


자바 8전에는 메서드가 특정 조건에서 값을 반환할 수 없을 때 예외를 던지거나 null을 반환했다. 하지만 예외는 진짜 예외적인 경우에만 사용해야 하며, 예외를 생성할 때 스택 추적 전체를 캡처하므로 비용도 만만치 않다. null은 이와 같은 문제는 생기지 않지만 NullPointerException과 null 처리 코드를 만들게 한다.

55-1. Optional<T>

자바8 이후로 또 하나의 선택지가 생겼는데 바로 Optional<T>이다.

Optional<T>

  • null이 아닌 T 타입 참조를 하나 담거나 혹은 아무것도 담지 않는 객체
    • 아무것도 담지 않은 옵셔널은 '비었다'고 말한다.
    • 어떤 값을 담은 옵셔널은 '비지 않았다'고 한다.
  • 원소를 최대 1개 가질 수 있는 '불변' 컬렉션이다.
  • 옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, 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; }

위 코드를 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.of(value)에 null을 넣으면 NullPointerException을 던진다.

옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자

55-2. Optional<T> 활용

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

  1. 기본값 설정
String lastWordInLexicon = max(words).orElse("단어 없음...");
  1. 상황에 맞는 예외 던지기

아래 코드는 실제 예외가 아니라 예외 팩터리를 건넨 것에 주목하자. 이렇게 하면 예외가 실제로 발생하지 않는 한 예외 생성 비용은 들지 않는다.

Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
  1. 항상 값이 채워져 있다고 가정

다만 잘못 한단한 것이라면 NoSuchElementException이 발생한다.

Element lastNobleGas = max(Elements.NOBLE_GASES).get();

✔ 기본값 설정 비용이 큰 경우

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

참고: 자바9에서는 Optional에 stream()메서드가 추가되었다.

55-3. Optional<T> 사용시 주의점

  1. 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너를 옵셔널로 감싸면 안된다. Optional<List<T>>를 반환하는 것보다 그저 빈 리스트 List<T>를 반환하는 것이 낫다. 빈 컨테이너를 그대로 반환하면 클라이언트에서는 옵셔널 처리 코드를 만들지 않아도 되기 때문이다.

    • ProcessHandle.Info 인터페이스의 arguments 메서드는 Optinal<String[]>을 반환하는 데 이는 예외적인 경우다.
  2. 옵셔널을 반환 타입으로 선언해야하는 경우: 결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional<T>를 반환한다.

    • 그렇지만 결국 옵셔널도 엄연히 새로 할당하고 초기화해야 하는 객체이고, 그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한 단계를 더 거치는 셈이다. 그래서 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있다.
  3. 박싱된 기본타입을 단은 옵셔널을 반환하는 일은 없도록 하자.

    • OptionalInt,OptionalLong, OptionalDouble 활용
    • 단, Boolean, Byte, Character, Short, Float은 예외일 수도 있다.
  4. 옵셔널을 맵의 값으로 사용하면 절대 안된다.

    • Map에 사용하는 것을 예로 들어보자. 맵 안에 키가 없다는 정의가 2가지가 된다. '키 자체가 없는 경우'와 '키는 있지만 속이 빈 옵셔널인 경우' 이렇게 모호해진다.
    • 옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.

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


API의 사용성을 높이려면 잘 작성된 문서도 있어야 한다. 자바에서 제공하는 자바독(javadoc)은 소스 코드 파일에서 문서화 주석(document comment)을 취합해서 API 문서로 만들어준다.

여러분의 API를 올바로 문서화하려면 공개된 모든 클래스, 인터페이스, 메서드 필드 선언에 문서화 주석을 달아야 한다. 메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 명료하게 기술해야 한다.

56-1. 태그

태그 용도 가이드
@param 모든 매개변수 관례상 명사구를 사용하며 마침표 없이 작성한다.
@return void가 아닌 반환 관례상 명사구를 사용하며 마침표 없이 작성한다.
메서드 설명과 같을 때는 생략할 수 있다.
@throws 발생할 가능성 있는 모든 예외 관례상 마침표 없이 작성한다.
@code 코드용 폰트로 렌더링 HTML 요소나 다른 자바독 태그를 무시한다.
@implSpec 구현스펙 안내 해당 메서드와 하위 클래스 사이의 계약 설명
@literal HTML 요소 무시 @code와 다르게 코드용 폰트로 렌더링하지 않는다.
@index 색인화 자바 9에서 추가되었으며 지정한 용어를 색인화할 수 있다.
@summary 요약 설명 해당 설명에 대한 요약(자바 10부터)

56-2. 사용 가이드

  1. 가독성 좋게 작성해야 한다.

  2. 한 클래스(혹은 인터페이스) 안에서 요약 설명이 똑같은 멤버(혹은 생성자)가 둘 이상이면 안된다.

    • 다중정의는 특히 더 조심할 것
  3. 제네릭 타입이나 제네릭 메서드를 문서화할 때는 모든 타입 매개변수에 주석을 달아준다.

  4. 열거 타입을 문서화할 때는 상수들에도 주석을 달아야 한다.

  5. 애너테이션 타입을 문서화할 때는 멤버들에도 모두 주석을 달아야 한다.

  6. 패키지 설명 문서 주석은 package-info.java에 작성한다.

    • 모듈 설명은 module-info.java 파일에 작성한다.
  7. 클래스 혹은 정적 메서드가 스레드 안전하든 그렇지 않든, 스레드 안전 수준을 반드시 API 설명에 포함해야 한다.

    • 직렬화 할 수 있는 클래스라면 직렬화 형태도 기술한다.
  8. 메서드 주석은 상속할 수 있다. 주석이 없는 경우 상위 클래스를 참고한다.

Written by@BottleH
Back-End Developer

GitHub