ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Generics와 wildcard
    java 2021. 6. 1. 10:42

    배경

    Java 5부터 나온 Generics는 매우 빈번하게 사용하는 문법이다. Spring코드를 좀만 들여다봐도 Generics를 사용하지 않은 클래스를 찾는 게 더 어려울 정도다. 기초적인 부분은 생략하고 복잡한 문법이나 주의사항에 대해 짚고 넘어가보자.

     

    토비님의 유튜브를 보며 정리했다.

    https://www.youtube.com/watch?v=ipT2XG1SHtQ&list=PLv-xDnFD-nnmof-yoZQN8Fs2kVljIuFyC&index=12 

    https://docs.oracle.com/javase/tutorial/extra/generics/morefun.html

    Generics를 사용하는 이유

    컬렉션에 데이터를 보관할 때 클래스 단위로 작업을 하는 경우가 많다. 위 예시의 경우 머릿속에서 String형을 담는 컬렉션 list를 생각했다고 하자. 그러던 중 의도치 않게 1이라는 정수를 넣었을 때 컴파일러는 이 상황에 대해 아무런 의심을 하지 않는다. 실행할 때도 문제가 없다. 다만, for문에서 보이듯, 프로그래머의 의도는 String을 담는 list이고 item을 꺼내 사용할 때 컴파일러는 해당 객체가 String형인지 모르므로 명시적으로 타입캐스팅을 해야한다. 또한 정수가 들어간 경우 런타임에 타입캐스팅에러가 발생할 것이다.

     

    제네릭스를 이용해 컬렉션의 타입을 제한하면 리스트에 1을 넣는 행위는 컴파일러가 잘못됨을 체크해준다. 또한 이런 체크는 런타임 시 발생할 오류를 근본적으로 해소해주며, 불필요한 타입캐스팅을 하지 않아도 된다.

     

    클래스와 메서드 제네릭스

    먼저 주의해야할 사항이 하나 있다. 클래스 수준에서 정의된 Generics는 멤버변수와 메서드에서 유효하다. 단, 메서드에서 접근자 뒤에 Generics가 재정의되어 있다면 클래스의 Generics는 무시된다. 예제에서는 instancePrint1에서 첫 파라미터가 그 예에 해당한다.

     

    static 메서드에서 클래스의 Generics를 사용하는 staticPrint2 경우 컴파일 에러가 난 걸 확인할 수 있는데 잘 생각해보면 당연한 결과다. 클래스에서 Generics의 타입이 결정되는 건 인스턴스 생성 시점이니 static메서드처럼 인스턴스가 없어 타입이 결정되지 못한 경우에는 Generics를 정할 방법이 없는 것이다.

     

    제네릭스의 상속관계

    Number클래스는 Integer클래스의 상위타입이다. 즉, Number타입 파라미터에 Integer를 넘길 수 있다. 반면, Generics로 들어간 Number는 Integer의 상위 타입이 아니다. 그러므로 위 예시에서 컴파일 에러가 나온다.

     

    제네릭스와 와일드카드

    제네릭스타입이나 와일드카드타입 모두 범용타입을 받기 위해 정의한다. 특정 타입이 정해진 리스트를 파라미터로 받는 게 아닌 범용타입을 파라미터로 받는다. 위 두 메서드 모두 기능적으로 완전히 동일한 역할을 하고 문제도 없다. 다만, 그 의도에 있어서 제네릭스는 타입을 인스턴스에 지정한 타입을 이용해 어떤 기능을 사용한다. 반면, 와일드카드는 타입과 전혀 관계없는 행동을 할 때 사용한다. 위 예시 같은 경우 타입에 관계없이 단지 List의 size()메서드만 수행하니 와일드카드 타입이 적절한 경우라고 보면 된다.(둘 다 가능하면 와일드카드가 적절한 것이다)

     

    와일드카드 super extends

    Collections의 copy메서드를 보면 dest는 List<? super T>로, src는 List<? extends T>로 파라미터가 정의된 걸 볼 수 있다. 대부분은 명확한 차이가 없지만 dest.set(i, src.get(i))를 보면 dest는 set()을 src는 get()을 사용하는 부분에서 차이가 있다. Java의 지침을 보면 input으로써 내부적으로 사용될 요소는 extends를 output으로써 반환될 요소는 super를 사용하도록 되어 있다. 일단 오픈소스를 읽는 데 이정도면 이해가 충분할 거고 나중에 설계단계까지 간다면 그때 고민을 깊게 해봐야 겠다.

     

    와일드카드 캡처

    reverse()내부적으로 get()함수를 호출하지만 결국 본인의 원소를 뒤집는 작업을 하기 때문에 논리적으로 List의 타입은 알 필요가 없다. 그래서 제네릭스코드를 사용하기보단 와일드카드를 사용했고 이때 컴파일에러가 발생한다. 이런 오류를 캡처오류라하고 제네릭스코드를 사용하면 문제는 해결된다. 다만, API설계자 입장에서 외부로 공개된 API의 스펙은 최대한 숨기는 게 맞으니 와일드카드를 사용하고 싶은데 컴파일오류를 피할 방법을 생각할 수 있다.

     

    공개 스팩으로는 와일드카드를 사용하고 내부적으로 다시 제네릭스를 이용하는 방법이다. 아니면 Raw Type 컬렉션을 사용하는 방법도 있다. 굳이 이렇게까지? 라는 생각이 강하게 들지만 어쨌든 만드는 사람입장에서 설계한 부분은 이해가 가고 그 코드를 들여다 볼 일이 많을테니 잘 알고 있는 건 필수인 듯하다.

     

    'java' 카테고리의 다른 글

    [java] 슈퍼타입토큰(super type token)  (0) 2022.01.01
    Future 클래스 정리  (0) 2021.06.03
    Stream 정리 및 예제  (0) 2021.05.29
    Optional 정리  (0) 2021.05.28
    Inner class와 static 조합  (0) 2021.05.27

    댓글

Designed by Tistory.