자바 제네릭 클래스(Java Generic Class) 개념
Language/Java 2026. 9. 14. 18:04

자바 제네릭 클래스(Java Generic Class) 개념

@Beemo9
목차

G(PT)eneric

Java Generic(제네릭)이 등장한 이유와 핵심 개념

제네릭(Generic)이란?

제네릭은 클래스나 메서드가 사용할 데이터 타입을 미리 고정하지 않고, 사용하는 시점에 타입을 지정할 수 있도록 하는 기능이다.

쉽게 말해,

"타입을 변수처럼 받아서 클래스나 메서드를 일반화하는 기능"

이라고 볼 수 있다.

대표적인 예로 ArrayList가 있다.

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

같은 ArrayList 클래스지만 사용하는 타입에 따라 전혀 다른 타입의 리스트처럼 동작한다.

 


제네릭은 왜 등장했을까?

제네릭의 가장 중요한 목적은

런타임 오류를 컴파일 타임 오류로 앞당기기 위해서

이다.

과거에는 다양한 타입을 저장하기 위해 Object를 사용했다.

List list = new ArrayList();

list.add("Hello");
list.add(100);

Object는 모든 클래스의 부모이므로 어떤 타입이든 저장할 수 있다.

문제는 꺼낼 때 발생한다.

String value = (String) list.get(1);

실제로는 Integer가 들어있는데 String으로 캐스팅하면 런타임에 예외가 발생한다.

ClassCastException

즉,

  • 저장할 때는 문제 없음
  • 컴파일도 성공
  • 실행 중에 오류 발생

이라는 문제가 있었다.

 


Object의 한계

Object를 사용하는 방식은 타입 검증을 개발자가 직접 해야 한다.

Object obj = list.get(0);

if(obj instanceof String){
    String str = (String)obj;
}

이처럼 매번 타입을 확인하고 캐스팅해야 한다.

즉,

  • 실수하기 쉽다.
  • 캐스팅 코드가 많아진다.
  • 런타임 오류가 발생할 수 있다.

 


제네릭이 해결한 문제

제네릭을 사용하면 컴파일러가 타입을 추적할 수 있다.

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

list.add("Hello");
list.add("World");

이때 아래 코드는 컴파일 자체가 되지 않는다.

list.add(100);

오류가 실행 중이 아니라 개발 단계에서 발견된다.

따라서 제네릭의 핵심 가치는

타입 안정성(Type Safety)

이다.

캐스팅 제거는 부가적인 이점일 뿐이다.

String value = list.get(0);

더 이상 캐스팅이 필요하지 않다.

 


제네릭의 본질

제네릭은

타입 간의 관계를 컴파일러가 안전하게 관리하도록 만드는 시스템

이라고 볼 수 있다.

라이브러리 작성자는 특정 타입에 종속되지 않은 코드를 만들고,

사용자는 원하는 타입을 지정할 수 있다.

Box<String>
Box<Integer>
Box<User>

동일한 코드가 다양한 타입에 대해 안전하게 동작한다.

 


제네릭 클래스

클래스 선언 시 타입을 매개변수로 받을 수 있다.

public class Box<T> {

    private T value;

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

    public T get(){
        return value;
    }
}

사용 예시

Box<String> box = new Box<>();
box.set("Hello");

String value = box.get();

컴파일러는 T를 String으로 간주하여 타입 검사를 수행한다.

 

제네릭 클래스 활용

1. 공통 응답 규격

: API 개발 시 상태 코드, 메시지 등의 공통 구조는 유지하면서 실제 전달할 데이터(data)의 타입만 매번 다를 때 사용한다.

public class ApiResponse<T> {

    private final T data;  //제네릭 필드

    private final Integer status;

    private final String msg;
    
}

2. 값이 있을 수도/없을 수도 - Optional

 


T는 특별한 키워드가 아니다

많은 사람들이 T를 자바 문법의 예약어처럼 생각하지만 그렇지 않다.

public class Box<MyType> {
}

이렇게 작성해도 된다.

다만 관례적으로 아래 이름들을 사용한다.

이름 의미
T Type
E Element
K Key
V Value
N Number
R Return

즉, T는 단순한 이름일 뿐이다.

 


제네릭 메서드

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

public static <T> T identity(T value){
    return value;
}

구조를 보면 다음과 같다.

public static <T> T identity(T value)
             │   │         │
             │   │         └─ 매개변수 타입
             │   └─────────── 반환 타입
             └─────────────── 타입 매개변수 선언

여기서

<T>

는 해당 메서드가 사용할 타입 매개변수를 선언하는 부분이다.

없으면 컴파일러는 T가 무엇인지 알 수 없다.

 


 

클래스 제네릭과 메서드 제네릭은 다르다

다음 코드를 보자.

public class Box<T> {

    public <T> T print(T value){
        return value;
    }
}

위 코드의 두 T는 서로 다른 타입 매개변수다.

즉,

  • 클래스의 T
  • 메서드의 T

는 독립적으로 동작한다.

또한 제네릭 메서드는 제네릭 클래스 내부에만 존재하는 것도 아니다.

public class Util {

    public static <T> T identity(T value){
        return value;
    }
}

일반 클래스에서도 얼마든지 사용할 수 있다.

 


다이아몬드 연산자(<>)와 와일드카드(?)

다이아몬드 연산자

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

<>는 왼쪽 타입을 보고 컴파일러가 타입을 추론한다.

위 코드는 사실상 다음과 같다.

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

 


 

와일드카드

List<?> list

"어떤 타입인지는 모르지만 무언가의 리스트"

라는 의미이다.

List<String>
List<Integer>
List<User>

모두 받을 수 있다.

중요한 점은

와일드카드는 타입 추론이 아니라 타입 미지정 상태를 의미한다.

는 것이다.

 

다만 타입 정보를 알 수 없기 때문에 요소를 꺼낼 때는 가장 상위 타입인 Object로만 받을 수 있다.

Object value = list.get(0);
 

컴파일러는 리스트 내부에 어떤 타입이 들어있는지 보장할 수 없기 때문이다.

또한 와일드카드는 타입이 아직 정해지지 않은 상태를 의미하지 않는다.

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

위 코드에서 실제 타입은 이미 String으로 정해져 있다.

다만 list를 사용하는 코드 입장에서는 그 타입을 알 수 없도록 표현한 것이다.

즉,

와일드카드는 "타입 미정"이 아니라 "타입 비공개"에 가깝다.

 

 


런타임에는 어떻게 동작할까?

흥미로운 점은 제네릭이 컴파일 단계에서만 존재한다는 것이다.

List<String>
List<Integer>

는 컴파일 이후 타입 정보가 제거된다.

이를 Type Erasure(타입 소거) 라고 한다.

실행 시점에는 대략 다음과 같이 동작한다.

List

또는

Object

기반 코드로 변환된다.

즉,

  • 컴파일 시점 → 타입 검사 수행
  • 런타임 시점 → 타입 정보 제거

라는 방식으로 동작한다.

 


정리

제네릭의 핵심은 단순히 T를 사용하는 문법이 아니다.

과거 Object 기반 프로그래밍의 한계를 해결하기 위해 등장했으며,

  1. 타입 안정성을 확보하고
  2. 런타임 오류를 컴파일 타임 오류로 전환하며
  3. 불필요한 캐스팅을 제거하고
  4. 타입 관계를 컴파일러가 관리할 수 있도록 만든다.

한 문장으로 요약하면,

제네릭은 "타입 간의 관계를 컴파일러가 안전하게 관리하기 위한 시스템"이다.

Beemo9
@Beemo9
개발 기술 블로그, Dev 포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!
image