관리 메뉴

HAMA 블로그

스칼라 강좌 (19) - implicit 본문

Scala

스칼라 강좌 (19) - implicit

[하마] 이승현 (wowlsh93@gmail.com) 2016. 8. 13. 13:25

스칼라에서 implict 는 편하기도 하지만 코드가독성을 엄청 떨어뜨릴 수도 있기 때문에 논란이 되곤합니다.
왜 그런지 한번 살펴 볼까요?

0. 암시규칙 

 x + y 라는 표현식에서 타입 오류가 있다면 컴파일러는 convert(x) + y 를 시도 해봅니다. 
여기서 자동으로 가져다 사용되는 convert 는 무엇일까요? 

 convert 는 암시적으로 적용되는 변환을 목적으로 자동적으로 사용됩니다.  convert 가 아래의 규칙들을 갖는다면 말이지요.

 

1. 표시규칙 : implicit 로 표시한 정의만 검토 대상이 된다.

즉 implicit def intToString(x : Int) = x.toString 과 같이 implict 를 붙여주면 컴파일러가 암시적 변환에 사용할 후보에 넣는다. 변수,함수,객체정의에 implict 표시를 달 수 있다. 

2. 스코프 규칙 : 삽입할 implict 변환은 스코프 내에 단일 식별자로만 존재하거나, 변환의 결과나 원래 타입과 연관이 있어야 한다.

즉 someVariable.convert 같이는 안되며, 외부에서 가져올 경우 import Preamble._ 를 이용해서 단일 식별자로 가리킬 수 있게 한다. 그리고 원타입과 변환 결과 타입의 동반 객체에 있는 암시적 정의도 가능하다. 

Dollar 에서 Euro 로 변환 하고자 할때, 
object Dollar {

implict def dollarToEuro (x : Dollar) : Euro = ....

}

class Dollar { ... } 

이렇게 하면된다. implict 변환은 프로그램  전체에 영향을 미치지 않는다는 것을 기억하라. 그렇다면 가독성에 크게 문제가 생길 것이다.

3. 한번에 하나만 규칙 : 오직 하나의 암시적 선언만 사용한다.
convert1(convert2 (x) ) + y  이렇게 안됨.

4. 명시성 우선 규칙: 코드가 그 상태 그대로 타입 검사를 통과 한다면  암시를 통한 변환을 시도치 않음
 
 

1. 암시적 변환 (implicit conversion) 

개념 

버튼이라는 클래스를 가지고  생각해 보죠.

버튼이라는 클래스는 이미 라이브러리로 제공되고  있는 상황 (우리가 임의로 못고치는) 에서 버튼이 

눌려졌을때 어떤 행동이 일어날지는 개발자가 정해서 버튼에게 넘겨주게 됩니다. 

일단 우리는 왼쪽 버튼이 눌려지면 "삐약" 이라는 소리를 내게 하고 싶다고 해보죠.

 

val button = new OkkyButton 

버튼 클래스는  버튼이 눌려졌을때 어떤 행동을 할 지에 대해서  제공 받아야 합니다.

 

button.addLButtonDownAction( ...어떤 행동... ) 

보는바와 같이 버튼은 메소드를 제공하고 우리는 파라미터에   어떤 "행동" 을 제공합니다.

선이 그려질 수 도 있고, 파일이 저장될 수 도 있고 등등

 

이때 행동을 버튼 클래스에 추가 시키는 방법은 아래와 같을 수 있습니다.

button.addLButtonDownAction(

new ActionListener {

def actionPerformed ( event : ActionEvent ) {

println (" 삐약 ")

}

}

}

 "삐약" 이라는 행동을 넣었습니다.  완성!!

근데 좀 껄쩍지근한게  달랑 "삐약" 을 넣고 싶었는데 너무 많은 코드가 사용 되었네요. 

(이런 코드는 행사코드/얼개코드 등으로 불립니다. 해당 언어의 문법상 어쩔 수 없이 구현해야하는 코드지요.)

ActionListener  클래스도 사용되었으며 actionPerformed   메소드도 사용되었습니다.

그냥  println (" 삐약 ")  만 사용하면 안될까요?  왜 복잡한 코드를  나중에 또 봐야하는가요? 

