본문 바로가기
Dev Books/Effective Java

[item 29] 이왕이면 제네릭 타입으로 만들라.

by Thumper 2025. 2. 4.

제네릭 타입을 만들기 전에, 제네릭이 없는 Stack 코드를 살펴보자.

import java.util.Arrays;
import java.util.EmptyStackException;

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

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

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

    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

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

제네릭 미사용의 문제: 타입 안전성을 보장하지 않는 Stack 클래스

pop 메서드는 스택에서 객체를 꺼낼 때 Object 타입으로 반환한다.
이는 클라이언트가 해당 객체를 실제로 사용할 수 있도록 적절한 타입으로 형변환해야 함을 의미한다.
예를 들어, String 타입의 객체를 푸시한 후 꺼내려고 할 때 String으로 형변환해야 한다.
image
예를 들어, 스택에 Integer를 푸시한 후 String으로 형변환하려 하면 다음과 같은 오류가 발생한다.

제네릭 타입의 Stack 으로 만들기

위 Stack 클래스는 원래 제네릭 타입이어야 한다. 그러니 제네릭으로 만들어보자.

제네릭 크래스로 만들기 위해서 다음과 같이 해야 한다.

  • 클래스 선언에 타입 매개변수를 추가한다. (보통 E를 많이 사용한다.)
  • Objec를 적절한 타입 매개변수로 바꾼다.
import java.util.EmptyStackException;
import java.util.Arrays;
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]; // 에러 발생!
        // 실체화 불가 타입으로는 배열을 만들 수 없음.
    }

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

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

이렇게 코드를 수정했을 때, 컴파일 오류가 발생한다.

image

제네릭 배열 생성 시 발생하는 컴파일 오류와 타입 소거의 한계

제네릭 타입 E를 사용하여 배열을 직접 생성하려고 할 때 발생한다.
Java에서는 제네릭 타입의 배열을 직접 생성할 수 없다. 이 제한은 Java의 타입 소거(type erasure) 메커니즘 때문에 발생한다.
Java의 제네릭은 런타임에 타입 정보가 지워지는 타입 소거(type erasure)를 사용한다.

실체화 불가 타입으로는 배열을 만들 수 없다.

즉, 제네릭 타입 정보는 컴파일 타임에만 유효하고 런타임에는 삭제된다.
따라서, 제네릭 타입 E는 런타임에 실체화되지 않는다. 이는 배열 생성 시에 E의 타입 정보를 알 수 없다는 것을 의미하며, 이로 인해 배열을 생성할 수 없다.

실체화 불가 타입으로는 배열을 만들 수 없으니 다음 방법으로 해결해보자.

첫 번째: Object 배열을 생성한 다음 제네릭 배열로 형변환해본다.

제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법이다.

  • elements는 private 피드로 저장된다.
  • E[]로 선언되어 push(E)만 담아 타입 캐스팅이 안전하다.
  • @SuppressedWarning("unchecked")로 경고 메시지를 숨긴다.
import java.util.EmptyStackException;
import java.util.Arrays;

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

    @SuppressWarnings("unchecked")
    public Stack() {
        elements = (E[]) 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;
    }

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

Stack을 사용한 예제 코드로도 확인해보자.

image
제네릭을 사용하여 배열 생성 시 발생하는 형변환 경고를 억제하고,
push와 pop 메서드를 통해 항상 타입 E로 유지하여 런타임에 형변환 문제를 방지했다.

장점

  • 가독성이 더 좋다. (E 타입만을 받는다는 점을 어필한다.)
  • 형변환을 배열 생성시 단한번만 해주면 된다.

단점

  • 배열의 런타임 타입이 컴파일타임 타입과 달라 힙오염(heap pollution)이 일어날 수 있다.

    heap pollution: 매개변수화 타입이 매개변수화 타입이 아닌 것을 참조할 떄 발생하는 현상.




두 번째: elements 필드의 타입을 E[]에서 Object[]로 바꾼다.

이렇게 바꾸면 첫 번째와는 다른 오류가 발생한다.

import java.util.EmptyStackException;
import java.util.Arrays;

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

    @SuppressWarnings("unchecked")
    public Stack() {
        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;
    }

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

배열이 반환한 원소를 E로 형변환하면 아래와 같이 경고가 뜬다.

image



E가 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없다.
이후 비검사 형변환을 수행하는 할당문에서만 경고를 숨긴다.

image

특징

  • 배열에서 원소를 읽을 떄마다 형변환 해주어야 한다.
  • 힙오염(heap pollution)이 해가 되지 않는다.

    heap pollution: 매개변수화 타입이 매개변수화 타입이 아닌 것을 참조할 떄 발생하는 현상.

한정적 타입 매개변수 (bounded type parameter)

class DelayQueue<E extends Delayed> implements BlockingQueue<E>
  • Delayed.class의 하위타입만 받는다는 의미이다. (모든 타입은 자기 자신의 하위 타입)

  • ClassCastException을 걱정할 필요가 없다.

    결론

  • 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다.

  • 그렇기 떄문에 새로운 타입을 설계할 때는 형변환 없이 사용할 수 있도록 하라.

  • 제네릭 타입을 사용하는 것은 클라이언트에는 아무 영향을 주지 않으면서, 새로운 사용자들을 편하게 해주는 방법이다.

댓글