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:
1package code
2package lib
3
4import model._
5
6import net.liftweb._
7import common._
8import http._
9
10/**
11 * 기본적인 Lift 도구들을 사용하는
12 * REST 스타일 인터페이스의 간단한 예제
13 */
14object BasicExample {
15 /*
16 * 주어진 접미사(suffix)와 아이템, LiftResponse를 만듭니다
17 */
18 private def toResponse(suffix: String, item: Item) =
19 suffix match {
20 case "xml" => XmlResponse(item)
21 case _ => JsonResponse(item)
22 }
23
24 /**
25 * /simple/item/1234.json를 찾습니다
26 * /simple/item/1234.xml를 찾습니다
27 */
28 lazy val findItem: LiftRules.DispatchPF = {
29 case Req("simple" :: "item" :: itemId :: Nil, // 경로(path)
30 suffix, // 접미사(suffix)
31 GetRequest) =>
32 () => Item.find(itemId).map(toResponse(suffix, _))
33 }
34
35 /**
36 * /simple2/item/1234.json를 찾습니다
37 */
38 lazy val extractFindItem: LiftRules.DispatchPF = {
39 // 추출자(extractor)를 가진 경로
40 case Req("simple2" :: "item" :: Item(item) :: Nil,
41 suffix, GetRequest) =>
42 // 응답을 리턴하는 함수
43 () => Full(toResponse(suffix, item))
44 }
45}
퍼즐의 추가적인 조각은 핸들러를 후킹하는 것입니다. 이것은 다음 코드처럼 Boot.scala에서 이루어집니다.
1// the stateless REST handlers
2// 무상태 REST 핸들러
3LiftRules.statelessDispatchTable.append(BasicExample.findItem)
4LiftRules.statelessDispatchTable.append(BasicExample.extractFindItem)
5
6// 동일한 상태를 가진 버전
7// LiftRules.dispatch.append(BasicExample.findItem)
8// LiftRules.dispatch.append(BasicExample.extractFindItem)
코드를 보겠습니다. 먼저 각 핸들러는 PartialFunction[Req, () => Box[LiftResponse]]이지만 부분함수(partial function)의 별칭인 Scala 타입인 LiftRules.dispatchPF의 약칙을 사용할 수 있습니다.
1lazy val findItem: LiftRules.DispatchPF =
요청 디스패처 핸들러의 타입 시그니처를 가진 findItem을 정의합니다.
1case Req("simple" :: "item" :: itemId :: Nil, // 경로(path)
2 suffix, // suffix
3 GetRequest) =>
매칭을 위해서 패턴을 정의합니다. 이 경우에 처음 두 단계가 /simple/item을 가진 어떤 3단계의 경로가 매치됩니다. 경로의 3번째부분은 변수 itemId로 추출될 것입니다. 마지막 경로 아이템의 접미사는 suffix 변수로 추출될 것이고 요청은 반드시 GET이어야 합니다.
위의 기준(criteria)을 만났다면 그 다음 부분함수(partial function)이 정의되고 Lift는 () => Box[LiftResponse]의 결과를 얻기 위해서 부분함수를 적용할 것입니다.
1() => Item.find(itemId).map(toResponse(suffix, _))
이것은 itemId를 찾고 결과 Item을 요청 접미사를 기반으로 응답으로 변환하는 함수입니다. toResponse 메서드는 다음과 같습니다:
1/*
2 * Given a suffix and an item, make a LiftResponse
3 * 주어진 접미사와 아이템으로 LiftResponse를 만듭니다.
4 */
5private def toResponse(suffix: String, item: Item) =
6 suffix match {
7 case "xml" => XmlResponse(item)
8 case _ => JsonResponse(item)
9}
약간 장황하더라도 아주 직설적입니다. 이 파일에서 다른 예제를 보겠습니다. 요청경로의 세번째 아이템의 String를 Item으로 변환하려고 추출자(extractor)를 사용합니다.
1// 추출자(extractor)를 가진 경로
2case Req("simple2" :: "item" :: Item(item) :: Nil,
3 suffix, GetRequest) =>
이 경우에 경로의 세번째 엘리먼트가 유효한 Item이 아닌 한 패턴은 매치되지 않을 것입니다. 만약 유효하다면 item변수는 프로세싱을 위해서 Item을 가지고 있을 것입니다. 이것은 유효한 응답으로 변환하는 것은 다음과 같습니다:
1// 응답을 리턴하는 함수
2() => Full(toResponse(suffix, item))
어떻게 추출이 동작하는지 보기위해서 obejct Item의 unapply메서드를 보겠습니다:
1/**
2 * String (id)를 Item으로 추출합니다
3 */
4def unapply(id: String): Option[Item] = Item.find(id)
사실, 전체 Item코드를 보겠습니다. Simply Lift가 약속한 것처럼 명시적으로 퍼시스턴스는 포함하지 않습니다. 이 클래스는 메모리내의 목(mock) 퍼시스턴스 클래스이지만 Lift에서 어떤 다른 퍼시스턴스 메카니즘과 같이 동작합니다.
리스팅 5.2: Item.scala
1package code
2package model
3
4import net.liftweb._
5import util._
6import Helpers._
7import common._
8import json._
9
10import scala.xml.Node
11
12/**
13 * 인벤토리내의 한 item
14 */
15case class Item(id: String, name: String,
16 description: String,
17 price: BigDecimal, taxable: Boolean,
18 weightInGrams: Int, qnty: Int)
19
20/**
21 * Item companion object
22 */
23object Item {
24 private implicit val formats = net.liftweb.json.DefaultFormats + BigDecimalSerializer
25
26 private var items: List[Item] = parse(data).extract[List[Item]]
27
28 private var listeners: List[Item => Unit] = Nil
29
30 /**
31 * JValue를 가능하다면 Item으로 변환합니다
32 */
33 def apply(in: JValue): Box[Item] = Helpers.tryo{in.extract[Item]}
34
35 /**
36 * String (id)를 Item으로 추출합니다
37 */
38 def unapply(id: String): Option[Item] = Item.find(id)
39
40 /**
41 * JValue를 Item으로 추출합니다
42 */
43 def unapply(in: JValue): Option[Item] = apply(in)
44
45 /**
46 * case 클래스를 위한 기본 unapply 메서드
47 * 여기서 그것을 복제(replicate)할 필요가 있습니다.
48 * 왜냐하면 unapply 메서드들을 오버로드했기 때문입니다.
49 */
50 def unapply(in: Any): Option[(String, String,
51 String,
52 BigDecimal, Boolean,
53 Int, Int)] = {
54 in match {
55 case i: Item => Some((i.id, i.name, i.description,
56 i.price, i.taxable,
57 i.weightInGrams, i.qnty))
58 case _ => None
59 }
60 }
61
62 /**
63 * item을 XML로 변환합니다
64 */
65 implicit def toXml(item: Item): Node = <item>{Xml.toXml(item)}</item>
66
67 /**
68 * item을 JSON 형식으로 변환합니다.
69 * 이것은 함축적(implicit)이고 companion 오브젝트 내에 있습니다.
70 * 그래서 Item은 JSON 호출로부터 쉽게 리턴될 수 있습니다.
71 */
72 implicit def toJson(item: Item): JValue = Extraction.decompose(item)
73
74 /**
75 * Seq[Item]을 JSON 형식으로 변환합니다.
76 * 이것은 함축적(implicit)이고 companion 오브젝트 내에 있습니다.
77 * 그래서 아이템은 JSON 호출로부터 쉽게 리턴될 수 있습니다.
78 */
79 implicit def toJson(items: Seq[Item]): JValue = Extraction.decompose(items)
80
81 /**
82 * Seq[Item]을 XML 형식으로 변환합니다.
83 * 이것은 함축적(implicit)이고 companion 오브젝트 내에 있습니다.
84 * 그래서 아이템은 XML REST 호출로부터 쉽게 리턴될 수 있습니다.
85 */
86 implicit def toXml(items: Seq[Item]): Node =
87 <items>{
88 items.map(toXml)
89 }</items>
90
91 /**
92 * 인벤토리내의 모든 아이템들을 얻습니다
93 */
94 def inventoryItems: Seq[Item] = items
95
96 // 로우(raw) 데이터
97 private def data =
98"""[
99 {"id": "1234", "name": "Cat Food",
100 "description": "Yummy, tasty cat food",
101 "price": 4.25,
102 "taxable": true,
103 "weightInGrams": 1000,
104 "qnty": 4
105 },
106 {"id": "1235", "name": "Dog Food",
107 "description": "Yummy, tasty dog food",
108 "price": 7.25,
109 "taxable": true,
110 "weightInGrams": 5000,
111 "qnty": 72
112 },
113 {"id": "1236", "name": "Fish Food",
114 "description": "Yummy, tasty fish food",
115 "price": 2,
116 "taxable": false,
117 "weightInGrams": 200,
118 "qnty": 45
119 },
120 {"id": "1237", "name": "Sloth Food",
121 "description": "Slow, slow sloth food",
122 "price": 18.33,
123 "taxable": true,
124 "weightInGrams": 750,
125 "qnty": 62
126 },
127]
128"""
129
130 /**
131 * 랜덤 Item을 셀렉트합니다
132 */
133 def randomItem: Item = synchronized {
134 items(Helpers.randomInt(items.length))
135 }
136
137 /**
138 * id로 item을 찾습니다
139 */
140 def find(id: String): Box[Item] = synchronize
141 items.find(_.id == id)
142 }
143
144 /**
145 * item을 인벤토리에 추가합니다
146 */
147 def add(item: Item): Item = {
148 synchronized {
149 items = item :: items.filterNot(_.id == item.id)
150 updateListeners(item)
151 }
152 }
153
154 /**
155 * 이름과 설명에 있는 문자열을 가지고 item을 모두 찾습니다.
156 */
157 def search(str: String): List[Item] = {
158 val strLC = str.toLowerCase()
159
160 items.filter(i =>
161 i.name.toLowerCase.indexOf(strLC) >= 0 || i.description.toLowerCase.indexOf(strLC) >= 0)
162 }
163
164 /**
165 * id를 가진 아이템을 삭제하고 삭제된 아이템이나
166 * 매치된 것이 없으면 Empty를 리턴합니다
167 */
168 def delete(id: String): Box[Item] = synchronized {
169 var ret: Box[Item] = Empty
170
171 val Id = id // 패턴매칭을 위한 안정적인 대문자 ID
172
173 items = items.filter {
174 case i@Item(Id, _, _, _, _, _, _) =>
175 ret = Full(i) // 사이드 이펙트
176 false
177 case _ => true
178 }
179
180 ret.map(updateListeners)
181 }
182
183 /**
184 * 데이터가 변경되었을때 리스너를 업데이트합니다
185 */
186 private def updateListeners(item: Item): Item = {
187 synchronized {
188 listeners.foreach(f => Schedule.schedule(() => f(item), 0 seconds))
189
190 listeners = Nil
191 }
192 item
193 }
194
195 /**
196 * Add an onChange listener
197 */
198 def onChange(f: Item => Unit) {
199 synchronized {
200 // 리스너들의 리스트의 앞에 함수를 추가합니다
201 listeners ::= f
202 }
203 }
204
205}
206
207/**
208 * A helper that will JSON serialize BigDecimal
209 */
210object BigDecimalSerializer extends Serializer[BigDecimal] {
211 private val Class = classOf[BigDecimal]
212
213 def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), BigDecimal] = {
214 case (TypeInfo(Class, _), json) => json match {
215 case JInt(iv) => BigDecimal(iv)
216 case JDouble(dv) => BigDecimal(dv)
217 case value => throw new MappingException("Can't convert " + value + " to " + Class)
218 }
219 }
220
221 def serialize(implicit format: Formats): PartialFunction[Any, JValue] = {
222 case d: BigDecimal => JDouble(d.doubleValue)
223 }
224}
결과 아웃풋이 무엇인지 보겠습니다:
1dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple/item/1234
2{
3 "id":"1234",
4 "name":"Cat Food",
5 "description":"Yummy, tasty cat food",
6 "price":4.25,
7 "taxable":true,
8 "weightInGrams":1000,
9 "qnty":4
10}
11
12dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple/item/1234.xml
13<?xml version="1.0" encoding="UTF-8"?>
14<item>
15 <id>1234</id>
16 <name>Cat Food</name>
17 <description>Yummy, tasty cat food</description>
18 <price>4.25</price>
19 <taxable>true</taxable>
20 <weightInGrams>1000</weightInGrams>
21 <qnty>4</qnty>
22</item>
23dpp@raptor:~/proj/simply_lift/samples/http_rest$
5.3 RestHelper로 쉽게 만들기
앞의 예제는 Lift가 REST 호출들을 어떻게 다루는 지 보여줍니다. 하지만 약간 장황합니다. Lift의 RestHelper 트레이트는 읽기 쉽고 유지보수하기 쉽게 코드를 더 간결하게 만들어주는 아주 유용한 숏컷들을 많이 담고 있습니다. 한 뭉치의 예제들을 본 다음 각각을 보겠습니다:
리스팅 5.3: BasicWithHelper.scala
1package code
2package lib
3
4import model._
5
6import net.liftweb._
7import common._
8import http._
9import rest._
10import json._
11import scala.xml._
12
13/**
14 * 기본 Lift 도구들을 사용한 REST 스타일 인터페이스의 간단한 예제
15 */
16object BasicWithHelper extends RestHelper {
17 /*
18 * URL을 제공하지만 item이 발견하지 못하면 404를 리턴했을때
19 * 유용한 에러메시지를 가지고 있습니다.
20 */
21 serve {
22 case "simple3" :: "item" :: itemId :: Nil JsonGet _ =>
23 for {
24 // item을 찾고 발견되지 않으면
25 // 404를 위한 친절한 메시지를 리턴합니다.
26 item <- Item.find(itemId) ?~ "Item Not Found"
27 } yield item: JValue
28
29 case "simple3" :: "item" :: itemId :: Nil XmlGet _ =>
30 for {
31 item <- Item.find(itemId) ?~ "Item Not Found"
32 } yield item: Node
33 }
34
35 serve {
36 // 접두사 노테이션
37 case JsonGet("simple4" :: "item" :: Item(item) :: Nil, _) =>
38 // 명시적으로 LiftResponse를 생성할 필요는 없고
39 // 단지 그것은 JSON으로 만들고 RestHelper가 rest를 수행합니다
40 item: JValue
41
42 // infix 노테이션
43 case "simple4" :: "item" :: Item(item) :: Nil XmlGet _ => item: Node
44 }
45
46 // 단일 접두사가 주어진 아이템의 뭉치를 제공합니다
47 serve ( "simple5" / "item" prefix {
48 // 모든 인벤토리
49 case Nil JsonGet _ => Item.inventoryItems: JValue
50 case Nil XmlGet _ => Item.inventoryItems: Node
51
52 // 개개의 아이템
53 case Item(item) :: Nil JsonGet _ => item: JValue
54 case Item(item) :: Nil XmlGet _ => item: Node
55 })
56
57 /**
58 * 요청의 Accepts 헤더에 따라 어떻게
59 * Item에서 JSON이나 XML로 변환하는지가 여기 있습니다
60 */
61 implicit def itemToResponseByAccepts: JxCvtPF[Item] = {
62 case (JsonSelect, c, _) => c: JValue
63 case (XmlSelect, c, _) => c: Node
64 }
65
66 /**
67 * item(이나 Box[Item])을 리턴함으로써 응답을 제공하고
68 * RestHelper가 itemToResponseByAccepts 부분 함수를 사용해서
69 * LiftResponse로의 변환을 결정하도록 합니다.
70 */
71 serveJx[Item] {
72 case "simple6" :: "item" :: Item(item) :: Nil Get _ => item
73 case "simple6" :: "item" :: "other" :: item :: Nil Get _ =>
74 Item.find(item) ?~ "The item you're looking for isn't here"
75 }
76
77 /**
78 * 앞의 serveJx 예제와 동일하지만
79 * 몇번이고 다시 경로 접두사를 복사하는 것을
80 * 피하기 위해서 prefixJx를 사용했습니다
81 */
82 serveJx[Item] {
83 "simple7" / "item" prefixJx {
84 case Item(item) :: Nil Get _ => item
85 case "other" :: item :: Nil Get _ =>
86 Item.find(item) ?~ "The item you're looking for isn't here"
87 }
88 }
89}
첫번째 것은 RestHelper기반의 서비스를 어떻게 선언하고 등록하는가 입니다.
1/**
2 * 기본 Lift 도구들을 사용한 REST 스타일 인터페이스의 간단한 예제
3 */
4object BasicWithHelper extends RestHelper {
우리의 BaseicWithHelper 싱글톤은 net.liftweb.http.rest.RestHelper 트레이트를 상속받습니다. Boot.scala에서 디스패치를 등록합니다.
1LiftRules.statelessDispatchTable.append(BasicWithHelper)
이것은 전체 BasicWithHelper 싱글톤이 그것을 내부에 탐고 있는 모든 서브패턴들을 모으는 PartialFunction[Req, () => Box[LiftResponse]]라는 것을 의미합니다.
1serve {
2 case "simple3" :: "item" :: itemId :: Nil JsonGet _ =>
3 for {
4 // item을 찾고 발견되지 않으면
5 // 404를 위한 친절한 메시지를 리턴합니다.
6 item <- Item.find(itemId) ?~ "Item Not Found"
7 } yield item: JValue
8
9 case "simple3" :: "item" :: itemId :: Nil XmlGet _ =>
10 for {
11 item <- Item.find(itemId) ?~ "Item Not Found"
12 } yield item: Node
13}
이제 다음을 분석해 보겠습니다:
1case "simple3" :: "item" :: itemId :: Nil JsonGet _ =>
위는 /simple3/item/xxx를 매치하고 xxx는 itemId 변수로 추출됩니다. 또한 요청은 JSON을 호출하는 Accepts 헤더를 가져야 합니다.
패턴이 매치되면 다음 코드를 실행합니다.
1for {
2 // item을 찾고 발견되지 않으면
3 // 404를 위한 친절한 메시지를 리턴합니다.
4 item <- Item.find(itemId) ?~ "Item Not Found"
5} 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를 생성할 것입니다. 두가지 경우를 보겠습니다:
1dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple3/item/12999
2Item Not Found
3?
4dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple3/item/1234
5{
6 "id":"1234",
7 "name":"Cat Food",
8 "description":"Yummy, tasty cat food",
9 "price":4.25,
10 "taxable":true,
11 "weightInGrams":1000,
12 "qnty":4
13}
XML 예제는 RestHelper가 XmlResponse로 변환하는 Box[Node]로 응답을 바꾼다는 것 외에는 완전히 동일합니다.
1case "simple3" :: "item" :: itemId :: Nil XmlGet _ =>
2 for {
3 item <- Item.find(itemId) ?~ "Item Not Found"
4 } yield item: Node
다음은 결과입니다:
1dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl -i -H "Accept: application/xml" http://localhost:8080/simple3/item/1234
2HTTP/1.1 200 OK
3Expires: Wed, 9 Mar 2011 01:48:38 UTC
4Content-Length: 230
5Cache-Control: no-cache; private; no-store
6Content-Type: text/xml; charset=utf-8
7Pragma: no-cache
8Date: Wed, 9 Mar 2011 01:48:38 UTC
9X-Lift-Version: Unknown Lift Version
10Server: Jetty(6.1.22)
11?
12<?xml version="1.0" encoding="UTF-8"?>
13<item>
14 <id>1234</id>
15 <name>Cat Food</name>
16 <description>Yummy, tasty cat food</description>
17 <price>4.25</price>
18 <taxable>true</taxable>
19 <weightInGrams>1000</weightInGrams>
20 <qnty>4</qnty>
21</item>
server 블럭내에 정의하였고 JValue와 Node를 올바른 응답타입으로의 변환이 처리되기 때문에 더 간단합니다. 단지 함축적 컨버전이 정의된 곳에 대해서 명식되어야 하고 그것들은 Item 싱글톤 내에 있습니다.
1/**
2 * item을 XML로 변환합니다
3 */
4implicit def toXml(item: Item): Node = <item>{Xml.toXml(item)}</item>
5
6/**
7 * item을 JSON 형식으로 변환합니다.
8 * 이것은 함축적(implicit)이고 companion 오브젝트 내에 있습니다.
9 * 그래서 Item은 JSON 호출로부터 쉽게 리턴될 수 있습니다.
10 */
11implicit def toJson(item: Item): JValue = Extraction.decompose(item)
그래서 우리는 더 간단한 REST를 할 수 있습니다. 어떻게 더욱 간단하게 만들수 있는지에 대한 예제를 계속 보겠습니다. 이 예제는 명시적으로 Item.find보다는 추출자(extractor)를 사용합니다.
1serve {
2 // 접두사 노테이션
3 case JsonGet("simple4" :: "item" :: Item(item) :: Nil, _) =>
4 // 명시적으로 LiftResponse를 생성할 필요는 없고
5 // 단지 그것은 JSON으로 만들고 RestHelper가 rest를 수행합니다
6 item: JValue
7
8 // infix 노테이션
9 case "simple4" :: "item" :: Item(item) :: Nil XmlGet _ => item: Node
10}
만약 DRY를 좋아하고 같은 경로 접두사들이 반복되는 것을 원하지 않는다면 예제처럼 prefix를 사용할 수 있습니다:
1// 단일 접두사가 주어진 아이템의 뭉치를 제공합니다
2serve ( "simple5" / "item" prefix {
3 // 모든 인벤토리
4 case Nil JsonGet _ => Item.inventoryItems: JValue
5 case Nil XmlGet _ => Item.inventoryItems: Node
6
7 // 개개의 아이템
8 case Item(item) :: Nil JsonGet _ => item: JValue
9 case Item(item) :: Nil XmlGet _ => item: Node
10})
위의 코드는 아래에서 보이는 것 처럼 /simple5/item에 대한 응답에서 모든 아이템을 리스팅할 것이고 /simple5/item/1235에 대한 응답으로 특정한 아이템을 제공할 것입니다.
1dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple5/item
2[{
3 "id":"1234",
4 "name":"Cat Food",
5 "description":"Yummy, tasty cat food",
6 "price":4.25,
7 "taxable":true,
8 "weightInGrams":1000,
9 "qnty":4
10},
11...
12,{
13 "id":"1237",
14 "name":"Sloth Food",
15 "description":"Slow, slow sloth food",
16 "price":18.33,
17 "taxable":true,
18 "weightInGrams":750,
19 "qnty":62
20}]
21?
22dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple5/item/1237
23{
24 "id":"1237",
25 "name":"Sloth Food",
26 "description":"Slow, slow sloth food",
27 "price":18.33,
28 "taxable":true,
29 "weightInGrams":750,
30 "qnty":62
31}
위의 예제에서 요청타입에 따라 명시적으로 결과를 JValue나 Node로 바꿉니다.Lift에서는 요청타입에 기반하여 주어진 타입을 응답타입(기본 응답타입은 JSON과 XML입니다)으로 바꾸는 변환을 정의하는 것이 가능하고 그 다음 매치하기 위한 요청 패턴을 정의하고 RestHelper가 rest를 처리합니다. Item에서 JVaue와 Node로의 변환을 정의하겠습니다.(implicit 키워드는 변환이 serveJx 문에서 이용가능하다는 것을 나타냅니다.)
1implicit def itemToResponseByAccepts: JxCvtPF[Item] = {
2 case (JsonSelect, c, _) => c: JValue
3 case (XmlSelect, c, _) => c: Node
4}
이것은 아주 직설적입니다. 그것이 JsonSelect라면 JValue를 리턴하고 XmlSelect라면 Node로 변환합니다.
이것은 serveJx문에서 사용됩니다.
1serveJx[Item] {
2 case "simple6" :: "item" :: Item(item) :: Nil Get _ => item
3 case "simple6" :: "item" :: "other" :: item :: Nil Get _ =>
4 Item.find(item) ?~ "The item you're looking for isn't here"
5}
그래서 /simple6/item/1235는 매치될 것이고 Item을 리턴하고 함축적 컨버전에 기반해서 Item을 JValue나 Node로 Accepts헤더에 따라 바꿀 것입니다. 그 다음 () => Box[LiftResponse]로 변환합니다. curl로 어떤 결과인지 보겠습니다.
1dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple6/item/1237
2{
3 "id":"1237",
4 "name":"Sloth Food",
5 "description":"Slow, slow sloth food",
6 "price":18.33,
7 "taxable":true,
8 "weightInGrams":750,
9 "qnty":62
10}
11?
12dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl -H "Accept: application/xml" http://localhost:8080/simple6/item/1234
13<?xml version="1.0" encoding="UTF-8"?>
14<item>
15 <id>1234</id>
16 <name>Cat Food</name>
17 <description>Yummy, tasty cat food</description>
18 <price>4.25</price>
19 <taxable>true</taxable>
20 <weightInGrams>1000</weightInGrams>
21 <qnty>4</qnty>
22</item>
또한 /simple6/item/other/1234는 올바른 것을 한다는 것을 알아야 합니다. 이것은 경로가 4개 엘리먼트로 길기때문에 패턴의 첫부분에 매치되지 않지만 두번째 부분에 매치됩니다.
마지막으로 serveJx를 합치고 이것이 DRY 헬퍼인 prefixJx입니다.
1serveJx[Item] {
2 "simple7" / "item" prefixJx {
3 case Item(item) :: Nil Get _ => item
4 case "other" :: item :: Nil Get _ =>
5 Item.find(item) ?~ "The item you're looking for isn't here"
6 }
7}
5.4 완전한 REST 예제
앞의 코드는 완전히 제구실을 할 REST 서비스로 합칠수 있는 조각들로 제공되었습니다. 합치는 작업을 하고 어떠한 서비스인지 보겠습니다:
리스팅 5.4: FullRest.scala
1package code
2package lib
3
4import model._
5
6import net.liftweb._
7import common._
8import http._
9import rest._
10import util._
11import Helpers._
12import json._
13import scala.xml._
14
15/**
16 * 완전한 REST 예제
17 */
18object FullRest extends RestHelper {
19
20 // /api/item과 그 친구들을 제공합니다
21 serve( "api" / "item" prefix {
22
23 // /api/item은 모든 아이템을 리턴합니다
24 case Nil JsonGet _ => Item.inventoryItems: JValue
25
26 // /api/item/count는 아이템의 갯수를 얻습니다
27 case "count" :: Nil JsonGet _ => JInt(Item.inventoryItems.length)
28
29 // /api/item/item_id는 특정한 아이템(또는 404)를 얻습니다
30 case Item(item) :: Nil JsonGet _ => item: JValue
31
32 // /api/item/search/foo이나 /api/item/search?q=foo
33 case "search" :: q JsonGet _ =>
34 (for {
35 searchString <- q ::: S.params("q")
36 item <- Item.search(searchString)
37 } yield item).distinct: JValue
38
39 // DELETE 질의에 있는 아이템을 삭제합니다
40 case Item(item) :: Nil JsonDelete _ => Item.delete(item.id).map(a => a: JValue)
41
42 // PUT JSON이 파싱가능하다면 item을 추가합니다
43 case Nil JsonPut Item(item) -> _ => Item.add(item): JValue
44
45 // POST 아이템을 찾았다면 POST의 바디로부터 필드들을 합치고
46 // 이이템을 업데이트 합니다.
47 case Item(item) :: Nil JsonPost json -> _ =>
48 Item(mergeJson(item, json)).map(Item.add(_): JValue)
49
50 // 아이템의 변화를 기다리지만
51 // 비동기적으로 합니다
52 case "change" :: Nil JsonGet _ =>
53 RestContinuation.async {
54 satisfyRequest => {
55 // 110초 후에 다른 대답이 없다면"NULL"을 리턴하도록 스케쥴합니다
56 Schedule.schedule(() => satisfyRequest(JNull), 110 seconds)
57
58 // "onChange"이벤트를 등록합니다.
59 // 이벤트가 fire되었을 때 응답으로 변경되 아이템이 리턴됩니다.
60 Item.onChange(item => satisfyRequest(item: JValue))
61 }
62 }
63 })
64}
전체 서비스는 JSON만 사용하고 단일 serve블락을 가지고 있으며 prefix 헬퍼를 서비스의 일부로써 /api/item하의 모든 요청을 정의하려고 사용합니다.
처음 2패턴은 무엇을 이미 커버하고 있는지 재해쉬(re-hash)합니다.
1// /api/item은 모든 아이템을 리턴합니다
2case Nil JsonGet _ => Item.inventoryItems: JValue
3
4// /api/item/count는 아이템의 갯수를 얻습니다
5case "count" :: Nil JsonGet _ => JInt(Item.inventoryItems.length)
6
7// /api/item/item_id는 특정한 아이템(또는 404)를 얻습니다
8case Item(item) :: Nil JsonGet _ => item: JValue
다음은 /api/item/search에 있는 검색부분입니다. 재미있는 작은 스칼라 라이브러리를 사용해서 search엘리먼트 다음에 오는 요청 경로 엘리먼트들의 리스트를 생성하고 모든 쿼리파라미터들은 q로 명명됩니다. 이것들에 기반해서 검색부분과 매치되는 모든 Item들을 검색합니다.
결과가 Lift[Item]이 되고 distinct로 중복부분을 제거하고 마지막으로 Lift[Item]을 JValue로 바꿉니다.
1// /api/item/search/foo이나 /api/item/search?q=foo
2case "search" :: q JsonGet _ =>
3 (for {
4 searchString <- q ::: S.params("q")
5 item <- Item.search(searchString)
6 } yield item).distinct: JValue
다음으로 Item을 어떻게 삭제하는지 보겠습니다:
1// DELETE 질의에 있는 아이템을 삭제합니다
2case Item(item) :: Nil JsonDelete _ => Item.delete(item.id).map(a => a: JValue)
실제로 다른 것은 JsonDelete HTTP요청을 찾는 것 뿐입니다.
PUT으로 Item을 어떻체 추가하는지 보겠습니다:
1// PUT JSON이 파싱가능하다면 item을 추가합니다
2case 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로 다시 시도할 것입니다.
1case Item(item) :: Nil JsonPost json -> _ =>
2Item(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로 만족될 것입니다.
1case "change" :: Nil JsonGet _ =>
2 RestContinuation.async {
3 satisfyRequest => {
4 // 110초 후에 다른 대답이 없다면"NULL"을 리턴하도록 스케쥴합니다
5 Schedule.schedule(() => satisfyRequest(JNull), 110 seconds)
6
7 // "onChange"이벤트를 등록합니다.
8 // 이벤트가 fire되었을 때 응답으로 변경되 아이템이 리턴됩니다.
9 Item.onChange(item => satisfyRequest(item: JValue))
10 }
11 }
/api/item/change에 대한 GET요청을 받았을 때 RestContinuation.async를 호출합니다. 호출을 설정하는 클로져를 건네줍니다. 110초후에 보내질 JNull 스케쥴링에 의한 호출을 설정합니다. 또한 저장소에서 변경사항이 생겼을때 호출되는 함수를 등록합니다. 이벤트(110초가 지나거나 저장소가 변경되었을때)가 발생했을때 함수들은 호출될 것이고 continuation을 호출하고 클라이언트에게 응답을 다시 보낼 satifyRequest 함수를 적용할 것입니다. 이 메카니즘을 사용해서 서버에서 쓰레드를 소비하지 않는 롱폴링 서비스를 만들 수 있습니다. satifyRequest 함수가 한번 호출되면 그것을 많이 호출할 수 있지만 오직 첫번째만 카운트 된다는 것을 유념하세요.
5.5 결론
이번 장에서 Lift에서 어떻게 웹서비스를 생성하는 지를 보았습니다. RestHelper가 커버하는 부분하에 많은 함축적 컨버전이 있어서 결과 코드는 아주 읽고, 만들고, 유지하기 쉽습니다. 코어부분에서 패턴에 대해서 패턴이 매치된다면 들어오는 요청을 매치하고 패턴의 우변의 표현식을 평가합니다.
Comments