Akashic Records

Java 제네릭스(Generics) 이해 하기 본문

Library

Java 제네릭스(Generics) 이해 하기

Andrew's Akashic Records 2024. 10. 21. 11:00
728x90

제네릭스(Generics)

 제네릭스는 컴파일 시점에 타입 검사를 수행하여 코드의 안정성을 높이고, 런타임에 발생할 수 있는 불필요한 캐스팅 에러를 방지합니다. 제네릭스를 통해 개발자는 타입의 불일치로 인한 ClassCastException을 예방할 수 있습니다. 

 

Java 제네릭스를 사용하면 컴파일 시점에 타입 검사를 수행하여 잘못된 타입 변환을 방지하고, 이에 따라 ClassCastException을 효과적으로 피할 수 있습니다. 제네릭스를 통해 코드의 타입 안전성을 높이고, 형 변환과 관련된 오류를 줄이는 것이 가능하므로, Java에서는 제네릭스를 적극적으로 활용하는 것이 좋습니다.

Generics and Type Safety

1. 제네릭스를 사용한 리스트

 Java에서 제네릭스를 사용하면 컴파일러가 타입을 명확히 알 수 있기 때문에 타입 변환을 안전하게 할 수 있습니다. 제네릭스 없이 List를 사용할 경우, 각 요소를 사용할 때마다 명시적으로 형 변환이 필요하고, 이로 인해 ClassCastException이 발생할 수 있습니다.

// 제네릭스를 사용하지 않은 경우
List list = new ArrayList();
list.add("Hello");
list.add(123);  // 서로 다른 타입의 요소 추가 가능

String str = (String) list.get(0); // 형변환 필요 (정상 작동)
String num = (String) list.get(1); // ClassCastException 발생

위 예시에서 리스트에 서로 다른 타입의 요소가 추가되고, 이를 형변환하려고 할 때 ClassCastException이 발생할 수 있습니다.

2. 제네릭스를 사용한 안전한 타입 지정

 제네릭스를 사용하여 타입을 명시적으로 지정하면 컴파일러가 타입 안전성을 보장해주므로 불필요한 형변환을 피할 수 있습니다.

// 제네릭스를 사용한 경우
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123);  // 컴파일 에러: 잘못된 타입을 추가하려고 시도할 때

String str = list.get(0); // 형변환 필요 없음

위 예시에서 List<String> 타입을 사용하면 리스트에 String 타입만 추가할 수 있도록 제한됩니다. 컴파일러는 리스트에 String 이외의 타입을 추가하려고 할 경우 컴파일 오류를 발생시키므로, ClassCastException을 예방할 수 있습니다.

3. 제네릭스 메서드 사용

제네릭스를 메서드에 적용하여 다양한 타입을 수용할 수 있게 하면서도 타입 안전성을 보장할 수 있습니다.

public class GenericMethodExample {

    // 제네릭스 메서드 정의
    public static <T> void printArray(T[] inputArray) {
        for (T element : inputArray) {
            System.out.println(element);
        }
    }

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4, 5};
        String[] stringArray = {"A", "B", "C"};

        // 제네릭스 메서드를 통해 서로 다른 타입의 배열을 안전하게 처리
        printArray(intArray);
        printArray(stringArray);
    }
}

위의 printArray 메서드는 타입 파라미터 <T>를 사용하여 어떤 타입의 배열도 인자로 받을 수 있습니다. 이러한 제네릭 메서드는 타입 안정성을 보장하면서 코드의 재사용성을 높여줍니다.

4. 제네릭 클래스

제네릭 클래스를 사용하여 특정 타입에 국한되지 않는 객체를 정의하면서도 타입 안전성을 유지할 수 있습니다.

public class Box<T> {
    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<>();
        integerBox.set(123);

        Box<String> stringBox = new Box<>();
        stringBox.set("Hello");

        System.out.println(integerBox.get());
        System.out.println(stringBox.get());
    }
}

Box<T> 클래스는 타입 파라미터 T를 사용하여 다양한 타입의 데이터를 처리할 수 있도록 설계되었습니다. 이로 인해 개발자는 런타임 타입 오류를 걱정하지 않고 컴파일 시점에 오류를 예방할 수 있습니다.

