Rule 28 - 한정적 와일드카드를 써서 API 유연성을 높여라

전제

List<Number>List<Integer> 간에는 부모-자식 관계가 성리될 수 없다. 즉 캐스팅이 불가능하다. 하지만 논리적(상식적)으로 두 Collection 간 putAll과 같은 메소드는 호출 가능해야 할 것 같다.

위와 같은 문제를 해결하고자 한정적 와일드카드 자료형(bounded wildcard type) 이 존재한다.

알아보자

example 한정적 와일드카드 자료형

// E 객체 생산자 역할을 하는 인자에 대한 와일드 카드 자료형
public void putAll(Iterable<? extends E> src) {
    for (E e : src)
        push(e);
}
// E의 소비자 구실을 하는 인자에 대한 와일드카드 자료형
public void popAll(Collection<? super E> dst) {
    while(!isEmpty())
        dst.add(pop());
}

PECS (Produce - Extends, Consumer - Super)

Stack 예제에서 pushAll의 인자 src는 스택이 사용할 E형의 객체를 만드는 생산자 이므로 src의 자료형은 Iterable<? extends E>가 되어야 한다. popAll의 dst는 Stack에 보관된 객체를 소비하므로 dst의 자료형은 Collection<? super E>가 되어야한다.

example

public static <T extends Comparable<? super T>> T max(List <? extends T> list)

Compareable의 경우 부모의 compare를 사용할 수 있으므로

나프탈린과 와들러는 이를 Get(Consumer) and Put(Produce) Principle이라고 표현한다.

반환값에는 와일드카드 자료형을 쓰면 안된다.

좀 더 유연한 코드를 만들 수 있도록 도와주기는 커녕, 클라이언트 코드 안에도 와일드카드 자료형을 명시해야 하기 때문이다.

명시적 형인자(explicit type parameter)

컴파일러가 자료형을 명확하게 유추하지 못할 경우 사용

before

Set<Integer> integers = ...;
Set<Double> doubles = ...;
Set<Number> numbers = Union.union(integers, doubles);

after

// 명시적 형인자 .<Number> 적용
Set<Number> numbers = Union.<Number>union(integers, doubles);

비한정 와일드카드 자료형 포착 helper

before

public static void swap(List<?> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
    // 하지만 컴파일 오류 발생. <?>이기 때문에 null이외에는 값을 넣을 수가 없음
    // 해결하려면 명시적으로 타입을 지정해주거나 포착(capture)할 수 있게 해줘야함
}

after

public static void swap(List<? list, int i, int j) {
    swapHelper(list, j, i);
}

