ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] Effective Java 3/E 정리 - 8장 메서드
    Study/이펙티브 자바 2021. 7. 18. 21:22
    반응형

     

    이번 장에서는 메서드를 설계할 떄 주의할 점을 살펴보자,

    매개변수와 반환값 처리, 메서드 시그니처 설꼐, 문서화 방법에 대한 것을 다루고,

    이번장은 메서드 뿐만 아니라 생성자에도 적용되는 부분이 많다.

     


    Item49. 매개변수가 유효한지 검사하라

    메서드와 생성자 대부분은 입력 매개변수의 값이 특정조건을 만족하기를 바란다.

    이런 제약은 반드시 문서화해야 하며 메서드가 시작되기 전 검사해야하한다.

    메서드 로직이 실행되기 전에 매개변수를 확인한다면 즉각적이고 깔끔한 방식으로 예외를 던질 수 있다.

     

    public과 protected 메서드는 매개변수 값이 잘못됐을 때 던지는 예외를 문서화해야한다. (@throws 자바독 태그를 사용한다.)

    보통은 IllegalArgumentException, IndexOutofBoundsException, NullPointerException 중 하나를 사용한다.

    아래는 전형적인 예시이다.

    /**
     * (현재 값 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);
      ... // 계산 수행
    }

    이 메서드는 매개변수가 null이면 NullPointerException을 던진다.

    이 부분은 BigInteger 클래스 수준에서 기술되었기 때문에 메서드 설명에 기술할 필요가 없다.

    클래스 수준 주석은 그 클래스의 모든 public 메서드에 적용되므로 각 메서드에 기술하는 것보다 훨씬 깔끔한 방법이다.

     

    requireNonNull

    자바 7에 추가된 java.utill.Objects.requireNonNull 메서드를 통해 null 검사를 수동으로 하지 않아도 된다.

    this.strategy = Objects.requireNonNull(strategy, "전략");

    requireNonNull은 명시성, 빠른 실패를 위해 사용하는데, 사용하는 이유에 대한 것은 아래 링크를 참고하자.

    [참고] rockjoon blog - Object.requireNonNull을 왜 쓸까?

     

    자바 9에서는 Objects 범위 검사 기능을 하는 checkFromIndexSize, checkFromToIndex, checkIndex 메서드가 추가되었다.

    null 검사 만큼 유연하지 않고 여러 제약이 있기 때문에 사용에 주의하자.

     

    assert(단언문)

    메서드가 호출되는 상황을 통제하기 위해 assert를 사용해 매개변수 유효성을 검증할 수 있다.

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

    assert는 조건이 무조건 참으로 선언한다.

    일반적인 유효성 검사와 다른 점은 실패하면 AssertionError를 던지고, 런타임에 아무런 효과도, 성능 저하도 없다.

     

    특히 생성자 매개 변수의 유효성 검사는 클래스 불변식을 어기는 객체가 만들어지지 않게 한다.

    그러나 메서드 및 생성자에서 매개변수 유효성 검사 비용이 너무 클 경우 예외가 있다.

    그리고 매개변수에 제약을 두는 것이 좋은 것이 아니라 메서드는 최대한 범용적으로 설계하고,

    구현하려는 개념이 특정한 제약을 내재한 경우 꼭 유효성 검사를 통해 미리 걸러내도록 하자.

     

     

    Item50. 적시에 방어적 복사본을 만들라

    어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능 하다.

    그러나 아래와 같이 자기도 모르게 수정할 수 있는 경우가 생긴다.

    public final class Period {
      private final Date start;
      private final Date end;
      
      /**
       * @param start 시작 시각
       * @param end 종료 시각; 시작 시각보다 뒤여야 한다.
       * @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
       * @thorws 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;
      }
      ... // 나머지 코드 생략
    }
    Date start = new Date();
    Date end = new Date();
    Period p = new Period(start, end);
    end.setYear(78); // p의 내부를 수정했다!

    Date는 가변 객체이기 때문에 end를 손쉽게 변경할 수 있다.

    자바 8 이후로는 불변인 Instant 혹은 LocalDateTime, ZonedDateTime을 사용하면 된다.

    (Date는 낡은 API이기때문에 더이상 사용하지 말자)

     

    생성자의 방어적 복사(defensive copy)

    위와 같이 가변 객체 혹은 다른 방법으로 내부를 보호하기 위해서는

    생성자에서 받은 가변 매개변수를 방어적으로 복사(defensive copy)해야 한다.

    public Peroid(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을 사용해서는 안된다.

     

    접근자의 방어적 복사(defensive copy)

    생성자를 변경해도 접근자 메서드에서 가변 필드를 직접 반환하고 있기 때문에 변경이 가능하다.

    이는 단순하게 접근자 메서드에서 가변 필드의 방어적 복사본을 반환하는 것으로 해결할 수 있다.

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

    Period내에 Date는 하위 클래스가 아님이 확실하기 때문에 접근자는 clone을 사용해도 된다.

    그렇지만 [Item13. clone 재정의는 주의해서 써라] 에 따라서 생성자나 정적 팩터리를 쓰는 것이 좋다.

     

    방어적 복사를 사용하는 이유

    매개 변수를 방어적으로 복사하는 목적은 불변 객체를 만들기 위함만이 아니다.

    클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관할 때,

    그 객체가 잠재적으로 변경되어 자료구조의 불변식이 깨질 수 있기 때문이다.

    내부 객체를 클라이언트에 전달할 때도 마찬가지다.

    특히 길이가 1인 이상의 배열을 반환할 때는 항상 방어적 복사를 수행하자.

     

    되도록 불변 객체를 조합하면 방어적 복사를 할 일이 줄어든다.

    방어적 복사는 성능 저하가 따르고, 같은 패키지에 속하거나 하는 이유로 사용할 수 없기도 하다.

    이런 경우에는 매개변수나 반환값을 수정하지 않아야 함과 해당 책임이 클라이언트에 있음을 문서에 명시하자.

     

    Item51. 메서드 시그니처를 신중히 설계하라

    이번 아이템은 API 설계 요령을 소개한다.

     

    메서드 이름을 신중히 짓자

    • 항상 표준 명명 규칙을 따르는 것 좋다.
    • 이해할 수 있고, 같은 패키지의 이름과 일관되게 짓는다.
    • 보편적으로 사용되는 이름을 사용한다.
    • 긴 이름은 피하자.

    애매하면 자바 라이브러리의 API 가이드를 참조하라.

     

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

    메서드가 너무 많은 클래스는 익히고, 사용하고, 문서화하고, 테스트하고, 유지보수하기 어렵다.

    인터페이스도 모두 구현하기 고통스럽다.

    각 기능을 완벽히 수행하는 메서드로만 제공하고, 편의 메서드는 자주 쓰일 경우만 구현하자.

     

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

    매개 변수는 4개 이하가 좋다.

    너무 많으면 기억하기도 힘들고, 같은 타입일 경우 순서를 바꿔 입력할 가능성이 높다.

    과도하게 긴 매개변수 목록을 줄이려면 아래와 같은 방법을 사용하자.

    • 여러 메서드로 쪼갠다.
    • 매개변수 여러 개를 묶어주는 도우미 클래스를 만든다.
    • 객체 생성에 사용한 빌더 패턴을 메서드 호출에 응용한다.

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

    예를 들어 HashMap보다는 Map을 사용하는 것이 좋다.

    어떠한 Map구현체를 사용할 수 있고 직접 구현한 Map도 가능하다.

    클래스를 사용하면 특정 구현체만 사용하도록 제한하게 되고,

    입력 데이터가 다른 형태로 존재한다면 명시한 구현체로 옮기느라 복사 비용을 치러야한다.

     

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

    열거 타입을 사용하면 코드를 읽고 쓰기가 더 쉬워진다.

    나중에 선택지를 추가하기도 쉽다.

    그리고 단위에 대한 의존성을 상수의 메서드 안으로 리팩터링해 넣을 수도 있다.

    그러나 메서드 이름상 boolean을 받아야 의미가 더 명확할 때는 예외다.

     

     

    Item52. 다중정의는 신중히 사용하라

    다중정의(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)); 
      }

    따라서 위의 코드에서 컴파일 타임에는 항상 Collection<T> 타입이기 때문에 '그 외'만 세번 연달아서 출력된다.

    이처럼 오버로딩이 혼동을 만드는 상황을 피하기 위해 오버로딩하기보다 메서드 이름을 다르게 지어서 분리하는 것이 좋다.

     

    생성자는 이름을 다르게 지을 수 없기 때문에 다른 방법도 존재한다.

    헷갈릴 수 없도록 서로 다른 함수형 인터페이스는 같은 위치에 인수로 받지 않거나 근본적으로 관련 없는 클래스로 구성하자.

    같은 객체를 입력받는 오버로딩 메서드라면 동일하게 동작하도록 만드는 것이 안전한 방법이다.

     

     

    Item53. 가변인수는 신중히 사용하라

    가변인수(varargs)메서드는 명시한 타입의 인수를 0개 이상 받을 수 있다.

    가변인수 메서드를 호출하면, 인수의 개수와 길이가 같은 배열을 만들고 저장하여 메서드에 건네준다.

     

    인수가 1개 이상 필요할 경우, 인수 개수는 런타임에 배열 길이(length)로 알 수 있다.

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

     

    위 코드는 런타임에 실패하기 하고, 코드도 지저분하기 때문에 좋은 코드라고 할 수 없다.

    이 문제는 첫번째 인자는 평범한 매개변수로 전달하고, 두번째부터 가변인수로 받아서 해결할 수 있다.

    static int min(int firstArg, int... remainArgs) { //...생략

    가변인수는 인수 개수가 정해지지 않았을 때 매우 유용하다. ex) printf, 리플렉션

    그러나 성능에 민감한 상황에서는 사용하기 애매하다.

    이럴 경우에 인수를 적게 사용하는 상황이 대다수라면, 다중정의 메서드를 사용하면 성능 문제를 회피할 수 있다.

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

     

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

    컬렉션을 반환하는 메서드에서는 반환할 것이 없을 경우 흔히 null을 반환한다.

    그리고 null을 반환할 때는 방어 코드를 넣어주어야 한다.

     

    빈 컬렉션이나 배열을 반환하는 것이 성능을 저하한다는 우려가 있지만 이 정도 성능 차이는 신경쓰지 않아도 된다.

    그리고 굳이 새로 할당하지 않더라도 미리 '빈 불변 컬렉션'을 만들고 이를 반환하면 된다.

     

    null을 반환하는 API는 사용하기도 어렵고, 오류 처리 코드도 필요하니 빈 컬렉션이나 배열을 반환하자.

     

     

    Item55. 옵셔널 반환은 신중하라

    자바 8 이전에는 메서드가 값을 반환할 수 없을 때는 예외를 던지거나 null을 반환했다.

    그러나 예외는 반드시 예외적인 상황에서 사용해야하며 스택 추적 전체를 캡처하므로 비용이 많이 들었다.

    null은 반드시 별도의 null 처리가 필요하고, 이를 무시한다면 어디선가 NullPointerException이 발생한다.

     

    자바 8에서 추가된 Optional<T>은 null이 아닌 T타입 참조를 담거나 아무것도 담지 않을 수 있다.

    옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 작다.

     

    빈 옵셔널은 Optional.empty()로 만들고, 값이 든 옵셔널은 Optional.of(value)로 만든다.

    Optional.of(value)에 null을 넣으면 NullPointerException이 발생한다.

    null을 허용하는 옵셔널을 만들기 위해서는 Optional.ofNullable(value)를 사용하자.

     

    메서드가 옵셔널을 반환하면 값을 받지 못했을 때 기본값을 설정하거나 예외를 던질 수 있다.

    //기본값 설정
    String lastWordInLexicon = max(words).orElse("단어 없음...");
    
    //예외 던지기
    Toy myToy = max(toys).orElseThrow(TemperTantrumExcepion::new);

    get을 이용해 바로 값을 꺼낼 수 있지만, 빈 옵셔널이라면 NoSuchElementException이 발생한다.

     

    기본값을 설정하는 비용이 커서 부담이 될 때는 Supplier<T>를 인수로 받는 orElseGet을 사용하자.

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

     

    isPresent 메서드는 옵셔널이 채워져 있으면 true를, 비어 있으면 false를 반환한다.

    그러나 앞서 언급한 메서드들로 더 짧고 용법에 맞게 대체 가능하다.

     

    스트림과 옵셔널의 혼용

    다음은 부모 프로세스의 프로세스 ID를 출력하거나 부모가 없다면 "N/A" 출력하는 코드다.

    Optional<ProcessHandle> parentProcess = ph.parent();
    System.out.println("부모 PID: " + (parentProcess.isPresent() ? 
    	String.valueOf(parentProcess.get().pid()) : "N/A"));

     

    위의 코드는 Optional의 map을 사용하면 아래와 같이 다듬을 수 있다.

    System.out.println("부모 PID: " + ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));

     

    스트림을 사용한다면 옵셔널들을 Stream<Optional<T>>로 받아서

    채워진 옵셔널들에게서 값을 뽑아 Stream<T>에 담아 처리하는 경우가 드물지 않다.

    streamOptionals
      .filter(Optional::isPresent)
      .map(Optional::get)

     

    자바 9에서는 Optional에 Stream으로 변환해주는 어댑터인 stream() 메서드가 추가되었다.

    옵셔널에 값이 있으면 값을 담은 스트림으로, 없다면 빈 스트림으로 변환한다.

    streamOfOptionals .flatMap(Optional::stream)

     

    Optional을 사용하면 안되는 경우

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

    빈 컨테이너를 반환할 경우 옵셔널 처리 코드가 필요하지 않다.

     

    결과가 없을 수 있으며, 클라이언트가 상황을 특별하게 처리해야한다면 Optional<T>를 사용해야하지만,

    새로 할당하고 초기화하는 비용이 필요하고 추가적인 처리가 필요하기 때문에 성능이 중요한 상황에는 맞지 않다.

     

    박싱된 기본 타입은 두 겹을 감싸기 때문에 기본타입보다 무거운데,

    이를 위하여 OptionalInt, OptionalLong, OptionalDouble같이 기본 타입을 전용 옵셔널 클래스들이 존재한다.

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

     

    그리고 맵의 값으로는 절대 사용하면 안된다.

    사용하게 된다면 키 자체가 없는 경우와 키가 빈 옵셔널인 경우 두가지가 존재하게 되어 쓸데없이 복잡하다.

     

     

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

    API는 문서를 작성하는 것도 중요한데, 자바에서는 자바독(javadoc)이라는 유틸리티가 작업을 도와준다.

    올바른 문서화를 위해 아래에 나열된 규칙을 참고하도록 하자.

    • 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 주석을 달아야 한다.
      직렬화할 수 있는 클래스라면 직렬화 형태에 대해서도 적어야 한다.
    • 메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 명료하게 기술해야 한다.
    • 각 문서화 주석의 첫번째 문장은 해당 요소의 요약 설명인데, 반드시 대상의 기능을 고유하게 기술해야 한다. 헷갈리지 않기 위해서는 요약 설명이 같은 멤버(혹은 생성자)를 두지 말자.
    • 제네릭 타입이나 제네릭 메서드는 모든 타입 매개변수에 주석을 달아야 한다.
    • 열거 타입에는 상수에도 모두 주석을 달아야 한다.
    • 어노테이션 타입에는 멤버 모두에 주석을 달아야한다.
    • 클래스 혹은 정적 메서드는 스레드 안전 수준을 반드시 포함해야 한다.

    댓글