공공부하자개발 · 영어 학습 노트
자바
고급모던 자바와 성능0/10 완료
  • 01제네릭과 와일드카드
  • 02멀티스레드와 동기화
  • 03람다식과 함수형 인터페이스
  • 04Stream API와 병렬 처리
  • 05Optional로 NPE 방지
  • 06메서드 활용 패턴 (고급)
  • 07Java 21 모던 문법
  • 08어노테이션·리플렉션·동적 프록시
  • 09CompletableFuture 심화와 가상 스레드 실전
  • 10JVM 메모리·GC·OOM 진단
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 고급 › 01 / 10

제네릭과 와일드카드

섹션 7진행 0 / 10
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

3. 코드 예제

예제 1: 제네릭 없는 코드의 런타임 폭발 vs 제네릭 컴파일 타임 검출

같은 실수가 제네릭 유무에 따라 런타임 에러가 되거나 컴파일 에러가 되는 것을 보여줍니다.

java
import java.util.*;

public class RawVsGeneric {
    public static void main(String[] args) {
        // raw type: 컴파일 통과, 런타임 폭발
        List raw = new ArrayList();
        raw.add("ORD-001");
        raw.add(42);
        try {
            for (Object o : raw) {
                String id = (String) o;   // 두 번째 요소에서 폭발
                System.out.println("id = " + id);
            }
        } catch (ClassCastException e) {
            System.out.println("런타임 에러: " + e.getMessage());
        }

        // 제네릭: 잘못된 add 는 아예 컴파일되지 않음
        List<String> ids = new ArrayList<>();
        ids.add("ORD-001");
        // ids.add(42);   // 컴파일 에러: int cannot be converted to String
        for (String id : ids) System.out.println("id = " + id);   // 캐스팅 불필요
    }
}
// 출력:
// id = ORD-001
// 런타임 에러: class java.lang.Integer cannot be cast to class java.lang.String ...
// id = ORD-001

예제 2: 타입 소거 증명 — List<String> 과 List<Integer> 는 같은 클래스

리플렉션으로 런타임 타입을 찍어 보면 제네릭 정보가 사라졌음을 확인할 수 있습니다.

java
import java.util.*;

public class ErasureProof {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        List<Integer> ints = new ArrayList<>();

        System.out.println(strings.getClass() == ints.getClass());   // true
        System.out.println(strings.getClass().getName());

        // raw type 을 통하면 String 리스트에 Integer 를 넣을 수도 있다 (경고만 발생)
        @SuppressWarnings({"rawtypes", "unchecked"})
        List raw = strings;
        raw.add(123);
        System.out.println("size = " + strings.size());
        try {
            String s = strings.get(0);   // 컴파일러가 삽입한 (String) 캐스팅에서 폭발
            System.out.println(s);
        } catch (ClassCastException e) {
            System.out.println("소거 때문에 여기서야 터진다: " + e.getClass().getSimpleName());
        }
    }
}
// 출력:
// true
// java.util.ArrayList
// size = 1
// 소거 때문에 여기서야 터진다: ClassCastException

예제 3: 바운디드 타입과 PECS — 숫자 합계와 Collections.copy

? extends Number 로 어떤 숫자 리스트든 받고, ? super 로 상위 타입 컬렉션에 결과를 쓰는 패턴입니다.

java
import java.util.*;

public class PecsDemo {
    // producer → extends: Integer, Double, Long 리스트 모두 받을 수 있다
    static double sum(List<? extends Number> nums) {
        double total = 0;
        for (Number n : nums) total += n.doubleValue();
        return total;
    }

    // consumer → super: List<Integer>, List<Number>, List<Object> 모두 채울 수 있다
    static void fillSequence(List<? super Integer> dest, int count) {
        for (int i = 1; i <= count; i++) dest.add(i);
    }

    // 최댓값: 재귀 타입 바운드
    static <T extends Comparable<? super T>> T max(Collection<? extends T> items) {
        Iterator<? extends T> it = items.iterator();
        T best = it.next();
        while (it.hasNext()) { T t = it.next(); if (t.compareTo(best) > 0) best = t; }
        return best;
    }

    public static void main(String[] args) {
        System.out.println(sum(List.of(1, 2, 3)));          // Integer
        System.out.println(sum(List.of(1.5, 2.5)));         // Double

        List<Number> numbers = new ArrayList<>();
        fillSequence(numbers, 3);
        System.out.println(numbers);

        List<Object> objects = new ArrayList<>(List.of("x", "y", "z"));
        Collections.copy(objects, List.of(10, 20));       // dest: ? super Integer, src: ? extends Integer
        System.out.println(objects);

        System.out.println(max(List.of("pear", "apple", "zoo")));
    }
}
// 출력:
// 6.0
// 4.0
// [1, 2, 3]
// [10, 20, z]
// zoo

예제 4: 실무 제네릭 — ApiResponse<T> 와 Result<T, E>

