Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Simply Lift : Chapter 5 - HTTP and REST

원문 : http://simply.liftweb.net/index-Chapter-5.html#toc-Chapter-5

5장
HTTP와 REST (HTTP and REST)


Lift의 HTML 생성 특징들을 보았습니다. 이제 낮은 레벨과 REST스타일의 HTTP요청을 다루는 데 뛰어들어보겠습니다.  이 챕터를 위한 코드는 https://github.com/dpp/simply_lift/tree/master/samples/http_rest 에서 볼 수 있습니다.



5.1 소개

Lift는 세션의 범위내 또는 범위밖에서 모두 저레벨 HTTP요청에 접근하도록 해줍니다. 세션이 없거나 상태가 없는 모드에서 Lift는 HTTP응답에 쿠키를 추가하려고 컨테이너의 세션관리 장치를 사용하지 않고 요청동안에 이용가능한 SessionVar이나 ContainerVar를 만들지 않습니다. 무상태(stateless) REST 요청은 session affinity을 요구하지 않습니다. 무상태 REST 핸들링을 위한 인증은 OAuth를 통해서 이루어질 수 있습니다. 요청이 상태를 가지고 다루어진다면 컨테이너 세션은 JSESSIONID 쿠키가 요청의 일부로 제공되지 않았을 때 생성될 것이고 JSESSIONID 쿠키는 응답에 포함될 것입니다.

Lift는 들어오는 HTTP요청을 매칭하고 패턴매칭 프로세스의 일부로써 값들을 추출하고 결과를 리턴하도록 Scala의 패턴매칭을 사용합니다. Scala의 패턴매칭은 아주아주 강력합니다. 반드시 매치되어야 하는 패턴과 와일드카드 값(서브표현식은 모든 공급된 값과 매치될것입니다.)과 와일드카드값을 변수로 추출하고 명시적인 추출자(extractor)(매치될 것인지 변수로 추출한 것인지를 결정하려고 값에 명령적인 로직을 적용합니다.)의 선언을 모두 허용합니다. Lift는 HTTP요청을 나타하는 주어진 Req를 위해서 정의되었는지 보기 위해서 Scala의 PartialFunction[Req, () => Box[LiftResponse]]를 테스트합니다. 매치되었다면 Lift는 함수의 결과를 취하고 Box[LiftResponse]를 얻기위해서 그것을 적용하고 Box가 꽉찼다면 응답은 브라우저에 다시 보내질 것입니다. 예제를 보겠습니다.



5.2 고생스러운 REST(REST the hard way)

Lift와 함께 로우레벨의 REST를 수행하는 것을 보도록 하겠습니다: 들어오는 HTTP 요청을 취하고 그것은 Box[LiftResponse]를 리턴하는 함수로 변환합니다.(그리고 걱정하지 않아도 됩니다. 이것을 얻는 것은 더 쉽지만 보이지 않는 곳에서 무슨일이 벌어지는지에 대한 아이디어를 얻기 위해서 간단한 것으로 시작하겠습니다.)

BasicExample.scala:

package code
package lib

import model._

import net.liftweb._
import common._
import http._

/**
 * 기본적인 Lift 도구들을 사용하는
 * REST 스타일 인터페이스의 간단한 예제
 */
object BasicExample {
   /*
    * 주어진 접미사(suffix)와 아이템, LiftResponse를 만듭니다
    */
   private def toResponse(suffix: String, item: Item) =
      suffix match {
         case "xml" => XmlResponse(item)
         case _ => JsonResponse(item)
       }

   /**
    * /simple/item/1234.json를 찾습니다
    * /simple/item/1234.xml를 찾습니다
    */
   lazy val findItem: LiftRules.DispatchPF = {
      case Req("simple" :: "item" :: itemId :: Nil, //  경로(path)
               suffix, // 접미사(suffix)
               GetRequest) =>
                     () => Item.find(itemId).map(toResponse(suffix, _))
   }

   /**
    * /simple2/item/1234.json를 찾습니다
    */
   lazy val extractFindItem: LiftRules.DispatchPF = {
      // 추출자(extractor)를 가진 경로
      case Req("simple2" :: "item" :: Item(item) :: Nil,
               suffix, GetRequest) =>
                     // 응답을 리턴하는 함수
                     () => Full(toResponse(suffix, item))
   }
}

퍼즐의 추가적인 조각은 핸들러를 후킹하는 것입니다. 이것은 다음 코드처럼 Boot.scala에서 이루어집니다.

// the stateless REST handlers
// 무상태 REST 핸들러
LiftRules.statelessDispatchTable.append(BasicExample.findItem)
LiftRules.statelessDispatchTable.append(BasicExample.extractFindItem)

