제네릭과 불공변

cutiepazzipozzi·2023년 6월 26일
3

지식스택

목록 보기
34/35

제네릭, 뭣도 모를 올 초에 공부하다가 크게 데인 친구다. 다시 개념을 복습하며, 불공변이라는 개념은 꼭 머리에 넣고자 다시 포스팅을 끄적인다. 제네릭에 대한 기본적인 얘기는 내꺼♥ 에 적어두었으니 한번 읽고 오시라!

일단, 공변이 뭐람?

그전에 변성(variance)이라는 단어의 의미부터 알아보자. 이는 서로 다른 타입 간의 관계성을 나타내는 개념이다. 이때 공변성의 의미는, A와 B가 상하 관계에 있을 때, 기저 타입이 같고 각각 타입 인자를 A와 B로 가질 때 두 객체의 관계도 상하일 때를 일컫는다.
(ex. 기저 타입과 타입인자가 용어가 어려워서 이해가 안될 수 있다. 바로 예시를 들면, List<Object>에서 List가 기저 타입, Object가 타입 인자이다)

이런 공변에서 파생된 두 가지 개념이 있다. 바로 불공변반공변이다.

  1. 불공변 = A와 B가 어떤 관계를 갖던지, A와 B를 각각 타입 인자로 갖는 같은 기저 타입 두 객체 간에는 아무런 관계가 없는 것
    ex. List<Obect>List<String>은 각각 다른 타입의 객체로 취급

  2. 반공변 = A와 B가 상하 관계를 가질 때, A와 B를 각각의 타입 인자로 갖는 같은 기저 타입 두 객체 간의 상하관계가 뒤바뀐 것
    ex. Kotlin < JVM < Language라는 상속 관계가 기저 타입을 만나면 Box<Language> < Box<JVM> ~ 으로 바뀜

여기서 자바의 제네릭은 불공변에 속하는데, 하나로 간단한 예시를 들어보자. List<Object> list = new ArrayList<String>();
이 코드를 실행 시켰을 때 과연 컴파일러는 이를 올바르다고 인식할까?

절대 아니다!!! list라는 이름을 가진 리스트는 List<Object> 형태이므로 관계없는 List<String>을 받아올 순 없다.

제네릭(불공변)의 장점과 단점

명확하게 장단점이 존재한다. 어쩌면 불공변이라는 제네릭의 개념을 이해한다면 그냥 손쉽게 얘기할 수 있을 것이다.

  1. 장점
  • 타입 안전성이 보장된다.

제네릭이 등장하기(JDK 1.5) 이전, 자바는 컬렉션을 다루는 요소들의 타입이 보장되지 않아 컴파일 시점에 나지 않던 타입 에러가 런타임 시점에 발생해 애를 먹곤 했다.

예를 들면, String을 타입으로 갖는 컬렉션에서 이를 Integer.parseInt()를 활용해 정수로 바꾸어 더해주는 메서드가 있다고 하자. 이때 인자로 받아오는 Collection 객체가 String이 아닐 수도 있어 메서드가 제대로 작동하지 못하게 된다.

이를 위해 타입의 매개변수화(<>)를 통해 컴파일 시점에 타입의 안전성을 보장받는 제네릭을 탄생시킨 것이다.

//아래는 제네릭의 특성(불공변)을 잘 보여주는 예시이다.
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; //불공변하므로 List<Animal> != List<Dog>, 컴파일 에러!!
animals.add(new Cat());  //그러나 Animal이 Cat의 상위 타입이므로 list에 넣어지긴 함
Dog dog = dogs.get(0); //만약 공변이라면 ClassCastException 발생
  1. 단점
  • (한정적 와일드카드 제외) 다형성을 갖지 못한다.
    (++여담이지만 이 다형성을 위해 배열은 공변이 허용된다)

불공변을 설명하며 가볍게 든 예시로도 이 단점이 이해가 갈 것이다. 자꾸 "나는 나에요" 라고 설명하는 것 같은데(ㅠㅠ), 이것이 바로 불공변 하기에 갖는 단점이다. 다양한 형태를 갖는 성질, 즉 다형성은 객체지향 프로그래밍이 갖는 특징인데 Java가 불가능하다고????? 생각보다 심각한 문제다. 이는 객체지향 5원칙 中 Liskov 치환 원칙(=서브 타입은 언제나 기반 타입으로 교체할 수 있음)에도 위배되기 때문이다.

불공변을 탈피?

이를 위해(불공변 탈피! 다형성 챙겨보자9) 등장시켜본 개념이 ?와일드카드이다. 말그대로 ? 이기에 모든 타입을 호출할 수 있다. 다만, ? 의 의미가 모든 타입이 가능하다 라기 보다는 정해지지 않은 타입 이라고 해석하는 것이 적합하겠다.

정해지지 않은 타입이므로 그래서 문제가 생긴다. 어떤 타입인지 알 수 없어, 추가하는 요소에 대한 자식 여부를 검사할 수 없다.

List<?> wild = new ArrayList<Integer>();
wild.add(1); // 컴파일 에러!!

요로코롬 선언을 할 수 있다만, wild에 들어가는 요소는 리스트 인자 타입과 그 하위 타입의 자식인데 정해지지 않았기 때문에 알 수가 없다.

똑똑한 자바 개발자들은 그래서 여기서 멈추지 않았다. 어느정도 타입을 한정시키면 좋겠다는 생각이 들었던 것이다. 그래서 한정적 와일드카드라는 개념을 도입한다. 여기서 사용되는 키워드가 우리가 흔히 듣는 extends, super 키워드이다.

//동물 클래스들을 활용한 예시
class Animal {};
class Dog extends Animal {};
class Cat extends Animal {};

List<? extends Animal> ani1 = new ArrayList<Dog>();
List<? extends Animal> ani2 = new ArrayList<Cat>();

//기본적인 예시
List<? extends Object> list2 = new ArrayList<String>(); 
List<? super String> list3 = new ArrayList<Object>();
  • extends = 상한 경계
    extends 뒤에 오는 클래스를 상한 limit으로 잡아 그 클래스 혹은 자식 클래스들을 모두 타입으로 가질 수 있게끔 한다. 다만, 하위 타입 중 어떤 타입이 등장할 지 모르기 때문에 받은 데이터는 읽기만 가능하다. 그렇기 때문에 잘못된 형변환이 발생하지 않기도 하다.

  • super = 하한 경계
    super 뒤에 오는 클래스를 하한 limit으로 잡아 그 클래스 혹은 부모 클래스들을 모두 타입으로 가질 수 있게끔 한다. 따라서 컬렉션에 이 클래스의 자식 타입들은 추가가 가능하므로 데이터의 쓰기도 가능해진다!

++ 공식문서

참조

https://sungjk.github.io/2021/02/20/variance.html
https://mangkyu.tistory.com/241
https://stackoverflow.com/questions/2745265/is-listdog-a-subclass-of-listanimal-why-are-java-generics-not-implicitly-po/2745301#2745301

profile
노션에서 자라는 중 (●'◡'●)

2개의 댓글

comment-user-thumbnail
2023년 6월 27일

잘 보고 갑니다!! 잘 모르고 있던 제네릭에 대해 알기 쉽게 설명을 잘 써주셨네요👍👍

1개의 답글