3주차 Unit 5.1 — 제네릭의 등장과 raw type

Psj·2026년 5월 19일

F-lab

목록 보기
92/240

Unit 5.1 — 제네릭의 등장과 raw type

F-LAB JAVA · 3주차 · Phase 5 · 제네릭과 와일드카드
🚀 Phase 5 시작 — 자바 타입 시스템 정복


📌 학습 목표

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

  • Java 1.4 까지의 컬렉션 문제 는 무엇이었나?
  • 제네릭 (Generics) 의 정확한 정의와 등장 배경은?
  • 타입 안전성 (Type Safety) 이 무엇이고 제네릭이 어떻게 보장하나?
  • raw type 의 정확한 의미와 위험성은?
  • 타입 소거 (Type Erasure) 가 무엇이고 왜 도입되었나?
  • 하위 호환성 (Backward Compatibility) 을 위한 타협은?
  • 컴파일 타임 vs 런타임 의 타입 정보 차이는?
  • @SuppressWarnings("unchecked") 의 의미는?
  • 다이아몬드 연산자 (<>) (Java 7+) 의 효과는?

🎯 핵심 한 문장

제네릭은 "컴파일 타임에 타입 안전성을 보장" 하는 자바 5+ 의 도구다.
Java 1.4 까지는 List 에 어떤 객체든 담을 수 있어 ClassCastException 위험이 컸지만,
제네릭 도입으로 List<String> 처럼 타입을 명시해 컴파일러가 검증.
단, 하위 호환성 을 위해 타입 소거 (Type Erasure) 라는 타협으로 런타임엔 타입 정보가 사라진다.
raw type 은 제네릭 이전의 호환 모드로 위험하므로 사용 회피.

비유 — 일반 상자 vs 라벨 붙은 상자

Java 1.4 까지 (raw type):
  상자에 라벨 없음 — 무엇이든 담을 수 있음
  꺼낼 때 매번 확인 필요
  - "이게 책인지 음식인지?"
  - 잘못 꺼내면 사고 (ClassCastException)

Java 5+ 제네릭:
  상자에 라벨 — "이 상자는 책 전용 (List<Book>)"
  컴파일러가 검증 — 음식 못 넣음
  꺼낼 때 자동으로 책 타입
  - 안전 + 깔끔

→ 제네릭 = 상자에 타입 라벨.


🧭 9개 섹션 로드맵

1. Java 1.4 까지의 컬렉션 문제
2. 제네릭의 등장과 도입
3. 타입 안전성의 보장
4. raw type 의 정의와 위험
5. 타입 소거 (Type Erasure)
6. 하위 호환성과 타협
7. 다이아몬드 연산자 (Java 7+)
8. @SuppressWarnings 와 컴파일러 경고
9. 면접 + 자기 점검

1️⃣ Java 1.4 까지의 컬렉션 문제

1.1 제네릭 이전의 컬렉션

// Java 1.4 까지 (1996 ~ 2004)

import java.util.ArrayList;

public class Old {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();   // 타입 없음
        list.add("Hello");                    // String 추가
        list.add("World");
        list.add(42);                         // Integer 도 추가됨!
        
        // 꺼낼 때
        Object obj = list.get(0);
        String s = (String) obj;              // 명시적 캐스트 필요
        
        // 위험!
        String s2 = (String) list.get(2);
        // ClassCastException at runtime
    }
}

문제점:

  • 모든 요소가 Object 타입
  • 어떤 타입이든 담을 수 있음
  • 꺼낼 때 명시적 캐스트
  • 잘못 캐스트하면 런타임 에러

1.2 ClassCastException 의 위험

ArrayList list = new ArrayList();
list.add("apple");
list.add("banana");

// 다른 개발자가 추가
list.add(42);   // 컴파일러 OK — 위험 감지 못 함

// 또 다른 곳
for (int i = 0; i < list.size(); i++) {
    String s = (String) list.get(i);
    System.out.println(s.length());
    // i = 2 에서 ClassCastException!
    // 컴파일 시점엔 모름, 런타임에 폭발
}

핵심:

  • 컴파일 타임에 검증 X
  • 런타임 에러로 폭발
  • 디버깅 어려움
  • 큰 시스템에서 위험

1.3 명시적 캐스트의 비효율

// Java 1.4
ArrayList list = getNames();   // ArrayList of Strings (개발자 약속)

// 모든 사용처에서 캐스트
String first = (String) list.get(0);
String second = (String) list.get(1);

for (int i = 0; i < list.size(); i++) {
    String name = (String) list.get(i);   // 매번 캐스트
    System.out.println(name);
}

// 단점:
// 1. 코드 장황
// 2. 캐스트 실수 가능
// 3. 컴파일 시 안전성 검증 X

1.4 문서화에 의존

/**
 * @return ArrayList of Strings   ← 주석에만 명시
 */
public ArrayList getNames() {
    ArrayList list = new ArrayList();
    list.add("Alice");
    list.add("Bob");
    return list;
}

// 사용
ArrayList names = getNames();
// 다른 개발자가 잘못 사용
names.add(42);   // OK (컴파일러는 모름)

// 또는 잘못 캐스트
Integer i = (Integer) names.get(0);   // ClassCastException

문제:

  • 타입 정보가 주석에만
  • 강제력 없음
  • 실수 가능성 ↑

1.5 비슷한 메서드의 중복

// 타입별로 별도 메서드 작성 필요
public static int findString(ArrayList list, String target) {
    for (int i = 0; i < list.size(); i++) {
        String s = (String) list.get(i);
        if (s.equals(target)) return i;
    }
    return -1;
}

