개발/자바

Java 제네릭(Generic) 클래스란?

피터JK 2025. 2. 19. 15:25
728x90

제네릭(Generic) 클래스란?

제네릭(Generic) 클래스는 타입을 파라미터로 받을 수 있는 클래스입니다. 즉, 클래스 내부에서 사용할 데이터 타입을 컴파일 타임에 지정할 수 있도록 하는 기능을 제공합니다. 이를 통해 코드의 재사용성을 높이고, 타입 안정성을 보장할 수 있습니다.


1. 제네릭 클래스 기본 구조

제네릭 클래스를 선언할 때는 클래스 이름 뒤에 <T>와 같은 타입 파라미터를 지정합니다.

// T는 타입 파라미터 (임의의 타입을 의미)
public class Box<T> {
    private T item; // T 타입의 변수

    // 생성자
    public Box(T item) {
        this.item = item;
    }

    // 값을 가져오는 메서드
    public T getItem() {
        return item;
    }

    // 값을 설정하는 메서드
    public void setItem(T item) {
        this.item = item;
    }
}

2. 제네릭 클래스 사용 예시

제네릭 클래스를 사용할 때는 원하는 타입을 지정해야 합니다.

public class Main {
    public static void main(String[] args) {
        // String 타입을 지정한 Box 객체 생성
        Box<String> stringBox = new Box<>("Hello");
        System.out.println(stringBox.getItem()); // 출력: Hello

        // Integer 타입을 지정한 Box 객체 생성
        Box<Integer> intBox = new Box<>(100);
        System.out.println(intBox.getItem()); // 출력: 100
    }
}
  • Box<String>을 사용하면 Box 클래스의 T가 String으로 대체됩니다.
  • Box<Integer>를 사용하면 T가 Integer로 대체됩니다.

3. 제네릭의 장점

  1. 타입 안정성(Type Safety) 보장
    • 컴파일 타임에 타입 검사가 이루어져 런타임 오류를 줄일 수 있음
    • 잘못된 타입이 들어가는 것을 방지
  2. 형변환(Casting) 불필요
    • 일반적인 Object 타입을 사용하면 값을 가져올 때 형변환이 필요하지만, 제네릭을 사용하면 불필요
    // 제네릭을 사용하지 않는 경우 (Object 타입)
    Object obj = "Hello";
    String str = (String) obj;  // 형변환 필요
    
    // 제네릭을 사용하는 경우
    Box<String> box = new Box<>("Hello");
    String str2 = box.getItem(); // 형변환 없이 바로 사용 가능
  3. 코드 재사용성 증가
    • 여러 타입에 대해 동일한 코드로 동작할 수 있으므로 중복 코드 감소
    • 유지보수성 향상

4. 제네릭 타입 제한 (Bounded Type Parameter)

extends 키워드를 사용하여 특정 클래스 또는 인터페이스를 상속한 타입만 사용할 수 있도록 제한할 수 있습니다.

// Number 또는 Number를 상속한 클래스만 사용 가능
public class NumberBox<T extends Number> {
    private T number;

    public NumberBox(T number) {
        this.number = number;
    }

    public T getNumber() {
        return number;
    }
}

public class Main {
    public static void main(String[] args) {
        NumberBox<Integer> intBox = new NumberBox<>(10); // 가능
        NumberBox<Double> doubleBox = new NumberBox<>(5.5); // 가능
        // NumberBox<String> strBox = new NumberBox<>("Hello"); // 오류 발생 (String은 Number를 상속하지 않음)
    }
}

5. 와일드카드(?)

와일드카드 ?를 사용하면 제네릭 타입을 정확히 알지 못하는 경우에도 사용할 수 있습니다.

public class Util {
    // ?를 사용하여 모든 타입을 받을 수 있음
    public static void printBox(Box<?> box) {
        System.out.println(box.getItem());
    }
}

public class Main {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>("Hello");
        Box<Integer> intBox = new Box<>(100);

        Util.printBox(stringBox); // 출력: Hello
        Util.printBox(intBox);    // 출력: 100
    }
}

와일드카드의 종류

  • <?> → 모든 타입 허용
  • <? extends T> → T 또는 T의 하위 타입만 허용
  • <? super T> → T 또는 T의 상위 타입만 허용

6. 제네릭 메서드

제네릭은 클래스뿐만 아니라 메서드에서도 사용할 수 있습니다.

public class Util {
    // 제네릭 메서드 선언 (T는 메서드에서만 사용됨)
    public static <T> void printItem(T item) {
        System.out.println(item);
    }

    public static void main(String[] args) {
        printItem("Hello");  // 출력: Hello
        printItem(100);      // 출력: 100
        printItem(3.14);     // 출력: 3.14
    }
}

7. 제네릭 인터페이스

인터페이스에도 제네릭을 적용할 수 있습니다.

// 제네릭 인터페이스 선언
public interface Repository<T> {
    void save(T item);
    T find();
}

// String을 타입으로 지정한 구현 클래스
public class StringRepository implements Repository<String> {
    private String data;

    @Override
    public void save(String item) {
        this.data = item;
    }

    @Override
    public String find() {
        return data;
    }
}

public class Main {
    public static void main(String[] args) {
        Repository<String> repo = new StringRepository();
        repo.save("Generic Interface");
        System.out.println(repo.find()); // 출력: Generic Interface
    }
}

8. 제네릭과 배열

제네릭 타입의 배열을 직접 생성할 수는 없습니다.

T[] arr = new T[10]; // 컴파일 오류

대신 Object[]를 사용하거나 Array.newInstance()를 활용해야 합니다.

@SuppressWarnings("unchecked")
public class GenericArray<T> {
    private T[] array;

    public GenericArray(Class<T> clazz, int size) {
        array = (T[]) java.lang.reflect.Array.newInstance(clazz, size);
    }

    public void set(int index, T value) {
        array[index] = value;
    }

    public T get(int index) {
        return array[index];
    }
}

정리

제네릭 클래스는 타입을 파라미터화하여 재사용성을 높이고, 타입 안정성을 보장하는 기능
제네릭 메서드는 특정 메서드에서만 제네릭을 사용할 수 있도록 함
✅ **와일드카드(?)**를 사용하면 다양한 제네릭 타입을 유연하게 받을 수 있음
✅ **타입 제한(extends 사용)**을 통해 특정 타입만 사용하도록 제약 가능
제네릭 인터페이스를 활용하면 다양한 타입을 지원하는 인터페이스를 만들 수 있음


제네릭을 적절히 활용하면 유지보수성이 높은 코드를 작성할 수 있습니다. 😊

728x90