// 동일한 상태를 가진 버전
// LiftRules.dispatch.append(BasicExample.findItem)
// LiftRules.dispatch.append(BasicExample.extractFindItem)

코드를 보겠습니다. 먼저 각 핸들러는 PartialFunction[Req, () => Box[LiftResponse]]이지만 부분함수(partial function)의 별칭인 Scala 타입인 LiftRules.dispatchPF의 약칙을 사용할 수 있습니다.

lazy val findItem: LiftRules.DispatchPF =

요청 디스패처 핸들러의 타입 시그니처를 가진 findItem을 정의합니다.

case Req("simple" :: "item" :: itemId :: Nil, //  경로(path)
         suffix, // suffix
         GetRequest) => 

매칭을 위해서 패턴을 정의합니다. 이 경우에 처음 두 단계가 /simple/item을 가진 어떤 3단계의 경로가 매치됩니다. 경로의 3번째부분은 변수 itemId로 추출될 것입니다. 마지막 경로 아이템의 접미사는 suffix 변수로 추출될 것이고 요청은 반드시 GET이어야 합니다.

위의 기준(criteria)을 만났다면 그 다음 부분함수(partial function)이 정의되고 Lift는 () => Box[LiftResponse]의 결과를 얻기 위해서 부분함수를 적용할 것입니다.

() => Item.find(itemId).map(toResponse(suffix, _))

이것은 itemId를 찾고 결과 Item을 요청 접미사를 기반으로 응답으로 변환하는 함수입니다. toResponse 메서드는 다음과 같습니다:

/*
 * Given a suffix and an item, make a LiftResponse
 * 주어진 접미사와 아이템으로 LiftResponse를 만듭니다.
 */
private def toResponse(suffix: String, item: Item) =
   suffix match {
      case "xml" => XmlResponse(item)
      case _ => JsonResponse(item)
}

약간 장황하더라도 아주 직설적입니다. 이 파일에서 다른 예제를 보겠습니다. 요청경로의 세번째 아이템의 String를 Item으로 변환하려고 추출자(extractor)를 사용합니다.

// 추출자(extractor)를 가진 경로
case Req("simple2" :: "item" :: Item(item) :: Nil,
      suffix, GetRequest) =>


이 경우에 경로의 세번째 엘리먼트가 유효한 Item이 아닌 한 패턴은 매치되지 않을 것입니다. 만약 유효하다면 item변수는 프로세싱을 위해서 Item을 가지고 있을 것입니다. 이것은 유효한 응답으로 변환하는 것은 다음과 같습니다:

// 응답을 리턴하는 함수
() => Full(toResponse(suffix, item))

어떻게 추출이 동작하는지 보기위해서 obejct Item의 unapply메서드를 보겠습니다:

/**
 * String (id)를 Item으로 추출합니다
 */
def unapply(id: String): Option[Item] = Item.find(id)

사실, 전체 Item코드를 보겠습니다. Simply Lift가 약속한 것처럼 명시적으로 퍼시스턴스는 포함하지 않습니다. 이 클래스는 메모리내의 목(mock) 퍼시스턴스 클래스이지만 Lift에서 어떤 다른 퍼시스턴스 메카니즘과 같이 동작합니다.

리스팅 5.2: Item.scala

package code
package model

import net.liftweb._
import util._
import Helpers._
import common._
import json._

import scala.xml.Node

/**
 * 인벤토리내의 한 item
 */
case class Item(id: String, name: String,
            description: String,
            price: BigDecimal, taxable: Boolean,
            weightInGrams: Int, qnty: Int)

/**
 * Item companion object
 */
object Item {
   private implicit val formats = net.liftweb.json.DefaultFormats + BigDecimalSerializer

   private var items: List[Item] = parse(data).extract[List[Item]]

   private var listeners: List[Item => Unit] = Nil

   /**
    * JValue를 가능하다면 Item으로 변환합니다
    */
   def apply(in: JValue): Box[Item] = Helpers.tryo{in.extract[Item]}

   /**
    * String (id)를 Item으로 추출합니다
    */
   def unapply(id: String): Option[Item] = Item.find(id)

   /**
    * JValue를 Item으로 추출합니다
    */
   def unapply(in: JValue): Option[Item] = apply(in)

   /**
    * case 클래스를 위한 기본 unapply 메서드
    * 여기서 그것을 복제(replicate)할 필요가 있습니다.
    * 왜냐하면 unapply 메서드들을 오버로드했기 때문입니다.
    */
   def unapply(in: Any): Option[(String, String,
                              String,
                              BigDecimal, Boolean,
                              Int, Int)] = {
      in match {
         case i: Item => Some((i.id, i.name, i.description,
                              i.price, i.taxable,
                              i.weightInGrams, i.qnty))
         case _ => None
      }
   }

