코드의 여백

[이펙티브 자바] Generic method와 Generic의 주의사항

by rowing0328

※ 책 내용을 바탕으로 제 관점에서 풀어쓴 글입니다. 일부 내용이 다를 수 있습니다.

 

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

 

제네릭 메서드란

제네릭 메서드는 타입 매개변수를 사용하여 컴파일 시 타입 안정성을 보장하는 메서드이다.
이는 형변환(casting) 없이도 다양한 타입에 대해 안전하고 유연하게 동작할 수 있게 한다.

 

Type-safe Generic Method의 구현

 

예제 코드

List<String> stringList = List.of("T1", "T2", "T3");

// 제네릭 메서드 예시
static <E> List<E> of(E e1, E e2, E e3) {
    return new ImmutableCollections.ListN<>(e1, e2, e3);
}

// UnaryOperator 예제
private static UnaryOperator<Object> IDENTIFY_FN = (t) -> t;

public static <T> UnaryOperator<T> identityFunction() {
    return (UnaryOperator<T>) IDENTIFY_FN; 
    // Object는 제네릭으로 캐스팅되지 않으므로 주의 필요
}

 

설명

IDENTIFY_FN은 Object 타입으로 정의되어 있으나, 내부적으로 타입 캐스팅을 수행한다.

이처럼 타입 안정성을 보장하려면 메서드를 제네릭으로 만들어야 한다.

 

Type 제한

제네릭 메서드는 때로 타입 제한(Bounded Type Parameter)을 통해 더 구체적인 타입 제약을 추가할 수 있다.

 

예제 코드

interface Comparable<T> {
    int compare(T o);
}

 

설명

이처럼 메서드의 파라미터 타입을 특정 인터페이스나 클래스 타입으로 제한하면, 보다 안전한 코드 작성을 할 수 있다.

 

 

아이템 31 - 한정적 와일드카드를 사용해 API 유연성을 높여라

 

Bounded Wildcard란

제네릭 타입의 범위를 제한하여 타입 안전성을 유지하면서도 더 유연한 API를 설계할 수 있도록 돕는 기능이다.

 

예제 코드

public class Stack<E> {
    
    public static final int DEFAULT_SIZE = 20;
    private int size;
    private E[] elements;

    public Stack() {
        elements = (E[]) new Object[DEFAULT_SIZE];
        size = 0;
    }

    public E push(E item) {
        elements[++size] = item;
        return item;
    }

    public void pushAll(Iterable<E> src) {
        for (E e : src) {
            push(e);
        }
    }
    
}

 

문제점

pushAll(Iterable<E> src)E 타입에 고정되어 있어,

Stack<Number>에 Iterable<Integer>Iterable <Integer>를 넘기면 컴파일 오류가 발생한다.

 

이는 제네릭 타입이 불공변성(Invariance)을 가지기 때문이다.

 

Bounded Wildcard를 활용한 유연한 제네릭 설계

 

코드 수정

public void pushAll(Iterable<? extends E> src) {
    for (E e : src) {
        push(e);
    }
}

// 사용 예시
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = List.of(1, 2);
numberStack.pushAll(integers); // 컴파일 가능

 

설명

  • <? extends E>
    E를 상속하거나 구현한 모든 타입을 받을 수 있다.
    상한 경계(Upper Bound)를 설정하여 API 유연성을 높인다.

 

popAll 메서드와 하한 경계 사용

 

코드 수정

public void popAll(Collection<? super E> dst) {
    while (size > 0) {
        dst.add(elements[--size]);
    }
}

// 사용 예시
Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = new ArrayList<>();
numberStack.popAll(objects); // 컴파일 가능

 

설명

  • <? super E>
    E 타입 또는 그 상위(super) 타입을 받는다.
    하한 경계(Lower Bound)를 설정하여 데이터를 안전하게 출력한다.

 

Set의 합집합 구현

 

예제 코드

public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
    Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}

// 사용 예시
Set<Integer> integers = Set.of(1, 3, 5);
Set<Double> doubles = Set.of(2.0, 4.0);
Set<Number> numbers = union(integers, doubles); // 컴파일 가능

 

설명

두 개의 Set을 합치면서 E 타입을 상속한 모든 타입을 지원한다.

유연하면서도 타입 안전성을 유지한다.

 

Target Typing과 자바 버전별 차이

 

자바 7 이하

Set<Number> numbers = Union.<Number>union(integers, doubles);
  • 명시적으로 타입 파라미터를 지정해야 한다.

 

