item 49. 매개변수가 유효한지 검사하라
메서드 몸체가 실행되기 전에 매개변수를 확인한다면 잘못된 값이 넘어왔을때 즉각적이고 깔끔한 방식으로 예외를 던질 수 있다.
매개변수 검사를 제대로 하지 못하면 생길 수 있는 문제
- 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.
- 메서드가 잘 수행되지만 잘못된 결과를 반환할 수 있다.
- 메서드는 문제없이 수행됐지만, 어떤 객체를 이상한 상태로 만들어놓아서 미래의 알 수 없는 시점에 이 메서드와는 관련 없는 오류를 낼 수도 있다.
public과 protected 메서드는 매개변수 값이 잘못됐을 때 던지는 예외를 문서화해야 한다.
클래스 수준 주석은 그 클래스의 모든 public 메서드에 적용되므로 훨씬 깔끔하다.
@Nullable과 같은 어노테이션을 사용할 수도 있지만 표준은 아니다.
더불어 생성자 매개변수 검사도 클래스 불변식을 어기는 객체가 생성되지 않게 하기 위하여 꼭 필요하다.
assert
private으로 공개되지 않은 메서드라면 개발자가 직접 호출되는 상황을 통제할 수 있다.
이럴 때는 assert를 사용하여 매개변수 유효성을 검사할 수 있다.
실행시에 assert를 수행하려면 인텔리제이 기준으로 VM Options에 -ea 또는 --enableassertions 를 넘겨주어야 한다.
값을 넘겨주지 않으면 무시된다.
넘어온 매개변수가 조건식을 참으로 만들지 않으면 AssertionError를 던진다.
// 재귀 정렬용 private 도우미 함수 private void sort(long a[], int offset, int length) { assert a != null; assert offset >= 0 && offset <= a.length; assert length >= 0 && length <= a.length - offset; ... // 계산 수행 }
유효성 검사가 필요 없는 경우
- 유효성을 검사하는 비용이 지나치게 큰 경우 또는 계산 과정에서 암묵적으로 유효성 검사가 진행될 때
item 50. 적시에 방어적 복사본을 만들라
클라이언트가 우리의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.
주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다.
Date는 낡은 API이니 새로운 코드를 작성할 때는 더 이상 사용하면 안 된다.
- Date가 가변이라는 사실을 이용해서 불변식을 깬다.
생성자
생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다.
매개변수의 유효성을 검사(item 49)하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사하자.
- 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.
- clone이 다른 곳에서 정의한 것일 수도 있다.
접근자
접근자 메서드는 가변 필드의 방어적 복사본을 반환하자.
public Date start() { return new Date(start.getTime()); }
생성자와 달리 접근자 메서드에서는 방어적 복사에 clone을 사용해도 된다.
- Period가 가지고 있는 Date 객체는 java.util.Date임이 확실하기 때문이다.
- 그렇더라도 item 13에서 설명한 이유 때문에 인스턴스를 복사하는 데는 일반적으로 생성자나 정적 팩터리를 쓰는게 좋다.
매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위해서만은 아니다.
클라이언트로부터 받을 때
메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다.
- 변경될 수 있는 객체라면 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 그 클래스가 문제없이 동작할지를 따져보라.
- 확신할 수 없다면 복사본을 만들어 저장해야 한다.
- 클라이언트가 건네준 객체를 내부의 Set 인스턴스에 저장하거나 Map 인스턴스의 키로 사용한다면, 추후 그 객체가 변경될 경우 객체를 담고 있는 Set 혹은 Map의 불변식이 깨질 것이다.
클래스가 불변이든 가변이든, 가변인 내부 객체를 클라이언트에 반환할 때는 반드시 심사숙고해야 한다.
클라이언트로 반환할 때
내부에서 사용하는 배열을 클라이언트에 반환할 때는 항상 방어적 복사를 수행해야 한다. 혹은 배열의 불변 뷰를 반환하라.(item 15)
되도록 불변 객체들을 조합해 객체를 구성해야 방어적 복사를 할 일이 줄어든다.
방어적 복사에는 성능 저하가 따르고, 항상 쓸 수 있는 것도 아니다(같은 패키지에 속하는 등의 이유로).
다른 패키지에서 사용한다고 해서 넘겨받은 가변 매개변수를 항상 방어적으로 복사해 저장해야 하는 것은 아니다.
- 때로는 메서드나 생성자의 매개변수로 넘기는 행위가 그 객체의 통제권을 명백히 이전함을 뜻하기도 한다.
- 이처럼 통제권을 이전하는 메서드를 호출하는 클라이언트는 해당 객체를 더 이상 직접 수정하는 일이 없다고 약속해야 한다.
- 관련 메서드나 생성자에 그 사실을 확실히 문서에 기재해야 한다.
방어적 복사를 생략해도 되는 상황
- 해당 클래스와 그 클라이언트가 상호 신뢰할 수 있을 때
- 불변식이 깨지더라도 그 영향이 오직 호출한 클라이언트로 국한될 때
- 래퍼 클래스 패턴(itme 18)의 특성상 클라이언트는 래퍼에 넘긴 객체에 여전히 직접 접근할 수 있다.
- 따라서 래퍼의 불변식을 쉽게 파괴할 수 있지만 그 영향을 오직 클라이언트 자신만 받게 된다.
item 51. 메서드 시그니처를 신중하게 설계하라
메서드 이름을 신중하게 짓자
- 표쥰 명명 규칙에 따라 지으며 긴 이름은 지양해야 한다.
- 애매하다면 자바 라이브러리 가이드를 참조해도 좋다.
- 같은 패키지에 속한 다른 이름들과 일관되게 짓는 것이 좋다.
편의 메서드를 너무 많이 만들지 말자.
- 너무 많은 메서드는 그에 따른 문서화, 유지보수, 테스트를 요구한다.
매개변수 목록은 짧게 유지하자.
- 메서드의 매개변수 목록도 4개 이하가 적절하다.
- 특히 같은 타입의 매개변수 여러 개가 연달아 나오는 것은 좋지 않다.
- 매개변수를 줄일 수 있는 방법들을 살펴보자.
매개변수 타입으로는 클래스 보다 인터페이스가 낫다.
- hashmap 보다는 map이 arraylist 보다는 list가 낫다.
- boolean보다는 원소 2개 짜리 enum 이 낫다.
- 이름을 가질수 있어서 코드를 보는데 조금더 도움이 된다.
- 물론 이름상 boolean 형태가 명확한 경우는 예외다.
item 52. 다중정의는 신중히 사용하라
다중정의(overloading)
- 어느 메서드를 호출할지가 컴파일타임에 정해진다.
- 다중정의한 메서드는 정적으로 선택된다.
재정의(override)
- 재정의한 메서드는 동적으로 선택된다.
API 사용자가 매개변수를 넘기면서 어떤 다중정의 메서드가 호출될지를 모른다면 프로그램이 오동작하기 쉽다.
다중정의가 혼동을 일으키는 상황을 피해야 한다.
안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자.
이름을 다르게
- 다중정의하는 대신 메서드 이름을 다르게 지어주는 방법이 있다.
- 한편, 생성자의 경우는 이름을 다르게 지을 수 없다. 그렇기 때문에 두 번째 생성자부터는 무조건 다중정의를 하게 되는 셈이다. 생성자는 정적 팩터리를 사용하는 대안을 활용할 수 있다.
함수형 인터페이스는?
- 다중정의된 메서드들이 함수형 인터페이스를 인수로 받을 때 비록 서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 생긴다.
- 따라서 메서드를 다중정의할 때 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다.
프로그래밍 언어가 다중정의를 허용한다고 해서 다중정의를 꼭 활용하란 뜻은 아니다.
item 53. 가변인수는 신중히 사용하라
가변인수 메서드를 호출하면 인수의 개수와 길이가 같은 배열을 만들고 인수들을 만들어진 배열에 저장한 후에 가변인수 메서드에 전달해준다.
인수가 1개 이상이어야 할 때는 아래와 같이 가변인수 앞에 필수 매개변수를 받도록 하자.
static int min(int firstArg, int... remainingArgs) {
int min = firstArg;
for (int arg : remainingArgs) {
if (arg < min) {
min = arg;
}
}
return min;
}
가변인수는 성능에 해가 될 수 있다.
가변인수 메서드가 호출될 때마다 배열을 새로 할당하고 초기화하기 때문이다.
따라서 아래와 같은 패턴으로 변경할 수도 있다.
```java
public void foo() {}
public void foo(int arg1) {}
public void foo(int arg1, arg2) {}
public void foo(int arg1, arg2, arg3) {}
public void foo(int arg1, arg2, arg3, int... restArg) {}
```
item 54. null이 아닌, 빈 컬렉션이나 배열을 반환하라
컬렉션이나 배열 같은 컨테이너가 비었을 때 null을 반환하는 메서드를 사용할 때면 항시 방어 코드를 넣어줘야 한다.
클라이언트에서 방어 코드를 빼먹으면 오류가 발생할 수 있다.
null을 반환하려면 반환하는 쪽에서도 이 상황을 특별히 취급해줘야 해서 코드가 더 복잡해진다.
null이 아닌 빈 배열이나 컬렉션을 반환하라.
- null을 반환하는 API는 사용하기 어렵고 오류 처리 코드도 늘어난다.
- 그렇다고 성능이 좋은 것도 아니다.
빈 컨테이너를 할당하는 데 비용이 든다?
- 이 할당이 성능 저하의 주범이라고 확인되지 않는 한(item 67), 신경 안 써도 된다.
- 빈 컬렉션과 배열은 굳이 새로 할당하지 않고도 반환할 수 있다.
null 반환 - 따라 하지 말 것!
// 컬렉션이 비었으면 null을 반환한다. -따라 하지 말 것!
private final List<Cheese> cheesesInStock = ...;
public List<Cheese> getCheeses() {
return cheesesInStock.isEmpty() ? null
: new ArrayList<>(cheesesInStock);
}
빈 컬렉션 반환
// 빈 컬렉션을 반환하는 올바른 예
public List<Cheese> getCheeses() {
return new ArrayList<>(cheesesInStock);
}
// 최적화 - 빈 컬렉션을 매번 새로 할당하지 않도록 했다.
// 매번 똑같은 빈 '불변' 컬렉션을 반환한다.
public List<Cheese> getCheeses() {
return cheesesInStock.isEmpty() ? Collections.emptyList()
: new ArrayList<>(cheesesInStock);
}
길이가 0일 수도 있는 배열 반환
// 길이가 0일 수도 있는 배열을 반환하는 올바른 방법
public Cheese[] getCheeses() {
return cheesesInStock.toArray(new Cheese[0]);
}
// 최적화 - 빈 배열을 매번 새로 할당하지 않도록 했다.
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() {
return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}
- cheesesInStock이 비었을 때면 언제나 EMPTY_CHEESE_ARRAY를 반환한다.
단순히 성능을 개선할 목적이라면 toArray에 넘기는 배열을 미리 할당하는 건 추천하지 않는다. 오히려 성능이 떨어진다는 연구 결과도 있다.
// 나쁜 예 = 배열을 미리 할당하면 성능이 나빠진다.
return cheesesInStock.toArray(new Cheese[cheesesInStock.size()]);
item 55. 옵셔널 반환은 신중히 하라
메서드가 특정 조건에서 값을 반환할 수 없을 때
자바 8 이전
- 예외를 던짐
- 예외는 진짜 예외적인 상황에서만 사용해야 한다.(item 69)
- 예외를 생성할 때 스택 추적 전체를 캡처하므로 비용이 만만치 않다.
- null을 반환
- 별도의 null 처리 코드를 추가해야 한다.
- 언젠가 NullPointerException이 발생할 수 있다.
자바 8 이후
- Optional<T> 사용
Optional
- null이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있다.
- 원소를 최대 1개 가질 수 있는 '불변'컬렉션이다.
Optional 메서드
Optional.empty()
- 내부 값이 비어있는 Optional 객체 반환
Optional.of(T value)
- 내부 값이 value인 Optional 객체 반환
- 만약 value가 null인 경우 NullPointerException 발생
Optional.ofNullable(T value)
- 가장 자주 쓰이는 Optional 생성 방법
- value가 null이면, empty Optional을 반환하고, 값이 있으면 Optional.of로 생성
T get()
- Optional 내의 값을 반환
- 만약 Optional 내부 값이 null인 경우 NoSuchElementException 발생
boolean isPresent()
- Optional 내부 값이 null이면 false, 있으면 true
- Optional 내부에서만 사용해야하는 메서드라고 생각
boolean isEmpty()
- Optional 내부의 값이 null이면 true, 있으면 false
- isPresent() 메서드의 반대되는 메서드
void ifPresent(Consumer<? super T> consumer)
- Optional 내부의 값이 있는 경우 consumer 함수를 실행
Optional<T> filter(Predicate<T> predicate)
- Optional에 filter 조건을 걸어 조건에 맞을 때만 Optional 내부 값이 있음
- 조건이 맞지 않으면 Optional.empty를 리턴
Optional<U> map(Funtion<? super T, ? extends U> f)
- Optional 내부의 값을 Function을 통해 가공
T orElse(T other)
- Optional 내부의 값이 있는 경우 그 값을 반환
- Optional 내부의 값이 null인 경우 other을 반환
T orElseGet(Supplier<? extends T> supplier)
- Optional 내부의 값이 있는 경우 그 값을 반환
- Optional 내부의 값이 null인 경우 supplier을 실행한 값을 반환
T orElseThrow()
- Optional 내부의 값이 있는 경우 그 값을 반환
- Optional 내부의 값이 null인 경우 NoSuchElementException 발생
T orElseThrow(Supplier<? extends X> exceptionSupplier)
- Optional 내부의 값이 있는 경우 그 값을 반환
- Optional 내부의 값이 null인 경우 exceptionSupplier을 실행하여 Exception 발생
Optional을 사용하지 않은 예제
// 컬렉션에서 최댓값을 구한다(컬렉션이 비었으면 예외를 던진다).
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.requiredNonNull(e);
}
}
return result;
}
Optional을 사용한 예제
// 컬렉션에서 최댓값을 구해 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.requiredNonNull(e);
}
}
return Optional.of(result);
}
- Optional을 반환하여 client에서 더욱 더 유연하게 로직을 작성할 수 있다.
- Optional.of에 null을 넣으면 NullPointerException 이 발생하니 주의해야 한다.
- Optional을 리턴하는 메서드에서는 null을 리턴해서는 안된다. (Optional의 취지와 맞지 않기 때문)
Optional을 사용해야 하는 이유
- Optional은 검사 예외와 취지가 비슷하다. 즉, 반환값이 있을 수도 있고, 없을 수도 있음을 API 사용자에게 명확히 알려준다.
Optional 활용1 - 기본값(defalut)를 정해둘 수 있다.
// 실제로 예외를 던진 것이 아니라, empty Optional이 리턴되기 때문에 예외 생성 비용이 들지 않는다.
String lastWordInLexicon = max(words).orElse("단어 없음..");
Optional 활용2 - 원하는 예외를 던질 수 있다.
// 값이 없는 경우 원하는 예외를 던질 수 있다.
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
Optional 활용3 - 항상 값이 채워져 있는 경우
// 값이 없는 경우에는 NoSuchElementException이 발생하니 반드시 값이 있는 경우에만 사용해야 한다.
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
Optional 활용4 - 기본값을 설정하는 비용이 큰 경우
// 기본값을 설정하는 비용이 아주 커서 부담이 되는 경우 orElseGet을 사용하면,
// 값이 처음 필요할 때 Supplier를 사용해 생성하므로 초기 생성비용을 낮출 수 있다.
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
Optional 안티 패턴
Collection, Stream, 배열은 Optional로 감싸지 말자
- Optional<List
>를 반환하기 보다는 빈 ArrayList를 반환하는 것이 좋다. 그렇게 하면 클라이언트 코드에서 Optional 처리 코드를 넣지 않아도 된다.
Optional을 Map의 키나 값으로 사용하지 말자
- Optional은 Collection의 키, 값, 원소나 배열의 원소로 사용하는게 적절한 상황은 거의 없다.
isPresent()를 사용하지 말자
Optional.ofNullable(school).map(School::getClassRoom) //Optional<School>
.map(ClassRoom::getTeacher) //Optional<ClassRoom>
.map(Teacher::getSubject) //Optional<Teacher>
.map(Subject::getSubjectName) //Optional<Subject>
.orElse(null);
- 반드시 이런식으로 사용하자.
추가 내용
- 박싱된 기본타입을 사용할 때에는 OptionalInt, OptionalDouble, OptionalLong을 사용하자
- 박싱된 기본타입을 담는 Optional은 기본타입 보다 무거울 수 밖에 없다.
- 따라서 OptionalInt, OptionalDouble, OptionalLong을 사용하는 것이 조금 더 낫다.
- 값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야 하는 메서드라면
Optional을 반환해야 하는 상황일 수 있다. - Optional을 반환값 이외의 용도로 쓰는 경우는 거의 없다.
출처
https://jaehun2841.github.io/2019/02/24/effective-java-item55/#optional-%EC%9D%B4%EB%9E%80
item 56. 공개된 API 요소에는 항상 문서화 주석을 작성하라
여러분의 API를 올바로 문서화하려면 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야 된다.
- 메서드 주석에서는 HOW가 아닌 WHAT을 기술해야 된다.
- 한 클래스 안에 설명이 똑같은 맴버(혹은 생성자)가 둘 이상이면 안 된다.
- 제네릭 타입이나 제네릭 메서드를 문서화할 때는 모든 타입 매개변수에 주석을 달아야 한다.
- 열거 타입을 문서화할 때는 상수에도 주석을 달아야 한다.
- 에너테이션 타입을 문서화할 때는 멤버들에도 모두 주석을 달아야 한다.
- 클래스 혹은 정적 메서드의 쓰레드 안전수준을 반드시 API 설명에 포함해야 한다. (item 82)
'Java' 카테고리의 다른 글
[Java] JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가 (live-study 1주차) (0) | 2020.11.21 |
---|---|
[이펙티브 자바] 일반적인 프로그래밍 원칙 (0) | 2020.09.21 |
[이펙티브 자바] 제네릭 (0) | 2020.09.21 |
[이펙티브 자바] 클래스와 인터페이스 (0) | 2020.09.10 |
[이펙티브 자바] 모든 객체의 공통 메서드 (0) | 2020.09.10 |