[Effective Java] 8장. 메서드

kkatal_chae·2022년 10월 23일
0

Effective Java

목록 보기
7/11
post-thumbnail

아이템 49. 매개변수가 유효한지 검사하라.

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

ex) 인텍스 값은 음수이면 안된다, 객체 참조는 null 이 아니어야 한다.

자바 7에 추가된 java.util.Objects.requireNonNull 메서드는 유연하고 사용하기도 편하니, 더 이상 null 검사를 수동으로 하지 않아도 된다.

메서드는 최대한 범용적으로 설계해야 한다. 메서드가 건네받은 값으로 무언가 제대로 된 일을 할 수 있다면 매개변수 제약은 적을수록 좋다. 하지만 구현하려는 개념 자체가 특정한 제약을 내재한 경우도 드물지 않다.

💡 메서드나 생성자를 작성할 때면 그 매개변수들에 어떤 제약이 있을지 생각해야 한다. 그 제약들을 문서화하고 메서드 코드 시작 부분에서 명시적으로 검사해야 한다.

아이템 50. 적시에 방어적 복사본을 만들라

자바는 대체로 안전한 언어이기 때문에 자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 그 불변식이 지켜진다.

하지만 자바라고 해도 다른 클래스로부터의 침범을 아무런 노력 없이 다 막을 수 있는 건 아니다. 그러니 클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.

Date는 낡은 API 이니 새로운 코드를 작성할 때는 더 이상 사용하면 안된다. ( Date 자체가 가변이기 때문에 불변식이 깨질 가능성이 있음 )

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

// Period 인스턴스의 내부 공격
Date start = new Date();
Date end = new Date();
Period p = new Period( start, end );
end.setYear(78);

// 매개변수의 방어적 복사본을 만든다. 
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 );
}

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

멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문에 유효성 검사는 복사본으로 하는 것이 바람직하다.

예를 들어, Date 클래스의 경우 final 이 아니므로 cloneDate 객체가 정의한 것이 아닐 수 있다. 즉, clone 이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다.

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

// 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. 메서드 시그니처를 신중히 설계하라

  • 메서드 이름을 신중히 짓자
    • 이해할 수 있고, 같은 패키지에 속한 다른 이름들과 일관되게 짓는게 최우선 목표!
  • 편의 메서드를 너무 많이 만들지 말자.
  • 매개변수 목록은 짧게 유지하자.
    • 4개 이하가 좋다.
    • 같은 타입의 매개변수 여러 개가 연달아 나오는 경우는 피하자

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

예를 들어 메서드에 HashMap 을 넘길 일은 전혀 없다. 대신 Map 을 사용하자. 그러면 HashMap 뿐 아니라 TreeMap, ConcurrentHashMap, TreeMap 의 부분맵 등 어떤 Map 구현체도 인수로 건넬 수 있다.

아이템 52. 다중정의는 신중히 사용하라

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

// 컬렉션 분류기 - 오류! 이 프로그램은 무엇을 출력할까?
public class CollectionClassifier {
	public static String classify( Set<?> s ) {
		return "집합";
	}

	public static String classify( List<?> lst ) {
		return "리스트";
	}

