Effective Java - Chapter 8

45 분 소요

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

매개변수 검사

메서드와 생성자의 매개변수의 값은 특정 조건을 만족한다는 가정 하에 코드를 작성할 것이다. 인덱스 값이 음수가 되지 않아야 하는게 대표적인 예다. 제약 조건은 코드 body를 작성하기 전에 문서화 해야 하며, 반드시 매개변수를 받자마자 검사하여야 한다.

만약 검사를 하지 않는다면 메서드가 수행되는 중간 모호한 예외를 던지며 실패할 수 있다. 혹은 예외를 던지지 않고 잘못 수행되어 예상하지 못한 결과를 던질 경우에는 해당 메서드 바깥으로 오류가 전파될 수 있다.

public과 protected 메서드는 매개변수 값이 잘못됐을 때 던지는 예외를 문서화 해야한다.

  • IllegalArgumentException
  • IndexOutOfBoundsException
  • NullPointerException

중 하나를 던지는 것을 고려할 수 있다. 어떤 매개변수에서 어떤 제역을 어겨서 어떤 예외가 발생하는 지 기술하면 좋다.

예제 1 : BigInteger mod() 메서드의 제약조건

public BigInteger mod(BigInteger m) {
		if (m.signum() <= 0)
				throw new ArithmeticException("Modulus <= 0: " + m);
		... // Do the computation
}

해당 메서드의 docs에는 예외에 대한 설명이 없다. BigInteger 클래스 수준에 기술했기 때문에 메서드에 기술하는 것 보다 훨씬 깔끔한 방법이 될 수 있다.

예제 2 : Objects.requireNonNull(strategy, “전략”);

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

자바에서는 null 검사를 돕는 requireNonNull 메서드를 제공한다.

예제 3 : 공개 API가 아닌 메서드를 위한 assert

// Private helper function for a recursive sort
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를 이용하자.

예외를 던지는 것과의 차이점은

  1. 실패 시 AssertionError를 던진다.
  2. 런타임에 아무런 성능 저하가 없다.

    다만 -ea 혹은 –enableassertions 플래그를 달고 실행시에는 영향을 준다.

나중을 위해 저장하는 매개변수는 더 엄격하게 검사해야 한다.

정적 팩터리 메서드 패턴을 생각해보자.

예제 4 : ITEM 20에서 다뤘던 정적 팩터리 메서드


static List<Integer> intArrayAsList(int[] a) {
		Objects.requireNonNull(a);
		return new AbstractList<>() {
				@Override public Integer get(int i) {
						return a[i]; // Autoboxing (Item 6)
				}
				@Override public Integer set(int i, Integer val) {
						int oldVal = a[i];
						a[i] = val; // Auto-unboxing
						return oldVal; // Autoboxing
				}
				@Override public int size() {
						return a.length;
				}
		};
}

입력받은 int 배열을 List 처럼 다룰 수 있는 view를 제공하는 정적 팩터리 메서드였다. 만약 Objects.requireNonNull(a); 검사를 수행하지 않았다면, null을 받아도 문제없이 수행하겠지만 나중에 접근할 때가 되어서야 NullPointerException을 던진다. 이 view는 어디서 왔는지 알 길도 없어서 디버그가 골치아파진다.

생성자도 나중을 위해 저장하는 매개변수의 특별한 예시 중 하나이다. 생성자 매개변수의 유효성 검사는 클래스 불변식을 망치는 객체가 만들어지지 않게 하는데 중요하다.

메서드 매개변수 유효성 예외

만약 매개변수 유효성 검사비용이 높은 경우, 실용적이지 않은 경우, 계산과정에서 암묵적으로 검사가 수행될 때다.

Collections.sort(List)를 생각했을 때, 리스트 안 객체는 Comparable을 구현하여야 한다. 그러나

  1. 리스트를 전체 순회하며 검사해야 하는데, 리스트가 매우 크다면 배보다 배꼽이 더 큰 경우가 생긴다.
  2. 비교가 불가능한 객체가 있다면 ClassCastException을 던질 것이기 때문에 비교과정에서 자연스레 유효성이 검사될 것이다.

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

자바는 C/C++보다 안전한 언어라는 점이 큰 장점이다. C/C++처럼 배열이나 struct, class 밖에서 안을 수정할 수 있는 언어와 달리 Java는 불변식을 지킬 수 있다. 하지만 아무 노력없이 이를 성취할 수 있는 것은 아니며 클라이언트가 불변식을 깨뜨리는 악마라고 가정하고 방어적으로 코드를 작성해야 한다. 보안적인 측면에서도, 오작동을 방지하기 위해서라도 말이다.

