메서드
아이템 49 : 매개변수가 유효한지 검사하라
- 생성자와 메서드의 입력 매개변수의 값은 보통 인덱스 값은 음수이면 안되며, 객체 참조는 null이 아니어야 한다.
- 이런 제약은 반드시 문서화해야 하며 메서드 몸체가 시작되기 전에 검사해야 한다.
매개변수 검사를 제대로 하지 못하면 발생하는 문제
- 매개변수 검사에 실패하면 실패 원자성을 어기는 결과를 낳을 수 있다.
- 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.
- 메서드가 잘 수행되지만 잘못된 결과를 반환할 수도 있다.
- 메서드는 문제 없이 수행되지만, 어떤 객체를 이상한 상태로 만들어놓아서 미래의 알 수 없는 시점에 해당 메서드와는 관련 없는 오류를 낼 수 도 있다.
매개변수의 제약을 어겼을 때 발생하는 예외도 함께 문서화 해둔다.
/**
* (현재 값 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());
- requireNonNull()을 이용하면 List에서 꺼낼 때가 아닌 받을 떄부터 null인지 알 수 있다.
- 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) 자신이 단언한 조건이 무조건 참임을 선언하고 클라이언트가 어떤 식으로 하든 상관 없지만 일반적인 유효성 검사와 다른 두가지가 있다.
- 실패하면 AssertionError를 던진다.
- 런타임에 아무런 효과도, 아무런 성능 저하도 없다.(단, java를 실행 할 때 vm 옵션에 -ea(—enableassertion)를 설정한다.
유효성 검사를 해야 하는 경우
- 생성자의 경우
- 생성자의 경우 "나중에 쓰려고 저장하는 매개변수의 유효성을 검사하라"는 원칙의 특수한 사례이다.
- 생성자 매개변수의 유효성 검사는 블래스 불변식을 어기는 객체가 만들어지지 않게 하는데 꼭 필요하다.
- 메서드의 경우
- 메서드는 몸체 실행 전에 매개변수 유효성을 검사해야 한다는 규칙이 있지만 예외의 경우도 있다.
- 유효성 검사 비용이 지나치게 높거나 실용적이지 않을 떄, 혹은 계산 과정에서 암묵적으로 검사가 수행될 떄다.
- 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;
}
- 매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자.
- 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
- 컴퓨터 보안 커뮤니티에서는 이를 검사시점/사용시점(time-of-check/time-of-use) 공격 혹은 영어 표기를 줄여서 TOCTOU 공격이라 한다.
- 방어적 복사에서 매개변수가 제 3자의 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.
- clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있기에 하위 클래스는 start와 end 필드의 참조를 private 정적 리스트에 담아뒀다가 공격자에게 이 리스트에 접근하는 길을 열어줄 수 있다.
- 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); }
- 가변 필드의 방어적 복사본을 반환하면 된다.
매개변수를 방어적으로 복사하는 목적
- 불변 객체를 만들기 위해서
- 외부 객체를 내부 객체로 건네 받을 경우, 내부 객체를 외부 객체로 건네 받을 경우
- 클라이언트가 제공한 객체의 참조가 잠재적으로 변경될 수 있는 객체라면 임의로 변경되어도 문제 없이 동작할지를 따져보고 확신하지 못하는 경우를 위해서
- set이나 map의 경우 겍체가 변경되면 불변식이 깨진다.
- 길이가 1 이상인 배열은 무조건 가변임을 잊지 말고 배열을 클라이언트에 반환 할 때는 항상 방어적 복사를 수행해야 한다.
- Period 예제의 경우, 자바 8 이상에서 개발해도 된다면 Instant(혹은 LocalDateTime이나 ZonedDateTime)를 사용하고 이전 버전의 자바에서 사용한다면 Date 참조 대신 Date.getTime()이 반환하는 long 정수를 사용하는 방법을 써도 된다.
방어적 복사를 하지 않아야 하는 경우
- 방어적 복사에는 성능 저하가 따르고, 또 항상 쓸 수 있는 것도 아니다.
- (같은 패키지에 속하는 등의 이유로) 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하면 방어적 복사를 생략할 수 있고 해당 매개변수나 반환 값을 수정하지 말아야 함을 명확히 문서화하는게 좋다.
- 다른 패키지에서 사용한다고 해서 넘겨받은 가변 매개변수를 항상 방어적 복사로 저장해야 하는건 아니다.
- 때로는메서드나 생성자의 매개변수로 넘기는 행위가 그 객체의 통제권을 명백히 이전함을 뜻하기도 한다.
- 이처럼 통제권을 이전하는 메서드를 호출하는 클라이언트는 해당 객체를 더 이상 직접 수정하는 일이 없다고 약속해야 하고 확실히 문서에 기재해야 한다.
- 통제권을 넘겨 받기로 한 메서드나 생성자는 악의적인 공격에 취약하므로 불변식이 까지더라도 영향이 오직 호출한 클라이언트로 국한되도록 한정해야 한다.
- 래퍼 클래스 패턴을 들 수 있는데 클라이언트는 래퍼에 넘긴 객체에 직접 접근할 수 있어 래퍼의 불변식을 쉽게 파괴할 수 있지만 영향은 오직 클라이언트 자신만 받게 된다.
⇒ 매개변수의 유효성을 검사(아이템49), 되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다.(아이템17)
클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야 한다. 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사를 수행하는 대신 해당 구성 요소를 수정했을 떄의 책임이 클라이언트에 있음을 문서에 명시하록 하자.
아이템 51 : 메서드 시그니처를 신중히 설계하라
- 메서드 이름을 신중하게 짓자
- 항상 표준 명명 규칙을 따라야 쉽게 이해할 수 있고, 같은 패키지에 속한 다른 이름들과 일관되게 짓는 게 최우선 목표다.
- 편의 메서드를 너무 많이 만들지 말자
- 모든 메서드는 각각 자신의 소임을 다해야 하는데 메서드가 너무 많은 클래스는 익히고, 사용하고, 문서화하고, 테스트하고, 유지보수하기 어렵다.
- 자주 쓰일 경우에만 별도의 약칭 메서드를 두고 확신이 서지 않으면 만들지 말자.
- 매개 변수 목록은 짧게 유지하라.
- 4개 이하가 좋으며 4개가 넘어가면 매개변수를 전부 기억하기가 쉽지 않다.
- 같은 타입의 매개변수 여려 개가 연달아 나오는 경우가 특히 해롭다.
과하게 긴 매개변수 목록을 짧게 줄여주는 기술 세 가지
- 여러 메서드로 쪼개어 각각은 원래 매개변수 목록의 부분집합을 받는다.
- 잘못하면 메서드가 너무 많아질 수 있지만, 직교성을 높여 오히려 메서드 수를 줄여주는 효과도 있다.
- java.util.List 인터페이스가 좋은 예로써 리스트에서 주어진 우너소의 인덱스를 찾아야 하는데, 전체 리스트가 아니라 지정된 범위의 부분리스트에서의 인덱스를 찾는다고 했을 때 subList, indexOf 메서드를 별개로 제공한다.
- 소프트웨어에서는 직교란 "공통점이 없는 기능이 잘 분리되어 있다." 혹은 "기능을 원자적으로 쪼개 제공한다."로 해석 할 수 있다. 직교성 하나로 해석하기에는 무리가 있지만 대체적으로 마이크로서비스 아키텍처는 직교성이 높고 모놀리식 아키텍처는 직교성이 낮다고 할 수 있다.
- 매개변수 여러 개를 묶어주는 도우미 클래스를 만드는 것이다.
- 도우미 클래스는 정적 멤버 클래스로 두며 잇따른 매개변수 몇 개를 독립된 하나의 개념으로 볼 수 있을 때 추천하는 기법이다.
- 예를 들어 카드 게임을 클래스로 구현하는 상황에서 메서드를 호출 할 떄 카드의 숫자, 무늬를 뜻하는 두 매개변수를 항상 같은 순서로 전달하게 된다. 이 둘을 묶는 도움 클래스를 만들어 하나의 매개변수로 주고받으면 API는 물론 클래스 내부 구현도 깔끔해질 것이다.
- 앞의 두 기법을 혼합한 것으로, 객체 생성에 사용한 빌더 패턴을 메서드 호출에 응용한다고 보면 된다.
- 모든 매개변수를 하나로 추상화한 객체를 정의하고, 클라이언트에서 이 객체의 세터 메서드를 호출해 필요한 값을 설정하게 하는것이다.
- 클라이언트는 먼저 필요한 매개변수를 다 설정한 다음, execute 메서드를 호출해 앞서 설정한 매개변수들의 유효성을 검사하고 설정이 완료된 객체를 넘겨 원하는 계산을 수행한다.
매개변수의 타입으로는 클래스보다는 인터페이스가 더 낫다.
- 매개변수로 적합한 인터페이스가 있다면(이를 구현한 클래스가 아닌) 그 인터페이스를 직접 사용하자.
- 예를 들어 메서드에 HashMap을 넘길 일은 없으니 Map을 대신 사용하자.
boolean보다는 원소 2개짜리 열거 타입이 낫다.
- 메서드 이름상 boolean을 받아야 의미가 더 명확할 때는 예외다.
enum TemperatureScale {
FAHRENHEIT, CELSIUS
}
⇒ 표준 명명 규칙(아이템68), 정적 멤버 클래스(아이템24), 빌더 패턴(아이템2), 매개변수의 타입으로는 클래스보다 인터페이스가 더 낫다(아이템64), 열거 타입 상수의 메서드 안으로 리팩터링해 넣을 수도 있다.(아이템34)
- 메서드 이름을 신중히 짓자.
- 편의 메서드를 너무 많이 만들지 말자.
- 확신이 서지 않으면 별도의 약칭 메서드를 만들지 말자.
- 매개변수 목록을 짧게 유지히자.
- 같은 타입의 매개변수 여러개가 연달아 나오는 경우가 특히 해롭다.
- 매개변수의 타입으로는 클래스보다는 인터페이스가 더 낫다.
- 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을 반환하는 쪽이 낫다는 주장이 있지만 두 가지 면에서 틀린 주장이다.
- 성능 분석 결과 성능 저하의 주범이라고 확인되지 않는 한 이 정도의 성능 차이는 신경 쓸 수준이 못된다.
- 빈 컬렉션과 배열은 굳이 새로 할당하지 않고도 반환할 수 있다.
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이 있다.
옵셔널을 맵의 값으로 사용하면 절대 안된다.
- 대신 맵에 키가 없다는 사실을 알려주는 두 가지 방법이 두 가지가 되어버려 혼란과 오류 가능성만 키운다.
- 키 자체가 없는 경우
- 키는 있지만 키가 속이 빈 옵셔널이 경우
옵셔널을 컬레션의 키, 값, 원소나 배열의 원소로 사용하는게 적절한 상황은 거의 없다.
- 인스턴스 필드에 저장해두면 해당 클래스를 확장해 하위 클래스를 따로 만들어야 함을 암시하는 '나쁜 냄새'가 난다.
⇒ 진짜 예외적인 상황(아이템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 요소들의 '선언'만 나열해주는 게 전부다.
- 기본 생성자에는 문서화 주석을 달 방법이 없으니 공개 클래스는 절대 기본 생성자를 사용하면 안된다.
메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 명료하게 기술해야 한다.
- how가 아닌 what을 기술해야 한다. 상속용으로 설계 된 메서드가 아니라면 (그 메서드가 어떻게 동작하는지가 아니라) 무엇을 하는지를 기술해야 한다.
- 클라이언트가 해당 메서드를 호출하기 위한 전제 조건을 모두 나열해야 한다.
- 메서드가 성공적으로 수행된 후에 만족해야 하는 사후조건도 모두 나열해야 한다.
- 일반적으로 전제조건은 @throws 태그로 비검사 예외를 선언하여 암시적으로 기술한다.
- 비검사 예외 하나가 전제조건 하나와 연결된다.
- @param 태그를 이용해 해당 조건에 영향 받는 매개변수에 기술할 수도 있다.
- 전체 조건과 사후 조건뿐만 아니라 부작용도 문서화해야 한다.
- 부작용은 사후 조건으로 명확히 나타나지는 않지만 시스템의 상태에 어떠한 변화를 가져오는 것을 의미한한다.
- 완벽히 기술하려면 모든 매개변수에 @param 태그를, void가 아니라면 @return 태그를, 발생할 가능성이 있는(검사든 비검사든) 모든 예외에 @throws 태그를 달아야 한다.
- 관례상 @param 태그와 @return 태그의 설명은 해당 매개변수가 뜻하는 값이나 반환값을 설명하는 명사구를 쓰고 드물게 산술 표현식을 쓰기도 한다.
- @param, @return, @throws 태그의 설명에는 마침표를 붙이지 않지만 한글로 작성한 경우 온전한 종결 어미에는 마침표를 써주는 게 일관돼 보인다.
- {@code} 태그는 감싼 내용을 코드용 폰트로 렌터링하고 HTML 요소나 다른 자바독 태그를 무시한다.
- 주석에 여러줄로 된 코드를 넣으려면
형태로{@code ... 코드 ... }
태그로 감싸면 된다.
- 주석에 여러줄로 된 코드를 넣으려면
- @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 태그를 무시해버린다.
- API 설명에서 <, > & 등의 HTML 메타 문자를 포함시키려면 {@literal} 태그로 감싸는게 가장 좋은 방법으로 HTML 마크업이나 자바독 태그를 무시하게 해주고 코드 폰트로 렌더링하지 않는다.
- 각 문서화 주석의 첫 번째 문장은 해당 요소의 요약 설명으로 간주하고 마침표까지를 요약 설명으로 판단하고 의도치 않은 마침표를 포함한 텍스트는 @{literal}로 감싼다.
/** * 테스트 {@literal 일거라고.} 그렇다. */ void test3() { } // 자바 10부터는 {@summary}라는 요약 설명 전용 태그가 추가 되었다. /** * {@summary 요약 설명을 만듬} */
- 한 클래스(혹은 인터페이스) 안에서 요약 설명이 똑같은 멤버(혹은 생성자)가 둘 이상이면 안된다.
- 다중정의 된 메서드들의 설명은 같은 문장으로 시작하는 게 자연스럽겠지만 문서화 주석에서는 허용되지 않는다.
- 메서드와 생성자의 요약 설명은 해당 메서드와 생성자의 동작을 설명하는 (주어가 없는) 동사구여야 한다.
ArrayList(int initialCapacity) : 지정한 초기 용량을 갖는 빈 리스트를 생성한다.
- 클래스, 인터페이스, 필드의 요약 설명은 대상을 설명하는 명사절이야 하므로 클래스와 인터페이스의 대상은 인스턴스이고, 필드의 대상은 자신이다.
Instant : 타임라인상의 특정 순간(지점) Math.PI : 원주율에 가까운 double 값
- 자바 9부터는 자바독이 생성한 HTML 문서에 검색 기능이 추가 되었고 {@index} 태그를 사용해 중요한 용어를 추가로 색인화할 수 있다.
/* * this method complies with the {@index IEEE 754} standard. */
- 제네릭 타입이나 제네릭 메서드를 문서화 할 때는 모든 매개변수에 주석을 달아야 한다.
/** * 키 하나가 가리킬 수 있는 값은 최대 1개이다. * * @param <K> 이 맵이 관리하는 키의 타입 * @param <V> 맵핑된 값의 타입 */ interface Map<K, V> { }
- 열거 타입을 문서화할 때는 상수들도 주석을 달아야 한다.
/** * Station info */ enum Station { /** subway station */ subway }
- 애너테이션 타입을 문서화할 때는 멤버들에도 모두 주석을 달아야 한다.
/** * 테스트하기 위한 애너테이션으로 테스트를 위해 존재한다. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Test { /** * 테스트 하기 위한 함수로 테스트한다. */ void test(); }
- 필드 설명은 명사구로 하고 요약 설명은 애너테이션 선언이 어떤 의미인지 동사구로 한다.
- 패키지를 설명하는 문서화 주석은 package-info.java 파일에 작성하며 패키지 선언을 반드시 포함해야 하며 패키지 선언 관련 애너테치션을 추가로 포함할 수 있다.
- 모듈 시세틈은 module-info.java 파일에 작성한다.
- 클래스 혹은 정적 메서드가 스레드 안전하든 그렇지 않든, 스레드 안전 수준을 반드시 API 설명에 포함해야 한다.
- 자바독은 메서드 주석을 '상속' 시킬수 있다. 문서화 주석이 없는 API 요소를 발견하면 자바독이 가장 가까운 문서화 주석을 찾아준다.
- 상위 '클래스'보다 그 클래스가 구현한 '인터페이스'를 먼저 찾는다.
- {@ingeriDoc} 태그를 사용해 상위 타입의 문서화 주석 일부를 상속할 수 있다.
자바독은 프로그래머가 자바독 문서를 올바르게 작성했는지 확인하는 기능을 제공한다.
- 자바 7에서는 명령줄에서 -Xdoclint 스위치를 키면 기능이 활성화 되고 자바 8부터는 기본으로 작동한다.
- checkstyle을 이용하면 더 완벽하게 검사된다.
- 자바 9와 10에서는 명령줄에서 -html5 스위치를 켜면 HTML5버전으로 만들어준다.
정말 잘 쓰인 문서인지를 확인하는 유일한 방법은 자바독 유틸리티가 생성한 웹페이지를 읽어보는 길뿐이다.
⇒ 직렬화 형태(아이템87), 상속(아이템19), 모든 예외(아이템74), 모듈 시스템(15), 스레드 안전 수준(아이템82), 직렬화 형태(아이템87)
문서화 주석은 여러분 API를 문서화하는 가장 휼룡하고 효과적인 방법이다.
'나(다) > 책' 카테고리의 다른 글
이펙티브 자바 - 10장 : 예외 (5) | 2021.01.26 |
---|---|
이펙티브 자바 - 9장 : 일반적인 프로그래밍 원칙 (3) | 2021.01.24 |
읽기 좋은 코드가 좋은 코드다 (5) | 2021.01.15 |
내 삶의 주인으로 산다는 것 (4) | 2021.01.14 |
이펙티브 자바 - 7장 : 람다와 스트림 (2) | 2021.01.10 |