티스토리 뷰

728x90

1강 제네릭과 타입 파라미터

Cage 클래스

  • 동물을 넣거나 꺼낼 수 있음
  • methods
    • getFirst() - 첫번째 동물을 가져옴
    • put(animal) - 동물을 넣는다.
    • moveFrom(cage) - 다른 cage에 있는 동물을 모두 가져온다.

Code

class Cage {
    private val animals: MutableList<Animal> = mutableListOf()

    fun getFirst(): Animal {
        return this.animals.first()
    }

    fun put(animal: Animal) {
        this.animals.add(animal)
    }

    fun moveFrom(cage: Cage) {
        this.animals.addAll(cage.animals)
    }

Animal 클래스

  • 동물을 형상화한 클래스

Code

abstract class Animal (
    val name: String
)

abstract class Fish(name: String) : Animal(name)

// 금붕어
class GoldFish(name: String) : Fish(name)
// 잉어
class Carp(name: String) : Fish(name

UML

@startuml
abstract class Animal {
    - name: String
}

abstract class Fish extends Animal

class GoldFish extends Fish
class Carp extends Fish

@enduml

Carp(잉어)를 Cage에 넣고 뺴기

fun main() {
    val cage = Cage()
    cage.put(Carp("잉어"))
    // 에러 발생!!
    // getFirst()는 Animal을 return
    val carp: Carp = cage.getFirst()
}
  • 위 에러를 해결하는 가장 간단한 방법은 as를 사용해 type casting을 하는것
fun main() {
    val cage = Cage()
    cage.put(Carp("잉어"))
    val carp: Carp = cage.getFirst() as Carp
}
  • BUT!! 위와같이 하는것은 위험함! Cage 내부에는 Carp만 들어있다는 보장이 없음
  • 아래와 같은 에러는 런타임이 되어야 찾을 수 있음
fun main() {
    val cage = Cage()
    cage.put(GoldFish("금붕어")) // gold fish
    val carp: Carp = cage.getFirst() as Carp // type cast error 발생!
}
  • Safe Type Castring과 Elvis Operator
  • 아래와 같은 방법을 사용할 수 있으니 IllegalArgumentException가 발생함
fun main() {
    val cage = Cage()
    cage.put(GoldFish("금붕어"))
    val carp: Carp = cage.getFirst() as? Carp ?: throw IllegalArgumentException()
}
  • 동일한 Cage 클래스이지만 잉어만 넣을 수 있는 Cage, 금붕어만 넣을 수 있는 Cage를 구분하면 어떨까?!

제너릭(Generic)

  • 클래스 뒤에 타입 파라미터를 추가할때는 클래스 뒤에 <타입>을 적어주면 됨

Generic을 적용한 Cage 클래스

class CageWithTypeParam<T> {
    private val animals: MutableList<T> = mutableListOf()

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

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

    fun moveFrom(cage: CageWithTypeParam<T>) {
        this.animals.addAll(cage.animals)
    }
}
fun main() {
    val cage = CageWithTypeParam<Carp>()
    cage.put(Carp("잉어"))
    // cage.put(GoldFish("금붕어")) // 금붕어는 넣을 수 없음
    val carp: Carp = cage.getFirst() // 타입 캐스팅이 필요 없음
}

금붕어 Cage에 금붕어를 한 마리 넣고, 물고기 Cage에 금붕어를 옮기자! (moveForm 메소드 사용)

  • 금붕어는 물고기의 하위 타입이기 때문에 fishCage로 넣을수 있을것 같으나 에러가 발생함
fun main() {
    val goldFishCage = CageWithTypeParam<GoldFish>()
    goldFishCage.put(GoldFish("금붕어"))

    val fishCage = CageWithTypeParam<Fish>()
    fishCage.moveFrom(goldFishCage) // type mismatch error!!
}

2강 배열과 리스트, 제네릭과 무공변

상위 타입과 하위 타입의 의미

@startuml
class Number
class Int

Number <|-- Int
@enduml
  1. 함수 매개 변수에 대신 들어갈 수 있음
    fun doSomething(num: Number) {
     ...
    }
    

val a: Int = 3
doSomething(a) // Int는 Number의 하위타입으로 가능!


2. 변수에 대신 들어갈 수 있음
```kotlin
val intNum: Int = 5
// 상위 타입 num에 하위 타입 intNum이 들어갔음
va num: Number = intNum

상속관계란 상위 타입이 들어가는 자리에 하위타입이 대신 위치 할 수 있음

이전코드 다시보기

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

    val fishCage = CageWithTypeParam<Fish>()
    fishCage.moveFrom(goldFishCage) // type mismatch error!!
}
  • CageWithTypeParam<Fish>CageWithTypeParam<GoldFish>를 넣으려 하고 있음

CageWithTypeParam<Fish>CageWithTypeParam<GoldFish> 관계

@startuml
agent "CageWithTypeParam<Goldfish>"
agent "CageWithTypeParam<Fish>"
@enduml
  • CageWithTypeParam<Fish>CageWithTypeParam<GoldFish>는 아무 관계도 아님!
  • Cage2는 무공변(불공변) 하다!

무공변

  • 왜 Fish와 Goldfish간의 상속 관계가 제네릭 클래스에서 유지되지 않을까?

JAVA의 배열과 리스트 비교

배열

  • JAVA의 배열은 A객체가 B객체의 하위타입이면 A배열이 B배열의 하위타입으로 간주됨
  • JAVA의 배열은 공변함!
    @startuml
    agent "Object"
    agent "String"
    agent "Object[]"
    agent "String[]"
    

Object <-- String
"Object[]" <-- "String[]"
@enduml


- 자바의 배열은 **공변**하므로 아래와같은 코드가 가능함
```java
String[] str = new String[]{"a", "b", "c"};
Object[] objects = strs;

objects[0] = 1 // 여기서 문제 발생!! java.lang.ArrayStoreException: java.leng.Integer
  • objects는 사실 String[]이기때문에 int를 넣을 수 없음, 때문에 런타임 에러가 발생!
    • 타입-안전 하지 않은 코드가 발생함

List With Generic

  • List는 무공변하므로 아래와 같은 코드는 원천적으로 불가능함!
  • 배열보다 Type-Safe하게 코드를 작성 할 수 있음
    List<String> strs = List.of("a", "b", "b");
    List<Object> objs = strs; // Type Mismatch!

다시 코틀린 코드로 돌아와 생각해보기

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

    val fishCage = CageWithTypeParam<Fish>()
    fishCage.moveFrom(goldFishCage) // type mismatch error!!
}
  • 위 코드를 동작하게는 못할까?
  • CageWithTypeParamCageWithTypeParam`의 상속 관계를 유지하게 된다면 위 코드는 동작이 가능!

3강. 공변과 반공변

  • 공변: 클래스의 상속관계가 제너릭 클래스의 상속관계까지 이어지는 것
  • 코틀린에서는 제너릭 앞에 out키워드를 붙이면 공변하게 변함
// moveFrom 타입 파라미터 앞에 `out` 키워드 추가
fun moveFrom(cage: CageWithTypeParam<out T>) {
    this.animals.addAll(cage.animals)
}
fun main() {
    val goldFishCage = CageWithTypeParam<GoldFish>()
    goldFishCage.put(GoldFish("금붕어"))

    val fishCage = CageWithTypeParam<Fish>()
    fishCage.moveFrom(goldFishCage) // 에러 사라짐
}

out을 타입 파라미터에 붙이면

  • out을 붙이게 되면 데이터를 꺼낼수만 있음 (생산자 역할만 가능)
fun moveFrom(otherCage: CageWithTypeParam<out T>) {
    otherCage.getFirst() // 성공
    otherCage.put(this.getFirst()) // 에러발생
    this.animals.addAll(otherCage.animals)
}

out 키워드를 붙이면 왜 꺼낼수만 있을까?

otherCage가 소비자 역할도 할수 있다면??

val goldFishCage = CageWithTypeParam<GoldFish>()
goldFishCage.put(GoldFish("금붕어"))

val fishCage = CageWithTypeParam<Fish>()
fishCage.put(Carp("잉어"))
fishCage.moveFrom(goldFishCage)

fun moveFrom(otherCage: CageWithTypeParam<out T>) {
    otherCage.put(this.getFirst()) // 만약 가능하다고 하면...
}
  • otherCage.put(this.getFirst())가 가능하다면
    • otherCage는 CageWithTypeParam이고 this는 CageWithTypeParam<Fish>이지만 안에는 Carp("잉어")가 들어있음
    • 즉, CageWithTypeParam에 Carp를 넣는 형식이 되어버리고 그러면 타입 안정성이 깨져버림

moveTo 함수

  • moveFrom함수와 반대로 내가 가지고 있는걸 다 넘겨주는 moveTo함수를 만들어보자
fun moveTo(otherCage: CageWithTypeParam<T>) {
    otherCage.animals.addAll(this.animals)
}
  • 금붕어 케이지의 모든 금붕어를 Fish 케이지로 옮겨보자!
val fishCage = CageWithTypeParam<Fish>()

val goldFishCage = CageWithTypeParam<GoldFish>()
goldFishCage.put(GoldFish("금붕어"))
goldFishCage.moveTo(fishCage) // 에러 발생!!
  • moveTo(otherCaget: CageWithTypeParam) 함수에 CageWithTypeParam를 넣고싶다!
  • CageWithTypeParam가 상위, CageWithTypeParam가 하위 타입! (반공변)

in 키워드를 붙여 반공변으로..

  • kotlin에서는 in키워드를 붙여 반공변으로 만들수 있음
  • in이 붙은 otherCage는 데이터를 받을수만 있다!
fun moveTo(otherCage: CageWithTypeParam<in T>) {
    // otherCage는 소비자(데이트를 받는)역할 만 할수 있음
    otherCage.animals.addAll(this.animals)
}

val fishCage = CageWithTypeParam<Fish>()

val goldFishCage = CageWithTypeParam<GoldFish>()
goldFishCage.put(GoldFish("금붕어"))
goldFishCage.moveTo(fishCage) // 정상 동작!!

정리

  • out: (함수 파라미터 입장에서) 생산자, 공변
    • 타입 파라미터의 상속관계가 제네릭 클래스에서 유지됨
  • in: (함수 파라미터 입장에서) 소비자, 반공변
    • 타입 파라미터의 상속관계가 제네릭 클래스에서 반대로 유지됨

4강. 선언 지점 변성 / 사용 지점 변성

  • in, out을 통해 공변, 반공변을 만들어 주었음

out을 이용해 상위타입 변수에 하위타입 변수 넣기

val goldFishCage: CageWithTypeParam<GoldFish> = CageWithTypeParam()
val fishCage: CageWithTypeParam<out Fish> = goldFishCage

제너릭 클랙스 자체를 공변하게 만들수 없나?

  • 아래 코드에서 out키워드를 빼고 쓸수는 없나?
  • 코틀린에서는 가능!
val goldFishCage: CageWithTypeParam<GoldFish> = CageWithTypeParam()
val fishCage: CageWithTypeParam<out Fish> = goldFishCage

CageWithTypeParam을 개조해보자!

생산만 하는 CageWithTypeParam (out)

  • CageWithTypeParam2는 오직 2가지 기능만 가지고있음
    • getFirst(): T / getAll(): List<T>
  • CageWithTypeParam2는 하나 또는 모든 데이터를 반환하는 역할만 함 (생산만 하는 클래스)
    • 타입 파라미터(T)가 반환타입에서만 사용됨
class CageWithTypeParam2<T> {
    private val animals: MutableList<T> = mutableListOf()

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

    fun getAll(): List<T> {
        return this.animals
    }
val fishCage: CageWithTypeParam2<Fish> = CageWithTypeParam2()
val animalCage: CageWithTypeParam2<Animal> = fishCage // 에러 발생 (Type Mismatch)
  • 위의코드는 아직 에러가 발생하지만 논리상으로는 문제가 없는코드 (animalCage는 Animal을 생산만하기 때문!!)
    • fishCage (잉어, 금붕어) -> animalCage (잉어, 금붕어)
      • animalCage의 겉은 CageWithTypeParam2<Animal>이지만 실제는 CageWithTypeParam2<Fish>
      • 잉어나 금붕어를 Animal로 가져오는건 전혀 문제가 없음!!
    • 반대로, animalCage (잉어, 금붕어)가 소비소 하게 된다면?
      • animalCage (잉어, 금붕어).put(참새)
      • animalCage는 겉으로는 CageWithTypeParam2<Animal>이기때문에 참새를 넣는것은 문제없어 보이나 실제로는 CageWithTypeParam2<Fish>이기때문에 RuntimeError 발생!
  • 클래스 자체에 out붙여주면 클래스를 공변하게 만들수 있음
  • T는 생산만 가능하고 소비를 위한 T는 사용하지 못함
  • class CageWithTypeParam2<out T> { private val animals: MutableList<T> = mutableListOf() fun getFirst(): T { return this.animals.first() } fun getAll(): List<T> { return this.animals } }

val fishCage: CageWithTypeParam2 = CageWithTypeParam2()
val animalCage: CageWithTypeParam2 = fishCage // 정상 동작!

// animalCage.put(참새) --> out은 소비가 불가능하기때문에 이러한 동작은 불가함


#### 소비만 하는 CageWithTypeParam (in)
- `CageWithTypeParam3`의 메서드는 모두 소비만 함

```kotlin
class CageWithTypeParam3<in T> {
    private val animals: MutableList<T> = mutableListOf()

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

