////
Search

8장 - 메서드

Created
2022/09/15 07:22
Tags
Java

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

메서드와 생성자 대부분은 입력 매개변수의 값이 특정 조건을 만족하기를 바란다.
ex) 인덱스 값은 음수면 안됨, 객체 참조는 null이면 안됨
이런 제약은 반드시 문서화해야하며 메서드 몸체가 시작되기 전에 검사해야 한다.
메서드 몸체가 실행되기 전 매개변수를 확인한다면 잘못된 값이 넘어왔을때 즉각적이고 깜끔한 방식으로 예외를 던질 수 있다.
publicprotected 메서드는 매개변수 값이 잘못됐을 때 던지는 예외를 문서화해야 한다.

전형적인 문서화 예시

/** * (현재 값 mod m) 값을 반환한다. 이 메서드는 * 항상 음이 아닌 BigInteger를 반환한다는 점에서 remainder 메서드와 다르다. * * @param m 계수 (양수여야 한다.) * @return 현재 값 mod m * @throws ArithmeticException m이 0보다 작거나 같으면 발생한다. */ public BigInteger mod(BigInteger m) { if (m.signum() < 0) throw new ArithmeticException("계수(m)는 양수여야 합니다. " + m); ... }
Java
복사
mnull일 때 NullPointerException을 던진다는 설명은 없다.
그 이유는 설명을 개별 메서드가 아닌 BigInteger 클래스 수준에서 기술했기 때문이다.
각 메서드에 일일이 기술하는 것보다 훨씬 깔끔한 방법이다.

자바의 null 검사기

this.startegy = Objects.requireNonNull(strategy, "전략");
Java
복사
자바 7에 추가된 java.util.Objects.requireNonNull 메서드는 유연하고 사용하기도 편하니, 더 이상 null 검사를 수동으로 하지 않아도 된다.
원하는 예외 메세지도 지정 가능하다.
값을 사용하는 동시에 null 검사를 수행할 수 있다.
반환값은 그냥 무시하고 필요한 곳 어디서든 순수한 null 검사 목적으로 사용해도 된다.
자바 9에서는 범위 검사 기능도 더해졌는데, null 검사 메서드 만큼 유연하지는 않다.
리스트와 배열 전용으로 설계되었고, 닫힌범위(양 끝단 값을 포함하는)는 다루지 못한다.

단언문(assert)를 사용한 유효성 검사

private static void sort(long[] a, int offset, int length) { assert a != null; assert offset >= 0 && offset <= a.length; assert length >= 0 && length <= a.length - offset; ... }
Java
복사
공개되지 않은 메서드라면 메서드가 호출되는 상황을 통제할 수 있다.
따라서 오직 유효한 값만이 메서드에 넘겨지리라는 것을 보증할 수 있고, 그렇게 해야한다.
단언문은 유효성 감사와 다르다.
실패하면 AssertionError를 던진다.
런타임에 아무런 효과도, 성능 저하도 없다.

나중에 사용하는 매개변수

메서드가 직접 사용하지 않으나 나중에 사용하려고 저장하는 매개변수는 특히 더 신경써야 한다.
만약 검사를 생략한다면 어느 단계에서 문제가 발생한것인지 추적하기 곤란하다.
생성자는 해당 원칙의 특수한 사례로, 매개변수의 유효성 검사는 클래스 불변식을 어기는 객체가 만들어지지 않게 하는데 꼭 필요하다.

예외

유효성 검사 비용이 지나치게 높거나 실용적이지 않을 경우
계산 과정에서 암묵적으로 검사가 수행되는 경우
암묵적 유효성 검사에 의존하다가는 실패 원자성을 해칠 수 있다.

정리

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

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

자바는 안전한 언어로, 자바를 쓰는 즐거움 중 하나다.
네이티브 메서드를 사용하지 않아 C/C++ 같이 안전하지 않은 언어에서 흔히보는 버퍼/배열 오버런, 와일드 포인터 같은 메모리 충돌에서 안전하다.
자바가 다른 클래스로부터의 침범을 아무 노력없이 다 막을 수 있는건 아니다.
클라이언트가 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍 해야한다.

