- 메소드와 생성자 대부분은 입력 매개변수의 값이 특정 조건을 만족하기를 바란다.
- ex) 인덱스 값은 음수면 안 된다. 객체 참조는 null이 아니어야 한다.
- 이런 제약은 반드시 문서화해야 하며 메소드 몸체가 시작되기 전에 검사해야 한다.
- 매개변수 검사를 제대로 하지 않았을 때의 문제점
- 메소드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.
- (더 나쁜 상황) 메소드가 잘 수행되지만 잘못된 결과를 반환한다.
- (한층 더 나쁜 상황) 메소드는 문제없이 수행됐지만, 어떤 객체를 이상한 상태로 만들어놓아서 미래의 알 수 없는 시점에 이 메소드와는 관련 없는 오류를 낸다.
- 매개변수 검사에 실패하면
실패 원자성 failure atomicity을 어기는 결과를 낳을 수 있다.- 따라서 자바독에서
@throws를 통해 문서화해야 한다.
- 따라서 자바독에서
java.util.Objects.requireNonNull메소드를 사용하면 null 검사를 수동으로 하지 않아도 된다.- 또한 Java9부터
Objects에checkFromIndexSize,checkFromToIndex,checkIndex등 범위 검사도 추가되었다. (null 검사만큼 유연하지는 않다.)
- 또한 Java9부터
- 공개되지 않은 메소드(
private)라면assert를 사용해 매개변수 유효성을 검증할 수 있다. - 핵심은 단언한 조건이 무조건 참이라고 선언하는 것. 단언문은 일반적인 유효성 검사와 다르다.
- 실패하면
AssertionError를 던진다. - 런타임에 아무런 효과도, 아무런 성능 저하도 없다. (런타임에 영향을 주는 방법도 있다.)
- 하지만 이번 아이템을 "매개변수에 제약을 두는 게 좋다"고 해석해서는 안 된다. 사실은 그 반대다.
- 메소드는 최대한 범용적으로 설계해야 한다. 메소드가 건네받은 값으로 무언가 제대로 된 일을 할 수 있디면 매개변수 제약은 적을수록 좋다.
- 하지만 구현하려는 개념 자체가 특정한 제약을 내재한 경우도 드물지 않다.
- 클라이언트가 우리의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.
- 만약
Date와 같이 가변인 옛날 자바 API를 코드에서 사용하고 있다고 해보자.- 일단 더이상 낡은 API를 사용해서는 안 된다. 하지만 그렇게 해서 문제에서 해방되는 것은 아니다. 많은 API와 내부 구현에 그 잔재가 남아 있을 것이다.
- 외부 공격으로부터 인스턴스 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을
방어적으로 복사 defensive copy해야 한다.
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("~~~");
}
}- 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있다.
- 따라서 반드시 위와 같이 방어적 복사를 해야 한다.
- 매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때
clone을 사용해서는 안 된다. - 또한 접근자 메소드가 남아 있다면 내부의 가변 정보가 드러나기 때문에 여전히 수정이 가능할 수 있다는 점을 주의하자.
- 이 때는 단순히 접근자가 가변 필드의 방어적 복사본을 반환하면 된다.
public Date start() {
return new date(start.getTime());
}- API 설계 요령
- 항상 표준 명명 규칙(item68)을 따라야 한다.
- 이해할 수 있고, 같은 패키지에 속한 다른 이름들과 일관되게 짓는 게 최우선 목표
- 메소드가 너무 많은 클래스는 사용하고, 문서화하고, 테스트하고, 유지보수하기 어렵다.
- 확신이 서지 않으면 만들지 말라.
- 4개 이하가 좋다. 4개가 넘어가면 매개변수를 전부 기억하기가 쉽지 않다.
- 같은 타입의 매개변수 여러 개가 연달아 나오는 경우가 특히 해롭다.
- 과하게 긴 매개변수 목록 짧게 줄여주는 기술 세 가지
- 여러 메소드로 쪼갠다. 잘못하면 메소드가 많아질 수 있지만,
직교성 orthogonality을 높여 오히려 메소드 수를 줄여주는 효과도 있다. - 매개변수 여러 개를 묶어주는 도우미 클래스를 만든다. 일반적으로 정적 멤버 클래스로 둔다.
- 앞의 두 기법을 혼합한 방식으로 모든 매개변수를 하나로 추상화한 객체로 정의하고, 클라이언트에서 이 객체의 세터 메소드를 호출해 필요한 값을 설정하게 한다. 객체 생성에 사용한 빌더 패턴을 메소드 호출에 응용한다고 보면 된다.
- 수학에서 온 용어. 직각을 이루며 교차한다는 뜻.
- 소프트웨어 설계 영역에서는 "직교성이 높다"라고 하면 "공통점이 없는 기능들이 잘 분리되어 있다." 혹은 "기능을 원자적으로 쪼개 제공한다." 정도로 해석할 수 있다.
- 매개변수의 타입으로는 클래스보다는 인터페이스가 더 낫다(item64).
- 또한
boolean보다는 원소 2개짜리 열거 타입이 낫다.
재정의한 메소드는 동적으로 선택되고, 다중정의한 메소드는 정적으로 선택된다.
오버로딩 overloading한 메소드는 컴파일타임에 정해진다는 말이다.- 다중정의가 혼동을 일으키는 상황을 피해야 하며, 안전하고 보수적으로 가려면 매개변수가 많은 다중정의는 만들지 않아야 한다.
- 다중정의하는 대신 메소드 이름을 다르게 짓는 것을 추천.
- ex)
writeBoolean(boolean),writeInt(int),writeLong(long)
- 생성자 다중정의 대신 정적 팩토리라는 대안을 활용해보자.
- 매개변수 수가 같은 다중정의 메소드가 많더라도, 그중 어느 것이 주어진 매개변수 집합을 처리할지가 명확히 구분된다면 헷갈릴 일은 없을 것이다.
- 메소드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안 된다.
- 이 말은 서로 다른 함수형 인터페이스라도 서로 근본적으로 다르지 않다는 뜻이다.
- 가변인수 메소드를 호출하면, 가장 먼저 인수의 개수와 길이가 같은 배열을 만들고 인수들을 이 배열에 저장하여 가변인수 메소드에 건네준다.
- 인수가 1개 이상이어야할 때도 있다. (기본적으로 가변인수는 0개 이상을 받을 수 있게 되어 있음)
- 인수 개수는 런타임에 배열의 길이로 알 수 있다. 하지만 배열의 길이를 검사하는 코드를 넣는 것은 코드가 지저분해진다.
- 차라리 아래와 같이 첫 번째 인자를 평범한 매개변수로 받고, 그 뒤를 가변인수로 받을 수 있을 것이다.
static int min(int firstArg, int... remainingArgs) {
// ...
}- 가변 인수는 인수 개수가 정해지지 않았을 때 매우 유용하다.
- 하지만 성능에 민감한 상황이라면 가변인수가 걸림돌이 될 수 있다.
- 가변인수 메소드는 호출될 때마다 배열을 새로 하나 할당하고 초기화한다.
- 만약 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) {}null을 반환하는 메소드가 있을 때, 널 체크를 하느라 고역이다.- 컬렉션이나 배열을 사용할 때는 빈 컬렉션이나 배열을 반환하자.
- 성능이 걱정이라면 불변 객체를 반환하자. ex)
Collections.emptyList
- 성능이 걱정이라면 불변 객체를 반환하자. ex)
- 옵셔널은 원소를 최대 1개 가질 수 있는 '불변' 컬렉션이다.
- 말이 그렇다는 거지,
Collection<T>를 구현하고 있진 않다.
- 말이 그렇다는 거지,
- 옵셔널을 사용하면, 예외를 던지는 메소드보다 유연하고 사용하기 쉬우며, null을 반환하는 메소드보다 오류 가능성이 작다.
- 특히 옵셔널을 반환하는 메소드에서는
null을 반환하지 말자. 옵셔널을 도입한 취지를 완전히 무시하는 행위다. - 옵셔널은
검사 예외 checked exception와 취지가 비슷하다. 반환값이 없을 수도 있음을 API 사용자에게 명확히 알려준다.- 검사 예외를 던지면 클라이언트에서는 반드시 이에 대처하는 코드를 작성해 넣어야 한다.
- 옵셔널은 당연히 기본 타입보다 무겁다. 자바 API 설계자는 이를 대비해
int,long,double전용 옵셔널 클래스를 준비해놨다.OptionalInt,OptionalLong,OptionalDouble- 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자.
- 옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.
- 또한 옵셔널을 인스턴스 필드에 저장해두는 게 필요할 때가 있을까?
- 이런 상황은 대부분 필수 필드를 갖는 클래스와, 이를 확장해 선택적 필드를 추가한 하위 클래스를 따로 만들어야 함을 암시하는 '나쁜 냄새'다.
- 하지만 가끔은 적절한 상황도 있다.
- javadoc 유틸리티
- 규칙은 How to Write Doc Comments 웹페이지에 기술되어 있다.
- 올바른 문서화를 하려면 공개된 모든 클래스, 인터페이스, 메소드, 필드 선언에 주석으로 달아야 한다.
- ex) 직렬화할 수 있는 클래스라면 직렬화 형태에 관해서도 적어야 한다.
- 메소드용 문서화 주석 -> 메소드와 클라이언트 사이의 규약을 명료하게 기술
- 무엇을 하는지를 기술해야 한다.
- 즉, how가 아닌 what을 기술해야 한다.
- 문서화 주석에는 클라이언트가 해당 메소드를 호출하기 위한
전제조건 precondition을 모두 나열해야 한다. - 또한 메소드가 성공적으로 수행된 후에 만족해야 하는
사후조건 postcondition도 모두 나열해야 한다. - 일반적인 전제조건은
@throws태그로 비검사 예외를 선언하여 암시적으로 기술 @param태그를 이용해 그 조건에 영향받는 매개변수에 기술할 수도 있다.
@param,@return,@throws등 태그의 설명에는 마침표를 붙이지 않는다.{@code}태그의 용도는 2가지- 태그로 감싼 내용을 코드용 폰트로 렌더링
- 태그로 감싼 내용에 포함된 HTML 요소나 다른 자바독 태그를 무시한다.
@implSpec주석은 해당 메소드와 하위 클래스 사이의 계약을 설명하여, 하위 클래스들이 그 메소드를 상속하거나super키워드를 이용해 호출할 때 그 메소드가 어떻게 동작하는지를 명확히 인지하고 사용하도록 해줘야 한다.- API 설명에
<,>,&등의 HTML 메타문자를 포함시키려면 특별한 처리를 해줘야한다.{@literal}태그로 감싸는 것이다.- 문서화 주석은 코드에서건 변환된 API 문서에서건 읽기 쉬워야 한다는 게 일반 원칙이다.
- 양쪽을 만족시키지 못하겠다면 API 문서에서의 가독성을 우선하자.
- 제네릭 타입이나 제네릭 메소드를 문서화할 때는 모든 타입 매개변수에 주석을 달아야 한다.
- 열거 타입을 문서화할 때는 상수들에도 주석을 달아야 한다.
- 어노테이션 타입을 문서화할 때는 멤버들에도 모두 주석을 달아야 한다.
- API 문서화에서 자주 누락되는 설명이 두 가지 있다.
스레드 안정성과직렬화 가능성이다.- 클래스 혹은 정적 메소드가 스레드 안전하든 그렇지 않든, 스레드 안전 수준을 반드시 API 설명에 포함해야 한다. (item82)
- 직렬화도 마찬가지 (item87)
- Javadoc은 문서를 '상속'시킬 수 있다.
- 자바독을 올바르게 작성했는지 확인할 수 있는 여러 방법들이 있지만, 가장 확실한 방법은 자바독 유틸리티가 생성한 웹페이지를 읽어보는 길뿐이다.
- Effective Java 3/E, Joshua Bloch