[자바/스프링] 제네릭

제네릭

제네릭 프로그래밍(generic programming)이란 다양한 데이터 타입에 대해 동작하는 코드를 작성하는 방법을 사용한 프로그래밍 방식이다. 특정 데이터 타입 대신 일반화된 데이터 타입에 대해 동작하는 코드를 작성함으로써 코드의 유연성과 재사용성을 높일 수 있다. 제네릭 프로그래밍을 통해 특정 데이터 타입에 종속되지 않고 일반화된 방식으로 코드를 작성할 수 있다. 제네릭 프로그래밍에서는 런타임이 아닌 컴파일 타임에 데이터 타입과 관련된 확인을 수행하여 코드에 안정성을 더한다. 제네릭(generic)이란 제네릭 프로그래밍에서 일반화된 데이터 타입을 처리하기 위한 언어 수준의 기능이라고 볼 수 있다. 객체가 여러 다른 객체를 구성 요소로 가지는 복합 객체(complex object)이거나 데이터를 요소로 갖는 컬렉션(collection) 객체인 경우 제네릭을 적용하면 보다 유연한 프로그래밍이 가능하다.

제네릭을 통해 선언한 클래스 또는 인터페이스를 제네릭 클래스 또는 제네릭 인터페이스라고 하며 이를 통틀어 제네릭 타입이라고 한다. 타입을 파라미터로 전달받을 수 있는 클래스 또는 인터페이스를 제네릭 타입으로 보면 된다. 제네릭 타입은 어떤 타입을 파라미터로 전달 받을 수 있으므로 제네릭에 담는 타입을 파라미터화된 타입(parameterized type)이라고도 한다. 메서드를 제네릭으로 선언할 수도 있다. 이를 제네릭 메서드라고 한다.

타입을 파라미터로 전달한다는 것은 제네릭 타입이 다룰 수 있는 타입을 명시하는 것과 같다. 코드 상에 제네릭 타입의 파라미터를 명시함으로써 컴파일 타임에 제네릭 타입이 해당 파라미터 타입을 처리하는지 확인할 수 있어 타입 안정성을 높일 수 있다.

제네릭을 사용하는 이유와 장점은 다음과 같다.

  1. 컴파일 시 더 강력한 타입 체크: 자바 컴파일러는 타입 체크를 통해 코드가 타입 안전성을 위반하는 경우 오류를 발생시킨다. 컴파일 타임 오류를 수정하는 것은 런타임 오류를 수정하는 것보다 쉽다.
  2. 타입 캐스팅(형 변환) 코드 제거: 제네릭 타입이 아닌 원시 타입을 사용하는 경우 타입 캐스팅이 필요하다. 제네릭을 사용하면 타입 캐스팅이 필요하지 않다.
  3. 제네릭 알고리즘 구현: 제네릭을 사용하면 다양한 타입을 원소로 갖는 컬렉션에서 작동하고, 사용자될 수 있으며, 타입 안전하고 읽기 쉬운 제네릭 알고리즘을 구현할 수 있다.


원시 타입

원시 타입(raw type)이란 타입 파라미터를 명시하지 않고 사용하는 제네릭 타입을 말한다. 예를 들어 다음 코드에서 List 타입은 원시 타입이다.

List list = new ArrayList<>();


타입을 명시한 제네릭 타입을 파라미터화된 타입이라고 한다. 다음 코드에서 List<String>은 파라미터화된 타입이다.

List<String> list = new ArrayList<>();


제네릭 타입을 정의하면 자동으로 원시 타입도 정의되며 원시 타입은 제네릭 타입(파라미터화된 타입)의 상위 타입이므로 클라이언트는 이를 그대로 사용할 수 있다.