타입 안전성(Type Safety)

 프로그램 내에서 변수의 데이터 타입이 일관되고 올바르게 사용되도록 보장하는 것을 의미합니다. 즉, 컴파일러나 런타임 환경이 프로그램이 데이터 타입과 관련된 오류를 방지하도록 도와줍니다. 타입 안전성을 지키는 프로그래밍 언어는 변수나 값이 예상되지 않은 방식으로 사용되지 않도록 보호해 줍니다.

 

 타입 안전성이 중요한 이유는 데이터 타입의 일관성 문제로 인한 잠재적인 오류와 예기치 않은 동작을 방지하기 위해서입니다. 잘못된 타입을 사용하거나 적절하지 않은 형 변환을 시도할 때 발생할 수 있는 오류는 런타임 크래시나 버그로 이어질 수 있기 때문에, 타입 안전성은 프로그램의 안정성과 신뢰성을 높여주는 중요한 개념입니다.

 

 타입 안전성은 코드의 신뢰성과 안정성을 높이는 중요한 프로그래밍 개념입니다. 컴파일 시점에 데이터 타입의 일관성을 검사함으로써 런타임 오류를 줄이고, 개발자가 실수로 잘못된 데이터 타입을 사용할 가능성을 줄이는 데 큰 도움이 됩니다. Java에서는 제네릭스를 이용하여 타입 안전성을 유지하는 것이 좋은 코드 품질을 유지하는 핵심적인 방법입니다.

타입 안전성의 예

타입 안전성이 확보된 언어에서는 데이터 타입 간의 부적절한 변환이 컴파일러에서 검출됩니다. 예를 들어, 자바에서는 다음과 같은 방식으로 타입 안전성을 보장합니다.

List<String> strings = new ArrayList<>();
strings.add("Hello");
// strings.add(123);  // 컴파일 오류: 올바르지 않은 타입을 추가하려고 시도했을 때

위의 코드에서 List<String>을 선언하면 이 리스트에는 String 타입의 데이터만 추가할 수 있습니다. 만약 Integer와 같은 다른 타입의 데이터를 추가하려고 시도하면 컴파일 오류가 발생합니다. 이러한 검사 덕분에 프로그램은 런타임에서 발생할 수 있는 오류를 사전에 예방할 수 있습니다.

타입 안전성의 장점

  1. 오류 감소: 컴파일 타임에 데이터 타입과 관련된 오류를 발견할 수 있기 때문에 런타임 오류를 줄입니다.
  2. 유지보수성 향상: 타입을 명확하게 지정함으로써 코드의 의도가 분명해지고, 나중에 코드를 수정하거나 유지보수하는 것이 쉬워집니다.
  3. 자동 완성 및 정적 분석 지원: IDE에서 타입을 기반으로 자동 완성 기능을 사용할 수 있으며, 코드의 의미가 명확하므로 정적 분석 도구들이 더 효과적으로 작동합니다.

타입 안전성과 자바 제네릭스

Java는 타입 안전성을 높이기 위해 제네릭스(Generics)를 도입했습니다. 제네릭스를 사용하면 컴파일 시점에 타입을 체크하므로, 부적절한 타입 사용을 방지하고 ClassCastException과 같은 런타임 오류를 줄일 수 있습니다.

List<Integer> numbers = new ArrayList<>();
numbers.add(1);
// numbers.add("Hello");  // 컴파일 오류 발생: String을 Integer 리스트에 추가할 수 없음

Integer num = numbers.get(0);  // 안전한 타입 반환, 형변환 필요 없음

위의 예시에서, List<Integer>를 사용하여 리스트에 추가할 수 있는 타입을 명시적으로 Integer로 제한함으로써 타입 안전성을 확보할 수 있습니다. 이렇게 하면 부적절한 타입을 추가할 때 컴파일 오류가 발생하여 런타임 오류를 방지할 수 있습니다.

타입 안전성을 갖지 않는 경우

만약 타입 안전성이 없는 경우, 프로그램은 잘못된 타입의 데이터로 인해 런타임에서 오류가 발생할 가능성이 큽니다. 예를 들어, Object 타입을 사용해 데이터를 관리하는 경우, 모든 타입을 담을 수 있지만 형 변환 과정에서 실수로 잘못된 타입을 사용할 수 있어 ClassCastException이 발생할 수 있습니다.

List list = new ArrayList();
list.add("Hello");
Integer num = (Integer) list.get(0);  // ClassCastException 발생 가능

위 코드에서는 리스트에 문자열을 추가한 후, 이를 잘못된 방식으로 형 변환하고 있어 ClassCastException이 발생할 수 있습니다. 제네릭스를 사용하면 이러한 상황을 사전에 예방할 수 있습니다.

제네릭 타입(Generic Types)과 타입 인자(Type Arguments)

1. 제네릭 타입 (Generic Types)

 제네릭 타입은 클래스나 인터페이스 선언에서 타입을 변수처럼 정의하여 다양한 타입으로 활용할 수 있도록 설계된 것입니다. 이를 통해 개발자는 특정 데이터 타입에 의존하지 않고도 클래스나 메서드를 정의할 수 있어 재사용성이 크게 증가합니다.

 

 제네릭 타입은 클래스나 인터페이스의 정의에서 <T>와 같은 형식으로 사용되며, 여기서 T는 타입 매개변수(type parameter)를 나타냅니다. 이 T는 어떤 타입이든 올 수 있으며, 실제 타입은 객체가 생성될 때 지정됩니다.

