Outsider's Dev Story

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

Simply Lift : Chapter 8 - Common Patterns

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

8장
공통 패턴들(Common Patterns)


8.1 지역화 (Localization)

Lift는 페이지와 엘리먼트 레벨에서 폭넓게 지역화를 지원하고 있습니다.



8.1.1 템플릿 지역화 (Localizing Templates)

현재 요청의 로케일(locale)은 LiftRules.localeCalculator에서 함수에 기반하여 계산됩니다. 기본적으로 함수는 HTTP 요청의 로케일을 봅니다. 하지만 LiftRules.localeCalculator를 변경해서 이 함수를 현재 사용자를 위한 로케일을 바라보도록 바꿀 수 있습니다.

템플릿이 요청되었을 때 Lift의 TemplateFinder는 _langCOUNTRY.html 접미사를 가진 템플릿을 찾은 다음 .html을 찾습니다. 그래서 /frog를 로딩하고 로케일이 enUS이면 Lift는 /frog_enUS.html을 찾을 것이고 그 다음에 /frog_en.html 그 후 /frog.html을 찾습니다. 하지만 로케일이 체코라면 Lift는 /frog_csCZ.htm, /frog_cs.html, /frog.html 순으로 찾을 것입니다. 동일한 검색(lookup) 메카니즘은 Surround(9.14 참고)와 Embed(9.13 참고) 스니펫을 통해서 접근된 템플릿을 위해서도 사용됩니다. 그래서 템플릿 레벨에서 Lift는 아주 유연한 템플릿을 제공합니다.

Node: Lift는 UTF-8로 모든 템플릿을 파싱합니다. 텍스트 에디터가 UTF-8 인코딩으로 설정되었는지 확인하세요.



8.1.2 리소스 검색 (Resource Lookup)

Lift는 리소스들을 찾기 위해서 다음의 메카니즘을 사용합니다. 지역화된 리소스들은 HTML 페이지와 나란히 있는 템플릿 파일들에 저장되어 있습니다. 동일한 파서는 리소스와 페이지를 스스로 로드하도록 사용됩니다. 리소스들의 전역세트는 다음의 파일들에서 검색됩니다: /_resources.html, /templates-hidden/_resources.html, /resources-hidden/_resources.html. Lift는 로케일에 기반한 접미사를 사용해서 _resources 파일을 찾을 것이라는 것을 기억하세요.

리소스 파일은 다음 포맷이 되어야 합니다:

<resources>
   <res name="welcome">Benvenuto</res>
   <res name="thank.you">Grazie</res>
   <res name="locale">Localit?</res>
   <res name="change">Cambia</res>
</resources>

전역 리소스 파일에 대해서 추가적으로 페이지당 리소스파일들이 있습니다.(현재 Req에 기반한) 지금 요청된 페이지가 /foo/bar이면 다음의 리소스 파일들 또한 찾을 것입니다: /foo/_resources_bar.html, /templates-hidden/foo/_resources_bar.html, /foo/resources-hidden/_resources_bar.html (그리고 모든 로케일에 특정한(Locale-specific) 접미사들) 각 로케일마다 분리된 리소스파일을 만들거나 아래의 포맷을 사용해서 여러개의 로케일을 _resources_bar.html파일 자체로 한 덩어리로 만들기로 선택할 수 있습니다.

<resources>
   <res name="hello" lang="en" default="true">Hello</res>
   <res name="hello" lang="en" country="US">Howdy, dude!</res>
   <res name="hello" lang="it">Benvenuto</res>
   <res name="thank.you" lang="en" default="true">Thank You</res>
   <res name="thank.you" lang="it">Grazie</res>
   <res name="locale" lang="en" default="true">Locale</res>
   <res name="locale" lang="it">Localit?</res>
   <res name="change" lang="en" default="true">Change</res>
   <res name="change" lang="it">Cambia</res>
</resources>



8.1.3 리소스 접근 (Accessing Resources)

Lift는 리소스 접근을 쉽게 만들어줍니다.

스니펫: <span class="lift:Loc.hello">This Hello will be replaced if possible</span> 에서 스니펫 호출의 .(마침표)뒤의 값은 리소스명을 검색하는데 사용됩니다.

코드에서 보면 :
  • S.loc("hello") - 리소스명이 "hello"인 지역화된 값을 담고 있는 Box[NodeSeq]를 리턴합니다.
  • S.??("Hello World") - 리소스명인 "Hello World"를 찾고 이 리소스를 위한 문자열 값을 리턴합니다. 리소스가 발견되지 않으면 "Hello World"를 리턴합니다.



