상세 컨텐츠

본문 제목

Effective Java 2/E 스터디 2일차

SW/미분류

by 푸로그 2019. 6. 21. 06:15

본문

 

https://www.runnablecode.com/entry/Effective-Java-2E-%EC%8A%A4%ED%84%B0%EB%94%94

 

Effective Java 2/E 스터디

일정 6월 19일 8장 프로그래밍 일반 6월 21일 7장 메소드 6월 25일 6장 열거형과 주석 6월 27일 9장 예외 7월 2일 2장 객체의 생성과 소멸 7월 5일 3장 모든 객체에 공통적인 메소드 7월 9일 4장 클래스와 인터페..

www.runnablecode.com

6월 21일 7장 메소드

7장 메서드 
규칙 38 인자의 유효성을 검사하라 
규칙 39 필요하다면 방어적 복사본을 만들라 
규칙 40 메서드 시그너처는 신중하게 설계하라 
규칙 41 오버로딩할 때는 주의하라 
규칙 42 varargs는 신중히 사용하라 
규칙 43 null 대신 빈 배열이나 컬렉션을 반환하라 
규칙 44 모든 API 요소에 문서화 주석을 달라 

이번장의 고민

인자(parameter)와 반환값(return value)은 어떻게 다루는 것이 좋은가?
메소드의 signature는 어떻게 설계할 것인가?
메소드 문서는 어떻게 만드는것이 바람직한가?
대부분의 내용은 생성자(constructor)에도 그대로 적용된다.
사용성(usability), 안정성(robustness), 유연성(flexibility) 기반 서술

규칙 38 인자의 유효성을 검사하라

대부분의 메소드와 생성자는 인자로 사용 할 수 있는 값을 제한하고 있음 eg. (null, minus value X).

  1. 이러한 제한은 반드시 문서로 남기자
  2. 메서드 시작부분에서 항상 validation 검사
    - 잘못된 인자가 전달되더라도, 예외를 통해 빠르게 문제를 파악하고 해결할 수 있다.

만약 이러한 부분들이 선행되지 않는다면,

  1. 처리 도중 이상한 예외를 내면서 죽는다.
  2. 실행이 제대로 되는 것 같기는 한데 잘못된 결과가 나온다.
    (more worst)> 메소드가 정상적으로 reture되기는 하지만, 어떤 객체의 상태가 비정상적으로 바뀌는 경우 -> (시간과 위치가 바뀌어) 나중에 메서드와는 아무상관도 없는 부분에서 오류가 발생한다.

@public 메서드라면,
문서화 Javadoc의 @throws 이용하여 하자.
보통 IllegalArgumentException, IndexOutOfBoundException, 또는 NullPointerException이 이용된다.

**
 * B함수와 달리 이 함수는 항상 양수의 값을 반환된다.
 * 
 * @param mod 연산을 수행할 값. 반드시 양수.
 * @return this mod m 
 * @throws ArithmeticException (m <= 0 일 때)
 */
public static BigInteger mod(BigInteger m) throws ArithmeticException {
    if (m.signum() <= 0) {
        throw new ArithmeticException("Modules <= 0: " + m);
    }
    ...//계산수행
}

@public이 아닌 메서드라면, 일반적으로 인자의 유효성을 검사할 때 확증문(Assertion)을 사용한다.

클라이언트가 어떻게 이용하건 확증 조건(Assertion condition)은 항상 참이되어야 한다고 강제 하는것.

//재귀적으로 정렬하는 private 도움 함수
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;
}
  • 조건이 만족도지 않으면, AssertionError를 낸다.
  • 통상의 유효성 검사와 달리, 활성화 되지 않은 Assertion은 실행되지 않으므로 비용이 0이다. ?
  • java 인터프리터에 -ea(-enableassertions) 옵션을 줘야함

호출된 메서드에서 바로 이용하진 않지만 나중을 위해 보관되는 인자의 유효성을 검사하는 것은 특히 중요하다. eg. 규칙 18에서 다룬 정적 팩토리 메소드를 보자. Effective Java 2판 127쪽 코드 참조

static List<Integer> intArrayAsList(final int[] a) {
    if ( a == null)
        throw new NullPointerException();
    ...
}

