Outsider's Dev Story

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

Simply Lift : Chapter 2 - The ubiquitous Chat app

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

2장
유비쿼터스 챗 앱(The ubiquitous Chat app)


Lift로 다중사용자 채팅 애플리케이션을 작성하는 것은 아주 간단하고 Lift의 핵심개념들중 많은 것들을 설명해 줍니다. 소스코드는 https://github.com/dpp/simply_lift/tree/master/chat에서 볼 수 있습니다.



2.1 뷰(View)

Lift 앱을 작성할 때 사용자가 보는 것을 만들고 HTML페이지에 동작을 추가하는 유저인터페이스에서 시작하는 것이 종종  가장 좋습니다. 그래서 우리의 채팅애플리케이션을 만들 Lift 템플릿을 보겠습니다.

리스트 2.1 : index.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head><title>Home</title></head>
    <body class="lift:content_id=main">
        <div id="main" class="lift:surround?with=default;at=content">
            <!-- the behavior of the div -->
            <div class="lift:comet?type=Chat">
                Some chat messages
                <ul>
                    <li>A message</li>
                    <li class="clearable">Another message</li>
                    <li class="clearable">A third message</li>
                </ul>
            </div>

            <div>
                <form class="lift:form.ajax">
                    <input class="lift:ChatIn" id="chat_in"/>
                    <input type="submit" value="Say Something"/>
                </form>
            </div>
        </div>
    </body>
</html>

유효한 HTML페이지이지만 약간 수상스러운 클래스속성들이 있습니다. 첫번째는 <body class=”lift:content_id=main”>입니다. 여기서 클래스는 “실제 페이지 내용은 id=’main’엘리먼트에 담겨있다”고 말해줍니다. 이것은 각각의 템플릿을 위한 유효한 HTML페이지를 가지도록 해주지만 동적으로 하나이상의 크롬(chrome) 템플릿에 기반한 컨텐츠에 “크롬(chrome)”을 추가합니다.

<div id=”main”>을 보겠습니다. 이것은 lift:surround?with=default;at=content같은 파격적인 클래스를 가지고 있습니다. 이 클래스는 기본템플릿으로 <div>를 둘러싸고 <div>와 기본템플릿에서 id가 “content”인 엘리먼트에서 그 자식들을 인서트하는 스니펫(snippet)을 호출합니다.  또는 <div>주변을 기본크롬으로 감쌉니다. 스니펫(snippet)에 대한 더 자세한 내용은 48페이지의 7.1을 보세요.

다음으로 챗 엘리먼트 <div class="lift:comet?type=Chat">와 동적인 동작을 어떻게 연계하는지 정의합니다. “comet” 스니펫은 CometActor를 상속받고 CometActor의 상태가 변경되었을 때 CometActor에서 브라우저로 컨텐츠를 푸싱(pushing)하는 메카니즘을 가능하게 하는 Chat이라는 이름의 클래스를 찾습니다.



2.2 Chat Comet 컴포넌트

Actor 모델은 Erlang을 포함하는 함수형 언어에서 상태(state)를 제공합니다. Lift는 Actor라이브러리와 강력한 상태와 동시성 모델을 제공하는 LiftActors(7.14 참고)를 가지고 있습니다. 이것은 모두 추상화되어 보이므로 Chat클래스를 보겠습니다.

리스팅 2.2 : Chat.scala

package code
package comet

import net.liftweb._
import http._
import util._
import Helpers._

/**
 * 브라우저상의 실제 스크린영역은 이 컴포넌트에 의해서 표현될 것입니다.
 * 서버에서 컴포넌트가 변경되었을때 변경사항은 자동적으로 브라우저에 반영됩니다.
 */
class Chat extends CometActor with CometListener {
    private var msgs: Vector[String] = Vector() // private state

    /**
     * 컴포넌트가 인스턴트화되었을때 ChatServer에 리스너로 등록됩니다.
     */
    def registerWith = ChatServer

    /**
     * CometActor는 Actor이므로 메시지를 처리합니다.
     * 이 경우에 Vector[String]을 리스닝하고 있고
     * 하나를 갖게 되었을때 private state를 업데이트하고 컴포넌트를 reRender()합니다.
     * reRender()는 변경사항들이 브라우저에 보내지도록 합니다.
     */
    override def lowPriority = {
        case v: Vector[String] => msgs = v; reRender()
    }