8.1.4 결론

Lift는 페이지별 페이지, 리소스별 리소스 원리에서 애플리케이션을 지역화하기 위해서 넓은 범위의 메카니즘을 제공합니다.




8.2 의존성 주입 (Dependency Injection)

의존성 주입은 Java세계에서 중요한 이야기거리입니다. Java는 추상 인터페이스를 구체적인 구현으로 바인딩하려는 경향이 있는 어떤 기본 특징들(예를들면 function)이 없기 때문에 중요합니다. 기본적으로 대부분의 개발자가 그냥 이렇게 하기 때문에 MyInterface thing = new MyInterfaceImpl()를 하기가 훨씬더 쉽습니다.

Scala의 cake 패턴은 스칼라 trait를 결합함으로써 개발자가 복잡한 행동을 구성하도록 돕는 먼 길을 갑니다. Jonas Boner는 의존성 주입(Dependency Injection)에 대한 우수한 글을 썼습니다.

cake 패턴은 자바개발자가 의존성 주입 기능을 완성하게 하는데 단지 절반쯤만 갑니다. cake 패턴은 Scala trait의 복잡한 클래스들을 구성하도록 해주지만 cake 패턴은 주어진 상황에서 판매하려는 cake의 조합에 대한 동적인 선택을 만들어 주는 관점에서 덜 도움됩니다. Lift는 의존성 주입 퍼즐을 완성하는 추가적인 특징들을 제공합니다.



8.2.1 Lift 라이브러리와 인젝터 (Lift Libraries and Injector)

Lift는 웹프레임워크이면서 Scala 라이브러리 세트입니다. Lift의 common, actor, json, util 팩키지들은 스칼라 개발자가 애플리케이션을 만들기 위한 공통적인 라이브러리들을 제공합니다. Lift의 라이브러리들은 잘 테스트되었고 넓게 사용되고 잘 지원되며 정해진 일정에 따라 릴리즈되었습니다.(월간 마일드스톤, 분기당 릴리즈)

Lift의 Injector trait는 의존성주입의 원리를 형성합니다:

/**
 * 기본 의존성 주입 trait
 */
trait Injector {
   implicit def inject[T](implicit man: Manifest[T]): Box[T]
}

이 trait를 다음처럼 사용할 수 있습니다:

object MyInjector extends Injector {...}

val myThing: Box[Thing] = MyInjector.inject

MyThing의 인스턴스가 Box안에 있는 이유는 MyInjector이 어떻게 Thing의 인스턴스를 생성하는지 알고 있다고 보장할 수 없기 때문입니다. Lift는 주입을 위한 함수들을 등록(그리고 재등록)하도록 해주는 SimpleInjector라고 부르는 Injector의 구현체를 제공합니다:

object MyInjector extends SimpleInjector

def buildOne(): Thing = if (testMode) new Thing with TestThingy {} else new Thing with Runt

MyInjector.registerInjection(buildOne _) // Thing를 만드는 함수를 등록합니다

val myThing: Box[Thing] = MyInjector.inject

이것은 나쁘지 않습니다. 주입시기를 결정하도록 만드는 함수를 정의하도록 해줍니다. 그리고 런타임시(또는 테스트시)에 함수를 바꿀 수 있습니다. 하지만 2가지 문제가 있습니다: 각 주입을 위한 Box를 얻는 것은 최적이 아닙니다. 게다가 전역범위의 함수는 로직(test 대 production 대 xxx)의 전체를 함수안에 넣어야 함을 의미합니다. SimpleInjector는 많은 방법으로 도와주고 있습니다.

object MyInjector extends SimpleInjector {
   val thing = new Inject(buildOne _) {} // 열심히 평가되고 등록되기 때문에 반드시 val로 thing을 정의합니다. 
}

def buildOne(): Thing = if (testMode) new Thing with TestThingy {} else new Thing with Runt

val myThingBox: Box[Thing] = MyInjector.injectval

myThing = MyInjector.thing.vend // Thing의 인스턴스를 판매합니다(vend)

Inject는 추가적인 트릭을 가지고 있습니다. Inject으로 함수의 범위를 정할 수 있습니다. 이것은 테스트하거나 특정한 호출범위를 위해 행동을 변경할 필요가 있을때 유용합니다:

MyInjector.thing.doWith(new Thing with SpecialThing {}) {
   val t = MyInjector.thing.vend // SpecialThing의 인스턴스
   val bt: Box[Thing] = MyInjector.inject // 전체(SpecialThing)
}