	public static String classify( Collection<?> c ) {
		return "그 외";
	}

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

“집합”, “리스트”, “그 외” 를 차례로 출력할 것 같지만, 실제로 수행해보면 “그 외” 만 세 번 연달아 출력한다. 이유가 뭘까?

다중정의된 세 classify 중 어느 메서드를 호출할지가 컴파일타임에 정해지기 때문이다.

컴파일 타임에는 for 문 안의 c 는 항상 Collection<?> 타입이다. 런타임에는 타입이 매번 달라지지만, 호출할 메서드를 선택하는 데는 영향을 주지 못한다.

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

// 재정의된 메서드 호출 메커니즘
class Wine {
	String name() { return "포도주"; }
}

class SparklingWine extends Wine {
	@Override String name() { return "발포성 포도주"; }
}

class Champagne extends SparklingWine {
	@Override 
	

위의 코드는 예상한 것처럼 “포도주”, “발포성 포도주”, “샴페인” 을 차례로 출력한다.

for 문에서의 컴파일타임 타입이 모두 Wine 인 것에 무관하게 항상 ‘가장 하위에서 정의한’ 재정의 메서드가 실행되는 것이다.

한편, 다중정의된 메서드 사이에서는 객체의 런타임 타입은 중요치 않다. 선택은 컴파일타임에, 오직 매개변수의 컴파일타임 타입에 의해 이뤄진다.

// 다중정의를 해결한 classify
public static String classify( Collection<?> c ) {
	return c instanceof Set ? "집합" : 
				 c instanceof List ? "리스트" : "그 외";
}				

안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들기 말자. 다중정의하는 대신 메서드 이름을 다르게 지어주는 길도 항상 열려 있으니 말이다.

매개변수 수가 같은 다중정의 메서드가 많더라도, 그중 어느 것이 주어진 매개변수집합을 처리할지가 명확히 구분된다면 헷갈릴 일은 없을 것이다.

즉, 매개변수 중 하나 이상이 “근본적으로 다르다” 면 헷갈릴 일이 없다.

여기서 근본적으로 다르다는 말은 두 타입( null 이 아닌 ) 값을 서로 어느 쪽으로든 형변환할 수 없다는 뜻이다.

메서드를 다중정의할 떄, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안 된다.

💡 프로그래밍 언어가 다중정의를 허용한다고 해서 다중정의를 꼭 활용하란 뜻은 아니다. 일반적으로 매개변수 수가 같을 때는 다중정의를 피하는 게 좋다.

아이템 53. 가변인수는 신중히 사용하라

가변인수는 인수 개수가 정해지지 않았을 때 아주 유용하다.

그런데 성능에 민감한 상황이라면 가변인수가 걸림돌이 될 수 있다.

가변인수 메서드는 호출될 때마다 배열을 새로 하나 할당하고 초기화한다.

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

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


// null 을 반환한다면 필요한 방어코드 
List<Cheese> cheeses = shop.getCheeses();
if ( cheeses != null && cheeses.contains( Cheese.STILTON ))
	System.out.println( " 통과 " );

컬렉션이나 배열 같은 컨테이너가 비었을 때 null 을 반환하는 메서드를 사용할 때면 항시 이와 같은 방어 코드를 넣어줘야 한다. 클라이언트에서 방어 코드를 빼먹으면 오류가 발생할 수 있다.

// 최적화- 빈 컬렉션을 매번 새로 할당받지 않음 
public List<Cheese> getCheese() {
	return cheesesInStorck.isEmpty() ? Collections.emptyList()
		: new ArrayList<>( cheesesInStock );
}

배열을 쓸 때도 마찬가지다. 절대 null 을 반환하지 말고 길이가 0 인 배열을 반환하라.

// 최적화 - 빈 배열을 매번 새로 할당 받지 않도록 함 
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

public Cheese[] getCheeses() {
	return cheesesInStock.toArray( EMPTY_CHEESE_ARRAY );
}
💡 null 이 아닌, 빈 배열이나 컬렉션을 반환하라. null 을 반환하는 API 는 사용하기 어렵고 오류 처리 코드도 늘어난다. 그렇다고 성능이 좋은 것도 아니다.

아이템 55. 옵셔널 반환은 신중히 하라

자바 8 전에는 메서드가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지가 두 가지 있었다.