메서드가 실제 계산을 수행하기 전에 그 인자를 반드시 검사해야 한다는 원칙에도 예외는 있다.
유효성 검사를 진행하는 오버헤드가 너무 크거나 비현실적이고, 계산 과정에서 유효성 검사가 자연스럽게 이루어지는 경우다. 예를 들어, Collections.sort(List)

예를 들어, Collections.sort(List)처럼 객체 리스트를 정렬하는 메스드를 생각해보면, 정렬을 하려면 리스트 내의 모든 객체는 서로 비교가 가능해야 한다. 그렇지 않으면 ClassCastException이 발생한다. 이런 암묵적인 검사에 전적으로 의존하다 보면 실패 원자성(failure atomicity)을 잃게 된다(규칙 64).
암묵적 유효성 검사가 문서와 다른 예외를 발생했을 때는 메서드 문서에 명시된 예외로 변환(exception translation)해야 한다.

인자에 제약을 두는 것이 바람직 하다는 이야기가 아니다.

  • 메서드나 생성자를 구현할 때는 받을 수 있는 인자에 제한이 있는지 따져봐야 한다는 것.
  • 그리고 제한이 있다면 그 사실을 문서에 남기고,
  • 메서드 앞부분에서 검사하도록 해야한다. 그러도록 습관을 잘 들여 두는 것이 좋다.

규칙 39 필요하다면 방어적 복사본을 만들라

자바를 사용하기가 편한 것은, 안전한 언어이기 때문이다. 네이티브 메서드를 사용하지 않으면 버퍼 오버런(버퍼 오버플로)나 배열 오버런, 와일드 포인터(잘못된 객체를 참조하는 포인터) 같은 메모리 훼손 오류가 생기지 않는다.

그러나 안전한 언어를 쓴다 해도, 스스로 노력하지 않으면 다른 클래스의 영향으로부터 완전히 자유로울 수는 없다. 따라서 여러분이 만드는 클래스의 클라이언트가 불변식(invariant)을 망가뜨리기 위해 최선을 다할 것이라는 가정하에, 방어적으로 프로그래밍해야 한다. 시스템의 보안을 무너뜨리려는 악의적 사용자가 있다면 분명 그렇겠지만, 실수로 API를 이상하게 사용하는 프로그래머가 있을 수 있다는 점도 고려해야 한다. 그러니 클라이언트가 이상한 짓을 해도 안정적으로 동작하는 클래스를 만들기 위해 노력할 필요가 있다.

https://yimsungjune.blog.me/221153668852 

하지만 방어적 복사본을 만들도록 하면 성능적인 손해를 본다.

규칙 40 메서드 시그너처는 신중하게 설계하라

  1. 메소드 이름은 신중하게 고르자
    최우선 목표는 이해하기 쉬우면서도 같은 패키지 안의 다른 이름들과 일관성이 유지되는 이름을 고르는 것이다. 두 번째 목표는, 좀 더 널리 합의된 사항에도 부합하는 이름을 고르는 것이다. 잘 모르겠다면 자바 라이브러리 API 이름들이 어떻게 지어졌는지 참고하자.

  2. 편의 메서드(convenience method)를 제공하는 데 너무 열 올리지 마라.
    모든 메서드는 "맡은 일이 명확하고 거기에 충실해야" 한다. 클래스에 메서드가 너무 많으면 학습, 사용, 테스트, 유지보수 등의 모든 측면에서 어렵다.
    인터페이스의 경우에는 메서드가 많으면 문제가 두 배는 더 심각하다. 구현하는 사람에게도 그렇고, 사용자에게도 그렇다. 클래스나 인터페이스가 수행해야 하는 동작 각각에 대해서 기능적으로 완전한 메서드를 제공하라.

  3. 인자 리스트를 길게 만들지 마라.
    4개 이하가 되도록 애쓰라. 대부분의 프로그래머는 인자 리스트가 길어지면 제대로 기억하지 못한다.

