영주의 개발노트

🏦 기술 부채 정산 | 제네릭 톺아보기(3): 유연한 코딩을 위한 세 걸음 본문

STUDY 📖/JAVA

🏦 기술 부채 정산 | 제네릭 톺아보기(3): 유연한 코딩을 위한 세 걸음

0JUUU 2024. 2. 15. 22:32
반응형

지난번에 예고했던 대로 제네릭의 한정된 타입 매개변수와 와일드카드에 다루며 제네릭 시리즈를 마무리하겠다. 

 

🏦 기술 부채 정산 | 제네릭 톺아보기(2): 유연한 코딩을 위한 두 걸음

지난번에 알아봤던 제네릭 기본 지식에 이어 좀 더 알아보겠다. 🏦 기술 부채 정산 | 제네릭 톺아보기(1): 유연한 코딩을 위한 한 걸음 김영한님의 '스프링 핵심 원리 - 고급편' 강의 예제 코드에

0juuu.tistory.com

 

해당 글을 통해 얻어갈 수 있는 내용

  • 한정된 타입 매개변수
  • 와일드카드

 

우리는 앞서 E, T 등 타입 매개변수에 대해 알게 되었다. 하지만, 그간의 정보로는 ? extends E 라는 코드를 이해하기 어렵다. 제대로 이해하려면 한정된 타입 매개변수와 와일드카드라는 개념을 알아야 한다. 

 

한정된 타입 매개변수

타입 매개변수를 적용함으로써 유연하게 클래스와 메서드를 선언할 수 있었다. 타입 매개 변수로 전달되는 타입의 종류를 제한하고 싶을 때, 한정된 타입 매개변수를 사용하여 특정한 종류의 객체들만을 받게 할 수 있다. extends 키워드를 사용하여 한정한다. 

예를 들어 배열 중 가장 큰 값을 찾는 제네릭 메서드가 있다고 가정하자. 

public class Arr {
    public static <T> T getMax(T[] arr) {
    	if(arr == null || arr.length == 0) {
        	return null;
        }
        T maxValue = arr[0];
        for(int i = 1; i < arr.length; i++) {
        	if(maxValue.compareTo(arr[i]) < 0) {
            	maxValue = arr[i];
            }
        }
        return maxValue;
    }
}​

 

여기서는 한 가지 문제가 있다. 타입 매개변수 T가 compareTo라는 메서드를 호출하고 있다. compareTo 메서드는 Comparable 인터페이스에 정의된 메서드이기 때문에, 만약 T가 Comparable 인터페이스를 구현하하지 않았다면 에러가 발생하게 된다. 따라서, 타입 매개변수 T에 올 수 있는 클래스의 범위를 Comparable 인터페이스를 구현한 클래스로 제한하는 것이 좋겠다. 이를 extends 키워드를 사용하여 해결한다. 

 

public class Arr {
    public static <T extends Comparable> T getMax(T[] arr) {
    	if(arr == null || arr.length == 0) {
        	return null;
        }
        T maxValue = arr[0];
        for(int i = 1; i < arr.length; i++) {
        	if(maxValue.compareTo(arr[i]) < 0) {
            	maxValue = arr[i];
            }
        }
        return maxValue;
    }
}​

 

T extends Comparable은 타입 T가 Comparable 인터페이스를 구현한 클래스들 (String, BigDecimal,...)에 대해서만 호출될 수 있음을 의미한다. 

와일드카드

카드 게임에서의 와일드카드는 다른 카드의 대응이 가능한 특수한 카드, 일명 만능인 조커를 일컫는다. 제네릭에서도 ?를 통해 와일드카드를 제공한다. 즉, 어떤 타입이든 나타낼 수 있게 된다. 와일드카드는 매개변수, 필드, 지역 변수의 타입을 나타내는 데 사용할 수 있다. 

와일드카드는 상한이 있는 와일드카드, 제한 없는 와일드카드, 하한이 있는 와일드카드 3가지 형태로 나타낼 수 있다. 

 

1. 상한이 있는 와일드카드

어떤 클래스 Type의 자손 클래스들을 나타내기 위해 <? extends Type>와 같은 와일드카드를 사용한다. 전체 타입을 나타내는 것이 아닌 일정한 상한이 있는 타입을 표시하는 데 사용된다. extends 다음에는 상한이 될 클래스가 온다. 예시를 보자.

public static void sum(List<? extends Number> list {
    double s = 0.0;
    for(Number n : list) {
    	s += n.doubleValue();
    }
    return s;
}

 

List<? extends Number>는 Number를 상속받은 클래스라면 누구든지 ? 자리에 올 수 있다는 뜻이다. 이렇게 상한이 있는 와일드카드를 사용하여 표시하게 되면 적용 대상을 더 넓힐 수 있다. 왜냐하면 List<Number>라고 명시하는 경우 Number에 대해서만 매치되지만 List<? extends Number>는 Number 뿐만 아니라 Number의 자식 클래스에 대해서도 매치된다. 위 코드는 다음과 같이 호출할 수 있다. 

List<Integer> integers = Arrays.asList(1, 2, 3);
System.out.println("sum of Integers = " + sum(integers));

List<Double> doubles = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum of Doubles = " + sum(doubles));

 

2. 제한 없는 와일드카드

제한 없는 와일드카드는 단순하게 ? 로만 이루어진다. 이는 어떤 타입이든 매치될 수 있음을 의미한다. List <?>는 List<Number>, List<Object>, List<Integer> 등이 될 수 있다. 제한 없는 와일드카드는 다양한 타입을 수용할 수 있다. 하지만, 해당 리스트에 대한 작업은 타입 안정성을 유지하기에는 다소 어려움이 있을 수 있다.

 

3. 하한이 있는 와일드카드

하한이 있는 와일드카드는 Type이라는 클래스가 있을 때 Type 또는 Type의 상위 타입을 나타낼 수 있다. 즉, 어떤 클래스의 조상 클래스들을 <? super Type>이라고 명시하여 나타낼 수 있다는 것이다.

public static void add(List<? super Integer> list) {
    for(int i = 1; i<= 10; i++) {
    	list.add(i);
    }
}

위 메서드는 하한이 있는 와일드카드를 이용하여 유연성을 극대화한 것이다. List<Integer>, List<Number>, List<Object>에 대해 모두 작동시킬 수 있다. 

 

글을 마무리하며

 제네릭은 타입의 제한을 두는 한정된 타입 매개변수와 타입의 확장을 두는 와일드카드를 제공하여 타입의 안정성을 보장하고 코드의 재사용성을 높인다. 이번 글을 마지막으로 제네릭 시리즈의 막을 내리려 한다. 그간의 글을 살펴본다면 제네릭과 관련된 코드가 아무리 등장해도 무섭지 않을 것이다.