본문 바로가기
스터디/이펙티브 자바

[이펙티브 자바] 07. 메서드

by 디토20 2022. 9. 21.
반응형

 

 

[이펙티브 자바] 07. 메서드

 

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

  • 메서드와 생성자 대부분은 입력 매개변수의 값이 특정 조건을 만족하기를 바란다.
  • 이런 제약은 반드시 문서화해야 하며 메서드 몸체가 시작되기 전에 검사해야 한다.
  • "오류는 가능한 한 빨리 (발생한 곳에서) 잡아야 한다"는 일반 원칙의 한 사례이기도 하다.
  • 오류를 발생한 즉시 잡지 못하면 해당 오류를 감지하기 어려워지고, 감지하더라도 오류의 발생 지점을 찾기 어려워진다.
  • 메서드 몸체가 실행되기 전에 매개변수를 확인한다면 잘못된 값이 넘어왔을때 즉각적이고 깔끔한 방식으로 예외를 던질 수 있다.

 

매개변수 검사를 제대로 하지 못했을 때 생기는 문제

  • 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.
  • 메서드는 잘 수행되었지만 잘못된 결과를 반환하는 경우도 있는데, 미래에 이 메서드와는 관련 없는 오류를 낼 위험이 있다.
  • 즉, 매개변수 검사에 실패하면 실패 원자성(failure atomicity)을 어기는 결과를 낳을 수 있다.
  • public과 protected 메서드는 매개변수 값이 잘못됐을 때 던지는 예외를 문서화해야 한다(@throws 자바독 태그를 사용하면 된다).
  • 매개변수의 제약을 문서화한다면 그 제약을 어겼을 때 발생하는 예외도 함께 기술해야 한다. 아래는 그 예시다.
  /**
     * (현재 값 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);
        }
        // 계산 수행
        return null; // 편의상 null을 리턴했다.
    }

 

  • 이 메서드에서 m이 null이면 NullPointerException을 던진다는 말은 설명에 없다. 그 이유는 이 설명을 개별 메서드가 아닌 BigInteger 클래스 수준에서 기술했기 때문이다.
  • 클래스 수준 주석은 그 클래스의 모든 public 메서드에 적용되므로 각 메서드에 일일이 기술하는 것보다 훨씬 깔끔한 방법이다.
  • 자바 7에 추가된 java.util.Objects.requireNonNull 메서드는 유연하고 사용하기도 편하니, 더 이상 null 검사를 수동으로 하지 않아도 된다 -> 원하는 예외 메시지도 지정할 수 있고, 입력을 그대로 반환하므로 값을 사용하는 동시에 null 검사를 수행할 수 있다. 
this.strategy = Objects.requireNonNull(strategy, "전략");

 

  • 반환값은 무시하고 필요한 곳 어디서든 순수한 null 검사 목적으로 사용해도 된다.

 

단언문(assert)을 사용한 매개변수 유효성 검증

  • 공개되지 않은 메서드라면 메서드가 호출되는 상황을 통제할 수 있다. 즉  유효한 값만이 메서드에 넘겨지리라는 것을 보증할 수 있다.
  • public이 아닌 메서드라면 단언문(assert)을 사용해 매개변수 유효성을 검증할 수 있다.

재귀 정렬용 private 도우미 함수

    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;
        // 계산 수행
    }

 

  • 이 단언문들은 자신이 단언한 조건이 무조건 참이라고 선언한다. 참이 아닐경우 AssertionError가 발생한다.
  • 단언문은 일반적인 유효성 검사와 다르다.
  1. 실패하면 AssertionError를 던진다.
  2. 런타임에 아무런 효과도, 아무런 성능 저하도 없다(단, java를 실행할 때 명령줄에서 -ea 혹은 --enableassertions 플래그 설정하면 런타임에 영향을 준다).

 

나중에 쓰기 위해 저장하는 매개변수의 유효성을 검사하라

  • 메서드가 직접 사용하지는 않으나 나중에 쓰기 위해 저장하는 매개변수는 특히 더 신경써서 검사해야 한다. 한참 뒤에서야 문제가 발생할 수 있다.
  • 생성자는 "나중에 쓰려고 저장하는 매개변수의 유효성을 검사하라"는 원칙의 특수한 사례다.
  • 생성자 매개변수의 유효성 검사는 클래스 불변식을 어기는 객체가 만들어지지 않게 하는 데 꼭 필요하다.
  • "메서드 몸체 실행 전에 매개변수 유효성을 검사해야 한다"는 규칙에도 예외가 있다. 유효성 검사 비용이 지나치게 높거나 상용적이지 않을 때, 혹은 계산 과정에서 암묵적으로 검사가 수행될 때다.
  • 다만, 암묵적 유효성 검사에 너무 의존하면 실패 원자성을 해칠 수 있으니 주의해야 한다.

 

 

API 문서에 정의된 예외와 다른 예외가 발생할 경우

  • 계산 중 잘못된 매개변수 값을 사용해 발생한 예외가 API 문서에서 던지기로 한 예외와 다를 수 있다.
  • 이럴 때는 예외 번역(exception translate, 또는 예외 전환) 관용구를 사용하여 API 문서에 기재된 예외로 번역해줘야 한다.

 

 

 

 

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

자바는 안전한 언어이다. 자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 그 불변식이 지켜진다. 메모리 전체를 하나의 거대한 배열로 다루는 언어에서는 누릴 수 없는 강점이다.

 

방어적 프로그래밍

그러나 자바라 해도 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있는 것은 아니다.

  • 따라서, 클라이언트가 불변식을 깨뜨리려한다고 가정하고 방어적으로 프로그래밍해야 한다.
  • 악의적인 공격으로부터 방어하지 못하거나, 또는 실수로 클래스를 오작동하게 만들 수도 있다.
  • 어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다. 하지만 주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 허락 하는 경우가 생긴다.

 

기간을 표현하는 클래스 - 불변식을 지키지 못했다.

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 + "after " + end);
        }
        this.start = start;
        this.end = end;
    }

    public Date start() {
        return start;
    }

    public Date end() {
        return end;
    }
}

 

  • 얼핏 이 클래스는 불변 같고, 시작 시각이 종료 시각보다 늦을 수 없다는 불변식이 무리 없이 지켜질 것 같다.
  • 하지만 이 클래스는 Date가 가변이라는 사실을 이용하면 어렵지 않게 불변식을 깨뜨릴 수 있다.

 

Period 인스턴스의 내부를 공격해보자.

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

 

  • 물론 자바 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 (start.compareTo(end) > 0) {
        throw new IllegalArgumentException(start + "after" + end);
    }
}

 

  • 매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사했다.
  • 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문에 반드시 해당 순서를 지켜야 한다.
  • Date는 final이 아니므로 clone이 Date가 정의한 게 아닐 수 있다. 즉, clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다. 따라서 Date의 clone 메서드를 사용하지 않았다.
  • 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.

 

Period 인스턴스를 향한 접근자 메서드 공격

Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
period.end().setYear(78); // period의 내부를 변경했다!

 

  • 접근자 메서드가 내부의 가변 정보를 직접 드러내기 때문에 Period 인스턴스는 아직도 변경 가능하다.
  • 이 공격을 막아내려면 단순히 접근자가 가변 필드의 방어적 복사본을 반환하면 된다.

 

수정한 접근자 - 필드의 방어적 복사본을 반환한다

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

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

 

  • 생성자와 달리 접근자 메서드에서는 방어적 복사에 clone을 사용해도 된다. Period가 가지고 있는 Date 객체는 java.util.Date임이 확실하기 때문이다(신뢰할 수 없는 하위 클래스가 아니다).
  • 그렇더라도 인스턴스를 복사하는 데는 일반적으로 생성자나 정적 팩터리를 쓰는 게 좋다.
  • 매개변수를 방어적으로 복사하는 목적에는 불변 객체를 만들기 위해서만은 아니다.
  • 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다.
  • 변경될 수 있는 객체라면 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스가 문제없이 동작할지를 따지고, 확신할 수 없다면 복사본을 만들어 저장해야 한다.
  • 클래스가 불변이든 가변이든, 가변인 내부 객체를 클라이언트에 반환할 때는 반드시 심사숙고해야 한다. 안심할 수 없다면 (원본을 노출하지 말고) 방어적 복사본을 반환해야 한다.
  • 길이가 1 이상인 배열은 무조건 가변이다. 따라서 내부에서 사용하는 배열을 클라이언트에 반환할 때는 항상 방어적 복사를 수행해야 한다.

 

방어적 복사의 생략

  • 방어적 복사에는 성능 저하가 따르고, 또 항상 쓸 수 있는 것도 아니다. (같은 패키지에 속하는 등의 이유로) 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략할 수 있다.
  • 호출자에서 해당 매개변수나 반환값을 수정하지 말아야 함을 명확히 문서화하는 게 좋다.
  • 다른 패키지에서 사용한다고 해서 넘겨받은 가변 매개변수를 항상 방어적으로 복사해 저장해야 하는 것은 아니다.
  • 메서드나 생성자의 매개변수로 넘기는 행위가 그 객체의 통제권을 명백히 이전함을 뜻하기도 한다.
  • 통제권을 이전하는 메서드를 호출하는 클라이언트는 해당 객체를 더 이상 직접 수정하는 일이 없다고 약속해야 한다.
  • 클라이언트가 건네주는 가변 객체의 통제권을 넘겨받는다고 기대하는 메서드나 생성자에서도 그 사실을 확실히 문서에 기재해야 한다.
  • 통제권을 넘겨 받기로 한 메서드, 생성자를 가진 클래스들은 취약하다. 따라서 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때,혹은 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될ㅜ때로 한정해서 방어적 복사를 생략해도 된다.

 

 

 

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

개별 아이템으로 두기 애매한 API 설계 요령들을 모아 배우기 쉽고, 쓰기 쉬우며, 오류 가능성이 적은 API를 만들어 보자.

 

메서드 이름을 신중히 짓자.

  • 항상 표준 명명 규칙을 따라야 한다. 이해할 수 있고, 같은 패키지에 속한 다른 이름들과 일관되게 짓는 게 최우선 목표다.
  • 긴 이름은 피하고 개발자 사이에서 널리 받아들여지는 이름을 사용하자. 애매하면 자바 라이브러리의 API 가이드를 참조하도록 한다.

 

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

  • 메서드가 너무 많은 클래스는 익히고, 사용하고, 문서화하고, 테스트하고, 유지보수하기 어렵다. 인터페이스도 마찬가지다.
  • 클래스나 인터페이스는 자신의 각 기능을 완벽히 수행하는 메서드로 제공해야 한다.
  • 아주 자주 쓰일 경우에만 별도의 약칭 메서드를 두도록 한다. 확신이 서지 않으면 만들지 않도록 한다.

 

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

  • 4개 이하가 좋다. 일단 4개가 넘어가면 매개변수를 전부 기억하기가 쉽지 않다.
  • 같은 타입의 매개변수 여러 개가 연달아 나오는 경우가 특히 해롭다.
  • 사용자가 매개변수 순서를 기억하기도 어렵고, 실수로 순서를 바꿔 입력해도 그대로 컴파일되고 실행된다. 그리고, 의도와 다르게 동작한다.

 

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

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

  • 쪼개진 메서드 각각은 원래 매개변수 목록의 부분집합을 받는다.
  • 잘못하면 메서드가 너무 많아질 수 있지만, 직교성(orthohonality)을 높여 오히려 메서드 수를 줄여주는 효과도 있다.
"직교성이 높다"라고 하면 "공통점이 없는 기능들이 잘 분리되어 있다" 혹은 "기능을 원자적으로 쪼개 제공한다" 정도로 해석할 수 있다.
기능을 원자적으로 쪼개다 보면, 자연스럽게 중복이 줄고 결합성이 낮아져 코드를 수정하기 수월해지며 테스트하기 쉬워진다.
일반적으로 직교성이 높은 설계는 가볍고 구현하기 쉽고 유연하고 강력하다. 단, API가 다루는 개념의 추상화 수준에 맞게 조절해야 한다.
또한 특정 조합의 패턴이 상당히 자주 사용되거나 최적화하여 성능을 크게 개선할 수 있다면 직교성이 낮아지더라도 편의 기능으로 제공하는 편이 나을 수도 있다.

 

2. 매개변수 여러 개를 묶어주는 도우미 클래스를 만든다.

3. 객체 생성에 사용한 빌더 패턴을 메서드 호출에 응용한다.

  • 이 기법은 매개변수가 많을 때, 특히 그중 일부는 생략해도 괜찮을 때 도움이 된다.
  • 먼저 모든 매개변수를 하나로 추상화한 객체를 정의하고, 클라이언트에서 이 객체의 세터(setter) 메서드를 호출해 필요한 값을 설정하게 한다.
  • 이때 각 세터 메서드는 매개변수 하나 혹은 서로 연관된 몇 개만 설정하게 한다.
  • 클라이언트는 먼저 필요한 매개변수를 다 설정한 다음, 앞어 설정한 매개변수들의 유효성을 검사한다.
  • 마지막으로, 설정이 완료된 객체를 넘겨 원하는 계산을 수행한다.

 

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

  • 매개변수로 적합한 인터페이스가 있다면 (이를 구현한 클래스가 아닌) 그 인터페이스를 직접 사용하도록 한다.
  • ex) 메서드에 HashMap을 넘길 일은 전혀 없다. 대신 Map을 사용한다. 그러면 다른 Map 구현체도 인수로 건넬 수 있다.
  • 인터페이스 대신 클래스를 사용하면 클라이언트에게 특정 구현체만 사용하도록 제한하는 꼴이며, 혹시라도 입력 데이터가 다른 형태로 존재한다면 명시한 특정 구현체의 객체로 옮겨 담느라 비싼 복사 비용을 치러야 한다.

 

boolean보다는 원소 2개짜리 열거 타입이 낫다.

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

 

 

 

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

메서드 재정의(Override)

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

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) {
        List<Wine> wineList = List.of(
                new Wine(), new SparklingWine(), new Champagne());

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

 

  • 예상대로 이 프로그램은 "포도주", "발포성 포도주", "샴페인"을 차례로 출력한다.
  • 메서드 재정의는 컴파일타임의 타입과는 무관하게 항상 가장 하위에서 정의한 재정의 메서드가 실행된다.

 

다중 정의(Overloading)

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

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

 

  • 해당 코드는 "집합", "리스트", "그 외"를 차례대로 출력할것 같지만, 실제로는 "그 외" 만 3번 출력된다.
  • 그 이유는 다중정의(overloading)된 메소드(classify)중 어떤것을 호출할지는 컴파일타임에 정해지기 때문이다.
  • for문에서 런타임에는 타입이 매번 달라지지만, 컴파일타임에는 for문 안의 c는 항상 Collection<?> 타입이다.
  • 재정의된 메소드(Override)는 동적으로 선택 되지만 다중정의된 메소드(overloading)는 정적으로 선택이 된다.
  • 해당 문제는 아래의 코드를 추가해서 가볍게 해결 할 수 있다.
public static String classify(Collection<?> c) {
    return c instanceof Set  ? "집합" :
            c instanceof List ? "리스트" : "그 외";
}

 

 

  • 이처럼 다중정의가 혼돈을 일으키는 상황을 피하자.
  • 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지말자. 가변인수를 사용하는 메서드라면 다중정의를 아예 하지 말아야 한다.
  • 다중정의 대신 메서드 이름을 다르게 지어주는 더 좋은 방법도 있다.
  • 생성자는 두번째 부터 무조건 다중정의가 되는데 정적팩터리라는 대안이 있으니 적절히 사용하자.
  • 어쩔수 없이 같은 수의 매개변수를 받아야 하는 경우 그중 어느 것이 주어진 매개변수 집합을 처리할지가 명확히 구분된다면 헷갈릴 일은 없을 것이다. 즉, 매개변수 중 하나 이상이 "근본적으로 다르다"면 헷갈릴 일이 없다.
  • 여기서 근본적으로 다르다라는 말은 두 타입의 값을 서로 어느 쪽으로든 형변환 할 수 없다는 뜻이다. 이 조건만 충족하면 어느 다중정의 메서드를 호출할지가 매개변수의 런타임 타입만으로 결정된다.
  • Ex) ArrayList의 인자가 1개인 생성자는 ArrayList(int initialCapacity)와 ArrayList(Collection<? extends E> c)가 있지만 int와 Collection은 근본적으로 다르므로 괜찮다

 

 

 

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

  • 가변인수(vargs) 메서드는 명시한 타입의 인수를 0개 이상 받을 수 있다.
  • 가변인수 메서드를 호출하면, 가장 먼저 인수의 개수와 길이가 같은 배열을 만들고 인수들을 이 배열에 저장하여 가변인수 메서드에 건네준다.

 

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

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

위의 방식에는 몇가지 문제가 있다.

 

  1. 가장 심각한 문제는 인수를 0개만 넣어 호출하면 컴파일타임이 아닌 런타임에 실패한다는 점이다.
  2. 코드도 지저분하다.
  3. args 유효성 검사를 명시적으로 해야 한다.

 

개선된 방법

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

 

성능에 민감한 상항이라면 가변인수 사용은 고려해보자

  • 가변인수 메서드는 호출될 때마다 배열을 새로 하나 할당하고 초기화한다.
  • 가변인수의 비용을 감당할 수는 없지만 유연성이 필요할 때 선택할 수 있는 멋진 패턴이 있다.
  • 해당 메서드 호출의 95%가 인수를 3개 이하로 사용한다고 해보자. 그렇다면 다음처럼 인수가 0개인 것부터 4개인 것까지, 총 5개를 다중정의하자. 마지막 다중정의 메서드가 인수 4개 이상인 5%의 호출을 담당하는 것이다.
public void foo() { }
public void foo(int a1) { }
public void foo(int a1, int a2) { }
public void foo(int a1, int a2, int a3) { }
public void foo(int a1, int a2, int a3, int... rest) { }

 

  • EnumSet의 정적 팩터리도 이 기법을 사용해 열거 타입 집합 생성 비용을 최소화한다.
  • EnumSet은 비트 필드(아이템 36)를 대체하면서 성능까지 유지해야 하므로 아주 적절하게 활용한 예시라 할 수 있다.

 

 

 

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

다음은 주변에서 흔히 볼 수 있는 메서드다
 
 
컬렉션이 비었으면 null을 반환한다 - 따라하지 말 것!
private final List<Cheese> cheesesInStock = ...;

/**
 * @return 매장 안의 모든 치즈 목록을 반환한다.
 * 	단, 재고가 하나도 없다면 null을 반환한다.
 */