(복잡함이라는게 코드가 길다고 복잡한건 아니긴 합니다. 즉 좀 길더라도 왜 그렇게 해야하는지에 대한 납득의 과정이 빠르면 문제되지 않으며, 코드가 짧더라도 그 납득의 과정이 불분명하면 오히려 더 코드가독성이 어려워 지곤 합니다. 이것이 바로 스칼라 언어의 딜레마 같습니다. 즉 웬만큼 익숙해 지기 전까지는 자바보다 분명 짧지만 코드가독성이 나아지지 않는? ) 

 

뭐시~ 중한디??

 

버튼의 addLButtonDownAction 메소드는 특정 인터페이스를 상속한 클래스를 파라미터로 강제하고 

있는거 같구요.. 그래서 어쩔 수 없이  저렇게 inner 클래스를 파라미터로 던져 주었습니다.

클래스 말고 그냥 함수를 던저 주면 안될까요? 외부 라이브러리라 바꿀 수 가 없습니다.

 

button.addLButtonDownAction { (_: ActionEvent) => printn(" 삐약 ") }

요로코롬 말이죠~   이렇게 해 보았지만  역시나 애초에~~ 

OkkyButton 버튼은  클래스를 매개변수로 받기 때문에 아쉽게도 요거는 꽝입니다. 

(여기서 java 8 스윙은 람다를 지원해서 어쩌구 저쩌구.. 입이 간질거리겠지만  잠시 잊어버리시고.. ) 

 

자 여기서 개념이 나오니깐 정신집중!!!!! 

 

저렇게 우아하게 처리해봤는데 아쉽게도 타입 불일치로 "꽝" 이 되버렸네요. 그냥 포기할까요?

우리 포기하지 말자구요 -.-V   스칼라는 암시적 변환이 있습니다.

자 다음 코드를 보세요.

implicit def functionToActionClass(f : ActionEvent => Unit ) = 

new ActionListener {

def actionPerformed (event : ActionEvent) = f (event)

}

 

implicit 로 처음에 시작하구요. 매개변수를 함수로 받아서 클래스를 리턴해 주고 있습니다.

이 코드를 사용해보죠.