객체의 허락없이 객체 외부에서 내부를 수정하도록 두어서는 안되지만, 실수로 내부를 수정할 수 있도록 여는 일이 생긴다.

예제 1 : Date를 이용한 Class

public final 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 + "가 " + end + "보다 늦다.");
        this.start = start;
        this.end   = end;
    }

    public Date start() {
        return start;
    }
    public Date end() {
        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); // Modifies internals of p!
		}

    public String toString() {
        return start + " - " + end;
    }
}

start가 end보다 늦다는 불변식을 지키려고 했지만, Date는 불변이 아니며 문제가 많은 객체다. 불변인 Instant를 사용하면 되지만 Date가 오래 널리 쓰인 탓에 API의 내부 구현에 잔재가 남아있으며 Date를 모두 덜어내기는 불가능하다. 만약 외부로부터 내부를 보호하려면,

  1. 외부에서 받은 매개변수를 복사해서 저장한다.
  2. 내부에서 외부로 데이터를 전달할 때 복사해서 반환한다.

예제 2: Date를 이용했지만 방어적 복사를 통해 불변식을 지킨다.


    public Period(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 + "보다 늦다.");
    }

    // 코드 50-5 수정한 접근자 - 필드의 방어적 복사본을 반환한다. (305쪽)
    public Date start() {
        return new Date(start.getTime());
    }

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

앞에서 다뤘던 매개변수의 유효성을 검사할 때도 방어적 복사를 이용하여야 한다. 멀티쓰레드 환경에서는 다른 쓰레드가 race condition에서 원본을 변경하는 일이 벌어질 수도 있다. 이를 방지하기 위해서 유효성 검사를 하기 전에 복사한 후에 유효성 검사를 수행ㅐ야한다.

clone을 사용하지 않은 이유는, Date가 재정의한 것이 아닐수도 있기 때문에 악의적인 하위 클래스의 인스턴스를 반환할 수도 있다. 만약 악의적인 clone 함수가 참조를 가지고 있도록 설계되었다면 공격자가 clone을 호출한 인스턴스 내부로 접근할 수 있는 길을 깔아줄 것이다.

start()와 end()도 Date 인스턴스의 복제를 외부에 전달한다. 클라이언트가 반환된 Date를 아무리 수정해도 내부의 Date는 변경되지 않을 것이다. 이제 외부에서 내부를 수정할수 있는 방법이 없으며 불변식을 지킬 수 있다.

불변 객체를 받는다면 방어적 복사에 드는 비용을 아낄 수 있다.

통제권과 약속

매개변수나 생성자를 통해서 넘겨받은 가변 인스턴스를 모두 방어적으로 복사해야할까? 가변 인스턴스를 넘기는 행위 자체가 ‘나는 더 이상 이 인스턴스를 수정하지 않을 것’이라는 의미를 내포할 수도 있다. 이러한 경우에는 클라이언트는 해당 객체를 수정할 일이 없어야 하며 그러한 기대를 하는 메서드나 생성자의 문서에 그러한 약속을 기술하여야 한다.

방어적 복사를 생략하고자 한다면,

  1. 약속을 지킬 수 있는 신뢰할 수 있는 클라이언트인지 확인해야하며,
  2. 불변식이 깨져 문제가 생기더라도 클라이언트만 그 책임을 지는 상황으로 한정지어야 한다.

변할 수 있는 것들

  • 길이가 1 이상인 배열
  • 컬렉션
  • 불변이 아닌 클래스 인스턴스

ITEM 51. 메서드 시그니쳐를 신중히 설계하라

메서드 이름을 신중하게 짓자.

  • 메서드 이름만 보고도 동작을 유추할 수 있도록
  • 같은 패키지에 있는 다른 메서드와 일관성
  • 개발자 커뮤니티에서 통용되는 convention 따르기
  • 긴 이름 피하기

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

  • 메서드가 너무 많은 클래스는 쓰기 어렵고 유지보수하기 어렵고 문서화 하기 어렵고 기타 등등 힘들다!
  • 메서드는 각 기능을 완벽히 수행하는 메서드로 제공해야 한다.
  • 자주 쓰이는 경우에는 고려해보자.
  • 편의성 vs 복잡성의 문제다.

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

  • 4개 이하가 좋다. 사람의 기억 능력엔 한계가 있다.

    참고하기 : [코싸인의 인지과학 이야기] 기억(3) by 코싸인

  • 특히 같은 타입의 매개변수가 연속하여 위치한다면, 개발자가 실수로 순서를 바꿔서 사용할 가능성이 있으며 의도와 다른 엉뚱한 동작이 있을 수 있다.

