29. 이왕이면 제네릭 타입으로 만들라
📝 아이템 29: 이왕이면 제네릭 타입으로 만들라
🔹 핵심 요약
✅ 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 사용하기 편함 ✅ 기존 타입을 제네릭 타입으로 변경하는 것은 클라이언트에게 영향을 주지 않음 (하위 호환성 유지) ✅ 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 제네릭 타입으로 만들어야 함 ✅ 기존 타입 중 제네릭이었어야 하는 것이 있다면 제네릭 타입으로 변경하는 것이 좋음
🔹 주의 사항
📌 제네릭 타입 안에서는 기본 타입(int, double 등)을 사용할 수 없음 📌 실체화 불가 타입(E, List 등)으로는 배열을 만들 수 없음 📌 제네릭 타입의 배열은 E[]가 아닌 Object[]로 생성 후 형변환해야 함
🔹 제네릭 타입의 필요성: 코드 진화 과정
flowchart TD
classDef stage1 fill:#FFEBCC,stroke:#333,stroke-width:1px
classDef stage2 fill:#E3F2FD,stroke:#333,stroke-width:1px
classDef stage3 fill:#DFF0D8,stroke:#333,stroke-width:1px
classDef issue fill:#FFCDD2,stroke:#D32F2F,stroke-width:1px
classDef benefit fill:#C8E6C9,stroke:#388E3C,stroke-width:1px
classDef customNode fill:#FFEFC8,stroke:#333,stroke-width:1px
subgraph 단계1["1단계: 개별 홀더 클래스"]
direction TB
H1[StudentHolder]:::customNode
H2[ProfessorHolder]:::customNode
H3[기타 정보별 Holder...]:::customNode
H1 -->|"필드: StudentInfo info"| P1[코드 중복]:::issue
H2 -->|"필드: ProfessorInfo info"| P1
H3 --> P1
end
class H1,H2,H3,P1 stage1
subgraph 단계2["2단계: Object 기반 홀더"]
direction TB
OH[InfoHolder]:::customNode
OH -->|"필드: Object info"| P2[타입 불안전]:::issue
P2 -->|"문제점"| C1["런타임 형변환 오류"]:::issue
P2 -->|"사용 시"| C2["명시적 형변환 필요<br>(ProfessorInfo)holder.info"]:::issue
end
class OH,P2,C1,C2 stage2
%% 3단계: 제네릭 홀더 (타입 안전)
subgraph 단계3["3단계: 제네릭 홀더"]
direction TB
GH["InfoHolder<T>"]:::customNode
GH -->|"필드: T info"| B1[타입 안전]:::benefit
B1 -->|"장점"| A1["컴파일 타임 타입 검사"]:::benefit
B1 -->|"장점"| A2["형변환 불필요<br>holder.info.grade"]:::benefit
end
class GH,B1,A1,A2 stage3
%% 흐름선
단계1 --> 단계2
단계2 --> 단계3
1️⃣ 기본 클래스 구현 (코드 중복)
각 데이터 유형별로 별도의 클래스를 구현
코드 중복이 발생하고 유지보수가 어려움
새로운 데이터 유형이 필요할 때마다 새로운 클래스 생성 필요
예시)
StudentHolder와ProfessorHolder클래스의 구조가 거의 동일하여 코드 중복
2️⃣ Object 타입 사용 (타입 불안전)
Object 타입을 사용하여 모든 데이터 유형을 저장
코드 중복은 해결되지만 타입 안전성이 보장되지 않음
런타임에 ClassCastException 발생 가능성이 높음
명시적 형변환이 항상 필요하여 불편함
3️⃣ 제네릭 타입 사용 (타입 안전성 확보)
제네릭을 사용하여 타입 안전성과 코드 재사용성 모두 확보
컴파일 시점에 타입 오류를 발견할 수 있음
명시적 형변환이 불필요하여 코드가 간결해짐
타입 파라미터에 제약을 둘 수 있음
📚 필수 개념 정리
💡 제네릭 타입의 장점
타입 안전성: 컴파일 시점에 타입 체크를 통해 잘못된 타입 사용을 방지합니다.
형변환 불필요: 제네릭을 사용하면 클라이언트 코드에서 명시적인 형변환이 필요 없습니다.
코드 재사용성: 다양한 타입에 대해 동일한 코드를 사용할 수 있습니다.
런타임 오류 감소: 타입 오류를 컴파일 시간에 잡아내므로 런타임 오류가 줄어듭니다.
🔑 제네릭 타입 설계 방법
클래스 선언에 타입 매개변수를 추가합니다.
클래스 내부에서 Object 타입을 사용하던 부분을 타입 매개변수로 대체합니다.
클라이언트 코드에서 타입 매개변수를 명시하면 컴파일러가 해당 타입에 맞게 처리합니다.
💡 제네릭 타입의 제약
제네릭 클래스가 인스턴스화될 때 타입 매개변수는 소거됩니다(타입 소거).
제네릭 타입으로는 배열을 직접 생성할 수 없습니다.
제네릭 타입 파라미터는 정적 필드나 메서드에서 사용할 수 없습니다.
🧐 제네릭 배열을 직접 만들 수 없는 이유
실체화 불가 타입(
E,List<E>등)으로는 배열을 만들 수 없습니다.배열은 런타임에도 자신의 타입을 유지하며, 타입 검사를 수행해야 하기 때문이다.
하지만, 제네릭 타입은 타입 소거로 인해 실체화되지 않는다.
다음과 같은 방법으로 우회할 수 있습니다:
Object 배열을 생성한 후 형변환 (비검사 경고 발생)
배열을 Object[]로 선언 후 원소를 가져올 때 형변환
❓ 이 코드의 결과는?
→ 결과는 true
👉 왜 다르게 보이는 두 리스트가 같은 타입일까? 👉 제네릭 타입이 컴파일 후 어떻게 변하는 걸까?
💡 제네릭의 타입 소거
제네릭 타입 정보는 컴파일 타임에만 존재하고, 런타임에는 제거된다.
따라서
List<String>과List<Integer>는 컴파일 후 같은ArrayList가 된다.왜 그럴까? 제네릭은 타입 안정성을 위해 JDK 1.5부터 도입된 문법으로, 기존 코드(Java 5 이전)와 호환 가능하기 위해 타입 소거된다.
🔥 타입 소거의 영향
장점: ✅ 제네릭을 사용해도 기존 코드(Java 5 이전)와 호환 가능 ✅ 다양한 타입을 하나의 코드로 처리 가능
단점: ❌ 제네릭 배열을 만들 수 없음 (
new E[]불가) ❌ 런타임에 타입 정보를 알 수 없어instanceof같은 타입 검사가 어려움 ❌ 잘못된 형변환 시 컴파일이 아니라 런타임에 오류 발생
🎯 중요한 점
🔹 제네릭 타입은 클라이언트에서 타입 안전성과 편의성을 모두 제공함 🔹 기존 코드의 하위 호환성을 유지하면서 제네릭 타입으로 변경 가능 🔹 제네릭 메서드도 제네릭 타입과 마찬가지로 형변환 없이 사용할 수 있어 타입 안전함 🔹 제네릭 타입에서는 원소의 타입을 컴파일러가 자동으로 관리하므로 오류 가능성이 낮음
💡 코드 예제 및 설명
✅ 제네릭 배열 생성 방법 - 두 가지 방법
🏆 결론: 두 번째 방법은 pop() 호출 때마다 (E) 캐스팅이 필요해서 형변환을 여러 번 수행해야 하고, 타입 안정성이 떨어지지만, 첫 번째 방법은 생성자에서 한 번만 형변환하면 되므로 코드가 더 깔끔하고 타입 안전성도 더 높아 첫 번재 방법이 선호된다.
❗ 어려웠던 점
⚠️ 제네릭 타입의 타입 소거 개념을 이해하기 어려웠음
➡️ 제네릭은 컴파일 타임에만 타입 체크를 하고, 런타임에는 모든 타입 정보가 제거됨. 이것이 실체화 불가 타입이라고 불리는 이유이며, 이로 인해 배열 생성 시 제약이 있음
💭 느낀 점
💡 제네릭 타입을 사용하면 코드의 안전성과 가독성이 크게 향상된다는 것을 깨달았다.
💡 제네릭 타입을 처음 설계할 때는 복잡해 보일 수 있지만, 클라이언트 코드에서 사용할 때 얻을 수 있는 이점이 매우 크다.
💡 기존 코드를 제네릭으로 변환하는 것은 단순한 작업은 아니지만, 타입 안전성을 높이고 코드 품질을 개선하는 데 큰 도움이 된다.
Last updated