public static int findInteger(ArrayList list, Integer target) {
    for (int i = 0; i < list.size(); i++) {
        Integer n = (Integer) list.get(i);
        if (n.equals(target)) return i;
    }
    return -1;
}

// 또는 Object 로 일반화
public static int find(ArrayList list, Object target) {
    for (int i = 0; i < list.size(); i++) {
        if (list.get(i).equals(target)) return i;
    }
    return -1;
}
// 하지만 타입 안전 X

→ 제네릭이 해결.

1.6 컬렉션의 본질적 한계

Java 1.4 까지:
  - 모든 컬렉션 = Object 의 컬렉션
  - 타입 정보 손실
  - 안전성 ↓
  - 가독성 ↓

해결 필요:
  - 컴파일 타임에 타입 검증
  - 명시적 캐스트 회피
  - 코드 재사용 + 안전성
  - C++ 의 template 같은 기능

1.7 자기 점검 답변

제네릭 도입 전 컬렉션의 가장 큰 문제는?

:
1. 타입 안전성 부족:

  • 어떤 객체든 추가 가능
  • 잘못된 타입 추가도 컴파일 OK
  1. ClassCastException 위험:
    • 런타임에 폭발
    • 컴파일 타임 검증 X
  2. 명시적 캐스트의 부담:
    • 모든 사용처에서 캐스트
    • 코드 장황 + 실수 가능
  3. 문서화 의존:
    • 타입 정보가 주석에만
    • 강제력 X

→ 제네릭이 모두 해결.


2️⃣ 제네릭의 등장과 도입

2.1 제네릭의 정의

제네릭 (Generics):

  타입을 매개변수화 (Parameterize) 하는 기능.
  
  컴파일 타임에 타입을 명시하고 컴파일러가 검증.

문법:
  ClassName<TypeParameter>
  
  예: List<String>, Map<K, V>

2.2 Java 5 의 제네릭 도입 (2004)

// Java 5 부터
import java.util.ArrayList;
import java.util.List;

public class New {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        // ↑ 타입 매개변수 <String>
        
        list.add("Hello");
        list.add("World");
        // list.add(42);   // ❌ 컴파일 에러!
        
        // 꺼낼 때 캐스트 불필요
        String s = list.get(0);   // 자동으로 String
        System.out.println(s.length());
    }
}

핵심:

  • <String> 으로 타입 명시
  • 다른 타입 추가 시도 → 컴파일 에러
  • 꺼낼 때 캐스트 불필요
  • 컴파일 타임에 검증

2.3 제네릭의 4가지 효과

1. 타입 안전성
   - 잘못된 타입 추가 시 컴파일 에러
   - 런타임 ClassCastException 회피

2. 명시적 캐스트 제거
   - 컴파일러가 자동 캐스트 삽입 (실제로는)
   - 코드 깔끔

3. 코드 재사용
   - 한 클래스로 다양한 타입 지원
   - List<E>, Map<K,V>

4. 가독성
   - 코드만 봐도 타입 명확
   - 문서화 의존 X

2.4 제네릭의 영향 — 자바 표준 라이브러리

// Java 1.4 → Java 5+ 전환

// 컬렉션
ListList<E>
MapMap<K, V>
SetSet<E>
QueueQueue<E>

// Comparable
ComparableComparable<T>

// 기타
ClassClass<T>
ThreadLocalThreadLocal<T>
IteratorIterator<E>

→ 모든 컬렉션 + 비교 + 클래스 등이 제네릭화.

2.5 제네릭화의 전후 비교

// 전 (Java 1.4)
public Object find(List list, Object target) {
    for (int i = 0; i < list.size(); i++) {
        if (list.get(i).equals(target)) {
            return list.get(i);
        }
    }
    return null;
}

// 호출
List names = new ArrayList();
String found = (String) find(names, "Alice");

// 후 (Java 5+)
public <T> T find(List<T> list, T target) {
    for (int i = 0; i < list.size(); i++) {
        if (list.get(i).equals(target)) {
            return list.get(i);
        }
    }
    return null;
}

// 호출
List<String> names = new ArrayList<>();
String found = find(names, "Alice");   // 캐스트 X

2.6 제네릭의 활용 예시

// 1. 변수 선언
List<String> names = new ArrayList<>();
Map<Long, Shipment> cache = new HashMap<>();
Set<Permission> permissions = new HashSet<>();

// 2. 메서드 매개변수
public void process(List<Shipment> shipments) { ... }

// 3. 반환 타입
public List<Shipment> findActive() { ... }

// 4. 필드
public class Repository {
    private Map<Long, Shipment> cache;
}

// 5. 함수형 인터페이스
Function<String, Integer> length = s -> s.length();
Predicate<Shipment> isActive = s -> s.getStatus() == ACTIVE;

2.7 다른 언어의 제네릭

다양한 언어의 제네릭 (parameterized types):

C++ (1990s):
  - template
  - 컴파일 시점 코드 생성 (monomorphization)
  - 매우 빠름

C# (2005):
  - Generics
  - 런타임에도 타입 정보 보유
  - 자바보다 강력

Java (2004, Java 5):
  - Generics + 타입 소거
  - 런타임에 타입 정보 X
  - 하위 호환성 우선

Kotlin / Scala:
  - 자바 호환 + 강화 (reified types 등)
  - 런타임 정보 일부 보유

2.8 자기 점검 답변

제네릭의 4가지 효과는?

:
1. 타입 안전성: 컴파일 타임 검증
2. 명시적 캐스트 제거: 코드 깔끔
3. 코드 재사용: 다양한 타입 지원
4. 가독성: 타입 정보 명확

추가:

  • 런타임 ClassCastException 회피
  • IDE 자동완성 정확
  • 리팩토링 안전