매개변수 목록을 짧게 쪼개는 테크닉

  1. 여러 메서드로 쪼갠다

    메서드가 많아질 걱정을 할 수도 있지만, 소프트웨어 공학적으로 널리 받아들여지는 개념인 직교성(Orthogonality)을 높여서 메서드를 줄일 수 있다.

    List 인터페이스의 부분 리스트에서 주어진 원소의 인덱스를 찾는다고 가정하면, 매개변수는 리스트의 시작, 끝, 찾을 원소 총 3개의 매개변수를 필요로 할 것이다. 그러나 인터페이스를 잘 살펴보면

    1. 부분 리스트를 반환하는 subList
    2. 인덱스를 알려주는 indexOf 메서드

    를 기술하고 있으며 각각 매개변수 2개, 1개로 쪼개서 메서드를 호출한 결과를 잘 버무리면 된다.

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

    복수의 매개변수를 하나의 개념으로 묶을 수 있을 때 사용할 수 있다. 예를 들면 2차원 평면상의 좌표인 x,y를 Coordination이란 클래스로 묶을 수도 있고, 카드는 숫자와 무늬로 묶을 수도 있다. 일반적으로 정적 멤버 클래스로 두는 것이 좋다.

  3. 빌더 패턴을 이용한다.

    매개변수가 많지만 일부는 생략해도 좋을 때 사용할 수 있다.

    1. 모든 매개변수를 하나로 추상화한 객체를 만든다. (2번 테크닉에서 다룬 카드, 좌표 등)
    2. setter 메서드로 필요한 매개변수를 세팅
    3. execute 메서드로 설정한 매개변수의 유효성 검사
    4. 해당 객체를 넘겨 원하는 계산 수행

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

  • Map 인터페이스를 사용하면 HashMap, TreeMap 등 Map 인터페이스를 구현한 모든 구현체를 사용할 수 있다.

boolean과 원소 2개짜리 열거타입

  • boolean이든 원소 2개짜리 열거타입이든 가장 의미를 잘 드러낼 수 있는 타입을 사용한다.
  • 만약 어떤 학생의 시험 통과여부를 나타내는 변수는, boolean이 더 적합할 수도 있다. (isPassed=true, false)
  • 온도계 클래스가 있다고 가정하자.

    public enum TemperatureScale {FARENHEIT, CELSIUS}
    
    Thermometer.newInstance(true);
    Thermometer.newInstance(TemperatureScale.CELSIUS);
    
    

    둘 중 가독성이 좋은 코드는 자명하다.

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

다중정의(Overloading)는 어느 메서드가 호출될 지 컴파일 타임에 정해진다.

예제 1 : 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));
    }
}

runtime에선 classify의 매개변수인 c의 타입은 변하지만, classify 호출이 Collection<?> c를 매개변수로 받는 메서드가 호출되는 것이 결정되기 때문에, “그 외”만 3번 출력이 될 것이다.

예제 1-1 : 수정한 classify 메서드

public class FixedCollectionClassifier {
    public static String classify(Collection<?> c) {
        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));
    }
}

예제 1을 작성한 개발자는 1-1과 같은 동작을 의도했을 것이지만, 실제로는 그렇지 않다는 것을 알 수 있다.

헷갈릴 수 있는 코드는 작성하지 말아야 한다. 이러한 문제가 공개 API에 존재한다면, 상황은 더 심각해진다. 다중정의가 혼동을 줄 수 있는 상황은 만들지 않는 것이 좋다. 예제 1과 같은 상황을 피하기 위해서 매개변수 수가 같은 다중정의는 만들지 않는 보수적인 해결법을 고려하자. 가변인수(varargs)를 사용하는 메서드도 다중정의를 하지 않는 편이 좋기는 마찬가지다.(ITEM 53 참조) 차라리 메서드 이름을 다르게 짓는 방법이 현명하다. 아래 Overloading 메서드 이름 다르게 짓기 문단 참조.

예제 2 : 무엇을 출력할까?


public class SetList {
    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);
            list.remove(i);
						//list.remove((Integer) i);
						//의도한 동작
        }
        System.out.println(set + " " + list);
				//[-3,-2,-1] [-2,0,2]
    }
}
  • set.remove(Object) ← int가 Integer로 오토박싱 되어 들어간다. 매개변수로 넘겨진 것을 set에서 제거한다. 문제없이 작동한다.
  • list.remove(Object)

    위의 set.remove와 동일하다.

  • list.remove(int)

    매개변수를 인덱스로 받아들여 해당 인덱스의 원소를 제거한다.

