제네릭 (Generic)

mskimdev·2026년 4월 18일

Java OOP-Advanced

목록 보기
8/8
post-thumbnail

제네릭 (Generics)

List를 처음 쓰다 보면 이런 코드를 만나게 된다.

List<String> names = new ArrayList<>();

<String>이 뭔지 모르고 그냥 따라 쓰다가, 어느 순간 "이게 왜 있는 거지?"라는 의문이 생긴다. 이게 바로 제네릭(Generics)이다.


제네릭이 없었다면

제네릭이 생기기 전에는 컬렉션에 뭘 담든 Object 타입으로 처리했다.

List list = new ArrayList();
list.add("안녕");
list.add(123);  // 문자열이든 숫자든 다 들어간다

String name = (String) list.get(0);  // 꺼낼 때 직접 형변환해야 한다
String wrong = (String) list.get(1); // 런타임 오류 — 123은 String이 아니다

어떤 타입이든 넣을 수 있다 보니 꺼낼 때 형변환이 필요했고, 잘못된 타입을 넣어도 컴파일 시점에 오류가 나지 않았다. 실행해봐야 터지는 오류는 찾기도 어렵고 고치기도 까다롭다.

제네릭은 이 문제를 해결하기 위해 등장했다.


제네릭의 기본 문법

타입을 <> 안에 명시해서 어떤 타입만 담을 수 있는지 컴파일러에게 알려준다.

List<String> names = new ArrayList<>();
names.add("김민수");
names.add("이지현");
names.add(123);  // 컴파일 오류 — String만 들어갈 수 있다

String name = names.get(0);  // 형변환 불필요

타입이 고정되니까 꺼낼 때 형변환도 필요 없고, 잘못된 타입을 넣으면 실행 전에 오류를 잡아준다.


제네릭 클래스 만들기

직접 제네릭 클래스를 만들 수도 있다. 타입 파라미터는 보통 T(Type), E(Element), K(Key), V(Value) 같은 대문자 한 글자를 관례로 쓴다.

public class Box<T> {
    private T item;

    public void set(T item) {
        this.item = item;
    }

    public T get() {
        return item;
    }
}

T는 실제 타입이 들어올 자리를 표시하는 placeholder다. 사용할 때 타입을 지정하면 그 자리에 들어간다.

Box<String> strBox = new Box<>();
strBox.set("안녕");
String value = strBox.get();  // 형변환 없이 String으로 받는다

Box<Integer> intBox = new Box<>();
intBox.set(42);
Integer num = intBox.get();

Box<String>Box<Integer>는 같은 클래스지만 다른 타입으로 동작한다.


제네릭 메서드

메서드 단위로도 제네릭을 적용할 수 있다. 반환 타입 앞에 <T>를 붙인다.

public static <T> void print(T item) {
    System.out.println(item);
}

print("안녕");   // String
print(42);       // Integer
print(3.14);     // Double

타입에 관계없이 동작하는 유틸리티 메서드를 만들 때 유용하다.


타입 제한 — extends

<T> 자리에 아무 타입이나 오지 못하도록 상한선을 걸 수 있다.

// T는 Number 또는 Number의 하위 클래스만 가능
public static <T extends Number> double sum(List<T> list) {
    double total = 0;
    for (T item : list) {
        total += item.doubleValue();
    }
    return total;
}

sum(List.of(1, 2, 3));        // Integer — 가능
sum(List.of(1.1, 2.2, 3.3)); // Double — 가능
sum(List.of("a", "b"));      // 컴파일 오류 — String은 Number가 아니다

doubleValue()Number 클래스의 메서드다. <T extends Number>로 제한해야 T가 doubleValue()를 가지고 있다고 컴파일러가 보장해준다.


와일드카드 — ?

제네릭 타입을 정확히 특정하지 않고 "어떤 타입이든" 받고 싶을 때 ?를 쓴다.

// 어떤 타입의 List든 받아서 출력
public static void printList(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

printList(List.of("a", "b", "c"));  // List<String>
printList(List.of(1, 2, 3));        // List<Integer>

List<Object>List<String>을 받을 수 없다. 제네릭은 상속 관계를 따르지 않기 때문이다. 이때 List<?>를 쓰면 어떤 타입의 리스트든 받을 수 있다.


제네릭은 처음엔 <>가 낯설게 느껴지지만, 결국 "이 컨테이너에 어떤 타입을 담을 건지 미리 알려주는 것"이다. 타입을 명시하는 순간 형변환도 사라지고, 잘못된 타입을 넣는 실수도 컴파일 시점에 잡힌다.

profile
<- 개발 공부하는 나

0개의 댓글