    fun putAll(animal: List<T>) {
        this.animals.addAll(animal)
    }
}

라이브러리를 통한 예시

in 선언지점 예시

  • Comparable의 경우 파라미터를 소비만 함
public interface Comparable<in T> {
    public operator fun compareTo(other: T): Int
}

out 선언지점 예시

  • 코틀린의 List는 불변이기 때문에 데이터를 생산만 함
public interface List<out E> : Collection<E> {
    ...
}
  • 그런데 contains() 또는 containsAll()의 경우는 타입파라미터 E를 받아야함
  • 타입 안전하다고 보장할수 있는경우 @UnsafeVariance를 사용해서 데이터를 소비할 수 있음
    public interface List<out E> : Collection<E> {
      override fun contains(element: @UnsafeVariance E): Boolean
    }

5강. 제네릭 제약과 제네릭 함수

Generic 제약

  • 우리는 CageWithTypeParam에 Animal만 사용하고 싶다!
    • 타입 파라미터에는 어떠한 클래스라도 들어올 수 있음
  • <T : Animal>을 사용하면 타입 파라미터의 상한(upper bound)을 Animal로 지정 가능
    • T는 Animal의 하위클래스만 들어올 수 있음
    • class CageWithTypeParam4<T : Animal> { private val animals: MutableList<T> = mutableListOf() fun getFirst(): T { return this.animals.first() } fun put(animal: T) { this.animals.add(animal) } fun moveFrom(otherCage: CageWithTypeParam4<T>) { this.animals.addAll(otherCage.animals) } }

val a = CageWithTypeParam4() // 불가
val b = CageWithTypeParam4() // 불가
val c = CageWithTypeParam4() // 가능!
val d = CageWithTypeParam4() // 가능!


### 제한 조건을 여러개로 하고싶다면?
- 예시) T에 Animal만 들어올수 있고 Comaprable을 구현하고 있어야함
```kotlin
// 제약이 여러개 일때는 where을 사용함
class CageWithTypeParam4<T> where T : Animal, T : Comparable<T> {
    private val animals: MutableList<T> = mutableListOf()

    fun printAfterSorting() {
        // Comparable을 구현한 Animal만 받기 떄문에 .sorted()를 바로 사용가능
        this.animals.sorted()
            .map { it.name }
            .let { println(it) }
    }
}

제너릭 제약을 Non-Null 타입 한정에 사용 가능

  • T만 사용할 경우 Nullable한 타입도 넣을 수 있음
    class CageWithTypeParam6<T> {
      private val animals: MutableList<T> = mutableListOf()
    }
    

CageWithTypeParam<GoldFish?>() // 가능


- Non-Null 제약을 걸고 싶다면 `Any`를 설정함
```kotlin
class CageWithTypeParam6<T : Any> {
    private val animals: MutableList<T> = mutableListOf()
}