다중정의의 모호함 때문에 혼란을 가져오는 예시다. 제네릭과 오토박싱이 이뤄내는 환장의 하모니다.

Overriding은 동적으로 선택되고, Overloading은 정적으로 선택된다. 타당한 설계인게, Interface I를 구현한 부모 클래스 Parent를 상속한 Child는 Parent의 메서드를 Overriding 하게 될 경우에는, I로 메서드를 호출해도 Child가 Overriding한 메서드를 호출하게 되며, Child는 여럿일 수 있으므로 정적으로 확정할 수 없다. 그에 반해 Overloading은 변동의 여지가 없다.

예제 3 : Overriding

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 메서드 이름 다르게 짓기

ObjectOutputStream 클래스에는, write 메서드를 다중정의하는 대신 wirteBoolean(boolean), writeInt(int)와 같이 이름을 다 다르게 짓는 방법을 택했다. readBoolean, readInt와 같이 read-write 짝을 맞추기 좋은 방법을 택했다.

생성자는?

생성자는 이름이 클래스 이름과 같아야 하니 두 번째 생성자부터는 Overloading이 될 수 밖에 없다. 생성자는 재정의할 수 없으니 Overloading과 Overriding이 섞일 걱정은 하지 않아도 되지만 같은 수의 매개변수를 받는 경우를 피할 수 없는 경우에는?

  1. 매개변수 중 단 하나라도 근본적(radically different)으로 다르다면 재정의 문제는 일어나지 않는다.

    radically different의 의미는 null이 아닌 두 타입을 어느 한쪽으로 형변환할 수 없음을 의미한다. int를 매개변수로 받는 생성자와 Collection을 받는 생성자는 어느 생성자가 호출될지 컴파일타임에 결정되지 않고, 런타임에 결정된다.

그 외

  • 함수형 인터페이스도 다중정의의 함정에서 자유로울 수 없다. 같은 위치의 인수로 받지 말자.
  • Java 언어의 업그레이드 및 API에 상속 관계가 복잡해져 위험해 보이는 다중정의를 하게 되더라도 메서드들이 모두 같은 기능을 한다면 문제없다. (예제2의 경우.)

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

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

가변인수는 명시한 타입의 0개 이상, 갯수가 정해지지 않았지만 원하는 만큼 매개변수를 받을 수 있는 Java의 언어적 기능이다.

예제 1 : 가변인수를 사용한 합 구하기

		static int sum(int... args) {
        int sum = 0;
        for (int arg : args)
            sum += arg;
        return sum;
    }

가변인수를 최소 1개 이상 받도록 강제하는 테크닉

예제 2 : 최솟값을 구하는 가변인수를 사용한 메서드

    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개이면, 최솟값은 정의가 되지 않을 것이다. 코드도 지저분해질 수 밖에 없고, 가장 중요한건 컴파일 타임에 이 문제를 잡지 못하고 런타임에 문제를 확인한다.

대신 매개변수 하나를 받고 가변인수를 하나, 총 2개를 받으면 최소 1개의 매개변수를 넣도록 강제할 수 있다.

예제 3 : 평범한 인자 + 가변인수로 최소 1개 이상의 인자 강제하기

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

가변인수 성능 최적화하기

가변인수는 내부적으로 배열을 생성한다. 메서드가 호출될 때 마다 배열을 만들어 전달하는 과정에서 오버헤드가 발생한다. 가변인수가 필요한 상황에서 오버헤드를 최소한으로 줄이고 싶다면, 프로파일링을 통해 인수가 몇 개인 메서드가 가장 자주 쓰이는지 수치를 얻어낸 다음, 호출의 대부분을 차지하는 범위까지 일반인자를 통해 메서드를 정의한다. 만약 인자 3개짜리 메서드가 대부분의 호출을 담당한다면, 아래와 같이 만들 수 있다.

예제 4 : 다중정의로 가변인수 최적화하기

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

Java API에서 EnumSet.of가 이 구현을 충실히 따르고 있다.

참조하기 : EnumSet.java Line 200~

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

일반적으로 API가 요청을 올바르게 수행하지 못했거나, 메서드가 해당하는 원소를 찾지 못했다거나 하는 것을 반환값으로 표시하기 위해 null을 리턴하는 경우가 잦다.

예제 1 : 치즈 재고관리

private final List<Cheese> cheesesInStock = ...;

public List<Cheese> getCheese(){
		return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}
// 만약 cheesesInStock이 비었다면  null을 반환한다.

List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON))
		System.out.println("Jolly good, just the thing.");