원시 타입에는 어떠한 타입도 원소로 넣을 수 있다. 따라서 런타임 시점에 서로 다른 타입의 원소들이 제네릭 타입에 담겨 있을 수 있다. 원시 타입은 제네릭이 도입되기 전에 사용되던 코드이며 제네릭 타입에 대해 원시 타입을 사용하면 제네릭이 제공하는 타입 안정성, 컴파일 타임 에러 확인 등의 장점을 잃게 되므로 권장되지 않는다.

제네릭 타입에 대해 원시 타입을 사용할 경우 런타임 시 ClassCastException 예외가 발생 가능하다. 예를 들어 원시 타입으로 정의한 List 변수에 String 타입의 값을 원소로 추가한 후 String으로 타입 캐스팅이 되지 않는 다른 타입을 원소로 추가하면 ClassCastException 예외가 발생하게 된다. 이러한 예외는 컴파일 타임이 아닌 런타임에 발생한다. 원시 타입 사용으로 인한 ClassCastException 예외를 막기 위해서는 타입 캐스팅 전 instanceof 연산자나 isInstance() 메서드를 통해 타입 간 관계를 확인한 후 cast() 메서드를 통해 타입 캐스팅을 수행함으로써 안전하게 타입 캐스팅을 수행하는 것이 필요하다.


제네릭과 서브타이핑

서브타이핑(subtyping)이란 상속 관계에 있는 타입에서 하위 타입이 상위 타입을 대체하는 것을 말한다. 변성(variance)이란 복합 타입(complex type)의 서브타이핑이 구성 요소 간의 서브타이핑과 어떻게 관련되는가에 대한 것이다. 복합 타입의 예로는 리스트, 배열 등 구성 요소를 포함하는 자료구조에 해당되는 타입이 있다. 변성의 종류와 개념은 다음과 같다.

  • 공변성 (covariance): 구성 요소의 하위 타입과 상위 타입의 관계가 복합 타입에 대해서도 그대로 유지된다. 이 경우 복합 타입의 하위 타입이 상위 타입 대신 사용될 수 있다. 즉, 복합 타입의 서브타이핑이 가능하다. 리스코프 치환 원칙은 공변성과 관련된 원칙이다.
  • 반공변성 (contravariance): 구성 요소의 하위 타입과 상위 타입의 관계가 복합 타입에 대해서는 역전된다. 이 경우 복합 타입의 상위 타입이 하위 타입 대신 사용될 수 있다. 즉, 복합 타입의 서브타이핑이 불가능하다.
  • 불공변성 (invariance)(또는 무공변성): 구성 요소의 하위 타입과 상위 타입 사이의 관계와 복합 타입 간 관계에는 아무런 관련이 없다. 따라서 하위 타입 대신 상위 타입을 사용하거나 상위 타입 대신 하위 타입을 사용할 수 없다. 즉, 복합 타입의 서브타이핑이 불가능하다.


일반적으로 제네릭에서 상위 클래스를 타입 파라미터로 하는 제네릭 타입과 하위 클래스를 타입 파라미터로 하는 제네릭 타입은 서로 상속 관계에 있을 수도, 그렇지 않을 수도 있다. 즉, 제네릭 타입에 대해 서브타이핑은 가능할 수도 불가능할 수도 있다. 프로그래밍 언어 마다 제네릭 타입의 공변성 지원 여부가 다르다. 자바에서 제네릭 타입은 불공변성을 갖는다. 따라서 제네릭 타입의 서브타이핑이 불가능하다. 예를 들어 대표적인 제네릭 타입인 List의 경우 List<SuperClass>List<SubClass>의 상위 타입이 아니므로 제네릭 타입은 서브타이핑이 불가능하다. 반면 배열은 제네릭 타입인 List와 달리 공변성을 갖는다. 따라서 SuperClass[]SubClass[]의 상위 타입이므로 배열은 서브타이핑이 가능하다. List나 배열 모두 특정 원소를 담는 저장소에 서로 다른 타입의 원소를 넣을 수는 없으며 오류가 발생한다. 배열에서는 런타임에서 오류가 발생하지만 제네릭 타입인 List의 경우 컴파일 시 오류가 발생한다. 이는 런타임에 발생하는 타입 소거(type erasure)라는 제네릭 타입의 특성 때문이다. 따라서 컴파일 시 타입 체크가 가능한 제네릭 타입의 타입 안정성이 더 높다.