3️⃣ 타입 안전성의 보장

3.1 타입 안전성의 정의

타입 안전성 (Type Safety):

  프로그램이 실행 시 타입 관련 오류를 일으키지 않는 보장.

자바의 타입 안전:
  - 컴파일 타임 검증
  - 캐스트 시 ClassCastException 가능
  - 제네릭이 캐스트 회피 도와줌

3.2 컴파일 타임 검증

List<String> list = new ArrayList<>();
list.add("Hello");           // OK
list.add("World");           // OK
// list.add(42);              // ❌ 컴파일 에러

String s = list.get(0);      // OK (자동 캐스트)
// Integer i = list.get(0);   // ❌ 컴파일 에러 (타입 불일치)

// 메서드도 검증
public void process(List<Integer> nums) { ... }

process(list);   // ❌ 컴파일 에러
// List<String> 을 List<Integer> 매개변수에 전달 불가

3.3 타입 추론과 검증

// Java 컴파일러의 타입 검증 흐름

List<String> list = new ArrayList<>();

// list.add("Hello") 호출 시:
// 1. list 의 타입: List<String>
// 2. add 메서드: boolean add(E e)  where E = String
// 3. "Hello" 의 타입: String
// 4. 매개변수 String 과 매칭 ✓

// list.add(42) 호출 시:
// 1. list 의 타입: List<String>
// 2. add 메서드: boolean add(String e)
// 3. 42 의 타입: Integer (autoboxed)
// 4. String 과 Integer 매칭 ✗ → 컴파일 에러

3.4 IDE 의 도움

// IDE 가 자동완성으로 도와줌
List<Shipment> shipments = ...;

// IDE 가 표시:
shipments.add(...);   // add(Shipment) 추천
shipments.get(0).getStatus();   // Shipment 의 메서드 자동완성

// 잘못된 타입 빨간 줄
shipments.add(new Cargo());   // 빨간 줄 — Cargo 아님

→ 제네릭 + IDE = 강력한 안전망.

3.5 자동 캐스트 삽입

// 소스 코드
List<String> list = new ArrayList<>();
String s = list.get(0);

// 컴파일러가 실제로 생성하는 바이트코드 (개념적):
List list = new ArrayList();
String s = (String) list.get(0);   // ★ 자동 캐스트 삽입

// 즉, 컴파일러가 명시적 캐스트를 자동으로 삽입
// 사용자는 의식할 필요 X

→ 컴파일러의 마법.

3.6 제네릭의 한계 — 런타임 캐스트는 여전히 위험

// 안전한 사용
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0);   // 안전

// 위험한 사용 — raw type 혼용
List<String> safeList = new ArrayList<>();
List rawList = safeList;   // raw type 으로 캐스트
rawList.add(42);           // 경고만, 컴파일 OK

String s = safeList.get(0);   // ClassCastException!
// 실제로는 Integer 가 들어있음

→ 제네릭은 컴파일 타임 도구, raw type 으로 우회 가능.

3.7 타입 안전 컬렉션의 활용

// 안전한 컬렉션 사용

// 1. 명확한 타입
private final Map<Long, Shipment> cache = new HashMap<>();
//                ↑                          ↑
//                키, 값 타입 명시            구현체

// 2. 메서드 시그니처
public List<Shipment> findByStatus(ShipmentStatus status) {
    return repository.findAll().stream()
        .filter(s -> s.getStatus() == status)
        .toList();
}

// 3. 함수형 인터페이스
Predicate<Shipment> isActive = s -> s.getStatus() == ShipmentStatus.ACTIVE;
Function<Shipment, BigDecimal> toFare = Shipment::getFare;

// 4. 불변 컬렉션
List<String> ports = List.of("BUSAN", "INCHEON", "GWANGYANG");
Set<ShipmentStatus> finals = Set.of(DELIVERED, CANCELLED);

3.8 자기 점검 답변

제네릭이 보장하는 타입 안전성의 정도는?

:

  • 컴파일 타임 안전성:

    • 잘못된 타입 추가 시 컴파일 에러
    • 캐스트 자동 삽입 안전
  • 한계:

    • raw type 혼용 시 우회 가능
    • 런타임 캐스트 ((List<String>)) 는 여전히 위험
    • 타입 소거로 인한 제약
  • 실용적 안전:

    • raw type 회피 + 제네릭 일관 사용
    • 거의 완벽한 타입 안전
    • IDE 도움까지 더하면 강력

4️⃣ raw type 의 정의와 위험

4.1 raw type 의 정의

raw type (원시 타입):

  제네릭 클래스를 타입 매개변수 없이 사용한 것.
  
예:
  List      ← raw type (List<E> 의 raw)
  Map       ← raw type
  ArrayList ← raw type

4.2 raw type 의 등장 이유

Java 5 의 도전:

  제네릭 도입하면서 기존 코드 호환?
  
  기존 코드:
    List list = new ArrayList();
    list.add("Hello");
  
  → 새 List<E> 와 호환 안 되면 모든 코드 깨짐
  → raw type 으로 하위 호환성 유지
  
결과:
  - List<E> 의 raw 가 List
  - 기존 코드 그대로 사용 가능
  - 단, 경고 발생

4.3 raw type 의 사용

// raw type 사용 (권장 X)
List list = new ArrayList();   // raw — 타입 없음
list.add("Hello");
list.add(42);                   // 다양한 타입 추가 가능
list.add(true);

// 꺼낼 때
Object obj = list.get(0);       // Object 타입

// 캐스트 필요
String s = (String) list.get(0);   // 명시적 캐스트

4.4 raw type 의 4가지 위험

1. 타입 안전성 손실
   - 어떤 타입이든 추가 가능
   - 컴파일 타임 검증 X

2. ClassCastException 위험
   - 런타임 에러로 폭발
   - 디버깅 어려움

3. 다른 제네릭과 혼용 시 위험
   List<String> safe = new ArrayList<>();
   List raw = safe;       // 경고
   raw.add(42);           // safe 도 오염
   String s = safe.get(0);   // ClassCastException

4. 코드 의도 불명확
   - "어떤 타입의 List?"
   - 가독성 ↓

4.5 raw type 의 컴파일러 경고

List list = new ArrayList();
list.add("Hello");

// 컴파일러 경고:
// warning: [rawtypes] found raw type: List
// warning: [unchecked] unchecked call to add(E) as a member of the raw type List

자바 컴파일러:

  • rawtypes: raw 타입 사용 경고
  • unchecked: 타입 안전성 검증 못 함 경고

4.6 raw type 과 정상 제네릭의 차이

// 정상 제네릭
List<String> safe = new ArrayList<>();
safe.add("Hello");
// safe.add(42);   // 컴파일 에러

// raw type
List raw = new ArrayList();
raw.add("Hello");
raw.add(42);   // 경고만, 컴파일 OK

// 둘 사이의 변환
List<String> back = safe;   // OK
List rawCopy = safe;        // OK (경고)

List<String> dangerous = (List<String>) raw;   // 경고 + 위험
dangerous.add(42);   // 실제로는 raw 라 추가됨

4.7 raw type vs List<Object> 차이

// raw type
List raw = new ArrayList();
raw.add("Hello");
raw.add(42);
raw.add(true);

// List<Object>
List<Object> objects = new ArrayList<>();
objects.add("Hello");
objects.add(42);
objects.add(true);

// 둘 다 다양한 타입 추가 가능
// 하지만 다름:

// 1. 다른 제네릭과의 변환
List<String> strings = new ArrayList<>();
List raw2 = strings;          // OK (경고)
List<Object> objs = strings;  // ❌ 컴파일 에러!

// 2. 타입 안전성
raw.add(true);          // 경고만
objects.add(true);      // 컴파일 OK + 안전 (Object 가 부모)

List<Object>:

  • 의도적 "어떤 객체든" 표현
  • 컴파일러가 검증 가능
  • 명확한 타입

List:

  • 타입 정보 없음
  • 컴파일러가 경고
  • 모호한 타입

4.8 raw type 사용 시점

권장:
  ✗ 새 코드에선 절대 사용 X

허용:
  - Class 리터럴: List.class (raw type 표현)
  - instanceof 검사: obj instanceof List
  - Java 5 이전 코드와 호환

회피:
  - 새 변수 선언
  - 매개변수
  - 반환 타입

대신:
  - List<E> 사용
  - List<?> 사용 (와일드카드, 다음 Unit)

4.9 자기 점검 답변

raw type 이 위험한 이유와 사용 회피 방법은?

:

  • 위험:

    1. 타입 안전성 손실
    2. ClassCastException 가능
    3. 다른 제네릭과 혼용 위험
    4. 코드 의도 불명확
  • 회피 방법:

    1. 새 코드에선 항상 제네릭 사용
    2. 변수: List<String> 명시
    3. 매개변수: List<E> 또는 List<?>
    4. raw type 보이면 즉시 교체
  • 예외:

    • List.class 같은 리터럴
    • 매우 옛 코드와의 호환

→ raw type 은 Java 5 이전과의 호환성을 위한 유산.


5️⃣ 타입 소거 (Type Erasure)

5.1 타입 소거의 정의

타입 소거 (Type Erasure):

  컴파일 시점에 제네릭 타입 정보가 제거되는 메커니즘.
  
  컴파일 후 바이트코드에는 raw type 만 남음.

영향:
  - 런타임에 타입 정보 X
  - List<String> 과 List<Integer> 는 같은 클래스
  - .class 비교: List<String>.class == List<Integer>.class

5.2 소거 전후 비교

// 소스 코드 (컴파일 전)
public class Box<T> {
    private T value;
    
    public T get() {
        return value;
    }
    
    public void set(T value) {
        this.value = value;
    }
}

// 바이트코드 (컴파일 후, 타입 소거)
public class Box {
    private Object value;       // T → Object
    
    public Object get() {        // T → Object
        return value;
    }
    
    public void set(Object value) {  // T → Object
        this.value = value;
    }
}

핵심:

  • TObject 로 소거
  • 모든 타입 매개변수가 Object 또는 bound 로 대체

5.3 bound type 소거

// bound 가 있는 경우
public class NumberBox<T extends Number> {
    private T value;
    
    public T get() { return value; }
    public void set(T value) { this.value = value; }
}

// 소거 후
public class NumberBox {
    private Number value;       // T → Number (bound)
    
    public Number get() { return value; }
    public void set(Number value) { this.value = value; }
}

T extends NumberTNumber 로 소거.

5.4 컴파일러의 캐스트 삽입

// 소스
Box<String> box = new Box<>();
box.set("Hello");
String s = box.get();

// 컴파일 후
Box box = new Box();
box.set("Hello");
String s = (String) box.get();   // ★ 자동 캐스트

→ 제거된 타입 정보를 캐스트로 보완.

5.5 타입 소거의 결과

List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();

// 같은 클래스
System.out.println(strings.getClass() == ints.getClass());   // true
System.out.println(strings.getClass().getName());   // java.util.ArrayList

// 둘 다 List<E> 의 raw 인 ArrayList
// 타입 매개변수는 런타임에 없음

5.6 타입 소거의 영향

// 1. 같은 시그니처 메서드 정의 불가
public class Box<T> {
    
    public void process(List<String> list) { }
    public void process(List<Integer> list) { }   // ❌ 컴파일 에러
    