CageWithTypeParam<GoldFish?>() // 불가능

제네릭 함수를 사용하면 유연한 코딩이 가능

  • 예시) 두 리스트에 겹치는 원소가 하나라도 있는지 확인하는 함수
fun List<String>.hasIntersection(other: List<String>): Boolean {
    return (this.toSet() intersect other.toSet()).isNotEmpty()
}

fun List<Int>.hasIntersection(other: List<Int>): Boolean {
    return (this.toSet() intersect other.toSet()).isNotEmpty()
}

// 위와같이 유연하지 않은 함수를 `T`를 통해 유연하게 만들어 줄수 있음
fun <T> List<T>.hasIntersection(other: List<T>): Boolean {
    return (this.toSet() intersect other.toSet()).isNotEmpty()
}

6강. 타입 소거와 Star Projection

타입 소거

  • 제네릭이란 개념은 JDK초기 버전에는 존재하지 않았음 (java 1.5 부터 나옴)
  • 여기서 문제점은 제네릭이 없는 버전과 있는버전의 호환성을 유지해야함
    • **런타임때는 타입정보 <T>를 제거함 (List<String> -> List)
  • 반면, 코틀린의 경우는 초기부터 제네릭이 고려되어 raw type객체를 만들 수 없음
val number: List = listOf(1, 2, 3) // ERROR! 타입 파라미터를 꼭 명시해야함
  • 하지만 코틀린도 JVM위에서 동작하기에 런타임때는 타입 정보가 사라짐 (타입 소거)

