제네릭 클래스 — 타입 매개변수를 가진 클래스 설계

제네릭 클래스 선언 문법, 단일·다중 타입 파라미터, 제네릭 인터페이스, 상속 관계에서의 타입 인자 전달, 그리고 Pair<K,V>와 Stack<E> 예제로 배우는 실전 설계

· 5 min read · PALDYN Team

지난 글에서 제네릭의 기본 개념과 Box<T> 예제를 살펴봤다. 이번에는 제네릭 클래스를 직접 설계하는 방법을 깊이 있게 다룬다.

제네릭 클래스 선언 문법

클래스 이름 뒤에 <T> 형식으로 타입 파라미터를 선언한다. 클래스 본문 어디서나 T를 타입처럼 사용할 수 있다.

// 기본 구조
class ClassName<T> {
    private T field;

    public ClassName(T field) {
        this.field = field;
    }

    public T getField() {
        return field;
    }
}

타입 파라미터는 필드 타입, 메서드 파라미터 타입, 반환 타입 모두에 사용할 수 있다.

단일 타입 파라미터 — Stack<E>

컬렉션 원소 타입을 나타낼 때는 관례적으로 E(Element)를 사용한다.

class Stack<E> {
    private List<E> elements = new ArrayList<>();

    public void push(E e) {
        elements.add(e);
    }

    public E pop() {
        if (elements.isEmpty()) throw new EmptyStackException();
        return elements.remove(elements.size() - 1);
    }

    public E peek() {
        if (elements.isEmpty()) throw new EmptyStackException();
        return elements.get(elements.size() - 1);
    }

    public boolean isEmpty() {
        return elements.isEmpty();
    }
}
// 사용 — T = String
Stack<String> stack = new Stack<>();
stack.push("first");
stack.push("second");
String top = stack.pop(); // "second", 캐스팅 없음

다중 타입 파라미터 — Pair<K, V>

두 가지 다른 타입을 함께 다룰 때는 타입 파라미터를 콤마로 구분해 여러 개 선언한다.

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

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

    public K getKey()   { return key; }
    public V getValue() { return value; }

    @Override
    public String toString() {
        return "(" + key + ", " + value + ")";
    }
}
// K = String, V = Integer
Pair<String, Integer> age = new Pair<>("Alice", 30);
System.out.println(age.getKey());   // "Alice"
System.out.println(age.getValue()); // 30

// K = Long, V = Boolean
Pair<Long, Boolean> flag = new Pair<>(100L, true);

Pair<K,V> 클래스 구조 다이어그램

제네릭 클래스 코드 예제

제네릭 인터페이스

인터페이스도 클래스와 동일한 방식으로 타입 파라미터를 선언한다.

interface Repository<T, ID> {
    T findById(ID id);
    List<T> findAll();
    void save(T entity);
    void delete(ID id);
}

// 구현 클래스 — 구체 타입 지정
class UserRepository implements Repository<User, Long> {
    @Override
    public User findById(Long id) { /* ... */ }

    @Override
    public List<User> findAll() { /* ... */ }
    // ...
}

구현 클래스가 타입 인자를 지정하지 않고 타입 파라미터를 유지할 수도 있다.

// T를 유지하는 구현
class InMemoryRepository<T, ID> implements Repository<T, ID> {
    private final Map<ID, T> store = new HashMap<>();

    @Override
    public T findById(ID id) { return store.get(id); }
    // ...
}

상속과 타입 인자

제네릭 클래스를 상속할 때 타입 인자를 지정하거나, 부모의 타입 파라미터를 그대로 넘길 수 있다.

// 부모 타입 고정
class StringBox extends Box<String> {
    public StringBox(String v) { super(v); }
    public int length() { return getValue().length(); }
}

// 타입 파라미터 전달
class NumberBox<N extends Number> extends Box<N> {
    public NumberBox(N v) { super(v); }
    public double asDouble() { return getValue().doubleValue(); }
}
StringBox sb = new StringBox("hello");
int len = sb.length(); // 5

NumberBox<Integer> nb = new NumberBox<>(42);
double d = nb.asDouble(); // 42.0

주의사항: static 멤버에 타입 파라미터 불가

인스턴스 타입 파라미터는 static 필드나 static 메서드에 사용할 수 없다. 인스턴스마다 다른 타입을 가질 수 있는데, static 멤버는 클래스 수준에서 하나만 존재하기 때문이다.

class Box<T> {
    // 컴파일 오류 — static 필드에 T 사용 불가
    // private static T sharedValue;

    // static 메서드에서 T 사용 불가 (클래스의 T)
    // public static T createDefault() { ... }

    // OK — static 제네릭 메서드는 별도 타입 파라미터 선언
    public static <E> Box<E> of(E value) {
        return new Box<>(value);
    }
}

지난 글: Java 제네릭 완전 정복 — 타입 매개변수의 기초

다음 글: 제네릭 메서드 — 메서드 수준의 타입 매개변수


읽어주셔서 감사합니다. 😊