3주차 Unit 5.5 — PECS 원칙

Psj·2026년 5월 19일

F-lab

목록 보기
96/239

Unit 5.5 — PECS 원칙

F-LAB JAVA · 3주차 · Phase 5 · 제네릭과 와일드카드
🎯 마스터 프롬프트 깊이 Unit — 🏆 Phase 5 완주


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • PECS 원칙 의 정확한 의미와 적용 방법은?
  • Producer Extends 의 입출력 분석은?
  • Consumer Super 의 입출력 분석은?
  • PECS 결정 알고리즘 의 4단계는?
  • Comparable<? super T> 의 정확한 의미는?
  • Collections.copy 시그니처 의 정밀 분석은?
  • Collections.max, min 의 PECS 활용은?
  • 자체 라이브러리 설계 에 PECS 적용 가이드는?
  • Java 표준 라이브러리의 모든 PECS 활용 패턴 종합은?
  • Phase 5 의 50가지 졸업 시험 통과 가능?

🎯 핵심 한 문장

PECS 원칙 — Producer Extends, Consumer Super — 은 제네릭 매개변수 설계의 황금률이다.
데이터를 꺼내는 매개변수는 ? extends T (Producer, 공변), 데이터를 넣는 매개변수는 ? super T (Consumer, 반공변).
자바 표준 라이브러리의 모든 정교한 제네릭 시그니처가 이 원칙을 따르며,
Collections.copy(List<? super T> dest, List<? extends T> src) 가 가장 완벽한 PECS 모범.
이 Unit 을 마치면 어떤 제네릭 메서드 시그니처도 자신 있게 설계 가능.

비유 — 강물의 흐름

PECS = 강물의 방향성

Producer (생산자):
  강의 상류 — 물이 흘러나옴
  - 강에서 물 받는 곳 (input)
  - 자식들 (구체적 종류) 도 됨
  - "? extends Number" = "Number 이거나 자식 (Integer, Double, ...)"

Consumer (소비자):
  강의 하류 — 물이 흘러들어감
  - 강에 물을 보내는 곳 (output)
  - 부모들 (더 넓은 통) 도 됨
  - "? super Integer" = "Integer 이거나 부모 (Number, Object)"

PECS = 강의 양쪽:
  - 상류 (extends): 데이터 출발지
  - 하류 (super): 데이터 도착지

→ PECS = 데이터 흐름의 두 방향.


🧭 9개 섹션 로드맵

1. PECS 원칙의 정확한 의미
2. Producer Extends 의 깊은 분석
3. Consumer Super 의 깊은 분석
4. PECS 결정 알고리즘 4단계
5. Comparable<? super T> 의 정밀
6. Collections 의 모든 PECS 분석
7. Stream, Function, Optional 의 PECS
8. 자체 라이브러리 설계 가이드
9. Phase 5 졸업 시험 + Phase 5 완주

1️⃣ PECS 원칙의 정확한 의미

1.1 PECS 의 정의

PECS:
  Producer Extends, Consumer Super

해석:
  - 매개변수가 데이터를 "생산" (외부로 제공) 한다 → `? extends T`
  - 매개변수가 데이터를 "소비" (외부에서 받음) 한다 → `? super T`

용도:
  제네릭 메서드 매개변수의 와일드카드 선택 가이드.

1.2 출처 — Effective Java

Joshua Bloch 의 Effective Java 에서 정립:

  Item 31: 한정적 와일드카드를 사용해 API 유연성을 높이라
  
  Producer Extends, Consumer Super (PECS)
  
  - 매개변수가 PE 처럼 사용되면 ? extends T
  - 매개변수가 CS 처럼 사용되면 ? super T

1.3 PE 의 시각화

public void process(List<? extends Number> producer) {
    //                       ↑
    //                       데이터를 꺼내는 매개변수
    
    for (Number n : producer) {   // 꺼냄 (read)
        System.out.println(n);
    }
    
    Number first = producer.get(0);   // 꺼냄
    
    // producer.add(...) → 추가 불가 (Producer 라서)
}

// 호출
List<Integer> ints = List.of(1, 2, 3);
process(ints);   // ints 는 메서드에 데이터 제공
//                  → ints 는 Producer

1.4 CS 의 시각화

public void fill(List<? super Integer> consumer) {
    //                  ↑
    //                  데이터를 받는 매개변수
    
    for (int i = 0; i < 10; i++) {
        consumer.add(i);   // 넣음 (write)
    }
    
    // Object o = consumer.get(0);   → 읽기는 Object 로만
}

// 호출
List<Number> nums = new ArrayList<>();
fill(nums);   // nums 는 메서드에 데이터를 받음
//             → nums 는 Consumer

1.5 PE 와 CS 비교 표

항목Producer ExtendsConsumer Super
와일드카드<? extends T><? super T>
방향자식 (좁은)부모 (넓은)
변성공변 (Covariant)반공변 (Contravariant)
읽기T 로 가능Object 로만
쓰기null 제외 불가T 와 자식 가능
역할데이터 제공데이터 수용
메서드 입출력입력 (in)출력 (out)
List<? extends Number>List<? super Integer>

1.6 PECS 의 동시 활용

// Collections.copy — 가장 완벽한 PECS 모범
public static <T> void copy(
        List<? super T> dest,        // Consumer (받음)
        List<? extends T> src) {     // Producer (제공)
    for (T t : src) {
        dest.add(t);
    }
}

// 사용
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();

copy(nums, ints);
// dest: nums (List<Number>) — Number 부모 → Integer 받음
// src: ints (List<Integer>) — Integer 또는 자식 → 꺼냄
// T = Integer 추론

1.7 ILIC 적용 예

public class ShipmentUtils {
    
    // Producer (꺼내기) — extends
    public static BigDecimal totalWeight(
            List<? extends Shipment> shipments) {
        BigDecimal total = BigDecimal.ZERO;
        for (Shipment s : shipments) {   // 꺼내기
            total = total.add(s.getWeight());
        }
        return total;
    }
    
    // Consumer (추가) — super
    public static void collectAll(
            List<? super Shipment> destination,
            List<Shipment> source) {
        for (Shipment s : source) {
            destination.add(s);   // 추가
        }
    }
    
    // 둘 다 — PECS
    public static <T extends Shipment> void copy(
            List<? extends T> source,        // Producer
            List<? super T> destination) {   // Consumer
        for (T s : source) {
            destination.add(s);
        }
    }
}

// 사용
List<SeaShipment> seaShipments = ...;
List<Object> allObjects = new ArrayList<>();

BigDecimal total = ShipmentUtils.totalWeight(seaShipments);
ShipmentUtils.collectAll(allObjects, ...);
ShipmentUtils.copy(seaShipments, allObjects);

1.8 자기 점검 답변

PECS 의 정확한 의미와 적용 방법은?

:

  • Producer Extends: 데이터 꺼내는 매개변수 → ? extends T
  • Consumer Super: 데이터 추가하는 매개변수 → ? super T

적용:
1. 매개변수의 역할 분석 (PE? CS? 둘 다?)
2. 그에 맞는 와일드카드 선택
3. 읽기/쓰기 동작 검증
4. 호출자의 유연성 ↑

자바 표준 라이브러리의 황금률.


2️⃣ Producer Extends 의 깊은 분석

2.1 PE 의 본질

Producer Extends:

  "데이터를 외부에 제공하는 매개변수는 자식 타입을 모두 받을 수 있어야 한다."
  
  → 공변성 (Covariance)
  → ? extends T

2.2 PE 가 필요한 이유

// 문제: 불공변성
public void sum(List<Number> numbers) {
    double total = 0;
    for (Number n : numbers) {
        total += n.doubleValue();
    }
}