   /**
    * item을 XML로 변환합니다
    */
   implicit def toXml(item: Item): Node = <item>{Xml.toXml(item)}</item>

   /**
    * item을 JSON 형식으로 변환합니다.
    * 이것은 함축적(implicit)이고 companion 오브젝트 내에 있습니다.
    * 그래서 Item은 JSON 호출로부터 쉽게 리턴될 수 있습니다.
    */
   implicit def toJson(item: Item): JValue = Extraction.decompose(item)

   /**
    * Seq[Item]을 JSON 형식으로 변환합니다.
    * 이것은 함축적(implicit)이고 companion 오브젝트 내에 있습니다.
    * 그래서 아이템은 JSON 호출로부터 쉽게 리턴될 수 있습니다.
    */
   implicit def toJson(items: Seq[Item]): JValue = Extraction.decompose(items)

   /**
    * Seq[Item]을 XML 형식으로 변환합니다.
    * 이것은 함축적(implicit)이고 companion 오브젝트 내에 있습니다.
    * 그래서 아이템은 XML REST 호출로부터 쉽게 리턴될 수 있습니다.
    */
   implicit def toXml(items: Seq[Item]): Node =
      <items>{
         items.map(toXml)
      }</items>

   /**
    * 인벤토리내의 모든 아이템들을 얻습니다
    */
   def inventoryItems: Seq[Item] = items

   // 로우(raw) 데이터
   private def data =
"""[
   {"id": "1234", "name": "Cat Food",
   "description": "Yummy, tasty cat food",
   "price": 4.25,
   "taxable": true,
   "weightInGrams": 1000,
   "qnty": 4
   },
   {"id": "1235", "name": "Dog Food",
   "description": "Yummy, tasty dog food",
   "price": 7.25,
   "taxable": true,
   "weightInGrams": 5000,
   "qnty": 72
   },
   {"id": "1236", "name": "Fish Food",
   "description": "Yummy, tasty fish food",
   "price": 2,
   "taxable": false,
   "weightInGrams": 200,
   "qnty": 45
   },
   {"id": "1237", "name": "Sloth Food",
   "description": "Slow, slow sloth food",
   "price": 18.33,
   "taxable": true,
   "weightInGrams": 750,
   "qnty": 62
   },
]
"""

   /**
    * 랜덤 Item을 셀렉트합니다
    */
   def randomItem: Item = synchronized {
      items(Helpers.randomInt(items.length))
   }

   /**
    * id로 item을 찾습니다
    */
   def find(id: String): Box[Item] = synchronize
      items.find(_.id == id)
   }

   /**
    * item을 인벤토리에 추가합니다
    */
   def add(item: Item): Item = {
      synchronized {
         items = item :: items.filterNot(_.id == item.id)
         updateListeners(item)
      }
   }

   /**
    * 이름과 설명에 있는 문자열을 가지고 item을 모두 찾습니다.
    */
   def search(str: String): List[Item] = {
      val strLC = str.toLowerCase()

      items.filter(i =>
         i.name.toLowerCase.indexOf(strLC) >= 0 || i.description.toLowerCase.indexOf(strLC) >= 0)
   }

   /**
    * id를 가진 아이템을 삭제하고 삭제된 아이템이나 
    * 매치된 것이 없으면 Empty를 리턴합니다
    */
   def delete(id: String): Box[Item] = synchronized {
      var ret: Box[Item] = Empty

      val Id = id // 패턴매칭을 위한 안정적인 대문자 ID

      items = items.filter {
         case i@Item(Id, _, _, _, _, _, _) =>
            ret = Full(i) // 사이드 이펙트
            false
         case _ => true
      }

      ret.map(updateListeners)
   }

   /**
    * 데이터가 변경되었을때 리스너를 업데이트합니다
    */
   private def updateListeners(item: Item): Item = {
      synchronized {
         listeners.foreach(f => Schedule.schedule(() => f(item), 0 seconds))

         listeners = Nil
      }
      item
   }

   /**
    * Add an onChange listener
    */
   def onChange(f: Item => Unit) {
      synchronized {
         // 리스너들의 리스트의 앞에 함수를 추가합니다
         listeners ::= f
      }
   }

}

/**
 * A helper that will JSON serialize BigDecimal
 */
object BigDecimalSerializer extends Serializer[BigDecimal] {
   private val Class = classOf[BigDecimal]