컨트롤러 응답 포맷을 통일하고, 예외 대신 값으로 실패를 표현하는 두 가지 실무 타입입니다. sealed + record + 패턴 매칭으로 JDK 21 스타일로 작성합니다.

java
import java.util.function.Function;

public class ApiTypes {
    // 응답 봉투: 성공/실패 모두 같은 포맷
    record ApiResponse<T>(boolean success, T data, String error) {
        static <T> ApiResponse<T> ok(T data) { return new ApiResponse<>(true, data, null); }
        static <T> ApiResponse<T> fail(String error) { return new ApiResponse<>(false, null, error); }
    }

    // Result: 성공(Ok) 또는 실패(Err) 둘 중 하나. sealed 라서 switch 가 완전성 검사
    sealed interface Result<T, E> permits Ok, Err {
        default <R> Result<R, E> map(Function<? super T, ? extends R> f) {
            return switch (this) {
                case Ok<T, E> ok -> new Ok<>(f.apply(ok.value()));
                case Err<T, E> err -> new Err<>(err.error());
            };
        }
    }
    record Ok<T, E>(T value) implements Result<T, E> {}
    record Err<T, E>(E error) implements Result<T, E> {}

    record Order(String id, long amount) {}

    static Result<Order, String> findOrder(String id) {
        if (id.startsWith("ORD-")) return new Ok<>(new Order(id, 12_000));
        return new Err<>("잘못된 주문 번호: " + id);
    }

    public static void main(String[] args) {
        for (String id : new String[]{"ORD-1", "XXX"}) {
            Result<Long, String> amount = findOrder(id).map(Order::amount);
            String msg = switch (amount) {
                case Ok<Long, String> ok -> "금액 = " + ok.value();
                case Err<Long, String> err -> "실패 = " + err.error();
            };
            System.out.println(msg);
        }
        ApiResponse<Order> res = ApiResponse.ok(new Order("ORD-9", 500));
        System.out.println(res);
        System.out.println(ApiResponse.<Order>fail("not found"));
    }
}
// 출력:
// 금액 = 12000
// 실패 = 잘못된 주문 번호: XXX
// ApiResponse[success=true, data=Order[id=ORD-9, amount=500], error=null]
// ApiResponse[success=false, data=null, error=not found]

예제 5: 제네릭 인메모리 Repository

JpaRepository<T, ID> 의 축소판입니다. 엔티티 타입과 ID 타입을 파라미터로 받고, ID 추출 함수를 생성자에서 받아 new T() 제약을 우회합니다.

java
import java.util.*;
import java.util.function.Function;

public class RepoDemo {
    interface Entity<ID> { ID id(); }

    static class InMemoryRepository<T extends Entity<ID>, ID> {
        private final Map<ID, T> store = new LinkedHashMap<>();

        public T save(T entity) { store.put(entity.id(), entity); return entity; }
        public Optional<T> findById(ID id) { return Optional.ofNullable(store.get(id)); }
        public List<T> findAll() { return new ArrayList<>(store.values()); }
        public boolean deleteById(ID id) { return store.remove(id) != null; }
        public <R> List<R> mapAll(Function<? super T, ? extends R> f) {   // PECS
            List<R> out = new ArrayList<>();
            for (T t : store.values()) out.add(f.apply(t));
            return out;
        }
    }

    record Customer(Long id, String name) implements Entity<Long> {}
    record Product(String id, String name, int price) implements Entity<String> {}

    public static void main(String[] args) {
        var customers = new InMemoryRepository<Customer, Long>();
        customers.save(new Customer(1L, "kim"));
        customers.save(new Customer(2L, "lee"));
        System.out.println(customers.findById(1L).map(Customer::name).orElse("없음"));
        System.out.println(customers.findById(9L).map(Customer::name).orElse("없음"));
        System.out.println(customers.mapAll(Customer::name));

        var products = new InMemoryRepository<Product, String>();
        products.save(new Product("P-1", "keyboard", 50_000));
        System.out.println(products.findAll());
        System.out.println(products.deleteById("P-1") + " " + products.findAll().size());
    }
}
// 출력:
// kim
// 없음
// [kim, lee]
// [Product[id=P-1, name=keyboard, price=50000]]
// true 0

예제 직접 실행

아래 폴더를 JDK 21 로 컴파일하고 실행합니다.

cd java-src\advanced\01_generics
javac -encoding UTF-8 *.java && java Main
코드 예제
  • 예제 1: 제네릭 없는 코드의 런타임 폭발 vs 제네릭 컴파일 타임 검출
  • 예제 2: 타입 소거 증명 — List<String> 과 List<Integer> 는 같은 클래스
  • 예제 3: 바운디드 타입과 PECS — 숫자 합계와 Collections.copy
  • 예제 4: 실무 제네릭 — ApiResponse<T> 와 Result<T, E>
  • 예제 5: 제네릭 인메모리 Repository
이전 섹션2 핵심 원리3 / 7다음 섹션4 응용 변형 예제