티스토리 뷰
49 매개변수가 유효한지 검사하라
오류가 발생한 즉시 잡아내지 못하면 해당 오류를 감지하기 어려워지고 감지하더라도 발생 지점을 찾기 어려워진다.
메서드 몸체가 실행되기 전에 매개변수를 확인한다면 잘못된 값이 넘어왔을 때 즉각적이고 깔끔한 방식으로 예외를 던질 수 있다.
매개 변수 검사를 제대로 하지 못하면 다음과 같은 문제가 생길 수 있다.
- 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.
- 메서드가 잘 수행되지만 잘못된 결과를 반환할 수 있다.
- 메서드는 문제없지 수행됬지만 어떤 객체를 이상한 상태로 만들어놓아서 예상할 수 없는 시점에 이 메서드와 관계 없는 오류를 낼 수 있다.
*public과 protected 메서드는 매개변수 값이 잘못됐을 때 전디는 예외를 문서화해야 한다.(보통은 IllegalArgumentException, IndexOutOfBoundsException, NullPonterException)
50 적시에 방어적 복사본을 만들라
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 + " after " + end);
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
얼핏보기에 필드값들은 모두 불변인것처럼 보이지만 Date class는 가변이기에 불변식을 깨뜨릴 수 있다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부를 수정했다!
위 문제를 해결하기 위해 Date대신 Isntant 혹은 LocalDateTime같은 불변인 class를 사용하면 되지만 아래와 같이 방어적 복사를 통해서도 문제를 해결할 수 있다.
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 + " after " + this.end);
}
생성자 수정을 통한 공격은 막아낼 수 있으나 Period 인스턴스는 접근자 메서드를 통한 문제가 남아있다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // p의 내부를 변경했다!
방어적 복사본을 반환하는 방식으로 접근자를 수정하도록 하자.
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
인스턴스를 복사하는 데는 일반적으로 생성자나 정적 팩터리를 쓰는게 좋다.
51 메서드 시그니처를 신중히 설계하라
- 메서드 이름은 이해할 수 있고 같은 패키지에 속한 다른 이름들과 일관되게 지어야 한다.
- 매개변수 목록은 짧게 유지한다.
- 매개변수의 타입으로는 클래스보다는 인터페이스가 더 낫다.
52 다중정의는 신중히 사용하라
일반적으로 매개변수가 같을 때는 다중정의를 피하는게 좋다.
*재정의한 메서드는 동적으로 선택되지만 다중정의한 메서드는 정적으로 선택된다.(어느 메서드를 호출할지 컴파일 시점에 결정된다.)
53 가변인수는 신중히 사용하라
가변인수 메서드는 명시한 타입의 인수를 0개 이상 받을 수 있다.
가변인수 메서드를 호출하면, 가장 먼저 인수의 개수와 길이가 같은 배열을 만들고 인수들을 이 배열에 저장하여 가변인수 메서드에 건네준다.
초깃값이나 최소값에 대한 애매한 점은 매개변수를 두 개 전달함으로 해결할 수 있다.
static int min(int firstArg, int... remainingArgs) {
int min = firstArg;
for (int arg : remainingArgs)
if (arg < min)
min = arg;
return min;
}
54 null이 아닌, 빈 컬렉션이나 배열을 반환하라
null이 아닌, 빈 배열이나 컬렉션을 반환하라. null을 반환하는 API는 사용하기 어렵고 오류 처리 코드도 늘어난다. 그렇다고 성능이 좋은 것도 아니다.
/**
* @return 매장 안의 모든 치즈 목록을 반환한다.
* 단, 재고가 하나도 없다면 null을 반환한다.
*/
public List<Cheese> getCheeses() {
return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}
위 코드처럼 null을 반환할 경우 대응하는 코드에서도 null인 경우를 추가해야 한다.
List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON)) System.out.println("좋았어, 바로 그거야.");
빈 컬렉션이 성능을 저하시킬까 우려된다면 똑같은 빈 불변 컬렉션을 반환하는 방법도 있다.
public List<Cheese> getCheeses() {
return cheesesInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(cheesesInStock);
}
경우에 따라 Collections.emptySet, Collections.emptyMap을 사용하는 방법도 있다.
55 옵셔널 반환은 신중히 하라
자바8 이전에는 메서드가 특정 조건에서 값을 반환할 수 있는 선택지는 아래 두 가지였다.
- 예외를 던진다.
- null을 반환한다.
두 방법 모두 허점이 있다.
예외
- 진짜 예외적인 상황에서만 사용해야함
- 예외를 생성할 때 스텍 추적 전체를 캡처하므로 비용도 만만치 않다.
null을 반환하면
- 별도의 null 처리 코드를 추가해야 한다.
- 반환된 null값이 언젠가 NullPointerException을 발생시킬 수 있다.
그러나 자바8에서 추가된 Optional<T>는 null이 아닌 T 타입 참조를 하나 담거나 혹은 아무것도 담지 않을 수 있다.
옵셔널은 원소를 최대 1개 가질 수 있는 ‘불변’ 컬렉션이다.
아무것도 담지 않은 옵셔널은 ‘비었다’ 고 말한다.
반대로 어떤 값을 담은 옵셔널은 ‘비지 않았다’라고 한다.
Optional<T>를 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 적다.
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;
}
이 메서드에 빈 컬렉션을 건네면 IllegalArgumentException을 던진다.
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);
}
Optional.of(value)에 null을 넣으면 NullPointerException을 던지니 주의하자.
만약 null도 허용하고 싶다면 Optional.ofNullable(value)를 사용하면 된다.
스트림의 종단 연산 중 상당수가 옵셔널을 반환한다. 앞의 max 메서드를 스트림 버전으로 다시 작성한다면 Stream의 max 옵션이 우리에게 필요한 옵셔널을 생성해줄 것이다.
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
return c.stream().max(Comparator.naturalOrder());
}
옵셔널 반환을 선택해야 하는 기준은 무엇인가? 옵셔널은 검사 예외와 취지가 비슷하다.
즉, 반환값이 없을 수도 있음을 API 사용자에게 명확히 안내한다.
비검사 예외를 던지거나 null을 반환한다면 API 사용자가 그 사실을 인지하지 못해 생각지 못한 에러가 발생할 수 있다.
하지만 검사 예외를 던지면 클라이언트에서는 반드시 이에 대처하는 코드를 작성해놔야 한다.
마찬가지로 메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 한다.
String lastWordInLexicon = max(words).orElse("단어 없음...");
위 코드처럼 기본 값을 설정할 수 있다.
또는 상황에 맞는 예외를 발생시킬 수 있다.
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
위 코드는 예외가 아니라 예외 팩토리를 건넨 것에 주목하자. 이렇게 하면 예외가 실제로 발생하지 않는 한 예외 생성 비용은 들지 않는다.
filter, map, flatMap, ifPresent같은 메서드들도 알아두도록하자.
한가지 예로 isPresent 메서드를 살펴보자. 옵셔널이 채워져 있으면 true를, 비어 있으면 false를 반환한다.
다음 코드는 부모 프로세스의 프로세스 ID를 출력하거나 부모가 없다면 N/A를 출력하는 코드다.
Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("부모 PID: " + (parentProcess.isPresent() ? String.valueOf(parentProcess.get().pid()) : "N/A"));
Optional의 map을 사용해 다음처럼 다듬을 수 있다.
System.out.println("부모 PID: " + ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));
자바 8에서는 다음과 같이 구현할 수 있다.
streamOfOptionals
.filter(Optional::isPresent) // 옵셔널에 값이 있다면
.map(Optional::get) // 그 값을 꺼내 스트림에 매핑한다.
컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안된다.
*컨테이너 클래스의 기본 타입들은 List, Set, Queue, Map이다.
Optional은 int, long, double 전용 클래스를 제공한다. 바로 OptionalInt, OptionalLong, OptionalDouble이다.
*단, ‘덜 중요한 기본 타입’용인 Boolean, Byte, Character, Short, Float은 예외일 수 있다.
옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.
핵심
값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야하는 메서드라면 옵셔널을 반환해야 할 상황일 수 있다.
하지만 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있다. 그리고 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.
56 공개된 API 요소에는 항상 문서화 주석을 작성하라
API를 옳바르게 문서화하려면 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야 한다. 이 때, 표준 규약을 일관되게 지켜야 한다.
메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 명료하게 기술해야 한다.
문서화 주석에 임의의 HTML 태그를 사용할 수 있음을 기억하라. 단, HTML 메타문자는 특별하게 취급해야 한다.
'프로그래밍 > 책' 카테고리의 다른 글
[이펙티브 자바, Effeective Java] 10장 예외 (0) | 2022.02.04 |
---|---|
[이펙티브 자바, Effeective Java] 9장 일반적인 프로그래밍 원칙 (0) | 2022.01.25 |
[이펙티브 자바, Effeective Java] 7장 람다와 스트림 (0) | 2022.01.04 |
[이펙티브 자바, Effeective Java] 6장 열거 타입과 어노테이션 (0) | 2021.12.28 |
[이펙티브 자바, Effeective Java] 5장 제네릭 (0) | 2021.12.22 |
- Total
- Today
- Yesterday
- 오라클
- 토이프로젝트 회고
- oracle
- oracle.jdbc.driver.T4CConnection.isValid(I)Z
- 젠킨스 웹훅
- CI/CD
- jenkins webhook
- GitHub
- java.lang.AbstractMethodError
- 스프링부트 젠킨스
- nginx to springboot tomcat
- 깃허브 웹훅 젠킨스
- 젠킨스 자동 배포
- nginx 내장톰캣 설정
- springboot jpa
- nginx 내장톰캣 연결
- github webhook jenkins
- slack
- 스프링부트 자동배포
- spring boot jenkins
- nginx to tomcat
- HTML
- 오라클 계정 오류
- nginx 톰캣 설정
- github webhook
- 국비 프로젝트
- nginx to 내장톰캣
- webhook
- spring boot
- springboot
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |