코드 저장소.

Generic 본문

Java

Generic

slown 2025. 9. 13. 21:53

목차

1.도입

2제네릭이란 ?

3.와일드 카드

4.제네릭 소거

5.정리 

 

1.도입

평소에 자바로 코드를 작성을 하다보면 제네릭을 쓰는 경우가 많다. 하지만 막상 떠올리는 것은 "컴파일시 타입을 체크해준다","API의 표현력을 높여준다" 정도로 알고 있었다. 이번 글에서는 그 동안 애매하게 알고 있었던 제네릭에 대한 내용을 다시 정리해 보려고 한다. 기본 문법부터 와일드카드, 그리고 제네릭 소거까지 살펴보면서 실제 코드 예제도 같이 다뤄보려고 한다.

2.제네릭이란 ?

2-1. 제네릭이 필요한 이유

자바 1.4 시절에는 컬렉션이 전부 Object 타입으로 동작했다. 이 경우 타입 체크가 컴파일 시점에 이뤄지지 않아서 런타임 오류로 이어질 수 있었다.

List list = new ArrayList();
list.add("hello");
list.add(123); // 다른 타입도 들어가 버림

String str = (String) list.get(1); // 런타임에서 ClassCastException 발생

 

이 문제를 해결하기 위해 제네릭(Generic) 개념이 도입되었다. 제네릭은 컴파일 시점에 타입을 지정해 안전하게 사용할 수 있도록 하는 기능이다.

 

2-2. 제네릭 기본 문법

제네릭은 클래스/인터페이스에 타입 파라미터(<T>,<K,V>)를 선언을 하고 구체적인 타입으로 사용 지점에서 치환을 합니다.

// 제네릭 클래스
public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

// 사용
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String data = stringBox.get(); // 캐스팅 불필요

 

2-3. 메서드 제네릭

클래스 전체가 제네릭이 아니어도, 메서드 단위에서 제네릭을 선언해 사용할 수 있다.

public class Util {
    public static <T> T pickFirst(T a, T b) {
        return a != null ? a : b;
    }
}

// 사용
String result = Util.pickFirst("A", "B");  // T는 String
Integer num   = Util.pickFirst(1, 2);      // T는 Integer

3.와일드 카드

3-1. 기본 개념

제네릭을 선언할 때 정확한 타입 대신 ?를 사용하면 와일드카드라고 부른다.

 

  • <?> : 어떤 타입이든 올 수 있다.
  • <? extends T> : T와 그 하위 타입만 허용.
  • <? super T> : T와 그 상위 타입만 허용.

3-2. 단순 와일드카드 (<?>)

List<?> list = new ArrayList<String>();
list.add(null); // null만 추가 가능
Object obj = list.get(0); // 꺼낼 때는 Object

->타입은 모르겠지만, 뭔가 담긴 리스트라는 뜻.

 

3-3. 상한 제한 와일드카드 (<? extends T>)

 

읽기 전용처럼 동작한다.

List<? extends Number> numbers = new ArrayList<Integer>();
Number n = numbers.get(0); // 읽기는 OK
// numbers.add(10); // 컴파일 에러 (추가 불가)

->Producer Extends: 값을 꺼낼 때만 안전하다.

 

3-4. 하한 제한 와일드카드 (<? super T>)

 

쓰기 전용처럼 동작한다.

List<? super Integer> integers = new ArrayList<Number>();
integers.add(10);  // 추가 가능
Object obj = integers.get(0); // 꺼낼 때는 Object

-> Consumer Super: 값을 넣을 때만 안전하다.

 

4.제네릭 소거

4-1. 제네릭 소거란?

 

자바에서 제네릭은 런타임 기능이 아니다. 컴파일 단계에서만 타입을 검사하고, 실제 실행 시점에는 타입 정보가 지워지는 방식을 쓴다. 이걸 제네릭 소거(Type Erasure)라고 한다. 덕분에 하위 호환성이 유지되지만, 몇 가지 제약이 생긴다.

 

4-1. 같은 클래스 취급

 

List<String>과 List<Integer>는 다른 타입처럼 보이지만, 컴파일이 끝나면 둘 다 단순히 ArrayList일 뿐이다.

List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();

System.out.println(strings.getClass());           // class java.util.ArrayList
System.out.println(integers.getClass());          // class java.util.ArrayList
System.out.println(strings.getClass() == integers.getClass()); // true

 

4-2. 상한(bound) 소거

 

타입 파라미터에 제한을 걸면, 결국 그 제한 타입으로 치환된다.

class NumberBox<T extends Number> {
    private T value;
    void set(T value) { this.value = value; }
    T get() { return value; }
}

NumberBox<Integer> intBox = new NumberBox<>();
intBox.set(100);
Number result = intBox.get(); // 반환 타입은 Number로 소거됨
System.out.println(result.getClass()); // class java.lang.Integer

 

4-3. 배열과의 충돌

 

제네릭 타입 배열은 만들 수 없다. 배열은 런타임에 타입 체크를 하지만, 제네릭은 타입 정보가 런타임에 없기 때문이다.

// List<String>[] array = new List<String>[10]; // 컴파일 에러
List<?>[] array = new List<?>[10];               // 이렇게만 가능
array[0] = Arrays.asList("hello");
System.out.println(array[0].get(0)); // hello

4-4. 오버로딩 불가

제네릭 소거 때문에, 타입 파라미터만 다른 메서드는 오버로딩이 불가능하다.

// 컴파일 시 둘 다 print(List list)로 소거됨 → 에러 발생
public void print(List<String> list) {}
public void print(List<Integer> list) {} // 컴파일 에러

5.정리 

마지막으로 제네릭을 공부한것을 다시 정리해보면 아래와 같습니다.

  • 제네릭이란? → 컴파일 시점에 타입을 강제해 캐스팅 문제를 줄이고, 코드 재사용성과 API 표현력을 높인다.
  • 와일드카드 → ?, extends, super를 통해 읽기 전용/쓰기 전용 제약을 표현할 수 있다. 
  • 제네릭 소거(Type Erasure) → 런타임에는 타입 정보가 사라져 같은 클래스 취급을 받으며, 배열/오버로딩/리플렉션에서 제약이 생긴다.

실제로 코드를 돌려보니, List<String>과 List<Integer>가 같은 클래스라는 점이나, 제네릭 배열이 안 되는 이유가 확실히 이해됐다. 그동안 막연히 “타입을 검사해준다” 정도로만 알고 있었는데, 이번에 정리하면서 제네릭의 원리와 한계까지 알 수 있었다.

제네릭은 단순히 문법적인 장식이 아니라, 안전하고 표현력 있는 API를 만드는 도구라는 점이 인상 깊다. 앞으로는 JPA의 JpaRepository<T, ID> 같은 실무 코드에서도 “왜 제네릭이 필요했는지”를 더 의식하면서 볼 수 있을 것 같다.

 

'Java' 카테고리의 다른 글

GC(Garbage Collector)  (0) 2025.09.06
== vs equals(), hashCode()  (0) 2025.09.06
Exception 처리 (Checked vs Unchecked)  (0) 2025.09.06
String vs StringBuilder vs StringBuffer  (0) 2025.09.05
GraalVM?  (0) 2025.08.05