긴 인자 리스트를 짧게 줄이는 방법은 세 가지다.

  1. 하나는 여러 메서드로 나누는 것이다.
  2. 두 번째 방법은 도움 클래스를 만들어 인자들을 그룹별로 나누는 것이다.
  3. static 멤버 클래스다(규칙 22).
  4. 세 번째 방법은 앞 두 방법을 결합한 것으로, 빌더 패턴을 고쳐서 객체 생성 대신 메서드 호출에 적용하는 것이다(규칙 2)
  1. 인자의 자료형으로는 클래스보다 인터페이스가 더 좋다(규칙 52).
    인터페이스 대신에 클래스를 사용하면 클라이언트는 특정한 구현에 종속된다.
    eg. HashMap을 인자 자료형으로 사용하는 메서드를 만들 이유는 없다는 것이다. Map을 사용하면 된다. 그렇게 하면 해당 메서드는 Hashtable을 인자로 받을 수도 있고, HashMap이나 TreeMap, TreeMap의 하위 자료형, 그리고 심지어는 아직 만들어지지도 않은 모든 Map 하위 클래스 객체를 인자로 받을 수 있게 된다.

  2. 인자 자료형으로 boolean을 쓰는 것보다는, 원소가 2개인 enum 자료형을 쓰는 것이 낫다.

public enum TemperatureScale { FAHRENHEIT, CELSTUS }

규칙 41 오버로딩할 때는 주의하라

아래의 프로그램의 목적은 컬렉션을 종류별로(집합이냐, 리스트냐 아니면 다른 종류의 컬렉션이냐에 따라서) 분류하는 것이다.

// 잘못된 프로그램 - 무엇이 출력되나?
public class CollectionClassifier {
    public static String classify(Set<?> s) {
        return "Set";
    }

