Learn business/Java

[5편] 제네릭이란?


Collectors에 선언된 제네릭 메소드들의 이해

 

java.util.Collections 에 선언된 변수 혹은 메소드를 보면 도무지 이해하기 힘든 부분들이 많다. 지금까지 1편부터 4편까지 제네릭에 대해서 공부를 해왔지만, 막상 java.util.Collections 에 선언된 메소드들을 보면 답답하다. 이번 포스팅을 통해서 어렵고 난해한 메소드에 대해 친숙해지는 기회가 되었으면 한다.

 

아래 java.util.Collections에 선언된 sort 메소드를 보자.

위 메소드를 보면 지금까지 공부했던 내용이 다 들어가 있다. 1편부터 4편까지의 내용을 조합해서 위 메소드를 해석하면 대충 어떠한 메소드인지 알 수 있을 것 같기도 하다. 하지만 제네릭 메소드 각각의 위치에 왜 하한 제한이 들어가고, 상한 제한이 들어가는지 명확하게 설명할 수 있는 사람은 많지 않을 것 같다. 적어도 나는 위 메소드를 본 순간 머리가 하얗게 되었다. 다시 한번 정리한다는 생각으로 위 메소드를 분석해보자. 일단은 익숙해지기 전까지 제네릭 메소드를 쪼개보자. 이렇게 쪼개서 메소드를 이해하는 것도 능력이라고 생각한다.

 

조금은 축약된 sort 메소드

static 키워드 바로 옆에 선언된 <T extends Comparable<T>> 는 sort 메소드가 제네릭 메소드임을 명시해 준다. 그래서 사실상 extends Comparable<T> 선언문을 지우면 우리가 지겹도록 봤던 익숙한 메소드가 나온다. public static <T> void sort(List<T> list) 이렇게 말이다. 제네릭 메소드는 호출하는 시점에 T가 어떤 타입인지 결정된다. List<Integer> 타입의 인스턴스를 생성해서 sort 매개변수에 넣어서 호출하면 T는 Integer 타입으로 결정되는 것이다. 하지만 제네릭 메소드에 상한 제한이 걸려있다. 즉, sort 메소드 매개변수에 List의 모든 파라미터화 타입이 올 수 있지만, 반드시 Comparable 클래스를 구현한 인스턴스이어야 한다. 여기까지 이해하는데 큰 무리가 없을 것이다.

 

그럼 이제 Comparable<? super T> 를 보도록 해보자. 왜 Comparable<T>에서 Comparable<? super T> 로 변경된 것일까? 자바의 상속과 관련해서 잘 생각해보면 왜 Comparable<? super T> 이렇게 선언할 수 밖에 없었는지 결론을 내릴 수 있다.  아래 예시를 보자.

Comparable<T> 타입의 클래스를 상속받아서 compareTo 메소드를 구현한 Computer 클래스가 있다. 그리고 List<Computer> 인스턴스를 생성해서 power가 1, 2인 Computer 인스턴스를 리스트에 넣어주고 우리가 이해하고자 하는 java.util.Collections 클래스의 sort 메소드를 호출한다. 자 그럼 여기서 조금 나가보자. 만약 Computer 클래스를 확장해야하는 니즈가 발생했다고 하자. 그래서 Computer 클래스를 상속받는 EComputer 라는 클래스를 아래와 같이 생성했다.

그럼  EComputer 는 분명 Computer 클래스를 상속받고 있기 때문에 Comparable 클래스를 간접 상속하고 있다고 말할 수 있다. 그리고 똑같이 List<EComputer> 인스턴스를 생성해서 power가 1, 2인 EComputer 인스턴스를 리스트에 넣어주고 java.util.Collections 클래스의 sort 메소드를 호출한다. 당연한 것이고 언어적인 측면에서도 허용되어야 한다. 상식적으로도 그러하다. 왜냐하면 EComputer 클래스는 Computer 클래스를 상속했으며 Comparable 클래스를 간접 상속하였기 때문이다. 하지만 위 코드에 주석을 달아 놓은 것 처럼 sort 메소드가 아래와 같이 선언되어 있다면 컴파일 에러가 발생한다.

