자바 백엔드 입문 6편 — 자바 제네릭

2026-05-17자바 백엔드 입문

자바 백엔드 입문 6편. List·Repository 같은 표현의 정체. 자바 제네릭이 어떻게 타입 안전을 보장하는지 도시락 통 비유로 풀어쓴 학습 노트.

📚 자바 백엔드 입문 · 6편 — 자바 제네릭

이 글은 자바 백엔드 입문 시리즈 59편 중 6편이에요. 5편 자바 컬렉션 에서 매번 등장한 List<String><> 부분 — 자바 제네릭 의 정체를 풀어 가요.

제네릭이 헷갈리는 이유

자바 코드 거의 어디에나 <T>·<E>·<K, V> 같은 표현이 박혀 있어요. 처음 보면 "이게 뭘 의미하지?" 가 안 잡혀요. 또 <? extends Number> 같은 와일드카드도 등장해 복잡.

이 글에서는 도시락 통 비유로 풀어요. List<String> = "문자열 전용 도시락 통", Map<Long, User> = "Long 키 → User 값 도시락 통". 도시락 통 자체는 같은데, "무엇을 담는지" 가 타입 매개변수로 명시.

제네릭이 등장한 배경

자바 5(2004) 이전엔 컬렉션이 "아무거나 담을 수 있는 도시락 통" 이었어요.

// 자바 1.4 시절 — 타입 명시 X
List names = new ArrayList();
names.add("Alice");
names.add(123);                      // 정수도 박힘 (의도와 다름)
String name = (String) names.get(0); // 매번 캐스팅 필요
String wrong = (String) names.get(1); // ClassCastException! (123은 String 아님)

문제: - 타입 안전 X — 의도와 다른 객체 박혀도 컴파일 통과 - 매번 캐스팅 — 코드 더러움 - 런타임 에러 — 캐스팅 실패가 실행 중 폭발

자바 5에서 제네릭 도입.

// 자바 5+ — 타입 명시
List<String> names = new ArrayList<>();
names.add("Alice");
names.add(123);                      // 컴파일 에러! Integer는 String 아님
String name = names.get(0);          // 캐스팅 불필요

컴파일 시점에 타입 안전 보장 + 캐스팅 자동. 자바 코드의 신뢰성·가독성 한 단계 상승.

타입 매개변수 — T·E·K·V

제네릭 정의 시 사용하는 "타입 변수". 컨벤션이 있어요.

글자 의미
T Type (일반)
E Element (컬렉션 요소)
K Key (Map 키)
V Value (Map 값)
N Number
R Return type
public interface List<E> {           // E = Element
    void add(E element);
    E get(int index);
}

public interface Map<K, V> {         // K = Key, V = Value
    V put(K key, V value);
    V get(K key);
}

<E>·<K, V>"이 클래스를 쓸 때 타입을 채워 넣을 자리". 이걸 타입 매개변수(Type Parameter).

제네릭 클래스 직접 만들기

타입 안전한 "한 개 값 박스" 만들기.

public class Box<T> {                // 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 s = stringBox.get();           // 캐스팅 불필요

Box<Integer> intBox = new Box<>();
intBox.set(42);
intBox.set("문자열");                // ❌ 컴파일 에러

같은 Box 클래스인데 — <String> 박으면 문자열 전용, <Integer> 박으면 정수 전용. 도시락 통 비유.

제네릭 메서드

클래스 자체는 제네릭이 아닌데 — 특정 메서드만 제네릭으로.

public class Utils {