    public static String classify(List<?> list) {
        return "List";
    }

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

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

이 프로그램이 Set, List, Unknown Collection을 순서대로 출력하지 않을까 기대하겠지만 그렇지 않다. Unknown Collection을 세 번 출력할 뿐이다. 왜일까? classify 메서드가 오버로딩되어 있으며, 오버로딩된 메서드 가운데 어떤 것이 호출될지는 컴파일 시점에 결정되기 때문이다. 루프가 세 번 도는 동안, 인자의 컴파일 시점 자료형은 전부 Collection<?>으로 동일하다. 각 인자의 실행시점 자료형은 전부 다르지만, 선택 과정에는 영향을 끼치지 못한다.

이 예제 프로그램은 직관과는 반대로 동작한다. 오버로딩된 메서드는 정적으로 선택되지만, 재정의된 메서드는 동적으로 선택되기 때문이다. 재정의된 메서드의 경우, 선택 기준은 메서드 호출 대상 객체의 자료형이다. 객체 자료형에 따라 실행 도중에 결정되는 것이다.

그렇다면 "오버로딩이 혼란스러운 상황"은 정확히 어떤 것인가? 논쟁의 여지가 있는 부분이다. 혼란을 피하는 안전하고 보수적인 전략은, 같은 수의 인자를 갖는 두 개의 오버로딩 메서드를 API에 포함시키지 않는 것이다.

요약하자면, 메서드를 오버로딩할 수 있다고 해서 반드시 그래야 하는 것은 아니다. 인자 개수가 같은 오버로딩 메서드를 추가하는 것은 일반적으로 피해야 한다. 하지만 특히 생성자에 대해서라면, 이 충고를 따를 수 없을 수도 있다. 그럴 때는, 형변환을 추가하면 같은 인자 집합으로 여러 오버로딩 메서드를 호출할 수 있는 상황은 피하는 것이 좋다. 그럴 수 없을 때는, 새로운 인터페이스를 구현하도록 기존 클래스를 개선해야 하는 경우가 그 예인데, 같은 인자를 넘겨 호출했을 떄, 모든 오버로딩 메서드가 똑같이 동작하도록 해야 한다. 그렇지 못하면 개발자들은 오버로딩 메서드나 생성자를 효과적으로 이용하지 못할 것이고, 왜 제대로 동작하지 않는지 이해할 수도 없을 것이다.

규칙 42 varargs는 신중히 사용하라

자바 1.5부터는 공식적으로는 가변 인자 메서드라고 부르는 varargs 메서드가 추가되었다. 이 메서드는 지정된 자료형의 인자를 0개 이상 받을 수 있다.

// varargs의 간단한 사용 예
static int num(int... args) {
    int sum = 0;
    for (int arg : args)
        sum += arg;
    return sum;
}
// 하나 이상의 인자를 받아야 하는 varargs 메서드를 잘못 구현한 사례
static int sum(int... args) {
    if (args.length == 0)
        throw new IllegalArgumentException("Too few arguments");
    int min = args[0];
    for (int i = 1; i < args.length; i++)
        if (args[i] < min)
            min = args[i];
    return min;
}

클라이언트가 인자 없이 메서드를 호출하는 것이 가능할 뿐 아니라, 컴파일 시점이 아니라 실행 도중에 오류가 난다는 것이다. 또 한가지 문제는 보기 흉한 코드라는 것이다.

args의 유효성을 검사하는 코드를 명시적으로 넣어야 하고, min을 Integer.MAX_VALUE로 초기화하지 않는 한 for-each 문을 사용할 수도 없다. 그럴 수 있다고 해도 보기 흉한 코드인 것은 매한가지다.

다행히도, 더 좋은 방법이 있다. 메서드가 인자를 두 개 받도록 선언하는 것이다. 하나는 지정된 자료형을 갖는 일반 인자고, 다른 하나는 같은 자료형의 varargs 인자다. 이 해법은 앞서 살펴본 방법의 모든 문제를 해결한다.

// 하나 이상의 인자를 받는 varargs 메서드를 제대로 구현한 사례
static int min(int firstArg, int... remainingArgs) {
    int min = firstArg;
    for (int arg : remainingArgs)
        if (arg < min)
            min = arg;
    return min;
}

성능이 중요한 환경이라면 varargs 사용에 더욱 신중해야 한다. varargs 메서드를 호출할 때마다 배열이 만들어지고 초기화되기 때문이다.
경험적으로 봤을 때 그런 오버헤드를 감당할 수 잆을 거라 생각된다면 다른 패턴을 따를 수도 있다.
가령 95% 정도는 메서드를 호출할 때 3개 이하의 인자가 전달된다고 해 보자. 그렇다면 아래의 코드처럼 다섯 개의 오버로딩 메서드를 준비하는 것이다.
마지막 메서드는 인자 개수가 3보다 클 때 이용되는 varargs 메서드다.

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% 정도의 호출에 대해서만 새로운 배열이 만들어진다. 대부분의 성능 최적화 기법이 그렇듯 이 방법도 보통은 적절치 않지만, 막상 필요해지면 유용하게 쓰일 것이다.

요약하자면, varargs 메서드는 인자 개수가 기변적인 메서드를 정의할 때 편리하지만, 남용되면 곤란하다. 부적절하게 사용되면 혼란스러운 결과를 초래할 수 있다.

규칙 43 null 대신 빈 배열이나 컬렉션을 반환하라

private final List<Cheese> cheesesInStack = ...;

/**
 * @return 재고가 남은 모든 치즈를 반환. 치즈가 남지 않았을 때는 null을 반환
 */

public Cheese[] getCheeses() {
    if ( cheesesInStack.size() == 0)
        return null;
    ...
}
Cheese[] cheeses = shop.getCheeses();
if (cheeses != null &&
    Arrays.asList(cheeses).contains(Cheese.STILTON))
    System.out.println("Jolly good, just the thing");
if (Arrays.asList(shop.getCheeses()).contains(Cheese.STILTON))
    System.out.println("Jolly good, just the thing");

빈 배열이나 컬렉션을 반환하는 대신 null을 반환하는 메서드를 사용하면 이런 상황을 겪게 된다. 이런 메서드는 오류를 쉽게 유발한다. 클라이언트가 null 처리를 잊어버릴 수 있기 때문이다.

배열 할당 비용은 피할 수 있으니 null을 반환해야 바람직한 것 아니냐는 주장도 있을 수 있으나, 이 주장은 두 가지 측면에서 틀렸다.

첫 번째는 프로파일링 결과로 해당 메서드가 성능 저하의 주범이라는 것이 밝혀지지 않는 한, 그런 수준까지 성능 걱정을 하는 것은 바람직하지 않다는 것(규칙 55).

두 번째는, 길이가 0인 배열은 변경이 불가능하므로 아무 제약없이 재사용할 수 있다는 것이다(규칙 15).