// 호출 못 함
List<Integer> ints = List.of(1, 2, 3);
sum(ints);   // ❌ List<Integer> 는 List<Number> 자식 아님

// PE 해결
public void sum(List<? extends Number> numbers) {
    double total = 0;
    for (Number n : numbers) {
        total += n.doubleValue();
    }
}

// 호출 OK
sum(ints);   // ✓ List<Integer> 가 List<? extends Number> 의 일종

2.3 PE 의 읽기 동작

public void process(List<? extends Number> list) {
    
    // ✓ 읽기 (Number 로 받음)
    Number first = list.get(0);
    for (Number n : list) { ... }
    int size = list.size();
    
    // 캐스트로 더 좁은 타입 받기 가능 (이지만 권장 X)
    Object obj = list.get(0);   // 너무 넓음
    
    // 안전한 읽기 보장:
    // - list 가 List<Integer> 면 Integer → Number ✓
    // - list 가 List<Double> 면 Double → Number ✓
    // - 어떤 자식이든 Number 로 안전
}

2.4 PE 의 쓰기 제약

public void process(List<? extends Number> list) {
    
    // ❌ 쓰기 (어떤 자식인지 모름)
    // list.add(1);                          // Integer? Double?
    // list.add(1.0);                        // Integer 면 깨짐
    // list.add(new BigDecimal("1"));        // 위험
    
    // ✓ null 만 가능
    list.add(null);
    
    // ✓ 같은 list 의 요소는 가능 (런타임 안전)
    list.add(list.get(0));   // ❌ 컴파일 에러 (정적으로 안전 보장 X)
    
    // 이유:
    // List<Integer> 일 수도, List<Double> 일 수도
    // 어떤 타입을 추가하든 안전 보장 못 함
    
    // 쓰기가 필요하면 ? super T (Consumer) 사용
}

2.5 PE 활용 패턴

// 1. 데이터 합치기
public static <T> List<T> concat(
        List<? extends T> a,
        List<? extends T> b) {
    List<T> result = new ArrayList<>();
    result.addAll(a);
    result.addAll(b);
    return result;
}

// 2. 집계
public static <T> long count(
        Collection<? extends T> coll,
        Predicate<? super T> filter) {
    return coll.stream().filter(filter).count();
}

// 3. 최대값
public static <T extends Comparable<? super T>> T max(
        Collection<? extends T> coll) {
    Iterator<? extends T> it = coll.iterator();
    T max = it.next();
    while (it.hasNext()) {
        T next = it.next();
        if (next.compareTo(max) > 0) {
            max = next;
        }
    }
    return max;
}

// 4. 변환
public static <T, R> List<R> map(
        List<? extends T> source,
        Function<? super T, ? extends R> mapper) {
    return source.stream().map(mapper).toList();
}

2.6 PE 의 자바 표준 활용

// Collection.addAll
public boolean addAll(Collection<? extends E> c) {
    //                         ↑
    //                         c 는 Producer (데이터 제공)
    // 자신에게 c 의 데이터 추가
}

// Stream.of
public static <T> Stream<T> of(T... values) { ... }

public static <T> Stream<T> of(
        Stream<? extends T>... values) {
    // values 의 각 Stream 이 Producer
}

// Collectors
public static <T, K, U> Collector<T, ?, Map<K, U>> toMap(
        Function<? super T, ? extends K> keyMapper,
        //                  ↑
        //                  ? extends K — K 자식의 키 생산
        Function<? super T, ? extends U> valueMapper) {
}

2.7 ILIC PE 패턴

public class ShipmentAnalytics {
    
    // 1. 총 무게 — Producer
    public static BigDecimal totalWeight(
            List<? extends Shipment> shipments) {
        return shipments.stream()
            .map(Shipment::getWeight)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    // 2. 가장 무거운 — Producer
    public static Optional<Shipment> heaviest(
            Collection<? extends Shipment> shipments) {
        return shipments.stream()
            .max(Comparator.comparing(Shipment::getWeight));
    }
    
    // 3. 그룹핑 — Producer
    public static <T extends Shipment, K> Map<K, List<T>> groupBy(
            Collection<? extends T> shipments,
            Function<? super T, ? extends K> keyExtractor) {
        return shipments.stream()
            .collect(Collectors.groupingBy(keyExtractor));
    }
    
    // 4. 평균 — Producer
    public static BigDecimal averageWeight(
            Collection<? extends Shipment> shipments) {
        if (shipments.isEmpty()) return BigDecimal.ZERO;
        
        BigDecimal total = totalWeight(new ArrayList<>(shipments));
        return total.divide(BigDecimal.valueOf(shipments.size()));
    }
}

// 사용
List<SeaShipment> seaShipments = ...;
List<AirShipment> airShipments = ...;

BigDecimal seaTotal = ShipmentAnalytics.totalWeight(seaShipments);   // ✓
BigDecimal airTotal = ShipmentAnalytics.totalWeight(airShipments);   // ✓

2.8 자기 점검 답변

Producer Extends 의 본질은?

:

  • 본질:

    • 매개변수가 데이터를 외부에 제공
    • 공변성 (Covariance)
    • 자식 타입 모두 받음
  • 동작:

    • 읽기: T 로 가능
    • 쓰기: null 만 (어떤 자식인지 모름)
  • 활용:

    • 데이터 소스 (input)
    • 컬렉션 순회
    • 함수의 입력
  • :

    • Collection.addAll(Collection<? extends E>)
    • Function<? super T, ? extends R> 의 R 부분
    • Comparator<? super T> 의 입력

3️⃣ Consumer Super 의 깊은 분석

3.1 CS 의 본질

Consumer Super:

  "데이터를 받는 매개변수는 부모 타입을 모두 받을 수 있어야 한다."
  
