본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
10장 : 제네릭

PECS로 컬렉션 API 설계하기

제네릭 컬렉션을 받는 메서드가 값을 읽는지 쓰는지에 따라 와일드카드 방향이 달라집니다.

Producer Extends, Consumer Super라는 PECS 규칙은 이 선택을 짧게 기억하게 합니다.

단순히 목록을 순회해 Number를 읽는다면 하위 원소 목록도 받을 수 있어야 합니다.

Integer 값을 대상 목록에 복사한다면 Integer의 상위 타입 목록도 받을 수 있습니다.

이 절에서는 복사 메서드의 두 경계를 실제로 연결합니다.

PECS는 선언부만 보고 기계적으로 붙이는 장식이 아닙니다. 메서드 본문에서 각 매개변수로부터 값을 꺼내는지, 그 안에 값을 넣는지 화살표를 그리면 방향이 결정됩니다. 같은 컬렉션이 두 역할을 모두 한다면 와일드카드보다 정확한 타입 매개변수가 더 적합할 수 있습니다.


생산자는 extends로 읽기 범위를 넓힌다

소스가 T 값을 제공한다면 List<? extends T>로 T 또는 하위 타입 목록을 받을 수 있습니다.

소스에는 값을 쓰지 않습니다.


소비자는 super로 쓰기 대상을 넓힌다

대상이 T를 받아야 한다면 List<? super T>로 T, 상위 타입 목록을 받을 수 있습니다.

읽을 때는 Object로만 안전합니다.

PecsCopy.java는 지금 설명한 규칙을 실제 값의 변화로 보여 줍니다.

PecsCopy.java
import java.util.ArrayList;
import java.util.List;

public class PecsCopy {
    static <T> void copy(List<? super T> target, List<? extends T> source) {
        for (T value : source) target.add(value);
    }

    public static void main(String[] args) {
        List<Integer> source = List.of(1, 2, 3);
        List<Number> numbers = new ArrayList<>();
        List<Object> objects = new ArrayList<>();
        copy(numbers, source);
        copy(objects, source);
        System.out.println(numbers);
        System.out.println(objects);
    }
}

copy에서는 T를 Integer로 두고 Number가 소비자가 되며, 둘째 호출에서는 Object가 소비자가 됩니다. 소스 코드는 같아도 대상이 받아들일 수 있는 상위 타입 범위가 달라집니다.

[1, 2, 3]
[1, 2, 3]

source는 Integer를 생산하고 Number/Object 목록은 Integer를 소비하므로 하나의 T로 안전하게 연결됩니다.

source에서 T 또는 하위 값을 읽기

소스가 T 값을 제공한다면 List로 T 또는 하위 타입 목록을 받을 수 있습니다.

T로 관계를 보존

대상이 T를 받아야 한다면 List로 T, 상위 타입 목록을 받을 수 있습니다.

target의 T 또는 상위 타입 위치에 추가

copy(List dst, List src)의 T가 소스 원소와 대상 허용 타입의 관계를 묶습니다.


타입 매개변수가 두 와일드카드를 연결한다

copy(List<? super T> dst, List<? extends T> src)의 T가 소스 원소와 대상 허용 타입의 관계를 묶습니다.

읽기 전용 생산자와 쓰기 전용 소비자를 작은 메서드로 분리하면 각 와일드카드에서 허용되는 연산이 명확해집니다.

비교 1
static double sum(List<? extends Number> producer) {
    return producer.stream().mapToDouble(Number::doubleValue).sum();
}
비교 2
static void appendOne(List<? super Integer> consumer) {
    consumer.add(1);
}
비교 3
// producer.add(1);          // 컴파일 오류
// Integer n = consumer.get(0); // 컴파일 오류: Object만 보장

불필요한 유연성은 API를 어렵게 한다

정확히 List<String>만 다루는 내부 메서드까지 와일드카드를 쓰지 않습니다.

실제 호출자가 필요로 하는 변성만 제공합니다.


읽고 쓰는 양쪽 요구가 있으면 타입 매개변수를 고려한다

한 컬렉션에서 값을 읽고 같은 타입을 다시 써야 하면 와일드카드 하나로 표현하기 어렵습니다.

메서드 타입 매개변수로 두 위치의 관계를 명시합니다.


반환 타입에는 불필요한 와일드카드를 피한다

호출자가 결과를 사용할 때 캡처 타입을 처리하게 만들 수 있습니다.

생산자가 소유한 구체적인 제네릭 계약을 반환하는 편이 대체로 단순합니다.

소스를 List<Double>로 바꾸고 numbers에 복사해 보세요. Number 목록은 여전히 소비자가 될 수 있지만, Integer만 받는 대상에는 복사할 수 없습니다. 다음으로 copy(source, numbers)처럼 방향을 뒤집으면 읽기 전용 List.of와 타입 관계 두 가지 이유 중 무엇이 먼저 컴파일을 막는지 확인합니다.

공개 API에서는 가장 유연한 시그니처보다 호출자가 자연스럽게 사용할 수 있는 시그니처가 중요합니다. 입력은 필요한 만큼 넓게 받되 반환 타입까지 ? extends T로 노출하면 호출자가 캡처 타입을 떠안습니다. 메서드가 새 목록을 소유해 반환한다면 List<T>처럼 구체적인 계약이 대체로 낫습니다.


PECS는 외우는 구호보다 각 매개변수가 값을 내놓는지 받아들이는지를 먼저 표시하면 자연스럽게 결정됩니다.