    /**
     * li엘리먼트에 메시지를 배치하고
     * clearable클래스를 가지고 있는 모든 엘리먼트를 클리어합니다.
     */
    def render = "li *" #> msgs & ClearClearable
}

Chat 컴포넌트는 private state와 ChatServer 레지스터, 인커밍(incoming) 메시지 핸들러들을 가지고 있고 그것들을 렌더링할 수 있습니다. 이 각각의 조각들을 보겠습니다.

프로토타입 객체지향 코드에서 다른 private state처럼 private state는 객체의 행위를 정의하는 state입니다.

registerWith는 어떤 컴포넌트가 Chat 컴포넌트에 등록하는 지를 정의하는 메서드입니다. 등록은 Listener(또는 Observer) 패턴의 하나입니다. 잠시 ChatServer의 정의를 보겠습니다.

lowPriority메서드는 어떻게 들어오는 메시지를 처리하는지를 정의합니다. 이경우에 들어오는 메시지를 패턴 매칭(7.15 참조)하고 그것이 Vector[String]이면 local state에 설정된 액션을 Vector에 수행하고 컴포넌트를 리랜더링(re-rendering)합니다. 리랜더링(re-rendering)은  컴포넌트를 디스플레이하는 모든 브라우저가 변경되도록 할 것 입니다.

매칭하고 교체하려고(7.10 참고) CSS를 정의함으로써 컴포넌트를 어떻게 render하는지 정의합니다. 템플릿의 모든 <li>태그를 매치하고 각 메시지를 위해서 메시지에 설정하는 <li>태그를와 자식노드들을 생성합니다. 추가적으로 class 속성이 clearable인 모든 엘리먼트를 클리어 합니다.

이것이 Chat CometActor 컴포너트를 위한 것입니다.



2.3 ChatServer

ChatServer 코드:

리스팅 2.3 : ChatServer.scala

package code
package comet

import net.liftweb._
import http._
import actor._

/**
 * 채팅 특징이 모든 클라이언트에 제공하는 싱글톤.
 * Actor이므로 한번에 오직 하나의 메시지만 처리되기 때문에 쓰레드세이프합니다.
 */

object ChatServer extends LiftActor with ListenerManager {
    private var msgs = Vector("Welcome") // private state

    /**
     * 리스너를 업데이트했을 때 어떤 메시지를 보내는가?
     * immutable 데이터스트럭처인 msgs를 보내므로 
     * 어떤 위험이나 락킹없이 많은 쓰레드에 공유될 수 있습니다.
     */
    def createUpdate = msgs

    /**
     * Actor에 보내진 메시지를 처리합니다.
     * 이경우에 ChatServer에 보내신 String들을 찾습니다.
     * 그것들은 메시지의 Vector에 덧붙히고 모든 리스터를 업데이트합니다.
     */
    override def lowPriority = {
        case s: String => msgs :+= s; updateListeners()
    }
}

ChatServer는 class보다는 object로써 정의됩니다.  이는 그것을 애플리케이션 어디서든 ChatServer 이름으로 참조될수 있는 싱글톤으로 만듭니다. Scala의 싱글톤은 object의 인스턴스이고 이 인스턴스는 다른 모든 인스턴스처럼 건네질(be passed) 수 있다는 점에서 Java의 static과는 다릅니다. 이것이 Chat컴포넌트에서 registerWith메서드로부터 ChatServer 인스턴스를 리턴할 수 있는 이유입니다.

ChatServer는 채팅메시지의 리스트를 표현하는 Vector[String]인 private state을 가집니다. 명백하게 타입을 정의하지 않았기 때문에 Scala의 타입추론기는  msgs의 타입을  추론합니다.

createUpdate메서드는 리스터에 보낼 최신정보를 생성합니다. 리스너가 ChatServer에 등록되거나 updateListeners()메서드가 호출되었을 때 이 최신정보가 보내집니다.

마지막으로 lowPriority메서드는 이 컴포넌트가 다룰 수 있는 메시지들을 정의합니다. ChatServer가 메시지로 String을 받으며 String을 메시지의 Vector에 덧붙히고 리스터들을 업데이트합니다.



2.4 유저 인풋(User Input)

뷰(view)로 돌아가서 chat에 라인들을 추가하기 위해서 동작(behavior)이 어떻게 정의되는지 보겠습니다.

<form class="lift:form.ajax">는 input form을 정의하고 form.ajax 스니펫은 form을 전체 페이지가 로드는 일 없이 서버에 submit될 수 있는 Ajax(7.12 참고) form으로 바꿉니다.