    // 이유: 소거 후 둘 다
    // public void process(List list) { }
    // 동일 시그니처 → 오버로딩 불가
}

// 2. 제네릭 배열 생성 불가
T[] arr = new T[10];   // ❌ 컴파일 에러
List<String>[] arr = new List<String>[10];   // ❌ 컴파일 에러

// 이유: 런타임에 T 가 무엇인지 모름

// 3. instanceof 검사 제약
if (obj instanceof List<String>) { }   // ❌ 컴파일 에러
if (obj instanceof List<?>) { }        // ✓ (와일드카드)
if (obj instanceof List) { }            // ✓ (raw)

// 이유: 런타임에 List<String> 과 List<Integer> 가 같음

// 4. 제네릭 타입의 static 멤버 제약
public class Box<T> {
    static T staticField;   // ❌ 컴파일 에러
    
    static void process(T t) { }   // ❌ 컴파일 에러
}

// 이유: T 가 인스턴스마다 다름, static 은 클래스 레벨

5.7 타입 소거의 의도

왜 타입 소거?

이유 1: 하위 호환성
  - Java 1.4 코드와 Java 5 코드 혼용
  - raw type 과 제네릭의 공존
  - .class 파일 호환

이유 2: 단순한 구현
  - JVM 변경 최소화
  - 기존 명령어 (invokevirtual 등) 그대로
  - 추가 메타데이터 X

이유 3: 메모리 효율
  - List<String> 과 List<Integer> 가 같은 클래스
  - 클래스 정보 중복 회피
  - C++ template 의 코드 폭증 회피

이유 4: 점진적 도입
  - 기존 라이브러리 그대로 사용
  - 새 코드는 제네릭 활용
  - 마이그레이션 용이

5.8 타입 소거의 단점

1. 런타임 타입 정보 X
   - 리플렉션 어려움
   - JSON 직렬화 트릭 필요

2. 제네릭 배열 불가
   - new T[] 못 함
   - Array 와 제네릭의 충돌

3. instanceof 검사 제약
   - List<String> 검사 X

4. 같은 시그니처 메서드 충돌
   - 오버로딩 제약

5. 캐스트 비용
   - 컴파일러가 자동 삽입
   - 약간의 오버헤드

5.9 C# 의 다른 접근

C# Generics:
  - 런타임에도 타입 정보 보유
  - List<string> 과 List<int> 가 다른 클래스
  - 더 강력 + 더 안전
  - 하지만 호환성 깨지는 큰 변화

자바:
  - 호환성 우선
  - 타입 소거 채택
  - 트레이드오프

5.10 자기 점검 답변

타입 소거의 정의와 의도는?

:

  • 정의:

    • 컴파일 시 제네릭 타입 정보 제거
    • TObject (또는 bound)
    • 런타임엔 raw type 만
  • 의도:

    1. 하위 호환성: Java 1.4 코드와 공존
    2. JVM 변경 최소화: 기존 명령어 활용
    3. 메모리 효율: 클래스 중복 회피
    4. 점진적 도입: 마이그레이션 용이
  • 단점:

    • 런타임 타입 정보 X
    • 제네릭 배열 불가
    • instanceof 제약
    • 같은 시그니처 오버로딩 제약

→ 호환성을 위한 의도적 타협.


6️⃣ 하위 호환성과 타협

6.1 호환성의 두 종류

소스 호환성 (Source Compatibility):
  - 기존 .java 파일 재컴파일 가능
  - 새 컴파일러로도 컴파일

바이너리 호환성 (Binary Compatibility):
  - 기존 .class 파일이 새 JVM 에서 실행
  - 링크 에러 없음

6.2 Java 5 의 도전

Java 5 의 목표:
  1. 제네릭 도입 (타입 안전성)
  2. 기존 코드와 호환 (소스 + 바이너리)
  3. 점진적 마이그레이션

도전:
  - 새 문법 (List<E>) 도입
  - 기존 List 와 공존
  - 기존 라이브러리 (.class) 그대로

해결:
  - raw type 허용
  - 타입 소거
  - 컴파일러 경고만 (에러 X)

6.3 호환의 예시

// Java 1.4 라이브러리 (변경 없이)
public class OldLibrary {
    public List getNames() {
        ArrayList list = new ArrayList();
        list.add("Alice");
        list.add("Bob");
        return list;
    }
}

// Java 5+ 코드
public class NewCode {
    public void process() {
        OldLibrary lib = new OldLibrary();
        
        // 1. raw type 으로 받기 (호환)
        List names = lib.getNames();
        
        // 2. 제네릭으로 받기 (캐스트 + 경고)
        List<String> names2 = (List<String>) lib.getNames();
        // unchecked cast 경고
    }
}

6.4 raw type 과 제네릭의 변환

// 정상 제네릭 → raw
List<String> safe = new ArrayList<>();
List raw = safe;   // ✓ OK (경고)

// raw → 정상 제네릭
List raw2 = new ArrayList();
List<String> safe2 = raw2;   // ✓ OK (unchecked 경고)

// 둘 다 컴파일 가능, 단 경고

→ Java 5 의 의도적 유연성.

6.5 unchecked 경고

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

public class Compat {
    public static void main(String[] args) {
        // raw type
        List list = new ArrayList();
        list.add("Hello");
        // warning: [unchecked] unchecked call to add(E) as a member of the raw type List
        
        // 변환
        List<String> typed = list;
        // warning: [unchecked] unchecked conversion
        
        // 위험한 사용
        String s = typed.get(0);   // 동작은 OK
        
        list.add(42);              // 잘못된 타입 추가
        String s2 = typed.get(1);  // ClassCastException!
    }
}

6.6 Java 5 의 타협 정리

타협:

1. raw type 허용
   - 기존 코드 그대로
   - 단, 경고 발생

2. 제네릭 + raw 변환 허용
   - 양방향 가능
   - 단, 경고

3. 타입 소거
   - 런타임에 타입 정보 X
   - 단순한 구현 + 호환

4. unchecked 경고
   - 에러 X
   - 단, 권장 X

결과:
  - 점진적 마이그레이션 가능
  - 기존 코드 깨짐 X
  - 새 코드는 안전한 제네릭

6.7 점진적 마이그레이션 예

// Step 1: Java 1.4 코드 (변경 없이)
public class V1 {
    public ArrayList getItems() {
        ArrayList items = new ArrayList();
        items.add("a");
        items.add("b");
        return items;
    }
}

// Step 2: 시그니처만 제네릭화
public class V2 {
    public ArrayList<String> getItems() {   // 반환 타입만
        ArrayList items = new ArrayList();   // 내부는 raw
        items.add("a");
        items.add("b");
        return items;   // 자동 변환 (경고)
    }
}

// Step 3: 완전 제네릭화
public class V3 {
    public List<String> getItems() {
        List<String> items = new ArrayList<>();
        items.add("a");
        items.add("b");
        return items;
    }
}

// Step 4: 사용 코드도 제네릭화
List<String> items = new V3().getItems();
for (String s : items) {
    System.out.println(s);
}

→ 점진적 개선 가능.

6.8 호환성의 가치

Java 5 (2004) 부터 20년 후 (2024):

- 기존 Java 1.4 코드 여전히 작동
- 라이브러리 점진적 제네릭화
- 사용자 코드 점진적 마이그레이션
- 깨지는 코드 없음

이런 호환성은:
- C# 보다 약한 제네릭의 대가
- 하지만 자바 생태계의 안정성
- 큰 시스템 운영 가능

6.9 자기 점검 답변

Java 5 의 제네릭 도입에서 호환성 타협 3가지는?

:
1. raw type 허용:

  • 기존 List 사용 가능
  • 단, 컴파일러 경고
  1. 타입 소거:

    • 런타임에 타입 정보 X
    • JVM 변경 최소화
    • .class 파일 호환
  2. 양방향 변환 허용:

    • 제네릭 ↔ raw type
    • unchecked 경고
    • 점진적 마이그레이션

→ 호환성과 안전성의 균형.


7️⃣ 다이아몬드 연산자 (Java 7+)

7.1 다이아몬드 연산자의 등장

// Java 5, 6 (제네릭 도입 후 ~ 7 이전)
List<String> list = new ArrayList<String>();
//                                    ↑
//                            반복적인 <String>

Map<Long, Shipment> cache = new HashMap<Long, Shipment>();
//                                       ↑
//                              반복적인 <Long, Shipment>

// Java 7+
List<String> list = new ArrayList<>();   // <> 다이아몬드
Map<Long, Shipment> cache = new HashMap<>();

// 컴파일러가 좌변에서 타입 추론

7.2 다이아몬드의 효과

1. 가독성 ↑
   - 중복 제거
   - 핵심 정보 (좌변) 강조

2. 작성 편의
   - 타이핑 ↓
   - IDE 자동완성과 잘 어울림

3. 리팩토링 안전
   - 좌변만 변경하면 됨
   - 우변 자동 따라옴

7.3 다이아몬드 사용 시점

// ✓ 권장: 명확한 좌변
List<String> list = new ArrayList<>();
Map<String, List<Integer>> map = new HashMap<>();

// ✓ 메서드 호출 시
public void process() {
    List<String> tmp = new ArrayList<>();
}

// △ 어색한 경우
ArrayList<String> list = new ArrayList<>();   // OK
List list = new ArrayList<>();                 // △ raw + 다이아몬드 (이상)

// ❌ 제약
// 익명 클래스에선 Java 8 까지 못 씀 (Java 9+ 가능)
List<String> list = new ArrayList<>() {        // Java 9+
    // 익명 클래스 본체
};

7.4 다이아몬드와 타입 추론

// 컴파일러가 좌변에서 타입 추론
List<String> list = new ArrayList<>();
//                              ↑
//                  <> 가 String 으로 추론됨

// 매개변수 추론
public <T> List<T> single(T value) {
    List<T> result = new ArrayList<>();
    result.add(value);
    return result;
}

// 호출
List<String> single = single("hello");
// 컴파일러가 T 를 String 으로 추론

// var 키워드 (Java 10+)
var list = new ArrayList<String>();
// 컴파일러가 list 의 타입을 ArrayList<String> 으로 추론

7.5 다이아몬드의 한계

// 한계 1: 좌변이 불명확하면 추론 안 됨
List<? extends Number> list = new ArrayList<>();
// 와일드카드 + 다이아몬드 → 추론 가능하지만 제약

// 한계 2: 메서드 인자 추론 어려움
process(new ArrayList<>());   // 컨텍스트에 따라 다름
// 명시 권장: process(new ArrayList<String>());

7.6 var 키워드와의 비교 (Java 10+)

// Java 7+
List<String> list = new ArrayList<>();   // 좌변 명시, 우변 다이아몬드

// Java 10+
var list = new ArrayList<String>();      // 좌변 var, 우변 명시
var map = new HashMap<String, Integer>();

// 결론:
// - 다이아몬드: 좌변 타입을 명시
// - var: 우변 타입을 명시
// - 둘 다 타입 정보 한 곳만 명시

권장:

  • 좌변 타입이 중요하면 다이아몬드
  • 단순한 로컬 변수면 var

7.7 다이아몬드 활용 예