// 와일드카드 자료형을 포착하기 위한 private 도움 메서드
private static <E> void swapHelper(List<E> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

결론

  • API에는 와일드카드 자료형을 사용해서 유연성을 높여라 - 필수다
  • PECS
  • Comparable, Comparator는 모두 소비자이다 - <? super T>

Rule 29 - 형 안전 다형성 컨테이너를 쓰면 어떨지 따져보라

example

// 형 안전 다형성(heterogeneous)컨테이너 패턴
public class Favorites {
    Map<Class<?>, Object> favroites = new HashMap<Class<?>, Object>();

    public <T> void putFavorite(Class<T> type, T instance) {
        if (type == null)
            throw new NullPointerException("Type is null");
        // before 타입안정성 사라짐
        // favorites.put(type, instance);
        // after 보완코드
        favorites.put(type, type.cast(instance));
    }

    public <T> T getFavorite(Class<T> type) {
        // 타입안정성 복구 - 동적 형변환
        return type.cast(favorites.get(type));
    }
}

형 안전 다형성 컨테이너 패턴의 문제점

  1. Class(raw type)를 인자로 넘기면 형이 지정되지 않았기 때문에 형 안정성이 깨질 수 있음
    1. favorites.put(type, type.cast(instance)); 로 커버
  2. 실체화 불가능 자료형에는 쓰일 수 없음. List<String>.class는 존재할 수 없음
    1. 실질적으로 완벽한 솔루션은 없음
    2. 외부 라이브러리에 ParameterizedType과 같은 헬퍼가 있긴 하나 라이브러리 별로 종속적임

asSubClass

실행시점에 특정한 Class객체를 인자로 주어진 하위클래스의 Class객체로 형변환 해준다.

  • 성공하면 Class 객체 반환
  • 형변환이 불가능하면 ClassCastException 발생

결론

컨테이너 대신 키를 제네릭으로 만들면 형인자 개수가 고정되는 제약없는 형 안전 다형성 컨테이를 만들 수 있다. 그런경우 Class 객체(자료형 토큰)를 키로 쓰면되며, Column<T>와 같이 직접 구현할 수도 있다.

간단/명료 하게 작성하도록 하자.

Rule 23 - 새 코드에는 무인자 제네릭 자료형을 사용하지 마라

타입 선언부에 형인자(Type parameter)가 포함되어 있으면 제네릭 타입이라 부른다.

마찬가지로 메소드 선언부에 Type parameter이 포함되어 있으면 제네릭 메서드라 부른다.

용어정리

  • Type parameter : 형인자 - < >
    • Formal type parameter : 형식 형인자 - <T>
    • Actual type parameter : 실 형인자 - <String>
  • Parameterized type : 형인자 자료형 - List<T>
  • Raw type : 무인자 자료형 - List
  • Unbounded wildcard type : 비한정적 와일드카드 자료형 - List<?>
  • Bounded wildcard type : 한정적 와일드카드 자료형 - List<? extends String>
  • Bound type parameter : 한정적 형인자 - <E extends String>

목적

형인자 선언을 통해서 컴파일 타임에 Type safety(형 안정성)을 확보하기 위함

고로 무인자 자료형(Raw type)을 쓰면 형 안정성이 사라지고, 제네릭의 장점 중 하나인 표현력(expressiveness) 측면에서 손해를 보게 된다.

List, List<Object>, List<?> 구분

  • List : 모든 타입 추가 가능 안전하지 않음
  • List<Object> : 모든 타입 추가 가능
    • 하지만 List<String>와 호환되지 않음 - 캐스팅 불가능
  • List<?> : 비한정적 와일드카드 자료형(unbounded wildcard type)
    • null 이외에 어떤 원소도 넣을 수 없음(어떤 자료형이 있는 건데 어떤 자료형이 담긴지는 알 수가 없음)
  • List<T extends String> : 한정적 와일드카드 자료형(bounded wildcard type)
    • String 포함 String 상속 받는 타입 호환
    • List<String>와 호환됨 - 캐스팅 가능

instanceof 사용법

// instanceof 연산자에는 무인자 자료형을 써도 OK
if (o instanceof Set) {         // 무인자 자료형
    Set<?> m = (Set<?>) o;      // 와일드카드 자료형
}

Rule 24 - 무점검 경고(unchecked warning)를 제거하라.

example : 무점검 경고

Set<Lark> exaltation = new HashSet();

/* 컴파일경고
    ???.java:4: warning: [unchecked] unchecked conversion
    found : HashSet, required: Set<Lark>
    Set<Lark> exaltation = new HashSet();
*/

모든 무점검 경고는, 절대로 무시하지 말아야한다. 가능한 없애야한다.

  • Type safety
  • No ClassCastException

단, 제거할 없는 경고 메세지는 형 안정성이 확실할 때문 @SupressWarnings("unchecked") 어노테이션을 사용해 억제하기 바란다. 가능하면 작은 범위에 적용하라. 그리고 해당 경고를 억제한 이유를 주석으로 표현하라.

Rule 25 - 배열 대신 리스트를 써라

배열과 리스트(Collection)의 차이

  • 배열 : 공변 자료형(covariant)
    • Sub extends Super이면 Super[] supers = new Sub[] 가능
  • 리스트 : 불변 자료형(invariant)
    • Sub extends Super이면 List<Super> <-> List<Sub>간 형변환 불가능
    • 하지만 List<Super> -> List<? extends Sub>으로 형변환 가능

이것 이외에도 반공변 자료형(contravariant)도 있음.

  • contravariant : Child extends Parent이면 Parent[] objs = new Child[] 가능. <T super Child>
  • convariant : <T extends Parent>

참고 : https://msdn.microsoft.com/ko-kr/library/dd799517(v=vs.110).aspx

하지만 취약한 것은 배열이다. 역시 불변식이 좋음

리스트의 장점

1. 컴파일 시 타입안정성 확보

본 서적에서는 reification실체화 로 표현했으나 이것은 realization 이랑 헷갈리고 개인적으로 판단할 시 개념과도 맞지 않아서 구체화 로 표현함

// 실행 시 예외 발생 (컴파일 성공)
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in";  // ArrayStoreException 발생

// 컴파일 되지 않음
List<Object> ol = new ArrayList<Long>();    // 자료형 불일치
ol.add("I don't fit in");

2. 배열은 구체화(reification) 자료형

배열의 자료형은 실행시간에 결정된다. 하지만 List는 안됨

example

String strs = new String[] {"str1", "str2"}

구체화 불가능 자료형(non-refiable types)

실행시점(runtime)에 해당 자료형의 모든 정보를 이용할 수 없는 자료형(a type this is not completely available at runtime)이라는 뜻이라는데 명확하게 이해가 안되었으나, 이제는 이해가 되는 거 같다. 선지식으로 Java가 Generic을 구현할 시 내부적으로 타입제거(Type erasure) 를 이용하는 것이다. List<String>의 경우 컴파일 시에는 List<String> 이지만 런타임 시에는 List가 된다. 즉. 런타임 시에는 Parameterized type 정보인 <String>가 없어지는 타입제거(Type erasure)가 된 후이기 때문에 해당 제네릭 정보를 알 수 없고 이것이 바로 타입의 모든 정보를 런타임 시에 알 수 없는 것이다.

E, List<E>, List<String>와 같은 자료형은 구체화 불가능(non-refiable) 자료형으로 알려져 있다. - 반대로 primitive, non-generic 타입, raw 타입, 비한정 와일드카드형 타입(<?>)은 구체화 가능(refiable) 자료형이다.

참고 : https://docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html

결론

구분 Type 구 체화 형안정성
배열 공변자료형 가능 실행타임에 보장
제네릭 불변자료형 불가능 컴파일타임에 보장

배열과 제네릭을 뒤섞어 쓰다가 컴파일 오류나 경고메시지를 만나게되면, 배열을 리스트로 바꿔야겠다는 생각이 본능적으로 들어야 한다.

Rule 26 - 가능하면 제네릭 자료형으로 만들 것

example

// 제네릭 사용해 작성한 최초 Stack 클래스 - 컴파일 되지 않는다.
public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new E[DEFAULT_INITIAL_CAPACITY];
        // 컴파일오류
        // Stack.java:8: generic array creation
        //     elements = new E[DEFAULT_INITIAL_CAPACITY];
    }

    public void pusht(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public E pop() {
        if (size = 0)
            throw new EmptyStackException();
        E result = elements[--size];
        elements[size] = null;  // 만기 참조 제거
        return result;
    }
    ...
}