public List<Cheese> getCheeses() {
	return cheesesInStock.isEmtpy() ? null
		: new ArrayList<>(cheesesInStock);
}
 
 
  • 재고가 없다고 해서 특별히 취급할 이유는 없다. 그럼에도 이 코드처럼 null을 반환한다면, 클라이언트는 이 null을 처리하는 코드를 추가로 작성해야 한다.

 

빈 컨테이너 VS null

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

 

 
 
 

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

  • JAVA 8 이전, 반환값이 없을 때의 선택지는 두가지가 있었다.

 

1. 예외 던지기

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

2. null을 반환(반환 타입이 객체 참조일 경우)

  • 별도의 null 처리 코드를 추가해야 한다.
  • null 처리를 무시하고 반환된 null은 언젠가 NullPointerException이 발생할 수 있다.

 

Optional (JAVA 8 이후)

  • 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<E>로 반환한다.

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.Empty()로 만들고, 값이 든 옵셔널은 Optional.of(value)로 생성했다.
  • Optional.of(value)에 null을 넣으면 NullPointerException을 던지니 주의하자.
  • null 값도 허용하는 옵셔널을 만들려면 Optional.ofNullable(value)를 사용하면 된다. 옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자.

 

 

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

옵셔널 활용1 - orElse : 기본값을 정해둘 수 있다

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

 

