9 minute read

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

메서드와 생성자 대부분은 입력 매개변수의 값이 특정 조건을 만족하기를 바란다.

예를 들면 인덱스 값은 음수이면 안 되며, 객체 참조는 null이 아니어야 한다.

이런 제약은 반드시 문서화해야 하며 메서드 몸채가 시작되기 전에 검사해야 한다.

메서드 몸체가 실행되기 전에 매개변수를 확인한다면 잘못된 값이 넘어왔을 때 즉각적이고 깔끔한 방식으로 예외를 던질 수 있다.

public과 protected 메서드는 매개변수 값이 잘못됐을 때 던지는 예외를 문서화해야 한다.

매개변수의 제약을 문서화한다면 그 제약을 어겼을 때 발생하는 예외도 함께 기술해야 한다.

공개되지 않은 메서드라면 패키지 제작자인 여러분이 메서드가 호출되는 상황을 통제할 수 있다. 따라서 오직 유효한 값만이 메서드에 넘겨지리라는 것을 보증할 수 있고, 그렇게 해야 한다. 다시 말해 public이 아닌 메서드라면 assert를 사용해 매개변수 유효성을 검증할 수 있다.

생성자 매개변수의 유효성 검사는 클래스 불변식을 어기는 객체가 만들어지지 않게 하는 데 꼭 필요하다.

이번 아이템을 ‘매개변수에 제약을 두는 게 좋다’고 해석해서는 안 된다. 사실은 그 반대다. 메서드는 최대한 범용적으로 설계해야 한다. 메서드가 건네받는 값으로 무언가 제대로 된 일을 할 수 있다면 매개변수 제약은 적을수록 좋다. 하지만 구현하려는 개념 자체가 특정한 제약을 내재한 경우도 드물지 않다.

메서드가 생서자를 작성할 때면 그 매개변수들에 어떤 제약이 있을지 생각해야 한다. 그 제약들을 문서화하고 메서드 코드 시작 부분에서 명시적으로 검사해야 한다. 이런 습관을 반드시 기르도록 하자. 그 노력은 유효성 검사가 실제 오류를 처음 걸러낼 때 충분히 보상받을 것이다.

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

자바는 안전한 언어다.

하지만 아무리 자바라 해도 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있는 건 아니다.

그러니 클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적응로 프로그래밍해야 한다.

실제로도 악의적인 의도를 가진 사람들이 시스템의 보안을 뚫으려는 시도가 늘고 있다.

어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다. 하지만 주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다.

public final class Period {
	private final Date start;
	private final Date end;

	public Period(Date start, Date end) {
		if (start.compareTo(end) > 0) 
			throw new IllegalArgumentException();
		this.start = start;
		this.end= end;
	}
	