  → 반공변성 (Contravariance)
  → ? super T

3.2 CS 가 필요한 이유

// 문제: 데이터 받는 곳을 더 유연하게

// 좁은 매개변수
public void addInteger(List<Integer> list, Integer value) {
    list.add(value);
}

// List<Integer> 만 받음 → 유연성 ↓
List<Number> nums = new ArrayList<>();
addInteger(nums, 42);   // ❌ List<Number> 못 받음

// CS 해결
public void addInteger(List<? super Integer> list, Integer value) {
    list.add(value);
}

// 호출 OK
addInteger(nums, 42);                  // ✓ List<Number>
addInteger(new ArrayList<Object>(), 42);   // ✓ List<Object>

3.3 CS 의 쓰기 동작

public void process(List<? super Integer> list) {
    
    // ✓ 쓰기 (Integer 와 자식 가능)
    list.add(42);
    list.add(Integer.MAX_VALUE);
    list.add(Integer.valueOf(0));
    
    // Integer 의 자식 (final 이라 없음, 가상)
    // 만약 SubInteger extends Integer 면 추가 가능
    
    // ❌ Number 추가 (Integer 부모)
    // list.add(3.14);                  // Double 은 Integer 아님
    // list.add(BigInteger.ONE);        // BigInteger 는 Integer 아님
    
    // 이유:
    // list 가 List<Integer> 면 → Integer 만 안전
    // list 가 List<Number> 면 → Number 자식 OK
    // 가장 좁은 보장은 Integer
}

3.4 CS 의 읽기 제약

public void process(List<? super Integer> list) {
    
    // ❌ 정확한 타입 모름
    // Integer i = list.get(0);   // ❌
    // Number n = list.get(0);    // ❌
    
    // ✓ Object 로만
    Object obj = list.get(0);
    
    // 이유:
    // list 가 List<Integer>, List<Number>, List<Object> 중 무엇인지 모름
    // 가장 좁은 보장은 Object
    
    // 읽기가 필요하면 ? extends T (Producer) 사용
}

3.5 CS 활용 패턴

// 1. 데이터 채우기
public static <T> void fillWith(
        List<? super T> dest,
        Supplier<T> supplier,
        int count) {
    for (int i = 0; i < count; i++) {
        dest.add(supplier.get());
    }
}

// 2. 컬렉션에 추가
public static <T> void addAll(
        Collection<? super T> coll,
        T... items) {
    for (T item : items) {
        coll.add(item);
    }
}

// 3. forEach 와 부모 Consumer
public static <T> void forEach(
        List<T> list,
        Consumer<? super T> action) {
    for (T item : list) {
        action.accept(item);
    }
}

// 4. Comparator 의 활용
public static <T> T max(
        List<T> list,
        Comparator<? super T> comparator) {
    T max = list.get(0);
    for (T item : list) {
        if (comparator.compare(item, max) > 0) {
            max = item;
        }
    }
    return max;
}

3.6 CS 의 자바 표준 활용

// Collections.addAll
public static <T> boolean addAll(
        Collection<? super T> c,
        T... elements) {
    //   ↑
    //   c 는 Consumer (데이터 받음)
}

// Stream.forEach
default void forEach(Consumer<? super T> action) {
    //                          ↑
    //                          action 은 T 또는 부모 받음
}

// Comparator
public interface Comparator<T> {
    int compare(T o1, T o2);
}

// Comparator<? super T> 활용
public static <T> void sort(
        List<T> list,
        Comparator<? super T> c) {
    //              ↑
    //              c 는 T 또는 부모 받아서 비교
}

3.7 Comparator<? super T> 의 예

// Shipment 비교기
Comparator<Shipment> byWeight = 
    Comparator.comparing(Shipment::getWeight);

// SeaShipment 의 자식 정렬에 사용
List<SeaShipment> seaShipments = ...;

Collections.sort(seaShipments, byWeight);
//                                 ↑
//                                 Comparator<Shipment> 가 
//                                 Comparator<? super SeaShipment> 로 받아짐
//                                 (Shipment 가 SeaShipment 의 부모)

// 즉:
// sort(List<SeaShipment>, Comparator<? super SeaShipment>)
// Shipment 가 SeaShipment 의 부모 → OK

3.8 ILIC CS 패턴

public class ShipmentCollector {
    
    // 1. 받는 통 — Consumer
    public static <T extends Shipment> void collectInto(
            List<? super T> destination,
            T... shipments) {
        for (T s : shipments) {
            destination.add(s);
        }
    }
    
    // 2. 처리 동작 — Consumer (Function)
    public static <T extends Shipment> void processAll(
            List<T> shipments,
            Consumer<? super T> processor) {
        shipments.forEach(processor);
    }
    
    // 3. 변환 후 받기 — Consumer
    public static <T extends Shipment, R> void transformInto(
            List<? extends T> source,
            List<? super R> destination,
            Function<? super T, ? extends R> mapper) {
        for (T s : source) {
            destination.add(mapper.apply(s));
        }
    }
}

// 사용
List<Object> all = new ArrayList<>();
ShipmentCollector.collectInto(all, new SeaShipment(), new AirShipment());

List<Shipment> shipments = ...;
ShipmentCollector.processAll(shipments, s -> log(s));

List<BigDecimal> weights = new ArrayList<>();
ShipmentCollector.transformInto(shipments, weights, Shipment::getWeight);

3.9 자기 점검 답변

Consumer Super 의 본질은?

:

  • 본질:

    • 매개변수가 외부에서 데이터 받음
    • 반공변성 (Contravariance)
    • 부모 타입 모두 받음
  • 동작:

    • 쓰기: T 와 자식 가능
    • 읽기: Object 로만
  • 활용:

    • 데이터 목적지 (output)
    • 컬렉션 추가
    • 함수의 매개변수 (Consumer, Comparator)
  • :

    • Collections.addAll(Collection<? super T>)
    • Stream.forEach(Consumer<? super T>)
    • sort(List<T>, Comparator<? super T>)

4️⃣ PECS 결정 알고리즘 4단계

4.1 결정 알고리즘

Step 1: 매개변수의 역할 분석
  - 데이터를 꺼냄? → Producer
  - 데이터를 추가? → Consumer
  - 둘 다? → 일반 T

Step 2: 입출력 검토
  - 메서드의 입력 (in)? → Producer 가능
  - 메서드의 출력 (out)? → Consumer 가능

Step 3: 와일드카드 선택
  - Producer → ? extends T
  - Consumer → ? super T
  - 둘 다 (PECS 안 됨) → T (와일드카드 X)

Step 4: 호환성 검증
  - 호출자가 다양한 타입 전달 가능?
  - 메서드 내부 사용 안전?

4.2 단계 시각화

매개변수 X 를 결정할 때:

질문 1: X 에서 데이터를 꺼내는가? (read)
  YES → Producer 후보
  
질문 2: X 에 데이터를 추가하는가? (write)
  YES → Consumer 후보
  
조합:
  - Producer 만 → ? extends T
  - Consumer 만 → ? super T
  - 둘 다 → T (불가, PECS 안 됨)
  - 둘 다 안 함 → ? (와일드카드, 다양한 타입 받기만)

4.3 실제 결정 예 1 — copy 메서드

// 시나리오: dest 에 src 의 모든 요소 복사

// Step 1: dest 와 src 의 역할
// - dest: 데이터 받음 (write) → Consumer
// - src: 데이터 꺼냄 (read) → Producer

// Step 2: 입출력
// - dest: 출력 (out, 데이터 받음)
// - src: 입력 (in, 데이터 제공)

// Step 3: 와일드카드
// - dest: Consumer → ? super T
// - src: Producer → ? extends T

// Step 4: 검증
public static <T> void copy(
        List<? super T> dest,
        List<? extends T> src) {
    for (T item : src) {
        dest.add(item);
    }
}

// 호출자 유연성:
// copy(new ArrayList<Number>(), List.of(1, 2, 3))   // T=Integer
//      ↑                          ↑
//      Number 는 Integer 부모     Integer 의 List

4.4 실제 결정 예 2 — filter 메서드

// 시나리오: source 에서 조건 만족하는 것만 반환

// Step 1: source 와 predicate 의 역할
// - source: 데이터 꺼냄 → Producer
// - predicate: predicate 자체는 데이터 받는 함수
//   - predicate.test(T) 는 T 를 입력 받음
//   - Predicate<? super T> 가 적절

// Step 2: 입출력
// - source: 입력 (Producer)
// - predicate: 입력 (사용자가 정의)
// - 반환: 출력

// Step 3: 와일드카드
// - source: List<? extends T>
// - predicate: Predicate<? super T>

// Step 4: 검증
public static <T> List<T> filter(
        List<? extends T> source,
        Predicate<? super T> predicate) {
    List<T> result = new ArrayList<>();
    for (T item : source) {
        if (predicate.test(item)) {
            result.add(item);
        }
    }
    return result;
}

// 호출
Predicate<Shipment> isActive = s -> s.getStatus() == ACTIVE;
List<SeaShipment> seaShipments = ...;

// SeaShipment 의 List + Shipment 의 Predicate
List<SeaShipment> active = filter(seaShipments, isActive);
//                                                ↑
//                                                ? super SeaShipment
//                                                = Shipment ✓

4.5 실제 결정 예 3 — map 메서드

// 시나리오: source 의 각 요소를 변환

// Step 1: 역할
// - source: 데이터 꺼냄 → Producer
// - mapper: 함수 자체
//   - mapper.apply(T) → R
//   - 입력 T 는 ? super T (Consumer 측면)
//   - 출력 R 은 ? extends R (Producer 측면)

// Step 2-3: Function<? super T, ? extends R>

public static <T, R> List<R> map(
        List<? extends T> source,
        Function<? super T, ? extends R> mapper) {
    List<R> result = new ArrayList<>();
    for (T item : source) {
        result.add(mapper.apply(item));
    }
    return result;
}

// 호출
Function<Shipment, BigDecimal> getFare = Shipment::getFare;
List<SeaShipment> seaShipments = ...;

List<Number> fares = map(seaShipments, getFare);
//                       ↑              ↑
//                       List<SeaShipment>
//                       ? extends Shipment ✓
//                       
//                       Function<Shipment, BigDecimal>
//                       Function<? super SeaShipment, ? extends Number>
//                       ✓ (BigDecimal 은 Number)

4.6 실제 결정 예 4 — reduce 메서드

// 시나리오: 컬렉션을 한 값으로 축약

// Step 1: 역할
// - source: 데이터 꺼냄 → Producer
// - identity: 초기값 (Producer? 아님, 단순 값)
// - accumulator: BiFunction (두 값을 받아 하나로)
//   - 입력 T 두 개
//   - 출력 T

public static <T> T reduce(
        List<? extends T> source,
        T identity,
        BinaryOperator<T> accumulator) {
    T result = identity;
    for (T item : source) {
        result = accumulator.apply(result, item);
    }
    return result;
}

// 자바 표준의 더 정밀
<U> U reduce(U identity,
              BiFunction<U, ? super T, U> accumulator,
              BinaryOperator<U> combiner);
//                            ↑
//                            T 의 Consumer

4.7 결정 시 주의사항

1. 매개변수가 둘 다 (read + write) 이면 ?  
   → T (와일드카드 X)
   
