코인 관련한 프로젝트를 진행하면서 generic을 적극적으로 사용하고 있습니다.
사용 중에 이슈가 있던 부분을 기록하기 위해 남깁니다.
mongo db에서 save 의 결과를 Mono<? extends 부모클래스> 의 형태로 리턴을 받아 사용하고 있습니다.
사실 이전에는 save의 결과를 따로 확인하는 로직은 없어서 문제가 발생하지는 않았었습니다. 그런데 이번에 테스트 코드를 작성하면서 실제 저장한 값을 확인할 일이 있어 확인하던 중 문제 상황이 발생했습니다.
given(coinGeckoApiRepository.save()).willReturn(자식클래스)
위의 코드에서 저는 Mono<? extends 부모클래스> 부모클래스를 상속받는 자식클래스면 제네릭을 구현하는데 문제가 없을 거라 생각하였는데 컴파일 에러가 발생했던 겁니다.
Wildcard는 generic의 활용도를 높이기 위해 도입이 되었습니다. 근데 여기서 알아야 할 것은 Wildcard는 any type 의 의미가 아니라 unkown type 의 의미란 것입니다. unkown type 이므로 범위가 문제한이 될 수 있습니다.
이러한 문제 상황을 해결하기 위해 Bounded Wildcard(한정적 와일드카드)를 제공합니다. 특정 타입을 기준으로 상한 혹은 하한을 정하여 범위를 지정함으로써 호출 범위를 확장하거나 제한할 수 있습니다. Bounded Wildcard 에는 2가지 종류가 있습니다.
public Mono<? extends 부모클래스> find();
위와 같이 extends를 사용하여 와일드카드의 최상위 타입으로 정의함으로써 상한경계를 설정하는 것입니다. 이로써 find() 메소드를 호출하여 Mono 에서 꺼낼 수(produce) 있는 객체는 부모클래스 혹은 부모클래스를 상속받는 모든 타입이 될것이며 객체 타입도 명확하게 명시가 가능합니다.
given(coinGeckoApiRepository.save()).willReturn(Mono.just(부모클래스))
하지만 위의 코드는 컴파일 에러가 발생합니다. 이유는 위와 같이 원소를 소모(consume)하는 경우에는 produce 와 상황이 다르기 때문입니다. 왜냐하면 Mono의 원소 타입인 <? extends 부모클래스> 로 가능한 타입은 부모클래스와 모든 부모클래스의 자식 타입들이므로 명확하게 어떤 타입이 올지 모르기 때문입니다.
이를 해결하기 위해 두번째 Lower Bounded Wildcard(하한 경계 와일드카드)가 존재합니다.
Mono<? super 자식클래스>
상한 경계 와일드카드와 반대로 super를 사용하여 와일드카드의 최하위 타입을 지정하여 하한 경계를 설정합니다. 위의 코드에서 Mono 에 consume 할 수 있는 타입은 자식클래스와 자식클래스의 부모클래스가 될 수 있을 겁니다.
given(coinGeckoApiRepository.save()).willReturn(Mono.just(부모클래스))
1번에서 발생한 문제 코드도 부모클래스 혹은 부모클래스의 부모클래스로 넣으면 문제 없이 들어갑니다.
Mono<부모클래스> test = find();
그런데 반대로 위와 같이 값을 꺼내는 상황(produce)에서는 문제 상황이 발생합니다. 해당 하한 경계 와일드 카드의 원소가 될 수 있는 타입은 부모클래스와 부모클래스의 부모클래스가 될것입니다. 그런데 실제 값을 꺼낼때는 이게 부모클래스인지 아니면 부모클래스의 부모클래스인지 보장을 할 수 없기 때문입니다.
그래서 이러한 문제를 해결하기 위해 PECS란 공식이 있습니다. Producer인 경우는 Extends 사용, Consumer 경우에는 Super 사용이라는 의미입니다.
이를 잘 활용하여 generic을 사용해보도록 하겠습니다.