불변식을 지키지 못하는 경우

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 + "가 " + end + "보다 늦다."); this.start = start; this.end = end; } public Date start() { return start; } public Date end() { return end; } public String toString() { return start + " - " + end; } }
Java
복사
엇필 이 클래스는 불변처럼 보이고, 시작 시각이 종료 시작보다 늦을 수 없다는 불변식이 무리없이 지켜질 것 같다.
하지만 Date가 가변이라는 사실을 이용하면 어렵지 않게 불변식을 깨뜨릴 수 있다.
Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); end.setYear(78); // p 내부 수정
Java
복사
다행이 자바8 이후로는 Date대신 Instance를 사용해 쉽게 해결할 수 있다.

방어적 복사

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

매개변수의 방어적 복사

public Period(Date start, Date end) { this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (this.start.compareTo(this.end) > ) throw new IllegalArgumentException(this.start + " after " + this.end); }
Java
복사
매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자.
순서가 부자연스러워 보이겠지만 반드시 이렇게 작성해야한다.
멀티스레딩 환경이라면 유효성 검사 후 복사를 진행하는 찰나의 순간에 값이 수정될 위험이 있다.
방어적 복사에 clone 메서드를 사용하지 않는 점에도 주목하자.
Date는 final이 아니므로 clone이 Date가 정의한게 아닐 수도 있다.
즉, clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다.
매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안된다.

필드의 방어적 복사본 반환

Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); p.end().setYear(78); // p의 내부를 공격
Java
복사
생성자를 수정하면 앞서의 공격은 막아낼 수 있지만, Period 인스턴스는 아직도 변경 가능하다.
접근자 메서드가 내부의 가변 정보를 직접 드러내기 때문이다.
두 번째 공격을 막아내려면 단순히 접근자가 가변 필드의 방어적 복사본을 반환하면 된다.
public Date start() { return new Date(start.getTime()); } public Date end() { return new Date(end.getTime()); }
Java
복사
필드의 방어적 복사본 반환
새로운 접근자까지 갖추면 Period는 완벽한 불변으로 거듭난다.
Period자신 말고는 가변 필드에 접근할 방법이 없으니 모든 필드가 객체 안에서 캡슐화되었다.
생성자와 달리 접근자 메서드에서는 방어적 복사에 clone을 사용해도 된다.
그렇더라도 인스턴스를 복사하는 데는 일반적으로 생성자나 정적 팩터리를 쓰는게 좋다.
매개변수를 방어적으로 복사하는 목적이 불변 객체를 만들기 위해서만은 아니다.
메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다.
확신할 수 없다면 복사본을 만들어 저장해야 한다.
가변인 내부 객체를 클리언트에 반환할 때는 반드시 심사숙고해야 한다.

정리

되도록 불견 객체들을 조합해 객체를 구성해야 방어적 복사를 할일이 줄어든다는 교훈을 얻을 수 있다.
방어적 복사에는 성능 저하가 따르고, 또 항상 쓸 수 있는것도 아니다.
호출자가 컴포넌트 내부를 수정하지 않으니라 확신하면 방어적 복사를 생략할 수 있다.
다른 패키지에서 사용한다고 해서 넘겨받는 가변 매개변수를 항상 방어적으로 복사해 저장해야 하는 것은 아니다.
메서드나 생성자의 매개변수로 넘기는 행위가 그 객체의 통제권을 이전함을 뜻해, 통제권을 이전하는 메서드를 호출하는 클라이언트는 해당 객체를 더 이상 직접 수정하는 일이 없다고 약속해야 한다.

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

메서드 이름을 신중히 짓자.

항상 표준 명명규칙을 따라야 한다.
이해할 수 있고, 같은 패키지에 속한 다른 이름들과 일관되게 짓는게 최우선 목표다.

편의 메서드를 너무 많이 만들지 말자.

모든 메서드는 각각 자신의 소임을 다해야 한다.
메서드가 너무 많은 클래스는 익히고, 사용하고, 문서화하고, 테스트하고 유지 보수하기 어렵다.
아주 자주쓰일 경우에만 약칭 메서드를 두고, 확신이 서질 않는다면 만들지 말자.

