Search
Duplicate

7. 메서드

목차

Previous

메서드 챕터에서는 메서드를 설계할 때 주의할 점에 대해 살펴본다.
매개변수와 반환값에 대한 이야기
메서드의 시그니처 설계 방법론
메서드의 문서화에 대한 이야기
핵심 키워드 : 사용성, 견고성, 유연성

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

요약

매개변수 혹은 생성자에서는 매개변수의 제약에 대해 고민해야 한다.
매개변수에 대한 제약조건은 메서드의 시작부분에서 명시적으로 검사해야 한다.
메서드의 제약에 대해서 문서화해야 한다.
잘못된 매개변수로 발생한 예외와 문서에 작성된 예외가 다를 수 있는데 이 경우 예외 번역(exception translate) 관용구를 사용해 API 문서에 기재된 예외로 번역해줘야 한다.
매개변수에 제약을 두는게 좋는 것이 아니라 범용적으로 설계 해야 한다는 것이다.

오류는 가능한 빨리 잡아야 한다.

우리는 메서드 혹은 생성자를 호출할 때 특정 값을 반환받기를 바라고, 메서드에서는 매개변수를 받을 때 해당 매개변수가 특정 조건을 만족하는 값이기를 바란다.
LottoNumber라는 객체 생성자의 값이 음수이면 안되고 45를 넘어서는 안된다는 조건이 있는 것처럼 말이다. 이런 예외를 빨리 잡지 못하고 늦춰질수록 오류 발생지점을 찾기 힘들어지고, 이는 디버깅의 어려움을 초래하게 된다. 최악의 경우 유효성 검증이 제대로 이뤄지지 않았지만 예외가 던져지지 않고, 잘못된 값을 반환하는 경우이다. 이 경우 실패 원자성을 어기는 결과가 생길수도 있다.
실패 원자성(fail atomicity)
객체가 메서드 호출에 실패하더라도 상태가 이전과 동일해야 한다는 것

public과 protected 메서드는 문서화 하라.