예를 들어, Java의 List<T>는 제네릭 타입입니다. T는 나중에 구체적인 타입으로 대체됩니다.

제네릭 클래스 예시

public class Box<T> {
    private T t;  // T는 나중에 지정될 타입을 의미

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<>();  // T가 Integer로 대체됨
        integerBox.set(123);
        System.out.println(integerBox.get());

        Box<String> stringBox = new Box<>();  // T가 String으로 대체됨
        stringBox.set("Hello");
        System.out.println(stringBox.get());
    }
}

위 코드에서 Box<T> 클래스는 제네릭 타입입니다. T는 나중에 사용할 때 구체적인 타입 (Integer 또는 String)으로 지정됩니다. 이를 통해 Box는 재사용성이 높은 타입으로 활용될 수 있습니다.

2. 타입 인자 (Type Arguments)

타입 인자(Type Arguments)는 제네릭 타입을 구체화할 때 실제로 전달되는 타입입니다. 즉, 제네릭 클래스를 사용할 때 그 타입이 어떤 데이터 타입인지를 지정하는 것이 타입 인자입니다.

예를 들어, Box<Integer>에서 Integer타입 인자가 됩니다. 여기서 Box는 제네릭 타입이고, Integer는 그 타입을 구체적으로 정하는 인자입니다.

타입 인자의 예시

Box<String> stringBox = new Box<>(); // 여기서 'String'은 타입 인자임
  • 위 코드에서 Box<String>은 타입 인자가 String인 제네릭 타입의 인스턴스입니다.
  • Box<Integer>는 타입 인자가 Integer인 인스턴스입니다.

타입 인자를 통해, 컴파일러는 그 제네릭 타입의 데이터 타입을 알 수 있게 됩니다. 이를 통해 데이터의 형 변환 없이 직접적으로 접근할 수 있게 되고, 컴파일러는 타입 안전성을 보장합니다.

타입 매개변수의 명명 규칙

제네릭 타입 매개변수는 일반적으로 대문자 한 글자를 사용하여 다음과 같은 관례를 따릅니다:

  • T (Type): 임의의 타입을 의미합니다.
  • E (Element): 컬렉션에서 사용하는 요소 타입.
  • K (Key): 키를 의미하며, 보통 Map과 같은 자료구조에서 사용됩니다.
  • V (Value): 값을 의미하며, 보통 Map의 값에 사용됩니다.
  • N (Number): 숫자와 관련된 타입을 나타낼 때 사용됩니다.

예를 들어, 다음과 같이 Map 인터페이스에서는 KV를 사용하여 키와 값의 타입을 나타냅니다.

Map<K, V> map = new HashMap<>();

와일드카드 (Wildcards)

제네릭 타입을 다룰 때, 와일드카드(?)를 사용하여 타입의 범위를 확장할 수 있습니다. 이는 특정 타입이 아닌 여러 타입을 허용하고자 할 때 사용됩니다.

  • List<?>: 모든 타입의 리스트를 받을 수 있습니다.
  • List<? extends Number>: Number나 그 하위 타입의 리스트를 받을 수 있습니다.
  • List<? super Integer>: Integer나 그 상위 타입의 리스트를 받을 수 있습니다.

와일드카드의 예시

public static void printList(List<?> list) {
    for (Object element : list) {
        System.out.println(element);
    }
}

위 코드에서 List<?>는 모든 타입의 리스트를 허용합니다. 따라서 다양한 타입의 리스트를 이 메서드에서 사용할 수 있습니다.

제네릭 메서드

제네릭 메서드는 메서드 선언 자체에 타입 파라미터를 사용하는 메서드입니다. 이를 통해 클래스가 제네릭이 아니더라도 메서드에서 제네릭 타입을 사용할 수 있습니다.

제네릭 메서드 예시

public class GenericMethodExample {
    // 제네릭 타입 T를 사용하는 메서드
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3};
        String[] stringArray = {"Hello", "World"};

        printArray(intArray);    // T는 Integer로 대체됨
        printArray(stringArray); // T는 String으로 대체됨
    }
}

위의 printArray 메서드는 타입 파라미터 <T>를 사용하여 다양한 타입의 배열을 처리할 수 있게 만듭니다.

제네릭 인터페이스 선언 및 사용

제네릭 인터페이스를 사용하여 특정 타입과 상관없이 다양한 구현을 제공할 수 있습니다.