다음으로 인풋 폼 엘리먼트 <input class="lift:ChatIn" id="chat_in"/>를 정의합니다. 이것은 plain old input form이지만 ChatIn 스니펫을 호출함으로써 Lift에게 <input>의 동작을 수정하라고 말합니다.



2.5 Chat In

ChatIn 스니펫(7.1 참조)는 다음과 같이 정의되었습니다:

리스팅 2.4 : ChatIn.scala

package code
package snippet

import net.liftweb._
import http._
import js._
import JsCmds._
import JE._

import comet.ChatServer

/**
 * 인풋을 아웃풋으로 바꾸는 snippet.
 * 템플릿들을 동적인 컨텐츠로 바꿔줍니다.
 * Lift의 템플릿들은 스니펫을 호출할 수 있고 
 * 스니펫들은 "by convention"을 포함하는 여러가지 다른 방법들안에서 해결됩니다.
 * 스니펫 팩키지는 스니펫들에 이름을 붙히고 이러한 스니펫들은 
 * 호출되었을때 인스턴스화되는 클래스나 오브젝트, 싱글톤이 될 수 있습니다.
 * 싱글톤은 스니펫에서 관리되는 명확한 상태가 없을때 유용합니다.
 */
object ChatIn {

    /**
     * 이 경우에 render메서드는 NodeSeq => NodeSeq로 바꾸는 함수를 리턴합니다.
     * 이 경우에 함수는 인풋에 동작을 추가함으로써 form 인풋엘리먼트를 변경합니다.
     * 동작은 메시지를 ChatServer에 보내는 것이고 input을 클리어하는 JavaScript를 리턴합니다.
     */
    def render = SHtml.onSubmit(s => {
        ChatServer ! s
        SetValById("chat_in", "")
    })
}

코드는 아주 간단합니다. 스니펩은 함수와 폼엘리먼트의 제출 onSubmit을 연결하는 메소드로 정의됩니다. 보통의 폼서밋이든 ajax든지 엘리먼트가 submit되었을 때,  함수는 form의 값에 적용합니다. 영어로말하면 사용자가 폼을 제풀했을때 함수는 사용자 인풋과 함께 호출됩니다.

함수가 메시지로 input을 ChatServer로 보내고 input box의 값을 빈스트링으로 설정하는 JavaScript를 리턴합니다.



2.6 실행(Running it)

어플리케이션을 실행하는 것은 쉽습니다. 서버에 Java1.6이상이 설치되어 있어야 합니다. chat디렉토리에서 sbt update jetty-run을 입력합니다. Simple Build Tool은 필요한 모든 의존성을 다운로드하고 프로그램을 컴파일한 뒤 실행합니다.

브라우저에서 http://localhost:8080에 접속하고 채킹을 시작합니다.

오, 재미를 위해서 <script>alert(’I ownz your browser’);<script> 입력하고 어떤 일이 일어나는지 보세요. 기대했던 대로 되는 것을 볼 것입니다.



2.7 당신이 보지 못한 것들

import와 주석을 제외하면 멀티쓰레드, 다중사용자 채팅애플리케이션을 구현하는데 약 20라인정도의 스칼라 코드정도입니다. 많지 않습니다.

첫째로 동기화나 다른 쓰레드락킹의 명백한 폼이 빠졌습니다. 애플리케이션은 Actor와 immutable 데이터 스트럭쳐의 이점을 취하기 때문에 개발자는 쓰레드와  primitive 락팅보다는 비즈니스로직에 집중할 수 있습니다.

다음으로 라우팅과 컨트롤러, Ajax호출과 서버변경에 대한 폴링을 연결하는 것들이 빠졌습니다. 애플리케이션에서 동작과 화면을 연결하면 나머지는 Lift가 알아서 합니다.(7.17 참조)

애플리케이션에서 크로스사이트 스크립팅을 피하는 어떤것도 하지 않았습니다. 왜냐하면 Lift가 Scala의 강한 typing과 type safety( 7.16참조)의 이점을 취했기 때문입니다. Lift는 HTML로 인코딩되어야 하는 문자열과 이미 알맞게 인코딩된 HTML 엘리먼트사이의 차이를 알고 있습니다. 기본적으로 Lift 애플리케이션은 OWASP 10대 보안취약성의 대부분을 방지합니다.
2011/02/10 01:18 2011/02/10 01:18