button.addLButtonDownAction(

functionToActionClass( 

(_: ActionEvent) => println (" 삐약 ") 

}

이렇게 사용되었습니다. 애초에 내부 클래스 코드보다는 조금 간단해 졌습니다. !! (어떤 면으로는  더 복잡해졌습니다.) 

음.. 근데 이건 implicit 가 먼지모르겠지만 그거 상관없이 되는 거 잖아? 그렇죠.. --;; 

좀 만 기다려주세요.

이제 implicit 의 파워가 나옵니다.

 

button.addLButtonDownAction( (_: ActionEvent) => println (" 삐약 ") }

 

이렇게 functionToActionClass 를 빼도 잘 작동합니다~ 정말 너무나 간단해 졌습니다.

이게 어떻게 가능하냐면

일단 저 코드가 컴파일되면 처음엔 타입오류가 발생하긴 합니다.근데 여기서 그냥 나 못해~ 하고 끝나는게 

아니라 우리의 컴파일러군은 포기하지 안고서 "암시적 변환" 을 통해서 해결 할 수 있지 않나 살펴봅니다.

살펴보다가   implicit 로 선언된 함수 functionToActionClass  를 찾았습니다. 이걸 가지고 시도해보니 잘됩니다.

 

네 

암시적 변환이란 컴파일러가 열심히 일해서 개발자가 성가신 일들을 하지 않게 도와주는 녀석이었습니다.

이제 클래스를 넣어도 잘되고 저렇게 람다식으로 넣어도 잘 될 것입니다. 

 


2. 암시적 파라미터 (implicit parameter) 

개념

이것도 암시적 변환하고 매우 비슷합니다. 이름처럼 파라미터를 암시적으로 넣어 준다는것인데요.

즉 내가 넣지 않아도 자동으로 들어간다는 거겠죠? 

 

예를들어 

object Greeter {

def greet (name : String ) (implicit prompt : MyPrompt) {

println (name)

println (prompt.preference)

}

}

보시는 바와 같이 greet 는 두개의 파라미터를 갖습니다.  뒤에 것이 암시적 파라미터 인데요.

 

이렇게 호출 해 보겠습니다.

val prompt = new MyPrompt("ubuntu> ")

Greeter.greet("삐약") (prompt)

이건 직접적으로 prompt 객체를 만들어서 넣어주고 있습니다. 네 이건 명시적입니다. 당연히 작동하겠죠.  

 

그럼 암시적은 무엇인가??

 

Greeter.greet("삐약")   

이렇게 해도 잘 된다는 말인데요. 즉  뒤에 파라미터를 명시적으로 안 넣어 줘도 잘 된다는 거죠. 

이게 어떻게 되는거냐면요.
암시적으로 들어갈 파라미터는 다음과 같이  어딘가에 미리 만들어 놓습니다.

object HamaPrefs{

implicit val prompt = new MyPrompt (" ubuntu> ")

}


저게  있어야  컴파일러는 이 변수를 빠진 파라미터 목록에서 찾아서 적용해 주거든요.

정리하면

- 미리 변수 정의   : implicit val b = new B;
- 함수를 인자 없이 호출 :   test (a)
- 호출 당하는 함수쪽에 인자를 implicit 로 설정  : 
def  test(a :A , implicit b : B)  { }

입니다.  이렇게 DI 를 해놓으니깐 유연성이 커질거 같네요. 그쵸? 

 

마지막으로 implicit 키워드는 개별 파라미터가 아니라 전체 파라미터 목록을  범위로 합니다.

다음을 보시죠.

class MyTest1(val  tell : String)

class MyTest2(val tell : String)

 

object Greeter {

def greet (name : String) ( implicit  test  : MyTest1 ,  test2 : MyTest2 ) {

println(name)

println(test.tell)

println(test2.tell)  

    }

}

object HamaPrefs {

implicit val test = new MyTest1 ("삐약")

implicit val test2 = new MyTest2 ("개굴")

}

 

위 코드에서 test 와 test2 파라미터 모두 implicit 에 영향 받는다는 겁니다.  Greeter.greet ("HAMA")  이렇게 단일 파라미터를 던져도 나머지 암시적 파라미터를 찾아서 작동하지요. 

프레임워크등을 사용하다가 implicit 가 나오면 해당 프레임워크에서 그 인자에 대해서는 미리 만들어 뒀구나 라고 생각하면 편할거 같습니다.

 

3. implicit  와 폴리모피즘 

trait Factory[T] {
  def create: T
}

object Factory {
  implicit def stringFactory: Factory[String] = new Factory[String] {
    def create = "foo"
  }

  implicit def intFactory: Factory[Int] = new Factory[Int] {
    def create = 1
  }
}

object Main {
  def create[T](implicit factory: Factory[T]): T = factory.create
 
  // or using a context bound
 
  // def create[T : Factory]: T = implicitly[Factory[T]].create


  def main(args: Array[String]): Unit = {
    val s = create[String]
    val i = create[Int]

    println(s) // "foo"
    println(i) // 1
  }
}


4. 기존 클래스를 확장하기 위한 방법 (kotlin extension vs scala implict)

kotlin 

data class Person(val firstName: String, val lastName: String)

fun Person.greet() = "Hello, $firstName $lastName!"

Person("Matt", "Moore").greet()

Scala

case class Person(val firstName: String, val lastName: String)

implicit class ExtendedPerson(p: Person) {
  def greet = s"Hello, ${p.firstName} ${p.lastName}!"
}

Person("Matt", "Moore").greet
implicit class ExtendedInt(i: Int) {
  def squared = i * i
  def doubled = i * 2
}

2.squared
2.doubled



5. Play2 웹 프레임워크에서의 implicit  

 

웹어플리케이션을 빠르고 즐겁게 만들 수 있는 cool 한 Play2 에서 implicit 는 적극적으로 사용됩니다.

반복을 줄이기 위해서 사용되는데요 예를 보면서 알아 보시죠. 

예를들어 쇼핑몰 싸이트에서 방문자의 장바구니에 상품이 몇개가 담겨져 있는지 보여주도록 모든 페이지의 

 상단에 쇼핑 카트를 유지 하길 원한다고 해보죠.  예) Shoping Cart : 3 Items (Show Cart) 

 

 

그러려면 모든 웹페이지 마다 cart 정보를 넘겨주어야합니다. 다음 소스를 보시죠.

* Shop.scala 