상속 관계에서 메서드에 대한 공변성과 반공변성의 특성은 다음과 같다. 상위 클래스에서 구현된 메서드를 하위 클래스에서 오버라이딩할 때 메서드의 반환 타입을 상위 클래스에서 선언한 메서드의 반환 타입의 하위 타입으로 지정할 수 있는 특성을 반환 타입 공변성(return type covariance)이라고 한다. 반환 타입 공변성을 만족하는 경우 클래스의 타입 계층 방향과 메서드의 반환 타입의 타입 계층 방향이 동일하다. 반환 타입 공변성을 사용하면 클라이언트는 타입 캐스팅을 신경 쓰지 않고 메서드를 호출할 수 있다. 공변성과 반공변성의 지원 여부는 언어에 따라 다르며 자바는 반환 타입 공변성을 지원한다. 반환 타입 공변성을 지원하는 언어는 반환 타입에 대해 공변적이지만 그렇지 않은 언어는 반환 타입에 대해 불공변적이라고 한다.

상위 클래스에서 구현된 메서드를 하위 클래스에서 오버라이딩할 때 파라미터 타입을 상위 클래스에서 선언한 메서드의 파라미터의 상위 타입으로 지정할 수 있는 특성을 파라미터 타입 반공변성(parameter type contravariance)이라고 한다. 파라미터 타입 반공변성을 만족하는 경우 클래스의 타입 계층 방향과 메서드의 파라미터의 타입 계층의 방향이 반대이다. 자바는 파라미터 타입 반공변성을 지원하지 않는다.

이처럼 프로그래밍 언어는 유연하게 타입을 할당할 수 있도록 공변성과 반공변성이라는 특성을 제공할 수 있다. 타입을 파라미터로 하는 제네릭 타입의 파라미터에 대해서도 공변성과 반공변성 특성을 통해 더욱 유연하게 제네릭 타입의 파라미터를 할당하고 사용할 수 있다.

제네릭 타입의 파라미터로 전달하는 타입들 간의 관계는 제네릭 타입의 관계와 관련이 있을 수도 있고 없을 수도 있다. 제네릭 타입의 파라미터 타입 간 상속 관계 측면에서 변성은 다음과 같이 정리할 수 있다.

  • 공변성: 하위 파라미터 타입과 상위 파라미터 타입의 관계가 그대로 유지된다. 이 경우 하위 타입을 파라미터로 갖는 제네릭 타입이 상위 타입을 파라미터로 갖는 제네릭 타입 대신 사용될 수 있다. C<SubClass>C<SuperClass>의 하위 타입이다.
  • 반공변성: 하위 타입과 상위 타입의 관계가 역전된다. 이 경우 상위 타입을 파라미터로 갖는 제네릭 타입이 하위 타입을 파라미터로 갖는 제네릭 타입 대신 사용될 수 있다. C<SuperClass>C<SubClass>의 하위 타입이 된다.
  • 불공변성: 하위 타입과 상위 타입 사이에 아무런 관계도 존재하지 않는다. 따라서 하위 타입을 파라미터로 갖는 제네릭 타입이 상위 타입을 파라미터로 갖는 제네릭 타입 대신 사용하거나 그 반대를 수행할 수 없다. C<SubClass>C<SuperClass>는 서로 관계가 없다.


자바에서 제네릭 타입은 기본적으로 불공변성을 갖지만 와일드카드(wildcard)라는 기능을 사용하여 제네릭 타입이 공변성 또는 반공변성을 갖도록 할 수 있다.


와일드카드