제네릭 인터페이스 선언 예시

public interface Container<T> {
    void add(T item);
    T get(int index);
}

public class ContainerImpl<T> implements Container<T> {
    private List<T> items = new ArrayList<>();

    @Override
    public void add(T item) {
        items.add(item);
    }

    @Override
    public T get(int index) {
        return items.get(index);
    }

    public static void main(String[] args) {
        Container<String> stringContainer = new ContainerImpl<>();
        stringContainer.add("Hello");
        stringContainer.add("World");
        System.out.println(stringContainer.get(0)); // 출력: Hello
        System.out.println(stringContainer.get(1)); // 출력: World

        Container<Integer> integerContainer = new ContainerImpl<>();
        integerContainer.add(10);
        integerContainer.add(20);
        System.out.println(integerContainer.get(0)); // 출력: 10
        System.out.println(integerContainer.get(1)); // 출력: 20
    }
}

상한 제한(Upper Bound)

 상한 제한(Upper Bound) <T extends SomeClass>와 같이 선언하여 제네릭 타입이 특정 클래스나 인터페이스의 서브타입에 속하도록 제한하는 기능입니다. 이를 통해 타입 안정성을 확보하고, 특정 메서드를 안전하게 사용할 수 있습니다. 와일드카드와 함께 사용하면 다양한 타입의 객체를 유연하게 처리하면서도 타입 안전성을 유지할 수 있습니다.

 

 상한 제한은 제네릭 타입에 대해 더 엄격한 규칙을 정의하여 타입 안전성을 높이고, 제네릭 \메서드 및 클래스에서 특정 기능을 사용할 수 있도록 보장하는 강력한 기능을 제공합니다.

 

다음은 상한 제한(Upper Bound)을 사용하여 제네릭 타입을 선언하고 사용하는 방법을 설명합니다.

1. 상한 제한을 지정하는 방법

상한 제한을 지정하려면 <T extends SomeClass>와 같이 사용합니다. 여기서 T는 제네릭 타입이며, 반드시 SomeClass 또는 SomeClass의 하위 클래스여야 합니다.

상한 제한 예시

public class NumberBox<T extends Number> {
    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public static void main(String[] args) {
        // Number의 하위 타입인 Integer 사용 가능
        NumberBox<Integer> integerBox = new NumberBox<>();
        integerBox.set(123);
        System.out.println("Integer Value: " + integerBox.get());

        // Number의 하위 타입인 Double 사용 가능
        NumberBox<Double> doubleBox = new NumberBox<>();
        doubleBox.set(12.34);
        System.out.println("Double Value: " + doubleBox.get());

        // Number의 하위 타입이 아닌 String 사용 불가
        // NumberBox<String> stringBox = new NumberBox<>(); // 컴파일 오류 발생
    }
}

설명

  • <T extends Number>: 이 선언은 NumberBoxNumber 클래스나 그 하위 클래스(Integer, Double, 등)에 대해서만 인스턴스를 생성할 수 있음을 의미합니다.
  • 이렇게 상한 제한을 지정하면, Number 클래스의 모든 메서드를 사용할 수 있게 되므로 코드 작성 시 여러 편리함이 생깁니다.

2. 상한 제한의 이점

상한 제한을 사용하면 다음과 같은 이점이 있습니다.

  1. 타입 안전성 보장: 특정 범위 내의 타입만 허용하므로, 클래스나 메서드에서 잘못된 타입을 사용하는 것을 방지할 수 있습니다.
  2. 메서드 사용 가능: 특정 상위 클래스나 인터페이스의 메서드를 사용할 수 있다는 것을 컴파일러가 보장합니다. 예를 들어, Number 클래스는 intValue(), doubleValue() 등의 메서드를 제공하므로 이를 안전하게 사용할 수 있습니다.

상한 제한의 메서드 사용 예시

public class MathUtils {
    // 상한 제한을 사용한 제네릭 메서드 정의
    public static <T extends Number> double add(T num1, T num2) {
        return num1.doubleValue() + num2.doubleValue();
    }

    public static void main(String[] args) {
        System.out.println("Sum of 3 and 4.5: " + add(3, 4.5));  // 출력: 7.5
        System.out.println("Sum of 10 and 20: " + add(10, 20));  // 출력: 30.0
    }
}

설명

  • 제네릭 메서드 <T extends Number>를 통해 Number 타입의 서브타입에 대해서만 이 메서드를 호출할 수 있도록 합니다.
  • doubleValue() 사용: T가 반드시 Number의 서브클래스이기 때문에 doubleValue()와 같은 메서드를 사용할 수 있습니다.

3. 상한 제한을 사용하는 와일드카드

