프로필 사진
하마
bolt icon8.8k·1년 이상·
1.1k
·수정됨
공유

[코틀린 코딩 습작] recursive types bound

오키 10만 양병기념으로 치킨도 받게 됬는데,
그 동안 뭘 쓴게 없어 양심에 찔려...작성해 봤습니다 :-) 
제목은 좀 무시무시해 보여도 정말 쉬운내용이며, 객체지향 개발에 필수적인 내용입니다.

[코틀린 코딩 습작] 1.recursive types bound

[코틀린 코딩 습작] 2. Interceptive Fillter

아래와 같은 이런 타입 시그니처 본 적이 있나요?

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에 대해서 알아보았습니다.

감사합니다.

cat-footer