자바는 제네릭 타입을 보다 유연하게 정의하고 사용하기 위해 와일드카드(wildcard)라는 타입을 제공한다. 와일드카드를 사용하면 제네릭 타입의 불공변성 특성으로 인한 제약을 극복할 수 있다.

와일드카드 타입에는 비한정적(unbounded) 와일드카드 타입과 한정적(bounded) 와일드카드 타입이 있다. 비한정적 와일드카드 타입은 제네릭 타입의 타입 파라미터를 특정 타입으로 제한하지 않는다. 따라서 모든 타입을 제네릭 타입에 넣을 수 있다. 비한정적 와일드카드 타입은 ?를 사용하여 선언한다. 비한정적 와일드카드 타입에 서로 다른 타입을 원소로 넣을 수 있다는 의미는 아니다. 비한정적 와일드카드 타입을 타입 파라미터로 정의한 제네릭 타입에는 특정 타입이 아닌 다른 타입의 원소는 넣을 수는 없다. null은 넣을 수 있다. 모든 타입에 대해 C<Type> 타입은 C<?> 타입의 서브타입이 된다.

한정적 와일드카드 타입은 제네릭 타입의 타입 파라미터를 특정 타입으로 제한한다. 이를 통해 파라미터의 타입을 특정 타입의 하위 타입 또는 상위 타입으로 제한할 수 있다. 한정적 와일드카드 타입은 특정 타입의 하위 타입으로의 제한 시 ? extends 타입, 특정 타입의 상위 타입으로의 제한 시 ? super 타입을 사용하여 선언한다. 하위 타입으로의 제한하는 ? extends 타입 와일드카드를 상한(upper-bounded) 와일드카드, 상위 타입으로 제한하는 ? super 타입 와일드카드를 하한(lower-bounded) 와일드카드라고 한다.

한정적 와일드카드 타입을 타입 파라미터로 정의한 제네릭 타입에는 특정 타입의 하위 타입(또는 상위 타입)이 아닌 다른 타입의 원소는 넣을 수 없다. null은 넣을 수 있다. C<SuperClass> 타입의 원소로 SubClass 타입 파라미터를 전달할 수 있지만 C<SubClass> 타입의 원소로 SuperClass 타입 파라미터를 전달할 수는 없다. ? extends 타입을 사용한 상한 와일드카드 타입을 사용하여 C<? extends SuperClass> 타입의 원소로 SubClass 타입 파라미터를 전달할 수 있으며 ? super 타입을 사용한 하한 와일드카드 타입을 사용하여 C<? super SubClass> 타입의 원소로 SuperClass 타입 파라미터를 전달할 수 있다.

자바에서는 기본적으로 제네틱 타입의 불공변성에 의해 SubClassSuperClass 하위 타입이지만, C<SubClass>List<SuperClass>의 하위 타입이 아니며, 두 타입은 서로 관련이 없다. C<SubClass>C<SuperClass>의 공통 상위 클래스는 C<?>이다.

C<SubClass>C<SuperClass> 두 타입 간의 관계를 만들기 위해 상한 또는 하한 와일드카드를 사용한다. 상한 와일드카드에 의해 C<? extends SubClass>C<? extends SuperClass>의 하위 타입이 된다. 하한 와일드카드에 의해 C<? super SuperClass>C<? super SubClass>의 하위 타입이 된다.

원시 타입은 타입 안전하지 않지만 와일드카드 타입은 타입 안전하다.


제네릭과 PECS