와일드카드와 함께 상한 제한을 사용하여 메서드에 전달할 수 있는 제네릭 타입의 범위를 제한할 수 있습니다. 이를 통해 더 유연한 메서드 구현이 가능합니다.

와일드카드 상한 제한 예시

import java.util.List;

public class WildcardExample {
    // 상한 제한을 사용하여 모든 Number의 하위 타입의 리스트를 처리
    public static void printList(List<? extends Number> list) {
        for (Number num : list) {
            System.out.println(num);
        }
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3);
        List<Double> doubleList = List.of(1.1, 2.2, 3.3);

        printList(intList);    // Integer 타입의 리스트를 전달할 수 있음
        printList(doubleList); // Double 타입의 리스트도 전달 가능
    }
}

설명

  • 와일드카드 상한 제한: <? extends Number>Number 클래스의 하위 타입들을 허용합니다.
  • 이로 인해 Integer, Double, FloatNumber의 서브타입을 가진 리스트라면 printList 메서드에 전달할 수 있습니다.

4. 상한 제한과 인터페이스

상한 제한은 특정 클래스뿐만 아니라 인터페이스에도 적용할 수 있습니다. 이를 통해 특정 인터페이스를 구현한 타입으로만 제한이 가능합니다.

인터페이스를 사용한 상한 제한 예시

import java.util.List;

interface Measurable {
    double getMeasure();
}

class Circle implements Measurable {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double getMeasure() {
        return Math.PI * radius * radius;
    }
}

public class MeasurableUtils {
    // Measurable 인터페이스를 구현한 클래스들만 사용 가능하도록 제한
    public static <T extends Measurable> void printMeasurements(List<T> list) {
        for (T item : list) {
            System.out.println("Measurement: " + item.getMeasure());
        }
    }

    public static void main(String[] args) {
        List<Circle> circles = List.of(new Circle(2.0), new Circle(3.0));
        printMeasurements(circles); // Circle은 Measurable을 구현하므로 전달 가능
    }
}

설명

  • 인터페이스 상한 제한: <T extends Measurable>Measurable 인터페이스를 구현하는 타입들만 사용 가능하도록 제한합니다.
  • Circle 클래스는 Measurable 인터페이스를 구현하므로 printMeasurements 메서드의 인자로 사용할 수 있습니다.

와일드카드(Wildcards)

 제네릭 타입에 대한 유연성을 높이기 위해 사용되는 특별한 종류의 제네릭 표기법입니다. 와일드카드를 사용하면 제네릭 타입의 매개변수로 다양한 타입을 수용할 수 있으면서도 안전성을 유지할 수 있습니다. 와일드카드의 가장 큰 장점은 제네릭 클래스를 사용할 때 발생할 수 있는 타입 제약을 완화하면서도 타입 안전성을 보장할 수 있다는 점입니다.

 

와일드카드는 제네릭 타입에 대해 더 많은 유연성을 제공하며, 다양한 유형의 리스트를 안전하게 처리할 수 있도록 도와줍니다.

  • Unbounded Wildcard (<?>)는 타입에 대한 제한이 없고, 모든 타입의 리스트를 허용하지만 읽기 전용입니다.
  • Upper Bounded Wildcard (<? extends SomeClass>)는 특정 클래스의 서브타입만을 허용하며, 주로 리스트의 읽기 작업을 수행할 때 유용합니다.
  • Lower Bounded Wildcard (<? super SomeClass>)는 특정 클래스의 상위 타입을 허용하며, 리스트에 데이터를 추가해야 할 때 유용합니다.

1. Unbounded Wildcard (<?>)

Unbounded Wildcard는 어떤 타입이든 수용할 수 있는 와일드카드입니다. 이 와일드카드는 타입 매개변수에 대해 특별한 제한이 없을 때 사용됩니다.

예시

import java.util.List;

public class WildcardExample {

    // Unbounded 와일드카드 메서드: 모든 타입의 리스트를 받을 수 있음
    public static void printList(List<?> list) {
        for (Object elem : list) {
            System.out.println(elem);
        }
    }

    public static void main(String[] args) {
        List<String> stringList = List.of("Hello", "World");
        List<Integer> intList = List.of(1, 2, 3);

        printList(stringList); // String 타입 리스트도 가능
        printList(intList);    // Integer 타입 리스트도 가능
    }
}

설명

  • List<?>: 모든 타입의 리스트를 받을 수 있습니다.
  • 타입 안전성: <?>를 사용함으로써 어떤 타입의 리스트든 printList에 전달할 수 있습니다.
  • 단, 리스트의 요소를 수정하거나 특정 타입으로 처리하려고 할 때는 Object 타입으로만 다룰 수 있습니다.

