30. 이왕이면 제네릭 메서드로 만들라

📝 아이템 30: 이왕이면 제네릭 메서드로 만들라

🔹 핵심 요약

✅ 메서드도 클래스와 마찬가지로 제네릭으로 만들면 더 안전하고 사용하기 편함 ✅ 제네릭 메서드는 클라이언트가 입력 매개변수와 반환값을 명시적 형변환 없이 사용 가능 ✅ 불필요한 형변환을 없애 코드 가독성과 안전성을 높임 ✅ 자기 자신이 속한 클래스에 정의된 타입 매개변수와는 별개의 타입 매개변수 사용 가능

🔹 주의 사항

📌 타입 매개변수 명명 규칙을 따르는 것이 좋음(E, T, K, V 등) 📌 제네릭 메서드를 호출할 때는 명시적 타입 인수를 대부분 생략 가능(컴파일러가 타입 추론) 📌 재귀적 타입 한정(recursive type bound)을 사용해 타입 매개변수의 허용 범위를 한정할 수 있음 📌 제네릭 싱글턴 팩토리를 사용하면 불변 객체를 여러 타입으로 활용 가능

🔹 제네릭 메서드의 필요성: 코드 진화 과정

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
        R1["Object 반환 타입"]:::customNode
        P1["Object[] 매개변수"]:::customNode
        R1 -->|"반환 시"| C1["클라이언트에서 형변환 필요"]:::issue
        P1 -->|"호출 시"| W1["컴파일러 경고 발생"]:::issue
        W1 -->|"런타임"| E1["ClassCastException 위험"]:::issue
    end
    class R1,P1,C1,W1,E1 stage1

    subgraph 단계2["2단계: 제네릭 메서드"]
        direction TB
        R2["<T> 타입 매개변수 도입"]:::customNode
        P2["T... 가변인자"]:::customNode
        R2 -->|"컴파일 시"| B1["타입 안전성 보장"]:::benefit
        P2 -->|"사용 시"| B2["형변환 불필요"]:::benefit
        B1 -->|"장점"| A1["컴파일 타임 오류 검출"]:::benefit
        B2 -->|"장점"| A2["코드 가독성 향상"]:::benefit
    end
    class R2,P2,B1,B2,A1,A2 stage2

    subgraph 단계3["3단계: 고급 활용"]
        direction TB
        U1["제네릭 싱글턴 팩토리"]:::customNode
        U2["재귀적 타입 한정"]:::customNode
        U1 -->|"사용"| B3["불변 객체의 타입별 재사용"]:::benefit
        U2 -->|"활용"| B4["타입 매개변수 제약 설정"]:::benefit
    end
    class U1,U2,B3,B4 stage3

    %% 흐름선
    단계1 --> 단계2
    단계2 --> 단계3

1️⃣ 원시적 메서드 구현 (타입 불안전)

  • Object 타입을 사용해 모든 타입의 객체를 처리

  • Set의 원소가 Object 타입으로 취급

  • 클라이언트에서 명시적 형변환이 필요

  • 형변환 오류가 런타임에 발생할 가능성 존재

  • 컴파일러가 타입 안정성을 보장하지 못함

2️⃣ 제네릭 메서드 구현 (타입 안전성 확보)

  • 타입 매개변수를 사용하여 타입 안전성을 확보

  • 컴파일 시점에 타입 오류를 확인 가능

  • 클라이언트에서 명시적 형변환이 불필요

  • 컴파일러가 타입 추론을 통해 오류를 방지

3️⃣ 고급 활용 기법

  • 제네릭 싱글턴 팩토리를 사용하여 불변 객체를 타입별로 활용

  • 재귀적 타입 한정을 사용하여 타입 매개변수의 범위 제한

  • 타입 추론과 메서드 참조를 통한 간결한 코드 작성


📚 필수 개념 정리

💡 제네릭 메서드란?

  • 메서드 선언부에 타입 매개변수를 선언한 메서드입니다.

  • 타입 매개변수의 범위는 메서드 내부로 한정됩니다.

  • 정적 메서드나 인스턴스 메서드 모두 제네릭 메서드로 만들 수 있습니다.

  • 제네릭 클래스가 아니더라도 제네릭 메서드는 정의할 수 있습니다.

🔑 제네릭 메서드 작성법

  • 메서드의 제한자와 반환 타입 사이에 타입 매개변수를 선언합니다.

  • 일반적으로 타입 매개변수는 대문자 한 글자로 명명합니다.