매개변수의 제약을 문서화하고 이 제약을 어겼을 때 발생할 수 있는 예외를 기술하면, 해당 API를 사용하는 개발자가 해당 제약을 지킬 확률을 높일 수 있다.
/** * (현재 값 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); } //... logic }
Java
복사
여기서 NullPointException(NPE)에 대한 내용은 기술되어 있지 않지만, BigInteger 클래스 수준에서 기술했기 때문에 이 클래스의 public 메서드 전체에 적용되기에 각 메서드에 기술 할 필요가 없다.

java.util.Objects.requireNonNull

this.strategy = Objects.requireNonNull(strategy, "전략");
Java
복사
Java 7에 추가된 이 requireNonNull 메서드는 null 검사를 자동으로 해준다.
내부적으로는 다음과 같이 되어있으면 null체크를 해주고 값을 그대로 반환하는 메서드이다.
public static <T> T requireNonNull(T obj) { if (obj == null) throw new NullPointerException(); return obj; } public static <T> T requireNonNull(T obj, String message) { if (obj == null) throw new NullPointerException(message); return obj; }
Java
복사
java.util.Objects.requireNoneNull 메서드

Objects 에서 제공하는 범위 검사 기능

checkFromIndexSize, checkFromToIndex, checkIndex
Java 9에서 추가된 기능으로 다음과 같은 기능은 제공하지 않지만, 그 외에는 유용하다
예외 메세지 지정이 불가능하다.
리스트와 배열 전용으로 설계되었다.
닫힌 범위(양 끝단 값을 포함)는 다루지 못한다.

비공개 메서드는 단언문을 통해 검증할 수 있다.

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; //... logics }
Java
복사
public이나 protected 와는 다르게 노출되지 않는 비공개 메서드는 개발자가 메서드의 호출을 통제할 수 있는데, 유효값에 대한 제약을 단언문(assert)를 사용해 유효성을 검증할 수 있다.
이러한 단언문을 통해 매개변수의 조건이 무조건 참이라고 선언하는 것이 중요한데, 일반적인 유효성 검사와 다음 몇 가지 내용이 다르다
1.
유효성 검사 실패시 AssertionError를 던진다.
2.
런타임에 아무런 효과도, 성능 저하도 없다.
a.
-ea, —-enableassertions 플래그 설정시 런타임에 영향을 줄 수 있다.

예외상황

항상 로직 최상단에 매개변수 유효성을 검사해야하는 것은 아니다.
유효성 검사 비용이 너무높거나 실용적이지 않거나, 계산 과정에서 암묵적인 검사가 이뤄질 때는 예외상황으로 둘 수 있다.
List를 정렬하는 Collections.sort(List) 메서드같은 경우 모든 원소가 상호 비교될 수 있어야 정렬을 할 수 있는데, 상호 비교할 수 없는 경우 ClassCastException을 던질 것이다. 그렇기에 이 경우 비교하기 전에 모든 객체의 상호 비교성 검사를 할 필요가 없게 된다.
public static <T extends Comparable<? super T>> void sort(List<T> list) { list.sort(null); }
Java
복사
public class Product implements Comparable<Product>{ private long price; @Override public int compareTo(Product o) { // ClassCastException 발생 return (int)(this.price - o.price); } }
Java
복사
물론, 이런 암묵적 검사에 의존하다가 실패 원자성을 해칠 수 있으니 주의해서 사용해야 한다.
(암묵적으로 내가 의도하지 않은 곳에서 객체의 상태를 변경시키는 경우 객체는 롤백되지 않는다.)

정리하자면...

일반적으로, 로직의 최상단에 메서드에서 사용하는 매개변수에 대한 유효성 검증 로직을 넣도록 하자.
이를 놓칠 경우 잘못된 값으로 인해 중간에 예외를 던질 수도 있고, 최악의 경우에는 잘못된 값을 반환하여 예기치 못한 문제를 발생시킬 수 있다.

참고: 유효성 검증의 모듈화 수준

메서드를 구현하면서 유효성 검증을 하다보면 비슷하게 겹치는 부분들이 생기기 마련이다.
null check를 한다던가 접두사 접미사에 특정 키워드가 있기를 바란다거나, 숫자의 값이 일정 범위이거나 한 경우 말이다. 그럼 이런 경우 이런 유효성 로직을을 하나로 합쳐 분리하는것이 정답일까?
public class TestProduct { private String name; private String ip; private String port; private int num1; private int num2; private int num3; public TestProduct(String name, String ip, String port, int num1, int num2, int num3) { validateString(name, ip, port); valiateInteger(num1, num2, num3); this.name = name; this.ip = ip; this.port = port; this.num1 = num1; this.num2 = num2; this.num3 = num3; } private void valiateInteger(int... nums) { for (int num : nums) { if (num < 0 || num > 99) { throw new IllegalArgumentException("범위를 벗어났습니다."+num); } } } private void validateString(String... strings) { for (String str : strings) { if (str == null || str.startsWith("a") || str.endsWith("z")) { throw new IllegalArgumentException("유효하지 않은 값입니다." + str); } } } }
Java
복사
얼핏 보면 상당히 편리해보이고, 가변타입 매개변수를 받음으로써 인수도 자유롭게 받을 수 있다.
하지만, num2의 범위가 0~99가 아니라 -10~ 50으로 변경된다면 어떨까? 하나라면 문제가 안될 것 같지만, 하나가 아니라면? 이런 부분들 때문에 개인적으로는 동일한 유효성 검증을 가질 수 있을지라도, 어느정도 분리를 하는게 더 맞지 않나 생각을 한다.

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

요약

클라이언트에 의해 객체의 상태가 변경되지 않도록 주의를 요해야 한다.
반환 값이 불변이 안되는 경우(final이 아니거나 수정가능한 객체의 주소값) 그대로 반환하지 말고 새롭게 객체를 생성해서 반환하거나 유틸리티 클래스의 도움을 받도록 하자.
매개변수가 3자에 의해 확장될 수 있는 타입일 경우 방어적 복사로 clone 메서드를 사용해서는 안된다.
방어적 복사에 드는 비용은 높기 때문에 불변 객체들로 객체를 구성하여 방어적 복사를 안하도록 하는게 좋다.

언제 문제가 생기는가?

불변에 대해 신경쓰지 않고, 외부에서 객체를 수정하는 일을 막지 않을 경우 어떤 문제가 생길까?

1. 수정 가능한 객체(mutable object)

: 책에서는 기간을 표현하는 Period 객체를 대표적으로 예시를 든다.
public final class Period { private final Date start; private final Date end; public Period(Date start, Date end) { if(start.compareTo(end) > ){ throw new IllegalArgumentException(start + "가" + end + "보다 늦다."); } this.start = start; this.end = end; } public Date start() { return start; } public Date end() { return end; } }
Java
복사
위 Period 객체를 보면 생성자를 통해 start 날짜가 end 날짜보다 늦지 않도록 유효성 검증을 해주고 있다. 하지만, 다음과 같은 코드로 값을 수정할 수 있어, 문제가 생길 수 있다.
Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); end.setYear(78); //p의 내부 수정이 가능하다.
Java
복사

2. 수정 가능한 컬렉션(mutable collection)

: 내가 원수들의 집합을 래핑하는 일급 컬렉션을 만들었다고 해보자.
public class LottoNumbers { private final List<LottoNumber> values = new ArrayList<>(); public LottoNumbers(List<LottoNumber> values){ this.values.addAll(values); } public List<LottoNumber> values() { return values; } } ... LottoNumbers ln = new LottoNumbers(Arrays.asList(n1, n2, n3, ... n)); ln.values().set(2, new LottoNumber(25)); ln.values().clear();
Java
복사
LottoNumbers 라는 일급 콜렉션을 보면 final 키워드를 사용하여 상태 값을 변경하지 못하게 했다고 생각했지만, 사실 이는 해당 리스트 자체에 대한 참조 값 변경이 안되는 것이고 내부적으로 참조하는 인덱스에 대한 불변을 보장하지는 않는다. 그렇기에 외부에서 내부 값을 꺼내어 인덱스를 위/변조할 수 있게 되는 문제가 생긴다.

방어적 복사(defensive copy)로 방어하기

위와 같은 문제들은 모두 레퍼런스를 그대로 반환하기에 해당 레퍼런스가 참조하는 객체가 불변이 아닐 경우 생기는 문제인데, 어떻게 막아줘야 할까?

1. 복사본을 생성하여 반환하기

public Period(Date start, Date end) { this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if(start.compareTo(end) > ){ throw new IllegalArgumentException(start + "가" + end + "보다 늦다."); } public Date start() { return start; } public Date end() { return end; } }
Java
복사
매개변수로 받은 객체의 값을 꺼내어 새로운 객체를 생성하여 복사본을 사용할 경우, 외부에서 전달했던 기존 값의 레퍼런스를 토대로 위/변조를 수행하더라도 Period 객체의 내부 필드는 변하지 않는다.
참고: 유효성 검사 로직이 아래로 내려간 이유
: 멀티스레딩 환경에서는 원본 객체의 유효성을 검사 후 복사본을 만드는 찰나의 순간에 다른 스레드에서 원본 객체를 수정할 위험이 있는데, 이를 검사시점/사용시점(time-of-check/time-of-use) 공격 혹은 TOCTOU 공격이라 한다.
참고: clone 메서드를 사용하지 않은 이유
Date 객체와 같이 final이 아닌 객체는 확장될 수 있기 때문에 실제 인수로 Date가 아닌 악의적으로 재정의된 clone 메서드를 가진 ExtendsDate 객체가 전달될 수 있다. 이 경우 복제에 대한 책임을 공격자에게 위임하게 되어 문제가 생길 수 있다. 즉, 3자에 의해 확장될 수 있는 클래스는 방어적 복사본을 만들 때 clone을 사용해서는 안된다.

2. 접근자 메서드에서 방어적 복사본을 반환하라.

생성자에서 방어적 복사본을 이용해 위 변조를 1차적으로 막아줬지만 getter와 같은 접근자 메서드를 통해 필드에 접근하여 수정하는건 어떻게 막아야 할까? 이 역시 방어적 복사본을 반환해 해결할 수 있다.
public Date start() { return new Date(start.getTime()); } public Date end() { return new Date(end.getTime()); } ... public List<LottoNumber> values() { return new ArrayList<>(values); }
Java
복사

참고: Java 8 이후로는 더 쉽게 해결이 가능하다.

Date 객체 자체가 Mutable Object 이기에 이런 문제가 생기는데 애초에 불변인 Instant를 사용하거나 LocalDateTime, ZonedDateTime을 사용하면, 이런 문제가 생기지 않는다.

참고: Collections

List와 같은 컬렉션 역시 Collections라는 유틸리티 클래스의 도움으로 좀 더 명확하게 불변성을 확보할 수 있다. Collections는 컬렉션의 불변을 위해 unmodifiableList(), unmodifiableCollection(), unmodifiableSet(), 등의 불변메서드를 제공하는 이런 메서드를 통해 값을 반환해도 외부에서 위/변조를 할 수 없게 된다.
하지만, 이런 컬렉션이 가지고 있는 원소가 불변이 아니라면, 역시 위 변조가 가능하다.

너무 많은 방어적 복사로 인한 성능 저하

객체를 생성할 때, 접근자를 통해 필드를 반환할때마다 매번 새롭게 객체를 생성하는 방어적 복사를 수행한다면, 성능저하가 필수적으로 뒤따를 수 밖에 없다. 그렇기에 되도록 불변 객체를 조합해 객체를 구성함으로써 방어적 복사를 할 일 자체를 막는 것이 좋다.

합의하에 방어적 복사를 생략할 수 있다.

방어적 복사의 목적은 외부에서 내부의 데이터를 함부로 변경할 수 없게 막음으로써 통제권을 유지하는데 있다. 하지만, 클라이언트에서 매개변수로 전달하는 인수들에 대한 통제권을 온전이 이전하고 수정하는 일이 없을 경우 방어적 복사를 생략할 수 있다. 다만, 이 경우에는 클라이언트가 통제권을 넘겨주는 가변 객체에 내용을 통제권을 넘겨받는 메서드와 생성자에서 문서화해야 한다.
이렇게 통제권을 넘겨받기로 한 메서드나 생성자를 가진 클래스는 클라이언트의 공격에 취약할 수 밖에 없기에 클라이언트와 서로 신뢰할 수 있고, 불변식이 깨지더라도 사이드 이펙트가 클라이언트로 국한되어야 한다. (Ex: Wrapper Class Pattern)

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

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

표준 명명 규칙(or 관례)를 따르도록 하자
패키지에서 일관되게 이름을 짓자.
이해하기 쉬운 네이밍을 하자.
이름이 너무 길어지는 것은 피하자.

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

메서드가 너무 많은 클래스는 사용,문서화,테스트, 유지보수에 어려움을 가진다.
인터페이스도 동일하다.
클래스나 인터페이스는 자신의 각 기능을 완벽히 수행하는 메서드로 제공해야 한다.
자주 쓰일 경우에만 별도의 약칭 메서드를 두도록 하자.
확신이 없으면 만들지 말자.
매개변수 목록은 짧게 유지하자.
4개 이하가 좋다.
필자의 경우 2개 이하로 유지하는 것을 권장하고 그 이상은 DTO를 권장한다.
같은 타입의 매개변수가 여러 개 연달아 나오는 경우를 주의하자.

매개변수의 목록을 줄이는 방법

1. 메서드를 분리한다.

: 하나의 메서드에 책임이 과중하게 집중될 수록 매개변수의 가짓수는 늘어날 수 밖에 없다.
책에서는 List 인터페이스를 예로 든다. List의 지정 범위에서 주어진 원소의 인덱스를 찾아야 하는 경우 매개변수는 범위의 시작, 범위의 끝, 찾을 원소까지 3개의 매개변수가 필요하다. 하지만, List에서 제공하는 메서드를 별개로 이용하면 매개변수를 줄일 수 있다.
List<Integer> list = List.of(1,2,3,4,5,....N); //before list.searchOfRange(4, 14, 7); // start, end, target //after List<Integer> subList = list.subList(4, 14); int searchedIdx = subList.indexOf(7);
Java
복사
참고: 직교
소프트웨어 설계 영역에서 직교성이 높다 라는 말은 공통점이 없는 기능들이 잘 분리 되어 있다는의미이다. List 컬렉션에서 제공하는 subList와 indexOf가 서로 관련이 없는 기능들로 이를 이용해 범위검색을 하는 것이 직교성이 높다고 할 수 있다.
이처럼 결합도는 낟고 응집도는 높은 메서드로 기능들을 쪼개다 보면 중복이 줄고 테스트하기도 쉬우며, 결국 메서드의 총 수는 줄어들게 된다.
그렇다 사실 절대라는건 없다. 무한정으로 쪼갠다고 완벽한건 아니다. 특정 패턴 조합이 자주 사용되는 경우에는 이를 묶어 최적화해서 성능을 높힐 수 있는데, 이런 경우 직교성이 낮아지더라도 편의 기능으로 제공하는게 나을수도 있다. 흔히 MSA는 직교성이 높고 모놀리식(monolithic) 아키텍처는 직교성이 낮다고 할 수 있다.
짜잔!~

2. 도우미 클래스를 만들기

매개변수 여러 개를 묶어주는 도우미 클래스를 정적 멤버 클래스로 만드는 방법이 있다.
이는 흔히 DTO로 볼 수 있는데, 특정 도메인에 소속된 필드 정보들을 묶어서 전달하기 위해 사용할 수 있다.

3. 빌더 패턴 응용하기

매개변수가 많고, 그 중 생략 가능한 매개변수들도 뒤섞여 있을 경우 유용한 방식이다.
매개변수를 하나로 추상화한 객체를 정의하고, 클라이언트에서는 이 객체의 setter 메서드를 호출해 값을 설정한 뒤 execute 메서드를 호출하여 매개변수의 유효성 검사 및 설정이 완료된 객체를 넘겨 계산을 수행하는 것이다.
public PathResponse searchShortestPath(Long source, Long target) { checkEqualsStation(source, target); GraphPath resultPath = PathFinder.condition() .edgeList(lineRepository.findAll()) .vertexList(stationRepository.findAll()) .startStation(stationService.findById(source)) .endStation(stationService.findById(target)) .search(); List<Station> resultStations = resultPath.getVertexList(); List<StationResponse> stationResponses = resultStations.stream() .map(StationResponse::of) .collect(Collectors.toList()); return new PathResponse(stationResponses, resultPath.getWeight()); }
Java
복사
최단 노선 검색에서 응용한 빌더 패턴

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

매개변수의 타입은 되도록 인터페이스를 사용하면 좋다.
Map만 하더라도 HashMap, TreeMap, ConcurrenthashMap, 등이 있는데, 매개변수 타입으로 특정 Map 구현체를 선언하면 해당 구현체로 제한되지만, Map 을 매개변수 타입으로 선언한다면, 위 어떤 Map의 구현체라도 인수로 전달할 수 있다.
구현체를 매개변수 타입으로 작성하는것은 OCP, DIP원칙에도 어긋나 객체지향 패러다임에도 벗어나게되고, 의존성의 흐름이 생겨버리게 된다. 이 경우에 구현체가 아닌 추상화를 의존하기 위해서라도 인터페이스를 사용하는 것이 좋다.

4. Boolean 보다 원소 2개짜리 열거 타입이 낫다.

메서드 이름상 boolean이 더 명확한 경우 예외
열거 타입을 이용하면 코드의 가독성도 더 높아진다.
//before public void changeSwitch(boolean on) { if(on){ this.switch = "on"; onLight(); return; } this.switch = "off"; offLight(); } //after public enum SwitchType { ON, OFF } public void changeSwitch(SwitchType type){ if(SwitchType.ON.equals(type)){ this.switch = type; onLight(); return; } this.switch = type; offLight(); }
Java
복사
온도계 클래스의 정적 팩토리 메서드를 열거타입을 인수로 전달해 인스턴스를 생성 할 수도 있다.
public enum TemperatureScale( FAHRENHEIT, CELSIUS } Thermometer.newInstance(true) Thermometer.newInstance(TemperatureScale.CELSIUS);
Java
복사
전자보다는 후자가 훨씬 가독성이 높고 무슨 온도 단위를 사용하는 인스턴스인지 알 수 있게 된다.
또한, 나중에 지원해야 할 타입이 추가 된다면, 열거타입(TemperatureScale)에 인스턴스를 추가하고, 정적 팩토리 메서드를 리팩토링 할 수도 있다.

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

요약

다중정의(overloading)된 메서드 중 무엇이 호출 될 지는 컴파일타임에 정해진다.
재정의(overriding)한 메서드는 동적으로 선택되고, 다중정의(overloading)한 메서드는 정적으로 선택된다.
안전하고 보수적으로 가려면, 매개변수 수가 같은 다중정의는 만들지 말자.
가변인수(varargs)를 사용하는 메서드는 다중정의를 하지 말자.
매개변수가 같아야 한다면, 메서드명을 다르게 짓는게 낫다.
매개변수가 하나 이상 근본적으로 다를 경우(radically different) 혼동되지 않을 수 있다.
다중정의 메서드에서 어떤 메서드가 호출 될 지 매개변수의 런타임 타입만으로 결정될 수 있다
매개변수로 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다.
참조 메서드와 호출 메서드가 모두 다중정의 되어있을 경우 다중정의 해소 알고리즘이 제대로 동작하지않기에 예외가 던져질 수 있다.

다중정의와 재정의의 메서드 선택 차이점

다음 코드를 보면 Classfier에 str이라는 메서드를 다중정의(overloading)했다.
그리고 메인 로직에서 A,B,C 객체를 순회하며 str 메서드를 호출하고 있는데, 가볍게 생각하면 A, B, C 가 차례대로 출력될 것이라 예상할 수 있다. 하지만, 실제로 출력 결과를 보면 생각과 다르게 A만 나오는 것을 확인할 수 있다.
public class Main { public static class A {} public static class B extends A {} public static class C extends A {} public static class Classifier { public static String str(A a){ return "A"; } //---(1) public static String str(B b){ return "B"; } //---(2) public static String str(C c){ return "C"; } //---(3) } public static void main(String[] args){ A[] list = {new A(), new B(), new C()}; Stream.of(list) .map(it -> Classifier.str(it)) .forEach(System.out::println); } }
Java
복사
print 메서드
어째서 B객체와 C 객체를 매개변수로 호출할 때 다중정의된 str 메서드들이 선택되지 않은 것일까?
이는 어느 메서드를 호출할 지 선택되는 시점이 커파일타임이기 때문이다.
즉, map() 오퍼레이터안의 it 매개변수는 항상 A 타입이다. 런타임시에는 원소별로 타입이 달라지지만, 이는 호출 할 메서드 선택에 영향을 주지 못하고 위 코드에서 (3) 번째 메서드만 호출하게 된다.
다음으로는, 재정의 메서드를 사용하는 코드를 보자.
public class Main { public static class A { public String str(){ return "A"; } } public static class B extends A { @Override public String str() { return "B"; } } public static class C extends A { @Override public String str() { return "C"; } } public static void main(String[] args){ A[] list = {new A(), new B(), new C()}; Stream.of(list) .map(A::str) .forEach(System.out::println); } }
Java
복사
이번에는 각각의 결과가 A,B,C로 의도한대로 출력이 되고 있다.
map() 오퍼레이터에 사용되는 매개변수 it의 컴파일타임시 타입은 A지만, 런타임시의 타입을 기준으로 메서드가 선택된다. 즉 가장 하위에서 재정의한 메서드가 실행된다.

다중정의와 재정의의 결정적인 차이점은 호출되는 메서드가 선택되는 시점이다.

다중정의 메서드의 선택시점은 정적으로 컴파일타임에 결정되고, 재정의 메서드의 선택시점은 런타임시점에 가장 하위 재정의 메서드로 선택된다.

다중정의가 혼동을 일으키는 상황을 피하자.

클라이언트에서 API를 사용할 때 어떤 다중정의 메서드가 호출될지 모른다면, 문제가 생길 가능성이 높다. 그렇기에 매개변수의 수가 같은 다중정의는 만들지 않는게 좋고, 더욱이 가변인수를 사용하는 메서드는 다중정의를 하지 않는게 좋다.

만약, 다중정의가 필요하다면?

메서드 이름을 다르게 지어서 메서드명을 통해 명시적으로 클라이언트에게 노출하는 방법이 있다.
ObjectOutputStream 클래스의 writeXXX 메서드를 예시로 들 수 있다.
public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants { public void writeBoolean(boolean val) throws IOException { bout.writeBoolean(val); } public void writeByte(int val) throws IOException { bout.writeByte(val); } public void writeShort(int val) throws IOException { bout.writeShort(val); } public void writeChar(int val) throws IOException { bout.writeChar(val); } //... logic }
Java
복사
ObjectOutputStream의 여러 writeXXX() 메서드
모두 같은 매개변수 갯수를 가진다. 하지만, 다중정의가 아닌 네이밍을 통해 메서드의 동작을 예상할 수 있게 해준다.

매개변수 하나 이상이 근본적으로 다르다면 가능하다.

매개변수의 갯수가 같더라도, 하나 이상의 매개변수 타입이 완전히 다를 경우 다중정의 메서드의 선택이 런타임시의 타입만 가지고 결정될 수 있다. 매개변수 타입이 완전히 다르다는 것은 다음을 의미한다.
두 타입간의 매개변수가 서로 어느 쪽으로든 형변환 할 수 없다. (exclude null)

Autoboxing의 등장으로 인한 주의사항

Java 5에 등장한 오토박싱으로 문제가 발생할 여지가 생겼다.
다음은 List 인터페이스의 remove API다.
public interface List<E> extends Collection<E> { //... logic boolean remove(Object o); E remove(int index); }
Java
복사
이렇게 remove 메서드가 다중정의 되어 있는데, List<Integer> indexList 라는 리스트에서 remove 메서드를 호출하면 어떨까?
List<Integer> indexList = List.of(1,2,3,4,5); indexList.remove(3);
Java
복사
내가 지우고자 하는 값은 3이다. 그래서 예상되는 반환 값은 [1,2,4,5] 이다. 하지만, 실제론 [1,2,3,5] 를 반환할 것이다. 다중 정의된 remove 메서드 중 E remove(int index) 메서드가 선택되었기 때문인데, 이 메서드는 매개변수를 인덱스로 사용하기 때문에 3이 아닌 4가 삭제된 것이고, 이를 내가 의도하는대로 사용하려면 다음과 같이 사용해야 한다.
indexList.remove((Integer) 3);
Java
복사
List 인터페이스도 완전히 안심하고 쓸 수는 없다는 생각을 하게 만드는 과정이다. 하지만, 문제는 이 뿐만이아니다.

Stream과 MethodReference로 인한 문제

new Thread(System.out::println).start(); ExecutorService exec = Executors.newCachedThreadPool(); exec.submit(System.out::println);
Java
복사
exec.submit() 메서드에서 발생한 컴파일 오류 메세지
둘 다 스레드를 할당해 콘솔 출력을 하는 로직이다. 하지만, submit() 메서드에서는 컴파일 오류가 발생한다. 이 원인은 println() 메서드와 submit() 메서드가 모두 다중정의가 되어있기 때문이다.
다중정의 해소 알고리즘에서 정상적으로 다중정의 메서드를 선택할 수 없기에 생기는 문제다.

참고: 부정확한 메서드 참조(inexact method reference)

System.out::println 은 부정확한 메서드 참조(inexact method reference)이다.
이런 부정확한 메서드 참조같은 인수표현식은 목표 타입이 선택되기 전에는 의미가 정해지지 않기에 적용성 테스트(applicability test) 때 무시되는데, 이게 문제의 원인이 된다.

그럼에도 불구하고 다중정의를 해야 한다면...

하지만, 같은 매개변수 갯수위 타입을 가지는 다중정의를 해야하는 경우가 생길 수 있다.
이런 경우 어떤 다중 정의가 선택되더라도 기능이 동일하다면 문제가 발생하지 않는다. 그렇기에 일반적으로 좀 더 특수한 다중정의 메서드에서 더 일반적인 혹은 범용적인 다중정의 메서드로 책임을 위임하는 것이다.
public boolean contentEquals(StringBuffer sb) { return contentEquals((CharSequence) sb); }
Java
복사
인수를 포워드(forward)해서 책임을 좀 더 일반적인 메서드로 위임한다.

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

가변인수(varargs)란?

매개변수를 동적으로 받을 수 있는 방법으로 Java 5부터 지원되기 시작한 기능
public void printArgs(String ... strs){ for(String str : strs){ System.out.println(str); } }
Java
복사
매개변수에 ... 을 사용하여 가변인수를 설정할 수 있고, 0개 이상의 인수를 받을 수 있다.

가변인수 메서드의 흐름

1.
인수의 개수와 길이가 같은 배열을 생성한다.
2.
생성된 배열에 인수들을 저장하여 메서드에 전달한다.
3.
메서드에서는 해당 배열을 사용한다.
이처럼 메서드를 호출 할 때마다 새로 배열을 생성해서 전달해주는데, 이 부분도 결국 비용이고, 낭비일 수 있다.

문제가 되는 경우

그렇다면, 인수의 개수가 1개 이상이어야 하는경우에는 메서드에서는 인수의 갯수를 검증해야 한다.
public void min(int... args){ if(args.length == 0){ throw new IllegalArgumentException(); } int min = args[0]; for(int arg : args){ if(arg < min){ min = arg; } } System.out.println(min); }
Java
복사
다음과 같이 최솟값을 구하는 메서드의 경우 인수를 0개도 받을 수 있으면 최솟값을 구할 수 없다.
그렇기에 상단에 가변인수 배열의 길이를 검증하는 로직이 들어가야하는데, 이 경우 컴파일 시점이 아닌 런타임 시점에 예외가 발생할 수 있다.

1개 이상의 인수를 받아야 하는 경우 해결책

평범한 매개변수를 받고, 가변인수를 두 번째로 받도록 하자.
public void min(int firstArg, int... args){ int min = firstArg; for(int arg : args){ if(arg < min){ min = arg; } } System.out.println(min); }
Java
복사
이렇게 하면 첫 번째 인수를 최솟값으로 지정하고 가변인수 배열을 비교해 최솟값을 구할 수 있다.

성능 문제와 해결책

가변인수는 인수 갯수가 정해지지 않고 가변적일 때 유용한 방식이다.
하지만, 매 번 배열을 새로 생성해서 할당해야하는 비용적인 문제가 있다. 그렇기에 가변인수를 무작정 쓰기보다는, 메서드의 호출 패턴을 분석하여 오버로딩을 활용할 수도 있다.
만약 가변인수를 받는 메서드에서 메서드 호출의 90%이상이 인수의 갯수를 3개 이하로 받을 경우, 다음과 같이 다중정의를 이용해 배열이 새로 생성되는 비용을 줄일 수 있다.
public void method(){ ... } public void method(int a1){ ... } public void method(int a1, int a2){ ... } public void method(int a1, int a2, int a3){ ... } public void method(int a1, int a2, int a3, int... args){ ... }
Java
복사
이렇게 오버로딩을 할 경우, 메서드 호출의 90%는 가변인수를 사용하지 않고 10%만이 가변인수를 사용해 배열을 새로 생성할 것이다.

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

public List<Cheese> getCheeses() { if(values.isEmpty()){ return null; } return new ArrayList<>(values); }
Java
복사
컬렉션이 비어있을 경우 null을 반환하는 getter 메서드이다. 그런데 이 메서드는 문제를 야기시킨다. 무엇일까?

클라이언트에게 검증의 책임을 부과한다.

public void logic(){ List<Cheese> cheeses = shop.getCheese(); if(cheeses != null && cheeses.contains(Cheese.STILTON)){ System.out.println("exists Cheese"); } }
Java
복사
getCheese() 메서드를 사용하는 위 코드를 보면 null check 방어 로직을 작성해야 한다.
이처럼 방어 코드를 작성하지 않을 경우 null 이 반환되었을 때 null에 메서드를 호출하는 NPE 예외가 발생할 수 있다. 게다가 일반적으로 99%의 상황에서 null이 반환되지 않아 이 예외를 바로 찾아내지 못하는 경우 나중에서야 발견될수도 있다.

비용 문제는?

리스트 혹은 배열이 비어있다고, 이를 그대로 반환해주면 외부에서 위/변조의 위험이 있기에 문제가 있다. 그래서 새로운 빈 배열이나 컬렉션을 반환하기를 권한다.
이 경우 비용 문제를 걱정할 수 있지만, 성능 분석 결과 이러한 할당이 성능을 떨어트리는 주범이라고 확인되지 않는 한 신경쓰지 않아도 된다. 대부분의 경우 다음처럼 작성하면 된다.
return new ArrayList<>(values);
Java
복사

참고: 비용이 문제가 된다면 빈 불변 컬렉션을 반환하자.

public List<Cheese> getCheeses() { if(values.isEmpty()){ return Collections.emptyList(); } return new ArrayList<>(values); }
Java
복사
Collections라는 컬렐션 유틸 클래스에서 제공하는 emptyList, emptySet, emptyMap 메서드를 이용하면 되는데, 정말 성능 최적화가 필요한 경우에만 사용하도록 하자.

참고: 빈 배열을 반환하기

컬렉션이 아닌 배열을 쓰는 경우에도 컬렉션과 동일하다. null이 아닌 비어있는 배열을 반환하자.
//default case public List<Cheese> getCheeses() { return values.toArray(new Cheese[0]); } //성능 최적화 private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0]; public List<Cheese> getCheeses() { return values.toArray(EMPTY_CHEESE_ARRAY); }
Java
복사

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

Previous

만약, Optional API에 대해 이해가 부족하다면 다음 링크를 통해 학습하고 오자.

값을 반환할 수 없을 때 두 가지 선택지

Java 8 이전 메서드에서 반환할 수 있는 값이 없는 경우 할 수 있는 선택지는 두 가지가 있었다.
하지만, 이 두가지 방법 모두 허점이 있기 때문에 허점을 해결하기 위한 비용도 적지 않은데 그 방법들은 다음과 같다.
1.
예외를 던진다.
허점: 진짜 예외적인 상황에서만 사용해야 하며, 예외 생성시점에서 stracktrace 전체를 캡처하기에 비용이 크다.
2.
반환 타입이 객체일 경우 null을 반환한다.
허점: 예외 생성으로 인한 비용은 들지 않지만, null을 반환할 수 있는 메서드를 호출하는 클라이언트는 null check 코드를 항상 추가해야 하며, 그렇게 하지 않을 경우 예기치 못한 상황에서 NPE(NullPointerException)가 발생할 수 있다.

Optional API의 등장

Java 8 에서 새로 나온 API 중 Optional API가 이러한 상황에서 또 하나의 선택지가 되었다.
해당 객체는 제네릭 타입 매개변수로 선언한 객체를 참조하거나 혹은 비어있는 상태로 존재할 수 있다.
즉, Optional은 하나의 값을 가지는 불변 컬렉션(Immutable Collection) 이다.
메서드에서 T라는 타입을 반환해야 하지만, 특정 조건에서 반환값이 없는 경우(null) T 대신 Optional<T>를 반환하도록 하는 방식으로, null이 반환되는게 아닌 Optional<T>이라는 객체가 반환되기에 클라이언트는 메서드의 로직 수행결과 값이 어떻든 null이 아닌 Optional<T> 객체가 반환되기에 NPE 문제에서 좀 더 자유로워진다. 또한, 예외를 던지는 비용도 줄일 수 있다.
//before 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; }
Java
복사
java 8 이전의 메서드
//after 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); }
Java
복사
Optional API 적용 메서드
 Optional을 반환하는 메서드는 절대 null을 반환하지 말자.

Optional API는 언제 사용해야 할까?

반환값이 없을 수도 있음을 클라이언트(API 사용자)에게 명확히 알려줘야 하는 경우
이럴 경우 클라이언트에서는 값을 받지 못할 경우에 대처해서 행동을 선택할수도 있다.
//case 1. 기본값 설정 String lastWordInLexicon = max(words).orElse("단어 없음..."); //case 2. 예외 던지기 Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
Java
복사
결과가 없으며 클라이언트가 이 상황을 특별하게 처리해야 하는 경우
DB에서 데이터를 조회했는데 없는 경우 반드시 특정 동작을 해야하거나, 특정 예외를 던지는등의 의미있는 비즈니스 로직이 동작되야 하는경우 Optional을 반환하여 orElse, orElseThrow, orElseGet등의 메서드 체이닝으로 동작을 할 수 있다.
하지만, 이 경우에도 Optional 선택은 한 번 더 고민해볼 문제다. 결국 새로운 객체를 생성하고 초기화 해야하는데, 내부에서 값을 꺼내는데 드는 비용까지 생각해보면 원하는 값을 얻기 위해 거쳐야하는 과정과 비용이 상당하다. 그래서 성능이 중요한 상황에서는 Optional 사용을 더욱 고민해봐야 한다.

주의사항

1.
반환값으로 Optional을 사용하는것이 항상 정답은 아니다.
Collection, Stream, Array, Optional같은 컨테이너 타입을 Optional로 감싸서는 안된다.
다시 말해, 비어있는 Optional<List<T>>가 아니라 비어있는 List<T>를 반환하는게 좋다는 의미이다. 빈 컨테이너를 그대로 반환하면 클라이언트는 Optional 처리코드를 굳이 작성하지 않아도 된다.
2.
박싱된 기본 타입을 담은 Optional을 반환하지 않도록 하자.
기본적으로 박싱된 타입을 담는 Optional API는 Primitive Type 값에 대해서도 박싱을 해서 담기 때문에 더 무거울 수 밖에 없다. 그래서 Stream API이나 표준 함수형 인터페이스에서 기본 타입들을 지원하는 전용 클래스(ex: IntStream, LongStream, IntFunction, IntConsumer, ...)를 제공하는 것처럼 OptionalInt, OptionalLong, OptionDouble 클래스를 제공한다. 그러니 박싱된 기본 타입을 담는 Optional을 사용하는일은 없도록 해야 한다.

객체에 필드로 Optional<T> 타입을 저장할 일이 있을까? 

일반적으로는 사용하지 않는게 좋지만, 때로는 적절한 상황도 있을 수 있다.
아래 코드는 아이템 2에 나왔던 영양정보 클래스인데, 상당수가 필수값이 아니다. 하지만, 모두 기본 타입이기에 옵셔널하다는 정보를 나타내기도 애매한데, 이런 경우 필드 자체를 Optional로 선언할 수도 있을 것이다.
public class NutritionalInformation { private final int calorie; //칼로리 - 필수 private final int sodium; //나트륨 private final int carbohydrate; //탄수화물 - 필수 private final int sugars; //당류 private final int fat; //지방 - 필수 private final int transFat; //트랜스지방 private final int saturatedFat; //포화지방 private final int cholesterol; //콜레스테롤 private final int protein; //단백질 - 필수 public NutritionalInformation() { this(0, 0, 0, 0, 0, 0, 0, 0, 0); } public NutritionalInformation(int calorie, int carbohydrate, int fat, int protein) { this(calorie, 0, carbohydrate, 0, fat, 0, 0, 0, protein); } public NutritionalInformation(int calorie, int sodium, int carbohydrate, int sugars, int fat, int transFat, int saturatedFat, int cholesterol, int protein) { this.calorie = calorie; this.sodium = sodium; this.carbohydrate = carbohydrate; this.sugars = sugars; this.fat = fat; this.transFat = transFat; this.saturatedFat = saturatedFat; this.cholesterol = cholesterol; this.protein = protein; } }
Java
복사

참고: Optional을 제대로 사용하는 26가지 방법

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

자바의 API 문서화 유틸리티 javadoc

소스코드 파일에서 문서화 주석(doc comment: 자바독 주석)이라는 특수한 형태로 기술된 설명을 추려 API로 변환해주는 유틸리티

API 문서화를 위해 문서화 주석을 달아야 하는 대상

우리가 구현하는 API를 javadoc을 통해 올바르게 문서화를 하기 위해서는 다음과 같은 대상은 모두 문서화 주석을 작성해야 한다. (이 때, 모두 공개된(public) 대상이라는 점을 알아두자)
이런 문서화 대상에 문서화 주석이 없을 경우 javadoc도 public API 요소의 선언만 나열해주기에 제대로된 문서가 될 수 없다.
클래스
인터페이스
(직렬화 가능 클래스인 경우) 직렬화 형태
메서드
필드
주의:  공개 클래스는 기본 생성자를 사용하면 안된다.
: 기본 생성자는 주석을 달 방법이 없기에 공개 클래스에서는 기본 생성자를 사용하면 안된다.
참고: 공개되지 않은 대상도 주석을 달아야 할까?
: 필수는 아니지만, 유지보수까지 고려한다면, 공개되지 않은 대상들에게도 디테일은 좀 줄이더라도 문서화 주석을 달아야 할 수 있다.

how가 아닌 what을 기술하자.

메서드에 문서화 주석을 작성할 때 메서드와 클라이언트 사이의 규약을 명료하게 작성해야 하는데, 내부 로직이 어떻게 돌아가는지에 대해 설명할게 아니라, 무엇을 하는지를 작성해야 한다.
클라이언트는 내부가 어떻게 돌아가는지가 궁금한게 아니라 무엇을 하고 어떤 결과를 반환할지가 궁금한 것이다. 또한, 내부 로직을 주석으로 모두 노출한다면 은닉성도 떨어지게되고, 로직의 변경시 주석도 같이 변경되야 하기 때문에 변화에 취약한 문서가 된다.
그럼 이렇게 문서화 주석을 작성할 때 어떤 것들을 작성해야 할까?
메서드가 성공적으로 수행된 후 만족해야 하는 사후조건(postcondition)
메서드 호출 전제조건
일반적으로 @throws 태그로 비검사 예외를 선언하여 암시적으로 기술한다.
(매개변수가 있을 경우) @param 태그를 이용해 전제조건에 영향받는 매개변수에 대해 기술
부작용
사후조건으로 명확히 나타나지는 않지만, 시스템의 상태에 변화를 가져오는 것.
(ex: 백그라운드 스레드를 시작시키는 메서드라면 이 내용을 문서에 작성해야 한다.)

문서화 주석 작성법

(매개변수가 있다면) @param 태그로 매개변수에 대해 명사구로 작성한다.
(반환 타입이 void가 아니라면) @return 태그로 반환 타입에 대해 명사구로 작성한다.
해당 태그의 설명과 메서드의 설명과 같은 경우 생략이 가능하다.
모든 예외는 @throw 태그로 예외와 예외 발생조건에 대해 기술한다.
상속용으로 설계된 클래스에서는 자기사용 패턴(self-use pattern)에 대해서도 문서에 남겨서 다른 프로그래머가 해당 메서드를 올바르게 재정의할 수 있도록 해야 한다.
@implSpec 주석으로 작성하며, 메서드와 하위 클래스 사이의 계약을 설명한다.
/** * Returns a sequential {@code Stream} with this collection as its source. * * <p>This method should be overridden when the {@link #spliterator()} * method cannot return a spliterator that is {@code IMMUTABLE}, * {@code CONCURRENT}, or <em>late-binding</em>. (See {@link #spliterator()} * for details.) * * @implSpec * The default implementation creates a sequential {@code Stream} from the * collection's {@code Spliterator}. * 이 기본 구현은 이 컬렉션으로부터 스트림을 생성한다. * * @return a sequential {@code Stream} over the elements in this collection * @since 1.8 */ default Stream<E> stream() { return StreamSupport.stream(spliterator(), false); }
Java
복사
List interface의 stream() 메서드의 문서화 주석
Java 11까지 javadoc 명령줄에 다음 옵션을 넣지 않으면 @implSpec 태그는 무시된다.
-tag “implSpec:a:Implementation Requirements:”
HTML 메타 문자를 사용하고 싶다면 {@literal } 태그로 래핑하면 된다.
문서화 주석의 첫 번째 문장은 해당 요소의 요약 설명(summary description)으로 간주되며, 기능을 고유하게 기술해야 한다.
한 클래스(혹은 인터페이스)안에 요약 설명이 같은 멤버나 생성자가 있어서는 안된다.
다중정의된 메서드도 설명은 같아서는 안된다.
메서드생성자의 요약 설명은 해당 메서드와 생성자의 동작을 설명하는 (주어가 없는) 동사구여야 한다.
클래스, 인터페이스, 필드의요약 설명은 대상을 설명하는 명사절이어야 한다.
마침표(.) 요약 설명의 끝으로 구분짓는 구분자이기에 조심해서 써야한다.
(구분자로 쓰기 싫다면 {@literal } 태그를 이용 해 보자.)
{<마침표> <공백> <다음 문장 시작>}
마침표: 요약설명이 끝나는 구분자 .
공백: 스페이스(space), 탭(tab), 줄바꿈(혹은 첫 번째 블록 태그)
다음 문장 시작: 소문자가 아닌 문자.
색인 기능은 {@index } 태그를 이용하여 적용할 수 있다.
다음은 위에서 말한 조건들을 모두 기술한 문서화 주석의 예로 List 인터페이스의 get메서드 부분으로 위에서 언급한 규칙들을 반영한 주석이다.
public interface List<E> extends Collection<E> { /** * Returns the element at the specified position in this list. * * @param index index of the element to return * @return the element at the specified position in this list * @throws IndexOutOfBoundsException if the index is out of range * ({@code index < 0 || index >= size()}) */ E get(int index); //method... }
Java
복사
List의 get 메서드 문서화 주석

그 밖에 문서화 주석을 작성해야 하는 경우

열거 타입

열거 타입과 내부의 public 메서드도 외부로 공개가 되는 자료 혹은 상태라면 모두 문서화 주석을 달아야 한다.
/** * 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; }
Java
복사

애너테이션

애너테이션 타입을 문서화 할 때는 멤버들도 모두 주석을 달아야 한다.
/** * Indicates that the annotated method is a test method that * must throw the designated exceptionto pass. * (kor) 이 애너테이션이 달린 메서드는 명시한 예외를 던져야만 성공하는 * 테스트 메서드임을 나타낸다. */ @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.) * (kor) 이 애너테이션을 단 테스트 메서드가 성공하려면 던져야 하는 예외. * (이 클래스의 하위 타입 예외는 모두 허용된다.) */ Class<? extends Throwable> value(); }
Java
복사