// 클라이언트가 불필요하게 null인지 확인하는 과정을 거쳐야한다.

문제는 null 값을 리턴하게 될 경우 클라이언트에서 별도로 null을 핸들링하는 수고를 지게된다. 클라이언트가 핸들링을 까먹는다면 오류가 발생할 것이며, 클라이언트가 까먹지 않고 핸들링을 제대로 해주길 기도하는 것은 좋은 설계가 아니다.(인간은 불완전하다.)

대신, 빈 컬렉션이나 빈 배열을 반환하자.

예제 2 : null 대신 빈 컬렉션 반환

public List<Cheese> getCheeses() {
		return new ArrayList<>(cheesesInStock);
}
// 만약 cheesesInStock의 길이가 0이라면,
// 빈 ArrayList를 반환할 것이다.
  1. length가 0인 배열은 불변이다.

    public Cheese[] getCheeses() {
    		return cheesesInStock.toArray(new Cheese[0]);
    }
    // cheesesInStock을 배열로 바꾸는 과정에서,
    // new Cheese[0]이 우리가 원하는 반환타입을 지정한다.
    // Cheese[]
    
    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()]);
    // 윗 코드와 같이 미리 필요한 공간만큼 배열을 할당하는건
    // 퍼포먼스를 해친다는 연구 결과가 있다.
    
  2. 불변 빈 컬렉션을 지원하는 메서드들이 있다.

    • Collections.emptyList()
    • Collections.emptyMap()
    • Collections.emptySet()

    상기 메서드들은 불변의 빈 컬렉션을 반환한다.

ITEM 55. 옵셔널 반환은 신중히 하라

만약 어떤 메서드가 값을 반환할 수 없으면, null 혹은 예외를 던지는 선택지가 있었다. null을 반환하는 것의 문제점은 ITEM 54에서 다뤘고, 예외를 남발하면 예외가 예외적인 상황으로 받아들여지지 않으며 Stack tracing에 많은 오버헤드를 지불하여야 한다.

Optional의 등장

Java 8부터 Optional을 지원하는데, T타입의 참조를 담을 수 있거나(‘present’) 혹은 아무것도 담지 않을 수 있다(‘empty’). 원소를 단 1개만 갖는 ‘불변’ 유사 컬렉션이다.

**예제 1 : Optional를 이용한 최댓값 구하는 메서드**

public class Max {
    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);
    }
    public static void main(String[] args) {
        List<String> words = Arrays.asList(args);

        System.out.println(max(words));

        String lastWordInLexicon = max(words).orElse("단어 없음...");
        System.out.println(lastWordInLexicon);
    }
}

주의할 점은 Optional.of(null)은 NullPointerException을 던진다. ofNullable() 메서드를 사용해 null을 반환할 수 있지만 Optional을 반환할 때 null값을 반환하지 말자. 이렇게 작성할 코드였으면 그냥 null을 반환하는 것이 낫다.

Optional을 반환하는 것 만으로 클라이언트가 반환 값이 없음을 인지할 수 있다. 클라이언트가 Optional을 사용하는 방법 3가지를 아래에 소개한다.

예제 2 : 기본값 정하기 / 예외 던지기 / 항상 값이 채워져있다고 가정하기

String lastWordInLexicon = max(words).orElse("No words...");

Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

Element lastNobleGas = max(Elements.NOBLE_GASES).get();
//만약 'empty' Optional일 경우 NoSucheElementException 발생

Optional.isPresent() 메서드는 empty / present 여부에 따라 각각 false, true를 반환한다. 그러나 null을 반환하는 메서드를 핸들링하는 로직과 크게 다르지 않으며 앞에 소개한 3개의 메서드로 대체할 수 있는 경우가 많다.

예제 3: isPresent() 메서드를 이용한 Optional 핸들링

Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("Parent PID: " + (parentProcess.isPresent() ?
		String.valueOf(parentProcess.get().pid()) : "N/A"));
// null을 처리하는 로직과 유사하다!

System.out.println("Parent PID: " +
		ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));
// Optional의 map() 메서드를 이용하여 다듬었다.

streamOfOptionals
		.filter(Optional::isPresent)
		.map(Optional::get)
// Stream으로 present의 value를 추출한다.