	public Date start() { return start; { 
	public Date end() {return end; } 

위의 클래스는 불변처럼 보이지만, Date가 가변이라는 사실을 이용하면 어렵지 않게 그 불변식을 깨뜨릴 수 있다.

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end) ;
end.setYear(78); // p의 내부를 수정했다.

다행히 자바 8 이후로는 쉽게 해결할 수 있다.

Date 대신 불변인 Instace를 사용하면 된다. 혹은 LocalDateTime, ZonedDateTime

Date는 낡은 API이니 새로운 코드를 작성할 때는 더 이상 사용하면 안 된다.

외부 공격응로부터 Period 인스턴스의 내부를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다.

public final class Period {
	private final Date start;
	private final Date end;

	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();

	}
	
	public Date start() { return start; { 
	public Date end() {return end; } 

새로 작성한 생성자를 이용하면 앞서의 공격은 더 이상 Period에 위협이 되지 않는다.

매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자.

꼭 이렇게 해야 한다. 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.

매개변수가 제3자의 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안된다.

두 번째 공격을 막아내려면 단순히 접근자가 가변 필드의 방어적 복사본을 반환하면 된다.

public Date start() { reutrn new Date(start.getTime());
public Date end() { return new Date(end.getTime());

매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위해서만은 아니다. 메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다.

클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면 그 요소는 반드시 방어적으로 복사해야 한다. 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사를 수행하는 대신 해당 구성용소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시하도록 하자.

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

  • 메서드 이름을 신중히 짓자.
    • 이해할 수 있고 같은 패키지에 속한 다른 이름들과 일관되게 짓는 게 최우선이다.
  • 편의 메서드를 너무 많이 만들지 말자.
    • 메서드가 너무 많은 클래스는 익히고, 사용하고, 문서화하고, 테스트하고, 유지보수하기 어렵다.
    • 클래스나 인터페이스는 자신의 각 기능을 완벽히 수행하는 메서드로 제공해야 한다. 아주 자주 쓰일 경우에만 별도의 약칭 메서드를 두기 바란다. 확신이 서지 않으면 만들지 말자.
  • 매개변수 목록은 짧게 유지하자.
    • 4개 이하가 좋다.
    • 4개가 넘어가면 매개변수를 전부 기억하기가 쉽지 않다.
    • 개인적인 생각으로 4개를 넘어가게 된다면 좋은 변수 이름으로 구분할 수 있게 하자 .
    • 같은 타입의 매개변수 여러 개가 연달아 나오는 경우가 특히 해롭다.

과하게 긴 매개변수 목록을 짧게 줄여주는 기술 세 가지

  1. 여러 메서드로 쪼갠다. 쪼개진 메서드 각각은 원래 매개변수 목록의 부분집합을 받는다.
  2. 매개변수 여러 개를 묶어주는 도우미 클래스를 만드는 것이다. 일반적으로 이런 도우미 클래스는 정적 멤버 클래스로 둔다.
  3. 객체 생성에 사용한 빌더 패턴을 메서드 호출에 응용한다고 보면 된다.
  4. 이 기법은 매개변수가 많을 때, 특히 그중 일부는 생략해도 괜찮을 때 도움이 된다.

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

→ 매개변수로 적합한 인터페이스가 있다면 그 인터페이스가 더 낫다.

  • 또한 boolean 보다는 원소 2개짜리 열거 타입이 낫다.

    (메서드 이름상 boolean을 받아야 의미가 더 명확할 때는 예외다.

    • 열거 타입을 사용하면 코드를 읽고 쓰기가 더 쉬워진다.
    • 나중에 선택지를 추가하기도 쉽다.

다중정의는 신중히 하자 .

다음은 컬렉션을 집합, 리스트, 그 외로 구분하고자 만든 프로그램이다.

public class CollectionClassifier {
	public static String classify(Set<?> s) {
		return "집합";
	}
	
	public static String classify(List<?> lst) {
		reutrn "리스트";
	}
	
	public static String classify(Collection<?> c) {
		return "그 외";
	}
}

psvm(args) {
	// hashset
	// arrayList
	// hashMap

	 // 위의 세 객체를 위의 클래스로 실행
	}

실제로 수행해보면 “그 외”만 세번 연달아 출력한다.

이유가 뭘까? 오버로딩된 세 classify 중 어느 메서드를 호출할지가 컴파일타임에 정해지기 때문이다.

이처럼 직관과 어긋나는 이유는 재정의한 메서드는 동적으로 선택하고, 다중정의한 메서드는 정적으로 선택되기 때문이다.

  • 헷갈릴 수 있는 코드는 작성하지 않는 게 좋다. 특히나 공개 API라면 더욱 신경 써야 한다.
  • 그러니 다중정의는 혼동을 일으키는 상황을 피해야 한다.
  • 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자.
  • 다중정의하는 대신 메서드 이름을 다르게 지어주는 길도 항상 열려 있으니 말이다.

한편, 생성자는 이름을 다르게 지을 수 없으니 두 번째 생성자부터는 무조건 다중정의가 된다. 하지만 정적 팩터리라는 대안을 활용할 수 있는 경우가 많다.

자바 언어에 제네릭과 오토박싱을 더한 결과 List 인터페이스가 취약해졌다.

List 인터페이스가 remove(object)와 remove(int)를 다중정의했기 때문이다.

자바 라이브러리는 이번 아이템의 정신을 지켜내려 애쓰고 있지만, 실패한 클래스도 몇 개 있다. 예컨대 String 클래스의 valueOf(char[])과 valueOf(Object)는 같은 객체를 건레더라도 전혀 다른 일을 수행한다.

프로그램 언어가 다중정의를 허용한다고 해도 다중정의를 꼭 활용하란 뜻은 아니다. 일반적으로 매개변수 수가 같을 때는 다중저의를 피하는 게 좋다. 상황에 따라, 특히 생성자라면 이 조언을 따르기가 불가능할 수 있다. 그럴 때는 헷갈릴 만한 매개변수는 형변환하여 정확한 다중정의 메서드가 선택되도록 해야 한다. 이것이 불가능하면, 예컨대 기존 클래스를 수정해 새로운 인터페이슬르 구현해야 할 때는 같은 객체를 입력받는 다중정의 메서드들이 모두 동일하게 동작하도록 만들어야 한다. 그렇지 못하면 프로그래머들은 다중정의된 메서드나 생성자를 효과적으로 사용하지 못할 것이고, 의도도래 동작하지 않는 이유를 이해하지도 못할 것이다.

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

가변인수 메서드는 명시한 타입의 인수를 0개 이상 받을 수 있다.

가변인수 메서드를 호출하면, 가장 먼저 인수의 개수와 길이가 같읕 배열을 만들고 인수들을 이 배열에 저장하여 가변인수 메서드에 건네준다.

그런데 성능에 민감한 상황이라면 가변인수가 걸림돌이 될 수 있다. 가변인수 메서드는 호출될 때마다 배열을 새로 하나 할당하고 초기화한다. 다행히, 이 비용을 감당할 수는 없지만, 가변인수의 유연성이 필요할 때 선택할 수 있는 멋진 패턴이 있다.

예를 들어 메서드 호출의 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의 정적 팩터리도 이 기법을 사용해 열거 타입 집합 생성 비용을 최소화한다. EnumSet은 비트 필드를 대체하면서 성능까지 유지해야 하므로 아주 적절하게 활용한 예라 할 수 있다.

인수 개수가 일정하지 않은 메서드를 정의해야 한다면 가변인수가 반드시 필요하다. 메서드를 정의할 때 필수 매개변수는 가변인수 앞에 두고, 가변인수를 사용할 때는 성능 문제까지 고려하자.

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

public List<Cheese> getCheeses() {
		return CheeseInStock.isempty() ? null : 
				new Arraylist<>(cheeseInStock);

사실 재고가 없다고 해서 특별히 취급할 이유가 없다. 그럼에도 이 코드처럼 null을 반환한다면, 클라이언트는 이 null 상황을 처리하는 코드를 추가로 작성해야 한다.

컬렉션이나 배열 같은 컨테이너가 비었을 때 null을 반환하는 메서드를 사용할 때면 항시 이와 같은 방어 코드를 넣어줘야 한다.

때로는 빈 컨테이너를 할당하는 데도 비용이 드니 null을 반환하는 쪽이 낫다는 주장이 있다. 하지만 이는 두 가지 면에서 틀린 주장이다. 첫 번째, 성능 분석 결과 이 할당이 성능 저하의 주범이라고 확인되지 않는 한 , 이 정도의 성능 차이는 신경 쓸 수준이 못 된다. 두 번째, 빈 컬렉션과 배열은 굳이 새로 할당하지 않고도 반환할 수 있다.

null이 아닌, 빈 배열이나 컬렉션을 반환하라. null을 반환하는 API는 사용하기 어렵고 오류 처리 코드도 늘어난다. 그렇다고 성능이 좋은 것도 아니다.

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

자바 8 전에는 메서드가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지가 두 가지 있었다. 예외를 던지거나, null을 반환하는 것이다. 두 방법 모두 허점이 있었다.

예외는 진짜 예외적인 상황에서만 사용해야 하며, 예외를 생성할 때 스택 추적 전체를 캡처하므로 비용도 만만치 않다.

null을 반환하면, 별도의 null 처리 코드를 추가해야 한다.

자바 버전이 8로 올라가면서 또 하나의 선택지가 생겼다. Optional는 nulll 이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있다. 아무것도 담지 않은 옵셔널은 ‘비었다’고 말한다. 반대로 어떤 값을 담은 옵셔널은 ‘비지 않았다’고 한다.

옵셔널은 원소를 최대 1개 가질 수 있는 ‘불변’ 컬렉션이다.

옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자. 옵셔널을 도입한 취지를 완전히 무시하는 행위다.

그렇다면 null을 반환하거나 예외를 던지는 대신 옵셔널 반환을 선택해야 하는 기준은 무엇인가? 옵셔널은 검사 예외와 취지가 비슷하다. 즉, 반환값이 없을 수도 있음을 API 사용자에게 명확히 알려준다.

메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 한다.

String result = max(words).orElse("단어 없음...");

Toy myToy = max(toys).orElseThrow(Exception::new);

기본값을 설정하는 비용이 아주 커서 부담이 될 때가 있다. 그럴 때는 Supplier를 인수로 받는 orElseGet을 사용하면, 값이 처음 필요할 때 Supplier를 이용해 생성하므로 초기 설정 비용을 낮출 수 있다.

더 특별한 쓰임에 대비한 기본 메서드도 준비되어 있다.

바로 filter, map, flatMap, ifPresent다.

스트림을 사용한다면 옵셔널들을 Stream<Optional>로 받아서, 그중 채워진 옵셔널들에서 값을 뽑아 Stream에 건네 담아 처리하는 경우가 드물지 않다.

streamOfOptional.filter(Optional::isPresent)
	.map(Optional::get) 

자바 9에서는 optional에 stream() 메서드가 추가되었다. 이 메서드는 값이 있으면 그 값을 원소로 담은 스트림으로, 값이 없다면 빈 스트림으로 변환한다.

반환값으로 옵셔널을 사용한다고 해서 무조건 득이 되는 건 아니다. 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안 된다.

→ 빈 Optional<List>를 반환하기보다는 빈 List를 반환하는 게 좋다.

그렇다면 어떤 경우에 메서드 반환 타입을 T 대신 Optional로 선언해야 할까? 기본 규칙은 이렇다. 결과가 없을 수 없으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional를 반환한다. 그런데 이렇게 하더라도 Optional를 반환하는 데는 대가가 따른다. Optional도 염연히 새로 할당하고 초기화해야 하는 객체이고, 그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한 단계를 더 거치는 셈이다. 그래서 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있다.

박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수밖에 없다. 값을 두 겹이나 감싸기 때문이다. 그래서 자바 API 설계자는 int, long, doubel 전용 옵셔널 클래스들을 준비해놧다.

박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자.

옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.

값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야하는 메서드라면 옵셔널을 반환해야 할 상황일 수 있다. 하지만 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있다. 그리고 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.

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

문서화 주석을 작성하는 규칙은 공식 언어 명세에 속하진 않지만 자바 프로그래머라면 응당 알아야 하는 업계 표준 API라 할 수 있다.

API를 올바로 문서화하려면 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야 한다.

기본 생성자에는 문서화 주석을 달 방법이 없으니 공개 클래스는 절대 기본 생성자를 사용하면 안 된다. 한편, 유지보수까지 고려한다면 대다수의 공개되지 않은 클래스, 인터페이스, 생성자, 메서드, 필드에도 문서화 주석을 달아야 할 것이다.

문서화 주석은 API를 문서화하는 가장 훌륭하고 효과적인 방법이다. 공개 API라면 빠짐없이 설명을 달아야 한다. 표준 규약을 일관되게 지키자. 문서화 주석에 임의의 HTML 태그를 사용할 수 있음을 기억하자.