// ILIC 예시
public class ShipmentService {
    
    // 필드
    private final Map<Long, Shipment> cache = new HashMap<>();
    private final List<EventListener> listeners = new ArrayList<>();
    
    // 메서드 내
    public List<Shipment> findByStatus(ShipmentStatus status) {
        List<Shipment> result = new ArrayList<>();
        for (Shipment s : findAll()) {
            if (s.getStatus() == status) {
                result.add(s);
            }
        }
        return result;
    }
    
    // Stream 결과
    public Map<String, List<Shipment>> groupByRoute(List<Shipment> shipments) {
        return shipments.stream()
            .collect(Collectors.groupingBy(Shipment::getRoute));
        // 좌변에서 타입 추론, 우변 명시 불필요
    }
}

7.8 자기 점검 답변

다이아몬드 연산자가 좋은 이유는?

:
1. 중복 제거:

  • List<String> list = new ArrayList<>();
  • 우변의 <String> 생략
  1. 가독성 ↑:

    • 좌변에 집중
    • 핵심 정보 강조
  2. 리팩토링 안전:

    • 좌변 변경 시 우변 자동
    • 한 곳만 수정
  3. 타이핑 ↓:

    • 코드 간결
    • IDE 와 잘 어울림

→ Java 7+ 의 작은 개선이지만 큰 효과.


8️⃣ @SuppressWarnings 와 컴파일러 경고

8.1 컴파일러 경고의 종류

// 1. unchecked 경고
List rawList = new ArrayList();
List<String> typed = rawList;   // unchecked conversion

// 2. rawtypes 경고
List raw = ...;   // raw type

// 3. deprecation 경고
Date.UTC(...);   // deprecated 메서드

// 4. removal 경고
sun.misc.Unsafe.getUnsafe();   // 제거 예정 API

8.2 @SuppressWarnings 의 정의

@SuppressWarnings:

  특정 컴파일러 경고를 무시하도록 지정하는 어노테이션.

문법:
  @SuppressWarnings("unchecked")
  @SuppressWarnings({"unchecked", "rawtypes"})

8.3 사용 예

// 메서드 레벨
@SuppressWarnings("unchecked")
public <T> T cast(Object o) {
    return (T) o;   // unchecked cast 경고 무시
}

// 클래스 레벨
@SuppressWarnings("rawtypes")
public class LegacyAdapter {
    private List list;   // raw type
    // ...
}

// 변수 레벨 (지역 변수)
public void process() {
    @SuppressWarnings("unchecked")
    List<String> list = (List<String>) getList();
}

// 필드 레벨
public class MyClass {
    @SuppressWarnings("unchecked")
    private List<String> list = (List<String>) raw;
}

8.4 주요 경고 종류

@SuppressWarnings 값:

"unchecked"
  - unchecked cast
  - unchecked conversion
  - unchecked call

"rawtypes"
  - raw type 사용

"deprecation"
  - deprecated API 사용

"removal"
  - 제거 예정 API

"serial"
  - Serializable 의 serialVersionUID 없음

"unused"
  - 사용 안 하는 변수/매개변수

"all"
  - 모든 경고 (권장 X)

8.5 적절한 사용

// ✓ 좋은 사용: 안전성 확인 후 무시
public class GenericCache<T> {
    private final Map<Class<?>, Object> cache = new HashMap<>();
    
    public <T> T get(Class<T> type) {
        @SuppressWarnings("unchecked")
        T value = (T) cache.get(type);   // 타입 안전 보장됨
        return value;
    }
    
    public <T> void put(Class<T> type, T value) {
        cache.put(type, value);
    }
}

// ✗ 나쁜 사용: 경고 그냥 무시
@SuppressWarnings("all")
public void doSomething() {
    List list = new ArrayList();   // 위험
    list.add(42);
    // ...
}

8.6 사용 가이드

권장:

1. 좁은 범위에 적용
   - 메서드 < 클래스 < 패키지 순
   - 가능한 한 작은 범위

2. 이유 주석
   - 왜 경고 무시하는지 명시
   - 안전성 검증 결과

3. 정기 검토
   - 코드 변경 시 재검토
   - 경고가 여전히 유효한지

회피:

1. @SuppressWarnings("all")
   - 모든 경고 무시
   - 위험 신호 놓침

2. 무분별한 사용
   - 경고가 알려주는 위험 무시
   - raw type 등 더 큰 문제

3. 잘못된 캐스트 정당화
   - 실제로 안전하지 않은데 무시

8.7 적절한 사용 예 — 제네릭 배열

// 제네릭 배열 생성 불가 우회
public class GenericArray<T> {
    private T[] array;
    
    @SuppressWarnings("unchecked")
    public GenericArray(int capacity) {
        array = (T[]) new Object[capacity];   // unchecked cast
        // 안전: 외부에선 T[] 로 보임
    }
    
    public T get(int i) {
        return array[i];
    }
    
    public void set(int i, T value) {
        array[i] = value;
    }
}

이런 패턴이 ArrayList, HashMap 등의 내부 구현에 활용.

8.8 IDE 의 도움

IDE 가 경고 + 추천:
  - 빨간 줄 (에러)
  - 노란 줄 (경고)
  - 회색 (사용 안 함)

Quick Fix:
  - @SuppressWarnings 자동 추가
  - 타입 추가
  - 캐스트 추가

설정:
  - 경고 레벨 조정
  - 특정 경고 무시

8.9 자기 점검 답변

@SuppressWarnings("unchecked") 를 적절히 사용하는 방법은?

:
1. 좁은 범위: 메서드 또는 지역 변수에만
2. 안전성 검증 후: 실제로 안전한지 확인
3. 이유 주석: 왜 무시하는지 명시
4. 정기 검토: 코드 변경 시 재검토