제네릭 타입은 타입 파라미터에 대해 공변성이 아닌 불공변성을 가진다. 와일드카드를 사용하면 서브타이핑 제약을 해결하고 제네릭 타입을 파라미터로 받는 API를 사용하는 클라이언트에게 유연성을 제공할 수 있다. PECS(Producer-Extends, Consumer-Super)는 리스트 같은 컬렉션(collection) 제네릭 타입에 공변성 또는 반공변성 특성을 제공하기 위해 한정적 와일드카드 타입을 사용한 제네릭의 타입 파라미터 정의 시 따라야하는 규칙이다. PECS는 컬렉션 제네릭 타입에 대한 규칙이다. 타입 파라미터가 컬렉션에 저장된 원소의 조회 또는 접근을 위해 사용된다면 클라이언트는 소비자이며 제네릭 타입은 생산자(producer)이다. 반면 타입 파라미터가 컬렉션의 원소로 저장된다면 클라이언트는 생산자이며 제네릭 타입은 소비자(consumer)이다. PECS에 따르면 제네릭 타입이 생산자라면 와일드카드를 사용한 타입 파라미터 정의 시 상한 와일드카드인 <? extends T>를 사용하고, 소비자라면 하한 와일드카드인 <? super T>를 사용해야 한다.

파라미터화된 타입을 컬렉션에 저장된 원소의 접근 또는 조회를 위해 사용한다면 제네릭 타입은 생산자이다. 제네릭 타입이 일반적인 타입 보다 구체적인 타입(하위 타입)을 파라미터 타입으로 받도록 하기 위해 파라미터 타입 정의 시 extends 키워드를 사용한 상한 와일드카드를 사용한다. 이 상한 와일드카드는 제네릭 타입에 공변성을 제공한다. 상한 와일드카드에 의해 C<SubClass>C<SuperClass>의 하위 타입이 된다. 컬렉션에 저장된 원소를 사용할 때 상한 와일드카드를 사용하는 이유는 제네릭 타입을 파라미터로 전달할 때 하위 타입 원소를 상위 타입 원소로 서브타이핑 가능하게 하고, 생산자인 제네릭 타입의 보다 구체적인 타입의 원소에 접근할 수 있도록 하기 위함이다.

반면 파라미터화된 타입을 컬렉션의 원소로 저장한다면 제네릭 타입은 소비자이다. 제네릭 타입이 구체적인 타입 보다 일반적인 타입(상위 타입)을 파라미터 타입으로 받도록 하기 위해 파라미터 타입(원소의 타입) 정의 시 super 키워드를 사용한 하한 와일드카드를 사용한다. 이 하한 와일드카드는 제네릭 타입에 반공변성을 제공한다. 하한 와일드카드에 의해 C<SuperClass>C<SubClass>의 하위 타입이 된다. 컬렉션에 저장된 원소를 사용할 때 하한 와일드카드를 사용하는 이유는 제네릭 타입을 파라미터로 전달할 때 상위 타입 원소를 하위 타입 원소로 서브타이핑 가능하게 하고, 소비자인 제네릭 타입에 보다 일반적인 타입의 원소를 저장할 수 있도록 하기 위함이다.

PECS는 컬렉션에서 원소를 꺼내 사용할 때는 서브타이핑에 의해 원소의 구체적인 타입을 명시하여도 동작에 문제가 없도록 하며, 컬렉션에 원소를 저장할 때에는 구체적인 타입을 명시하지 않더라도 동작에 문제가 없도록 하는 규칙이다. 컬렉션에서 파라미터 타입 원소를 조회하면서 동시에 컬렉션에 저장한다면 하한 와일드카드나 상한 와일드카드를 사용해서는 안되며 정확한 타입을 지정하는 것이 좋다.

컴포넌트 간 계약(contract)을 기반으로 하는 소프트웨어 설계 기법인 계약에 의한 설계(Design by Contract, DbC)에서는 클라이언트 컴포넌트와 서버 컴포넌트 간의 상호작용에 따른 오퍼레이션(동작)에 관한 명세(규약)를 의미하는 계약이 존재한다. 이 계약은 객체 간 상호작용 또는 협력의 제약 조건이다. 제네릭 타입의 파라미터 타입 정의 시 계약을 위반하는 코드 구현에 의해 서브타이핑이 제한되는 상황을 피해야 한다. PECS는 계약 위반을 피하기 위해 제네릭을 사용한 프로그래밍 시 따라야 하는, 변성과 관련된 규칙으로 볼 수 있다.


