스터디/이펙티브 자바

[Effective Java] Item29. 이왕이면 제네릭 타입으로

📝 작성 : 2022.06.12  ⏱ 수정 : 
728x90

클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편합니다. 이를 위해서는 제네릭 타입으로 만들어야 할 경우가 많습니다. 새로운 타입을 설계할 때 뿐만 아니라 기존 타입 중 제네릭이있어야 하는 게 있다면 제네릭 타입으로 변경하는 것이 좋습니다.

간단한 실습을 통해서 기존 코드를 제네릭 타입으로 바꾸는 것을 알아보겠습니다.

기존 코드

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }

    public Object pop() {
        if (size == 0) throw new EmptyStackException();

        Object result = elements[--size];
        elements[size] = null; // 다 쓴 참조 해제
        return result;
    }

    public boolean isEmpty() {
        return size == 0;
    }
}

elements의 타입이 Object[]이기 때문에 push 메서드의 파라미터 타입도 Object이고 pop 메서드의 반환타입도 Object입니다. 따라서 이 클래스를 사용하면 값을 꺼낼 때 항상 형변환을 해야하고, 이 때 런타입 오류가 날 위험이 있습니다.

1. 클래스 선언에 타입 매개변수 추가

public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new E[DEFAULT_INITIAL_CAPACITY];
    }

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

    public E pop() {
        if (size == 0) throw new EmptyStackException();

        E result = elements[--size];
        elements[size] = null; // 다 쓴 참조 해제
        return result;
    }
    ... // isEmpty와 ensureCapacity의 코드는 그대로
}

일반 클래스를 제네릭 클래스로 만드는 첫 단계는 클래스 선언에 타입배개 변수를 추가하는 일입니다. 이때는 보통 E를 사용합니다.
위 코드에서 Object였던 것을 전부 E로 바꿨습니다.

코드를 보면 이상한 부분이 있는데 역시 컴파일 오류가 발생합니다. 바로 생성자의 new E[DEFAULT_INITIAL_CAPACITY]부분 입니다. E와 같은 실체화 불가 타입으로는 배열을 만들 수 없기 때문에 컴파일 오류가 발생하는 것 입니다.

이 때 적절한 해결책은 두가지가 있는데 첫 번째는 제네릭 배열 생성을 금지하는 제약을 우회하는 것이고 elements 필드의 타입을 E[]에서 Object[]로 바꾸는 것 입니다.

먼저 첫 번째 제네릭 배열 생성을 금지하는 제약을 우회하는 방법을 알아보겠습니다.

...

@SuppressWarnings("unchecked")
public Stack() {
    this.elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
...

elements는 push(E)로 넘어온 E 인스턴스만 담으므로 타입 안전성을 보장합니다.
하지만 이 배열의 타입은 Object[]이기 때문에 Uncecked cast: 'java.lang.Object[]' to 'E[]'란 경고가 발생합니다. 그래서 생성자에 @SuppressWarnings("unchecked")을 붙여 해당 경고를 제거합니다.

다음으로 두 번째 방법인 elements의 타입 자체를 Object[]로 바꾸는 방법을 알아보겠습니다.

public class Stack<E> {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        // new Object[]로 변경
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

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

    public E pop() {
        if (size == 0) throw new EmptyStackException();

        E result = elements[--size]; // 컴파일 오류 발생
        elements[size] = null;
        return result;
    }
}

elements와 생성자 부분을 Object[]로 변경해주면 pop메서드에서 java: incompatible types: java.lang.Object cannot be converted to E라는 컴파일 오류가 발생합니다. 이를 고치기 위해 배열의 반환 타입을 E로 형변환을 해줍니다.

... 

public E pop() {
    if (size == 0) throw new EmptyStackException();

    E result = (E) elements[--size];
    elements[size] = null;
    return result;
}

...

이러면 첫 번째 방법과 동일하게 Unchecked cast: 'java.lang.Object' to 'E'란 경고가 발생합니다. 마찬가지로 @SuppressWarnings("unchecked")를 붙여 경고를 제거할 수 있습니다.

...

public E pop() {
    if (size == 0) throw new EmptyStackException();

    @SuppressWarnings("unchecked") E result = (E) elements[--size];

    elements[size] = null;
    return result;
}
...

두 방법 중 첫 번째 방법은 가독성이 더 좋습니다. 또한 첫 번째 방법은 형변환을 배열 생성시 해주면 되지만, 두 번째 방법은 pop을 할 때마다 형변환을 해야합니다. 따라서 현업에서는 첫 번째 방식을 더 선호하며 자주 사용합니다.

하지만 첫 번째 방법은 E가 Object인 경우를 제외하면 배열의 런타임 타입이 컴파일타임 타입과 달라 힣ㅂ 오염(heap pollution)을 일으킵니다. 힙 오염이 맘에 걸리면 두 번째 방법을 사용하는 것도 좋습니다.

elements를 배열이 아닌 리스트로 사용면 되는 것 아닌가 하는 생각이 들지만 제네릭 타입 안에서 리스트를 사용하는게 항상 가능하지도, 꼭 더 좋은 것도 아닙니다. 리스트는 기본 타입으로 제공되지 않으므로 ArrayList 같은 제네릭 타입도 결국은 기본 타입인 배열을 사용해 구현해야하며, HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 합니다.

반응형