매개변수 목록은 짧게 유지하자.

4개 이하가 좋다.
같은 타입의 매개변수가 연달아 여러 개 나오는 경우가 특히 해롭다.

과하게 긴 매개변수 목록을 짧게 줄이는 법

여러 메서드로 쪼갠다.
쪼개진 메서드 각각은 원래 매개변수의 부분집합을 받는다.
매개변수 여러 개를 묶어주는 도우미 클래스를 만들자.
일반적으로 이런 도우미 클래스는 정적 멤버 클래스로 둔다.
빌더 패턴을 메서드 호출에 응용한다.
특히 일부 매개변수를 생략해도 괜찮을 때 도움이 된다.
모든 매개변수를 하나로 추상화한 객체를 정의하고, 클라이언트에서 이 객체의 세터 메서드를 호출해 필요한 값을 설정하게 한다.

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

구현체와 무관하게 구현된 모든 인터페이스를 사용할 수 있다.
특정 구현체로 명시하게 된다면 비싼 복사 비용을 치뤄야 할 수도 있다.

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

메서드 이름상 boolean을 받아야 의미가 더 명확할 때는 예외다.
열거 타입을 사용하면 코드를 읽고 쓰기가 쉬워진다.
나중에 선택지를 추가하기도 쉽다.

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)); } }
Java
복사
오류가 발생하는 다중정의 예제
다중정의된 메서드는 어떤 메서드를 호출할지에 대해서 컴파일타임에 정해진다.
때문에 "집합", "리스트", "그 외"를 차례로 출력하기를 예상했지만, 실제로 수행해보면 "그 외"만 세 번 출력한다.
런타임에는 매번 타입이 달라지지만, 컴파일타입을 기준으로는 항상 Collection<?>만 호출하는 것이기 때문이다.
이처럼 직관과 어긋나는 이유는 재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택되기 때문이다.
객체의 런타임 타입은 전혀 중요치 않다.
선택은 컴파일타임에 오직 매개변수의 컴파일타임 타입에 의해 이뤄진다.

문제 해결 방법

public static String classify(Collection<?> c) { return c instanceof Set ? "집합" : c instanceof List ? "리스트" : "그 외"; }
Java
복사

더 좋은 방법

헷갈릴 수 있는 코드는 작성하지 않는게 좋다.
사용자가 매개변수를 넘기면서 어떤 다중정의 메서드가 호출될지를 모른다면 프로그램이 오동작하기 쉽다.
다중정의가 혼동을 일으키는 상황을 피해야한다.
안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의만들지 말자.
다중정의 대신 메서드 이름을 다르게 지어주는 방법도 열려있다.

생성자의 경우

생성자는 2개 이상의 경우 무조건 다중정의 된다.
정적 팩터리라는 대인을 활용할 수 있는 경우가 많다.
생성자는 재정의할 수 없으니 다중정의와 재정의가 혼용될 걱정은 안해도 된다.

매개변수 수가 같은 다중정의

매개변수 수가 같더라도, 어느 것이 주어진 매개변수 집합을 처리할지가 명확히 구분된다면 헷갈릴 일은 없을것이다.
즉, 매개변수 중 하나 이상이 "근본적으로 다르다"면 헷갈릴 일이 없다.

재정의

class Wine { String name() { return "포도주"; } } class SparklingWine extends Wine { @Override String name() { return "발포성 포도주"; } } class Champagne extends SparklingWine { @Override String name() { return "샴페인"; } } public class Overriding { public static void main(String[] args) { List<Wine> wineList = List.of(new Wine(), new SparklingWine(), new Champagne()); for (Wine wine : wineList) System.out.println(wine.name()); } }
Java
복사
메서드 재정의란 상위 클래스가 정의한 것과 똑같은 시그니처의 메서드를 하위 클래스에서 다시 정의한 것을 말한다.
메서드를 재정의한 다음 '하위 클래스의 인스턴스'에서 그 메서드를 호출하면 재정의한 메서드가 실행된다.
예상한 것처럼 이 프로그램은 "포도주", "발포성 포도주", "샴페인"을 차례로 출력한다.
컴파일타임 타입이 모두 Wine인 것에 무관하게 항상 ‘가장 하위에서 정의한 재정의 메서드’가 실행되는 것이다.

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