옵셔널 활용2 - orElseThrow : 원하는 예외를 던질 수 있다

Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
  • 위 코드처럼 상황에 맞는 예외를 던질 수 있다.
  • 실제 예외가 아니라 예외 팩터리를 건넨 것에 주목하자. 이렇게 하면 예외가 실제로 발생하지 않는한 예외 생성 비용은 들지 않는다.

 

옵셔널 활용 3 - get : 항상 값이 채워져 있다고 가정한다

Element lastNobleGas = max(Elements.NOBLE_GASES).get();
  • 옵셔널에 항상 값이 없다면 NoSuchElementException이 발생할 것이다.

 

 

Optional를 사용하면 안되는 경우

  • 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안 된다.
  • 빈 Optional<List>를 반환하기보다는 빈 List를 반환하는게 좋다.
  • 빈 컨테이너를 그대로 반환하면 클라이언트에 옵셔널 처리 코드를 넣지 않아도 된다.

 

언제 Optional를 사용해야 하는가?

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

성능 이슈

  • Optional도 엄연히 새로 할당하고 초기화해야 하는 객체이고, 그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한 단계를 더 거치는 셈이다. 그래서 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있다.
  • 박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수밖에 없다. 값을 두 겹이나 감싸기 때문이다.
  • 그래서 자바 API 설계자들은 int, long, double 전용 옵셔널 클래스들을 준비해놨다. 바로 OptionalInt, OptionalLong, OptionalDouble이다.
  • 이 옵셔널들도 Optional가 제공하는 메서드를 거의 다 제공한다.
  • 이렇게 대체제까지 있으니 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자. 상대적으로 덜 중요한 Boolean, Byte, Character, Short, Float은 예외일 수 있다.

 

