Jay's Blog

Effective java item 55, 56
이펙티브 자바 정리

Item 55: Optional을 신중하게 리턴하라.

Java 8 이전에는 특정 상황에서 값을 리턴하지 못하는 메소드를 작성할 때 선택할 수 있는 것은 2가지이다. null을 리턴하던지, exception을 던지던지.

Exception은 예외 상황을 위해서만 남겨둬야 한다. (Item 69) Exception을 던지는 것은 매우 비싸다. 왜냐하면 전체 stacktrace가 execption이 생성될 때 잡혀야 하기 때문이다.

null을 리턴하는 것은 위의 문제가 없다. API 메소드에서 null이 리턴되면 클라이언트 메소드에서는 null을 처리하는 로직이 필요하게 된다. 그게 아니면 프로그래머가 null을 리턴하는 것이 불가능하다는 것을 증명해야한다.

이렇게 하지않으면 NullPointerException이 나게 된다…

Java8이 등장한 뒤에 3번째 방법이 등장했다. Optional 클래스는 하나의 non-null값이나 아무것도 안가질 수 있는 불변(Immutable) 컨테이너이다. Collection을 상속하고 있지는 않지만 1개의 element만 가질 수 있는 불변 컬렉션으로 볼 수도 있다.

Optional로 리턴하는 메소드는 값을 리턴하지 않을 수 있기 때문에 좀 더 유연성을 가지게 된다.

// Returns maximum value in collection - throws exception if empty
public static <E extends Comparable<E>> E max(Collection<E> c) {
    if (c.isEmpty())
        throw new IllegalArgumentException("Empty collection");

    E result = null;
    for (E e : c)
        if (result == null || e.compareTo(result) > 0)
            result = Objects.requireNonNull(e);

    return result;
}

이 메소드는 콜렉션이 비어있을 때 IllegalArgumentException을 던진다.

// Returns maximum value in collection as an 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);
}

적절한 static factory만 사용하면 끝이다.. 이상황에서 잘못 Optional.of(null)이 호출되게 되면 NullPointerException이 발생하게 된다. Optional.ofNullable(value)를 사용해야 null 값일 때 empty Optional이 리턴된다.

Optional이 리턴되는 메소드에서는 null 값이 리턴되서는 안된다. 그러지 않으면 Optional 기능의 목적을 잃게 된다.

스트림에 있는 많은 종단 연산(terminal operation)들도 optional을 리턴한다.

// Returns max val in collection as Optional<E> - uses stream
public static <E extends Comparable<E>>
        Optional<E> max(Collection<E> c) {
    return c.stream().max(Comparator.naturalOrder());
}

위와 같이 max값을 구하는 메소드를 stream으로 구현하면 optional을 얻을 수 있다.

Optional은 checked exception과 비슷하다. API를 사용하는 유저가 강제로 그 exception을 처리해야만 한다. unchecked exception을 던지던지, null을 리턴하게 되면 유저가 완전히 무시하게 할수도 있다. checked exception을 던지는 것은 client의 boilerplate 코드를 늘린다.

// Using an optional to provide a chosen default value
String lastWordInLexicon = max(words).orElse("No words...");

empty optional이 리턴될 수 있다면 기본값을 설정해주던, exception을 던지든 위와 같은 코드가 필요하다.

// Using an optional to throw a chosen exception
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

Exception을 직접 만들어서 던지게 되면 비용을 조금을 줄일 수 있다.

// Using optional when you know there’s a return value
Element lastNobleGas = max(Elements.NOBLE_GASES).get();

값이 있다고 가정한다면, 위와 같이 작성가능하고, 만약에 Optional이 empty라면 NoSuchElementException이 발생한다.

기본값을 얻는데 비용이 많이 드는 경우(계산이 필요한 경우)에 orElseGet(Supplier)를 제공해주고 있다.

filter, map, flatMap, and ifPresent 메소드를 제공하고 있고, 자바9에는 or and ifPresentOrElse도 제공하고 있다.

Optional은 isPresent() 메소드를 제공하고 있고 매우 유용하다. 위에 소개된 메소드들과 잘 비교하면서 사용할 필요는 있다.

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

위를 잘 비교해봐야 한다.

Stream<Optional> 스트림에서 Stream로 모든 element가 nonempty optional일 필요가 있는 경우도 있다.

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

그 경우에는 위와 같이 사용할수 있다.

Java9에서는 Optional이 stream() 메소드를 제공한다. element가 존재하는 경우만 stream으로 흘려보내준다.

streamOfOptionals.
    .flatMap(Optional::stream)

Container 타입인 경우에는 Optional을 사용해서는 안된다

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

Optional의 문제점들

사용하지 말아야 할 경우

결론

값을 항상 리턴하지 않는 메소드가 필요한 경우 Optional을 사용할 수 있다. 컬렉션에 사용될지, Container 객체인지, 성능에 영향을 없는지 잘 살펴볼 필요는 있다.