주의 사항

  1. 컬렉션, 스트림, 배열, 옵셔널과 같은 컨테이너는 또 optional로 감싸지 말아야 한다.

    앞의 아이템에서 다뤘듯이 컬렉션, 배열은 length/size가 0인 컨테이너를 반환하는 것이 좋다.

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

    그러나 Optional로 wrap하고 다시 꺼내는 비용, 객체 생성 비용 등이 있으니 오버헤드가 발생한다. 유저가 실수할 여지를 줄이느냐, 성능을 중시하느냐 잘 고려해야 할 것이다.

  3. 박싱된 기본 타입을 옵셔널로 반환하지 말자.

    기본 타입을 감싸서 Optional로 또 감싼다면 2중으로 감싸게 된다. 당연히 성능 문제가 있을 것이다. 친절하게 자바 API에는 int, double, long 전용 Optional 클래스를 만들었고 Optional의 메서드 대부분을 지원하므로 더더욱 2중으로 감쌀 필요가 없다.

  4. Optional을 컬렉션의 Key, Value, 혹은 배열의 Element로 사용하지 말자.

    Map의 Key로 Optional을 사용한다면, Key가 Map에 존재하지 않는 상태는 Key가 진짜 존재하지 않는 경우와 Key는 존재하지만 empty인 경우이다. 복잡성만 키우고 이득은 없으니 쓰지말자.

  5. 필드를 Optional로 선언하는 건 대부분의 상황에서 좋지 않다.

    대부분의 상황에서는 Optional을 사용하는 대신에 클래스를 나누어 필수 필드만을 담고 있는 클래스를 만들고 이를 확장해 선택 필드를 추가해야 한다는, 일종의 신호이기도 하다 (책에서는 ‘bad smell’로 표기했다).

    NutritionFacts, 영양 정보는 대부분의 필드가 값이 없을 수 있으며 특별한 상속관계로 만들기도 마땅치 않으니 이 때는 Optional을 인스턴스 필드로 가질 수 있지만 일반적인 상황은 아닐 것이다.

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

doc 주석이란?

예제 1 : Java 8의 ArrayList doc 주석

/**
 * Resizable-array implementation of the <tt>List</tt> interface.  Implements
 * all optional list operations, and permits all elements, including
 * <tt>null</tt>.  In addition to implementing the <tt>List</tt> interface,
 * this class provides methods to manipulate the size of the array that is
 * used internally to store the list.  (This class is roughly equivalent to
 * <tt>Vector</tt>, except that it is unsynchronized.)
 *
 * <p>The <tt>size</tt>, <tt>isEmpty</tt>, <tt>get</tt>, <tt>set</tt>,
 * <tt>iterator</tt>, and <tt>listIterator</tt> operations run in constant
 * time.  The <tt>add</tt> operation runs in <i>amortized constant time</i>,
 * that is, adding n elements requires O(n) time.  All of the other operations
 * run in linear time (roughly speaking).  The constant factor is low compared
 * to that for the <tt>LinkedList</tt> implementation.
 *
 * <p>Each <tt>ArrayList</tt> instance has a <i>capacity</i>.  The capacity is
 * the size of the array used to store the elements in the list.  It is always
 * at least as large as the list size.  As elements are added to an ArrayList,
 * its capacity grows automatically.  The details of the growth policy are not
 * specified beyond the fact that adding an element has constant amortized
 * time cost.
 *
 * <p>An application can increase the capacity of an <tt>ArrayList</tt> instance
 * before adding a large number of elements using the <tt>ensureCapacity</tt>
 * operation.  This may reduce the amount of incremental reallocation.
 *
 * <p><strong>Note that this implementation is not synchronized.</strong>
 * If multiple threads access an <tt>ArrayList</tt> instance concurrently,
 * and at least one of the threads modifies the list structurally, it
 * <i>must</i> be synchronized externally.  (A structural modification is
 * any operation that adds or deletes one or more elements, or explicitly
 * resizes the backing array; merely setting the value of an element is not
 * a structural modification.)  This is typically accomplished by
 * synchronizing on some object that naturally encapsulates the list.
 *
 * If no such object exists, the list should be "wrapped" using the
 * {@link Collections#synchronizedList Collections.synchronizedList}
 * method.  This is best done at creation time, to prevent accidental
 * unsynchronized access to the list:<pre>
 *   List list = Collections.synchronizedList(new ArrayList(...));</pre>
 *
 * <p><a name="fail-fast">
 * The iterators returned by this class's {@link #iterator() iterator} and
 * {@link #listIterator(int) listIterator} methods are <em>fail-fast</em>:</a>
 * if the list is structurally modified at any time after the iterator is
 * created, in any way except through the iterator's own
 * {@link ListIterator#remove() remove} or
 * {@link ListIterator#add(Object) add} methods, the iterator will throw a
 * {@link ConcurrentModificationException}.  Thus, in the face of
 * concurrent modification, the iterator fails quickly and cleanly, rather
 * than risking arbitrary, non-deterministic behavior at an undetermined
 * time in the future.
 *
 * <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
 * as it is, generally speaking, impossible to make any hard guarantees in the
 * presence of unsynchronized concurrent modification.  Fail-fast iterators
 * throw {@code ConcurrentModificationException} on a best-effort basis.
 * Therefore, it would be wrong to write a program that depended on this
 * exception for its correctness:  <i>the fail-fast behavior of iterators
 * should be used only to detect bugs.</i>
 *
 * <p>This class is a member of the
 * <a href="{@docRoot}/../technotes/guides/collections/index.html">
 * Java Collections Framework</a>.
 *
 * @author  Josh Bloch
 * @author  Neal Gafter
 * @see     Collection
 * @see     List
 * @see     LinkedList
 * @see     Vector
 * @since   1.2
 */

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//...
}