   예: 
   public void modify(List<T> list) {
       T item = list.get(0);   // read
       list.add(item);          // write
   }
   
2. 호출자의 의도 고려
   - 다양한 타입 받기 → 와일드카드
   - 같은 타입 일관 → T

3. 반환 타입은 와일드카드 회피
   - List<? extends T> 반환은 사용 어려움
   - List<T> 반환 권장

4. PECS 가 항상 필요한 것은 아님
   - 단순한 경우 T 만으로 충분
   - 호출자 유연성이 중요할 때 PECS

4.8 자기 점검 답변

PECS 결정의 4단계 알고리즘은?

:
1. 역할 분석:

  • read (꺼냄) → Producer
  • write (추가) → Consumer
  1. 입출력 검토:

    • 메서드의 input → Producer 가능
    • 메서드의 output → Consumer 가능
  2. 와일드카드 선택:

    • Producer → ? extends T
    • Consumer → ? super T
    • 둘 다 → 그냥 T
  3. 호환성 검증:

    • 호출자 유연성 확인
    • 메서드 내부 안전성 검증

→ 자바 표준 라이브러리의 모든 시그니처가 이 알고리즘.


5️⃣ Comparable<? super T> 의 정밀

5.1 단순 Comparable 의 한계

// 단순 시그니처
public static <T extends Comparable<T>> T max(List<T> list) {
    // ...
}

// 문제 시나리오
public class Parent implements Comparable<Parent> {
    @Override
    public int compareTo(Parent other) { ... }
}

public class Child extends Parent {
    // Comparable<Parent> 상속 (Comparable<Child> 가 아님)
}

// 호출
List<Child> children = ...;
max(children);   // ❌ 컴파일 에러
// Child 는 Comparable<Child> 가 아니라 Comparable<Parent>

5.2 Comparable<? super T> 의 도입

// 자바 표준의 정밀
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
    //                              ↑
    //                              T 또는 부모와 비교 가능
    // ...
}

// 의미:
// T 가 자기 자신 또는 부모를 비교할 수 있는 Comparable

// Child 의 경우:
// Child 는 Comparable<Parent>
// Parent 는 Child 의 부모
// 즉, Child 는 Comparable<? super Child> = Comparable<Parent> ✓

// 호출
List<Child> children = ...;
max(children);   // ✓ OK

5.3 시각적 비교

타입 계층:
  Comparable<Parent>
    ↑
    Parent
      ↑
      Child (Comparable<Parent> 상속)

Comparable<T> 시도:
  - Child 가 Comparable<Child> 인가? NO
  - 거부

Comparable<? super T> 시도:
  - Child 가 Comparable<? super Child> 인가?
  - ? super Child = Child or Parent or Object
  - Child 는 Comparable<Parent> 보유 → ✓

→ 부모의 Comparable 을 자식이 활용

5.4 자바 표준의 모든 활용

// Collections.sort
public static <T extends Comparable<? super T>> void sort(List<T> list) {
    // T 또는 부모의 compareTo 사용
}

// Collections.binarySearch
public static <T> int binarySearch(
        List<? extends Comparable<? super T>> list, T key) {
    // list 의 요소는 T 또는 자식 (Producer)
    // 요소들은 T 또는 부모와 비교 가능
}

// Collections.max
public static <T extends Object & Comparable<? super T>> T max(
        Collection<? extends T> coll) {
    // ...
}

// Collections.min — 같은 시그니처
public static <T extends Object & Comparable<? super T>> T min(
        Collection<? extends T> coll) {
    // ...
}

// TreeMap, TreeSet
public class TreeMap<K, V> {
    // K 는 Comparable 또는 Comparator 제공해야
}

// Collections.reverseOrder
public static <T> Comparator<T> reverseOrder() {
    return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
}

5.5 일반화된 PECS — Comparable 의 super

// 이런 시그니처를 본다면:

<T extends Comparable<? super T>>

// 해석:
// 1. T 는 어떤 타입
// 2. T 는 Comparable 의 구현
// 3. Comparable 의 타입 매개변수는 T 또는 그 부모
// 4. 즉, T 는 자기 자신 또는 부모와 비교 가능

// 효과:
// - T 가 Comparable<T> 면 OK
// - T 가 Comparable<Parent> (Parent 가 T 부모) 면 OK
// - 자식 클래스가 부모의 Comparable 을 상속받은 경우 활용

5.6 Comparator<? super T> 와의 결합

// 자바 표준 sort
public static <T> void sort(
        List<T> list,
        Comparator<? super T> c) {
    // T 의 List + T 또는 부모를 비교하는 Comparator
}

// 사용
Comparator<Shipment> byWeight = Comparator.comparing(Shipment::getWeight);

// Shipment 자식 정렬
List<SeaShipment> seaShipments = ...;
Collections.sort(seaShipments, byWeight);
//                  ↑              ↑
//                  T = SeaShipment
//                  Comparator<? super SeaShipment>
//                  = Comparator<Shipment> ✓

5.7 ILIC 적용

public class Shipment implements Comparable<Shipment> {
    private LocalDateTime createdAt;
    
    @Override
    public int compareTo(Shipment other) {
        return this.createdAt.compareTo(other.createdAt);
    }
}

public class SeaShipment extends Shipment {
    // Comparable<Shipment> 상속
    // Comparable<SeaShipment> 가 아님
}

public class AirShipment extends Shipment {
    // 같음
}

// 사용
List<SeaShipment> seaShipments = ...;

// Collections.sort 가 가능한 이유:
// SeaShipment extends Comparable<? super SeaShipment>
// = Comparable<Shipment>
// SeaShipment 는 Comparable<Shipment> ✓
Collections.sort(seaShipments);

// 마찬가지로 max, min, binarySearch 모두 가능
SeaShipment first = Collections.max(seaShipments);

5.8 직접 작성한 generic 메서드

public class ShipmentUtils {
    