Item 56: 모든 공개된 API에 doc 코멘트를 달아라

API가 사용가능하려면, 문서화되어야 한다. 전통적으로 수동으로 관리되어왔다. Javadoc 유틸리티가 등장하면서 코멘트를 이용해서 이를 관리할 수 있게 되었다.

Java 9, {@index}

Java 8, {@implSpec}

Java 5, {@literal} and {@code}

위의 태그들이 추가되어 왔다.

공개된 class, interface, constructor, method, and field declaration은 doc 코멘트를 달아야 한다.

클래스가 serializable하다면 serialized form을 문서화 해야한다(item 87)

문서화 없이는 API를 사용하기가 힘들고, 매우 에러가 생기기 쉽다. Public API는 javadoc을 제공할 수 없기 때문에 default 생성자를 사용하면 안된다.

또한 코드가 지속적으로 유지보수 되기 위해서는 공개되지 않은 클래스, 인터페이스, 생성자, 메소드, 필드에 대해서도 doc 코멘트를 작성해야 한다.

메소드의 doc comment

javadoc은 html로 코멘트들을 변환한다.

/**
 * Returns true if this collection is empty.
 *
 * @implSpec
 * This implementation returns {@code this.size() == 0}.
 *
 * @return true if this collection is empty
 */
public boolean isEmpty() { ... }

위와 같이 implSpec을 이용해서 상속해서 사용했을 때에 관한 코멘트를 남길 수 있다.

/**
* A geometric series converges if {@literal |r| < 1}.
**/

{@literal} 태그로 < > &를 표현할수 있다.

doc 코멘트는 코멘트로도 html로도 읽기 편해야 한다.

첫줄은 전체적인 요약을 나타내고, 같은 description을 가진 것들은 만들면 안된다.

기간 같은게 포함되면 주의해야한다.

/**
* This method complies with the {@index IEEE 754} standard.
**/

Java9부터는 index를 나타낼 수 있는 태그가 생겼다. 클라이언트가 엄청 큰 API를 찾아갈 때 유용하게 사용될 수 있다.

제네릭의 경우 모든 type parameter를 표시해야한다.

/**
 * An object that maps keys to values.  A map cannot contain
 * duplicate keys; each key can map to at most one value.
 *
 * (Remainder omitted)
 *
 * @param <K> the type of keys maintained by this map
 * @param <V> the type of mapped values
 */
public interface Map<K, V> { ... }

enum타입의 경우에는

/**
 * An instrument section of a symphony orchestra.
 */
public enum OrchestraSection {
    /** Woodwinds, such as flute, clarinet, and oboe. */
    WOODWIND,

    /** Brass instruments, such as french horn and trumpet. */
    BRASS,

    /** Percussion instruments, such as timpani and cymbals. */
    PERCUSSION,

    /** Stringed instruments, such as violin and cello. */
    STRING;
}

위와 같이 한줄씩 달수도 있다.

annotation 타입의 경우에는

/**
 * Indicates that the annotated method is a test method that
 * must throw the designated exception to pass.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
     /**
      * The exception that the annotated test method must throw
      * in order to pass. (The test is permitted to throw any
      * subtype of the type described by this class object.)
      */
    Class<? extends Throwable> value();
}

타입과 member에 다 문서화가 필요하다.

Package-level의 코멘트는 package-info.java에 작성할 수 있다.

module-level의 코멘트(item 15, java9)는 module-info.java에 작성할 수 잇다.

종종 무시되는 문서화는 thread-safety와 serializablility에 관한 것이다.

메소드가 thread-safety한지에 관계없이 thread-safety에 관해 문서화 해야한다. (item 82)

클래스가 serializable하다면, serialized form을 문서화 해야한다(item 87)

Javadoc은 method 코멘트를 상속할 수 있다. {@inheritDoc} tag를 사용하면, 상위 클래스 코멘트의 일부를 가져올 수 있다. 복사하기보다는 상속해서 사용하는 것이 가능하다는 의미이다. 나중에 유지보수의 비용을 줄여줄수 있다.

복잡한 API일 경우에는 코멘트 뿐 아니라 다른 형태의 문서화도 필요하다(아키텍처 그림 등)

Javadoc은 자동으로 이 챕터 내용을 체크할 수 있는 기능이 있다. Java7에서는 -Xdoclint 옵션으로 가능했고, Java 8 and 9은 자동으로 이 기능이 켜져있다.

checkstyle같은 plugin도 도움이 될 수 있다. Html의 결과를 W3C-validator 확인해 볼 수도 있다.

이미 만들어진 다른 코멘트를 많이 보는 것이 도움이 된다.

결론

문서화는 공개된 API에는 필수적으로 해야하고, 가장 쉽게 클라이언트에게 API기능을 알려줄 수 있는 방법이다.

*****
Written by Jay on 05 November 2020