가변인수 메서드는 명시한 타입의 인수를 0개 이상 받을 수 있다.
가변인수 메서드를 호출하면, 가장 먼저 인수의 개수와 길이가 같은 배열을 만들고 인수의 인수들을 이 배열에 저장하여 가변인수 메서드에 건네준다.

간단한 가변인수 활용

static int sum(int... args) { int sum = 0; for (int arg : args) sum += arg; return sum; }
Java
복사
인수가 1개 이상이어야할 때도 있다.
인수 개수는 런타임에 배열의 길이로 알 수 있다.

잘못 구현한 예시

static int min(int... args) { if (args.length == 0) throw new IllegalArgumentException("인수가 1개 이상 필요합니다."); int min = args[0]; for (int i = 1; i < args.length; i++) if (args[i] < min) min = args[i]; return min; }
Java
복사
이 코드의 문제는 인수를 0개만 넣어 호출했을 때 런타임에 실패한다는 것이다.
코드도 지저분하다.
args 유효성 검사를 명시적으로 해야하고, min의 초깃값을 Integer.MAX_VALUE로 설정하지 않고는 더 명료한 for-each문도 사용할 수 없다.

올바른 구현 예시

static int min(int firstArgs, int... remainingArgs) { int min = firstArg; for (int arg : remainingArgs) if (arg < min) min = arg; return min; }
Java
복사

성능에 민감한 경우

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

다중정의를 통한 성능향상

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) { }
Java
복사
만약 95%의 메서드 호출이 인수를 3개 이하로 사용한다고 가정한다면
인수가 0개인 것부터 4개인 것까지, 총 5개를 다중정의하자.
마지막 다중정의 메서드가 인수 4개 이상인 5%의 호출을 담당하는 것이다.
EnumSet의 정적 팩터리도 이 기법을 사용해 열거 타입 집합 생성 비용을 최소화한다.
EnumSet은 비트 필드를 대체하면서 성능까지 유지해야 하므로 아주 적절하게 활용한 예시라 할 수 있다.

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

흔히 볼 수 있는 메서드

private final List<Cheese> cheesesInStock = ... ; public List<Cheese> getCheeses() { return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock); }
Java
복사
재고가 없다고 해서 특별히 취급할 이유는 없다.
그럼에도 이 코드처럼 null을 반환한다면, 클라이언트는 이 null을 처리하는 코드를 추가로 작성해야 한다.
List<Cheese> cheeses = shop.getCheeses(); if (cheeses != null && cheeses.contains(Cheese.STILTON)) System.out.println("좋았어, 바로 그거야.");
Java
복사
null 방어코드

null 대신 빈 컨테이너를 리턴하자.

빈 컨테이너를 할당하는 데도 비용이 드니 null을 반환하는 쪽이 낫다는 주장도 있다.
하지만 이것은 틀렸다.
성능 분석 결과 이 할당이 설능 저하의 주범이라고 확인되지 않는 한 신경 쓸 수준임 못된다.
빈 컬렉션과 배열은 굳이 할당하지 않고도 반환할 수 있다.

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

자바 8이전에는 메서드가 특정 조건을 반환할 수 없을 때 두 가지 선택지가 있었다.
예외 던지기
null 반환

문제점

예외는 진짜 예외적 상황에서만 사용해야 한다.
예외를 생성할 때 스택 추적 전체를 캡처하므로 비용이 비싸다.
TMI. fillInStackTrace를 재정의한다면 해당 캡쳐 비용을 없애거나 줄일 수 있다.
별도의 null 처리 코드를 추가해야 한다.
그것도 null을 반환하게 한 실제 원인과는 상관 없는 코드에서 처리해야한다.

옵셔널