    // PECS + Comparable<? super T> 활용
    public static <T extends Comparable<? super T>> T findOldest(
            Collection<? extends T> shipments) {
        if (shipments.isEmpty()) {
            throw new IllegalArgumentException("Empty");
        }
        
        Iterator<? extends T> it = shipments.iterator();
        T oldest = it.next();
        
        while (it.hasNext()) {
            T next = it.next();
            if (next.compareTo(oldest) < 0) {
                oldest = next;
            }
        }
        
        return oldest;
    }
}

// 사용
List<SeaShipment> seaShipments = ...;
SeaShipment oldest = ShipmentUtils.findOldest(seaShipments);
// T = SeaShipment
// Collection<? extends SeaShipment> ← seaShipments
// SeaShipment extends Comparable<? super SeaShipment>
// = Comparable<Shipment> ✓

5.9 자기 점검 답변

Comparable<? super T> 가 Comparable 보다 유연한 이유는?

:

  • Comparable:

    • T 자신만 비교
    • 자식 클래스가 부모의 Comparable 상속 시 거부
  • Comparable<? super T>:

    • T 또는 그 부모와 비교 가능
    • 자식 클래스가 부모의 Comparable 활용 가능
  • :

    • Child extends Parent implements Comparable<Parent>
    • Comparable 가 아닌 Comparable
    • <? super Child> 가 Parent 를 포함 → OK
  • PECS 의 일종:

    • Comparable 도 일종의 Consumer (T 받아 비교)
    • super 가 유연성 제공

6️⃣ Collections 의 모든 PECS 분석

6.1 핵심 메서드 시그니처 종합

public class Collections {
    
    // sort
    public static <T extends Comparable<? super T>> void sort(List<T> list);
    public static <T> void sort(List<T> list, Comparator<? super T> c);
    
    // binarySearch
    public static <T> int binarySearch(
        List<? extends Comparable<? super T>> list, T key);
    public static <T> int binarySearch(
        List<? extends T> list, T key, Comparator<? super T> c);
    
    // max, min
    public static <T extends Object & Comparable<? super T>> T max(
        Collection<? extends T> coll);
    public static <T> T max(
        Collection<? extends T> coll, Comparator<? super T> comp);
    
    // copy
    public static <T> void copy(
        List<? super T> dest,
        List<? extends T> src);
    
    // fill
    public static <T> void fill(List<? super T> list, T obj);
    
    // replaceAll
    public static <T> boolean replaceAll(
        List<T> list, T oldVal, T newVal);
    
    // addAll
    public static <T> boolean addAll(
        Collection<? super T> c, T... elements);
    
    // disjoint
    public static boolean disjoint(
        Collection<?> c1, Collection<?> c2);
    
    // frequency
    public static int frequency(Collection<?> c, Object o);
    
    // unmodifiableList
    public static <T> List<T> unmodifiableList(
        List<? extends T> list);
    
    // emptyList
    public static <T> List<T> emptyList();
    
    // singletonList
    public static <T> List<T> singletonList(T o);
    
    // reverseOrder
    public static <T> Comparator<T> reverseOrder();
    public static <T> Comparator<T> reverseOrder(
        Comparator<T> cmp);
}

6.2 분석 1 — sort

public static <T extends Comparable<? super T>> void sort(List<T> list);

// 분석:
// - T 는 자기 자신 또는 부모와 비교 가능
// - list 는 T 의 List (정확히, 와일드카드 X)
//   - 이유: sort 가 list 의 내부 순서 변경 → T 의 일관성 필요
//   - PE 도 CS 도 아니라 그냥 T

// 와일드카드 안 쓴 이유:
// list 의 요소를 읽고 쓰기 모두 함 (sort 내부)
// PE 도 CS 도 아닌 일반 T

6.3 분석 2 — sort with Comparator

public static <T> void sort(List<T> list, Comparator<? super T> c);

// 분석:
// - list: T 의 List
// - c: Comparator<? super T> — CS
//   - c.compare(T, T) — T 받아 비교
//   - T 또는 부모 비교 가능

// 사용
List<SeaShipment> seaShipments = ...;
Comparator<Shipment> byWeight = ...;
Collections.sort(seaShipments, byWeight);   // ✓

6.4 분석 3 — binarySearch

public static <T> int binarySearch(
    List<? extends Comparable<? super T>> list, T key);

// 분석:
// - list: List<? extends Comparable<? super T>> — 복잡!
//   - list 의 요소는 Comparable<? super T> 의 구현
//   - 그 자체가 ? extends Comparable<? super T>
//   - PE 이중 활용
// - key: T (검색할 키)

// 의미:
// list 의 요소는 T 또는 그 부모와 비교 가능

// 예
List<Shipment> shipments = ...;
SeaShipment target = ...;
int idx = Collections.binarySearch(shipments, target);
// list: List<Shipment>
// ? extends Comparable<? super SeaShipment>
// = Comparable<Shipment>
// Shipment ✓

6.5 분석 4 — copy

public static <T> void copy(
    List<? super T> dest,
    List<? extends T> src);

// 분석:
// - dest: Consumer (src 의 요소 받음)
// - src: Producer (요소 제공)
// - PECS 의 완벽한 모범

// 사용
List<Number> dest = new ArrayList<>(Arrays.asList(0, 0, 0));
List<Integer> src = List.of(1, 2, 3);
Collections.copy(dest, src);
// T = Integer
// dest: List<Number> = List<? super Integer> ✓
// src: List<Integer> = List<? extends Integer> ✓

6.6 분석 5 — addAll

public static <T> boolean addAll(
    Collection<? super T> c, T... elements);

// 분석:
// - c: Consumer (elements 추가)
// - elements: T... (가변 인자)

// 사용
List<Number> nums = new ArrayList<>();
Collections.addAll(nums, 1, 2, 3);   // T = Integer
// c: List<Number> = List<? super Integer> ✓

6.7 분석 6 — unmodifiableList

public static <T> List<T> unmodifiableList(List<? extends T> list);

// 분석:
// - list: Producer (요소 읽기)
//   - 사용자가 List<Integer> 를 List<Number> 의 unmodifiableList 로 받기 가능
// - 반환: List<T> — 정확한 T

// 사용
List<Integer> ints = ...;
List<Number> unmodifiable = Collections.unmodifiableList(ints);
//                                                       ↑
//                                                       T = Number
//                                                       List<? extends Number> ← List<Integer> ✓

6.8 자바 표준의 PECS 종합 정리

패턴 1: T 자기 자신 비교
  <T extends Comparable<? super T>>

패턴 2: 외부 Comparator
  Comparator<? super T>

패턴 3: 데이터 받는 곳
  Collection<? super T>
  List<? super T>

패턴 4: 데이터 주는 곳
  Collection<? extends T>
  List<? extends T>

패턴 5: 변환 함수
  Function<? super T, ? extends R>

패턴 6: 검사 함수
  Predicate<? super T>

패턴 7: 동작 함수
  Consumer<? super T>

패턴 8: 공급 함수
  Supplier<? extends T>

6.9 자기 점검 답변

Collections 메서드의 PECS 활용을 정리하면?

:
1. sort: <T extends Comparable<? super T>> + Comparator<? super T>
2. binarySearch: List<? extends Comparable<? super T>> (이중 PE)
3. copy: List<? super T> dest (CS) + List<? extends T> src (PE)
4. max/min: Collection<? extends T> (PE) + Comparator<? super T> (CS)
5. addAll: Collection<? super T> (CS) + T... 가변 인자
6. unmodifiableList: List<? extends T> (PE)

→ 모든 메서드가 PECS 의 일관된 적용.


7️⃣ Stream, Function, Optional 의 PECS

7.1 Stream 의 PECS

public interface Stream<T> {
    
    // filter — Predicate is Consumer
    Stream<T> filter(Predicate<? super T> predicate);
    
    // map — Function input is Consumer, output is Producer
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    
    // flatMap — 더 복잡
    <R> Stream<R> flatMap(
        Function<? super T, ? extends Stream<? extends R>> mapper);
    