배열을 사용하는 제네릭 자료형을 구현할 때 실체화 불가능한 자료형(E)으로 배열을 생성할 수 없는 경우 해결책

1. 제네릭 배열을 만들 수 없다는 조건을 우회하는 것이다.

배열 객체 생성 시 Object 배열을 만들어서 제네릭 배열 자료형으로 형변환(cast)하는 방법

example

// elements 배열에는 push(E)를 통해 전달된 E 형의 객체만 저장된다.
// 이 정도면 형 안정성은 보장할 수 있지만, 배열의 실행시간 자료형은 E[]가 아니라 항상 Object[] 이다.
@SuppressWarnings("unchecked")
public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}

2. 배열의 자료형을 E[]에서 Object[]로 바꾸는 것이다.

example

private Object[] elements;
...
public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
...
public E pop() {
    if (size = 0)
        throw new EmptyStackException();
    // 자료형이 E인 원소만 push하므로, 아래의 형변환은 안전하다.
    @SuppressWarnings("unchecked") E result = elements[--size];
    elements[size] = null;  // 만기 참조 제거
    return result;
}

제네릭 배열 해결책 정리

  1. 무점검 형변환(unchecked cast) 경고 억제의 위험성은 스칼라(scala) 자료형 보다 배열 자료형이 더 크기 때문에, 두번째 해법이 더 낫다고 볼 수 있다.
  2. 하지만 현실적으로 배열을 사용하는 코드가 여러군데라면 배열 생성 시 첫번째 방법으로는 E[]으로 한번만 형변환을 하면되지만, 두번째 방법으로는 코드 여기저기에서 E로 형변환을 해야하므로 첫번째 방법이 더 낫다

결론

가능하면 하위호환성을 유지하면서 제네릭 자료형으로 리팩터링하라.

Rule 27 - 가능하면 제네릭 메서드로 만들 것

패턴

  • 일반적인 사용
  • 제네릭 정적 팩터리 메서드 (1.6 이하)
  • 제네릭 싱글턴 팩터리 패턴
    • Collections.emptyList()
  • 재귀적 자료형 한정(recursive type bound)
    • <T extends Compareable<T>> T max(List<T> list)

결론

  • 제네릭 메서드는 클라이언트의 cast 비용을 줄이고 타입안정성도 높다.
  • 시간 날 때 기존 메서드를 제네릭 메서드로 확장해 놓으면 기존 클라이언트 코드를 깨지 않고도 새 사용자에게 더 좋은 API를 제공할 수 있다.

Rule 18 - 추상 클래스 대신 인터페이스를 사용하라.

자바에서 지원하는 2가지 일반화 방법

  • 인터페이스 : 행위만 존재 (추상)
  • 추상클래스 : 일부 구현 가능 (추상 + 구체)

인터페이스는 믹스인을 정의할 때도 좋다. 즉, 주 자료형(primary type) 이외에 추가로 구현할 자료형(행위)으로 선택적 기능을 제공한는 사실을 선언할 수 있다. 물론 주 자료형은 클래스(or 추상 클래스)이거나 또 다른 인터페이스 일 것이다.

