본문 바로가기

나(다)/책

이펙티브 자바 - 8장 : 메서드

반응형

메서드

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

  • 생성자와 메서드의 입력 매개변수의 값은 보통 인덱스 값은 음수이면 안되며, 객체 참조는 null이 아니어야 한다.
  • 이런 제약은 반드시 문서화해야 하며 메서드 몸체가 시작되기 전에 검사해야 한다.

매개변수 검사를 제대로 하지 못하면 발생하는 문제

  • 매개변수 검사에 실패하면 실패 원자성을 어기는 결과를 낳을 수 있다.
  1. 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.
  2. 메서드가 잘 수행되지만 잘못된 결과를 반환할 수도 있다.
  3. 메서드는 문제 없이 수행되지만, 어떤 객체를 이상한 상태로 만들어놓아서 미래의 알 수 없는 시점에 해당 메서드와는 관련 없는 오류를 낼 수 도 있다.

매개변수의 제약을 어겼을 때 발생하는 예외도 함께 문서화 해둔다.

/**
 * (현재 값 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;
}
  • 위 코드는 하나의 예시이며 적절한 문서화이다.
  • "m이 null이면 NullPointerException을 던진다"라는 말은 메서드 설명 어디에도 없는데 이유는 BigInteger 클래스 수준에서 기술했기 때문이다.
  • @Nullable이나 비슷한 애너테이션으로 null 될 수 있다고 알려줄 수 있지만 표준적인 방법은 아니다.

매개변수 하는 방법

  • 자바 7에서 추가된 java.util.Objects.requireNonNull 메서드는 유연하고 사용하기도 편하니, 더 이상 null 검사를 수동으로 하지 않아도 된다.
    • requireNonNull()을 이용하면 List에서 꺼낼 때가 아닌 받을 떄부터 null인지 알 수 있다.
      System.out.println(Objects.requireNonNull(null, "null임").toString());
  • checkFromIndexSize, checkFromToIndex, checkIndex라는 메서드들고 있지만 null 검사 메서드만큼 유연하지는 않고 예외 메시지를 지정할 수도 없지만 리스트와 배열 전용으로 설계 됐다.
  • 공개되지 않은 메서드라면 호출되는 상황을 통제할 수 있는데 단언문(assert)를 사용해 매개변수 유효성을 검증할 수 있다.
      private static void test(int a, int b, int c) {
              assert a != 0;
              assert b >= 0;
              System.out.println("테스트");
          }
    
          public static void main(String[] args) {
              test(10,10,10);
              test(0,10,10); // Exception in thread "main" java.lang.AssertionError
              test(10,-10,10);
          }
  • 단언문들은(assert) 자신이 단언한 조건이 무조건 참임을 선언하고 클라이언트가 어떤 식으로 하든 상관 없지만 일반적인 유효성 검사와 다른 두가지가 있다.
    1. 실패하면 AssertionError를 던진다.
    2. 런타임에 아무런 효과도, 아무런 성능 저하도 없다.(단, java를 실행 할 때 vm 옵션에 -ea(—enableassertion)를 설정한다.

유효성 검사를 해야 하는 경우

  1. 생성자의 경우
    • 생성자의 경우 "나중에 쓰려고 저장하는 매개변수의 유효성을 검사하라"는 원칙의 특수한 사례이다.
    • 생성자 매개변수의 유효성 검사는 블래스 불변식을 어기는 객체가 만들어지지 않게 하는데 꼭 필요하다.
  2. 메서드의 경우
    • 메서드는 몸체 실행 전에 매개변수 유효성을 검사해야 한다는 규칙이 있지만 예외의 경우도 있다.
      • 유효성 검사 비용이 지나치게 높거나 실용적이지 않을 떄, 혹은 계산 과정에서 암묵적으로 검사가 수행될 떄다.
        • Collections.sort(list)처럼 객체 리스트를 정렬하는 메서드인 경우에 만약 상호 비교될수 없는 타입의 객체가 들어있다면 비교하기 앞서 리스트 안의 모든 객체가 상호 비교될 수 있는지 검사해봐야 별다른 실익이 없다.

계산 과정에서 필요한 유효성 검사가 이뤄지지만 실패하여 잘못된 예외를 던지기도 한다.

  • 계산 중 잘못된 매개변수 값을 사용해 발생한 예외와 API문서에서 던지기로 한 예외가 다를 수 있는데 이런 경우에는 예외 번역 관용구를 사용해 API문서에 기재된 예외를 해주어야 한다.

이번 아이템을 "매개 변수에 제약을 두는 게 좋다. 라고 해석하면 안된다. "

  • 그 반대로 메서드는 최대한 범용적으로 설계해야 한다.
  • 메서드가 건네 받은 값으로 무언가 제대로 된 일을 할 수 있다면 매개변수 제약은 적을수록 좋다.

매개변수 검사에 실패하면 실패 원자성(아이템76), @throws 자바독 태그를 사용하면 된다.(아이템74), Expcetion(아이템72), 예외번역(아이템73)

메서드나 생성자를 작성할 때면 그 매개변수들에 어떤 제약이 있을지 생각해야 한다. 그 제약들을 문서화하고 메서드 코드 시작 부분에서 명시적으로 검사해야 한다.


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

  • 자바는 c, c++보다 안전한 언어로 자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 불변식이 지켜진다.
  • 클라이언트가 여러분의 불변식을 꺠드리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.
    • 악의적인 의도를 가진 사람들이 시스템의 보안을 뚫으려는 시도가 늘고 있다.
    • 평범한 프로그래머도 순전히 실수로 여러분의 클래스를 오작동하게 만들 수 있다.
  • 어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하지만 주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다.
static 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 + " after " + end);
            this.start = start;
            this.end = end;
        }

        public Date getStart() {
            return start;
        }

        public Date getEnd() {
            return end;
        }
    }
    public static void main(String[] args) {
        Date start = new Date();
        Date end = new Date();
        Period p = new Period(start, end);
        end.setYear(78); // p의 내부를 수정했다.
    }
  • Period는 불변처럼 보이지만 Date가 가변이라는 사실을 이용하면 쉽게 start보다 end가 빠를 수 있다.
  • Date는 낡은 API이니 새로운 코드를 작성할 때는 더 이상 사용하면 안된다.
  • 과거부터 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);
//            this.start = start;
//            this.end = end;
}
  1. 매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자.
    • 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
    • 컴퓨터 보안 커뮤니티에서는 이를 검사시점/사용시점(time-of-check/time-of-use) 공격 혹은 영어 표기를 줄여서 TOCTOU 공격이라 한다.
  2. 방어적 복사에서 매개변수가 제 3자의 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.
    • clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있기에 하위 클래스는 start와 end 필드의 참조를 private 정적 리스트에 담아뒀다가 공격자에게 이 리스트에 접근하는 길을 열어줄 수 있다.
  3. Period 인스턴스는 아직도 변경 가능하여 접근자 메서드가 내부의 가변 정보를 직접 드러낸다.
    • 가변 필드의 방어적 복사본을 반환하면 된다.
      public Date start() {
        return new Date(start.getTime()); // 방어적 복사본
      }
      
      public Date end() {
        return new Date(end.getTime()); // 방어적 복사본
      }
      
      public static void main(String[] args) {
        Date start = new Date();
        Date end = new Date();
        Period p = new Period(start, end);
      //        end.setYear(78);
        p.end().setYear(78);
      }

매개변수를 방어적으로 복사하는 목적

  1. 불변 객체를 만들기 위해서
  2. 외부 객체를 내부 객체로 건네 받을 경우, 내부 객체를 외부 객체로 건네 받을 경우
    • 클라이언트가 제공한 객체의 참조가 잠재적으로 변경될 수 있는 객체라면 임의로 변경되어도 문제 없이 동작할지를 따져보고 확신하지 못하는 경우를 위해서
    • set이나 map의 경우 겍체가 변경되면 불변식이 깨진다.
    • 길이가 1 이상인 배열은 무조건 가변임을 잊지 말고 배열을 클라이언트에 반환 할 때는 항상 방어적 복사를 수행해야 한다.
  • Period 예제의 경우, 자바 8 이상에서 개발해도 된다면 Instant(혹은 LocalDateTime이나 ZonedDateTime)를 사용하고 이전 버전의 자바에서 사용한다면 Date 참조 대신 Date.getTime()이 반환하는 long 정수를 사용하는 방법을 써도 된다.

방어적 복사를 하지 않아야 하는 경우

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

매개변수의 유효성을 검사(아이템49), 되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다.(아이템17)

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


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

  1. 메서드 이름을 신중하게 짓자
    • 항상 표준 명명 규칙을 따라야 쉽게 이해할 수 있고, 같은 패키지에 속한 다른 이름들과 일관되게 짓는 게 최우선 목표다.
  2. 편의 메서드를 너무 많이 만들지 말자
    • 모든 메서드는 각각 자신의 소임을 다해야 하는데 메서드가 너무 많은 클래스는 익히고, 사용하고, 문서화하고, 테스트하고, 유지보수하기 어렵다.
  3. 자주 쓰일 경우에만 별도의 약칭 메서드를 두고 확신이 서지 않으면 만들지 말자.
  4. 매개 변수 목록은 짧게 유지하라.
    • 4개 이하가 좋으며 4개가 넘어가면 매개변수를 전부 기억하기가 쉽지 않다.
  5. 같은 타입의 매개변수 여려 개가 연달아 나오는 경우가 특히 해롭다.

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

  1. 여러 메서드로 쪼개어 각각은 원래 매개변수 목록의 부분집합을 받는다.
    • 잘못하면 메서드가 너무 많아질 수 있지만, 직교성을 높여 오히려 메서드 수를 줄여주는 효과도 있다.
    • java.util.List 인터페이스가 좋은 예로써 리스트에서 주어진 우너소의 인덱스를 찾아야 하는데, 전체 리스트가 아니라 지정된 범위의 부분리스트에서의 인덱스를 찾는다고 했을 때 subList, indexOf 메서드를 별개로 제공한다.
    • 소프트웨어에서는 직교란 "공통점이 없는 기능이 잘 분리되어 있다." 혹은 "기능을 원자적으로 쪼개 제공한다."로 해석 할 수 있다. 직교성 하나로 해석하기에는 무리가 있지만 대체적으로 마이크로서비스 아키텍처는 직교성이 높고 모놀리식 아키텍처는 직교성이 낮다고 할 수 있다.
  2. 매개변수 여러 개를 묶어주는 도우미 클래스를 만드는 것이다.
    • 도우미 클래스는 정적 멤버 클래스로 두며 잇따른 매개변수 몇 개를 독립된 하나의 개념으로 볼 수 있을 때 추천하는 기법이다.
    • 예를 들어 카드 게임을 클래스로 구현하는 상황에서 메서드를 호출 할 떄 카드의 숫자, 무늬를 뜻하는 두 매개변수를 항상 같은 순서로 전달하게 된다. 이 둘을 묶는 도움 클래스를 만들어 하나의 매개변수로 주고받으면 API는 물론 클래스 내부 구현도 깔끔해질 것이다.
  3. 앞의 두 기법을 혼합한 것으로, 객체 생성에 사용한 빌더 패턴을 메서드 호출에 응용한다고 보면 된다.
    • 모든 매개변수를 하나로 추상화한 객체를 정의하고, 클라이언트에서 이 객체의 세터 메서드를 호출해 필요한 값을 설정하게 하는것이다.
    • 클라이언트는 먼저 필요한 매개변수를 다 설정한 다음, execute 메서드를 호출해 앞서 설정한 매개변수들의 유효성을 검사하고 설정이 완료된 객체를 넘겨 원하는 계산을 수행한다.

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

  • 매개변수로 적합한 인터페이스가 있다면(이를 구현한 클래스가 아닌) 그 인터페이스를 직접 사용하자.
    • 예를 들어 메서드에 HashMap을 넘길 일은 없으니 Map을 대신 사용하자.

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

  • 메서드 이름상 boolean을 받아야 의미가 더 명확할 때는 예외다.
enum TemperatureScale {
    FAHRENHEIT, CELSIUS
}

표준 명명 규칙(아이템68), 정적 멤버 클래스(아이템24), 빌더 패턴(아이템2), 매개변수의 타입으로는 클래스보다 인터페이스가 더 낫다(아이템64), 열거 타입 상수의 메서드 안으로 리팩터링해 넣을 수도 있다.(아이템34)

  1. 메서드 이름을 신중히 짓자.
  2. 편의 메서드를 너무 많이 만들지 말자.
  3. 확신이 서지 않으면 별도의 약칭 메서드를 만들지 말자.
  4. 매개변수 목록을 짧게 유지히자.
  5. 같은 타입의 매개변수 여러개가 연달아 나오는 경우가 특히 해롭다.
  6. 매개변수의 타입으로는 클래스보다는 인터페이스가 더 낫다.
  7. boolean보다는 원소 2개짜리 열거 타입이 낫다.

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


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

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

    public static String classify(Collection<?> c) {
        // return "그 외";
                return c instanceof Set ? "집합" : c instanceof List ? "리스트" : "그 외"; // 의도한 대로 동작하기 위한 조치
    }

    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번 실행되는데 다중 정의 된 세 classify 중 어느 메서드를 호출할지를 컴파일타임에서 정하기 때문이다.
  • 재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택되기 때문에 직관에 어긋난다.
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());
    }
}

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

    class SparklingWine extends Wine {
        @Override
        String name() { // 오버라이딩
            return "발포성 포도주";
        }
    }

    class Champagne extends SparklingWine {
        @Override
        String name() {
            return "샴페인";
        }
    }
  • 위 코드는 오버라이딩 된 코드이다.

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

  • 가변 인수를 사용하는 메서드라면 다중정의를 아예 하지 말아야 한다.
  • 다중정의하는 대신 메서드 이름을 다르게 지어주는 길도 항상 열려 있다.
    • 생성자는 이름을 다르게 지을 수 없으니 정적 팩터리를 고려하자.

다중 정의 메서드가 많을 경우

  • 매개변수 중 하나 이상이 근본적으로 다르면 두 타입의(null이 아닌) 값을 서로 어느 쪽으로든 형변환할 수 없기에 문제가 되지 않는다.
public class Item52 {
    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); // remove(Object) : 0 이상의 수를 제거 - 기대하는 동작
            //list.remove(i); // remove(int index) : 지정 된 위치 제거 - 문제가 되는 동작
            list.remove((Integer) i); // 형변환
        }
        System.out.println(set + " " + list); // [-3, -2, -1] [-2, 0, 2]
    }
}
  • 오토박싱이 생기면서 기본 타입도 안전하지 않다.
new Thread(System.out::println).start();

ExecurotService exec = Executors.newCachedThradPool();
exec.submit(System.out::println);
  • 자바 8에서 람다와 메서드 참조도 안전하지 않다.

메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수를 받아서는 안 된다.

  • 서로 다른 함수형 인터페이스라도 서로 근본적으로 다르지 않다는 뜻이다.
  • 컴파일할 때 명령줄 스위치로 -Xlint:overloads를 지정하면 다중정의 경고를 준다.

자바 라이브러리는 이번 아이템의 정신을 지키려고 애쓰지만 실패한 클래스도 몇 개 있다.

  • String 클래스의 valueOf(char[])와 valueOf(Object)는 같은 객체를 건네더라도 다르게 수행한다.

가변인수(아이템53), 정적 팩터리(아이템1)

다중정의가 허용된다고 해서 꼭 활용하란 의미는 아니며 일반적으로 매개변수 수가 같을 때는 다중정의를 피하는게 좋다.


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


static int sum(int... args) {

  int sum = 0;
  for (int arg : args) {
      sum += arg;
  }
  return sum;
}
  • 가변인수 메서드는 명시한 타입의 인수를 0개 이상 받을 수 있다.
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;
}
  • 인수는 0개만 받을 수도 있도록 설계하는건 좋지 않다.
static int min(int firstArg, int... args) {
    int min = firstArg;
    for (int arg : args) {
        if (arg < min)
            min = arg;
    }
    return min;
}
  • 첫 번쨰 값을 받으면 깔끔한 코드로 바꿀 수 있다.

성능에 민감한 상황이라면 가변인수가 걸림돌이가 될 수 있다.

  • 가변인수 메서드는 호출될 때마다 배열을 새로 하나 할당하고 초기화한다.

성능은 감당할 수 없지만 가변인수의 유연성이 필요할 때 선택할 수 있는 패턴

  • 해당 메서드의 호출 95%가 인수를 3개 이하로 사용한다는 가정하에 아래와 같이 작성할 수 있다.
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) { } // 단 5%로만 사용
  • 성능 최적화에 이득은 없지만 꼭 필요한 상황에 유용하게 사용할 수 있다.
  • EnumSet의 정적 팩터리도 이 기법을 사용해 열거 타입 집합 생성 비용을 최소화한다.

리플렉션 기능(아이템65), 비트 필드(아이템36)

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


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

// 서버에서 null을 반환하면
public List<Cheese> getCheeses() {
    return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock); // 절대 따라하지 말자.
}

// 클라이언트는 방어 로직을 구현해야 함
List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON)
    System.out.println("좋았어, 바로 그거야.");
  • 컬렉션이나 배열 같은 컨테이너가 비었을 때 null을 반환하는 메서드를 사용할 때면 항시 이와 같은 방어 코드를 넣어주어야 한다.
  • 때로는 빈 컨테이너를 할당하는 데도 비용이 드니 null을 반환하는 쪽이 낫다는 주장이 있지만 두 가지 면에서 틀린 주장이다.
    1. 성능 분석 결과 성능 저하의 주범이라고 확인되지 않는 한 이 정도의 성능 차이는 신경 쓸 수준이 못된다.
    2. 빈 컬렉션과 배열은 굳이 새로 할당하지 않고도 반환할 수 있다.
       public List<Cheese> getCheeses() {
           return new ArrayList<>(cheesesInStock); // 빈 컬렉션을 반환하는 전형적인 방법
       }
  • 만약 눈에 띄는 성능 저하가 발생하면 매번 똑같은 빈 '불변' 컬렉션을 반환하면 자유롭게 공유해도 안전하다.
    • 집합이 필요하면 Collections.emptySet을, 맵이 필요하면 Collections.emptyMap을 사용하면 된다.
// 최적화 - 빈 컬렉션을 매번 새로 할당하지 않도록 했다.
public List<Cheese> getCheeses() {
    return cheesesInStock.isEmpty() ? Collections.emptyList()
        : new ArrayList<>(cheesesInStock);
}
  • 성능 최적화 한 컬렉션 코드
// 배열도 null이 아닌 길이가 0인 배열을 반환하라
public Cheese[] getCheeses() {
    return cheeseInStock.toArray(new Cheese[0]);
}

//불변 배열의 방식
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

public Cheese[] getCheeses() {
    return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}
  • 길이가 0인 배열을 반환하는 코드
  • 단순히 성능을 개선할 목적이라면 toArray에 넘기는 배열을 미리 할당하는건 추천하지 않는다.
    • toArray는 원소가 하나라도 있다면 배열을 새로 생성하고 0개이면 전달받은 배열은 반한한다.

성능 저하의 주범(아이템67), 불변 객체(아이템17)

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


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

  • 자바 8 이전에는 메서드가 특정 조건에서 값을 반환할 수 없을 때 예외를 던지거나, (반환 타입이 객체 참조라면) null을 반환하는 것이었지만 허점이 있다.
    • 예외는 진짜 예외적인 상황에서만 사용해야 하고 예외 생성 시 스택 추적 전체를 캡처하므로 비용도 만만치 않다.
    • null을 반환하면 null이 반환 되지 않는다고 확신하지 않는 한 별도의 null 처리 코드를 추가해야 한다.
  • 자바 8이 되면서 Optional라는 대안이 생겼는데 null이 아닌 T타입 참조를 하나 담거나 혹은 아무것도 담지 않을 수 있다.
    • 아무것도 담지 않은 옵셔널은 '비었다'고 말하고 어떤 값을 담으면 '비지 않았다'고 한다.
    • 원소를 최대 1개 가질 수 있는 '불변' 컬렉션이다.

보통 T를 반환해야 하지만 특정 조건에서는 아무것도 반환하지 않아야 할 때 T 대신 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을 반환하지 말자.

  • 옵셔널을 도입한 취지를 완전히 무시하는 행위다.
  • 스트림의 종단 연산 중 상당수가 옵셔널을 반환하다.

null을 반환하거나 예외를 던지는 대신 옵셔널 반환을 선택해야 하는 기준

  • 옵셔널은 검사 예외와 취지가 비슷해, 반환 값이 없을 수도 있음을 API 사용자에게 명확하게 알려준다.
    • 메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 한다.
      public static void main(String[] args) {
      String lastWordInLexicon = max(words).orElse("단어 없음"); // 활용 1 : 기본 값 지정
      Toy myToy = max(toys).orElseThrow(TemperTranTrumException::new); // 활용 2 : 예외를 던지기
      Element lastNobleGas = max(Elements.NOBLE_GASES).get(); // 활용 3 : 항상 값이 채워져 있다고 가정
      }

기본 값 설정하는 비용이 부담 될 때 해결 방법

  • Supplier를 인수로 받는 orElseGet을 사용하면 값이 처음 필요할 때 Supplier를 사용해 생성하므로 초기 비용을 낮출 수 있다.
  • isPresent 메서드는 안전 밸브 역할로 채워져 있으면 true를, 비어있으면 false를 반환하고 앞서 말한 메서드들로 대체할 수 있다.
Optional<String> parentProcess = Optional.of("Test");
System.out.println(parentProcess.isPresent() ? String.valueOf(parentProcess.get().pid()) : "N/A");

System.out.println(parentProcess.map(h -> String.valueOf(h.pid())).orElse("N/A")); // map을 이용해 다듬을 수 있다.
  • Optional의 map으로 코드를 다듬을 수 있다.
streamOfOptionals.filter(Optional::isPresent).map(Optional::get)
streamOfOptionals.flatMap(Optional::stream) // flatMap()과 stream()으로 다듬을 수 있다.
  • Stream에서 자주 사용하고 있다.
  • 자바 9에서는 Optional에 stream() 메서드가 추가되었다.

컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안 된다.

  • 빈 Optional<List>를 반환하기보다는 빈 List를 반환하는게 좋다.
  • 빈 컨테이너를 그대로 반환하면 클라이언트에 옵셔널 처리 코드를 넣지 않아도 된다.

어떤 경우에 메서드 반환 타입을 T 대신에 Optional로 선언하는 규칙

  • 결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional를 반환한다.
  • Optional도 엄연히 새로 할당하고 초기화해야 하는 객체이고 값을 사용하려면 메서드를 호출해야 하므로 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있다.

박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자

  • 단, '덜 중요한 기본 타입'인 Boolean, Byte, Charater, Short, Float은 예외일 수 있다.
  • OptionalInt, OptionalLong, OptionalDouble이 있다.

옵셔널을 맵의 값으로 사용하면 절대 안된다.

  • 대신 맵에 키가 없다는 사실을 알려주는 두 가지 방법이 두 가지가 되어버려 혼란과 오류 가능성만 키운다.
    1. 키 자체가 없는 경우
    2. 키는 있지만 키가 속이 빈 옵셔널이 경우

옵셔널을 컬레션의 키, 값, 원소나 배열의 원소로 사용하는게 적절한 상황은 거의 없다.

  • 인스턴스 필드에 저장해두면 해당 클래스를 확장해 하위 클래스를 따로 만들어야 함을 암시하는 '나쁜 냄새'가 난다.

진짜 예외적인 상황(아이템80), 검사 예외(아이템71), stream의 flatMap(아이템45), 빈컬렉션을 반환하자(아이템54), 세심한 측정(아이템67)

값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환 값이 없을 가능성을 염두에 둬야 하는 메서드라면 옵셔널을 반환해야 할 상활일 수 있다.
하지만 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있다.


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

  • 문서화 주석을 작성하는 규칙은 공식 언어 명세에 속하진 않지만 자바 프로그래머라면 응당 알아야 하는 업계 표준 API라 할 수 있고 이 규칙은 문서화 주석 방법(How to Write Doc Comments) 웹페이지에 기술되어 있다.
    • 자바 4이후로는 갱신되지 않은 페이지지만, 그 가치는 여전하다.
  • 자바 버전이 올라가며 추가 된 중요한 자바독 태그로는 자바 5의 @literal, @code 자바 8의 @implSpec, 자바 9의 @index를 꼽을 수 있다.

여러분의 API를 올바로 문서화하려면 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야 한다.

  • 직렬화할 수 있는 클래스라면 직렬화 형태에 관해서도 적어야 한다.
  • 문서화 주석이 없다면 자바독도 그저 공개 API 요소들의 '선언'만 나열해주는 게 전부다.
  • 기본 생성자에는 문서화 주석을 달 방법이 없으니 공개 클래스는 절대 기본 생성자를 사용하면 안된다.

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

  1. how가 아닌 what을 기술해야 한다. 상속용으로 설계 된 메서드가 아니라면 (그 메서드가 어떻게 동작하는지가 아니라) 무엇을 하는지를 기술해야 한다.
  2. 클라이언트가 해당 메서드를 호출하기 위한 전제 조건을 모두 나열해야 한다.
  3. 메서드가 성공적으로 수행된 후에 만족해야 하는 사후조건도 모두 나열해야 한다.
  4. 일반적으로 전제조건은 @throws 태그로 비검사 예외를 선언하여 암시적으로 기술한다.
    • 비검사 예외 하나가 전제조건 하나와 연결된다.
  5. @param 태그를 이용해 해당 조건에 영향 받는 매개변수에 기술할 수도 있다.
  6. 전체 조건과 사후 조건뿐만 아니라 부작용도 문서화해야 한다.
    • 부작용은 사후 조건으로 명확히 나타나지는 않지만 시스템의 상태에 어떠한 변화를 가져오는 것을 의미한한다.
  7. 완벽히 기술하려면 모든 매개변수에 @param 태그를, void가 아니라면 @return 태그를, 발생할 가능성이 있는(검사든 비검사든) 모든 예외에 @throws 태그를 달아야 한다.
  8. 관례상 @param 태그와 @return 태그의 설명은 해당 매개변수가 뜻하는 값이나 반환값을 설명하는 명사구를 쓰고 드물게 산술 표현식을 쓰기도 한다.
  9. @param, @return, @throws 태그의 설명에는 마침표를 붙이지 않지만 한글로 작성한 경우 온전한 종결 어미에는 마침표를 써주는 게 일관돼 보인다.
  10. {@code} 태그는 감싼 내용을 코드용 폰트로 렌터링하고 HTML 요소나 다른 자바독 태그를 무시한다.
    • 주석에 여러줄로 된 코드를 넣으려면
      {@code ... 코드 ... } 
      형태로
       태그로 감싸면 된다.
  11. @implSpec 주석은 해당 메서드와 하위 클래스 사이의 계약을 설명하여 하위 클래스들이 그 메서드를 상속하거나 super 키워드를 이용해 호출 할 때 메서드가 어떻게 동작하는지를 명확히 인지하고 사용하도록 해줘야 한다.
    /**
     * @implSpec This is implements returns {@code this.size(0 == 0}.
     * @return true if this collection is empty
     */
    
    void test2() {
    
    }
    • 자바 11까지도 자바독 명령줄에서 -tag "implSpec:a:Implementation Requirements:" 스위치를 켜주지 않으면 @implSpec 태그를 무시해버린다.
  12. API 설명에서 <, > & 등의 HTML 메타 문자를 포함시키려면 {@literal} 태그로 감싸는게 가장 좋은 방법으로 HTML 마크업이나 자바독 태그를 무시하게 해주고 코드 폰트로 렌더링하지 않는다.
  13. 각 문서화 주석의 첫 번째 문장은 해당 요소의 요약 설명으로 간주하고 마침표까지를 요약 설명으로 판단하고 의도치 않은 마침표를 포함한 텍스트는 @{literal}로 감싼다.
    /**
    * 테스트 {@literal 일거라고.} 그렇다.
    */
    void test3() {
    
    }
    
    // 자바 10부터는 {@summary}라는 요약 설명 전용 태그가 추가 되었다.
    /**
     * {@summary 요약 설명을 만듬}
     */
  14. 한 클래스(혹은 인터페이스) 안에서 요약 설명이 똑같은 멤버(혹은 생성자)가 둘 이상이면 안된다.
  15. 다중정의 된 메서드들의 설명은 같은 문장으로 시작하는 게 자연스럽겠지만 문서화 주석에서는 허용되지 않는다.
  16. 메서드와 생성자의 요약 설명은 해당 메서드와 생성자의 동작을 설명하는 (주어가 없는) 동사구여야 한다.
    ArrayList(int initialCapacity) : 지정한 초기 용량을 갖는 빈 리스트를 생성한다.
  17. 클래스, 인터페이스, 필드의 요약 설명은 대상을 설명하는 명사절이야 하므로 클래스와 인터페이스의 대상은 인스턴스이고, 필드의 대상은 자신이다.
    Instant : 타임라인상의 특정 순간(지점)
    Math.PI : 원주율에 가까운 double 값
  18. 자바 9부터는 자바독이 생성한 HTML 문서에 검색 기능이 추가 되었고 {@index} 태그를 사용해 중요한 용어를 추가로 색인화할 수 있다.
    /*
    * this method complies with the {@index IEEE 754} standard.
    */
  19. 제네릭 타입이나 제네릭 메서드를 문서화 할 때는 모든 매개변수에 주석을 달아야 한다.
    /**
     * 키 하나가 가리킬 수 있는 값은 최대 1개이다.
     * 
     * @param <K> 이 맵이 관리하는 키의 타입
     * @param <V> 맵핑된 값의 타입
     */
    interface Map<K, V> {
    
    }
  20. 열거 타입을 문서화할 때는 상수들도 주석을 달아야 한다.
    /**
    * Station info
    */
    enum Station {
      /** subway station */
      subway 
    }
  21. 애너테이션 타입을 문서화할 때는 멤버들에도 모두 주석을 달아야 한다.
    /**
    * 테스트하기 위한 애너테이션으로 테스트를 위해 존재한다.
    */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @interface Test {
      /**
       * 테스트 하기 위한 함수로 테스트한다.
       */
      void test();
    
    }
    • 필드 설명은 명사구로 하고 요약 설명은 애너테이션 선언이 어떤 의미인지 동사구로 한다.
  22. 패키지를 설명하는 문서화 주석은 package-info.java 파일에 작성하며 패키지 선언을 반드시 포함해야 하며 패키지 선언 관련 애너테치션을 추가로 포함할 수 있다.
  23. 클래스 혹은 정적 메서드가 스레드 안전하든 그렇지 않든, 스레드 안전 수준을 반드시 API 설명에 포함해야 한다.
  24. 자바독은 메서드 주석을 '상속' 시킬수 있다. 문서화 주석이 없는 API 요소를 발견하면 자바독이 가장 가까운 문서화 주석을 찾아준다.
    • 상위 '클래스'보다 그 클래스가 구현한 '인터페이스'를 먼저 찾는다.
    • {@ingeriDoc} 태그를 사용해 상위 타입의 문서화 주석 일부를 상속할 수 있다.

자바독은 프로그래머가 자바독 문서를 올바르게 작성했는지 확인하는 기능을 제공한다.

  • 자바 7에서는 명령줄에서 -Xdoclint 스위치를 키면 기능이 활성화 되고 자바 8부터는 기본으로 작동한다.
  • checkstyle을 이용하면 더 완벽하게 검사된다.
  • 자바 9와 10에서는 명령줄에서 -html5 스위치를 켜면 HTML5버전으로 만들어준다.

정말 잘 쓰인 문서인지를 확인하는 유일한 방법은 자바독 유틸리티가 생성한 웹페이지를 읽어보는 길뿐이다.

직렬화 형태(아이템87), 상속(아이템19), 모든 예외(아이템74), 모듈 시스템(15), 스레드 안전 수준(아이템82), 직렬화 형태(아이템87)

문서화 주석은 여러분 API를 문서화하는 가장 휼룡하고 효과적인 방법이다.

반응형