[코틀린 코딩 습작] recursive types bound
아래와 같은 이런 타입 시그니처 본 적이 있나요?
Entity<E: Entity<E>>
처음 이런 모습을 보았을때, "머야 이 퐝당한 코드" 같은 생각이 드는건 어쩌면 당연합니다. 그리고 이것에 대해 알아 보기 위해 구글링등을 하기 시작 했을테고, 결국 이 블로그를 찾아 왔을 지도 모르겠네요. 그렇다면 잘 찾아 왔습니다.
도대체 이것은 뭘 까요? 보통 우린 interface Entity<T>이 정도로만 써 왔지 않습니까?
하지만 알고보면 매우 간단하니깐 겁먹지 말고 , 간단한 예를 통해서 이해해 봅시다.
이것을 이해하기 위한 기본적인 부분들도 설명을 하니깐 걱정마세요.
1. 인터페이스
먼저 코틀린에서의 인터페이스를 살펴 봅시다.
interface MyInterface {
fun bar()
}
일반적으로 내용이 없는 메소드들을 선언합니다. 보통 이것을 상속받아서 사용하겠죠. 아래 처럼요.
class Child : MyInterface {
override fun bar() {
// body
}
}
근데 코틀린에서는 인터페이스에 변수도 넣을 수 있으며, 메소드의 본문도 채울 수가 있어요.
interface MyInterface {
val prop: Int // abstract
fun foo() {
//do somthing
print(prop)
}
}
class Child : MyInterface {
override val prop: Int = 29
}
이렇게 본문이 채워진 인터페이스의 메소드는 자식이 오버라이딩을 할 필요가 없어집니다.
interface Named {
val name: String
}
interface Person : Named {
val firstName: String
val lastName: String
override val name: String get() = "$firstName $lastName"
}
data class Employee(
// implementing 'name' is not required
override val firstName: String,
override val lastName: String,
val position: Position
) : Person
인터페이스 자체를 상속받기도 합니다.
2. 일반적인 코드
자 여기 사과와 오렌지 클래스가 있다고 합시다.
data class Apple (val price : Int){
fun compareTo(other: Apple) : Boolean {
return this.price > other.price
}
}
data class Orange (val price : Int){
fun compareTo(other: Orange) : Boolean {
return this.price > other.price
}
}
각 과일들은 가격이 있으며, 서로 동일한 과일들끼리만 가격을 비교 할 수가 있다고 해봅시다.
먼가 중복되는걸 싫어하는 리팩토링의 화신인 우리로써는 이 코드가 탐탁치 않습니다.
네!! price와 compareTo를 추출하고 싶어지죠? 이렇게 만들어 봅니다.
interface Fruits {
val price : Int
fun compareTo(other: Fruits) : Boolean {
return this.price > other.price
}
}
data class Apple (override val price : Int): Fruits
data class Orange (override val price : Int): Fruits
좋습니다!! 중복된 코드들이 없어졌습니다. 보통 여기서 코드 만지기를 그만 두곤 하는데요.
이런 선현의 지혜를 들어봤나요?
"니가 좀 더 고생해서 후임자가 실수하기 어려운 코드를 만들어라 "
위의 코드는 아래 처럼 문제가 될 수 있습니다.
val apple1 = Apple(30)
val apple2 = Apple(50)
val orange1 = Orange(100)
val orange2 = Orange(200)
app.compareTo(or) // 사과와 오렌지는 서로 비교하면 안되요!!
사과와 오렌지는 서로 다른 과일이기 때문에 비교하면 안되지만, 비교해 버렸습니다.
우리는 컴파일 타임에 미리 이런 실수를 알아채길 원해요.
3. 코틀린에서의 제네릭스
class Box<T>(t: T) {
var value = t
}
val box: Box<Int> = Box<Int>(1)
코틀린에서는 자바와 비슷하게 <T>이런식으로 타입매개변수를 지원합니다.
T: Any?
사실 위의 <T>는 T: Any? 의 줄임말입니다. T는 Any?타입을 상속받은 것들이라면 다 된다는 의미입니다. Upper Bounded 되었다고 합니다.
class Box<T : Number>(t: T) {
var value = t
}
val box: Box<Int> = Box<Int>(1)
즉 이렇게 <T: Number>로 제약을 가하면, Box<String>은 불가능합니다.컴파일타임에 문제를 알려주죠.
이제 다시 본론으로 들어가 봅시다.
interface Fruits<T> {
val price : Int
fun compareTo(other: T) : Boolean {
return this.price > other.price
}
}
data class Apple (override val price : Int): Fruits<Apple>
data class Orange (override val price : Int): Fruits<Orange>
위처럼 타입을 매개변수로 주니깐 apple1.compareTo(orange1) 이렇게 다른 과일끼리 비교하면 안된다고 알려줍니다.
하지만 여전히 문제가 있습니다. 어디 일까요?
interface Fruits<T> {
val price : Int
fun compareTo(other: T) : Boolean {
return this.price > other.price // 여기서 T타입에 price가 있는지 모릅니다.!! 에러
}
}
T타입은 무엇이건 될 수 있기 때문에, price가 없을 수도 있어요.
data class Apple (override val price : Int): Fruits<Int> // Fruits<Int> ??
그리고 Furits에 Int를 할당해도 컴파일에 문제가 없습니다. 저렇게 하면 안되는데 말이죠.
4. recursive types 으로 제한(bound) 해서 해결하기
자 이제 결론입니다!!! 집중하세요.
아래처럼 코드를 짜면 문제를 해결 할 수 있습니다.
interface Fruits<T : Fruits<T>> {
val price : Int
fun compareTo(other: T) : Boolean {
return this.price > other.price
}
}
data class Apple (override val price : Int): Fruits<Apple>
data class Orange (override val price : Int): Fruits<Orange>
T 는 Fruits<T>의 제한을 받는 타입이어야만 해요. 즉 T타입은 Fruits를 상속받은 타입이어야 한다는 겁니다.
위에 코드를 보면 Apple과 Orange는 Fruits를 상속받았기 때문에 Fruits의 타입으로 들어 갈 수 있으며 (Int가 타입매개변수로 들어갈 수 도 있는 문제의 해결) T는 Fruits를 상속받는 것이기 때문에 price는 반드시 있게 됩니다( other.price문제 해결)
5. 한계
다 잘된것 같았지만 결국 다음과 같은 구멍은 존재하게 되었습니다.
역시 의도치 않게 코드를 짜는 빌런은 항상 등장하게 마련이죠. ㅎㅎ
data class Apple (override val price : Int): Fruits<Apple> // 좋습니다.
data class Orange (override val price : Int): Fruits<Orange> // 좋아요!
data class Banana (override val price : Int): Fruits<Apple> // 엇 이건 먼가요?
바나나라는 새로운 클래스를 만들었는데, 상속은 Fruits<Apple>을 이용했네요. ;;;;;
이 코드는 컴파일은 잘됩니다. 하지만 버그죠.
자바와 코틀린에서는 이런 문제까지는 해결해주지 못하는 것으로 알고 있습니다.
다만 스칼라에서는 가능합니다. ( self: E => 라는 방식을 통해서)
그럼 여기까지 recursive type bound에 대해서 알아보았습니다.
감사합니다.