타입 소거

타입 소거(type erasure)란 런타임에 제네릭의 타입 정보가 삭제되는 것을 말한다. 런타임에 원소의 타입 정보가 소거되어 원소가 무슨 타입인지 알 수 없으며 컴파일 타임에만 원소의 타입을 검사한다. 제네릭 타입은 타입 소거로 인해 컴파일 타임에는 타입 안전하지만 런타임에는 타입 안전하지 않다. 런타임에 타입 소거가 되는 타입을 비구체화(non-reify)(또는 비실체화) 타입이라고 한다. 제네릭 타입은 비구체화 타입이다.

런타임에 타입 소거가 되지 않는 경우도 있다. 이를 구체화(reify)(또는 실체화) 타입이라고 한다. 런타임에 타입 소거가 일어나지 않는 구체화 타입은 런타임에는 타입 안전하지만 컴파일 타임에는 타입 안전하지 않다. 자바에서 배열은 런타임에 타입 소거가 되지 않는 구체화 타입이다. 따라서 배열은 런타임에는 타입 안전하지만 컴파일 타임에는 타입 안전하지 않다. 배열에 잘못된 타입의 원소를 저장하는 코드의 경우 컴파일 타임에 오류가 발생하지는 않지만 런타임에 원소의 타입이 확인되어 예외(ArrayStoreException)가 발생한다. 컴파일러에 의해 타입 안정성이 보장되도록 하려면 제네릭 타입을 사용해야 한다.

제네릭 타입은 타입 소거로 인해 하위 타입 대신 상위 타입을 사용할 수 있는 암시적 다형성을 지원하지 않는다. 제네릭 타입이 아닌 인터페이스와 상위 클래스의 구현체 및 하위 클래스는 인터페이스 타입 및 상위 클래스를 타입으로 선언할 수 있지만 제네릭(제네릭의 타입 파라미터)에 대해서는 그렇지 않다. 자바에서 제네릭으로 다형성을 활용하기 위해서는 명시적 타입 캐스팅이나 와일드카드를 사용해야 한다.


제네릭 메서드

클래스나 인터페이스 뿐만 아니라 메서드에도 제네릭을 적용할 수 있다. 메서드의 반환 타입 또는 파라미터 타입을 제네릭 타입으로 선언할 수 있다. 메서드 정의 시 타입 파라미터 정의는 반환 타입 정의 앞에 위치시킨다. 제네릭 선언을 하지 않은 클래스 또는 인터페이스에 제네릭 메서드를 선언할 수 있으며 제네릭 클래스 또는 인터페이스에서 제네릭 메서드를 선언할 수도 있다.

다음 코드는 제네릭 타입이 아닌 일반 클래스에서 제네릭 메서드를 정의하는 코드이다.

public class MyClass {
  <T> T getElement() {
    ...
  } 
  
  <T> getElement(T param) {
    ...
  }
  
  <T> T getElement(T param) {
    ...
  } 
  
  <T> List<T> getElement() {
    ...
  }
  
  <T> List<T> getElement(T param) {
    ...
  }
}

제네릭 메서드 정의 시 타입 파라미터는 반환 타입 앞에 위치시키며 꺽쇠(<>) 기호를 사용한다. 메서드의 반환 타입 또는 파라미터를 제네릭으로 타입 파라미터화할 수 있으며 반환 타입은 또다른 제네릭 클래스 또는 인터페이스가 될 수 있다.

다음 코드는 제네릭 클래스에서 제네릭 메서드를 정의하는 코드이다.

public class MyClass<T> {
  T getElement() {
    ...
  }
  
  // 제네릭 클래스의 타입 파라미터로 인해 잘못된 타입 파라미터 선언이 된다.
  <T> T getElement() {
    ...
  }
  
  // 제네릭 클래스의 타입 파라미터로 인해 잘못된 타입 파라미터 선언이 된다.
  <T> getElement(T param) {
    ...
  }
  
