TIL #22 제네릭

HYEON JIN CHOI·2024년 5월 22일

제네릭(Generics) 은 자바 프로그래밍 언어에서 코드의 재사용성과 타입 안전성을 높이는 데 중요한 역할을 한다. 제네릭 을 사용하면 클래스, 인터페이스, 메서드를 다양한 타입으로 동작하게 할 수 있으며, 컴파일 시 타입 검사를 통해 오류를 사전에 방지할 수 있다. 이번 글에서는 제네릭 의 개념과 필요성, 기본적인 사용법, 그리고 주의사항에 대해 자세히 알아보려고 한다.

1. 제네릭의 필요성

제네릭이 도입되기 전에는 컬렉션 프레임워크와 같은 데이터 구조를 사용할 때 Object 타입을 이용하여 모든 종류의 객체를 저장하고 관리하였다. 예를 들어, ArrayList 는 다양한 타입의 객체를 저장할 수 있었지만, 이를 꺼내 사용할 때마다 명시적 타입 캐스팅이 필요했다. 이는 다음과 같은 문제점을 야기한다.

  • 타입 안전성 문제 : 잘못된 타입으로 캐스팅할 경우 런타임 오류가 발생할 수 있다.
  • 가독성 저하 : 코드의 가독성이 떨어지고 유지보수가 어려워진다.
  • 개발자 실수 증가 : 타입 캐스팅을 자주 사용하면 실수할 가능성이 높아진다.

이러한 문제를 해결하기 위해 Java 5 에서 제네릭이 도입되었다. 제네릭을 사용하면 컴파일 시 타입 검사를 통해 잘못된 타입 사용을 방지할 수 있으며, 명시적 타입 캐스팅이 필요 없어 코드가 간결해진다.

2. 제네릭의 기본 개념

제네릭 은 클래스, 인터페이스, 메서드에 타입 매개변수를 사용하는 것이다. 이를 통해 다양한 타입에 대해 유연하게 동작하는 코드를 작성할 수 있다. 제네릭의 기본적인 문법은 다음과 같다.

  • 제네릭 클래스 : 클래스 선언 시 타입 매개변수를 사용하여 정의한다.
  • 제네릭 메서드 : 메서드 선언 시 타입 매개변수를 사용하여 정의한다.

다음은 제네릭 클래스의 예제이다.

public class Box<T> {
    private T content;

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

위 예제에서 Box 클래스는 타입 매개변수 T 를 사용하여 정의되었다. 이를 통해 Box 클래스는 다양한 타입으로 사용할 수 있다.

다음은 제네릭 메서드의 예제이다.

public class Util {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }
}

위 예제에서 printArray 메서드는 타입 매개변수 T 를 사용하여 정의되었다. 이를 통해 다양한 타입의 배열을 출력할 수 있다.

3. 제네릭 클래스와 메서드

제네릭 을 사용하는 클래스와 메서드는 여러 가지 형태로 구현될 수 있다. 다음은 제네릭 클래스와 메서드를 더 자세히 살펴보자.

3.1. 제네릭 클래스

제네릭 클래스를 사용하면 다양한 타입을 처리할 수 있는 클래스를 작성할 수 있다. 예를 들어, Pair 클래스를 정의하여 두 개의 객체를 저장할 수 있다.

public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

위 예제에서 Pair 클래스는 두 개의 타입 매개변수 KV 를 사용하여 키와 값을 저장할 수 있다.

3.2. 제네릭 메서드

제네릭 메서드는 메서드에 타입 매개변수를 사용하여 정의한다. 예를 들어, 두 개의 값을 교환하는 메서드를 정의할 수 있다.

public class SwapUtil {
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

위 예제에서 swap 메서드는 타입 매개변수 T 를 사용하여 배열의 두 요소를 교환한다.

4. 제네릭의 한계와 주의사항

제네릭은 강력한 기능이지만 몇 가지 한계와 주의사항이 있다. 이를 이해하고 사용하는 것이 중요하다.

4.1. 타입 소거

Java 에서 제네릭은 타입 소거(Type Erasure)를 사용하여 구현된다. 컴파일 시 제네릭 타입은 Object 로 변환되고, 타입 검사와 변환이 제거된다. 이로 인해 런타임에는 제네릭 타입 정보가 사라지게 된다. 따라서 다음과 같은 한계가 있다:

  • 런타임 타입 검사 불가 : 제네릭 타입은 런타임에 타입 정보를 알 수 없으므로 instanceof 연산자나 getClass 메서드를 사용할 수 없다.
  • 제네릭 배열 생성 불가 : 제네릭 타입으로 배열을 생성할 수 없다. 예를 들어, T[] array = new T[10]; 와 같은 코드는 컴파일 오류가 발생한다.

4.2. 제네릭과 상속

제네릭 을 사용할 때 상속 관계에 주의해야 한다. 예를 들어, List<String>List<Object> 의 하위타입이 아니다. 이는 제네릭 의 타입 안전성을 보장하기 위함이다. 다음과 같은 코드는 컴파일 오류가 발생한다.

List<String> stringList = new ArrayList<>();
List<Object> objectList = stringList; // 컴파일 오류

4.3. 와일드카드

제네릭을 사용할 때 와일드카드(?) 를 사용하여 다양한 타입을 유연하게 처리할 수 있다. 와일드카드 는 세 가지 종류가 있다:

  • 제한 없는 와일드카드 : <?> 는 어떤 타입이든 허용한다.
  • 상한 제한 와일드카드 : <? extends T>T 와 그 하위 타입을 허용한다.
  • 하한 제한 와일드카드 : <? super T>T 와 그 상위 타입을 허용한다.

예를 들어, 다음과 같이 와일드카드 를 사용할 수 있다.

public void printList(List<?> list) {
    for (Object element : list) {
        System.out.println(element);
    }
}

위 예제에서 printList 메서드는 어떤 타입의 리스트든 인자로 받을 수 있다.

5. 회고

제네릭은 자바 프로그래밍 언어에서 코드의 재사용성과 타입 안전성을 높이는 데 중요한 역할을 한다. 제네릭을 사용하면 다양한 타입에 대해 유연하게 동작하는 코드를 작성할 수 있으며, 컴파일 시 타입 검사를 통해 오류를 사전에 방지할 수 있을 것이라 생각한다.

0개의 댓글