example : Comparable 비교를 위한 인터페이스

계층적인 것은 클래스 기반으로 어울리며, 비 계층 적인 것은 인터페이스가 어울리다. 물론 인터페이스는 계층적인 표현에도 어울린다.

추상 구현체

추상 골격 구현(abstract skeleta implementation) 클래스를 중요 인터페이스마다 두면, 인터페이스의 장점과 추상 클래스의 장점을 결합할 수 있다.

  • naming rule : AbstractInterface

example 추상클래스를 통한 구상클래스

// 골격 구현 위에서 만들어진 완전한 List 구현
static List<Integer> intArrayAsList(final int [] a) {
    if (a == null) {
        throw new NullPointerException();
    }
    return new AbstractList<Integer>() {
        public Integer get(int i) {
            return a[i];    // 자동객체화 - Auto Boxing (규칙 5)
        }

        @Override
        public Integer set(int i, Integer val) {
            int oldVal = a[i];
            a[i] = val;     // 자동비객체화 - Auto unboxing
            return oldVal;  // 자동객체화
        }

        public int size() {
            return a.length;
        }
    }
}

int배열을 Integer배열로 만드는 Adapter 겸 정적 팩터리 예제

중요한 것은 골격 구현체가 있으면 인터페이스 전체가 아니라 일부를 구현할 수 있게 도움이 된다. 물론 골격 구현도 계승(상속)을 기반으로 하기 때문에 골격구현 관련해서 문서화를 통해서 구현 가이드를 제공해야한다.

골격 구현체를 뛰어 넘어서 기본 간단 구현체를 제공하는 방법도 있다.

  • example : AbstractMap.SimpleEntry

결론

적절하게 인터페이스와 추상클래스를 일반화의 도구로 사용해야한다. 그리고 각각의 장단점도 이해애햐한다.

  • 인터페이스 : 유연
  • 추상클래스 : 확장

추상타입은 한 번 API에 포함되면 수정 비용이 매우 크거가 수정할 수 없으므로 설계에 주의한다. 하지만 더 유연한 인터페이스를 사용하고 그 인터페이스의 구현 편의를 돕는 추상 골격 클래스나 기본 간단 구현체로 지원하자.

Rule 19 - 인터페이스는 자료형을 정의할 때만 사용하라

인터페이스는 타입이다.

인터페이스 안티패턴

상수 인터페이스 : 인터페이스 내 상수만 정의된 것

  • 인터페이스는 행위에 대한 표현을 나타낼 뿐(그런 타입)이지 상수를 표현하기에는 적합하지 않다.
  • 개선안 : 상수 유틸리티 클래스 : 하지만 이것도 적절한 방법은 아니다. 개인적으로 enum을 추천한다.

Rule 20 - 태그 달린 클래스 대신 클래스 계층을 활용하라.

SRP 원칙을 상기하자

클래스의 여러개의 역할이나 기능을 담지 말고 하나의 기능을 담도록 하자.

태그 달린 클래스 - 안티패턴

example

// 태그 달린 클래스
class Figure {
    enum Shape {RECTANGLE, CIRCLE};

    // 어떤 모양인지 나타내는 태그 필드
    final Shape shape;

    // 태그가 RECTANGLE일 때만 사용되는 필드들
    double length;
    double width;

    // 태그가 CIRCLE일 때만 사용되는 필드들
    double radius;

    // 원을 만드는 생성자
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    // 사각형을 만드는 생성자
    Figure(double length, double width) {
        shape = CIRCLE;
        this.length = length;
        this.width = width;
    }

    double area() {
        switch(shape) {
            case RETANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError();
        }
    }
}

태그 기반 클래스는 너저분한데다 오류 발생가능성이 높고, 효율적이지도 않다. 그거 클래스 계층을 모아서 분기로 처리한 것 뿐이다.

태그 클래스를 클래스 계층 으로 변경 - 추천

example

// 태그기반 클래스를 클래스 계층으로 변환한 결과
abstract class Figure {
    abstract double area();
}

class Circle extends Figure {
    final double radius;

    Circle(double radius) { this.radius = radius; }

    double area() { return Math.PI * (radius * radius); }
}

class Rectangle extends Figure {
    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    double area() { return length * width; }
}

장점

  • 단순하고 명료
  • 완벽하게 역할 분리 (SRP 원칙 만족)
  • 의미없이 반복적인 상투적인(boilerplate) 코드가 없어짐
  • 확장이 용이함

example 클래스의 다형성 기반 확장

// 정사각형으로 확장
class Share extends Rectangle {
    Square(double side) {
        super(side, side);
    }
}

