나를 기록하다
article thumbnail
반응형

자바 와일드카드

Generics(<>) & WildCard(?)

자바의 제네릭과 와일드 카드를 공부하다가 다음과 같은 내용을 발견했다.

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

public class Main {
	public static void main(String[] args) {
		List<? extends Parent> list = new ArrayList<>();
		Parent parent1 = new Parent("James", 50);
		Parent parent2 = new Child("David", 26, "INFJ");
		Child child1 = new Child("Crystal", 23, "ENTP");
		list.add(parent1);
		list.add(parent2);
		list.add(child1);
	}
}

class Parent {
	String name;
	int age;

	public Parent(final String name, final int age) {
		this.name = name;
		this.age = age;
	}
}

class Child extends Parent {
	String mbti;

	public Child(final String name, final int age, String mbti) {
		super(name, age);
		this.mbti = mbti;
	}
}

위 코드는 얼핏 봤을 때는 문제없이 작동할 것으로 추측되나, 실제로는 컴파일 아래와 같은 컴파일 에러가 발생한다. 이유가 뭘까?

이유를 살펴보기 위해 우선 오라클 와일드 카드 공식 문서를 참고하였다.

 

공식 문서에서 보여주는 예시를 가져왔다. 예시는 컬렉션의 모든 요소를 출력하는 루틴을 작성하는 문제를 다룬다.

 

제네릭스 사용 전(Java 5.0 이전의 구현 방식)

void printCollection(Collection c) {
    Iterator i = c.iterator();
    for (k = 0; k < c.size(); k++) {
        System.out.println(i.next());
    }
}

위 메소드는 컬렉션을 받아와서 그 안의 요소들을 출력하는 역할을 한다. 컬렉션에서 요소를 하나씩 가져오기 위해 반복자 Iterator를 선언한다. 그리고 컬렉션의 크기만큼 for 루프를 순회하고 반복자를 이용하여 현재 위치의 요소를 가져와서 출력한다. 하지만 제네릭스를 활용하지 않았기에 컬렉션의 타입 안정성이 보장되지 않는다. 자바 5.0부터는 제네릭스를 통해 타입 안정성을 제공하기에 가능하면 제네릭스를 활용할 것을 권장한다.

 


제네릭스 및 향상된 for문 사용

void printCollection(Collection<Object> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}


이 메서드는 제네릭스를 활용하여 어떤 종류의 객체도 담을 수 있는 컬렉션을 받아와서 그 안의 요소들을 출력하는 역할을 한다. 향상된 for문을 사용하여 컬렉션 c의 각 요소를 순회하고 이때 각 요소는 Object 타입으로 선언된 e에 저장된 후 println구문으로 출력된다.

 

아주 좋아진 것처럼 보이지만 문제가 존재한다. 이렇게 구현된 메서드는 모든 종류의 객체를 다룰 수 있지만, 특정 타입의 컬렉션을 받아서 처리할 수 없다는 단점이 있다. 이는 Collections<Object>가 모든 종류의 컬렉션의 슈퍼타입이 아닌 것으로 증명되었다.

 

그렇다면 모든 종류의 컬렉션의 슈퍼타입은 과연 무엇일까?

 

이것은 Collection<?>과 같이 작성된다. 이는 어떤 요소 타입이든 일치하는 컬렉션으로 와일드카드 타입이라고 불린다. 와일드카드 타입은 아래 코드처럼 작성할 수 있다.

void printCollection(Collection<?> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

이제 우리는 이 메소드를 어떤 종류의 컬렉션이든 상관없이 호출할 수 있다. 그리고 printCollection() 내부에서는 여전히 c에서 요소를 읽고 이를 Object 타입으로 지정할 수 있다. 이는 컬렉션의 실제 타입이 무엇이든지 객체를 포함하고 있기 때문에 항상 안전하다.

 

Collection<?> c 에는 객체를 추가할 수 없다


단, 여기에 임의의 객체를 추가하는 것은 안전하지 않다!(컴파일 에러)

 

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 컴파일 타임 에러

서두에서 언급했듯이, 이와 같은 코드는 에러가 발생한다. 이유가 무엇일까?

공식 문서에서 언급하기를 c의 요소 타입이 무엇인지 모르기 때문에 여기에 객체를 추가할 수 없다. add() 메소드는 컬렉션의 요소 타입인 E 타입의 인수를 가져간다. 실제 타입 매개변수가 ? 인 경우, 이는 어떤 알려지지 않은 타입에 대한 서브 타입이어야 한다. 여기서 우리가 어떤 타입인지 모르기 때문에 어떤 것도 전달할 수 없다. 유일한 예외는 모든 타입의 멤버인 null이 존재한다.

 

반면에 List<?>가 주어지면 get()을 호출하고 결과를 활용할 수 있다. 결과 타입은 알 수 없는 타입이지만 항상 객체를 알고 있다. 따라서 get()의 결과를 Object 타입의 변수에 할당하거나 타입 Object가 예상되는 곳에 매개변수로 전달하는 것은 안전하다.

 

범위가 제한된 와일드카드(?)

다음으로 범위를 제한하는 와일드카드에 대해 알아보자. 공식문서에서는 간단한 도형을 그릴 수 있는 그림 애플리케이션을 예시로 들었다. 프로그램 내에서 이러한 도형을 나타내려면 다음과 같은 클래스 계층 구조로 정의할 수 있다.

public abstract class Shape {
    public abstract void draw(Canvas c);
}

public class Circle extends Shape {
    private int x, y, radius;
    public void draw(Canvas c) {
        ...
    }
}

public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) {
        ...
    }
}

 