    public static <T> T pickFirst(List<T> list) {       // <T> 메서드 레벨 선언
        return list.isEmpty() ? null : list.get(0);
    }
}

// 사용
String s = Utils.pickFirst(List.of("Alice", "Bob"));   // T = String 자동 추론
Integer n = Utils.pickFirst(List.of(1, 2, 3));         // T = Integer 자동

메서드 시그니처 앞에 <T> 박으면 — 그 메서드 안에서 T 사용 가능. 호출 시 자동 추론.

Bounded — <T extends Number>

타입 매개변수에 "제약" 박기.

public class NumberBox<T extends Number> {           // T는 Number 또는 그 자식
    private T value;
    public double doubled() {
        return value.doubleValue() * 2;              // Number 메서드 호출 가능
    }
}

NumberBox<Integer> intBox = new NumberBox<>();      // OK — Integer는 Number 자식
NumberBox<Double> doubleBox = new NumberBox<>();    // OK
NumberBox<String> wrong = new NumberBox<>();        // ❌ String은 Number 자식 X

<T extends Number> = "T는 Number 또는 그 하위 클래스만". 그 안에서 Number의 메서드(doubleValue·intValue 등) 호출 가능.

와일드카드 <?> — 읽기 전용

이건 함수 매개변수에 자주.

// 어떤 타입이든 받지만 — 안의 요소를 정확한 타입으로 다룰 수 없음
public void printAll(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}

printAll(List.of("a", "b"));         // OK
printAll(List.of(1, 2, 3));          // OK

<?> = "무엇이든 OK, 다만 안에 박을 수는 없음". "읽기 전용 매개변수" 같은 용도.

상·하한 와일드카드

List<? extends Number> readOnly;     // Number 또는 자식의 List만 받음 (읽기)
List<? super Integer> writeOnly;     // Integer 또는 부모의 List 받음 (쓰기 가능)

이건 입문 단계에서 깊이 X. "PECS — Producer Extends, Consumer Super" 라는 룰이 있는데, 처음엔 신경 안 써도 OK.

타입 소거 — JVM의 비밀

자바 제네릭의 흥미로운 사실 — 컴파일 후엔 타입 정보가 사라져요. 이걸 타입 소거(Type Erasure).

// 우리가 쓰는 코드
List<String> names = new ArrayList<>();

// 컴파일 후 .class 파일 안 (개념)
List names = new ArrayList();        // <String> 사라짐

JVM은 "리스트" 만 알지 "문자열 리스트" 라는 정보는 런타임에 없어요. 그래서:

// 리플렉션으로 타입 확인
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.getClass() == intList.getClass());   // true (둘 다 ArrayList.class)

"타입 소거" 가 자바 제네릭의 한계 — 런타임에 T.class 같은 표현을 직접 못 씀. JPA·Jackson 같은 라이브러리가 "TypeReference<List<User>>" 같은 우회 패턴을 쓰는 이유.

Spring·JPA에서 자주 보는 제네릭

이 시리즈의 거의 모든 글에서 등장.

// JPA Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    Optional<Order> findById(Long id);
    List<Order> findByStatus(String status);
}

// Service
public Optional<User> findByEmail(String email) { ... }

// Controller
public ResponseEntity<List<Order>> list() { ... }

<Order, Long>·<Order>·<List<Order>> — 다 제네릭. 자바 백엔드의 일상.

🎯 입문자 핵심

제네릭은 "이 컨테이너에 무엇이 들어가는지 명시" 도구. List<String> 한 줄로 컴파일러가 타입 검증 자동. <T extends ...> 와 와일드카드는 깊이 가면 복잡하지만 — 입문에선 "클래스 이름 옆 <타입>" 만 알면 80% 끝.

한 줄 정리 — 자바 제네릭 = 컴파일 시점 타입 안전 도구. <T> 타입 매개변수로 "이 컨테이너에 무엇이 들어가나" 명시. ArrayList·HashMap 같은 컬렉션이 핵심 사용처. 런타임엔 타입 소거로 정보 사라짐.

시험 직전 한 번 더 — 자바 제네릭 입문자가 매번 헷갈리는 것

  • 제네릭 = 컴파일 시점 타입 안전 도구 (자바 5+)
  • 도입 전 = List names·매번 캐스팅·런타임 에러
  • 도입 후 = List<String>·캐스팅 불필요·컴파일 시점 검증
  • 타입 매개변수 컨벤션 = T(Type)·E(Element)·K(Key)·V(Value)·N(Number)·R(Return)
  • 제네릭 클래스 = class Box<T> { ... }
  • 제네릭 메서드 = <T> T pickFirst(List<T> list)
  • Bounded = <T extends Number> — Number 자식만
  • 자바엔 <T super ...> 같은 클래스 레벨 하한 X (메서드 매개변수 와일드카드에만)
  • 와일드카드 <?> = 무엇이든, 읽기만
  • <? extends Number> = 읽기 (Number 자식들)
  • <? super Integer> = 쓰기 (Integer 부모들)
  • PECS = Producer Extends, Consumer Super (입문 단계 X)
  • 타입 소거 = 컴파일 후 타입 정보 사라짐
  • 런타임에 T.class 직접 사용 X
  • TypeReference<List<User>> = 우회 패턴 (Jackson 등)
  • <> 다이아몬드 연산자 = 자바 7+ — 우변 타입 자동 추론
  • new ArrayList<String>()new ArrayList<>() (자바 7+)
  • 제네릭 배열 생성 X — new T[10] 불가
  • 자바 백엔드 = 매일 제네릭 다룸
  • Spring·JPA·Jackson 모두 제네릭 기반

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!