    // forEach — Consumer
    void forEach(Consumer<? super T> action);
    
    // peek — Consumer
    Stream<T> peek(Consumer<? super T> action);
    
    // sorted
    Stream<T> sorted(Comparator<? super T> comparator);
    
    // min, max
    Optional<T> min(Comparator<? super T> comparator);
    Optional<T> max(Comparator<? super T> comparator);
    
    // reduce
    T reduce(T identity, BinaryOperator<T> accumulator);
    Optional<T> reduce(BinaryOperator<T> accumulator);
    <U> U reduce(U identity, 
                  BiFunction<U, ? super T, U> accumulator,
                  BinaryOperator<U> combiner);
    
    // collect
    <R, A> R collect(Collector<? super T, A, R> collector);
    <R> R collect(Supplier<R> supplier,
                   BiConsumer<R, ? super T> accumulator,
                   BiConsumer<R, R> combiner);
}

7.2 Stream 의 PECS 분석

1. filter — Predicate<? super T>
   - Predicate.test(T) 받음 → CS
   - T 또는 부모의 Predicate 사용 가능

2. map — Function<? super T, ? extends R>
   - 입력 T: CS (받음)
   - 출력 R: PE (생산)

3. forEach — Consumer<? super T>
   - Consumer.accept(T) 받음 → CS

4. min/max — Comparator<? super T>
   - Comparator.compare(T, T) 받음 → CS

5. reduce 3-arg — BiFunction<U, ? super T, U>
   - 첫 U: 누적
   - 두 번째 T: CS (받음)
   - 반환 U: 누적

6. collect — Collector<? super T, A, R>
   - T 받음 → CS
   - A 는 내부
   - R 은 결과

7.3 Function<T, R> 의 PECS

@FunctionalInterface
public interface Function<T, R> {
    
    R apply(T t);
    
    // compose — 다른 Function 을 앞에
    // Function<V, R> = Function<V, T> . Function<T, R>
    default <V> Function<V, R> compose(
            Function<? super V, ? extends T> before) {
        //                     ↑
        //                     입력 V (앞에 올 함수의 입력) → CS
        //                     출력 T (앞에 올 함수의 출력) → PE
        return (V v) -> apply(before.apply(v));
    }
    
    // andThen — 다른 Function 을 뒤에
    // Function<T, V> = Function<T, R> . Function<R, V>
    default <V> Function<T, V> andThen(
            Function<? super R, ? extends V> after) {
        //                     ↑
        //                     입력 R (뒤에 올 함수의 입력) → CS
        //                     출력 V (뒤에 올 함수의 출력) → PE
        return (T t) -> after.apply(apply(t));
    }
}

7.4 Optional 의 PECS

public final class Optional<T> {
    
    // map — Function input is CS, output is PE
    public <U> Optional<U> map(
            Function<? super T, ? extends U> mapper) {
        if (!isPresent()) return empty();
        return Optional.ofNullable(mapper.apply(value));
    }
    
    // flatMap
    public <U> Optional<U> flatMap(
            Function<? super T, ? extends Optional<? extends U>> mapper) {
        // 더 복잡 — Optional<? extends U> 도 ? extends
    }
    
    // ifPresent — Consumer
    public void ifPresent(Consumer<? super T> action) {
        if (isPresent()) action.accept(value);
    }
    
    // filter — Predicate
    public Optional<T> filter(Predicate<? super T> predicate) {
        // ...
    }
    
    // orElseGet — Supplier
    public T orElseGet(Supplier<? extends T> supplier) {
        return isPresent() ? value : supplier.get();
    }
    
    // orElseThrow with supplier
    public <X extends Throwable> T orElseThrow(
            Supplier<? extends X> exceptionSupplier) throws X {
        if (isPresent()) return value;
        throw exceptionSupplier.get();
    }
}

7.5 Comparator 의 PECS

public interface Comparator<T> {
    
    int compare(T o1, T o2);
    
    // thenComparing — 다른 Comparator 와 결합
    default Comparator<T> thenComparing(Comparator<? super T> other) {
        //                                          ↑
        //                                          CS (T 받음)
        return (Comparator<T> & Serializable) (c1, c2) -> {
            int res = compare(c1, c2);
            return (res != 0) ? res : other.compare(c1, c2);
        };
    }
    
    // thenComparing with key extractor
    default <U extends Comparable<? super U>> Comparator<T> thenComparing(
            Function<? super T, ? extends U> keyExtractor) {
        //  ↑                ↑              ↑
        //  U 비교 가능       T 의 CS        U 의 PE
        return thenComparing(comparing(keyExtractor));
    }
    
    // comparing — static
    static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor) {
        return (c1, c2) -> 
            keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
    }
}

7.6 ILIC 활용

public class ShipmentService {
    
    public List<Shipment> processShipments(List<Shipment> shipments) {
        return shipments.stream()
            .filter(this::isValid)                          // Predicate<? super Shipment>
            .map(this::enrichWithData)                       // Function<? super Shipment, ? extends Shipment>
            .sorted(Comparator.comparing(Shipment::getCreatedAt))   // Comparator<? super Shipment>
            .peek(s -> log(s))                               // Consumer<? super Shipment>
            .toList();
    }
    
    public Optional<BigDecimal> findFare(Long id) {
        return repository.findById(id)
            .map(Shipment::getFare)                          // Function<? super Shipment, ? extends BigDecimal>
            .filter(fare -> fare.compareTo(BigDecimal.ZERO) > 0);   // Predicate<? super BigDecimal>
    }
    
    public BigDecimal totalFare(List<Shipment> shipments) {
        return shipments.stream()
            .map(Shipment::getFare)
            .reduce(BigDecimal.ZERO, BigDecimal::add);       // BiFunction<BigDecimal, ? super BigDecimal, BigDecimal>
    }
    
    public Map<String, List<Shipment>> groupByRoute(List<Shipment> shipments) {
        return shipments.stream()
            .collect(Collectors.groupingBy(Shipment::getRoute));    // Function<? super Shipment, ? extends String>
    }
}

7.7 자기 점검 답변

Function<? super T, ? extends R> 의 의미는?

:

  • ? super T (입력):

    • Function 이 받는 T 의 부모도 OK
    • Consumer 측면 (받음)
  • ? extends R (출력):

    • Function 이 반환하는 R 의 자식도 OK
    • Producer 측면 (제공)
  • 활용:

    • Stream.map(Function<? super T, ? extends R>)
    • 호출자가 더 일반적인 Function 도 전달 가능
    • 호출자가 더 구체적인 반환 타입도 전달 가능
  • 결과:

    • 최대한의 유연성
    • 자바 표준의 일관된 패턴

8️⃣ 자체 라이브러리 설계 가이드

8.1 설계 원칙

PECS 적용 가이드:

1. 매개변수의 역할 명확화
   - 무엇을 위해 받는가?
   - 어떻게 사용되는가?

2. 호출자 유연성 vs 단순성
   - 다양한 타입 받기 → 와일드카드
   - 항상 같은 타입 → 일반 T

3. 일관성
   - 비슷한 메서드는 비슷한 시그니처
   - 자바 표준과 일관

4. 가독성
   - 너무 복잡한 시그니처 회피
   - 명확한 의도

8.2 단계별 설계 가이드

Step 1: 매개변수 식별
  - 무엇을 받는가?
  - 어떻게 사용하는가?

Step 2: 역할 분류
  - Producer (read only)
  - Consumer (write only)
  - Both (read + write)
  - Neither (메타 작업)

Step 3: PECS 적용
  - Producer → ? extends T
  - Consumer → ? super T
  - Both → T (제약)
  - Neither → ? (와일드카드)