위와 같은 클래스들은 아래 코드처럼 사용하여 캔버스에 그림을 그릴 수 있다.

public class Canvas {
    public void draw(Shape s) {
        s.draw(this);
    }
}


그림에는 일반적으로 여러 도형이 포함될 것이다. 목록으로 표현된다고 가정하면 이를 모두 그릴 수 있는 메소드가 편리할 것이다. 메소드는 아래 코드처럼 작성할 수 있다.

public void drawAll(List<Shape> shapes) {
    for (Shape s: shapes) {
        s.draw(this);
    }
}

 

이제 타입 규칙에 따라 drawAll()은 정확히 Shape 목록에 대해서만 호출될 수 있다. 예를 들어 List<Circle>에서는 호출할 수 없다. 이 메서드가 하는 일은 목록에서 도형을 읽는 것 뿐이므로 List<Circle>에서도 호출할 수 있기 때문에 아쉬움이 남는다. 우리가 정말로 원하는 것은 메서드가 모든 종류의 도형 목록을 받아들이는 것이다.

 

이제 메소드가 모든 종류의 도형 목록을 수용할 수 있도록 변경해보자. 아래 코드처럼 수정할 수 있다.

public void drawAll(List<? extends Shape> shapes) {
    ...
}

 

어떤 차이가 존재하는지 보이는가? 우리는 List<Shape> 타입을 List<? extends Shape>로 변경하였다.

이제 drawAll()은 Shape의 모든 서브클래스 목록을 받아올 수 있게 되었다. 따라서 List<Circle>에 대해서도 호출할 수 있게 됐다.

 

List<? extends Shape>는 경계를 설정한 와일드카드의 예시이다. 여기서 ?는 와일드카드, 즉 알 수 없는 타입을 나타낸다. 그러나 이와 같이 표시할 경우 이 알 수 없는 타입이 실제로 Shape의 서브타입(자식)임을 알고 있다.(단, 이것은 Shape 자체일 수도 있고, 명시적으로 Shape를 확장하지 않아도 된다. 우리는 여기서 Shape를 와일드카드의 상한이라고 부른다.

 

와일드카드 사용의 유연성을 위해 필요한 것

이처럼 와일드카드 사용의 유연성을 위해서는 대가를 치뤄야 한다. 그 대가는 바로 메소드 본문에서 `shapes`에 사용하는 것이 허용되지 않는다는 것이다. 예시로 아래 코드는 컴파일 에러가 발생한다.

public void addRectangle(List<? extends Shape> shapes) {
    // Compile-time error!
    shapes.add(0, new Rectangle());
}

 

컴파일 에러의 이유

왜 위의 코드가 컴파일 에러가 발생하는 것일까?

shapes.add()의 두 번째 매개변수 타입은 ? extends Shape이므로 Shape의 알려지지 않은 서브타입이다. 우리는 이 타입이 무엇인지 모르기 때문에 이곳에 Rectangle을 전달하는 것은 안전하지 않다. 이것이 바로 컴파일 에러가 발생하는 원인이다.

 

Bounded wildcards는 예시에서 자동차 등록소(DMV)가 인구 통계국에 데이터를 전달하는 경우를 다루는 데 필수적이다. 이 예시에서는 데이터가 이름(문자열로 표현)에서 사람으로 매핑되는 구조로 가정하며, 사람은 Person이나 Driver와 같은 하위 클래스로 표현된다. Map<K, V> 타입은 맵의 키(K)와 값(V)을 나타내는 두 개의 타입 인수를 필요로 하는 일반적인 형태의 예시로 언급된다.

형식 매개변수에 대한 명명 규칙을 주목하는 것이 중요하다. 일반 타입인 Map<K, V>에서는 키에 K가 사용되고, 값에는 V가 사용된다.

이제 코드를 살펴보자.

public class Census {
    public static void addRegistry(Map<String, ? extends Person> registry) {
    }
}

// 사용 예시
Map<String, Driver> allDrivers = ... ;  // 운전자 데이터를 담은 맵 생성
Census.addRegistry(allDrivers);  // 운전자 데이터를 등록부에 추가

 

이와 같이 운전자 데이터를 담은 맵을 생성하고, addRegistry에 전달하여 운전자 데이터를 등록부에 추가할 수 있다.

List<? extends Parent> list에 list.add(Child)와 같이 값을 추가할 수 없는 이유에 대해 공식문서를 참고하여 알아보았다. 공식문서에 제네릭과 관련된 다양한 내용들을 앞으로 더 다뤄볼 예정이다.

반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...