object Shop extends Controller {

def catalog() = Action { implicit request =>
val products = ProductDAO.list
Ok(views.html.products.catalog(products, cart(request))

}

def cart(request: Request) = {

 

// Get Cart from session

}

 

Shop 컨트롤러의 catalog 호출을 하는 세션으로 부터 Cart 정보를 얻은후에 

catalog 뷰 템플릿의 인자로 products  리스트와 함께 넘겨주고 있습니다.

 

* catalog.scala.html

@

(products:

Seq

[Product], cart: Cart)

@

main(cart) {

<h2>

Catalog

</h2>
<ul>
@for

(product <- products) {

<li>
<h3>@

product.name

</h3>
<p class="description">@

product.description

</p>
</li>

}

</ul>

}

catalog 뷰 템플릿에서는 매개변수로 products  와 cart 를 받아서 , products 는 자신이 드로잉 하고 
cart 는 명시적으로  메인 뷰 템플릿으로 보내주네요.

 

* main.scala.html 

@(content: Html)(cart: Cart)
<!DOCTYPE html>
<html>
<head>
... 생략 ...
<span id="cartSummary" class="label label-info">
@cart.productCount match {

case 0 => {
Your shopping cart is empty.
}

case n => {
You have @n items in your shopping cart.
}
}
</span>

<div class="container">
  @content
</div>

마지막으로 메인뷰 템플릿에서는 넘어온  catalog 뷰 템플릿과 cart 를 이용하여  페이지를 완성합니다.
cart 에 아무것도 없을 경우와 있을 경우를 case 로 분리해서 처리 하고 있군요.

 

 

위에 보다시피 이런 페이지가 많으면 많을 수록 일일이 객체를 넘겨줘야하는데요.

implicit 를 활용하면 그러지 않아도 됩니다. implicit  가 적용된 다음  소스를 보시죠.

 

* Shop.scala 

object Shop extends Controller with WithCart {

  def catalog() = Action { implicit request =>
    val products = ProductDAO.list
    Ok(views.html.products.catalog(products))
  }

}

cart 를 넘겨주는 코드가 사라 졌습니다. 대신 with WithCart 가 생겼군요. WithCart 는 제일 마지막에 살펴보겠습니다.

 

* catalog.scala.html

@(products: Seq[Product])(implicit cart: Cart)

@main() {

<h2>Catalog</h2>
<ul>
@for(product <- products) {

<li>
<h3>@product.name</h3>
<p class="description">@product.description</p>
</li>
}

</ul>

}

cart 매개변수 앞에 implicit 가 생겼으며 , 역시 main 뷰 템플릿으로 cart 를 명시적으로 넘겨주지 않습니다.

 

* main.scala.html 

@(content: Html)(implicit cart: Cart)
<!DOCTYPE html>
<html>
... 생략 ...


<span id="cartSummary" class="label label-info">
@cart.productCount match {

case 0 => {
Your shopping cart is empty.
}

case n => {
You have @n items in your shopping cart.
}
}
</span>

<div class="container">
@content

</div>

역시 cart 매개변수 앞에 implicit 가 생겼습니다. 

이렇게 cart 를 모든 페이지에서 보여주기 원할때 컨트롤러에서 cart 를 매번 세션에서 직접가져오는 반복이 사라졌습니다.

 

그럼 cart 는 어디로 사라졌을까요? 매직인가요?  아닙니다. Play2 와 스칼라는 이해하기 불가능한 마술같은
일이 거의 없습니다.
개발자 친화적이라고 할까요?

 

trait WithCart {

  implicit def cart(implicit request: RequestHeader) = {
    // Get a fake cart. In a real app, you'd get it from the session here.
    Cart.demoCart()
  }

}

 이런 trait 가 만들어져 있습니다. trait 는  자바 인터페이스 + 추상클래스와 비슷하다고 일단 생각하시면 되는데요. 

 위에 Shop.scala 에서 이것을 믹싱하고 있습니다.

object Shop extends Controller with WithCart {

결국 WitdCart 만 믹싱해주고  매개변수들을 implicit 로 선언해주기만하면 컴파일러가 알아서 적용시켜 주는 것이죠.

 

웹개발경험+Play2+Scala에 대한 기초지식이 없다면 어려울 수 도 있을거 같네요. :-| 

Comments