Step 4: 반환 타입
  - 일반 T 권장
  - 와일드카드 회피

Step 5: 호출자 유연성 검증
  - 다양한 타입 사용 가능?
  - 다양한 자식 사용 가능?

8.3 자체 라이브러리 예 — ShipmentLibrary

public class ShipmentLibrary {
    
    // 1. 단순 PE — 컬렉션 처리
    public static BigDecimal totalWeight(
            Collection<? extends Shipment> shipments) {
        return shipments.stream()
            .map(Shipment::getWeight)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    // 2. 단순 CS — 데이터 수용
    public static <T extends Shipment> void collectInto(
            Collection<? super T> destination,
            T... shipments) {
        for (T s : shipments) {
            destination.add(s);
        }
    }
    
    // 3. 양방향 PECS — 복사
    public static <T extends Shipment> void copy(
            List<? extends T> source,
            List<? super T> destination) {
        for (T s : source) {
            destination.add(s);
        }
    }
    
    // 4. 변환
    public static <T extends Shipment, R> List<R> map(
            Collection<? extends T> source,
            Function<? super T, ? extends R> mapper) {
        List<R> result = new ArrayList<>();
        for (T s : source) {
            result.add(mapper.apply(s));
        }
        return result;
    }
    
    // 5. 필터링
    public static <T extends Shipment> List<T> filter(
            Collection<? extends T> source,
            Predicate<? super T> filter) {
        List<T> result = new ArrayList<>();
        for (T s : source) {
            if (filter.test(s)) {
                result.add(s);
            }
        }
        return result;
    }
    
    // 6. 비교 + PE + Comparable
    public static <T extends Shipment & Comparable<? super T>> T heaviest(
            Collection<? extends T> shipments) {
        return shipments.stream()
            .max(Comparator.naturalOrder())
            .orElseThrow();
    }
    
    // 7. 외부 Comparator
    public static <T extends Shipment> T heaviestBy(
            Collection<? extends T> shipments,
            Comparator<? super T> comparator) {
        return shipments.stream()
            .max(comparator)
            .orElseThrow();
    }
    
    // 8. 그룹핑
    public static <T extends Shipment, K> Map<K, List<T>> groupBy(
            Collection<? extends T> shipments,
            Function<? super T, ? extends K> keyExtractor) {
        return shipments.stream()
            .collect(Collectors.groupingBy(keyExtractor));
    }
}

8.4 활용 예

// 다양한 자식 타입과의 활용
List<SeaShipment> seaShipments = ...;
List<AirShipment> airShipments = ...;

// PE — 다양한 자식 받음
BigDecimal seaTotal = ShipmentLibrary.totalWeight(seaShipments);   // ✓
BigDecimal airTotal = ShipmentLibrary.totalWeight(airShipments);   // ✓

// CS — 다양한 부모 받음
List<Object> all = new ArrayList<>();
ShipmentLibrary.collectInto(all, new SeaShipment(), new AirShipment());   // ✓

// PECS — 양방향
List<Shipment> mixed = new ArrayList<>();
ShipmentLibrary.copy(seaShipments, mixed);   // SeaShipment → Shipment

// 변환
List<String> blNos = ShipmentLibrary.map(
    seaShipments,
    Shipment::getBlNo);   // ✓ (Shipment 의 메서드)

// 필터링
List<SeaShipment> heavy = ShipmentLibrary.filter(
    seaShipments,
    s -> s.getWeight().compareTo(new BigDecimal("1000")) > 0);

// 그룹핑
Map<String, List<SeaShipment>> byRoute = ShipmentLibrary.groupBy(
    seaShipments,
    Shipment::getRoute);

8.5 흔한 실수

실수 1: PECS 과도 적용
  - 단순한 경우에 와일드카드 남용
  - 가독성 ↓

해결:
  단순하면 일반 T

실수 2: 반환 타입에 와일드카드
  public List<? extends T> ... { ... }   ❌
  
