본문 바로가기
개발 공부/Java

이펙티브 자바 아이템 29 - 이왕이면 제네릭 타입으로 만들라 - 핵심 정리

by 개발인생 2022. 12. 13.
반응형

아이템 29 - 이왕이면 제네릭 타입으로 만들라 - 핵심 정리

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

어떤 클래스들을 만들다보면 그 클래스 안에 다른 객체들을 담는 역활을 하는 클래스를 만드는 경우가 많다.

대표적으로 Stack 같이 자료구조 안에 다른 객체들을 담는 역활을 하는 클래스를 작성하는 경우이다.

무언가 다른것들을 담는 역활을 하는 클래스들은 제네릭 타입 으로 만들면 유용하다.

특히 그 안에 Object 타입으로 무언가를 담고있다면 더더욱 명확하게 제네릭 타입 을 사용하도록 유도하면

런 타임 시에 ClassCastException 이 발생하는 것을 많이 줄일 수 있다.

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

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

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

   // 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
   public static void main(String[] args) {
      Stack stack = new Stack();
      for (String arg : List.of("a", "b", "c"))
         stack.push(arg);
      while (!stack.isEmpty())
         System.out.println(((String) stack.pop()).toUpperCase());
   }
}

Stack 안에 Object 배열을 가지고 있고, 스택을 사용하는 코드도 같이 있다.

이 클래스를 제네릭을 사용해서 만들면 클라이언트 코드에서 형 변환 을 사용하지 않도록 만들 수 있다.

또한 형 변환 을 잘못 사용했을 때 발생할 수 있는 ClassCastException 을 미연에 방지 할 수 있다.

제네릭을 사용하지 않는다면 Stack 클래스에서 값을 꺼낼 때 Object 타입이므로 반듯이 형 변환 을 해줘야한다.

두가지 방법으로 구현할 수 있다.


// E[]를 이용한 제네릭 스택 (170-174쪽)
public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    // 코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법 1 (172쪽)
    // 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
    // 따라서 타입 안전성을 보장하지만,
    // 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
    @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;
    }

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

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

    // 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
    public static void main(String[] args) {
        Stack<String> stack = new Stack<>();
        for (String arg : List.of("a", "b", "c"))
            stack.push(arg);
        while (!stack.isEmpty())
            System.out.println(stack.pop().toUpperCase());
    }
}

첫 번째 방법은 Stack<E> 로 제네릭 타입을 선언한 뒤 E[] 제네릭 타입의 배열을 사용하는 것이다.

대신에 클래스 안에서

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

오브젝트 타입의 배열을 만들어야 한다.

제네릭 배열을 만들 수 없기 때문에 Object 배열을 만든 뒤 E[] 제네릭 타입의 배열로 형 변환 을 해줘야한다.

대신 런 타임 에는 E[] 부분이 소거 되기 때문에 결국 Object 타입의 배열로 동작하게 된다.

그러나 이렇게 만들어두면 값을 꺼낼 때 전달받은 실제 타입 매개변수 로 꺼낼 수 있게된다.

   elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];

위의 코드에서 경고 메세지가 발생하는데

    // 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
   // 따라서 타입 안전성을 보장하지만,
   // 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
    @SuppressWarnings("unchecked")
    public Stack() {
        elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    }

@SuppressWarnings("unchecked") 을 붙여 경고 메세지를 무시할 수 있다.

사실상 Object 배열이지만 해당 배열에 들어가는 타입은 제네릭에 전달받은 실제 타입 매개변수 만 들어오게되고

해당 배열을 어딘가에 공개하지 않기 때문에 안전성을 보장한다.

만약 배열을 리턴하게되면 어딘가에서 제네릭에 전달받은 실제 타입 매개변수 와는 다른 타입이 들어갈 가능성이 생긴다.

위 방법은 형 변환 을 배열을 만들 때 한번만 해도된다는 장점이 있다.

가독성도 좋아지지만 힙 오염 이 발생할 가능성이 있다는 단점이 있다.


힙 오염 이 발생하지 않도록 하려면

// Object[]를 이용한 제네릭 Stack (170-174쪽)
public class Stack<E> {
    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(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

    // 코드 29-4 배열을 사용한 코드를 제네릭으로 만드는 방법 2 (173쪽)
    // 비검사 경고를 적절히 숨긴다.
    public E pop() {
        if (size == 0)
            throw new EmptyStackException();

        // push에서 E 타입만 허용하므로 이 형변환은 안전하다.
        @SuppressWarnings("unchecked") E result = (E) elements[--size];

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

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

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

    // 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
    public static void main(String[] args) {
        Stack<String> stack = new Stack<>();
        for (String arg : List.of("a", "b", "c"))
            stack.push(arg);
        while (!stack.isEmpty())
            System.out.println(stack.pop().toUpperCase());
    }
}

위처럼 제네릭 배열(E[]) 대신 Object 배열을 사용하는 것이다.

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

        // push에서 E 타입만 허용하므로 이 형변환은 안전하다.
        @SuppressWarnings("unchecked") E result = (E) elements[--size];

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

대신 담아뒀던 객체를 꺼낼 때 제네릭 타입으로 형 변환 을 해야한다.

Object 배열에 들어오는 값은 전부 제네릭 타입이므로 제네릭 타입으로 형 변환 을 하는 것이 안전하다.

때문에 @SuppressWarnings("unchecked") 사용해 경고 메세지를 무시할 수 있다.

이 방법의 장점은 힙 오염 이 발생할 여지가 없다는 점이다.

단점은 객체를 꺼낼 때마다 해당하는 타입으로 형 변환 을 내부적으로 해줘야한다는 것이다.

   public static void main(String[] args) {
      Stack stack = new Stack();
      for (String arg : List.of("a", "b", "c"))
         stack.push(arg);
      while (!stack.isEmpty())
         System.out.println(((String) stack.pop()).toUpperCase());
   }

Stack 클래스를 제네릭으로 수정해도 기존의 클라이언트 코드는 수정없이도 그대로 동작한다.

제네릭을 도입할 때 소거 방식을 도입했기 때문에 기존 코드가 깨지지 않고 그대로 동작하게된다.

배열대신 List 를 사용하라고 했지만

항상 List 를 사용해야 하는 것은 아니고, 성능에 민감한 자료구조 같은 경우는 배열을 사용한다.

배열을 쓰는 경우를 항상 피할 수 없기 때문에 배열을 쓰는 경우 피치못하게

@SuppressWarnings("unchecked") 을 사용하는 경우가 생길 수 있다.

위의 두가지 방법 중에서 대부분의 경우에는 E[] 을 사용해 형 변환 을 한번만 하는 방법을 사용한다.

대신 힙 오염 이 발생하지 않도록 주의해야한다.

반응형

댓글