2. Upper Bounded Wildcard (<? extends SomeClass>)

상한 제한 와일드카드(Upper Bounded Wildcard)는 특정 클래스나 인터페이스를 상속받는 모든 타입을 허용합니다. 이 와일드카드는 특정 클래스의 서브타입만 받아야 할 때 사용됩니다.

예시

import java.util.List;

public class UpperBoundWildcardExample {

    // Upper Bounded 와일드카드: Number의 하위 클래스만 허용
    public static void sumList(List<? extends Number> list) {
        double sum = 0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        System.out.println("Sum: " + sum);
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3);
        List<Double> doubleList = List.of(1.1, 2.2, 3.3);

        sumList(intList);    // Integer는 Number의 서브클래스이므로 가능
        sumList(doubleList); // Double도 Number의 서브클래스이므로 가능
    }
}

설명

  • <? extends Number>: Number 클래스의 서브타입인 Integer, Double, Float 등의 타입만 허용합니다.
  • 제약: Upper Bounded Wildcard는 읽기에는 좋지만 리스트에 새 요소를 추가하는 것은 제한됩니다. 왜냐하면 제네릭 타입이 더 상위 타입으로 인해 구체적인 하위 타입을 알 수 없기 때문입니다.
  • 따라서 메서드 내에서 이 리스트에 새로운 요소를 추가하는 것은 불가능합니다. 단지 읽기 전용으로 활용됩니다.

3. Lower Bounded Wildcard (<? super SomeClass>)

하한 제한 와일드카드(Lower Bounded Wildcard)는 특정 클래스나 인터페이스의 상위 클래스를 허용합니다. 즉, 해당 클래스 이상의 모든 상위 타입을 받을 수 있습니다.

예시

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

public class LowerBoundWildcardExample {

    // Lower Bounded 와일드카드: Integer의 상위 타입만 허용
    public static void addNumbers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println(list);
    }

    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        List<Number> numList = new ArrayList<>();
        List<Object> objList = new ArrayList<>();

        addNumbers(intList); // Integer 타입 리스트 가능
        addNumbers(numList); // Number 타입 리스트 가능
        addNumbers(objList); // Object 타입 리스트 가능
    }
}

설명

  • <? super Integer>: Integer 클래스의 상위 클래스인 Number, Object 등을 허용합니다.
  • 목적: 리스트에 새로운 Integer 값을 추가할 수 있도록 하기 위해 사용됩니다.
  • 읽기 제한: 리스트에 값을 읽을 때는 Object 타입으로만 반환됩니다. 따라서 특정 타입으로 사용하기 위해서는 타입 캐스팅이 필요할 수 있습니다.

와일드카드의 주요 차이점과 사용 시 고려사항

  • Unbounded Wildcard (<?>): 모든 타입을 허용하지만 타입 정보가 없기 때문에 요소 추가가 불가능합니다. 주로 메서드가 리스트의 요소를 읽기 전용으로 사용할 때 유용합니다.
  • Upper Bounded Wildcard (<? extends SomeClass>): 특정 클래스의 서브타입만 허용합니다. 메서드에서 데이터를 읽어야 할 때 주로 사용됩니다. 리스트에 요소를 추가하지 않고, 특정 타입을 기반으로 처리할 때 유용합니다.
  • Lower Bounded Wildcard (<? super SomeClass>): 특정 클래스의 상위 타입만 허용합니다. 메서드에서 데이터를 추가해야 할 때 유용하며, 상위 클래스들에 대해 안전하게 요소를 추가할 수 있습니다.

예제: 와일드카드의 실사용 비교

와일드카드를 사용할 때 중요한 것은 유연성과 타입 안전성 간의 균형을 맞추는 것입니다. 아래 예시에서는 와일드카드의 각 사용 사례를 비교합니다.

import java.util.List;

public class WildcardComparison {

    // Upper Bounded 와일드카드: 리스트를 읽기만 할 수 있음
    public static void processElements(List<? extends Number> list) {
        for (Number number : list) {
            System.out.println("Processing: " + number.doubleValue());
        }
    }

    // Lower Bounded 와일드카드: 리스트에 요소 추가 가능
    public static void addElements(List<? super Integer> list) {
        list.add(10);
        list.add(20);
        System.out.println("Added elements to list: " + list);
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3);
        List<Number> numList = new ArrayList<>();
        List<Object> objList = new ArrayList<>();

        processElements(intList); // 상한 제한: Integer는 Number의 서브타입이므로 가능

        addElements(numList); // 하한 제한: Number는 Integer의 상위 타입이므로 가능
        addElements(objList); // 하한 제한: Object는 Integer의 상위 타입이므로 가능
    }
}