MyInjector.thing.default.set(() => new Thing with YetAnotherThing {}) // 전역범위로 설정합니다

doWith 호출의 범위내에서 MyInjector.thing는 SpecialThing의 인스턴스를 판매(vend)할 것입니다. 호출의 범위나 전역범위내에서 행동을 변경하는 것뿐만 아니라 테스트에도 유용합니다. Java를 위한 의존성 주입 팩키지를 얻는 많은 기능성을 제공해줍니다. 하지만 Lift WebKit내에서 더 좋습니다.



8.2.2 Lift WebKit과 강화된 주입 유효범위화 (Lift WebKit and enhanced injection scoping)

Lift의 WebKit은 HTTP 요청들을 다루고 HTML을 조작하기 위해서 넓은 범위화(ranging) 도구들을 제공합니다.

Lift의 WebKit의 Factory는 SimpleInjector를 상속받지만 현재 HTTP 요청이나 현재의 컨테이너 세션에 기반한 함수에 유효법위화 능력을 추가합니다.


object MyInjector extends Factory {
   val thing = new FactoryMaker(buildOne _) {} // 열심히 평가되고 등록되기 때문에
                                                   // val이 되어야 하는 thing을 정의합니다.
}

MyInjector.thing.session.set(new Thing with ThingForSession {}) // 요청의 지속기간(duration)을 위해서
                                                                // 판매될(vend) 인스턴스를 설정합니다

MyInjector.thing.request.set(new Thing with ThingForRequest {}) // 요청의 지속기간(duration)을 위해서
                                                                // 판매될(vend) 인스턴스를 설정합니다

WebKit의 LiftRules는 Factory이고 LiftRules가 가지고 있는 많은 속성들(properties)은 FactoryMakers입니다. 이것은 유효범위(scope) 호출중에 행동(behavior)을 바꿀수 있음을 의미합니다.(테스트할때 유용합니다.)

LiftRules.convertToEntity.doWith(true) { ... test that we convert certain characters to entities}

또는 현재 요청에 기반합니다.(예를 들면 현재 요청동안에 docType을 계산하기 위한 룰을 바쑬 수 있습니다.)

if (isMobileReqest) LiftRules.docType.request.set((r: Req) => Full(DocType.xhtmlMobile))

또는 현재 세션에 기반합니다.(예를 들면, maxConcurrentRequests를 바꾸는 것은 세션이 만들어졌을때 어떤 규칙에 기반합니다.)

if (browserIsSomethingElse) LiftRules.maxConcurrentRequests.session.set((r: Req) => 32)
              // 이 세션에는 32 동시 요청을 허용합니다



8.2.3 결론

Lift의 SimpleInjector/Factory 기능은 전역함수, 호출 스택 유효범위화, 요청과 세션 범위화를 위한 강력하고 유용한 메카니즘을 제공하고 설정을 위한 XML이나 바이트코드를 재작성하는 마법없이도 대부분의 Java기반의 의존성 주입 프레임워크보다 더 유용한 특성들을 제공합니다.




8.3 모듈 (Modules)

Lift는 2007년 프로젝트의 첫 버전에서 모듈을 제공했습니다. Lift의 HTTP 요청/응답 사이클을 다루는 전체는 후킹에 열려있습니다. 게다가 HTML 페이지를 만드는 Lift의 템플릿 메카니즘은 HTML을 취하고 HTML을 리턴하는(NodeSeq => NodeSeq) 간단한 함수인 스니펫(7.1장 참고)을 통한 페이지 컨텐츠 변환으로 구성됩니다. Lift의 스니펫 해결 메카니즘은 열려있고 모든 코드가 Boot에서(7.4 참고) 참조되었기 때문에 LiftRules에서 그것의 스니펫들이나 다른 리소스들을 등록하는 장범에 의해 어떤 코드도 Lift의 “모듈”이 될 수 있습니다. PayPal, OAuth, OpenID, LDAP, 많은 jQuery 위젯들을 가지고 있는 모듈을 포함하여 많은 Lift 모듈들이 이미 존재합니다.

외부 모듈과 Lift를 통합하는 것과 관련되 가장 어려운 이슈는 SiteMap(3.2 참조) 메뉴 계층안에 모듈의 메뉴아이템을 어떻게 적적히 추가할 수 있냐느 것입니다. Lift 2.2는 SiteMap: SiteMap 뮤테이터(mutators)을 변화시키는 더 유연한 메카니즘을 소개합니다. Sitemap 뮤테이터는 모듈의 메뉴들을 메뉴 계층 어디에 추가하는가에 대한 규칙들에 기반해서 SiteMap을 재작성한 함수들입니다. 각 모듈은 마커(marker)들을 발행할 것입니다. 예를 들면 ProtoUser에 대한 마커가 있습니다.

