자바에서 제네릭의 장점과 사용할 때 주의할 점

자바에서 제네릭을 사용하면 좋은 점은 무엇일까? 그리고 주의할 점은?

#java #generic #generic-class #generic-interface #generic-method


제네릭은 무엇일까?

지난 포스팅에서는 자바에서 제네릭은 무엇인지, 클래스와 인터페이스 그리고 메서드에는 어떻게 제네릭을 적용하는 지에 대해서 알아보았습니다.

이번 포스팅에서는 자바에서 제네릭이 왜 좋은지, 사용할 때 주의할 점은 없는 지 살펴봅니다.


제네릭이 왜 좋을까?

우선 컴파일 타임에 타입을 체크하기 때문에 객체 자체의 타입 안전성을 높일 수 있습니다. 개발자가 의도하지 않은 타입의 객체가 저장되는 것을 방지할 수 있고 저장한 객체를 다시 가져올 때 기존 타입과 다른 타입으로 캐스팅되어 발생하는 오류(ClassCastException)를 줄일 수 있습니다.

형 변환(Type Casting)의 번거로움을 줄일 수 있습니다. 아래의 코드를 볼까요? 제네릭없이 최상위 객체 Object를 사용한다면 아래와 같이 코드를 작성할 수 있습니다.

class MadPlay {
    private Object obj;

    public MadPlay(Object obj) { this.obj = obj; }
    public Object getObj() { return obj; }
}

class GenericTester {
    public void executeMethod() {
        MadPlay instance1 = new MadPlay(new String("Hello"));
        MadPlay instance2 = new MadPlay(new Integer(123));
        MadPlay instance3 = new MadPlay(new Character('a'));

        String obj1 = (String) instance1.getObj();
        Integer obj2 = (Integer) instance2.getObj();
        Character obj3 = (Character) instance3.getObj();
    }
}

하지만 여기서 제네릭을 사용하면 타입 캐스팅을 하지 않아도 됩니다. 변환하여 사용할 객체의 타입을 사전에 명시하므로서 타입 캐스팅의 수고를 줄일 수 있습니다.

class GenericMadPlay<T> {
    private T obj;

    public GenericMadPlay(T obj) { this.obj = obj; }
    public T getObj() { return obj; }
}

class GenericTester {
    public void executeMethod() {
        GenericMadPlay<String> genericInstance1 = new GenericMadPlay<>("Hello");
        GenericMadPlay<Integer> genericInstance2 = new GenericMadPlay<>(123);
        GenericMadPlay<Character> genericInstance3 = new GenericMadPlay<>('a');

        String genericObj1 = genericInstance1.getObj();
        Integer genericObj2 = genericInstance2.getObj();
        Character genericObj3 = genericInstance3.getObj();
    }
}

타입을 지정하지 않으면 최상위 Object 객체 타입으로 정의되므로 다양한 종류의 타입을 다뤄야하는 메서드의 매개변수에 Object 타입을 사용하고(첫 번째 예제 코드에서의 생성자) 그로 인한 타입 캐스팅이 불가피했지만 두 번째 코드를 보면 알 수 있듯이 제네릭을 사용하면 원하는 타입을 사전에 지정하기만 하면 됩니다.

끝으로 이번 글에서 타입 매개변수의 이름을 T로 지정했으나, 사실 아무 이름이나 가능합니다. MadPlay<Kimtaeng> 이런식도 가능하지요. 하지만 아래와 같이 컨벤션(Convention)이 있습니다. 다른 개발자가 보았을 때 조금 더 쉽게 이해할 수 있도록 지키는 것이 좋을 것 같습니다.

  • E(Element) : 요소, 예를 들어 List<E>
  • K(Key) : 키, 예를 들어 Map<K, V>
  • N(Number) : 숫자
  • T(Type) : 타입
  • V(Value) : 리턴 값 또는 매핑된 값
  • S, U, V : 2번째, 3번째 그리고 4번째에 선언된 타입


제네릭 사용시 주의할 점은?

제네릭은 클래스와 인터페이스만 적용되기 때문에 자바 기본 타입(Primitive Type)은 사용할 수 없습니다. (관련 링크 : 자바의 데이터 타입)

public void someMethod() {
    List<int> intList = new List<>(); // 기본 타입 int는 사용 불가
    List<Integer> integerList = new List<>(); // Okay!
}

또한 제네릭 타입을 사용하여 객체를 생성하는 것은 불가능합니다. 즉, 제네릭 타입의 객체는 생성이 불가능합니다.

public void someMethod() {
    // Type parameter 'T' cannot be instantiated directly
    T t = new T();
    return t;
}

그리고 제네릭에서는 배열에 대한 제한을 두고 있습니다. 제네릭 클래스 또는 인터페이스 타입의 배열을 선언할 수 없습니다. 하지만 제네릭 타입의 배열 선언은 허용됩니다.

public void someMethod() {
    // generic array creation
    // (자바 8이전) Cannot create a generic array of MadPlay<Integer>
    MadPlay<Integer>[] arr1 = new MadPlay<>[10];
    
    MadPlay<Integer>[] arr2 = new MadPlay[10]; // Okay!
}