자바 8 이상

Set<Number> numbers = union(integers, doubles);
  • Target Typing이 지원되어 왼쪽 변수의 타입을 기반으로 추론이 가능하다.

 

 

아이템 32 - 제네릭과 가변 인수를 함께 쓸 때는 신중하라

 

가변 인수 (Variadic Arguments)

메서드가 불특정 다수의 인수를 받을 수 있도록 허용하는 기능이다.

메서드를 호출할 때 전달할 인수의 개수를 고정하지 않고, 원하는 개수만큼 전달할 수 있도록 설계된 문법이다.

 

사용 예시

1. 가변 인수 선언

public void printNumbers(int... numbers) {
    for (int number : numbers) {
        System.out.println(number);
    }
}

 

2. 가변 인수 메서드 호출

printNumbers(1, 2, 3);              // 개별 인수 전달
printNumbers(new int[]{4, 5, 6});   // 배열 전달
  • 가변 인수 메서드를 호출할 때는 다음과 같이 다양한 형태로 전달할 수 있다.

 

힙 오염 (Heap Pollution)

자바에서 제네릭과 관련된 런타임 문제로, 제네릭 타입 안전성(type safety)이 깨지는 상황을 말한다.

이는 주로, 제네릭 배열 가변 인수, 그리고 비제네릭 코드와의 혼합에서 발생할 수 있다.

 

예제 코드

class Printer {

    // 제네릭 가변 인수 메서드
    public <T> T[] toArray(T... args) {
        return args; // 가변 인수는 내부적으로 Object[] 배열로 처리됨
    }

    // 세 개의 값을 받아 배열로 반환
    public <T> T[] pick(T a, T b, T c) {
        T[] arr = toArray(a, b, c); // 가변 인수 메서드 호출
        return arr;
    }
    
}

public class Main {
    public static void main(String[] args) {
        Printer p = new Printer();

        // pick 메서드 호출: String 타입으로 기대
        String[] s = p.pick("1", "2", "3"); // 런타임 오류 가능성
    }
}

 

 

가변 인수와 제네릭의 동작 방식의 문제점

toArray(T... args)는 가변 인수를 받아 배열로 반환하는 메서드이다.

가변 인수는 내부적으로 Object[] 배열로 처리되며, 컴파일 시 제네릭 타입 정보는 사라지게 된다.

문제는 이렇게 반환된 배열이 실제로는 Object [] 타입인데,

호출하는 쪽에서는 이를 T [](예를 들어 String []) 타입으로 간주하고 사용한다는 점이다.

 

이 과정에서 타입이 맞지 않으면,

런타임에 ClassCastException이 발생할 가능성이 있다.

 

컴파일 단계에서는 아무 문제가 없어 보이지만,

실행 중에 갑자기 오류가 발생할 수 있기 때문에 주의가 필요하다.

 

불확실한 타입 안정성

T [] arr = toArray(a, b, c);에서, toArray 메서드는 실제로 Object[] 배열을 반환한다.

문제는 이후에 String[] s = p.pick("1", "2", "3");처럼 배열을 사용하려고 하면,

Object []를

 

이로 인해 런타임에 ClassCastException이 발생할 가능성이 있다.

 

pick 메서드는 반환 타입을 T []로 지정했지만,

내부적으로 반환되는 배열은 실제로 Object [] 타입이다.

호출자가 이를 타입 안전한 T []로 착각하는 것이 문제의 핵심이다.

 

힙 오염이 발생하는 이유

  • 제네릭 타입 소거 (Type Erasure)
    자바의 제네릭은 컴파일 타임에만 타입 정보를 검사하고, 런타임에는 타입 정보가 소거된다.
    따라서, 가변 인수로 전달된 T... args는 내부적으로 Object []로 처리되며, 제네릭 타입 정보(T)는 런타임에 유지되지 않는다.
  • 가변 인수와 배열의 결합
    자바의 가변 인수는 내부적으로 배열로 구현된다.
    가변 인수를 사용할 때는 제네릭 타입(T)이 아닌 Object [] 배열이 생성된다.
    이 배열은 호출한 코드에서는 T [] 배열로 처리한다고 믿기 때문에, 런타임에 타입 불일치 문제가 발생한다.

 

힙 오염을 방지하기 위한 해결 방법

 

1. 제네릭 배열 대신 List 사용

import java.util.Arrays;
import java.util.List;

class Printer {
    
