관리 메뉴

HAMA 블로그

스칼라 강좌 (42) 고급 타입 다루기 - F-bounded polymorphism / recursive types 본문

Scala

스칼라 강좌 (42) 고급 타입 다루기 - F-bounded polymorphism / recursive types

[하마] 이승현 (wowlsh93@gmail.com) 2017. 3. 26. 13:32



         F-bounded polymorphism / recursive types

                         번역:http://blog.originate.com/blog/2014/02/27/types-inside-types-in-scala/  


지금까지 이런 타입 시그니처 본 적이 있어?   (역주: 난 얼마전에 봤다...당황 많이 했어 ;;) 

1
trait T[U <: T[U]]

처음 이런 모습을 보았을때,  "머야 이 퐝당한 코드" 같은 생각이 드는건 어쩌면 당연해. 그리고 이것에 대해 알아 보기 위해 구글링등을 하기 시작 했을테고, 결국 이 블로그를 찾아 왔을 지도? 그렇다면 잘 찾아 왔네 친구~ ^^

좋아,  저 헤괴망측한것은 도대체 뭘까? 보통 우린  trait T[U]  이렇게 써 왔잖아? 
(역주:  T[U <: T[U]] 라니? U 타입이긴 한데 T[U] 의 하위 타입이라고? U 가 두군데 저렇게 쓰여도 됨? 미리 답을 얘기하면 
여기서 U 타입은 T[U] 를 상속받은 타입만으로 강제 되는 거야. )   

알고보면  매우 간단하니깐 겁먹지 말고 , 간단한 예를 통해서 이해해 보자고. 

자 출발!

우리에게 매우 익숙한 CRUD 에 대한 코드를 짜 볼거야. 아래와 같이 case class 를 이용해서 간단하게

1
2
3
4
5
6
7
8
9
10
11
12
13
14

case class Apple(name: String, price: Double) {
  def create(entityData: String): Apple
  def read(id: String): Option[Apple]
  def update(f: Apple => Apple): Apple
  def delete(id: String): Unit
}

case class Bird(name: String, birthday: DateTime) {
  def create(entityData: String): Bird
  def read(id: String): Option[Bird]
  def update(f: Bird => Bird): Bird
  def delete(id: String): Unit
}

사과랑 새라는 두개의 클래스가 있는데, 잘 보면  두 클래스가 하는 역할이 너무 비슷하잖아? 중복되는 느낌도 들고~ 앞으로 비슷한 클래스를 작성 할때 저 코드를 또 타이핑 해야할 것을 생각하면 한숨이 나오지..그렇다면 리팩토링의 화신인 우리가 해야할 것은 무엇일까?

그래 인터페이스로 끌어 올리는거야. 스칼라에서는 trait 라는 쿨한 녀석이 있잖아?

1
2
3
4
5
6
7
8
9
10
trait CrudEntity {
  def create(entityData: String): CrudEntity
  def read(id: String): Option[CrudEntity]
  def update(f: CrudEntity => CrudEntity): CrudEntity
  def delete(id: String): Unit
}

case class Apple(name: String, age: Int) extends CrudEntity

case class Bird(name: String, hobby: String) extends CrudEntity

앗..망할 ㅋㅋ  trait 의 메소드 시그니처 보면 좀 문제가 있네. 우리가 원한것은 Apple 이면 Apple 에 대해 작업 하길 원하는데 이 코드라면  그냥 CrudEntity 를 리턴하잖아;;

CrudEntity 트레잇에 좀 더 명확한 타입을 추가해보자..

1
2
3
4
5
6
7
8
9
10
trait CrudEntity_2[E] {
  def create(entityData: String): E
  def read(id: String): Option[E]
  def update(f: E => E): E
  def delete(id: String): Unit
}

case class Apple(name: String, age: Int) extends CrudEntity_2[Apple]

case class Bird(name: String, hobby: String) extends CrudEntity_2[Bird]

좋아. 우리가 생각한건 이런거잖아. ( 대부분 여기 까지 작업하고 종료를 하겠지 ?) 
하지만 좀 만 더 생각해보면  살짝 문제가 있어.  무엇일까?

문제점은 바로 누군가 CrudEntity_2 를 이렇게 정의하면 생겨..

1
case class Orange(name: String, bankAccount: Double) extends CrudEntity_2[FloobyDust]

웁스~ 위에 코드에선 CrudEntity_2[E] 의 E타입에 제약이 없어서, 아무거나 넣어도 컴파일러가 불평불만을 안하잖아? 우리가 강력한 타입시스템을 사용하는 이유가 조금 의미 없어졌네.. Orange 만 넣도록 해야하는데 말이야.

그래 무조건 CrudEntity_2[E] 에서 E 에는 CrudEntity_2 를 상속한 녀석만 들어가도록 제약을 걸고 싶지?
짜잔~~ 이때 등장하는것이 우리가 처음 퐝당해 했던 저 코드야~! 아래를 보라고.

1
2
3
4
5
6
7
8
9
10
trait CrudEntity_3[E <: CrudEntity_3[E]] {
  def create(entityData: String): E
  def read(id: String): Option[E]
  def update(f: E => E): E
  def delete(id: String): Unit
}

case class Apple(name: String, age: Int) extends CrudEntity_3[Apple]

case class Bird(name: String, hobby: String) extends CrudEntity_3[Bird]

아주 훌륭해 졌어. 이제 E 타입은 무조건 해당 subtype 만 들어 갈 수 있어. 응? 해당 subtype?? 이라고?? 그럼 아래처럼도 되겠네?  이건 우리가 원한게 아니잖아?

1
case class Orange(name: String, age: Int) extends CrudEntity_3[Apple]

그럼 이건 어떻게 해결 해야 할까?  이 경우에 우리는 self type 을 써서 해결 할 수 있어.

이제 마지막이야..다 왔어. 친구~ CrudEntity는 다음과 같이 정의면 모든게 해결이야.
(역주: self 대신 this 로 해도되고, foo 로 해도 됩니다.)

1
2
3
4
5
6
trait CrudEntity[E <: CrudEntity[E]] { 
  self: E =>
  def create(entityData: String): E
  def read(id: String): Option[E]
  def update(f: E => E): E
  def delete(id: String): Unit
}

self: E => 를 추가하면 CrudEntity [E] 를 상속받은 객체는 반드시 자신의 타입인 E 가 되야해.

1
case class Orange(name: String, age: Int) extends CrudEntity[Apple]

Orange 는 Apple타입이 아니라는것을 컴파일러가 확실히 제약해주지.

이 정도는 해야 우리가 원하는 안전하고 확실한 강력한 타입을 가지는 코드라고 할 수 있지 않을까?
그럼 빠잉~


Comments