자바 8부터는 Optional<T>이 생기며 하나의 선택지가 늘어났다.
옵셔널은 null이 아닌 T타입 참조 하나를 담거나, 혹은 아무것도 담지 않을 수도 있다.
옵셔널은 원소를 최대 1개 가질 수 있는 '불변' 컬렉션이다.
보통은 T를 반환해야 하지만 특정 조건에서는 아무것도 반환하지 않아야할 때 T 대신 Optional<T>를 반환하도록 선언하면 된다.
옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 작다.

옵셔널을 이용한 개선

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) > ) result = Objects.reuqireNonNull(e); return Optional.of(result); }
Java
복사
컬렉션에서 최대값을 구해 Optional<E>로 반환한다.
빈 옵셔널은 Optional.Empty()로 만들고, 값이 든 옵셔널은 Optional.of(value)로 생성했다.
Optional.of(value)에 null을 넣으면 NullPointerException을 던지니 주의하자.
null 값도 허용하는 옵셔널을 만들려면 Optional.ofNullable(value)를 사용하면 된다.
옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자.
옵셔널은 검사 예외와 취지가 비슷하다.
즉 반환 값이 없을 수도 있음을 API 사용자에게 명확히 알려준다라는 점이다.

옵셔널을 리턴받았을 경우

메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 한다.
기본값 설정
String lastWordInLexicon = max(words).orElse("단어 없음...");
Java
복사
예외 던지기
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
Java
복사
항상 값이 채워져있다고 가정
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
Java
복사
잘못 판단한 것이라면 NoSuchElementException이 발생한다.

옵셔널을 사용하지 말아야 할 경우

컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안된다.
Optional<List<T>>를 반환하기보다는 빈 List<T>를 반환하는게 좋다.

옵셔널을 반환해야할 경우

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

옵셔널의 단점

Optional도 엄연히 새로 할당하고 초기화해야 하는 객체이고, 그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한 단계를 더 거치는 셈이다.
그래서 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있다.
박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수밖에 없다.
값을 두 겹이나 감싸기 때문이다.
그래서 자바 API 설계자들은 int, long, double 전용 옵셔널 클래스들을 준비해놨다. 바로 OptionalInt, OptionalLong, OptionalDouble이다.
이 옵셔널들도 Optional가 제공하는 메서드를 거의 다 제공한다.
이렇게 대체제까지 있으니 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자.
덜 중요한 Boolean, Byte, Character, Short, Float은 예외일 수 있다.

절대 금지

옵셔널을 맵의 값으로 사용하면 절대 안 된다.
그리 한다면 맵 안에 키가 없다는 사실을 나타내는 방법이 두 가지가 된다.
키 자체가 없는 경우고
다른 하나는 키는 있지만 그 키가 속이 빈 옵셔널인 경우
쓸데없이 복잡성만 높여서 혼란과 오류 가능성을 키울 뿐이다.

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

API를 쓸모있게 하려면 잘 작성된 문서도 곁들여야 한다.
API를 올바로 문서화하려면 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야 한다.
기본 생성자에는 문서화 주석을 달 방법이 없으니 공개 클래스는 절대 기본 생성자를 사용하면 안된다.
메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 명료하게 기술해야 한다.
how가 아닌 what을 기술해야 한다.
메서드를 호출하기 위한 전제조건(precondition)을 모두 나열해야 한다.
메서드가 성공적으로 수행된 후에 만족해야 하는 사후조건도 모두 나열해야 한다.
일반적으로 전제조건은 @throws 태그로 비검사 예외를 선언하여 암시적으로 기술한다.
@param 태그를 이용해 그 조건에 영향 받는 매개변수에 기술할 수 있다.
부작용도 문서화 해야 한다.
부작용이란 사후조건으로 명확히 나타나지는 않지만 시스템의 상태에 어떠한 변화를 가져오는 것을 말한다.
예컨대 백그라운드 스레드를 시작시키는 메서드라면 그 사실을 문서에 밝혀야 한다.
관례상 @param 태그와 @return 태그의 설명은 해당 매개변수가 뜻하는 값이나 반환값을 설명하는 명사구를 쓴다.
드물게는 명사구 대신 산술 표현식을 쓰기도 한다.
역시 관례상 @param, @return, @throws 태그의 설명에는 마침표를 붙이지 않는다.