타입 소거를 확인 하는 방법

  • 아래 코드는 런타임때 String 정보가 사라지기에 List인지 알수 없음
    fun checkStringList(data: Any) {
      // Cannot check for instance of erased type: List<String>
      if (data is List<String>) {
          // ....
      }
    }

start project

  • start project을 통해 최소한 List인지는 확인 가능함
  • fun checkStringList(data: Any) { // data가 List인지는 확인 가능 if (data is List<*>) { // .... } }
  • data가 List 타입이니 데이터를 가져올수는 있으나 타입은 알수 없어 Any?로 반환됨
  • 데이터를 넣을때는 어떤 타입인지 알수 없어 함부로 넣을 수 없음
  • fun checkList(data: Any) { if (data is List<*>) { val ele: Any? = data[0] // 가능 data.add(3) // 불가능 } }
  • 제너릭 함수에서도 타입정보는 사라짐
  • fun <T> T.toSuperString() { val className = T::class.java.name // T는 제거되므로 클래스명을 가져올 수 없음 }

reified 키워드 + inline 함수

  • 위 문제는 reified 키워드 + inline 함수로 해결할 수 있음
  • fun <T> List<*>.hasAnyInstance(): Boolean { return this.any { it is T } // T의 내용은 소거되기 때문에 현재 에러가 발생함 }
  • inline: 함수의 본문이 함수 호출 부분에 복사/붙여넣기 됨
    • 위처럼 함수 본문이 호출 부분에 들어가기 때문에 T자체가 본문으로 옮겨져 타입파라미터로 간주되지 않음
      inline fun <reified T> List<*>.hasAnyInstance(): Boolean {
      return this.any { it is T } // 가능
      }
reified 한계
  • reified가 붙은 T는 인스턴스를 만들거나 comapanion object를 가져올 수 없음

7강. 제네릭 용어 정리 및 간단한 팁

  • Generic 타입
  • Raw 타입
  • 변성: 제네릭 틀래스의 타입 파라미터에 따라 제네릭 클래스간의 상속관계가 어떻게 되는지 나타내는 용어
    • 무공변: 타입 파라미터끼리 상속 관계라도 제네릭 클래스간에는 상속관계가 없다는 의미
      • 제네릭 클래스는 기본적으로 무공변함
    • 공변: 타입 파라미터끼리 상속 관계가 제네릭 클래스에도 동일하게 유지됨 (out)
    • 반공변: 타입 파라미터끼리 상속 관계가 제네릭 클래스에서는 반대로 유지됨 (in)
  • 선언 지점 변성: 클래스 자체를 공변/반공변하게 만드는 방법
  • 사용 지정 변성: 특정 함수/변수에 공변/반공변을 만드는 방법
  • 제네릭 제약: 제네릭 클래스의 타입 파라미터에 제약을 거는 방법
  • 타입소거: JDK호환성을 위해 런타임때 제네릭 클래스의 타입 파라미터 정보가 지워지는 것
    • kotlin에서는 inline + reified를 통해 타입소거를 일부 막을 수 있음
  • star projection: 어떤 타입이든 들어갈 수 있음 (List<*>)

타입 파라미터 쉐도잉

  • 똑같은 T지만 클래스에서의 T와 함수의 T는 다른것으로 간주 됨 (like 지역, 전역 변수)
    class CageShadow<T: Animal> {
      fun <T: Animal> addAnimal(animal: T) {
          // 함수에서 사용하는 `T`와 클래스의 `T`를 가리게 됨
      }
    }
    

val cage = CageShadow()
cage.addAnimal(GoldFish("금붕어"))
cage.addAnimal(Carp("잉어")) // 클래스 T가 함수 T에 가려져 Animal 하위 속성은 모드 들어가 버림


## 제네릭 클래스 상속
```kotlin
open class CageV1<T : Animal> { }
class CageV2<T : Animal> : CageV1<T>() {} // 같은 제약의 T를 넘겨줌
class CageV3 : CageV1<GoldFish>() {} // T 제약에 맞는 클래스를 직접 넣어줌

제네릭과 Type Alias

  • 제네릭을 사용하면 클래스명이 길어지니 Type Alias를 사용하여 축약해 사용할 수 있음

위 내용은 인프런 코틀린 고급편강의를 시청하고 작성했습니다
인프런

728x90
댓글