Kotlin. Generics

Tnalxmsk·2024년 1월 2일

Kotlin

목록 보기
5/8

제네릭

제네릭은 다양한 종류의 데이터 타입에 대해 작동할 수 있는 코드나 컴포넌트를 나타낼 때 사용한다. 이는 특정한 데이터 타입에 의존하지 않고, 다양한 데이터 타입에 대해 재사용 가능한 코드를 작성할 수 있도록 도와준다.

제네릭을 사용하면 코드의 재사용성이 증가하며, 타입 안정성이 향상된다.

abstract class Animal
class Dog(private val name: String) : Animal()

class Cat(private val name: String) : Animal()

class Cage<T> {
    private val animals = mutableListOf<T>()

    fun put(animal: T) {
        animals.add(animal)
    }

    fun getFirst(): T {
        return animals.first()
    }

    fun moveFrom(otherCage: Cage<T>) {
        this.animals.addAll(otherCage.animals)
    }
}

제네릭 타입을 활용하면 다양한 타입의 Cage 클래스를 생성할 수 있다.

공변, 무공변, 반공변

변성

‘타입의 계층 관계에서 서로 다른 타입 간에 어떤 관계가 있는지’를 나타내는 개념

제네릭 사용 시 Base Type이 같고 Type argument가 다른 경우, 서로 어떤 관계가 있는지 나타내는 것

예를 들어, List< String > 에서 List는 Base Type, Srting은 Type argument라고 할 수 있다.

타입 S가 타입 T의 하위 타입일 때, Cage<S>가 Cage<T>의 하위 타입인가?

이것은 객체지향의 프로그래밍 원칙 중 하나인 리스코프 치환 원칙에 해당한다. 이 원칙은 상위 타입이 사용되는 곳에는 언제나 하위 타입의 인스턴스를 넣어도 이상 없이 동작해야 함을 의미한다.

무공변

abstract class Animal

class Dog(private val name: String) : Animal()

class Cat(private val name: String) : Animal()

Dog와 Cat은 Animal의 하위 타입이며 다음과 같은 상속관계이다

그렇다면

 val cage1 = Cage<Animal>()
    
    cage1.put(Cat("cat"))
    cage1.put(Dog("dog"))

cage1은 Animal의 하위 타입인 Cat과 Dog 타입을 put 가능하다. 하지만

fun moveFrom(otherCage: Cage<T>) {
        this.animals.addAll(otherCage.animals)
    }

Cage< Aniaml > 타입의 cage는 Cage< Cat > 타입의 catCage를 인자로 작성하면 ‘Type mismatch’ 에러가 발생한다. 위 코드는 Cage< Animal >에 Cage< Cat >을 넣으려 하고 있다.

Cat은 Animal 타입을 상위 타입으로 하는 상속관계로 정의하였다. 하지만 Cage< Cat >과 Cage< Animal > 이 상속관계라고 할 수 있는가? 이 둘은 아무런 관계가 아니다. 컴파일러는 이 둘을 완전히 다른 타입으로 인식하고 있다.

이것을 cage는 ‘무공변’하다(in-variant, 불공변) 라고 한다.

그렇다면 어떻게 이 코드를 동작하도록 수정할 수 있을까?, 어떻게 Cage< Cat >을 Cage< Animal >의 하위 타입으로 정의할 수 있을까?

공변

위에 나타났던 문제에 대한 해결은 간단하다. 제네릭 타입 T 앞에 out 키워드만 작성해주면 된다.

fun moveFrom(otherCage: Cage<out T>) {
        this.animals.addAll(otherCage.animals)
    }

실제로 ‘Type mismatch’ 에러가 사라진 것을 확인할 수 있다. 즉, 컴파일러가 하위 타입으로 인식한 것이다.

이러한 방법을 ‘공변을 주다’, ‘변성을 주다’라고 하며, moveFrom() 함수 호출 시 Cage는 공변하게 된다.

이러한 out 키워드를 ‘variance annotation’이라고 부른다.

이때 out 키워드가 붙은 파라미터 otherCage는 메서드 구현 시 데이터를 가져올 수 만 있고 넣는 행위는 불가능하다.

otherCage는 ‘생산자’ 역할만 할 수 있다.

만약 다음과 같은 코드를 에러가 발생하지 않고 허용하게 된다면?

otherCage가 Cage< Cat >이고 this.getFirst()가 Fish 타입을 반환하는 경우 하위 타입에 상위 타입을 넣는다. 이렇게 되면 타입 안정성이 깨진다. 그래서 out은 생산자 역할만 가능하다.

반공변

나의 Cage가 아닌 다른 Cage로 옮기는 moveTo() 메서드가 있다고 생각해보자.

fun moveTo(otherCage: Cage<T>) {
        otherCage.animals.addAll(this.animals)
    }

Cage< Cat > 타입의 내용물을 Cage< Animal > 타입의 Cage로 옮겨보자

컴파일 시 Type mismatch 에러가 발생하는 것을 알 수 있다.

위 코드를 동작하도록 만들기 위해선 Cage< Animal >이 Cage< Cat > 의 하위 타입이어야 하며, ‘반공변’하게 만들어 주어야 한다. 위 문제의 해결도 아주 간단하다. in 키워드만 작성해주면 된다.

fun moveTo(otherCage: Cage<in T>) {
        otherCage.animals.addAll(this.animals)
    }

에러가 사라진 것을 알 수 있다.

이 때 in 키워드가 붙은 otherCage는 데이터를 받는 ‘소비자’ 역할만 할 수 있다.

정리

위에서 알아보았던 것들에 대해 정리하면

  • out: 생산자, 공변
  • in: 소비자, 반공변
  • 공변: 타입 파라미터의 상속 관계가 제네릭 클래스에서도 유지된다.
  • 반공변: 타입 파라미터의 상속 관계가 제네릭 클래스에서 반대로 된다.

참고 자료

0개의 댓글