개발 공부 기록하기/01. JAVA & Kotlin

코틀린 제네릭 - 배열과 리스트, 제네릭과 무공변

lannstark 2023. 8. 30. 15:52

지난 시간의 문제를 정리해 보면, 우리는 금붕어 Cage에서 금붕어를 꺼내 물고기 Cage에 옮길 수 없었다. 하지만 사실은, 그냥 금붕어를 물고기 Cage에 넣는 것은 아무 문제가 없다.

val cage = Cage2<Fish>()
cage.put(GoldFish("금붕어"))

물고기 Cage에 금붕어가 들어가는 것은 문제가 아니라는 의미이다. 그렇다면 도대체 왜 이런 차이가 발생하는 것일까??! 근본적인 이유를 알기 위해 우리는 상속관계의 의미를 살펴보아야 한다.

여기 상속 관계인 두 클래스가 있다. NumberInt라고 해보자. NumberInt의 상위 타입이고 IntNumber의 하위 타입이다. 이때 다음과 같은 코드는 정상 동작할 수 있다.

fun doSomething(num: Number) {
  // ...
}

val a: Int = 3
doSomething(a) // Number 타입에 Int 타입이 들어갔다!

Number를 파라미터로 받는 함수에 Int가 들어간 것이다! 즉, 두 클래스가 상속관계에 있을 때 하위 클래스는 상위 클래스 대신 함수 파라미터에 들어갈 수 있다.

또 다른 예시도 있다.

val intNum: Int = 5
val num: Number = intNum // Number 타입 변수에 Int 타입 변수가 들어갔다!

이번에도 마찬가지로 Number 타입 변수에 Int 타입 변수가 들어갔는데, Number가 상위 타입이고 Int가 하위 타입이기에 가능하다. 즉 상속관계는 다음과 같은 의미를 갖는다.

  • 상위 타입이 들어가는 자리에 하위 타입이 대신 위치할 수 있다.

이제 다시 Cage2 코드를 찬찬히 뜯어보자.

fun main() {
  val goldFishCage = Cage2<GoldFish>()
  goldFishCage.put(GoldFish("금붕어"))

  val cage = Cage2<Fish>()
  cage.moveFrom(goldFishCage) // Error: Type Mismatch
}

cage의 타입 파라미터는 Fish 이므로 cage.moveFrom(otherCage: Cage2<Fish>) 가 된다.

그리고 이 함수에 넣으려는 goldFishCageCage2<GoldFish> 타입을 가지고 있고, Type Mismatch 에러가 발생하고 있다.

따라서 우리는 FishGoldFish 는 상속관계이지만, Cage2<Fish>Cage2<GoldFish> 는 아무 관계가 아니라는 것을 추측할 수 있다.

Cage2<Fish>Cage2<GoldFish> 가 아무 관계가 아니어서, Type Mismatch가 발생한 것이다.

이를 어려운 말로 가리켜 “Cage2는 무공변 (in-variant, 불공변)하다”고 한다.

그렇다면 왜 FishGoldFish간의 상속관계가 제네릭 클래스에 유지되지 않는 것일까? 즉, 왜 제네릭 클래스는 타입 파라미터 간의 상속관계가 있더라도, 무공변할까? 그 근본적인 이유를 알기 위해서는 잠시 Cage를 잊고, Java 코드를 살펴보아야 한다. Java의 배열과 리스트를 비교해 보자. 먼저 Java의 배열이다.

Java의 배열은 제네릭과 다르다. Java의 배열에서는 A 객체가 B 객체의 하위 타입일 때, A 배열이 B 배열의 하위 타입으로 간주된다. 즉, StringObject 의 하위 타입이므로 String[]Object[] 의 하위 타입으로 여겨진다. 이를 어려운 말로 “배열은 공변 (co-variant) 하다”라고 한다.

배열은 공변하기 때문에 아래와 같은 코드가 가능하다.

public static void main(String[] args) throws IOException {
  String[] strs = new String[]{"A", "B", "C"};
  // Object가 String의 상위 타입이므로 Object[]가 String[]의 상위 타입으로 간주된다.
  // 따라서 objs에 strs를 대입할 수 있다.
  Object[] objs = strs; 
  objs[0] = 1; // java.lang.ArrayStoreException: java.lang.Integer
}

Object[] 는 사실 String[] 인데 숫자를 넣을 수 있는 것이다! 이러한 동작은 런타임 때 예외를 낼 수 있기 때문에 굉장히 위험하다!

반면 List는 다르다. List는 제네릭을 사용하고 있고, 불공변 하기 때문에 컴파일 때 오류를 바로 확인할 수 있다.

public static void main(String[] args) throws IOException {
  List<String> strs = List.of("A", "B", "C");
  List<Object> objs = strs; // Type Mismatch
}

만약 제네릭도 배열과 같은 원리로 동작했다면, List 역시 배열과 같은 결함을 가지게 되고 타입-안전하지 않은 코딩을 해야 했을 것이다. 제네릭은 이런 결함 자체를 막기 위해 무공변하도록 만들어졌다. 또한 이러한 차이가 바로, 배열보다 리스트를 사용하라는 격언의 배경이기도 하다. (Effective Java #3 Item28)

매우 좋다~! 👍 우리는 왜 금붕어 Cage에서 물고기 Cage로 금붕어를 옮기지 못하는지 이해했다.

하지만 여전히 문제가 해결된 것은 아니다. 분명 물고기 Cage에 금붕어를 넣을 수 있는데도 금붕어 Cage에서 물고기 Cage로 금붕어를 옮기지 못하는 것은 이상하다.

어떻게 하면 이 문제를 해결할 수 있을까?! 바로 moveFrom 함수를 호출할 때 FishGoldFish 의 상속관계를 Cage2<Fish>Cage2<GoldFish> 에도 이어주는 것이다!

이를 어려운 말로, “공변(co-variant)하도록 만든다” 라고 한다.

 

공변에 대해 더 궁금하시다면 아래 링크를 확인해보세요~!! 😊

내용 출처 : https://inf.run/F9gX

 

코틀린 고급편 - 인프런 | 강의

코틀린의 모든 언어적 특성을 이해할 수 있습니다. 강의를 들으신 후 제네릭, 위임과 지연, DSL과 리플렉션 등 코틀린 고급 기술을 활용해 마음껏 프로그래밍하실 수 있습니다., 남들보다 깊은 코

www.inflearn.com