회피:

  • @SuppressWarnings("all")
  • 모든 경고 무시
  • 검증 없이 사용

→ "경고는 친구, 무시는 신중하게".


9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
제네릭의 정의?타입을 매개변수화하는 기능
제네릭 등장 이유?타입 안전성 + 캐스트 제거
Java 5 도입 시기?2004년
raw type 의 정의?타입 매개변수 없는 제네릭 사용
raw type 위험?타입 안전성 손실, CCE
타입 소거의 정의?컴파일 시 제네릭 타입 제거
타입 소거 이유?하위 호환성, JVM 최소 변경
List.class?List.class (소거로 같음)
제네릭 배열 불가 이유?런타임 타입 정보 X
다이아몬드 연산자?Java 7+, 우변 타입 추론
@SuppressWarnings 사용?검증 후 좁은 범위에
C# Generics 차이?C# 은 런타임 타입 보유

9.2 자기 점검 체크리스트

기본 이해

  • Java 1.4 까지의 컬렉션 문제를 안다
  • 제네릭의 등장 시기와 이유를 안다
  • 타입 안전성의 의미를 안다
  • List 의 동작을 안다
  • 자동 캐스트 삽입을 안다

raw type

  • raw type 의 정의를 안다
  • raw type 의 4가지 위험을 안다
  • raw type vs List 차이
  • 컴파일러 경고 (rawtypes, unchecked)
  • 사용 회피 방법을 안다
  • 타입 소거

    • 타입 소거의 정의를 안다
    • 소거 전후 코드를 비교할 수 있다
    • bound 소거 (T extends Number)
    • 4가지 제약 (오버로딩, 배열, instanceof, static)
    • 의도 4가지 (호환성, 단순함, 효율, 점진적)

    호환성과 도구

    • 점진적 마이그레이션 패턴을 안다
    • raw type ↔ 제네릭 변환
    • 다이아몬드 연산자 활용
    • var 키워드와의 비교
    • @SuppressWarnings 적절한 사용

    9.3 추가 심화 질문

    Q1: List 과 List 의 관계는?

    답:

    • 서로 무관
    • List<String>List<Object> 의 자식 X
    • 이유: 제네릭의 불공변성 (invariant)
    • 자세한 건 Unit 5.3 (와일드카드)
    List<String> strings = new ArrayList<>();
    List<Object> objects = strings;   // ❌ 컴파일 에러

    Q2: 제네릭 메서드와 제네릭 클래스의 차이는?

    답:

    // 제네릭 클래스
    public class Box<T> {
        private T value;
        // ...
    }
    
    // 제네릭 메서드 (일반 클래스에 추가)
    public class Utils {
        public static <T> T first(List<T> list) {
            return list.get(0);
        }
    }
    
    // 차이:
    // - 클래스: 인스턴스마다 타입 결정
    // - 메서드: 호출마다 타입 결정
    // - 자세한 건 Unit 5.4

    Q3: List<? extends Number> 의 의미는?

    답:

    • 와일드카드 (wildcard)
    • "Number 또는 그 자식 타입의 List"
    • 예: List, List, List
    • Unit 5.3 에서 자세히

    Q4: 제네릭 + 정적 타입 + 다이아몬드 조합은?

    답:

    // Java 7+
    List<String> list = new ArrayList<>();   // 다이아몬드
    list.add("hello");                         // 타입 안전
    String s = list.get(0);                    // 자동 캐스트
    
    // 효과:
    // - 좌변에서 타입 추론 (다이아몬드)
    // - 컴파일 시 타입 검증 (제네릭)
    // - 런타임 안전 (캐스트 자동)

    Q5: 제네릭의 한계 3가지는?

    답:
    1. 런타임 타입 정보 X (소거)
    2. 제네릭 배열 불가 (new T[] 못 함)
    3. static 멤버에 T 불가 (인스턴스마다 다름)

    추가:

    • 같은 시그니처 오버로딩 X
    • instanceof 검사 제약
    • 기본 타입 (int, long) 직접 사용 X (boxing 필요)

    🎯 핵심 요약 — 3줄 정리

    1. 제네릭 = 타입 매개변수화

    • 컴파일 타임에 타입 안전성 보장
    • 자동 캐스트 삽입
    • 코드 재사용 + 가독성

    2. raw type = 하위 호환의 유산

    • Java 1.4 코드와 호환
    • 컴파일러 경고 발생
    • 새 코드에선 회피

    3. 타입 소거 = 호환성의 대가

    • 컴파일 시 타입 정보 제거
    • JVM 변경 최소화
    • 런타임 제약 (배열, instanceof 등)

    📚 다음으로...

    Unit 5.2 — 타입 매개변수와 타입 인자

    이번 Unit에서 제네릭의 기본을 봤다면, 다음은 타입 매개변수의 활용.

    • T, E, K, V, R 등의 의미
    • 제네릭 클래스 정의
    • 제네릭 메서드 정의
    • 다중 타입 매개변수
    • bounded type parameter

    Phase 5 진행 상황

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

    3주차 누적 진행

    ✅ Phase 1 — Pass by Value (1.1 ~ 1.3 완주)
    ✅ Phase 2 — 컬렉션 프레임워크 (2.1 ~ 2.6 완주)
    ✅ Phase 3 — 해시의 원리 (3.1 ~ 3.4 완주)
    ✅ Phase 4 — 추상화의 두 도구 (4.1 ~ 4.4 완주)
    🚀 Phase 5 — 제네릭과 와일드카드 (1/5 진행)
    
    총: 18/43 Unit 작성 (약 42%)
profile
Software Developer

0개의 댓글