/*_ … _/의 형태를 띄고 있으며, 메서드 혹은 클래스 선언 위에 작성할 때 JavaDoc이 자동으로 문서화를 해주는 특별한 주석이다. 대다수의 IDE에서 doc 주석을 인지하고 설명을 툴팁 형태로 표기해주는 기능을 가지고 있다.

예제 2 : Eclipse에서 JavaDoc 주석을 표기하는 기능

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c34fd058-bdee-459c-97b6-cfc9534d93f3/Untitled.png

참고하기 : How to Write Doc Comments for the Javadoc Tool

많은 개발자들이 이 API 문서를 참고하기 때문에, API 유저가 혼란에 빠지지 않도록 문서를 잘 작성하는 것이 중요하다.

API 문서화 규칙

  1. 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에doc comment를 달아야 한다.

    다른 사람이 유지보수 할 때를 대비해서 공개되지 않은 요소들에도 doc comment를 다는 것이 좋다.

  2. 기본 생성자엔 doc comment를 달 수가 없으니, 공개된 클래스는 기본 생성자를 사용하면 안된다.
  3. 메서드용 문서화 주석에는 해당 메서드와 클라이언트의 규약을 명료하게 기술하여야 한다.

    해당 메서드가 어떻게(How) 동작하는 지가 아닌, 무엇을(What) 하는지 기술하여야 한다.

    • 메서드를 호출하기 위한 전제조건(precondition)

      전제 조건으로 인해 발생할 수 있는 비검사 예외는 @throws 태그로 암시적으로 기술하며, @param 태그로 전제조건에 의해 영향받는 매개변수에 기술할 수도 있다.

    • 메서드가 성공적으로 수행된 후 만족해야 하는 사후조건(postcondition)
    • 부작용(side effect)

      백그라운드 스레드를 시작시키는 메서드는 API User가 알 수 있도록 그 사실을 기술하는 등의 예시가 있다.

  4. 모든 매개변수에 @param, void가 아니라면 @return, 발생할 가능성이 있는 모든 예외에 @throws 태그를 붙인다.

    예제 3 : DocComment 예시

    public class DocExamples<E> {
        // 메서드 주석 (333-334쪽)
        /**
         * Returns the element at the specified position in this list.
         *
         * <p>This method is <i>not</i> guaranteed to run in constant
         * time. In some implementations it may run in time proportional
         * to the element position.
         *
         * @param  index index of element to return; must be
         *         non-negative and less than the size of this list
         * @return the element at the specified position in this list
         * @throws IndexOutOfBoundsException if the index is out of range
         *         ({@code index < 0 || index >= this.size()})
         */
    }
    
  5. doc comment엔 HTML 태그를 사용할 수 있다.
  6. @code 태그는 감싼 주석을 JavaDoc 내 코드블럭을 생성하여 코드로 출력하며 다른 HTML 태그를 무시한다.

    여러 줄의 코드 주석을 작성하고 싶다면 <pre>태그로 감싸면 된다. 예를 들면 <pre>{@code (여러줄의 코드)}</pre>와 같이 작성하면 된다.

  7. 클래스를 상속용으로 설계할 때 자기사용 패턴(self-use pattern)에 대해서 알려주어야 한다.

    해당 메서드를 올바르게 재정의하는 방법을 문서에 기술하지 않으면, API User의 코드에 끔찍한 버그를 유발할 수 있다.(ITEM 15 참조)

    @implSpec 태그를 이용하며, 해당 메서드와 하위 클래스 사이의 계약(contract)을 설명하여 하위 클래스가 이 메서드를 상속하거나 super 키워드로 호출할 때 어떻게 동작하는지 명확히 해야한다.

    예제 4 : @implSpec으로 자기사용 패턴 기술하기

    /**
         * 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() {
    		return this.size() == 0;
    }
    

    this.size()는 상속 후 메서드 재정의시 문제가 될 소지가 충분히 있다.

    주의할 점은 JavaDoc 명령줄이 -tag “implSpec:a:Implemetntation Requeirements:” 스위치를 켜지 않으면 @implSpec을 무시해버린다.

  8. <, >, & 등의 HTML 메타문자를 포함시키려면 @literal을 사용하자.

    예제 5 : HTML 메타문자 표기하기

    /**
    	* A geometric series converges if {@literal |r| < 1}
    	*/
    
  9. 각 doc comment의 첫 문장은 해당 요소의 요약으로 간주된다.

    첫 문장을 공들여 써야하며, 대상의 기능을 고유하게 기술해야 한다. 또한 처음 발견되는 {<.><다음문장 시작(소문자가 아닌 문자로 시작하는 문장)>}을 기준으로 첫 문장을 구분하기 때문에 사용에 유의해야 한다. "김철수와 Mr. 코난의 asdf"이란 문장은, "김철수와 Mr."까지만 요약설명으로 간주되는 문제가 있다.

    @literal로 감싸면 마침표를 포함한 요약 설명을 마칠 수 있다.

    영어로 요약설명을 작성할 경우 2인칭 문장이 아닌 3인칭으로 작성하고, 동작을 설명하는 주어가 없는 동사구여야 한다.

    예제 6 : 올바른 요약설명 예시

    ArrayList(int initialCapacity) — Constructs an empty list with the specified initial capacity.
    Collection.size() — Returns the number of elements in this collection
    

    return the number가 아닌 returns the number로 3인칭 취급함을 주의하자(영어에 능숙한 사람에겐 당연한 얘기일 수 있다).

    클래스, 인터페이스의 요약 설명은 ‘인스터스’를 설명해야 하고, 필드는 필드 자신을 설명해야 한다.

    예제 7

    Instant—An instantaneous point on the time-line.
    Math.PI—The double value that is closer than any other to pi, the ratio of the
    circumference of a circle to its diameter.
    
  10. 색인으로 감쌀 용어는 @index 태그를 사용한다.(Java 9~)

    예제 8 : @index 태그 사용하기

    This method complies with the {@index IEEE 754} standard.
    
  11. 제네릭 타입, 제네릭 메서드를 문서화할 때는 모든 타입 매개변수에 주석을 달아야 한다.

    예제 9 : 제네릭 타입/메서드의 매개변수 주석

    /**
     * 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> { ... }
    
  12. 열거 타입은 상수들에도 주석을 달아야 한다.

    public 메서드도 마찬가지로 주석을 달아야 한다.

    예제 10 : Enum 타입의 doc comment 작성하기

    		/**
         * 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;
        }
    
  13. annotation 타입을 문서화 할 때에는, 멤버들과 타입 자기자신 모두에 주석을 달아야 한다.

    예제 11 : annotation doc comment 작성하기

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

    타입은 타입을 다는 것의 의미를 동사구로, 필드는 명사구로 설명한다.

  14. 패키지를 설명하는 문서화 주석은 package-info.java에 작성한다.

    모듈 설명을 기술하는 module-info.java도 마찬가지다.

  15. thread safe와 Serializable을 누락하지 말아야 한다.

    위 두 가지는 반드시 기술되어야 하며, thread safe 여부와 관계없이 무조건 기술하여야 한다. 직렬화 가능한 클래스는 직렬화 형태에 대해서 기술하여야 한다.

  16. @inheridDoc 태그는 상위 타입의 문서화 주석 일부를 상속하는데 사용한다.

    유지보수가 수월한 측면은 있지만, 제약이 조금 있고 까다롭다. 참고하기 : Oracle 공식 문서

  17. 전체 아키텍쳐의 설명이 필요하면 문서의 링크를 제공하자.

    각각의 요소에 대해서만 doc comment를 작성할 수 있을 뿐 전체적인 아키텍쳐나 상호작용하는 API의 복잡한 상황을 기술하기는 어렵다. 별도의 문서로 분리하고 링크를 제공하자.

  18. JavaDoc 생성시 -Xdoclint 플래그는 문법 오류를 검사한다.

    책 전반을 관통하는 컴파일 타임에 에러잡기(?)와 유사하다.

    checkstyle 같은 IDE 플러그인도 대안이 될 수 있으며, W3C-validator와 같은 HTML 유효성 검사 도구도 사용할 수 있다.

  19. -html5 플래그는 HTML 5로 문서를 생성한다.
  20. 작성을 마쳤다면 생성한 웹페이지를 읽어보자.

댓글남기기