결론

태그 기반 클래스 사용은 피해야한다.

Rule 21 - 전략을 표현하고 싶을 때는 함수 객체를 사용하라.

함수객체

  • 함수 성격의 하나뿐인 메서드를 가진 객체
  • 대부분 상태가 없음 (stateless)

example

class StringLengthComparator {
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

example : 함수 객체 전략 인터페이스

// 전략 인터페이스
public interface Comparator<T> {
    public int compare(T t1, T t2);
}

// 구상클래스
class StringLengthComparator implements Comparator<String> {
    ...
}

만약 전략 인터페이스를 익명 클래스로 사용하면 의미없는 객체가 반복될 수 있으므로 정적인 필드(private static final)를 고려해보자

example : 익명 클래스

Array.sort(stringArray, new Comparator<String>() {
   public int compare(String s1, String s2) {
       return s1.length() - s2.length();
   }
});

example : 전략 정적 클래스

// 실행 가능한 전략들을 외부에 공개하는 클래스
class Host {
    private static class StrLenCmp implements Comparator<String>, Serializable {
        public int compare(String s1, String s2) {
            return s1.length() - s2.length();
        }
    }

    // 이 비교자는 직렬화가 가능
    public static final Comparator<String> STRING_LENGTH_COMPARATOR =
            new StrLenCmp();
    // 다른 전략들...
}

결론

  • 함수 객체의 주된 용도는 전략 패턴(Strategy pattern)을 구현하는 것
  • 전략을 표현하는 인터페이스를 선언하고, 실행 가능 전략 클래스를 구현한다.
  • 만약 전략 클래스가 반복적으로 사용된다면 private static 맴버 클래스로 전략을 표현한 다음, public static final 필드를 통해서 외부에 공개하는 것이 바람직하다.
    • 그냥 바로 public static final 필드를 통해서 구현 + 외부 공개까지 한 번에 하는 것이 더 낫지 않은가?

Rule 22 - 맴버 클래스는 가능하면 static으로 선언하라.

중첩 클래스(nested class)의 4가지 종류

정적 맴버 클래스 (static member class)

클래스 안에 선언된 일반 클래스

example : Calculator.Operation

비-정적 맴버 클래스 (nonstatic member class)

outer class의 객체를 참조할 수 있음 - [1]this한정구문(qualified this)구문을 통해서

example

// 비-정적 맴버 클래스의 전형적인 용례
public class MySet<E> extends AbstractSet<E> {
    // ...
    public Interator<E> iterator() {
        return new MyItefator();
    }

    private class MyIterator implements iterator<E> {
        // ...
    }
}

exampe : Map.keySet, Map.entrySet

바깥 클래스 객체에 접근할 필요가 없으면 정적 맴버 클래스로 만들자

비정적 맴버 클래스가 되면 해당 객체는 내부적으로 바깥 객체에 대한 참조를 유지하게 되므로, 각종 시간 공간 요구량이 늘어나게 되면 GC가 힘들어진다.

익명 클래스 (anonymous class)

  • 이름이 없다
  • 바깥 클래스의 맴버도 아니다.
  • 사용하는 순간에 선언하고 객체를 만든다.
  • 코드 어디에서나 사용할 수 있다.
  • 비-정적(nonstatic context)안에서 사용될 때는 바깥 객체를 갖는다.
  • 정적 문맥(static context) 안에서 사용된다 해도 static 맴버를 가진 수는 없다.
  • 여러 인터페이스를 구현하는 익명 클래스는 선언할 수 없다.
  • 표현식 중간에 등장하므로, 10줄 이하로 짧게 작성되어야 가독성이 좋아진다.
  • 함수 객체를 정의할 때 특히 많이 쓰인다.

지역 클래스 (inner class)

  • 지역 변수로 클래스를 선언한 것이다.

[1]this한정구문(qualified this)

this한정구문은 바깥 객체를 참조하기 위해서 this 앞에 바깥 객체의 자료형의 이름을 붙이는 것을 말한다.

class Envelope {
    void x() {
        System.out.println("Hello");
    }
    class Enclosure {
        void x() {
            Envelope.this.x();  // 한정됨
        }
    }
}

결론