   def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), BigDecimal] = {
      case (TypeInfo(Class, _), json) => json match {
         case JInt(iv) => BigDecimal(iv)
         case JDouble(dv) => BigDecimal(dv)
         case value => throw new MappingException("Can't convert " + value + " to " + Class)
      }
   }

   def serialize(implicit format: Formats): PartialFunction[Any, JValue] = {
      case d: BigDecimal => JDouble(d.doubleValue)
   }
}

결과 아웃풋이 무엇인지 보겠습니다:

dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple/item/1234
{
   "id":"1234",
   "name":"Cat Food",
   "description":"Yummy, tasty cat food",
   "price":4.25,
   "taxable":true,
   "weightInGrams":1000,
   "qnty":4
}

dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple/item/1234.xml
<?xml version="1.0" encoding="UTF-8"?>
<item>
   <id>1234</id>
   <name>Cat Food</name>
   <description>Yummy, tasty cat food</description>
   <price>4.25</price>
   <taxable>true</taxable>
   <weightInGrams>1000</weightInGrams>
   <qnty>4</qnty>
</item> 
dpp@raptor:~/proj/simply_lift/samples/http_rest$



5.3 RestHelper로 쉽게 만들기

앞의 예제는 Lift가 REST 호출들을 어떻게 다루는 지 보여줍니다. 하지만 약간 장황합니다. Lift의 RestHelper 트레이트는 읽기 쉽고 유지보수하기 쉽게 코드를 더 간결하게 만들어주는 아주 유용한 숏컷들을 많이 담고 있습니다. 한 뭉치의 예제들을 본 다음 각각을 보겠습니다:

리스팅 5.3: BasicWithHelper.scala

package code
package lib

import model._

import net.liftweb._
import common._
import http._
import rest._
import json._
import scala.xml._

/**
 * 기본 Lift 도구들을 사용한 REST 스타일 인터페이스의 간단한 예제
 */
object BasicWithHelper extends RestHelper {
   /*
    * URL을 제공하지만 item이 발견하지 못하면 404를 리턴했을때 
    * 유용한 에러메시지를 가지고 있습니다.
    */
   serve {
      case "simple3" :: "item" :: itemId :: Nil JsonGet _ =>
         for {
            // item을 찾고 발견되지 않으면
            // 404를 위한 친절한 메시지를 리턴합니다.
            item <- Item.find(itemId) ?~ "Item Not Found"
         } yield item: JValue

      case "simple3" :: "item" :: itemId :: Nil XmlGet _ =>
         for {
            item <- Item.find(itemId) ?~ "Item Not Found"
         } yield item: Node
   }

   serve {
      // 접두사 노테이션
      case JsonGet("simple4" :: "item" :: Item(item) :: Nil, _) =>
         // 명시적으로 LiftResponse를 생성할 필요는 없고
         // 단지 그것은 JSON으로 만들고 RestHelper가 rest를 수행합니다
         item: JValue

      // infix 노테이션
      case "simple4" :: "item" :: Item(item) :: Nil XmlGet _ => item: Node
   }

   // 단일 접두사가 주어진 아이템의 뭉치를 제공합니다
   serve ( "simple5" / "item" prefix {
      // 모든 인벤토리
      case Nil JsonGet _ => Item.inventoryItems: JValue
      case Nil XmlGet _ => Item.inventoryItems: Node

      // 개개의 아이템
      case Item(item) :: Nil JsonGet _ => item: JValue
      case Item(item) :: Nil XmlGet _ => item: Node
   })

   /**
    * 요청의 Accepts 헤더에 따라 어떻게
    * Item에서 JSON이나 XML로 변환하는지가 여기 있습니다
    */
   implicit def itemToResponseByAccepts: JxCvtPF[Item] = {
      case (JsonSelect, c, _) => c: JValue
      case (XmlSelect, c, _) => c: Node
   }

   /**
    * item(이나 Box[Item])을 리턴함으로써 응답을 제공하고
    * RestHelper가 itemToResponseByAccepts 부분 함수를 사용해서
    * LiftResponse로의 변환을 결정하도록 합니다.
    */
   serveJx[Item] {
      case "simple6" :: "item" :: Item(item) :: Nil Get _ => item
      case "simple6" :: "item" :: "other" :: item :: Nil Get _ =>
               Item.find(item) ?~ "The item you're looking for isn't here"
   }

   /**
    * 앞의 serveJx 예제와 동일하지만 
    * 몇번이고 다시 경로 접두사를 복사하는 것을
    * 피하기 위해서 prefixJx를 사용했습니다
    */
   serveJx[Item] {
      "simple7" / "item" prefixJx {
         case Item(item) :: Nil Get _ => item
         case "other" :: item :: Nil Get _ =>
            Item.find(item) ?~ "The item you're looking for isn't here"
      }
   }
}