  // 제네릭 클래스의 타입 파라미터로 인해 잘못된 타입 파라미터 선언이 된다.
  <T> T getElement(T param) {
    ...
  }
  
  // 제네릭 메서드의 타입 파라미터가 숨겨진다.
  <T> List<T> getElement() {
    ...
  }
  
  // 제네릭 메서드의 타입 파라미터가 숨겨진다.
  <T> List<T> getElement(T param) {
    ...
  }
  
  <U> U getElement() {
    ...
  }
  
  <U> getElement(U param) {
    ...
  }
  
  <U> U getElement(U param) {
    ...
  }
  
  <U> List<U> getElement() {
    ...
  }
  
  <U> List<U> getElement(U param) {
    ...
  }
}

제네릭 클래스의 타입 파라미터명과 제네릭 메서드의 타입 파라미터명이 중복되도록 정의하는 경우 컴파일이 되지 않는다. 따라서 타입 파라미터명을 구별해야 한다. 제네릭 메서드에 대해서만 타입을 제한하기 위해서라도 타입 파라미터명을 다르게 정의해야 한다.

타입 파라미터명을 구분함으로써 다음과 같이 메서드의 반환 타입을 제네릭 클래스의 타입 파라미터로 정의하고, 메서드의 파라미터 타입만 제네릭 메서드의 타입 파라미터로 정의하는 것이 가능하다.

public class MyClass<T> {
  <U> T getElement(U param) {
    ...
  }
  
  <U> List<T> getElement(U param) {
    ...
  }
}

이 경우 메서드의 파라미터에 대한 컴파일 타입 검사는 메서드 호출 시 전달하는 인자의 타입에 의해, 메서드의 반환 타입에 대한 컴파일 타입 검사는 클래스 인스턴스화 시 전달하는 타입에 의해 수행된다.


메서드 반환형이 리스트일 경우 원소를 제네릭 타입으로 선언

리스트를 반환하는 메서드를 제네릭 메서드로 정의하는 경우 인터페이스의 메서드가 리스트를 반환하고 인터페이스를 구현하는 클래스마다 서로 다른 타입을 원소로 갖는 리스트를 반환하도록 하려면 리스트 제네릭 타입의 원소 타입을 파라미터로 전달하면 된다.

다형성을 이용하여 단순히 상속 관계나 인터페이스 구현 관계에 있는 클래스에서 상위 클래스나 인터페이스를 리스트 원소의 타입으로 선언할 수도 있지만 이 경우 제네릭이 제공하는 다양한 이점을 활용하지 못하게 된다.


코드

  • 제네릭 타입 선언
public interface MyService<T> {
  List<T> getListOfType();
}
  • 메서드 호출
    • 타입 파라미터 없이 사용 (원시 타입 사용)
    public class MyServiceImpl {
      @Override
      List getListOfType() {
        List list = new ArrayList<>();
        return list;
      }    
    }
      
    public class MyClient {
      void getListOfType() {
        List list = myServiceImpl.getListOfType();
      }    
    }
    
    • 타입 파라미터 명시
    public class MyServiceImpl<String> {
      @Override
      List<String> getListOfType() {
        List<String> list = new ArrayList<>();
        return list;
      }
    } 
        
    public class MyClient {
      void getListOfType() {
        List<String> list = myServiceImpl.getListOfType();
      }    
    }
    
  • 리스트 원소의 타입이 서로 캐스팅 가능한 경우 (다형성 사용)
public interface MyService<T extends SuperClass> {
  List<T> getListOfType();
}

public class MyServiceImplA<SubClassA> {
  @Override
  List<SubClassA> getListOfType() {
    List<SubClassA> list = new ArrayList<>();
    return list;
  }
}

public class MyServiceImplB<SubClassB> {
  @Override
  List<SubClassB> getListOfType() {
    List<SubClassB> list = new ArrayList<>();
    return list;
  }
}

