|

자바 제네릭 와일드카드 extends super 차이: PECS 원칙 쉽게 이해하기

자바 제네릭 와일드카드 extends super PECS 정리 대표 이미지
자바 제네릭 와일드카드는 타입을 느슨하게 열어주지만, 읽기와 쓰기 가능 범위가 달라집니다.

extends super 차이는 자바 제네릭 와일드카드에서 가장 헷갈리는 지점입니다. `? extends T`와 `? super T`는 둘 다 타입 범위를 넓히는 문법처럼 보이지만, 실제 차이는 읽기와 쓰기 가능 여부에서 드러납니다.

핵심은 값을 꺼내 쓰는 생산자면 extends, 값을 넣는 소비자면 super입니다. 이 글은 Oracle Java Tutorial의 bounded wildcard 설명을 기준으로 PECS 원칙을 코드 예제로 정리합니다.

자바 제네릭 와일드카드 PECS 요약 카드
PECS는 Producer Extends, Consumer Super의 줄임말입니다.

자바 제네릭 와일드카드가 필요한 이유

제네릭은 타입 안정성을 지키면서 여러 타입에 재사용할 수 있는 코드를 만들게 해 줍니다. 그런데 `List`를 받는 함수는 `List`을 받을 수 없고, `List`도 단순히 모든 숫자 리스트를 받는 뜻이 아닙니다.

void printNumbers(List<Number> numbers) {
    for (Number number : numbers) {
        System.out.println(number);
    }
}

List<Integer> ints = List.of(1, 2, 3);
// printNumbers(ints); // 컴파일 오류

`Integer`는 `Number`의 하위 타입이지만, `List`가 `List`의 하위 타입은 아닙니다. 이때 와일드카드가 필요해집니다.

? extends T는 꺼내 읽기 좋다

`? extends Number`는 Number 또는 Number의 하위 타입 리스트를 받겠다는 뜻입니다. Integer, Double 같은 값을 Number로 꺼내 읽을 수 있습니다.

void printNumbers(List<? extends Number> numbers) {
    for (Number number : numbers) {
        System.out.println(number.doubleValue());
    }
}

printNumbers(List.of(1, 2, 3));
printNumbers(List.of(1.5, 2.5));

하지만 이 리스트에 새 Number를 마음대로 넣기는 어렵습니다. 실제 리스트가 `List`일 수도 있는데, 거기에 `Double`을 넣으면 타입 안정성이 깨지기 때문입니다.

? super T는 넣기 좋다

`? super Integer`는 Integer 또는 Integer의 상위 타입 리스트를 받겠다는 뜻입니다. `List`, `List`, `List`가 후보가 될 수 있습니다.

void addIntegers(List<? super Integer> numbers) {
    numbers.add(1);
    numbers.add(2);
}

List<Number> numbers = new ArrayList<>();
addIntegers(numbers);

이 경우 Integer를 넣는 것은 안전합니다. 실제 리스트가 Integer를 받을 수 있는 타입이라는 하한이 보장되기 때문입니다. 대신 꺼낼 때는 정확한 타입을 알 수 없어 보통 Object로만 안전하게 받을 수 있습니다.

extends super와 PECS 원칙

PECS는 Producer Extends, Consumer Super의 줄임말입니다. 컬렉션이 값을 생산해서 내가 꺼내 읽는 역할이면 `extends`, 내가 값을 넣는 소비자 역할이면 `super`를 쓰라는 기준입니다.

  • Producer Extends: 값을 꺼내 읽는 쪽이면 `? extends T`
  • Consumer Super: 값을 넣는 쪽이면 `? super T`
  • 읽기도 하고 쓰기도 해야 하면 와일드카드보다 명확한 타입 파라미터가 나을 수 있다

복사 예제로 보는 extends와 super

가장 유명한 예는 한 리스트에서 값을 읽어 다른 리스트로 복사하는 함수입니다. source는 값을 생산하므로 extends, destination은 값을 소비하므로 super가 자연스럽습니다.

static <T> void copy(List<? extends T> source, List<? super T> destination) {
    for (T item : source) {
        destination.add(item);
    }
}

List<Integer> ints = List.of(1, 2, 3);
List<Number> numbers = new ArrayList<>();
copy(ints, numbers);

이 예제를 기억하면 PECS가 단순 암기가 아니라 데이터 이동 방향을 설명하는 규칙이라는 점이 보입니다.

자주 생기는 실수

  • `List`가 `List`를 받을 수 있다고 착각한다
  • `? extends T`에 값을 넣으려다가 컴파일 오류를 만난다
  • `? super T`에서 꺼낸 값을 바로 T로 받으려 한다
  • 읽기와 쓰기를 모두 해야 하는데 와일드카드로 억지로 해결하려 한다

실무 판단 기준

  1. 함수가 컬렉션에서 값을 꺼내기만 하는지 확인한다
  2. 꺼내기만 한다면 `? extends T`를 검토한다
  3. 함수가 컬렉션에 값을 넣는지 확인한다
  4. 넣기만 한다면 `? super T`를 검토한다
  5. 둘 다 필요하면 `` 타입 파라미터나 더 구체적인 타입을 사용한다

정리

자바 제네릭 와일드카드는 타입을 넓게 받기 위한 문법이지만, 아무렇게나 읽고 쓸 수 있게 해 주지는 않습니다. `extends`는 읽기, `super`는 쓰기라는 방향을 먼저 잡아야 합니다.

Java 실무 글과 함께 보려면 Spring Security 401과 403 차이, Spring Boot JWT 인증 구현, Spring Data JPA countQuery는 언제 직접 써야 할까가 도움이 됩니다. 외부 기준은 Oracle Java Tutorial – Wildcards, Oracle Java Tutorial – Upper Bounded Wildcards, Oracle Java Tutorial – Lower Bounded Wildcards를 확인했습니다.

함께보면 좋은 글