설명

  • 상한 제한 와일드카드 (<? extends Number>): processElements 메서드는 리스트의 요소를 읽기만 할 수 있습니다.
  • 하한 제한 와일드카드 (<? super Integer>): addElements 메서드는 리스트에 새로운 요소를 추가할 수 있습니다. 추가한 요소는 Integer 타입으로 안전하게 처리됩니다.

제네릭(Generics)힙 오염(Heap Pollution)

 힙 오염(Heap Pollution)은 Java의 제네릭 타입 시스템과 관련된 문제로, 제네릭 타입을 부적절하게 사용하여 타입 안정성이 손상되는 상태를 의미합니다. 힙 오염은 특히 타입 안전성을 잃을 때 예상치 못한 ClassCastException을 유발할 수 있기 때문에 주의가 필요합니다. 이번에 힙 오염의 개념, 발생하는 이유, 그리고 이를 방지하는 방법에 대해 설명하겠습니다.

1. 힙 오염(Heap Pollution)이란?

 힙 오염제네릭 타입 시스템의 제한으로 인해 Java의 런타임 힙 메모리(heap memory)에 잘못된 타입이 들어가게 되는 상황을 의미합니다. 이는 제네릭의 타입 정보가 타입 소거(Type Erasure)에 의해 런타임에 제거되면서, 잘못된 타입이 힙에 저장되어도 이를 컴파일러가 제대로 인지하지 못하는 상황이 발생하기 때문에 일어납니다.

 

 힙 오염이 발생하면 Java의 타입 시스템이 제네릭 타입과 관련된 타입 불일치를 감지하지 못하고, 결국 프로그램의 다른 부분에서 런타임 오류로 이어질 수 있습니다. 이러한 오류는 특히 ClassCastException을 발생시킬 수 있어 프로그램의 안정성을 해칠 수 있습니다.

2. 힙 오염이 발생하는 이유와 예시

힙 오염은 주로 제네릭 타입과 비제네릭 타입이 혼합될 때 발생하며, 이를 일으키는 일반적인 방법 중 하나는 로 타입(raw type)을 사용하는 것입니다.

힙 오염 예시: 로 타입과 제네릭 타입의 혼합 사용

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

public class HeapPollutionExample {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        addElement(stringList, Integer.valueOf(10));  // 의도와 다른 타입을 추가

        // 런타임 오류 발생 가능
        String str = stringList.get(0); // ClassCastException 발생
    }

    // 로 타입을 사용하여 제네릭의 타입 안정성을 깨뜨리는 메서드
    public static void addElement(List list, Object element) {
        list.add(element); // List<String>인데 Integer를 추가함
    }
}

설명

  • 로 타입 사용 (List list): 위 코드에서 addElement() 메서드는 제네릭 타입인 List<String>을 받아야 하지만, 로 타입(List)을 사용하고 있습니다.
  • 힙 오염: 이로 인해 stringListInteger 값을 추가하게 되면, 힙 메모리의 타입 정보가 실제 기대하는 타입(String)과 다르게 저장됩니다. 결국 stringList에서 요소를 가져와서 String으로 처리할 때 ClassCastException이 발생할 수 있습니다.
  • 런타임 오류: 컴파일러는 이 오류를 잡을 수 없기 때문에 런타임에 오류가 발생하며, 이때 발생하는 오류가 힙 오염의 전형적인 예시입니다.

3. 힙 오염의 또 다른 예시: 가변 인자와 제네릭

가변 인자(Varargs)제네릭을 함께 사용할 때에도 힙 오염이 발생할 가능성이 높습니다. 특히 제네릭 타입의 가변 인자 배열을 생성하거나 사용할 때 타입이 안전하지 않은 상황이 만들어질 수 있습니다.

가변 인자와 힙 오염

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

public class VarargsHeapPollutionExample {

    // 가변 인자를 사용한 제네릭 메서드
    @SafeVarargs
    public static <T> void unsafe(T... elements) {
        Object[] array = elements; // 배열의 타입 안정성이 무너짐
        array[0] = "String";       // 컴파일러는 문제를 잡지 못함
        T t = elements[0];         // 예상한 타입과 다른 타입이 배열에 들어가 있음
    }

    public static void main(String[] args) {
        unsafe(1, 2, 3); // T가 Integer로 추론되지만 String이 삽입됨
    }
}

설명

  • 제네릭 가변 인자: unsafe(T... elements) 메서드에서 가변 인자를 사용하여 제네릭 타입의 배열을 전달받습니다.
  • 배열을 가리키는 elements는 컴파일 시점에 Integer[]로 취급될 수 있지만, 내부에서 타입이 불일치한 객체(String)를 할당하게 되면 힙 오염이 발생합니다.
  • @SafeVarargs: 이러한 가변 인자의 제네릭 메서드는 종종 @SafeVarargs로 선언되어 경고를 억제합니다. 그러나 이 경우 여전히 타입 안전성 문제가 남아있기 때문에 사용에 주의해야 합니다.