컴파일 에러가 발생하는 이유는 Comparable<? super T> 라는 문법적 장치가 없기 때문이다. 아마 감이 바로 오는 사람도 있을테지만, 나와 같이 바로 이해가 안가는 사람들을 위해서 다시 생각해보자. 우리는 제네릭 메소드가 호출 시에 타입이 결정된다는 것을 알기 때문에 EComputer 타입으로 sort 메소드를 호출하면 아래와 같이 메소드가 변환될 것이다.

 

public static <EComputer extends Comparable<EComputer> void sort(List<EComputer> list)

 

위 변환된 메소드를 해석해보면 sort 매개변수에 올 수 있는 타입은 분명 EComputer 타입이 올 수 있되 단, Comparable<EComputer> 타입을 구현하는 인스턴스가 와야한다. 그런데 EComputer 클래스는 Comparable<EComputer> 클래스를 구현하고 있을까?(핵심이다. 잘 생각해보고 다음 문장으로 가자.)

 

정답은 아니다. EComputer 클래스는 Comparable<Computer> 클래스를 구현하고 있다. 이전 제네릭편에도 상속에 관해 말했지만, 분명 Comparable<Computer> 클래스와 Comparable<EComputer> 클래스는 다른 타입니다.(List<Object>와 List<Integer> 는 다른 타입인 것 처럼!!) 그래서 java.util.Collections 클래스는 이러한 언어적 차원에서 허용이 되어야하는 것을 고려해서 Comparable<? super T> 라는 문법적 요소를 추가해 주었다.

 

완전한 sort 메소드

자 그러면 다시 위 메소드를 보도록 하자. 처음 봤을 때 보다 위 메소드가 왜 저렇게 구현이 될 수 밖에 없는지 이해가 될 것이다. 그리고 나머지 메소드들도 사실 위 내용을 100% 이해가 되었다면 충분히 이해할 수 있는 부분들이 있다. 사실 이렇게 1편에서 5편까지 장편으로 제네릭을 포스팅하게된 계기는 토비님이 운영하지는 토비의 봄TV이라는 유튜브 채널에서 제네릭을 강의 해주시면서 정말 이해가 가지 않는 부분이 있었다. 그래서 기초로 돌아가 다시 제네릭 공부를 하게 되었다. (https://www.youtube.com/watch?v=PQ58n0hk7DI) 26:00부터 설명해주시는 부분을 들어보면 어느 정도 이해는 되었다. 하지만 "당연하다"라고 말씀하시는 부분이 나에게는 많이 낯설게 다가왔다. 그래서 이게 맞는지는 모르겠지만 지금까지 제네릭 내용을 토대로 내 방식대로 정리하고자 한다.

 

java.util.max 메소드

1. max 메소드는 <T extends Comparable<? super T>> 선언에 의해서 제네릭 메소드이다.

2. max 메소드의 매개변수에 Collection 타입이 올 수 있되 메소드 호출시 결정될 T 타입을 상속한 메소드만 올 수 있다. 다만, max라는 메소드를 모든 타입에 대해서 Overloading 을 대신하여 제네릭 타입과 와일드 카드를 혼합하여 상한 제한 하였다.

3. 그리고 호출시 결정될 T타입은 Comparable<T> 타입을 구현해야하며, Comparable<? super T>로 선언한 이유는 상속과 관련된 이유때문이다.

 

이제 이펙티브 자바 "[ITEM31] 한정적 와일드카드를 이용해 API 유연성을 높여라"의 내용만 남은 것 같다. 아마 지금까지의 내용을 이해했다면 무리없이 읽힐 것이다. 다만 생산자, 소비자의 개념이 그 발목을 잡을 수 있지만 extend와 super가 갖는 문법적 제한 장치를 생각해보면 문제는 없다. 그럼 다음 포스팅은 이펙티브 자바의 ITEM 31이 될 것 같다.

'Learn business > Java' 카테고리의 다른 글

Enum과 Generic 함께 사용하기  (0) 2020.05.17
자바 클래스패스  (1) 2020.03.31
[4편] 제네릭이란?  (1) 2020.01.05
[3편] 제네릭이란?  (1) 2019.11.02
[2편] 제네릭이란?  (0) 2019.11.02