첫번째 것은 RestHelper기반의 서비스를 어떻게 선언하고 등록하는가 입니다.

/**
 * 기본 Lift 도구들을 사용한 REST 스타일 인터페이스의 간단한 예제
 */
object BasicWithHelper extends RestHelper {

우리의 BaseicWithHelper 싱글톤은 net.liftweb.http.rest.RestHelper 트레이트를 상속받습니다. Boot.scala에서 디스패치를 등록합니다.

LiftRules.statelessDispatchTable.append(BasicWithHelper)

이것은 전체 BasicWithHelper 싱글톤이 그것을 내부에 탐고 있는 모든 서브패턴들을 모으는 PartialFunction[Req, () => Box[LiftResponse]]라는 것을 의미합니다.

serve {
   case "simple3" :: "item" :: itemId :: Nil JsonGet _ =>
      for {
         // item을 찾고 발견되지 않으면
         // 404를 위한 친절한 메시지를 리턴합니다.
         item <- Item.find(itemId) ?~ "Item Not Found"
      } yield item: JValue

   case "simple3" :: "item" :: itemId :: Nil XmlGet _ =>
      for {
         item <- Item.find(itemId) ?~ "Item Not Found"
      } yield item: Node
}

이제 다음을 분석해 보겠습니다:

case "simple3" :: "item" :: itemId :: Nil JsonGet _ =>

위는 /simple3/item/xxx를 매치하고 xxx는 itemId 변수로 추출됩니다. 또한 요청은 JSON을 호출하는 Accepts 헤더를 가져야 합니다.

패턴이 매치되면 다음 코드를 실행합니다.

for {
   // item을 찾고 발견되지 않으면
   // 404를 위한 친절한 메시지를 리턴합니다.
   item <- Item.find(itemId) ?~ "Item Not Found"
} yield item: JValue

Box[LiftResponse]를 돌려주는 함수를 명시적으로 생성하지 않았다는 것을 기억해야 합니다. 대신에 타입은 Box[JValue]입니다. RestHelper는 Box[JValue]에서 () => Box[LiftResponse]로 함축적 컨버전(implicit conversion)을 제공합니다. 명확하게 Box가 Failure이면 RestHelper는 404의 바디로써 Failure 메시지를 가진 404 응답을 생성할 것입니다. Box가 Full이면 RestHelper는 페이로드(payload)내의 값을 가진 JsonResponse를 생성할 것입니다. 두가지 경우를 보겠습니다:

dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple3/item/12999
Item Not Found
?
dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple3/item/1234
{
   "id":"1234",
   "name":"Cat Food",
   "description":"Yummy, tasty cat food",
   "price":4.25,
   "taxable":true,
   "weightInGrams":1000,
   "qnty":4
}

XML 예제는 RestHelper가 XmlResponse로 변환하는 Box[Node]로 응답을 바꾼다는 것 외에는 완전히 동일합니다.

case "simple3" :: "item" :: itemId :: Nil XmlGet _ =>
   for {
      item <- Item.find(itemId) ?~ "Item Not Found"
   } yield item: Node

다음은 결과입니다:

dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl -i -H "Accept: application/xml" http://localhost:8080/simple3/item/1234
HTTP/1.1 200 OK
Expires: Wed, 9 Mar 2011 01:48:38 UTC
Content-Length: 230
Cache-Control: no-cache; private; no-store
Content-Type: text/xml; charset=utf-8
Pragma: no-cache
Date: Wed, 9 Mar 2011 01:48:38 UTC
X-Lift-Version: Unknown Lift Version
Server: Jetty(6.1.22)
?
<?xml version="1.0" encoding="UTF-8"?>
<item>
   <id>1234</id>
   <name>Cat Food</name>
   <description>Yummy, tasty cat food</description>
   <price>4.25</price>
   <taxable>true</taxable>
   <weightInGrams>1000</weightInGrams>
   <qnty>4</qnty>
</item>

server 블럭내에 정의하였고 JValue와 Node를 올바른 응답타입으로의 변환이 처리되기 때문에 더 간단합니다. 단지 함축적 컨버전이 정의된 곳에 대해서 명식되어야 하고 그것들은 Item 싱글톤 내에 있습니다.

/**
 * item을 XML로 변환합니다
 */
implicit def toXml(item: Item): Node = <item>{Xml.toXml(item)}</item>

/**
 * item을 JSON 형식으로 변환합니다.
 * 이것은 함축적(implicit)이고 companion 오브젝트 내에 있습니다.
 * 그래서 Item은 JSON 호출로부터 쉽게 리턴될 수 있습니다.
 */
implicit def toJson(item: Item): JValue = Extraction.decompose(item)

그래서 우리는 더 간단한 REST를 할 수 있습니다. 어떻게 더욱 간단하게 만들수 있는지에 대한 예제를 계속 보겠습니다. 이 예제는 명시적으로 Item.find보다는 추출자(extractor)를 사용합니다.

serve {
   // 접두사 노테이션
   case JsonGet("simple4" :: "item" :: Item(item) :: Nil, _) =>
      // 명시적으로 LiftResponse를 생성할 필요는 없고
      // 단지 그것은 JSON으로 만들고 RestHelper가 rest를 수행합니다
      item: JValue

   // infix 노테이션
   case "simple4" :: "item" :: Item(item) :: Nil XmlGet _ => item: Node
}

만약 DRY를 좋아하고 같은 경로 접두사들이 반복되는 것을 원하지 않는다면 예제처럼 prefix를 사용할 수 있습니다:

// 단일 접두사가 주어진 아이템의 뭉치를 제공합니다
serve ( "simple5" / "item" prefix {
   // 모든 인벤토리
   case Nil JsonGet _ => Item.inventoryItems: JValue
   case Nil XmlGet _ => Item.inventoryItems: Node

   // 개개의 아이템
   case Item(item) :: Nil JsonGet _ => item: JValue
   case Item(item) :: Nil XmlGet _ => item: Node
})

위의 코드는 아래에서 보이는 것 처럼 /simple5/item에 대한 응답에서 모든 아이템을 리스팅할 것이고  /simple5/item/1235에 대한 응답으로 특정한 아이템을 제공할 것입니다.

dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple5/item
[{
   "id":"1234",
   "name":"Cat Food",
   "description":"Yummy, tasty cat food",
   "price":4.25,
   "taxable":true,
   "weightInGrams":1000,
   "qnty":4
},
...
,{
   "id":"1237",
   "name":"Sloth Food",
   "description":"Slow, slow sloth food",
   "price":18.33,
   "taxable":true,
   "weightInGrams":750,
   "qnty":62
}]
?
dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple5/item/1237
{
   "id":"1237",
   "name":"Sloth Food",
   "description":"Slow, slow sloth food",
   "price":18.33,
   "taxable":true,
   "weightInGrams":750,
   "qnty":62
}

위의 예제에서 요청타입에 따라  명시적으로 결과를 JValue나 Node로 바꿉니다.Lift에서는 요청타입에 기반하여 주어진 타입을 응답타입(기본 응답타입은 JSON과 XML입니다)으로 바꾸는 변환을 정의하는 것이 가능하고 그 다음 매치하기 위한 요청 패턴을 정의하고 RestHelper가 rest를 처리합니다. Item에서 JVaue와 Node로의 변환을 정의하겠습니다.(implicit 키워드는 변환이 serveJx 문에서 이용가능하다는 것을 나타냅니다.)

implicit def itemToResponseByAccepts: JxCvtPF[Item] = {
   case (JsonSelect, c, _) => c: JValue
   case (XmlSelect, c, _) => c: Node
}

이것은 아주 직설적입니다. 그것이 JsonSelect라면 JValue를 리턴하고 XmlSelect라면 Node로 변환합니다.
이것은 serveJx문에서 사용됩니다.

serveJx[Item] {
   case "simple6" :: "item" :: Item(item) :: Nil Get _ => item
   case "simple6" :: "item" :: "other" :: item :: Nil Get _ =>
         Item.find(item) ?~ "The item you're looking for isn't here"
}

그래서 /simple6/item/1235는 매치될 것이고 Item을 리턴하고 함축적 컨버전에 기반해서 Item을 JValue나 Node로 Accepts헤더에 따라 바꿀 것입니다. 그 다음 () => Box[LiftResponse]로 변환합니다. curl로 어떤 결과인지 보겠습니다.

dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple6/item/1237
{
   "id":"1237",
   "name":"Sloth Food",
   "description":"Slow, slow sloth food",
   "price":18.33,
   "taxable":true,
   "weightInGrams":750,
   "qnty":62
}
?
dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl -H "Accept: application/xml" http://localhost:8080/simple6/item/1234
<?xml version="1.0" encoding="UTF-8"?>
<item>
   <id>1234</id>
   <name>Cat Food</name>
   <description>Yummy, tasty cat food</description>
   <price>4.25</price>
   <taxable>true</taxable>
   <weightInGrams>1000</weightInGrams>
   <qnty>4</qnty>
</item>  

또한 /simple6/item/other/1234는 올바른 것을 한다는 것을 알아야 합니다. 이것은 경로가 4개 엘리먼트로 길기때문에 패턴의 첫부분에 매치되지 않지만 두번째 부분에 매치됩니다.

마지막으로 serveJx를 합치고 이것이 DRY 헬퍼인 prefixJx입니다.

serveJx[Item] {
   "simple7" / "item" prefixJx {
      case Item(item) :: Nil Get _ => item
      case "other" :: item :: Nil Get _ =>
            Item.find(item) ?~ "The item you're looking for isn't here"
   }
}



5.4 완전한 REST 예제

앞의 코드는 완전히 제구실을 할 REST 서비스로 합칠수 있는 조각들로 제공되었습니다. 합치는 작업을 하고 어떠한 서비스인지 보겠습니다:

리스팅 5.4: FullRest.scala

package code
package lib

import model._

import net.liftweb._
import common._
import http._
import rest._
import util._
import Helpers._
import json._
import scala.xml._

/**
 * 완전한 REST 예제
 */
object FullRest extends RestHelper {