4. 힙 오염의 영향

  • 런타임 타입 오류: 힙 오염의 가장 일반적인 결과는 ClassCastException과 같은 런타임 오류입니다. 이 오류는 잘못된 타입이 힙 메모리에 들어가게 되면 코드의 다른 부분에서 예상하지 못한 형태로 나타날 수 있습니다.
  • 타입 안정성 훼손: Java의 제네릭은 컴파일 시점에서 타입 안전성을 확보하도록 설계되었지만, 힙 오염이 발생하면 이 목적이 손상되어 결국 예상치 못한 문제를 일으킵니다.

5. 힙 오염 방지 방법

힙 오염을 방지하려면 다음과 같은 몇 가지 베스트 프랙티스를 따르는 것이 좋습니다.

(1) 로 타입(Raw Type) 사용 피하기

로 타입은 제네릭 타입이 추가된 이전의 Java 코드와의 하위 호환성을 유지하기 위해 제공되지만, 제네릭 타입을 제대로 사용하여 타입 안전성을 확보하는 것이 좋습니다.

List<String> stringList = new ArrayList<>(); // 로 타입이 아닌 제네릭 타입을 사용

(2) @SuppressWarnings의 남용 피하기

제네릭을 사용할 때 컴파일러 경고를 억제하기 위해 @SuppressWarnings("unchecked") 어노테이션을 사용하는 경우가 있습니다. 이 경고를 억제하면 타입 안전성을 확인하지 않겠다는 의미가 되므로, 이 어노테이션의 사용은 반드시 필요한 경우로 제한해야 합니다.

(3) @SafeVarargs 사용

제네릭 타입의 가변 인자를 사용할 때에는 @SafeVarargs 어노테이션을 붙여 경고를 억제할 수 있지만, 이를 사용하는 경우 메서드가 타입 안전성을 보장할 수 있도록 작성되었는지 확인해야 합니다.

@SafeVarargs
public static <T> void safeMethod(T... args) {
    for (T arg : args) {
        System.out.println(arg);
    }
}
  • @SafeVarargs: 이 어노테이션을 사용하면 가변 인자와 제네릭 타입을 사용하는 메서드에서 발생할 수 있는 경고를 억제할 수 있지만, 이는 타입 안정성을 보장할 수 있을 때만 사용해야 합니다.

(4) 제네릭 배열 대신 리스트 사용

제네릭 타입의 배열을 사용하지 않는 것이 좋습니다. 대신 리스트와 같은 제네릭 컬렉션을 사용하는 것이 안전합니다.

public class GenericListExample<T> {
    private List<T> list;

    public GenericListExample() {
        list = new ArrayList<>();
    }

    public void addElement(T element) {
        list.add(element);
    }
}
  • 제네릭 배열의 문제를 피하기 위해, 리스트(List)와 같은 제네릭 컬렉션을 사용하면 타입 안전성을 높일 수 있습니다.

6. 결론

Java에서 힙 오염(Heap Pollution)은 주로 제네릭 타입과 비제네릭 타입이 혼용되거나, 가변 인자를 잘못 사용할 때 발생하는 타입 안전성 문제입니다.

  • 힙 오염은 런타임 힙 메모리에 잘못된 타입이 저장되어 ClassCastException과 같은 예기치 않은 오류를 유발할 수 있습니다.
  • 힙 오염을 방지하기 위해서는 로 타입 사용을 피하고, 제네릭 배열을 사용하지 않는 것이 좋습니다. 대신 제네릭 컬렉션을 사용하는 것이 바람직합니다.
  • 가변 인자와 제네릭을 사용할 때는 @SafeVarargs 어노테이션을 통해 신중하게 타입 안전성을 확보해야 합니다.

힙 오염을 이해하고, 이를 방지하기 위한 올바른 코딩 관행을 따르면 Java 프로그램의 타입 안전성을 높일 수 있습니다. 제네릭의 강력함을 제대로 활용하기 위해서는 이러한 한계를 인식하고 적절한 방식으로 사용하는 것이 중요합니다.

728x90

'Library' 카테고리의 다른 글

람다 표현식(Lambda Expression)  (3) 2024.10.21
DevSecOps에서 자동화를 적용하는 방법  (0) 2024.10.10
DevSecOps 란 무엇인가  (0) 2024.10.10
NGINX 부하 분산 및 Proxy  (0) 2024.09.23
NGINX 설정  (0) 2024.09.10
Comments