    public <T> List<T> toArray(T... args) {
        return Arrays.asList(args); // 가변 인수를 List로 반환
    }

    public <T> List<T> pick(T a, T b, T c) {
        return toArray(a, b, c); // 안전한 List 반환
    }

}

public class Main {
    public static void main(String[] args) {
        Printer p = new Printer();
        List<String> s = p.pick("1", "2", "3"); // 정상 동작
        System.out.println(s); // 출력: [1, 2, 3]
    }
}
  • List를 사용하면 제네릭 타입 정보를 런타임에 유지할 수 있어 타입 안정성을 보장할 수 있다.

2. @SafeVarargs 어노테이션

class Printer {

    @SafeVarargs
    public final <T> T[] toArray(T... args) {
        return args; // 안전한 작업만 수행
    }

    public <T> T[] pick(T a, T b, T c) {
        return toArray(a, b, c);
    }

}

public class Main {
    public static void main(String[] args) {
        Printer p = new Printer();
        String[] s = p.pick("1", "2", "3"); // @SafeVarargs로 경고 억제
        System.out.println(Arrays.toString(s)); // 출력: [1, 2, 3]
    }
}
  • @SafeVarargs는 호출자 측 경고를 억제할 뿐, 메서드 내부의 타입 안정성을 보장하지 않는다.
  • 안전한 작업만 수행해야 하며, 가변 인수 배열을 외부로 노출하거나 수정하는 작업은 피해야 한다.

 

 

아이템 33 - 타입 안정 이종컨테이너를 고려하라

 

동적 형 변환

이중 컨테이너를 사용하면 런타임에서도 타입 안전성을 보장할 수 있다.

특히, Class <T>를 활용해 타입 정보를 명시적으로 전달하고,

type.cast()를 통해 안전하게 형 변환을 수행한다.

 

예제 코드

public <T> void putFavorite(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), type.cast(instance));
}

Favorites favorites = new Favorites();
favorites.putFavorite(Game.class, new Game());

 

설명

HashSet <Integer>에 String을 넣는 것과 같은 타입 불일치 문제를 방지할 수 있다.

컴파일 시 강제적인 타입 체크는 하지 않지만, 런타임에 type.cast()를 통해 타입 오류를 예방한다.

 

타입 안정 이중 컨테이너

제네릭과 Map을 활용하여 다양한 타입의 데이터를 안전하게 저장하고 관리할 수 있는 구조이다.

 

Map <Class <?>, Object>를 기반으로 설계되며,

저장 시 타입 정보를 전달하고, 반환 시 요청한 타입으로 안전하게 변환하는 방식으로 동작한다.

 

이 방식은 다양한 타입을 유연하게 처리하면서도, 런타임 타입 오류를 방지할 수 있다는 점에서 큰 장점이 있다.

 

예제 코드

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Favorites {
    
    private Map<Class<?>, Object> favorites = new HashMap<>();

    // 특정 타입의 데이터를 저장
    public <T> void putFavorite(Class<T> type, T instance) {
        favorites.put(Objects.requireNonNull(type), instance);
    }

    // 특정 타입의 데이터를 요청한 타입으로 반환
    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
    
}

 

장점

  • 타입 안정성
    데이터를 Object 타입으로 저장하지만, 저장 시와 반환 시에 명시적인 타입 정보(Class <T>)를 사용하여 런타임 오류를 방지한다.
  • 유연성
    다양한 데이터 타입을 하나의 컨테이너에서 관리할 수 있다.
    예를 들어, String, Integer, CustomClass 등 여러 타입을 동시에 저장하고 관리할 수 있다.

 

 

마무리

이번 아이템을 학습하며,

와일드카드와 제네릭을 적절히 활용하면 타입 안정성을 유지하면서도 유연한 코드를 설계할 수 있다는 점을 배웠다.

 

제네릭은 강력한 도구지만, 단순히 사용한다고 해서 모든 문제가 해결되는 것은 아니었다.

특히, 상한 경계 (? extends E)와 하한 경계 (? super E)를 적재적소에 활용해야

타입 안정성과 유연성을 동시에 달성할 수 있다는 점을 배웠다.

 

또한, @SafeVarargs를 활용한 타입 검증과 컴파일러 경고 억제 방식도 흥미로웠다.

특히, 컴파일러 경고가 단순한 방해 요소가 아니라, 안전한 코드 설계를 돕는 도구라는 점이 인상 깊었다.

 

블로그의 정보

코드의 여백

rowing0328

활동하기