Effective Java 2/E - Chatper 05 제네릭 (1)

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

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를 제공할 수 있다.

MJ

MJ
Backend 개발자 사람입니다. 어플리케이션의 복잡성을 다루는 DDD에 관심이 많습니다. 어제보다 더 나은 개발자가 되려고 항상 노력합니다.

spring boot 2.4.x 에서 openfeign + hystrix 통합하기

spring-boot 2.4.x spring-cloud 2020.x 의존성 상황에서 feign.hystrix.enabled=true가 안됨`feign.circuitbreaker.enabled=true` 로 바꿔보지만 openfeign과 hystr...… Continue reading

IDDD 14장. 애플리케이션

Published on June 19, 2018