하마
7k
2021-05-11 12:02:07 작성 2021-05-17 16:40:00 수정됨
4
863

[코틀린 코딩 습작] 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에 대해서 알아보았습니다.

감사합니다.

4
  • 댓글 4

  • 마사키군
    1k
    2021-05-12 08:53:22

    좋은 글 고맙습니다.

    아직 코틀린 문법이 익숙하지 않아서 한눈에 들어오질 않네요😅 혹시 이 내용은 자바 제네릭의 한정적 타입 매개변수의 코틀린 이야기일까요?

  • fender
    23k
    2021-05-12 09:00:38

    아주 많은 언어를 접해본 것은 아니지만 타입 시스템은 스칼라가 가장 마음에 들더군요. 요샌 자바나 C#에도 트레잇 비슷한 기능이 들어가던데 본문 내용까지는 아니더라도 셀프 타입 정도만이라도 추가되면 참 유용할 것 같다는 생각이 듭니다.

    그나저나 코틀린에도 자바에 새로 생긴 레코드 유형 같은 것이 있나보군요. 요샌 업무로는 핵폐기물 급 자바 코드 건드리고 취미로는 파이썬이나 쓰다보니 언어들 발전을 체감할 기회가 적어져서 아쉽더군요.

    좋은 글 감사합니다. 저도 비슷한 이유로 조만간에 초보 개발자 분들 위한 글이나 한 번 써볼까 고민 중이었네요 ㅎㅎ;

  • 하마
    7k
    2021-05-12 11:17:06

    마사키군// 동일합니다. ^^

  • 방관
    318
    2021-05-12 17:58:17

    재밌네요, 좋은 글 잘 읽었습니다!

  • 로그인을 하시면 댓글을 등록할 수 있습니다.