제네릭은 추상화를 하는데 있어 굉장히 필수적인 기능이고, 정말 잘 활용되는 언어적 특징이다. 제네릭을 이해하기 위해 Cage 클래스를 만들어 보자. Cage 에는 여러 동물을 넣거나 꺼낼 수 있다. 가장 간단한 Cage 클래스는 다음과 같다.
class Cage {
private val animals: MutableList<Animal> = mutableListOf()
fun getFirst(): Animal {
return animals.first()
}
fun put(animal: Animal) {
this.animals.add(animal)
}
fun moveFrom(cage: Cage) {
this.animals.addAll(cage.animals)
}
}
당연히 Animal
클래스도 필요하니 간단하게 다음과 같이 금붕어, 잉어를 만들어 주자.
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)
여기까지 그림으로 나타내면 다음과 같다.
이제 Cage
에 잉어를 넣었다 잉어를 빼보자.
fun main() {
val cage = Cage()
cage.put(Carp("잉어"))
val carp: Carp = cage.getFirst() // Error: Type Mismatch
}
앗! Error: Type Mismatch
가 발생한다. 생각해 보면 당연하다. 우리는 Cage를 만들어 잉어를 넣었다가 바로 가져오려 했는데, Cage
클래스의 getFirst()
함수 반환 타입은 Animal
이기 때문에 바로 Carp
타입을 가져올 수 없는 것이다.
이 에러를 해결하기 위해서는 어떻게 해야 할까?! 가장 간단한 방법은 타입 캐스팅(type casting)을 하는 것이다. Kotlin에서는 as
키워드를 이용해 타입 캐스팅을 할 수 있다.
fun main() {
val cage = Cage()
cage.put(Carp("잉어"))
val carp: Carp = cage.getFirst() as Carp
}
이렇게 하면 바로 잉어 타입을 가져올 수 있게 된다.
자, 그런데 사실 위 코드는 매우 위험한 코드이다. 지금 당장은 잉어로 타입 캐스팅을 해도 문제가 없어 보이지만, 누군가 Cage
에 GoldFish
를 넣을 수도 있기 때문이다.
fun main() {
val cage = Cage()
cage.put(GoldFish("사실은 금붕어"))
val carp: Carp = cage.getFirst() as Carp // 에러가 나지 않는다!!
}
실제로 두 번째 줄을 cage.put(GoldFish("금붕어"))
라고 바꾸었을 때 실제 오류는 런타임이 돼서야 발견하게 된다. 지금은 한 함수 내에서 금붕어를 넣고 잉어를 빼니 금방 발견할 수 있지만, 실제로는 복잡한 코드 내에서 이런 버그를 찾기란 쉽지 않다.
그렇다면 어떻게 타입 안전하게 잉어를 가져올 수 있을까??! 한 가지 방법은 Kotlin의 안전한 타입 캐스팅(safe type casting)과 엘비스 연산자(elivs operator)를 활용하는 것이다.
fun main() {
val cage = Cage()
cage.put(Carp("잉어"))
val carp: Carp = cage.getFirst() as? Carp
?: throw IllegalArgumentException()
}
하지만 이 방법 역시 as
를 사용했을 때와 마찬가지로 여전히 실수할 여지가 있고, 실수로 금붕어가 Cage에 들어가면 에러가 발생한다. 우리는 아예 에러가 발생하지 않았으면 좋겠다.
그렇다면, 동일한 Cage 클래스이지만 잉어만 넣을 수 있는 Cage, 금붕어만 넣을 수 있는 Cage를 구분하는 방법은 어떨까? 이 방법을 사용하면 타입 안전하게 잉어를 Cage에 넣었다가 잉어 타입으로 가져올 수 있다.
이럴 때 바로 제네릭을 사용할 수 있다. Cage
클래스를 다시 한 번 만들어 보자. 이때 특정 타입만 Cage가 받을 수 있도록 처리하기 위해 타입 파라미터를 사용할 것이다. 타입 파라미터를 클래스에 적용하는 방법은 간단하다. 클래스 뒤에 < >
를 이용해 대문자를 적어주면 된다.
class Cage2<T> {
private val animals: MutableList<T> = mutableListOf()
fun getFirst(): T {
return animals.first()
}
fun put(animal: T) {
this.animals.add(animal)
}
fun moveFrom(cage: Cage2<T>) {
this.animals.addAll(cage.animals)
}
}
이렇게 Cage2
처럼 타입 파라미터가 적용된 클래스를 제네릭 클래스라 부르고, < >
에 들어간 T
를 타입 파라미터라고 부른다.
Cage2
를 제네릭 클래스로 만들며, getFirst()
, put()
, moveFrom()
함수에도 모두 T
가 들어가게 되었다. 이렇게 되면, Cage2
클래스를 인스턴스화 할 때 타입 정보를 넣어주어야 하고 그때 넣어준 타입 정보가 모두 T를 대체한다는 의미이다.
아까 살펴보았던 예제에 제네릭 클래스인 Cage2를 바로 적용해 보자.
fun main() {
val cage = Cage2<Carp>()
cage.put(Carp("잉어"))
// 이제 as Carp 없이도 getFirst 메소드를 호출하면 바로 Carp가 나온다!!!
val carp: Carp = cage.getFirst()
}
제네릭을 활용한 덕분에 타입 안전하게 잉어를 가져올 수 있게 되었다! 👍
실제로 이런 제네릭 클래스는 List나 Collection 같은 자바 표준 라이브러리, 코틀린 표준 라이브러리에서 활발하게 사용되고 있다.
자 이제 다음 요구사항을 생각해 보자.
- 금붕어 Cage에 금붕어 한 마리를 넣고, 물고기 Cage에
moveFrom
메소드를 사용해 금붕어를 옮기자!
이 요구사항을 코드로 표현하면 다음과 같다.
fun main() {
val goldFishCage = Cage2<GoldFish>()
goldFishCage.put(GoldFish("금붕어"))
val cage = Cage2<Fish>()
cage.moveFrom(goldFishCage) // Error: Type Mismatch
}
자 그런데! 분명, 금붕어는 물고기 Cage에 들어갈 수 있을 것 같음에도 cage.moveFrom(goldFishCage)
는 Type Mismatch 에러가 발생한다! 😭
다음 시간에 왜 이런 에러가 발생하는지, 어떻게 해결할 수 있는지 알아보자.
내용 출처: https://inf.run/F9gX
'개발 공부 기록하기 > 01. JAVA & Kotlin' 카테고리의 다른 글
코루틴이란 무엇인가? (루틴과 코루틴) (0) | 2023.08.30 |
---|---|
코틀린 제네릭 - 배열과 리스트, 제네릭과 무공변 (0) | 2023.08.30 |
코틀린 + 스프링 ArgumentResolver 주의할 점 (0) | 2023.08.26 |
mockk 간단 사용법 (1) | 2022.05.23 |
이펙티브 코틀린 간단 정리 (Effective Kotlin) (0) | 2022.05.21 |