  사용자가 활용 어려움
  
해결:
  반환은 일반 T 또는 명시적

실수 3: 같은 매개변수에 양방향 사용
  public void process(List<? extends T> list) {
      T item = list.get(0);   // 읽기 OK
      list.add(item);          // 쓰기 ❌
  }
  
해결:
  read + write 면 일반 T

실수 4: 와일드카드 캡처 무시
  public void swap(List<?> list, int i, int j) {
      // ? 의 이름이 없어 swap 어려움
  }
  
해결:
  헬퍼 메서드의 <T> 캡처

8.6 PECS 안티 패턴

// 안티 패턴 1: 단순한 매개변수에 와일드카드
public <T> int size(Collection<? extends T> coll) {   // ❌ 과함
    return coll.size();
}

// 권장
public int size(Collection<?> coll) {   // ✓ 단순
    return coll.size();
}

// 안티 패턴 2: 반환에 와일드카드
public List<? extends T> getItems() { ... }   // ❌

// 권장
public List<T> getItems() { ... }   // ✓

// 안티 패턴 3: 항상 일치하는 타입에 와일드카드
public <T> void process(List<? extends T> a, List<? extends T> b) {   // ❌
    // T 가 두 매개변수에서 같아야 하는데 와일드카드 사용
}

// 권장
public <T> void process(List<T> a, List<T> b) { ... }   // ✓

// 또는 다른 자식들 OK 면
public <T> void process(List<? extends T> a, List<? extends T> b) { ... }   // ✓
// 단, 호출 시 T 의 LUB 추론

8.7 자기 점검 답변

PECS 적용 시 흔한 실수 4가지는?

:
1. 과도 적용: 단순한 경우 와일드카드
2. 반환 타입 와일드카드: 사용자 활용 어려움
3. 양방향 매개변수에 와일드카드: read + write 충돌
4. 캡처 무시: 헬퍼 메서드 없이 자기 자신과의 작업

PECS 는 도구, 만능 아님. 단순한 경우엔 일반 T.


9️⃣ Phase 5 졸업 시험 + Phase 5 완주

9.1 Phase 5 졸업 시험 — 50 문항

제네릭 기본 (10 문항)

Q1. 제네릭이 도입된 Java 버전과 연도?
A1. Java 5 (2004)

Q2. 제네릭의 4가지 효과?
A2. 타입 안전성, 캐스트 제거, 코드 재사용, 가독성

Q3. raw type 정의?
A3. 타입 매개변수 없는 제네릭 사용 (List, Map)

Q4. raw type 의 4가지 위험?
A4. 타입 안전성 손실, CCE, 혼용 위험, 의도 불명확

Q5. 타입 소거 정의?
A5. 컴파일 시 제네릭 타입 정보 제거 (T → Object)

Q6. 타입 소거의 4가지 의도?
A6. 호환성, JVM 최소 변경, 메모리 효율, 점진적 도입

Q7. 다이아몬드 연산자 (Java 7+) 효과?
A7. 우변 타입 추론, 중복 제거

Q8. 제네릭 배열 불가 이유?
A8. 런타임 타입 정보 X (소거)

Q9. instanceof T 불가 이유?
A9. 런타임에 T 무엇인지 모름

Q10. C# 과 자바 제네릭 차이?
A10. C# 은 런타임 타입 보유, 자바는 소거

타입 매개변수 (10 문항)

Q11. T, E, K, V, R 의 의미?
A11. Type, Element, Key, Value, Return

Q12. 제네릭 클래스 정의?
A12. class Name<T> { ... }

Q13. 제네릭 메서드 정의?
A13. public <T> ReturnType method(...) { ... }

Q14. 다중 매개변수 예?
A14. Map<K, V>, Function<T, R>, BiFunction<T, U, R>

Q15. bounded type parameter?
A15. <T extends UpperBound>

Q16. 다중 bound?
A16. <T extends Number & Comparable<T>>

Q17. 재귀 bound?
A17. <T extends Comparable<T>>

Q18. static 멤버에 T 못 쓰는 이유?
A18. 인스턴스 vs 클래스 레벨 충돌

Q19. new T() 못 하는 이유?
A19. 런타임에 T 무엇인지 모름

Q20. 우회 방법?
A20. Class<T>, Supplier<T> 사용

와일드카드 (10 문항)

Q21. 제네릭의 불공변성 의미?
A21. List<Integer> 와 List<Number> 무관

Q22. 와일드카드 ? 의 정의?
A22. "어떤 타입" placeholder

Q23. <?> 의 의미?
A23. 어떤 타입의 List 든 (unbounded)

Q24. <? extends T> 의미?
A24. T 또는 자식 (공변, Producer)

Q25. <? super T> 의미?
A25. T 또는 부모 (반공변, Consumer)

Q26. List<?> 에 add 가능한 것?
A26. null 만

Q27. List<? extends Number> 에 add 가능?
A27. null 만 (어떤 자식인지 모름)

Q28. List<? super Integer> 의 get 결과 타입?
A28. Object

Q29. 와일드카드 캡처?
A29. 헬퍼 메서드의 T 가 ? 를 캡처

Q30. List<?> vs List<Object> 차이?
A30. 어떤 타입 vs Object 전용

제네릭 클래스 + 메서드 (10 문항)

Q31. 제네릭 클래스 vs 메서드 차이?
A31. 인스턴스 단위 vs 호출 단위

Q32. 타입 추론 메커니즘?
A32. 컨텍스트 분석 + 제약 수집 + LUB

Q33. 타깃 타입?
A33. 표현식의 기대되는 타입

Q34. var 와 다이아몬드 동시 사용?
A34. ArrayList<Object> 가 됨 (의도와 다름)

Q35. 클래스 T + 메서드 U 패턴?
A35. Optional.map, Stream.map

Q36. S extends T 의 의미?
A36. T 의 자식만 받음

Q37. Self-referential bound 활용?
A37. Builder 패턴, Fluent API

Q38. 익명 vs 람다?
A38. SAM 만 람다, 일반은 익명

Q39. Function.compose vs andThen?
A39. V→T→R vs T→R→V

Q40. 자바 표준의 정적 팩토리 예?
A40. Optional.of, Stream.of, List.of

PECS (10 문항)

Q41. PECS 의 의미?
A41. Producer Extends, Consumer Super

Q42. PE 의 본질?
A42. 데이터 꺼내는 매개변수, 자식 받기

Q43. CS 의 본질?
A43. 데이터 받는 매개변수, 부모 받기

Q44. PECS 결정 4단계?
A44. 역할 분석, 입출력 검토, 와일드카드 선택, 호환성 검증

Q45. Comparable<? super T> 의미?
A45. T 또는 부모와 비교 가능

Q46. Collections.copy 시그니처?
A46. <T> void copy(List<? super T> dest, List<? extends T> src)

Q47. Function<? super T, ? extends R> 의미?
A47. T 의 부모 받고, R 의 자식 반환

Q48. PECS 안티 패턴?
A48. 과도 적용, 반환 와일드카드, 양방향 매개변수

Q49. Comparator<? super T> 가 필요한 이유?
A49. T 의 부모를 비교하는 Comparator 도 받음

Q50. Phase 5 마스터 후 가능한 일?
A50. 어떤 제네릭 시그니처도 자신 있게 설계

9.2 채점

50 / 50 → Phase 5 마스터
45-49   → 거의 마스터, 일부 복습
40-44   → 핵심 다시 학습
< 40    → Unit 5.1 ~ 5.5 재학습

9.3 Phase 5 완주 후 가능한 일

1. 자바 제네릭 자유자재
   - 어떤 시그니처도 이해
   - 직접 설계 가능
   - PECS 자동 적용

2. 자바 표준 라이브러리 깊이 이해
   - Collections, Stream, Optional
   - Function, Comparator
   - 모든 시그니처 분석 가능

3. 면접 자신감
   - 제네릭 모든 질문 즉답
   - PECS 설명 가능
   - 와일드카드 활용

4. 자체 라이브러리 설계
   - 유틸리티 클래스
   - 함수형 인터페이스
   - 도메인 추상화

5. 다음 Phase 준비
   - Phase 6: 객체 비교 (Comparable/Comparator)
   - Phase 7: I/O 시스템
   - Phase 8: Stream 실전
   - Phase 9: I/O 강화
   - Phase 10: 함수형 프로그래밍

9.4 3주차 누적 진행

✅ Phase 1 — Pass by Value (3 Unit)
✅ Phase 2 — 컬렉션 프레임워크 (6 Unit)
✅ Phase 3 — 해시의 원리 (4 Unit)
✅ Phase 4 — 추상화의 두 도구 (4 Unit)
✅ Phase 5 — 제네릭과 와일드카드 (5 Unit) ← 완주
🚀 Phase 6 — 객체 비교 (다음)
⏭ Phase 7 — I/O 시스템 큰 그림
⏭ Phase 8 — Stream 실전
⏭ Phase 9 — I/O 강화
⏭ Phase 10 — 함수형 프로그래밍

총: 22/43 Unit 작성 (Phase 5 완주, 약 51%)

🎯 핵심 요약 — 3줄 정리

1. PECS 원칙

  • Producer Extends: 데이터 꺼냄 → ? extends T
  • Consumer Super: 데이터 받음 → ? super T
  • 자바 표준의 황금률

2. 결정 알고리즘

  • 역할 분석 → 입출력 검토 → 와일드카드 선택 → 호환성 검증
  • Comparable<? super T>: 부모와 비교
  • Comparator<? super T>: 부모를 비교

3. Phase 5 마스터

  • 모든 제네릭 시그니처 이해
  • 자체 라이브러리 설계 가능
  • 자바 표준 라이브러리 깊이 분석
  • 면접 자신감

🏆 Phase 5 완주 — 제네릭 마스터 달성

🚀 Phase 5 — 제네릭과 와일드카드
  ✅ Unit 5.1 제네릭의 등장과 raw type
  ✅ Unit 5.2 타입 매개변수와 타입 인자
  ✅ Unit 5.3 와일드카드 ? extends, ? super
  ✅ Unit 5.4 제네릭 메서드 + 제네릭 클래스
  ✅ Unit 5.5 PECS 원칙 (마스터 깊이) ← 여기, Phase 5 완주

→ 자바 제네릭 정복
→ PECS 원칙 자유 활용
→ 자바 표준 라이브러리 깊이 이해
→ 자체 라이브러리 설계 가능

📚 다음으로...

Phase 6 — 객체 비교

다음 Phase 는 Comparable/Comparator 정밀.

Phase 6 — 객체 비교

Unit 6.1 — equals 와 hashCode 의 계약
  → Object 의 두 메서드 정밀
  → 5가지 계약 (자기성, 대칭성, 추이성 등)

Unit 6.2 — Comparable<T> 의 자연 순서
  → compareTo 의 구현
  → 일관성 (equals 와의 관계)

Unit 6.3 — Comparator<T> 의 외부 비교
  → 다양한 기준의 비교
  → comparing, thenComparing 활용

Unit 6.4 — 비교의 종합 활용 (마스터 깊이)
  → 정렬, 검색, TreeMap, TreeSet
  → equals/hashCode/compareTo 의 일관성

Phase 5 와의 연결:

  • Comparable, Comparable<? super T> (Phase 5.5)
  • Comparator, Comparator<? super T>
  • 와일드카드 + 비교

🏆 Phase 5 완주 — 제네릭 마스터 달성

profile
Software Developer

0개의 댓글