💡 제네릭 싱글턴 팩토리

  • 제네릭은 런타임에 타입 정보가 소거되므로, 여러 타입으로 활용 가능한 하나의 객체를 만들 수 있습니다.

  • 불변 객체를 타입 안전하게 여러 타입으로 재사용할 수 있습니다.

  • Collections.emptyList(), Collections.emptyMap() 등이 이 패턴을 사용합니다.

🧐 재귀적 타입 한정(Recursive Type Bound)

  • 타입 매개변수가 자신을 포함하는 표현식에 의해 한정됩니다.

  • 주로 타입의 자연적 순서를 정의하는 Comparable 인터페이스와 함께 사용됩니다.

  • 타입 매개변수에 특정 인터페이스나 클래스를 구현/상속한 타입만 받도록 제한합니다.

재귀적 타입 한정(Recursive Type Bound)이 왜 필요한가요?

  • 이 제약이 없다면, max 메서드는 compareTo 메서드를 호출할 수 없습니다.

  • compareTo 메서드는 Comparable을 구현한 클래스에서만 사용 가능하기 때문입니다.

  • 타입 EComparable을 구현한다는 보장이 있어야만 사용할 수 있습니다.

  • 재귀적 타입 한정을 사용한다면 얻을 수 있는 이점이 있습니다.:

  1. 컴파일러가 타입 안전성을 검증할 수 있습니다.

  2. 타입 제약을 통해 메서드가 예상대로 동작하도록 할 수 있습니다.

  3. 비교 불가능한 타입으로 메서드를 호출하면 컴파일 오류가 발생합니다.

💡 타입 추론과 제네릭 메서드

  • Java 컴파일러는 메서드 호출 시 타입 매개변수의 값을 추론할 수 있음

  • 대부분의 경우 호출 시 타입 인수를 명시할 필요가 없음

  • 타입 추론의 한계가 있는 경우 명시적으로 타입을 지정할 수 있음

🔥 제네릭 메서드의 이점과 한계

  • 장점: ✅ 타입 안전성 - 컴파일 시점에 타입 오류 검출 ✅ 가독성 향상 - 명시적 형변환 제거 ✅ 코드 재사용성 - 다양한 타입에 동일한 로직 적용

  • 한계: ❌ 기본 타입(int, double 등)은 직접 사용할 수 없음 ❌ 타입 소거로 인해 런타임에 타입 정보 접근 제한적 ❌ 한 메서드에서 서로 다른 타입에 대해 다른 동작을 구현하기 어려움


🎯 중요한 점

🔹 제네릭 메서드는 타입 매개변수를 선언하는 메서드 🔹 제네릭 메서드를 사용하면 클라이언트 코드에서 형변환이 불필요 🔹 컴파일 시점에 타입 안전성을 보장해 런타임 오류 가능성 감소 🔹 제네릭 싱글턴 팩토리와 재귀적 타입 한정은 제네릭 메서드의 활용도를 높임


💡 코드 예제 및 설명

✅ 제네릭 메서드의 다양한 활용

✅ 재귀적 타입 한정을 이용한 예제

🏆 더 복잡한 재귀적 타입 한정 예제

이 메서드는 <T extends Comparable<? super T>>라는 타입 한정을 사용해 T 타입이 자신과 비교 가능함을 보장합니다. ? super T는 T의 상위 타입을 허용하므로, T가 상위 타입과 비교 가능한 경우에도 활용할 수 있습니다.


❗ 어려웠던 점

⚠️ 재귀적 타입 한정이 처음에는 이해하기 어려웠음

➡️ <E extends Comparable<E>>는 E 타입이 자기 자신과 비교 가능해야 함을 의미. 즉, E 타입은 Comparable 인터페이스를 구현해야 한다는 제약 조건을 나타냄

⚠️ 제네릭 싱글턴 팩토리 패턴의 활용이 어려웠음

➡️ 제네릭은 타입 소거로 인해 런타임에는 하나의 객체만 존재. 이를 활용해 여러 타입으로 재사용할 수 있는 패턴임을 이해함


💭 느낀 점

💡 제네릭 메서드는 복잡해 보이지만, 코드 안전성과 가독성을 크게 향상시킨다.

💡 타입 추론 덕분에 제네릭 메서드를 사용할 때 실제로는 타입을 명시할 필요가 거의 없어 편리하다.

💡 제네릭의 타입 한정을 잘 활용하면 API의 표현력과 안전성을 동시에 높일 수 있다.

💡 제네릭 메서드를 처음부터 설계하면 나중에 타입 관련 문제가 발생할 가능성이 크게 줄어든다.

Last updated