  1. 맴버클래스 : 중첩 클래스를 메서드 밖에서 사용할 수 있어야 하거나, 메서드 안에 놓기에는 너무 길 경우
    1. 비-정적 맴버 클래스 : 바깥 객체에 대한 참조를 가져야하는 경우
    2. 정적 맴버 클래스 : 바깥 객체에 대한 참조가 필요없는 경우
  2. 익명 클래스 : 중첩 클래스가 특정한 메서드에 속해야 하고, 오직 한 곳에서만 객체를 생성하며, 해당 중첩 클래스의 특성을 규정하는 자료형이 이미 있다면(인터페이스, 추상클래스) 사용
  3. 지역 클래스 : 익명 클래스를 이용하지 않을 경우

Spring MVC Test를 위해서 com.jayway.jsonpath.json-path(jsonPath 메서드) 라이브러리를 사용할 시 아래와 같은 오류가 발생하는 경우가 있습니다.

java.lang.NoSuchMethodError: com.jayway.jsonpath.JsonPath.compile(Ljava/lang/String;[Lcom/jayway/jsonpath/Filter;)Lcom/jayway/jsonpath/JsonPath;

상세오류로그는 아래 참조

확인결과 JsonPath 라이브러리와 Spring MVC Test와 버전 호환성에 이슈가 있어서 관련 표를 정리해봅니다.

java버전 1.8 기준으로 테스트했습니다.

  • 2016-02-27 기준
  • json-path 0.8.1 이상
  • spring-test 3.2.0 이상 (spring-mvc test 최초 릴리즈가 3.2 부터임)

정리도표

spring-test json-path 비고
4.2.5.RELEASE 0.8.1, 0.9.0, 1.0.0, 1.2.0, 2.0.0, 2.1.0  
4.1.9.RELEASE 0.8.1, 0.9.0, 1.0.0, 1.2.0, 2.0.0, 2.1.0  
4.1.2.RELEASE 0.8.1, 0.9.0, 1.0.0, 1.2.0, 2.0.0, 2.1.0 4.1.2.RELASE 이상 모두 성공
4.1.1.RELEASE 0.8.1, 0.9.0  
4.1.0.RELEASE 0.8.1, 0.9.0  
4.0.9.RELEASE 0.8.1, 0.9.0  
4.0.0.RELEASE 0.8.1, 0.9.0  
3.2.16.RELEASE 0.8.1, 0.9.0  
3.2.2.RELEASE 0.8.1, 0.9.0 3.2.2.RELEASE부터 0.9.0 이하 성공
3.2.1.RELEASE   모두 실패
3.2.0.RELEASE   모두 실패

테스트코드

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = "classpath:root-context.xml")
public class JsonControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void setUp() throws Exception {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
                .alwaysDo(print())
                .build();
    }

    @Test
    public void getNameCard() throws Exception {
        this.mockMvc.perform(get("/namecards/{0}", 1L))
                .andExpect(status().isOk())
                .andExpect(content().contentType("application/json;charset=UTF-8"))
                .andExpect(jsonPath("$.seq", is(1)))
                .andExpect(jsonPath("$.name", is("정명주")))
                .andExpect(jsonPath("$.company", is("red")))
                ;
    }
}

상세오류로그

java.lang.NoSuchMethodError: com.jayway.jsonpath.JsonPath.compile(Ljava/lang/String;[Lcom/jayway/jsonpath/Filter;)Lcom/jayway/jsonpath/JsonPath;

	at org.springframework.test.util.JsonPathExpectationsHelper.<init>(JsonPathExpectationsHelper.java:54)
	at org.springframework.test.web.servlet.result.JsonPathResultMatchers.<init>(JsonPathResultMatchers.java:43)
	at org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath(MockMvcResultMatchers.java:146)
	at io.redutan.jsonpath.controller.JsonControllerTest.getNameCard(JsonControllerTest.java:47)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:88)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:119)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

JsonPath Version https://github.com/jayway/JsonPath/blob/master/changelog.md

Spring Version http://mvnrepository.com/artifact/org.springframework/spring-core

Rule 13 - 클래스와 맴버의 접근 권한은 최소화하라

중요한 것은 외부로 제공되는 인터페이스이지, 내부 구현이 아니다.

  • 정보은닉
  • 캡슐화

정보은닉

  • 의존성을 낮춘다.
  • 재사용성을 높인다.
  • 개발과정의 위험성을 낮춘다.
  • 단순함(복잡하지 않음)

정보은닉을 위한 대원칙 : 각 클래스와 맴버는 가능한 접근불가능하도록 만든다

  • 소프트웨어의 정상적인 동작을 보증하는 한도 내에서 가장 낮은 접근 권한을 설정

Java의 접근제어 매커니즘 : 권한수정자(접근자)

  • public : 제한없음
  • protected : 같은 패키지와 하위클래스
  • package(default) : 같은 패키지
  • private : 클래스내부

유의사항

protected의 경우 public과 마찬가지로 외부 공개이므로 수정에 비용이 매우 크다. 고로 protected도 가능하면 사용을 자제해야한다. private와 package는 외부 노출이 안된다.

객체 필드(instance field)는 절대로 public 으로 선언하면 안된다.

  • 불변식 강제가 안됨
  • ThreadSafe하지 않음