하지 말아야 할 것

  • 옵셔널을 맵의 값으로 사용하면 절대 안 된다. 그리 한다면 맵 안에 키가 없다는 사실을 나타내는 방법이 두 가지가 된다.
  • 하나는 키 자체가 없는 경우고, 다른 하나는 키는 있지만 그 키가 속이 빈 옵셔널인 경우다. 쓸데없이 복잡성만 높여서 혼란과 오류 가능성을 키울 뿐이다.
  • 옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.

 

 

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

  • 자바독은 소스코드 파일에서 문서화 주석이라는 특수한 형태로 기술된 설명을 추려 API 문서로 변환해준다.
  • 자바 버전에 따라 자바독 태그도 발전해왔다.
  • API를 올바로 문서화하려면 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야 한다.
  • 직렬화할 수 있는 클래스라면 직렬화 형태에 관해서도 적어야 한다.
  • 기본 생성자에는 문서화 주석을 달 방법이 없으니 공개 클래스는 절대 기본 생성자를 사용하면 안된다.
public class Person(){
	...

	public Person(String name, int age){
		this.name = name;
		this.age = age;
	}
}

 

메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 명료하게 기술해야 한다.

 

  • how가 아닌 what을 기술해야 한다.
  • 전제조건(precondition)을 모두 나열해야 한다.
  •  메서드가 성공적으로 수행된 후에 만족해야 하는 사후조건도 모두 나열해야 한다.
  • 일반적으로 전제조건은 @throws 태그로 비검사 예외를 선언하여 암시적으로 기술한다.
  • @param 태그를 이용해 그 조건에 영향 받는 매개변수에 기술할 수 있다.
  • 부작용도 문서화 해야 한다. 부작용이란 사후조건으로 명확히 나타나지는 않지만 시스템의 상태에 어떠한 변화를 가져오는 것을 말한다. 예를들어 백그라운드 스레드를 시작시키는 메서드라면 그 사실을 문서에 밝혀야 한다.