/**
 * User의 메뉴 아이템들을 아이템뒤 같은 레벨에 추가하기를 원하면
 * 여러분의 메뉴에 이 LocParam을 추가합니다.
 */
final case object AddUserMenusAfter extends Loc.LocParam[Any]
/**
 * User의 메뉴 아이템들의 이 LocParam을 가진 메뉴를 치환합니다.
 */
final case object AddUserMenusHere extends Loc.LocParam[Any]
/**
 * User의 메뉴 아이템들을 그 메뉴의 자식드리 되기를 원한다면
 * 여러분의 메뉴에 이 LocParam을 추가합니다.
 */
final case object AddUserMenusUnder extends Loc.LocParam[Any]

또한 모듈은 SiteMap  뮤테이터를 이용할 수 있게 하고 이것은 모듈의 init 메서드로부터나 모듈의 다른 메서드를 통해서 리턴될 수 있습니다. ProtoUser는 SiteMap => SiteMap을 리턴하는 sitemapMutator 메서드를 이용할 수 있게 합니다.

애플리케이션은 적절한 메뉴 아이템에 마커(marker)추가할 수 있습니다.

Menu("Home") / "index" >> User.AddUserMenusAfter

그리고 애플리케이션이 LiftRules를 가진 SiteMap을 등록했을 때 그것은 뮤테이터를 적용합니다.

LiftRules.setSiteMapFunc(() => User.sitemapMutator(sitemap()))

왜냐하면 뮤테이터들은 구성 가능하기(composable) 때문입니다:

val allMutators = User.sitemapMutator andThen FruitBat.sitemapMutator
LiftRules.setSiteMapFunc(() => allMutators(sitemap()))

각 모듈에서 뮤테이터의 구현은 아주 간단합니다:

private lazy val AfterUnapply = SiteMap.buildMenuMatcher(_ == AddUserMenusAfter)
private lazy val HereUnapply = SiteMap.buildMenuMatcher(_ == AddUserMenusHere)
private lazy val UnderUnapply = SiteMap.buildMenuMatcher(_ == AddUserMenusUnder)

/**
 * SiteMap 뮤테이터 함수
 */
def sitemapMutator: SiteMap => SiteMap = SiteMap.sitemapMutator {
   case AfterUnapply(menu) => menu :: sitemap
   case HereUnapply(_) => sitemap
   case UnderUnapply(menu) => List(menu.rebuild(_ ::: sitemap))
}(SiteMap.addMenusAtEndMutator(sitemap))

패턴매칭을 돕는 익스트랙터(extractor)들을 정의했습니다. SiteMap.buildMenuMatcher는 익스트랙터들을 아주 간만하게 만들도록 하는 헬퍼메서드입니다. 그 다음 LocParam 마커를 찾고 마커에 기반한 메뉴를 재작성하는 PartialFunction[Menu, List[Menu]]를 제공합니다. 매치되는 것이 없다면 추가적인 규칙이 적용됩니다. 이 경우에 SiteMap의 끝에 메뉴들을 추가합니다.




8.4 HtmlProperties, XHTML, HTML5

Lift는 단일 trait HtmlProperties에서 HTML 페이지를 파싱하고 보여주는 많은 관점을 통합합니다.

HtmlProperties은 session-by-session(그리고 request-by-request) 원리상으로 템플릿이 파싱되는 방법과 Scala의 NodeSeq가 유효한 HTML 결과로 변환되는 방법을 정의합니다. HtmlProperties의 속성은 다음과 같습니다:

  • docType - HTML페이지에 대한 DocType. 예를 들면 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">나 <!DOCTYPE html>
  • encoding - 페이지 인코딩. 예를 들면 <?xml version="1.0" encoding="UTF-8"?>
  • contentType - Content-Type 응답헤더의 설정. 예를 들면 application/xhtml+xml; charset=utf-8이나 text/html; charset=utf-8
  • htmlOutputHeader - docType와 encoding을 결합하는 방법을 계산합니다.(이것은 docType뒤에 encoding이 오는 IE6을 지원하기 위해서 중요합니다.)
  • htmlParser - InputStream을 Box[NodeSeq]로 변환하는 함수입니다. 이것은 Lift에 의해서 템플릿을 파싱하려고 사용됩니다.
  • htmlWriter - NodeSeq를 Writer로 작성하려는 함수입니다. 이것은 Lift에 의해서 페이지의 내부 XML 표현을 HTML페이지를 표현하는 바이트 스트림으로 변환하려고 사용됩니다.
  • html5FormSupport - 현재 브라우저가 HTML5 폼을 지원하는지를 가리키는 플래그(flag)입니다.
  • maxOpenRequests - 명명된 호스트에 대해 브라우저가 지원하는 동시 HTTP 요청들의 최대 수
  • userAgent - 브라우저가 보낸 User-Agent 문자열