public class MyClient {
  void getListOfType() {
    List<SubClassA> list = myServiceImplA.getListOfType();
    List<SubClassB> list = myServiceImplB.getListOfType();
  }
}
  • 리스트 원소의 타입이 서로 캐스팅 불가능한 경우 -> 와일드카드 사용


스프링 프레임워크에서 제네릭 타입 빈의 의존성 주입

스프링 프레임워크에서는 다음과 같이 런타임 시 컨테이너에서 특정 타입의 빈을 탐색할 수 있다.

@RequiredArgsConstructor
public class MyClient {
  @Autowire
  private Map<String, MyService> serviceByBeanName;
  
  @Autowire
  private List<MyService> services;
}


제네릭 타입인 ListMap 타입의 필드 선언 및 필드 의존성 주입 시 탐색하고자 하는 빈의 타입을 파라미터로 명시하면 된다. 이렇게 하면 해당 타입의 빈들이 ListMap 자료구조에 담기게 된다. 스프링 4.0 버전부터는 제네릭 타입 빈 또한 탐색 가능하도록 ResolveableType이 제공되며 다음과 같이 제네릭 타입의 클래스 또는 인터페이스 타입으로 등록된 빈도 의존성 주입할 수 있다. 제네릭 타입을 빈 등록하기 위해서는 제네릭 클래스 정의 시 @Component 어노테이션을 설정한다.

@Component
public class MyService<T> {
}

public class MyClient {
  @Autowired
  private List<MyService<Integer>> servicesContainingInteger;
  
  @Autowired
  private List<MyService<String>> servicesContainingString;
} 


상속 구조를 갖는 클래스를 제네릭 타입 파라미터로 갖는 제네릭 클래스 또한 빈 등록 및 탐색할 수 있다.

public interface MyService<T extends SuperClass> {
  ...
}

@Component
public class MyServiceImpl implements MyService<SubClassA> {
  ...
}

@Component
public class MyServiceImpl implements MyService<SubClassB> {
  ...
}

@RequiredArgsConstructor
public class MyClient {
  @Autowire
  private Map<String, MyService<SubClassA> serviceByBeanName;
  @Autowire
  private Map<String, MyService<SubClassB> serviceByBeanName;
  
  @Autowire
  private List<MyService<SubClassA> services;
  @Autowire
  private List<MyService<SubClassB> services;
}


그러나 위와 같은 경우 상위 클래스 타입을 제네릭 타입 파라미터로 갖는 클래스를 빈 등록 및 탐색할 수는 없다. 일반적으로 인터페이스 구현체나 하위 클래스를 빈 등록하는 경우 인터페이스 또는 상위 클래스 타입으로 빈 탐색 및 의존성 주입이 가능하지만 제네릭의 파라미터 타입의 경우 이러한 스프링 프레임워크의 동작이 동일하게 적용되지는 않는다.

@RequiredArgsConstructor
public class MyClient {
  // MyService<SuperClass> 타입 빈 등록이 되지 않는다.
  @Autowire
  private Map<String, MyService<SuperClass> serviceByBeanName;
  
  // MyService<SuperClass> 타입 빈 등록이 되지 않는다.
  @Autowire
  private List<MyService<SuperClass> services;
}


이를 위해서는 인터페이스 구현체 정의 시 파라미터 타입을 상위 클래스로 명시해야 빈 등록이 된다.

public interface MyService<T extends SuperClass> {
  ...
}

@Component
public class MyServiceImpl implements MyService<SuperClass> {
  ...
}

@RequiredArgsConstructor
public class MyClient {
  // MyService<SuperClass> 타입 빈 등록이 된다.
  @Autowire
  private Map<String, MyService<SuperClass> serviceByBeanName;
  
  // MyService<SuperClass> 타입 빈 등록이 된다.
  @Autowire
  private List<MyService<SuperClass> services;
}


참고

Comments