   // /api/item과 그 친구들을 제공합니다
   serve( "api" / "item" prefix {

      // /api/item은 모든 아이템을 리턴합니다
      case Nil JsonGet _ => Item.inventoryItems: JValue

      // /api/item/count는 아이템의 갯수를 얻습니다
      case "count" :: Nil JsonGet _ => JInt(Item.inventoryItems.length)

      // /api/item/item_id는 특정한 아이템(또는 404)를 얻습니다
      case Item(item) :: Nil JsonGet _ => item: JValue

      // /api/item/search/foo이나 /api/item/search?q=foo
      case "search" :: q JsonGet _ =>
         (for {
            searchString <- q ::: S.params("q")
            item <- Item.search(searchString)
         } yield item).distinct: JValue

      // DELETE 질의에 있는 아이템을 삭제합니다
      case Item(item) :: Nil JsonDelete _ => Item.delete(item.id).map(a => a: JValue)

      // PUT JSON이 파싱가능하다면 item을 추가합니다
      case Nil JsonPut Item(item) -> _ => Item.add(item): JValue

      // POST 아이템을 찾았다면 POST의 바디로부터 필드들을 합치고
      // 이이템을 업데이트 합니다.
      case Item(item) :: Nil JsonPost json -> _ =>   
            Item(mergeJson(item, json)).map(Item.add(_): JValue)

      // 아이템의 변화를 기다리지만
      // 비동기적으로 합니다
      case "change" :: Nil JsonGet _ =>
         RestContinuation.async {
            satisfyRequest => {
               // 110초 후에 다른 대답이 없다면"NULL"을 리턴하도록 스케쥴합니다
               Schedule.schedule(() => satisfyRequest(JNull), 110 seconds)

               // "onChange"이벤트를 등록합니다. 
               // 이벤트가 fire되었을 때 응답으로 변경되 아이템이 리턴됩니다.
               Item.onChange(item => satisfyRequest(item: JValue))
            }
         }
   })
}

전체 서비스는 JSON만 사용하고 단일 serve블락을 가지고 있으며 prefix 헬퍼를 서비스의 일부로써 /api/item하의 모든 요청을 정의하려고 사용합니다.

처음 2패턴은 무엇을 이미 커버하고 있는지 재해쉬(re-hash)합니다.

// /api/item은 모든 아이템을 리턴합니다
case Nil JsonGet _ => Item.inventoryItems: JValue

// /api/item/count는 아이템의 갯수를 얻습니다
case "count" :: Nil JsonGet _ => JInt(Item.inventoryItems.length)

// /api/item/item_id는 특정한 아이템(또는 404)를 얻습니다
case Item(item) :: Nil JsonGet _ => item: JValue

다음은 /api/item/search에 있는 검색부분입니다. 재미있는 작은 스칼라 라이브러리를 사용해서 search엘리먼트 다음에 오는 요청 경로 엘리먼트들의 리스트를 생성하고 모든 쿼리파라미터들은 q로 명명됩니다. 이것들에 기반해서 검색부분과 매치되는 모든 Item들을 검색합니다.
결과가 Lift[Item]이 되고 distinct로 중복부분을 제거하고 마지막으로 Lift[Item]을 JValue로 바꿉니다.

// /api/item/search/foo이나 /api/item/search?q=foo
case "search" :: q JsonGet _ =>
   (for {
      searchString <- q ::: S.params("q")
      item <- Item.search(searchString)
   } yield item).distinct: JValue

다음으로 Item을 어떻게 삭제하는지 보겠습니다:

// DELETE 질의에 있는 아이템을 삭제합니다
case Item(item) :: Nil JsonDelete _ => Item.delete(item.id).map(a => a: JValue)

실제로 다른 것은 JsonDelete HTTP요청을 찾는 것 뿐입니다.

PUT으로 Item을 어떻체 추가하는지 보겠습니다:

// PUT JSON이 파싱가능하다면 item을 추가합니다
case Nil JsonPut Item(item) -> _ => Item.add(item): JValue

JsonPut뒤에 Item(item) -> _를 주목하세요. JsonPut에 대한 시그니처 추출은 (List[String], (JValue, Req))입니다. List[String]부분은 간단합니다... 그것은 요청경로를 담고 있는 List입니다. Pair의 두번째 부분은 JValue와 기초를 이루는 Req(이 경우 요청 그 자체와 함께 무언가를 해야할 필요가 있습니다.)를 담고 있는 Pair 그 자체입니다. Item 싱글톤의 def unapply(in: JValue): Option[Item]메서드가 있기 때문에 PUT 요청 바디로부터 만들어진 JValue를 추출(패턴매칭)할 수 있습니다. 이것은 사용자가 패턴매칭이 될 Item으로 변환될 수 있는 JSON blob을 PUT하는 것을 의미하고 인벤토리에 Item을 추가하는 case문의 우변을 평가할 것입니다. 그것은 크고 빽빽한 정보의 더미입니다. 그래서 POST로 다시 시도할 것입니다.

case Item(item) :: Nil JsonPost json -> _ =>
Item(mergeJson(item, json)).map(Item.add(_): JValue)

이 경우에 POST바디에 파싱가능한 JSON을 가지고 있는 /api/item/1234상의 POST를 매치합니다. mergeJson메서드는 찾아낸 Item의 모든 필드들을 취하고 POST바디의 JSON에 있는 필드들로 교체합니다. 그래서 {"qnty": 123} 의 POST 바디는 Item의 qnty필드를 교체할 것입니다. Item은 저장소(backing store)에 추가됩니다.

Cool. 그래서 우리의 REST서비스에서 GET, DELETE, PUST, POST 지원의 변화를 가졌습니다. 모두 패턴을 사용해서 RestHelper가 제공하는 것입니다.

여기서 약간 재미있는 것이 있습니다.

Lift의 HTML부분의 특징들중 하나는 Comet(long-polling을 통한 서버 푸쉬)에 대한 지원입니다. 웹 컨테이너가 그것을 지원한다면 Lift는 자동적으로 비동기적인 지원을 사용할 것입니다. 이것은 long poll중에 요청의 서비스와 연관되어 수행되고 있는 계산이 없더라도 소비될 쓰레드가 없다는 것을 의미합니다. 이것은 아주 많은 수의 롱폴링 클라이언트를 허용합니다. Lift의 REST지원은 비동기적인 지원을 포함합니다. 이 경우네 HTTP요청이 /api/item/change로 열리고 저장소의 변화를 기다리는 것을 보여주겠습니다. 요청은 저장소의 변화나 110초후에 JSON JNull로 만족될 것입니다.

case "change" :: Nil JsonGet _ =>
   RestContinuation.async {
      satisfyRequest => {
         // 110초 후에 다른 대답이 없다면"NULL"을 리턴하도록 스케쥴합니다
         Schedule.schedule(() => satisfyRequest(JNull), 110 seconds)

         // "onChange"이벤트를 등록합니다. 
         // 이벤트가 fire되었을 때 응답으로 변경되 아이템이 리턴됩니다.
         Item.onChange(item => satisfyRequest(item: JValue))
      }
   }

/api/item/change에 대한 GET요청을 받았을 때 RestContinuation.async를 호출합니다. 호출을 설정하는 클로져를 건네줍니다. 110초후에 보내질 JNull 스케쥴링에 의한 호출을 설정합니다. 또한 저장소에서 변경사항이 생겼을때 호출되는 함수를 등록합니다. 이벤트(110초가 지나거나 저장소가 변경되었을때)가 발생했을때 함수들은 호출될 것이고 continuation을 호출하고 클라이언트에게 응답을 다시 보낼 satifyRequest 함수를 적용할 것입니다. 이 메카니즘을 사용해서 서버에서 쓰레드를 소비하지 않는 롱폴링 서비스를 만들 수 있습니다. satifyRequest 함수가 한번 호출되면 그것을 많이 호출할 수 있지만 오직 첫번째만 카운트 된다는 것을 유념하세요.



5.5 결론

이번 장에서 Lift에서 어떻게 웹서비스를 생성하는 지를 보았습니다. RestHelper가 커버하는 부분하에 많은 함축적 컨버전이 있어서 결과 코드는 아주 읽고, 만들고, 유지하기 쉽습니다. 코어부분에서 패턴에 대해서 패턴이 매치된다면 들어오는 요청을 매치하고 패턴의 우변의 표현식을 평가합니다.
2011/04/11 04:19 2011/04/11 04:19