 // 컬렉션에서 배열을 만들어 반환하는 올바른 방법
private final List<Cheese> cheesesInStack = ...;

private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

/*
 * @return 재고가 남은 모든 치즈 목록을 배열로 만들어 반환
*/
public Cheese[] getCheeses() {
    return cheesesInStack.toArray(EMPTY_CHEESE_ARRAY);
}

위 에서 toArray 메서드에 전달되는 빈 배열 상수는 반환값의 자료형을 명시하는 구실을 한다. 보통 toArray는 반환되는 원소가 담긴 배열을 스스로 할당하는데, 컬렉션이 비어 있는 경우에는 인자로 주어진 빈 배열을 쓴다. 그리고 Collection.toArray(T[])의 명세를 보면, 인자로 주어진 배열이 컬렉션의 모든 원소를 담을 정도로 큰 경우에는 해당 배열을 반환값으로 사용한다고 되어 있다. 따라서 위의 숙어대로 하면 빈 배열은 절대로 자동 할당되지 않는다.

마찬가지로, 컬렉션을 반환하는 메서드도 빈 컬렉션을 반환해야 할 때마다 동일한 변경 불가능 빈 컬렉션 객체를 반환하도록 구현할 수 있다. Collections, enumSet, emptyList, emptyMap 메서드가 그런 용도로 사용된다. 다음의 예제를 보자.

요약하자면, null 대신에 빈 배열이나 빈 컬렉션을 반환하라는 것이다. null 값을 반환하는 것은 C 언어에서 전해진 관습으로, C에서 배열의 길이가 배열과 따로 반환된다. C에서는 길이 0인 배열을 할당해서 반환하더라도 아무 이득이 없다.

규칙 44 모든 API 요소에 문서화 주석을 달라

좋은 API 문서를 만들려면 API에 포함된 모든 클래스, 인터페이스, 생성자, 메서드, 그리고 필드 선언에 문서화 주석을 달아야 한다.
직렬화가 가능한 클래스라면 직렬화 형식도 밝혀야 한다(규칙 75).

문서화 주석이 달려 있지 않을 경우, Javadoc은 해당 요소가 어떻게 선언됐는지만 문서로 남긴다. 문서화 주석이 없는 API를 사용하는 것은 피곤한 일일 뿐더러 오류 발생 가능성도 높다.

아울러, 유지보수가 쉬운 코드를 만들려면 API가 아닌 클래스나 인터페이스, 생성자, 메서드, 필드에 대해서도 문서화 주석을 남겨야 한다.

메서드에 대한 문서화 주석은 메서드와 클라이언트 사이의 규약을 간명하게 설명해야 한다.

계승을 위해 설계된 메서드가 아니라면(규칙 17)?
메서드가 무엇을 하는지 설명해야지 메서드가 어떻게 그 일을 하는지를 설명해서는 안 된다.
아울러 문서화 주석에는 해당 메서드의 모든 선행조건과 후행조건을 나열해야 한다.

  • 선행조건은 클라이언트가 메서드를 호출하려면 반드시 참이 되어야 하는 조건들이다.

  • 후행조건은 메서드 실행이 성공적으로 끝난 다음에 만족되어야 하는 조건들이다.

문서화 주석에 있을지 모르는 오류를 줄이는 간단한 방법은, Javadoc 실행 결과 만들어진 HTML 파일들을 HTML 유효성 검사 도구로 검사해 보는 것이다.

문서화 주석에 관해서, 마지막으로 한 가지 주의사항만 더 살펴보자. 모든 공개 API 요소에는 문서화 주석을 달 필요가 있지만, 항상 그 정도면 충분하진 않다. 관련된 클래스가 많아서 복잡한 API의 경우, API의 전반적인 구조를 설명하는 별도 문서가 필요한 경우가 많다. 그런 문서가 있다면, 관련 클래스나 패키지의 문서화 주석에는 해당 문서로 연결되는 링크가 있어야 한다.

 

관련글 더보기

댓글 영역