  1. 예외를 던진다
  2. null 을 반환한다.

예외를 던질 때는 스택 추적 전체를 캡처하므로 비용이 만만치 않다.

null 을 반환할 수 있는 메서드는 별도의 null 처리 코드를 작성해야 한다.

자바 버전이 8 로 올라가면서 또 하나의 선택지가 생겼다.

그 주인공인 Optional<T>null 이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있다.

옵셔널을 반환하는 메서드에서는 절대 null 을 반환하지 말자

옵셔널은 검사 예외와 취지가 비슷하다 즉, 반환 값이 없을 수도 있음을 API 사용자에게 명확히 알려준다.

// 옵셔널 활용 
// 기본값 설정
String lastWordInLexicon = max( words ).orElse( "단어 없음...");

// 원하는 예외를 던질 수 있다.
Toy myToy = max( toys ).orElseThrow( TemperTantrumException :: new );

// 항상 값이 채워져 있다고 가정한다. 
Element lastNobleGas = max( Elements.NOBLE_GASES ).get();

컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안 된다

어떤 경우에 메서드 반환 타입을 T 대신 Optional<T> 로 선언해야 할까?

결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional<T> 를 반환한다

박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수밖에 없다. 값을 두 겹이나 감싸기 때문이다.

그래서 자바 API 설계자들은 기본 타입 전용 옵셔널 클래스들을 준비해놨다. 그러니 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자

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

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

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

API 를 쓸모 있게 하려면 잘 작성된 문서도 곁들여야 한다.

자바독은 소스코드 파일에서 문서화 주석이라는 특수한 형태로 기술된 설명을 추려 API 문서로 변환해준다.

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

메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 명료하게 기술해야 한다.

// 규칙들을 반영한 문서화 주석의 예
/**
* Returns the element at the specified position in this list.
*
*<p> This method is <i> not </i> guaranteed to run in constant 
* time. In some implementations it may run in time proportional 
* to the element position.
*
* @param index 반환할 원소의 인덱스; 0 이상이고 리스트 크기보다 
* 작아야 한다.
* @return 이 리스트에서 지정한 위치의 원소
* @throws IndexOutOfBoundsException index 가 범위를 벗어나면,
*         즉, {@code index < 0 || index >= this.size()}) 면 발생
*/

자바독 유틸리티는 문서화 주석을 HTML 로 변환하므로 문서화 주석 안의 HTML 요소들이 최종 HTML 문서에 반영된다.

@throws 절에 사용한 {@code} 태그의 효과는 두 가지이다.

  1. 태그로 감싼 내용을 코드용 폰트로 렌더링한다.
  2. 태그로 감싼 내용에 포함된 HTML 요소나 다른 자바독 태그를 무시한다.

문서화 주석에 여러 줄로 된 코드 예시를 넣으려면 {@code} 태그를 다시 <pre> 태그로 감싸면 된다.

클래스를 상속용으로 설계할 때는 자기사용 패턴에 대해서도 문서에 남겨 다른 프로그래머에게 그 메서드를 올바로 재정의하는 방법을 알려줘야 한다.

자기 사용 패턴은 자바 8 에 추가된 @implSpec 태그로 문서화한다.

/** 
* 이 컬렉션이 비었다면 true 를 반환한다.
*
* @implSpec 
* 이 구현은 {@code this.size() == 0} 의 결과를 반환한다. 
*
* @return 이 컬렉션이 비었다면 true, 그렇지 않으면 false 
*/
public boolean isEmpty() { ... }

API 설명에 <, >, & 등의 HTML 메타문자를 포함시키려면 특별한 처리를 해줘야 함을 잊지 말자.

가장 좋은 방법은 {@literal} 태그로 감싸는 것이다.

* {@literal |r| < 1} 이면 기하 수열이 수렴한다. 

자바 10부터는 {@summary} 라는 요약 설명 전용 태그가 추가되어, 다음처럼 한결 깔끔하게 처리할 수 있다.

제네릭 타입이나 제네릭 메서드를 문서화할 때는 모든 타입 매개변수에 주석을 달아야 한다.

/**
* 키와 값을 매핑하는 객체. 맵은 키를 중복해서 가질 수 없다.
* 즉, 키 하나가 가리킬 수 있는 값은 최대 1개다.
*
* ( 생략 )
* 
* @param <K> 이 맵이 관리하는 키의 타입 
* @param <V> 매핑된 값의 타입
*/
public interface Map<K, V> {...}

애너테이션 타입을 문서화할 때는 멤버들에도 모두 주석을 달아야 한다.

클래스 혹은 정적 메서드가 스레드 안전하든 그렇지 않든, 스레드 안전 수준을 반드시 API 설명에 포함해야 한다

또한, 직렬화할 수 있는 클래스라면 직렬화 형태도 API 설명에 기술해야 한다.

💡 문서화 주석은 API 를 문서화하는 가장 훌륭하고 효과적인 방법이다. 표준 규약을 일관되게 지키자. 문서화 주석에 임의의 HTML 태그를 사용할 수 있음을 기억하라. 단, HTML 메타문자는 특별하게 취급해야 한다.

0개의 댓글