public static final 배열필드를 두거나, 배열 필드를 반환하는 접근자(accesor)를 정의하면 안된다.

  • 클라이언트가 배열 내용을 변경할 수 있어서 캡슐화가 깨진다.

example

// 보안 문제를 초래할 수 있는 코드
public static final Thing[] VALUES = { ... };

솔루션

  1. 변경불가 객체로 반환
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
    Collections.unmodifiableList(Array.asList(PRIVATE_VALUES));
  1. 방어복사 반환
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
    return PRIVATE_VALUES.clone();
}

결론

  • 접근 권한은 가능한 낮추라.
  • 최소한의 public API를 설계한다.
    • 다른 모든 클래스, 인터페이스, 맴버는 API에서 제외하라.
  • public static final 필드를 제외한 어떤 필드도 public으로 선언하지 마라.
  • public static final 필드가 참조하는 객체는 불변객체로 만들라.

Rule 14 - public 클래스 안에는 public 필드를 두지 말고 접근자메서드(Setter)를 사용하라.

// 이런 저급한 클래스는 절대로 public으로 선언하지 말 것
class Point {
    public double x;
    public double y;
}

문제점

  • 캡슐화의 이점을 누릴 수가 없음
  • API에 필드가 포함되어 있어서 수정 비용이 매우 큼
  • 불변식을 강제할 수 없음 - private final

솔루션

  • getter와 setter(가변객체라면)로 제공한다.
  • 기본적으로 필드는 캡슐화 하자.
// getter과 setter를 이용한 데이터 캡슐화
class Point {
    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() { return x; }
    public double getY() { return y; }
    public void setX(double x) { this.x = x; }
    public void setY(double y) { this.y = y; }
}

public, protected로 선언된 클래스에는 getter를 제공하라.

하지만, package 클래스, private 중첩 클래스는 데이터 필드를 public으로 제공할 수 있다.

  • 어짜피 클래스 자체가 외부로 공개되어 있지 않기 때문에 클래스를 기반으로 충분히 캡슐화의 이점을 누릴 수 있다.

결론

가능하면 public 클래스의 필드는 외부로 공개하지 않는다. package 클래스, private 중첩 클래스의 필드는 외부로 공개하는 것이 나을 수도 있다.

Rule 15 - 변경가능성을 최소화하라

불변객체(값객체)를 사용하라.

장점

  • 설계하기 쉽다.
  • 구현하기 쉽다.
  • 사용하기 쉽다.
  • 오류가능성이 적다.
  • 안전하다. - threadsafe

불변객체 5규칙

  1. setter를 제공하지 않는다.
  2. 계승(상속)할 수 없도록 한다.
    1. final class
    2. private 생성자
  3. 모든 필드를 final로 선언한다.
  4. 모든 필드를 private로 선언한다.
  5. 변경 가능 컴포넌트에 대한 독점적 접근권을 보장한다.
    1. 생성자, 접근자, readObject(규칙 76)안에서는 방어복사로 설정한다.

example

public final class Complex {
    private final double re;
    private final double im;

    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    // 대응되는 수정자가 없는 접근자들
    public double realPart() { return re; }
    public double imageinaryPart { return im; }

    public Complex add(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }

    public Complex subtract(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }

    @Overrice
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if(!(o instanceof Complex))
            return false;
         Complex c = (Complex) o;

         return Double.compare(re, c.re) == 0 &&
                Double.compare(im, c.im);
    }

    @Override
    public int hashCode() {
        int result = 17 + hashDouble(re);
        result = 31 * result + hashDouble(im);
        return result;
    }

    private static int hashDouble(double val) {
        long longBits = Double.doubleToLongBits(val);
        return (int) (longBits ^ (longBits >>> 32));
    }

    @Override
    public String toString() {
        return "(" + re + " + " + im + "i)";
    }
}

장점상세

  • 단순하다.
  • 스레드에 안전하다. 동기화도 필요없다.
  • 자유롭게 공유할 수 있다.
  • 내부도 공유할 수 있다.
    • 객체 내부의 일부 상태값을 공유함
  • 다른 객체의 구성요소로 훌륭하다.

단점

  • 항상 값마다 별도의 객체를 생성해야한다.
    • 가능한 기본타입(primitive)로 제공한다.
    • 가변 헬퍼 클래스 제공 - ex) String(불변)과 StringBuilder(가변)
  • 방어복사 기법이 요구됨

유의사항

불변객체는 정적 팩터리 생성자 를 이용하면 좋은 점이 많다

  • 상속불가
  • 객체 자체 캐싱기능 추가 가능

일부 non-final 객체를 내부에 설정해서 응용해도 된다 (캐시 등)

  • 모든 필드가 final 일 필요는 없다.