관례상 @param 태그와 @return 태그의 설명은 해당 매개변수가 뜻하는 값이나 반환값을 설명하는 명사구를 쓴다. 드물게는 명사구 대신 산술 표현식을 쓰기도 한다. BigIntegerAPI 문서를 참고해보면 된다.

 

 

javadoc 주석 태그

@param 

  • 매개 변수 설명
  • 모든 매개변수에 설명을 달아야한다.

 

@return

  • 반환 값 설명
  • 반환 타입이 void가 아니면 달아야한다.

 

@throws

  • if로 시작해 해당 예외를 던지는 조건 설명

 

{@code}

  • 주석 내에 HTML 요소나 다른 자바독 태그를 무시한다.
  • 주석에 여러 줄로 된 코드 예시를 넣으려면 {@code}를 <pre>태그로 감싸준다. <pre>{@code ...코드... }</pre>

 

{@literal}

  • 주석 내에 HTML 요소나 다른 자바독 태그를 무시한다.
  • {@code}와 비슷하지만 코드 폰트로 렌더링하지 않는다.

 

@implSpec

  • 해당 메서드와 하위 클래스 사이의 계약을 설명
  • 하위 클래스들이 그 메서드를 상속하거나 super 키워드를 이용해 호출할 때 그 메서드가 어떻게 동작하는지 명확히 인지할 수 있도록 도와준다.

 

{@inheritDoc}

  • 상위 타입의 문서화 주석 일부를 상속할 수 있다.

 

 API 문서화에서 자주 누락되는 설명 두가지 

  • 클래스 혹은 정적 메서드가 스레드 안전하든 그렇지 않든, 쓰레드 안전 수준을 반드시 API 설명에 포함해야 한다.
  • 직렬화할 수 있는 클래스라면 직렬화 형태도 API 설명에 기술해야 한다. 

 

 

 

 

 

 

 

728x90
반응형

댓글