8.4.1 OldHtmlProperties을 통한 XHTML

이종의 LiftRule간의 호환성을 유지하는 기본 속성들은 DocType와 Encoding을 계산하려고 사용됩니다. 유효한(well-formed) XML파일이 필요한 PCDataXmlParser 파서를 사용합니다. AltXML.toXML을 통해서 아웃풋은 일반적으로 XHTML이지만 특정한 태그들(예를들면 <br>)은 IE6/IE7에 친숙한 방법으로 작성되었습니다.



8.4.2 Html5Properties을 통한 HTML5

Lift 2.2이전에 Lift는 항상 XHTML을 발행했고 기본적으로 Content-Type헤더를 application/xhtml+xml; charset=utf-8으로 설정했습니다. 이것은 계속 Lift의 기본 동작이 됩니다.
이것은 현대적인 것(Firefox, Chrome, Safari)을 포함한 대부분의 브라우저가 가지고 있는 XHTML과 관련된 이슈들을 없애버립니다. 게다가 XHTML은 특정한 JavaScript 라이브러리들의 동작을 제한합니다.

Boot.scala에서 LiftRules.htmlProperties.default.set((r: Req) => new Html5Properties(r.userAgent))를 호출함으로써 Lift가 HTML5를 완전히 지원하도록 설정할 수 있습니다. Lift는 nu.validator(http://about.validator.nu/htmlparser/) HTML 파서를 사용하고 모든 테스트된 브라우저들(IE6+, Firefox 2+, Safari 2+, Chrome 1+)이 페이지를 올바르게 렌더링하는 올바른 DocType과 응답 헤더를 발행합니다.

HTML5파서는 표준 XML 파서와는 다르기 때문에 아래의 방법대로 여러분이 가지고 있는 템플릿을 조절할 필요가 있을 것입니다:

  • 모든 태그들은 소문자로 변환되어야 합니다. 이것은 <lift:FooBar/>가 <lift:foobar/>로 변환되어야 함을 의미합니다. 가능한 부분에서 디자이너에게 친숙하도록 변환하는 것을 권합니다.(예를 들면 <div class="lift:FooBar"></div>)
  • <div/>와 <my_thing:bind/> 포맷의 태그들은 규칙에 맞지 않습니다. 이것들은 반드시 <div></div>와 <my_thing:bind></my_thing:bind>로 변환되어야 합니다. 불행히고 파서는 아주 관대하기 때문에 닫는 태그가 빠진것에 대해서 알려주기 보다는 파서는 기대하지 않는 방법으로 중첩시킬 것입니다.
  • 파서가 “보장하는” 태그들이 있습니다. 예를 들면 <tr>, <thead>, <tbody>태그는 반드시 <table>안의 첫 태그가 되어야 합니다. 이것은 다음의 코드를 깨뜨립니다.
    <table><mysnippet:line>
    <tr><td><mysnippet:bind_here></mysnippet:bind_here></td></tr>
    </mysnippet:line><table>
    기대하는 동작은 다음처럼 작성해서 얻을 수 있습니다.
    <table><tr lift:bind="mysnippet:line"><td><mysnippet:bind_here></mysnippet:bind_here></td></tr><table>.



8.4.3 세션이나 요청 중간에 동작 변경하기(Changing behavior mid-session or mid-request)

HtmlProperties 세션이나 요청 중간에 동작을 변경할 수 있습니다. LiftSession.sessionHtmlProperties는 세션을 위한 HtmlProperties를 담고 있는 SessionVar입니다. LiftSession.requestHtmlProperties는 요청을 위한 HtmlProperties를 담고 있는 TranientRequestVar입니다. 요청의 시작부분에서 requestHtmlProperties는 sessionHtmlProperties의 값을 설정합니다. 다음 코드를 사용해서 요청동안에 프로퍼티를 대체할 수 있습니다.

for {
   session <- S.session
} session.requestHtmlProperties.set(session.requestHtmlProperties.is.setDocType(() => Full("<!DOCTYPE moose>")))

2011/03/05 00:12 2011/03/05 00:12