직렬화(Serialization) 구현 시 주의해야한다

결론

  • 변경 가능한 클래스로 만들 타당한 이유가 없다면, 반드시 변경 불가능 클래스로 만들어야 한다.
  • 변경 불가능한 클래스로 만들 수 없다면, 변경 가능성을 최대한 제한하라.
  • 특별한 이유가 없다면 모든 필드는 final로 선언하라.
  • 초기화 메서드(재초기화포함)를 제공하지 마라.

Rule 16 - 계승(상속)하는 대신 구성하라 (중요)

계승은 캡슐화 원칙을 위반한다.

  • 상위클래스에 의존적이기 때문에 만약 상위클래스가 변화하면 하위클래스가 망가질 수 있다.
  • 상위클래스의 하위클래스가 강결합 상태이고, 즉 상위 클래스가 캡슐화가 되지 않는 다는 것

간단한 해결방법 (구성)

  • 계승하지 않고 객체에 private 필드 하나를 두는 것

example 포장클래스를 통한 구성

// ForwardingSet은 Set을 포장한 Wrapper class
public class InstrumentedSet<E> extends ForwardingSet<E> {
    private int addCount = 0;

    public InstrumentedSet(Set<E> s) {
        super(s);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }
    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }
    public int getAddCount() {
        return addCount;
    }
}

계승은 하위 클래스가 상위 클래스의 하위 자료형(subtype)이 확실한 경우에만 바람직하다IS-A 관계가 성립할 때만 계승해야한다.

  • IS-A 의 관계는 클래스와 객체의 관계이고 IS KIND OF가 더 적절한 거 같다.

계승을 사용한 안좋은 예

  • PropertiesHashMap : Properties#getPropertyHashMap#get 이 상호 접근을 허용한다.

결론

가능하면 계승 을 하지 않고 구성 을 사용하자. 포장 클래스 구현에 인터페이스가 있다면 더욱 그렇다.

Rule 17 - 계승을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 계승을 금지하라

규칙 세부사항

  1. 재정의 가능 메서드를 내부적으로 어떻게 사용하는지(self-use) 반드시 문서에 남기라는 것이다.
    1. 재정의 가능 메서드 : public, protected의 일반(non final)메서드
    2. 어떤 메서드가 어떤 다른 메서드나 클래스에 의존하고 있고, 어떻게 작동되는지 상세하게 서술한다.
  2. 클래스 내부 동작에 개입할 수 있는 훅(hooks)을 신중하게 고른 protected 메서드 형태로 제공해야한다.
  3. 계승을 위해 설계한 클래스를 테스트할 유일한 방법은 하위클래스를 직접 만들어 보는 것이다.
    1. 적절하게 테스트케이스를 만들어서 다양한 케이스로 테스트를 해보는 것 뿐이다. : 3개는 만들어보자.
  4. 생성자 는 직접적이건 간접적이건 재정의 가능 메서드를 호출해서는 안된다는 것이다.

example

public class Super {
    // 생성자가 재정의 가능 메서드를 호출하는 잘못된 사례
    public Super() {
        overrideMe();
    }
    public void overrideMe() {
    }
}

public final class Sub extends Super {
    private final Date date; // 생성자가 초기화하는 final필드

    Sub() {
        // super(); 을 자동으로 호출한다.
        date = new Date();
    }

    // 상위 클래스 생성자가 호출하게 되는 재정의 메서드
    @Override
    public void overrideMe() {
        System.out.println(date);
    }

    public static void main(String[] args) {
        Sub su = new Sub();
        sub.overrideMe();
        // 과연 생각한대로 date가 정상으로 출력되는가??
    }
}

CloneableSerializable와 같은 인터페이스를 구현한다면 계승을 사용하지 않는 것이 낫다.

  • 너무 과도한 책임이 졔약이 생긴다.

결론

계승을 위한 클래스를 설계하면 클래스에 상당한 제약이 가해진다. 계승에 맞도록 철저하게 설계하고, 문서화하지 않은 클래스에 대한 하위클래스를 만들지 않는 것이다. final 로 클래스를 만들던지, public 생성자를 제공하지 않으면 된다. 계승을 할 수없게 막았다면 구성을 통해 확장하면 된다.

계승을 위한 안전한 구현

클래스 내부적으로 재정의 가능 메서드를 사용하는 경우(self-use)를 완전히 제거하는 방법

  1. 재정의 가능 메서드의 내부 코드를 private로 선언된 도움 메서드 안으로 옮긴다.
  2. 각각의 재정의 가능 메서드가 해당 메서드를 호출하게 한다.
  3. 재정의 가능 메서드를 호출하는 내부 코드는 전부 해당 private 도움 메서드 호출로 바꾼다.