패키지(그리고 모듈)

패키지를 설명하는 문서화 주석은 package-info.java 파일에 작성한다.
(Java 9부터 지원하는 모듈 시스템은 module-info.java에 작성하면 된다.)
/** * Contains the collections framework, some internationalization support classes, * a service loader, properties, random number generation, string parsing * and scanning classes, base64 encoding and decoding, a bit array, and * several miscellaneous utility classes. This package also contains * legacy collection classes and legacy date and time classes. * * <h2><a id="CollectionsFramework"></a>{@index "Java Collections Framework"}</h2> * <p>For an overview, API outline, and design rationale, please see: * <ul> * <li><a href="doc-files/coll-index.html"> * <b>Collections Framework Documentation</b></a> * </ul> * * <p>For a tutorial and programming guide with examples of use * of the collections framework, please see: * <ul> * <li><a href="http://docs.oracle.com/javase/tutorial/collections/index.html"> * <b>Collections Framework Tutorial</b></a> * </ul> * * @since 1.0 */ package java.util;
Java
복사
java.util 패키지의 문서화 주석(package-info.java)
/** * Defines JUnit Jupiter API for writing tests. */ module org.junit.jupiter.api { requires static transitive org.apiguardian.api; requires transitive org.junit.platform.commons; requires transitive org.opentest4j; exports org.junit.jupiter.api; exports org.junit.jupiter.api.condition; exports org.junit.jupiter.api.extension; exports org.junit.jupiter.api.function; exports org.junit.jupiter.api.io; exports org.junit.jupiter.api.parallel; opens org.junit.jupiter.api.condition to org.junit.platform.commons; }
Java
복사
org.junit.jupiter.api의 모듈 주석(module-info.java)

자주 누락되는 설명들

API 문서화에서 자주 누락되는 두 가지 설명이 있는데 다음과 같이 스레드 안정성직렬화 가능성이 있는데, 이 주석들도 작성을 해줘야 하는 내용이다.

스레드 안정성

: 클래스, 정적 메서드에서 스레드가 안전한지, 그렇지 않은지에 대한 스레드 안전 수준을 반드시 API 설명에 반드시 포함해야 한다.

직렬화 가능성

직렬화가 가능한 클래스인 경우 직렬화 형태도 문서를 작성할 때 기술해야 한다.

메서드 주석의 상속기능

javadoc은 메서드 주석을 상속시킬 수 있는데, 주석이 없는 API 요소를 발견하면, 그 클래스가 구현한 인터페이스를 먼저 찾는다. 또는 {@inheritDoc} 태그를 이용해 상위 타입의 문서화 주석 일부를 상속할 수도 있다. 다만, 사용하기가 까다롭고 제약이 있어 사용이 조심스럽다.

이전 챕터로

다음 챕터로