역주: 몇년 지난 이 글에 오류가 있을 수 있으며, 겉핣기 수준의 플레이 지식에 기반한 제 번역에도 오류기 있을 수 있음을 알려드립니다. 수정해야할 부분에 대해서 코멘트 주시면 반영하겠으며, 많은 정보 공유가 필요합니다. 더 정확한 이해를 위해서 직접 소스를 보는 방법이 가장 확실합니다. 저도 나중에 시간만 된다면 전체 소스리딩을 하고 싶네요. 지난 1년간 웹,서버개발보다는 거의 데이터 분석 쪽만 하고 있다는 개인 현황도 알려드리면 서 ㅎㅎ 시작해보죠. 




Play 해부 :  웹서버 


원문 링크 : http://jto.github.io/articles/play_anatomy_part1_bootstrap/
이 연재에서는 플레이 프레임 워크의 내부에 대해 설명하겠습니다. 애플리케이션 시작에서 HTTP 응답 렌더링에 이르기까지 랜더링 작동 방식을 보여 주려고합니다. .오늘은 "배포(prod)"모드에서 플레이 응용 프로그램의 시작에 대해 다루겠습니다. 또한 우리는 play가 HTTP 요청을 리스닝하고 파싱하고 응용 프로그램 코드를 호출하는 방법에 대해 살펴 봅니다.

일반적인 플레이 철학 


이 글을 읽고 있는 독자는 이미 플레이 프레임 워크에 대해 어느 정도는 알고 있을 것으로 생각합니다만, 이 글에서는 반드시 알아야 할 인사이드적인 것들에 대해 썰을 풀 것 입니다:

  • 플레이는 상태가 없습니다. 아주 중요한 이야기에요. 밑줄 쫙~!!  프레임워크에 관해서는 요청들 사이에 서버에 저장되는 것은 없습니다. 물론 일반적인 웹 응용 프로그램에는 일종의 퍼시스턴스 엔진 (SQL, NoSQL, 파일 등)이 있지만 그것이 프레임 워크의 일부는 아닙니다.
    Play에는 세션이라고 불리는 항목이 있지만 실제로는 쿠키일 뿐입니다. 따라서 여기에다 문자열만 저장할 수 있는 이유에 대해 문서에 자세히 설명되어 있습니다.

  • 플레이는 리액티브입니다. 요즘 많이들 사용하고 있는데요. 단순히 말하자면 가능한 한 적은 수의 스레드를 사용하자는 것이고, 클라이언트간에 스레드가 "공유"됩니다. 이는 Server Sent Events 또는 웹 소켓과 같이 오래 지속되는 연결에 있어서 매우 중요합니다. 일반적인 JEE 애플리케이션은 각 HTTP 연결에 스레드를 제공하고 있습니다 (적어도 JSR 315 : Servlet 3.0까지). 그것은 프레임워크 디자인과 당신이 그것을 사용하는 방식 모두에 큰 영향을 미칩니다 (특히 당신이하지 말아야 할 것들).

  • 플레이 버전 2는 완전한 재 작성되었으며 완전히 다른 작품입니다. 

  • 타입 세이프 (typesafe)라는 회사가 플레이를 뒷받침하는데, 이름에도 느껴지듯이 타입 안정성에 훨씬 더 중점을 두고 있습니다.  (역주: Lightbend로 이름이 변경됨. 2016년2월, Akka 등도 제공 )

                                                   리액티브 플랫폼 구성 요소들


  • 플레이의 내부는 함수형 프로그래밍과 객체지향의 장점을 혼합하여 설계되었습니다. 대부분은 스칼라로 작성되었고 프레임워크는 그 위에 Java 를 위한 변경 레이어를 가지고 있습니다. 일반적으로 코드는 매우 간단하며, 그것을 이해하기 위해 스칼라 전문가일 필요는 없습니다. 가변성 상태를 가능한 한 많이 피합니다. 스칼라에서 선호되듯이 말이죠. 

  • 플레이는 오픈 소스입니다!  커뮤니티는 당신이 참여하기를 기다리고 있습니다. 문서 또는 버그 수정에 대한 도움은 언제나 환영합니다. 

본격적으로 탐구를 해 봅시다.!



플레이 어플리케이션 런칭!


당신은 play start 입력했습니다. 자! 무슨 일이 발생 한 걸까요? 

사실, 당신은 방금 특정 매개변수가 있는 기본 스칼라 빌드 도구인 sbt를 호출한 거 였습니다. (예, 'S'는 심플이 아닙니다.Sbt는 당신의 응용 프로그램 빌드 정의를 "읽고" 나서, play % sbt-plugin 라는 불리는 플러그인을 찾습니다. project/plugins.sbt 에서 볼 수 있을 것입니다.


이 플러그인에는 응용 프로그램의 진입점 (main)이 어디인지 sbt에게 알려주는 config 키가 있습니다. 여기서는 sbt를 다루지 않겠지만 (play 빌드만으로도 많은 얘기 꺼리가 있습니다) mainplay.core.server.NettyServer 여기에 있다는 것만 알려드립니다.


표준 JEE와는 반대로 애플리케이션을 호스팅 (톰캣같은하는 애플리케이션 서버는 없습니다. play new를 사용하여 만든 애플리케이션은 그 자체로 서버이며, Play는 그 중 하나의 종속성 일뿐입니다 (정확히 말하면, 플레이는 모듈로 분할되어 있기 때문에 여러 종속성이 있습니다).


위의 그림에도 있듯이 Play는 현재 매우 유명한 (그리고 자바 기반의)고성능 비동기 네트워크 프레임워크인 Netty를 기반으로 합니다. Sbt는 이 객체를 조사하여 main를 찾습니다. mainDev라는 메소드를 발견했을 것인데요 이것은 dev 모드에서 호출됩니다 ( play run 을 사용할 때). 이제 main 에 집중합니다.


역주: 프로그램을 프로덕션 모드(prod mode) 로 배포하려는 경우 play start 로 시작해야 합니다.
auto-reloading-class 및 기본적으로 개발에만 필요한 기능들을 꺼버리기 때문에 응답속도가 빠릅니다.


코드를 읽으면 알 수 있듯이 인수를 구문 분석하고 PID를 포함하는 파일을 만들고 (응용 프로그램을 중지하려는 경우를 대비하여) 다른 지루한 초기화 상용구를 작성합니다. 재미있는 곳으로 건너 뜁시다.


val server = new NettyServer(
new StaticApplication(applicationPath),
Option(System.getProperty("http.port")).fold(Option(9000))(p => if (p == "disabled") Option.empty[Int] else Option(Integer.parseInt(p))),
Option(System.getProperty("https.port")).map(Integer.parseInt(_)),
Option(System.getProperty("http.address")).getOrElse("0.0.0.0")
)
view rawstart.scala hosted with ❤ by GitHub

결국 새로운 NettyServer가 일련의 인수로 만들어집니다. 하지만 먼저 스칼라는 새로운 StaticApplication (applicationPath)을 평가합니다.

부트스트랩 

먼저 Play 애플리케이션을 초기화 해야 합니다. StaticApplication을 살펴 보시죠. "정적어플리케이션 이라고 하는 이유는 무엇인가요?" 묻는다면, 단순히 hot redeploy 코드변경을 하지 않기 때문이라고 답해드립니다. (우리는 프로덕션 모드로 사용한다고 한걸 기억하고 있나요? 현명한 사람들은 프로덕션 환경에서 hot redeploy 를 하지 않습니다).


그래서 좀 더 그곳을 살펴보면. DeafaultappApplication을 만듭니다. 이 클래스는 app 폴더, configuration, classloader 등 (코드 here)와 같이 현재 앱에 대한 일반 정보를 포함하는 case 클래스이며 Play.start (application)를 호출합니다.


그것이 하는 일은 상당히 간단합니다.

Threads.withContextClassLoader(classloader(app)) {
app.plugins.foreach(_.onStart())
}
view rawPlay.scala hosted with ❤ by GitHub


각 플러그인에서 onStart를 하나씩 호출하여 올바른 클래스로더를 사용하고 있는지 확인합니다. 이러한 플러그인은 예를 들어 DB에 대한 커넥션 풀을 만듭니다.


이 시점에서 플러그인이 예외를 던지면 응용 프로그램이 중지됩니다.


우리는 애플리케이션 설정을 알고 있기 때문에 플러그인을 성공적으로 시작했습니다. 이제 HTTP 요청을 리스닝 할 준비가 되었습니다.

Http 요청에 대한 리스닝 


이제 NettyServer를 만들 차례입니다. 앞에서 보았듯이 NettyServer 클래스의 인스턴스를 생성합니다 (NettyServer 객체와 혼동하지 마십시오).


이 클래스는 서버의 인스턴스를 만들고, 스레드 풀, 파이프 라인 인코더 및 디코더를 구성하고, 서버를 주소 및 포트에 바인딩하고, SSL을 구성하지만 더 중요한 것은
newPipeline.addLast ( "handler", defaultUpStreamHandler) 입니다.


// Our upStream handler is stateless. Let's use this instance for every new connection
val defaultUpStreamHandler = new PlayDefaultUpstreamHandler(this, allChannels)
view rawnettyserver.scala hosted with ❤ by GitHub


따라서 HTTP 또는 HTTPS 요청이 수신 될 때마다 Netty는이 PlayDefaultUpstreamHandler 인스턴스의 messageReceived를 호출합니다.


이 메소드는 다음과 같은 작업을 수행합니다.


  • Netty의 HttpRequest에서 play.api.mvc.RequestHeader를 만듭니다.기본적으로 헤더 값을 채우고 쿼리 문자열을 파싱하는 등의 작업을 수행하지만 요청 본문을 파싱하지는 않습니다.
  • 플래시 쿠키를 관리하여 1 회의 요청에 대해서만 유효하게 만듭니다.
  • 요청에 "태그"를 달아 라우팅에 대한 요청 객체에 메타 데이터를 추가 할 수 있습니다.
  • 이 메소드는 응용 프로그램 전역 객체를 사용하여 이 요청으로 수행 할 작업을 찾습니다.
val (untaggedRequestHeader, handler) = Exception
.allCatch[RequestHeader].either(tryToCreateRequest)
.fold(
e => {
val rh = createRequestHeader()
val r = server.applicationProvider.get.fold(e => DefaultGlobal, a => a.global).onBadRequest(rh, e.getMessage)
(rh, Left(r))
},
rh => (rh, server.getHandlerFor(rh)))
view rawhandler.scala hosted with ❤ by GitHub


여기에서는 예외 및 매개 변수 구문 분석을 처리합니다. 무엇인가 실패하면 onBadRequest (rh, e.getMessage)가 응용 프로그램 Global 객체에서 호출됩니다 (전역이 제공되지 않으면 기본값 Global을 사용함).이 객체는 상태 500의 Error 페이지를 렌더링해야합니다. 그렇지 않으면 server.getHandlerFor rh)) [Result, (Handler, Application)] 중 하나를 반환합니다.


getHandlerFor는, 불려가는 Handler를 해결합니다. Handler의 두 가지 주요 유형은 다음과 같습니다.


  • EssentialAction : 기본적으로 컨트롤러에서 Action {request => ...}을 작성하여 정의한 내용입니다.
  • 웹 소켓

def getHandlerFor(request: RequestHeader): Either[Result, (Handler, Application)] = {
import scala.util.control.Exception
def sendHandler: Either[Throwable, (Handler, Application)] = {
try {
applicationProvider.get.right.map { application =>
val maybeAction = application.global.onRouteRequest(request)
(maybeAction.getOrElse(Action(BodyParsers.parse.empty)(_ => application.global.onHandlerNotFound(request))), application)
}
} catch {
case e: ThreadDeath => throw e
case e: VirtualMachineError => throw e
case e: Throwable => Left(e)
}
}
def logExceptionAndGetResult(e: Throwable) = {
Logger.error(
"""
|
|! %sInternal server error, for (%s) [%s] ->
|""".stripMargin.format(e match {
case p: PlayException => "@" + p.id + " - "
case _ => ""
}, request.method, request.uri),
e)
DefaultGlobal.onError(request, e)
}
Exception
.allCatch[Option[Result]]
.either(applicationProvider.handleWebCommand(request))
.left.map(logExceptionAndGetResult)
.right.flatMap(maybeResult => maybeResult.toLeft(())).right.flatMap { _ =>
sendHandler.left.map(logExceptionAndGetResult)
}
}
view rawgetHandlerFor.scala hosted with ❤ by GitHub

이 코드는 Global.onRouteRequest를 호출하여 호출 할 Handler를 찾거나 오류가 발생하면 적절한 Response를 반환합니다. 실패한 경우 Response (예 : 상태가 500 인 HTML 페이지)가 포함된 Left를 반환하고, 그렇지 않으면 앱에 정의 된 Handler가 들어있는 Right (예 : 컨트롤러에 정의 된 Action)를 반환합니다.


요청 본문 다루기


굿! 지금까지 우리는 호출되어야하는 코드를 어떻게든 발견했습니다. RequestHeader로만 작업한다는 사실을 인지했을 수도 있구요. RequestHeader는 본문이 없는 요청입니다. 패턴매칭을 사용하여 getHandlerFor가 반환 한 값을 어떻게 처리할지 결정합니다.


가장 일반적인 경우는 EssentialAction입니다.

case Right((action: EssentialAction, app)) =>
val a = EssentialAction { rh =>
Iteratee.flatten(action(rh).unflatten.map(_.it)(internalExecutionContext).recover {
case error => Done(app.handleError(requestHeader, error),Input.Empty): Iteratee[Array[Byte],SimpleResult]
}(internalExecutionContext))
}
handleAction(a, Some(app))
view rawgistfile1.scala hosted with ❤ by GitHub

근본적으로, EssentialAction은 단지 함수 (RequestHeader) => Iteratee[Array[Byte], Result] 일뿐입니다. 이 함수에 RequestHeader를 주면 요청 본문을 리액티브적으로 소비하는 Iteratee를 제공하고 결국 Result를 "반환"합니다.


다음 단계는 요청 본문을 사용할 항목이 있기 때문에 매우 명확합니다. 우리가 수행해야 할 것은 바이트를 피드로 가져와 결과를 얻는 것뿐입니다. 그것이 바로 handleAction이 하는 일입니다. 클라이언트로부터 바이트를 받아 필요할 경우 청킹을 처리하고 결과를 얻기 위해 Iteratee에게 피드를 보내지만 먼저 전역에 정의 된 필터를 호출합니다. (val filteredAction = app.map (_. global) .getOrElse (DefaultGlobal) .doFilter (action)).


Iteratee에 피드를 제공하려면 Enumerator [Array [Bytes]]를 만들어야합니다.

val bodyEnumerator = {
val body = {
val cBuffer = nettyHttpRequest.getContent()
val bytes = new Array[Byte](cBuffer.readableBytes())
cBuffer.readBytes(bytes)
bytes
}
Enumerator(body).andThen(Enumerator.enumInput(EOF))
}
bodyEnumerator |>> eventuallyBodyParser

Play는 클라이언트로부터의 청크를 열거하는 Enumerator를 만들고 BodyParser로 구성합니다. 그런 다음 Future[Result] 를 얻기 위해  결과 Iteratee를 실행하고 이 결과를 클라이언트에 다시 보내면 됩니다.

val eventuallyResult = eventuallyResultIteratee.flatMap(it => it.run)(internalExecutionContext)
val sent = eventuallyResult.recover {
case error =>
Play.logger.error("Cannot invoke the action, eventually got an error: " + error)
e.getChannel.setReadable(true)
app.map(_.handleError(requestHeader, error)).getOrElse(DefaultGlobal.onError(requestHeader, error))
}(internalExecutionContext).flatMap { result =>
NettyResultStreamer.sendResult(cleanFlashCookie(result), !keepAlive, nettyVersion)
}

 이게 다에요. 

3줄 요약


1. play는 실제로 sbt의 별칭입니다.

2. 각 플레이 응용 프로그램은 그 자체로 서버입니다.

3. 앱을 시작한다는 것은 다음을 의미합니다.

  - params + config 읽기

  - 각 플러그인에서 onStart 호출하기

  - Netty 서버 생성 및 HTTP 요청 수신

  

HTTP 요청이 플레이로 들어 오면 :


1. 서버가 Global.onRouteRequest (rh : RequestHeader)를 호출합니다.
2. 대부분의 경우 
이 핸들러는 실제로 EssentialAction입니다.

3. EssentialAction은 함수 (RequestHeader) => Iteratee [Array[Byte],Result]입니다.

4. 이 함수가 호출되고 결과 Iteratee는 요청 본문에서 청크 (Array [Byte])로 피드됩니다.

5. 마침내 클라이언트에게 Result 가 전달됩니다.






역주: 몇년 지난 이 글에 오류가 있을 수 있으며, 겉핣기 수준의 플레이 지식에 기반한 제 번역에도 오류기 있을 수 있음을 알려드립니다. 수정해야할 부분에 대해서 코멘트 주시면 반영하겠으며, 많은 정보 공유가 필요합니다. 더 정확한 이해를 위해서 직접 소스를 보는 방법이 가장 확실합니다. 저도 나중에 시간만 된다면 전체 소스리딩을 하고 싶네요. 지난 1년간 웹,서버개발보다는 거의 데이터 분석 쪽만 하고 있다는 개인 현황도 알려드리면 서 ㅎㅎ 시작해보죠. 





Play 해부 :  Hot redeploy.

이 연재에서는 플레이 프레임 워크의 내부에 대해 설명하겠습니다. 애플리케이션 시작에서 HTTP 응답 렌더링에 이르기까지 랜더링 작동 방식을 보여 주려고합니다. 오늘은 특히 플레이 프레임 워크가 코드를 컴파일하고 핫로드하는 방법을 보여주고, 플레이와 sbt와의 관계에 대해 주목하겠습니다.

Play 와 SBT

Play의 핵심 기능 중 하나는 대부분의 JVM 기반 프레임 워크와 달리 외부 도구 (JRebel이 필요 없음)에 의존하지 않고 개발 모드에서 모든 코드 수정을 핫로드하는 것입니다. 어떻게 작동하는지 이해하려면 먼저 Play에서 SBT를 사용하여 클래스를 컴파일하는 방법과 어떻게 SBT가 Play에 긴밀하게 통합되어 있는지 확인해 보는 것입니다.

이전 글(play 해부 : 웹서버이야기에서 SBT가 모든 플레이 응용 프로그램의 진입점에 있고 플레이 명령이 기본적으로 sbt의 별칭임을 확인하였는데요. SBT로 제작된 전형적인 스칼라 애플리케이션과 다른 애플리케이션을 구분 짓는 중요한 부분이 바로 play의 sbt 플러그인 (sbt plugin으로 볼 수 있습니다.

보시다시피, 이것은 꽤 큰 SBT 플러그인입니다. 거기에는 당연한 이유가 있습니다.

"매우 많은 것을 하니깐" ^^ 

우선, 플레이는 많은 설정을 추가합니다.

play-assets-directories           play-build-require-assets        play-closure-compiler-options
play-coffeescript-entry-points    play-coffeescript-options        play-common-classloader
play-compile-everything           play-conf                        play-copy-assets
play-default-port                 play-dev-settings                play-dist
play-external-assets              play-javascript-entry-points     play-less-entry-points
play-less-options                 play-monitored-files             play-onStarted
play-onStopped                    play-package-everything          play-plugin
play-reload                       play-require-js                  play-require-js-folder
play-require-js-shim              play-require-native-path         play-routes-imports
play-templates-formats            play-templates-imports           play-version

스크도 추가합니다.

classpath Display the project classpath. clean Clean all generated files. compile Compile the current application. console Launch the interactive Scala console (use :quit to exit). dependencies Display the dependencies summary. dist Construct standalone application package.(* 스탠드얼론 패키지 구축) exit Exit the console. h2-browser Launch the H2 Web browser. license Display licensing informations. package Package your application as a JAR. play-version Display the Play version. publish Publish your application in a remote repository. publish-local Publish your application in the local repository. reload Reload the current application build file. run <port> Run the current application in DEV mode. test Run Junit tests and/or Specs from the command line eclipse generate eclipse project file idea generate Intellij IDEA project file sh <command to run> execute a shell command start <port> Start the current application in another JVM in PROD mode. update Update application dependencies.

PlaySettings.scala 여기에 모든 기본 설정이 정의 되 있구요. 

play run 을 타이핑하면 playRun.scala에 정의 된 playRunSetting을 호출합니다.

무엇을 하는지 좀 더 살펴보면:

  • sbt 클래스로더를 유지한다
  • "common" jar 를 로드 할 수 있는 "common" 클래스로더를 만든다. 이 jar들은 당신의 어플리케이션과 sbt 양쪽에서 이용된다. 그들은 리로드 되지 않을 것이다.
  • 모든 어플리케이션 종속성들을 로드하는 ,공통 클래스 로더의 자식인, 어플리케이션 클래스로더를 만듭니다.

이 특정 클래스 로더는 특히 흥미로운데요 우선, 클래스 리로딩의 "책임자"중 하나입니다. sbtClassloader의 하위 클래스는 아니지만 공유 클래스 로드를 위임합니다.

플레이 어플리케이션 실행하기 (Running)

우리는 멋진 클래스 로더 계층을 만들었으며 이제 애플리케이션을 실행할 준비가 되었습니다. 물론, start 와 마찬가지로, 그것은 어딘가에 있는 메인을 호출하는 것 일 뿐이죠.
이전 글에서 main은 play.core.server.NettyServer 에 있었음을 보았는데요. 흥미롭게도, 플레이는 여기서 SBT설정을 사용하지 않고 있으며, 클래스 이름은 하드코딩
되어 있습니다.

따라서 우리가 할 것은  mainDevHttpMode를 호출하는 것 입니다.!!


이제 거의 끝났네요... ?? 응?  이봐, 우리 아직 클래스 리로딩에 대해 이야기하지 않았잖아?!

메소드에 reloader가 주어집니다. 해당 리로더는 SBTLink 타입입니다. 요청이 서버에 도달 할 때마다 응용 프로그램은 reloader를 호출하여 코드가 변경되었는지 확인합니다. 리로더는 재컴파일 (또는 정적 애셋 복사와 같은 다른 필요한 작업)을 SBT에 위임합니다.


뭐 좋아, 상당히 간단한거 같긴한데, SBT가 기본적으로 파일들의 수정에 대해 알아 볼 수 있는 능력이 있는데, SBTLink를 작성해야 하는 이유는 무엇이지요?


두가지 이유:

  • 첫째, 플레이는 컴파일 오류가 발생하는 시점을 알아야 브라우저에 멋진 오류 페이지가 표시 될 수 있습니다.
  •  핫리로드! 코드를 다시 컴파일하는 것이 좋지만 응용 프로그램은 과거 클래스를 가지고 계속 실행 중입니다. 할 일 많아요.~

어디에서 마법이 시작되나

여기에(Here진짜 속임수가 있습니다. 컴파일이 성공했다면 JVM에서 로드된 클래스를 업데이트해야합니다. 어떻게 해야 할까요? 간단합니다. 이전 어플리케이션 클래스 로더를 제거하고 업데이트된 클래스와 함께 새 클래스로더를 만듭니다.

그런 다음 애플리케이션을 다시 시작하기 만 하면됩니다. JVM을 다시 시작할 필요가 없습니다.
그리고 그게 다에요! 새 코드가 실행됩니다.!! 모든 일이 몇 초 안에 말이죠. 매우 행복합니다 :)

트레이드 오프

클래스 로더를 핫 리로드 코드로 대체하는 것은 간단하고 잘 작동하지만 비용이 따릅니다. 하지만 가능한 이유가 우선 Play는 상태가 없으며 매우 가볍기 때문이죠. (빠른 시작). 그런 다음 빌드 시스템과 프레임 워크 사이에 긴밀한 종속성을 만듭니다. 대부분의 Java 또는 Scala 프레임 워크는 원하는 빌드 시스템과 함께 사용할 수 있습니다. 실제로 SBT를 제거하지는 않습니다. 마지막으로, 때때로 "정확한"클래스 로더를 사용하지 않는 특정 라이브러리를 사용하는 경우가 있습니다.

왜 JEE frameworks 는 불가능 한가? 


잠깐! 그게 단순한 이유라면 왜 누구나 그 기술을 사용하지 않는 것이지 !!! ??? 내 사랑하는 "Java Enterprise Server®"는 매일 AGES를 위해 다시 시작되기를 기다리고. 루비온레일스는 나를 놀리고 있는데요. 왜 내 시간을 낭비하고, 나를 비참하게 느끼게 하는지..

Enterprise Java Architect, having an epiphany



대답은 꽤 간단합니다. 당신의 응용 프로그램은 상태가 있기 때문이죠.

요청간에 객체 인스턴스를 메모리에 보관하고, 코드가 변경되었다고 가정하면, "오래된" 객체 인스턴스로 무엇을 하나요? 클래스 정의가 변경 되었을 수 있습니다. 클래스 로더를 삭제하면 생성 한 모든 인스턴스도 삭제됩니다. 이는 일반적으로 각각의 reload에서 모든 세션 데이터를 잃어 버리는 것을 의미하는데, 전형적인 어플리케이션에서는 매우 성가시게됩니다.

JEE 응용 프로그램에서 재배포 한다는 것은 오랜 시간이 걸리는 응용 프로그램 서버 (JNDI, 데이터베이스 연결 풀, EBJ, JMX, CDI 등)가 제공하는 많은 "서비스"를 다시 시작하는 것을 의미합니다. 핫리로드 만이 유일한 문제는 아닙니다.

플레이는 상태가 없으며, 상당히 가볍기 때문에 모든 것을 버리는 것은 별 문제가 아닙니다.

엔터프라이즈 애플리케이션 서버의 "포괄적인" 철학은 비용이 추가 책정됩니다. 그 포괄적인 것에서 10%도 제대로 사용하지 않는것을 생각하면 뭐..

뭐 ..그냥 JRebel! 사용할래요.

응?!

Jrebel은 오픈 소스가 아니기 때문에 정확히 어떻게 작동하는지 알기가 어렵습니다. 그들의 FAQ 에 많은 정보가 있지 않지만 , 어쨌꺼나 기본 사항을 설명하는 블로그 게시물들은 있는데요.

간단히 설명하면 JVM에 로드된 바이트 코드를 다시 작성하는 Java 에이전트를 추가합니다. 다음을 보시죠.

  • 객체에 추가 된 필드에 대한 참조를 보유하는 각 클래스에 필드를 추가합니다.
  • 실제 메서드 구현으로 익명 클래스에 대한 참조를 보유하는 각 클래스에 필드를 추가합니다.
  • 위의 변경 사항을 마스크 하기 위해 클래스들의 메소드를 수정하라.

물론 클래스가 수정 될 때 변경 영향을 추적하고 이에 따라 클래스를 "다시로드" 해야합니다. 그것은 힘든 일이죠.

3줄요약 

  • Play는 애플리케이션 클래스로더를 폐기하고 ,모든 것을 다시 시작함으로써 간단히 코드를 리로드합니다. 이는 프레임 워크에 깊이 통합된 SBT의 비용으로 발생합니다.
  • JEE 아키텍처는 상태유지타입이며, 어쨌든 그러한 종류의 기능을 허락하기에는 너무 무겁습니다.
  • JRebel은 플레이와 비교하면 대수술입니다.






Play2 는 뼛속부터 비동기로 이루어져 있기 때문에 최강성능&부드러운 서버라는 장점을 가지고 있지만, 비동기라는 그리 직관적이지 않은 기술을 내부에 포함하고 있기 때문에 때론 굉장히 헥깔리게 만들기도 합니다. 하지만 Scala 언어및 다양한 동시성 라이브러리의 지원으로 추상층을 끌어올려 아주 간단한 코드로 그런 강력한 능력을 얻게 해주니깐 걱정마세요 ^^

우리가 SQL 문을 작성할때, 그 짧은 코딩으로 매우 많은 일들이 물밑에서 이루어지는 것처럼 즉 모든것을 알지 않아도 편하게 소기의 성과를 이루는 것처럼, Play2 내부에서 이루어지는 모든 것들을 상세하게 이해하지 않아도 됩니다. "해결" 을 하는게 응용개발자의 목적이니까요.  

이제 Play2 에서 HTTP 안의 body 를 어떻게 다루는지 살펴보겠습니다.


선행 참고) 

스프링에서 컨트롤러 함수의 예가 다음과 같은 모습을 취하는 반면 

@RequestMapping(method = RequestMethod.GET)
public String printHello(ModelMap model) {
model.addAttribute("message", "Hello Spring MVC Framework!");
return "hello";
}

Play2 웹개발에서는 아래와 같습니다. (위의 자바 예와 동일한 내용의 예는 아닙니다.) 

def doSomething = Action{
Ok.apply(views.html.index("Hi there"))
}


Body parsers


바디 파서란 무엇인가요? 

HTTP 요청은 보통 헤더와 함께 body가 따라옵니다. 헤더는 일반적으로 매우 작기 때문에  메모리에 안전하게 버퍼링 될 수 있기 때문에 Play에서는 RequestHeader 클래스를 사용하여 그 내용을 모델링하죠. body는 어떤 경우에는 매우 길 수도 있어서 메모리에 버퍼링 되지 않고 스트림으로 모델링 됩니다.그러나 대부분 request body 페이로드는 작고 메모리에서 모델링 될 수 있으므로 body 스트림을 메모리의 객체에 매핑하기 위해 Play는 BodyParser 를 제공합니다.

Play는 비동기향 프레임워크 이므로 전통적인 InputStream은 요청 본문을 읽는 데 사용할 수 없습니다. 입력 스트림이 블로킹하는 경우 읽기를 호출하면 호출하는 스레드가 데이터를 사용할 수있을 때까지 기다려야합니다. 대신 PlayAkka Streams라는 비동기 스트리밍 라이브러리를 사용하는데요. Akka Streams는 많은 비동기식 스트리밍 API가 원활하게 작동 할 수 있도록 해주는 Reactive Streams의 구현입니다. 따라서 전통적인 InputStream 기반 기술은 Play와 함께 사용하기에 적합하지 않으며 대신  Akka Streams 및 Reactive Streams 주변의 비동기 라이브러리의 생태계는 당신이 필요한 모든 것을 가지고 있습니다.


액션(Actions) 에 대하여

이전에 Action이 Request => Result 함수라고 했었던 적이 있는데요. 이것은 사실이 아닙니다. Action 트레잇을 보다 자세하게 살펴 보죠. 

* 보통 우리는 이렇게 사용했었죠?

Action { request =>
  Ok("Got request [" + request + "]")
}

* trait 은 이렇습니다. (역주: 사실 이것을 정확히 이해하려면  Request[A] => Result 라는 함수리터럴이 클래스라는 것과 내부에 apply 메소드를 가진다는 것을 알아야한다. 즉 스칼라언어에 대한 기초가 전제됨.) 

trait Action[A] extends (Request[A] => Result) {
  def parser: BodyParser[A]
}

제너릭 타입 A가 있으며, 액션에서 BodyParser [A]를 정의해야 한다는 것을 알 수 있습니다.
이어서 Request[A]는 다음과 같이 정의됩니다.

trait Request[+A] extends RequestHeader {
  def body: A
}

A 타입은 리퀘스트 바디의 타입이구요. 그런 리퀘스트 바디 타입은 스칼라 타입으로 (예 : String, NodeSeq, Array [Byte], JsonValue 또는 java.io.File) 사용할 수 있습니다.

요약하면 Action [A]는 BodyParser [A]를 사용하여 HTTP 요청에서 A 타입의 값을 검색&뽑아내고 Action apply 코드로 전달합니다.

기본으로 제공하는 바디 파서들 

일반적인 웹앱들 경우 굳이 커스텀 파서를 만들어서 사용할 필요가 없구요. Play의 내장형 파서로 간단하게 작업 할 수 있습니다. 여기에는 JSON, XML, Forms 용 파서가 포함되며 일반 텍스트 본문을 String으로 처리하고 ByteString으로 바이트 body를 처리합니다.


기본 파디 파서 

명시적으로 바디 파서를 선택하지 않은 경우에 적용되는 기본 본문 파서의 경우,  
들어오는 Content-Type 헤더를 살펴보고 이에 따라 본문을 파싱 합니다.
예를 들어, application / json 유형의 Content-Type은 JsValue로 구문 분석되지만
Application / x-www-form-urlencoded의 Content-Type은 Map [String, Seq [String]]으로 구문 분석됩니다.

기본 바디 파서는 AnyContent 타입의 바디를 만들어내는데요.  AnyContent에서 지원하는 다양한 타입은 asJson과 같은 as 메소드로 액세스 할 수 있습니다. 이것은 body 에서 Option 타입을 반환 하구요.

코드를 보고 이해를 해봅시다.

def save = Action { request =>
  val body: AnyContent = request.body
  val jsonBody: Option[JsValue] = body.asJson

  // Expecting json body
  jsonBody.map { json =>
    Ok("Got: " + (json \ "name").as[String])
  }.getOrElse {
    BadRequest("Expecting application/json request body")
  }
}

다음은 기본 바디 파서가 지원하는 타입들의 매핑입니다.

text/plain: String, accessible via asText.

application/json: JsValue, accessible via asJson.

application/xml, text/xml or application/XXX+xml: scala.xml.NodeSeq, accessible via asXml.

application/x-www-form-urlencoded: Map[String, Seq[String]], accessible via asFormUrlEncoded.

multipart/form-data: MultipartFormData, accessible via asMultipartFormData.

Any other content type: RawBuffer, accessible via asRaw.


성능상의 이유로 기본 body Parser는 HTTP spec에 정의 된 데로 request 메서드가 의미있는 본문으로 정의되지 않았다면 본문을 구문 분석하지 않습니다. 이는 POST, PUT 및 PATCH 요청의 바디만 구문 분석하지만 GET, HEAD 또는 DELETE는 분석하지 않는다는 것을 의미합니다. 이 메소드에 대한 요청 본문을 구문 분석하려면 아래에 설명 된 anyContent 본문 파서를 사용할 수 있습니다.


명시적 바디 파서 선택하기 

바디 파서를 명시적으로 선택하려면 바디 파서를 Action apply 또는 async 메서드에 직접 전달하여 수행 할 수 있습니다. Play는 여러 개의 바디 파서를 기본적으로 제공하는데 이는 BodyParsers.parse 객체를 통해 사용할 수 있습니다. 이 객체는 Controller 트레잇으로 편리하게 가져옵니다.

예를 들어, json 본문을 기대하는 동작을 정의하려면 (앞의 예와 같이) :

def save = Action(parse.json) { request =>
  Ok("Got: " + (request.body \ "name").as[String])
}

바디의 타입이 첨부터 JsValue 이므로 더 이상 Option 타입이 아니기 때문에 바디로 작업하기가 더 쉬워졌습니다. 이것이 옵션이 아닌 이유는 json body 파서가 request 에 있는 Content-Type이 application / json임을 먼저 확인하고 실제 요청이 그것과 다르면 415 Unsupported Media Type 응답을 되돌려 보내기 때문입니다. 따라서 우리의 Action 코드에서 다시 확인할 필요가 없는거죠.

이것은 물론 클라이언트가 요청시 올바른 Content-Type 헤더를 보내고 모든것이 제대로 작동되는 경우이며, 조금 더 편하게하고 싶다면, Content-Type을 무시하고 body를 json으로 구문 분석하려고 시도하는 tolerantJson을 대신 사용할 수 있습니다. 아래와 같은 : 

def save = Action(parse.tolerantJson) { request =>
  Ok("Got: " + (request.body \ "name").as[String])
}

다음은 request 바디를 파일에 저장하는  예제입니다.

def save = Action(parse.file(to = new File("/tmp/upload"))) { request =>
  Ok("Saved the request content to " + request.body)
}


바디 파서를 서로 엮기 (Combining) 

이전 예에서 모든 request 바디는 동일한 파일에 저장되는데 , 사실 이렇게 사용하지는 않지 않을까요? 요청 세션에서 사용자 이름을 추출하여 각 사용자에 대해 고유 한 파일을 제공하는 다른 방식의 사용자 정의 바디 파서를 작성해 보겠겠습니다.

val storeInUserFile = parse.using { request =>
  request.session.get("username").map { user =>
    file(to = new File("/tmp/" + user + ".upload"))
  }.getOrElse {
    sys.error("You don't have the right to upload here")
  }
}

def save = Action(storeInUserFile) { request =>
  Ok("Saved the request content to " + request.body)
}

Note: 여기서는 실제로 우리 자신의 BodyParser 를 작성하는 것이 아니라, 기존의 BodyParser 들을 결합하는 것입니다. BodyParser 를 바닥부터 다루는 방법은 고급 항목 섹션에서 다룹니다.


최대 내용 길이(Max content length)

text, json, xml 또는 formUrlEncoded와 같은 텍스트 기반 바디 파서는 모든 내용을 메모리에 로드 해야하기 때문에 최대 내용 길이를 사용합니다. 기본적으로 구문 분석 할 최대 콘텐츠 길이는 100KB 이며  application.confplay.http.parser.maxMemoryBuffer 속성을 지정하면 재정의 할 수 있습니다.

play.http.parser.maxMemoryBuffer=128K

raw 파서나 multipart / form-data와 같이 디스크의 내용을 버퍼링하는 파서의 경우 최대 내용 길이는 play.http.parser.maxDiskBuffer 속성을 사용하여 지정됩니다. 기본값은 10MB 이며  multipart / form-data 파서는 데이터 필드의 집계에 text max length 속성을 적용합니다.

주어진 동작에 대한 기본 최대 길이를 재정의 할 수도 있습니다.

// Accept only 10KB of data.
def save = Action(parse.text(maxLength = 1024 * 10)) { request =>
  Ok("Got: " + text)
}

maxLength를 사용하여 바디 파서를 래핑 할 수도 있구요.

// Accept only 10KB of data.
def save = Action(parse.maxLength(1024 * 10, storeInUserFile)) { request =>
  Ok("Saved the request content to " + request.body)
}


사용자 정의 바디 파서 작성

BodyParser 트레잇을 구현하여 사용자 정의 파서를 만들 수 있습니다. 이 트레잇은 그저 단순한 함수입니다.

trait BodyParser[+A] extends (RequestHeader => Accumulator[ByteString, Either[Result, A]])

이 함수의 시그니처는 처음에는 조금 어려울 수 있는데요.

이 함수는 RequestHeader를 사용합니다. 이것은 요청에 대한 정보를 확인하는 데 사용할 수 있으며 가장 일반적으로 Content-Type을 가져와서 바디를 올바르게 파싱 할 수 있습니다.

함수의 반환 형식은 Accumulator(누적기) 이며 Akka Streams Sink 주변의 얇은 레이어입니다. 이 누적기는 비동기적으로 요소 스트림을 결과로 누적하며 Akka 스트림 소스를 전달하여 실행할 수 있으며  완료시 사용할 Future 를 반환합니다. 그것은 Sink [E, Future [A]]와 본질적으로 동일하며 타입을 감싸는 래퍼 일뿐입니다.

그러나 Sink 와의 큰 차이점은 Accumulator가 map, mapFuture, recover 등과 같은 편리한 메소드를 제공한다는 것입니다. Sink의 경우는 이 모든 작업을 promise과 같은 결과로 작업 할 경우  mapMaterializedValue 호출로 래핑해야 한다는 점이죠. 

 apply 메소드가 반환하는 누적기는 ByteString 유형의 요소를 사용합니다. 이는 본질적으로 바이트 배열이지만 ByteString은 변경 불가능하고 바이트 및 슬라이스와 같은 많은 작업이 일정 (constant) 시간 내에 발생한다는 점에서 바이트 []와는 다릅니다. 

누적기의 반환 타입은 Either[Result, A] 입니다. Result를 반환하거나 A 타입의 본문을 반환한다는 것이죠. 일반적으로 오류가 발생한 경우 Result가 반환됩니다. 오류는 Content-Type이 바디 파서가 수락하는 타입과 일치하지 않거나 메모리 버퍼가 초과 된 경우 발생합니다. 


body 를 다른 어딘가로 직접 전송하기

바디 파서를 작성하는 일반적인 사용 사례는 실제로 바디를 파싱하고 싶지 않을 때를 위한 것 입니다. 무슨 얘기냐 하면 다른 곳으로 스트리밍 하려고 할 때를 말하는데요. 다음 코드를 보시죠.

import javax.inject._
import play.api.mvc._
import play.api.libs.streams._
import play.api.libs.ws._
import scala.concurrent.ExecutionContext
import akka.util.ByteString

class MyController @Inject() (ws: WSClient)(implicit ec: ExecutionContext) {

  def forward(request: WSRequest): BodyParser[WSResponse] = BodyParser { req =>
    Accumulator.source[ByteString].mapFuture { source =>
      request
        // TODO: stream body when support is implemented
        // .withBody(source)
        .execute()
        .map(Right.apply)
    }
  }

  def myAction = Action(forward(ws.url("https://example.com"))) { req =>
    Ok("Uploaded")
  }
}


Akka Streams 을 사용하여 파싱하기 

드문 경우지만 Akka Streams를 사용하여 사용자 지정 파서를 작성해야 할 수도 있습니다. 대부분의 경우, 대부분의 케이스에는 ByteString에 바디를 버퍼링하는 것으로 충분합니다.이 메서드는 명령형 메서드와 임의 액세스를 바디에 사용할 수 있기 때문에 일반적으로 훨씬 간단한 구문 분석 방법을 제공합니다.

그러나, 가능하지 않은 경우, 예를 들어 파싱해야 하는 본문이 너무 길어서 메모리에 저장되지 않는 경우와 같이 사용자 정의 파서를 작성해야 할 수도 있습니다.

Akka Streams 사용법에 대한 자세한 설명은 이 설명서의 범위를 벗어납니다. 시작하는 가장 좋은 방법은  Akka Streams documentation 설명서를 읽는 것이지만 아래 코드에서 Akka Streams 쿡북의 Parsing lines from a stream of ByteStrings 을 기반으로 하는 CSV 파서를 보여드리죠.

import play.api.mvc._
import play.api.libs.streams._
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import akka.util.ByteString
import akka.stream.scaladsl._

val csv: BodyParser[Seq[Seq[String]]] = BodyParser { req =>

  // A flow that splits the stream into CSV lines
  val sink: Sink[ByteString, Future[Seq[Seq[String]]]] = Flow[ByteString]
    // We split by the new line character, allowing a maximum of 1000 characters per line
    .via(Framing.delimiter(ByteString("\n"), 1000, allowTruncation = true))
    // Turn each line to a String and split it by commas
    .map(_.utf8String.trim.split(",").toSeq)
    // Now we fold it into a list
    .toMat(Sink.fold(Seq.empty[Seq[String]])(_ :+ _))(Keep.right)

  // Convert the body to a Right either
  Accumulator(sink).map(Right.apply)
}





평범한 사람들을 위한 Play2 Iteratees 의 이해 



http://mandubian.com/2012/08/27/understanding-play2-iteratees-for-normal-humans/   
내용을 번
역하고 감수했습니다. 후반부에 많이 졸렸지만... Scala 와 Play2 를 시작하는 분에겐 도움이 되었으면 합니다.  좀더 간단한 내용을 원하시는 아래 링크도 참고 하십시요.
http://hamait.tistory.com/767


Play2 를 시작하고 나면 아마  Iteratee 와 그의 형제들인  Enumerator and Enumeratee에 대해 관심이 생겼을꺼야. 그리고 나서 좀 어버버하겠지 ㅋㅋ 이 기사의 목적은 어버버하고 있는 평범한 우리 모두를 위한 정리라고 보면 되. 거창한 함수형/수학 이론을 배제하고 말이지.



이 게시물은 Iteratee / Enumerator / Enumeratee 모든것을 설명하는 것이 아님. 의도정도? 
나중에 기회되면 Iteratee/Enumerator/Enumeratee 에 관련된 주요 실습코드에 대해 작성할 예정.


소개

Play2 doc 문서에서 Iteratees 는 데이타 스트림을 논블럭에서 리액티브적으로 조작할 수 있고 분산환경에서 현대적인 웹 프로그래밍을 할때  일반적이고 강한 구성을 할 수 있는 툴이라고 소개한다.

 먼가 쩔지? 그렇지 않나?
 근데 Iteratee 가 정확히 먼데?
 Iteratee 와 일반적인 Iterator 와 무슨 차이가 있어?
 어떻게 사용하고 어떤 상황에서 사용하지?
 꽤 복잡해 보이는데  안그런가?

만약 당신이 실용주의자 (게으르고) 이고 필요한 몇가지 것들만 알길 원한다면 

  • Iteratee 는 넌블럭과 비동기적으로 데이터들을 순회하는 추상층이다. 
  • Iteratee Enumerator 라 불리는 (소비하는것과) 동일한 타입의 데이타 뭉치들을 생산하는 것으로 부터 해당 타입의 데이터를 소비 하는데 사용된다.
  • Iteratee 는 iteration 스텝마다 데이터 청크에 대한 결과를 연속,순차적으로 계산 할 수 있다.
  • Iteratee 는 여러 "Enumerators" 를 넘어 재사용될 수 있게 쓰레드 안전하고 불변객체를 다룬다.

첫번째 충고 : GOOGLE 에서 ITERATEES 에 대한 정보를 찾지 마라.  

Google에서 Iteratee를 검색하면 순수한 기능적 접근 방식이나 수학 이론을 기반으로 하는 매우 모호한 설명을 찾을 수 있다. 심지어 Play Framework에 대한 문서  (역주:  초보자에게 매우 어려울 수도 있는 low 접근법에 대해 Iteratee 를 설명함 ;;; 사실 Play2 에 대한 설명은 대부분 어려운거 같다. 그러다 보니 유튜브나 블로그를 통한 쉽게 설명한 글이 나오지도 않는거 같다. 원래 직관적이지 않은 비동기는 어려우니깐 라고 말하고 싶기도 하다.) 도 마찬가지~

Play2의 초심자로서, 데이터 청크를 조작하는 추상적인 방식으로 제시 된 Iteratee 개념을 다루는 것은 어려울 수도 있긴해요.. 희귀하고 오컬트적으로 복잡하게 보일 수 도 있고..그래서 사용하기 싫어지고..

하지만 Iteratees가 웹 애플리케이션에서 데이터 흐름을 조작하는 데  흥미롭고 강력한 새로운 방법을 제공하기 때문에 이 훌륭한 개념을 사용 안 하는것은 부끄러운 일이다. 포기하지마시라~

나는 여러분들을 포기하고 싶지 않기 때문에 최대한 간단한 방법으로 설명하려고 한다. 기능적 개념에 대한 이론적인 전문가인 척하지 않을 것이며 ,뭐 잘못된 것들을 말할 수도 있지만 Iteratee가 의미하는 것을 평범한 여러분이 잘 반영 할 수 있는 방법으로 글을 쓸 것이다. 


이 기사에서는 코드 샘플에 대해 스칼라를 사용하지만 코딩에 대한 어느정도의 개념을 갖춘 사람이라면 코드를 이해 할 수 있을 것으로 본다. 따라서 너무 심하게 이질적인 연산자를 사용하지는 않기로 약속하며 > <>> <>> <>> <> . 코드 샘플은  Play2.1 마스터 코드를 기반으로 하므로 Iteratee의 코드가 단순화되고 앞으로 사용하기에는 더 합리적일 것이다.  따라서 API가 Play2.0.x와 같이 보이지 않는 경우 놀랄 필요가 없습니다요.


iteration 떠올려 보기  

Iteratee 의 바다로 뛰어 들기 전에, 나는 내가 iteration 이라고 부르는 것을 명확히 하고 Iterator의 개념에서 Iteratee로 점진적으로 가려고 한다.

Java에서 찾을 수 있는 Iterator 개념을 알죠?  반복자 (Iterator)는 컬렉션에 대해  반복의 각 단계에서 무언가를 수행 한다. List [Int]의 모든 정수를 합하는 매우 간단한 반복부터 시작해본다.


 Iterator의 자바스러운 구현 방식 

val l = List(1, 234, 455, 987)

var total = 0 
var it = l.iterator
while( it.hasNext ) {
  total += it.next
}

total
=> resXXX : Int = 1677

특별할것도 없다. 컬렉션을 순회하는 이 코드의 의미는:

  • 컬렉션에서 iterator 를 얻고,
  • itorator 로 부터 요소를 얻고 (만약 있다면 ) ,
  • 뭔가를 하고 : 여기서는 요소값을 total 변수에 더함 ,
  • 요소가 더 있다면 다음 요소로 이동
  • 반복
  • 요소가 없을 때까지 순회한다..

클렉션을 순회하는 동안 우리가 조작 하는 것들은:

  • iteration 의 상태 ( 끝났냐? 더 이상 요소가 없나?)
  • A context 업데이트 (여기서는 total) 
  • An action updating the context

 Scala for-comprehension 으로 다시 작성해보자

for( item <- l ) { total += item }

직접 iterator 를 사용하는 것 보다 나아졌다.


좀 더 함수형적인 방식으로 다시 작성해보자

l.foreach{ item => total += item }

List.foreach 함수는 익명 함수 (Int => Unit)를 매개 변수로 받아들이고 목록을 반복한다. 목록의 각 요소에 대해 컨텍스트 (여기서는 총계)를 업데이트 할 수있는 함수를 호출한다.

익명 함수는 컬렉션을 반복하는 동안 각 루프에서 실행되는 액션을 포함한다.


더 일반적인방식으로 다시 작성해보자

익명의 함수는 다른 장소에서 재사용 될 수 있도록 변수에 저장 될 수 있습니다.

val l = List(1, 234, 455, 987)
val l2 = List(134, 664, 987, 456)
var total = 0
def step(item: Int) = total += item
l foreach step

total = 0
l2 foreach step

아마 나에게 말하고 싶은게 있을거 같다: "이것은 구린 디자인이야. 저 함수는 부수효과를 가지고 있고, 좋은 디자인이 아닌 변수를 사용하고 있지. 심지어 두 번째 호출에서는 총계를 0으로 재설정해야해."

사실 맞는 말이다 -.-:

부수효과를 가진 함수는 꽤 위험하지  왜냐하면 그것들은 함수의 외부에 있는 어떤 것의 상태를 바꾸기 때문이야. 이 상태는 함수와 배타적이지 않고 잠재적으로 다른 스레드의 다른 엔티티에 의해 변경 될 수 있어. 부수효과가 있는 함수는 깨끗하고 견고한 디자인을 권장하지 않으며 스칼라와 같은 함수 언어는 이런 함수를 엄격하게 (예 : IO 작업) 줄이는 경향이 있다.

변형가능 변수는 위험성을 내포하고 있어 왜냐하면 2 개의 쓰레드가 변수의 값을 변경하려 한다면, 누가 이기지? 이 경우에는 Play2 (비 블로킹 웹앱)가 쩌는 웹프레임워크가 되는 이유 중 하나를 망가뜨리는 변수를 쓰는 동안 스레드 차단을 의미하는 동기화가 필요해.. 낭비지..

부수효과 없는 불변적인 방식으로 코드 재작성하기 

def foreach(l: List[Int]) = {
  def step(l: List[Int], total: Int): Int = {
    l match {
      case List() => total
      case List(elt) => total + elt
      case head :: tail => step(tail, total + head)
    }
  }

  step(l, 0)
}

foreach(l)

코드가 꽤 늘었네 그렇지?

하지만 적어도 

  • var total 가 사라졌고.
  • step 함수는 반복의 각 단계에서 실행되는 동작으로 이전보다 더 많은 작업을 수행하며 step 는 반복 상태도 관리하며 다음과 같이 실행됩니다.

    • 만약 리스트가 비었으면 현재 total 을 리턴
    • 만약 요소가 리스트에 한개라면 total + elt 를 리턴 
    • 만약 리스트에 1개보다 더 많다면, step 를 tail 요소들과 리턴, 새로운 total은  total + head게됨


따라서 반복의 각 단계에서 이전 반복의 결과에 따라 단계는 2 가지 상태 중 하나를 선택할 수 있다.

  • 요소가 더 많으므로 반복을 계속하십시오.
  • 목록의 끝에 도달했거나 요소가 전혀 없으므로 반복을 중지하십시오.

알림 :

  • step 은 꼬리재귀 함수(재귀가 끝날 때 전체 호출 스택을 펼치지 않고 즉시 반환합니다.) 스택 오버플로를 방지하고 Iterator를 사용하여 이전 코드와 거의 비슷하게 동작합니다.) 
  • step 목록의 나머지 요소 및 새 합계를 다음 단계로 전송합니다.
  • step 부작용이없는 총계를 반환합니다.

그래서,이 코드는 각 단계에서 목록의 일부를 다시 복사하기 때문에 (단지 요소에 대한 참조만) 부작용이 없고 불변의 데이터 구조만 사용하기 때문에 조금 더 많은 메모리를 사용한다. 이것은 매우 강력하고 아무 문제없이 편한 마음의 배포를 할 수 있게 된다.


스칼라 컬렉션이 제공하는 훌륭한 기능을 사용하여 매우 짧은 방법으로 코드를 작성할 수있다.

l.foldLeft(0){ (total, elt) => total + elt }


이제 본론으로 들어가 보죠


하나씩  Iterator & Iteratees 에 대해 알아보자

이제 반복에 대해 명확히 했으므로 우리의 반복문으로 돌아가 보자!


이전 반복 메커니즘을 일반화 하고 다음과 같이 작성할 수 있다고 가정 해 보면:

def sumElements(...) = ...
def prodElements(...) = ...
def printElements(...) = ...

l.iterate(sumElements)
l.iterate(prodElements)
l.iterate(printElements)

예, 스칼라 컬렉션 API를 사용하면 많은 일을 할 수 있다 :)

다른것을 사용하여 첫 번째 반복과 구성 한다고 가정 해 보면:

def groupElements(...) = ...
def printElements(...) = ...

l.iterate(groupElements).iterate(printElements)


이 반복을 컬렉션이 아닌 다른 것에 적용하려고한다고 가정 해보자

  • 파일에 의해 점진적으로 생성되는 데이터 스트림, 네트워크 연결, 데이터베이스 연결,
  • 어떤 알고리즘에 의해 생성 된 데이터 흐름,
  • 스케줄러 또는 액터와 같은 비동기 데이터 생성자로부터의 데이터 흐름.

Iteratees는 정확히 이 의미이다 ...


여기에 반복문을 사용하여 이전 합계 반복을 작성하는 방법이 있다

val enumerator = Enumerator(1, 234, 455, 987)
enumerator.run( Iteratee.fold(0){ (total, elt) => total + elt } ) 

좋아, 이전 코드처럼 보이고 더 많은 일을 하지 않는 것 같군..

적어도, 그렇게 복잡하지는 않습니까? 그러나 코드를 보면 Iteratee는 Enumerator와 함께 사용되는 부분이 좀 어색할 것이며 이 두 개념이 밀접 하게 관련되어 있음을 알 수 있을 것이다.

앞으로 점점 둘 사이의 어색함은 줄어 들 것임을 약속한다.  
즉 enumerator 는 생산하고  Iteratee 는 그것을 가져다 무엇인가를 하며 소비하는 모습 말이다. 


이제 단계별 접근 방식으로 이러한 개념을 하나씩 살펴 보자.



><> Enumerator 에 대하여 ><>

Enumerator는 컬렉션 또는 배열보다 일반적인 개념이다.


지금까지 우리는 반복을 위해 컬렉션을 사용했지만 앞서 설명했듯이, 우리는 보다 일반적인 것들도 반복 할 수 있다. 즉각적으로 또는 비동기적으로 사용할 수 있는 간단한 데이터 청크를 생성 할 수 있을 것이다.

Enumerator 의 디자인 목적

간단한 Enumerator 의 몇 가지 예 :  

// 리스트와 같은 형식으로 다양한 타입에 대해 만들어지고 있는 모습

val stringEnumerator: Enumerator[String] = Enumerate("alpha", "beta", "gamma")

val integerEnumerator: Enumerator[Int] = Enumerate(123, 456, 789)

val doubleEnumerator: Enumerator[Double] = Enumerate(123.345, 456.543, 789.123)

// 파일로 부터 생산되는 모습 

val fileEnumerator: Enumerator[Array[Byte]] = Enumerator.fromFile("myfile.txt")

// 비동기적으로 즉흥적으로 만들어 지고 있는 모습 

val dateGenerator: Enumerator[String] = Enumerator.generateM(
  play.api.libs.concurrent.Promise.timeout(
    Some("current time %s".format((new java.util.Date()))),
    500
  )
)

Enumerator 는 정적으로 typed 된 데이터 청크의 PRODUCER이다.

Enumerator[E] 는 E 타입의 데이터 청크를 생성하며 다음과 같은 3 가지 종류가 있을 수 있다.

  • [E]는 타입 E의 데이터. 예를 들어, Input [피자]는 피자 데이터들 이다.
  • Input.Empty는 enumerator 가 비어 있음을 나타낸다. 예를 들어 빈 파일을 스트리밍하는 enumerator .
  • Input.EOF는 enumerator 가 끝났음을 의미한다.예를 들어, enumerator 는 파일을 스트리밍하고 파일의 끝에 도달 할 것이다.


위에서 제시한 청크 종류와 상태 (더있다 / 아니오 / 더 이상의 요소가 있음) 사이에 평행선을 그릴 수 있습니다.

사실, Enumerator[E] [E]는 Input[E]를 포함하므로 Input[E]을 입력 할 수 있습니다.

// create an enumerator containing one chunk of pizza

val pizza = Pizza("napolitana")
val enumerator: Enumerator[Pizza] = Enumerator.enumInput(Input.el(pizza))

// create an enumerator containing no pizza

val enumerator: Enumerator[Pizza] = Enumerator.enumInput(Input.Empty)

Enumerator 는 논블럭킹 생산자이다.


Play2의 주요 아이디어 및 강점은 넌블럭 & 비동기이다. 따라서, Enumerator / Iteratee는 이러한 철학을 반영하는게 당연하다. Enumerator 는 청크를 완전히 비동기 및 넌블럭 방식으로 생성하는데 즉, Enumerator 개념은 기본적으로 활성화 된 프로세스 또는 백그라운드 작업으로 인해 데이터 청크와 기본적인 관련이 없다고 할 수 있다.

위의 코드 조각을 dateGenerator 과 함께 기억하라. 이 코드는 Enumerator / Iteratee의 비동기 및 넌블럭 특성을 정확히 반영하고 있을까?

// an Enumerator generated by a callback

// it generates a string containing current time every 500 milliseconds

// notice the Promise.timeout which provide a non-blocking mechanism

val dateGenerator: Enumerator[String] = Enumerator.generateM(
  play.api.libs.concurrent.Promise.timeout(
    Some("current time %s".format((new java.util.Date()))),
    500
  )
)


What’s a Promise?

.

그것을 이해하기 위해서는 새로운 글타래가 요구될 것이지만 대략적으로 말해보면 Promise [String]은 다음과 같은 의미이다. "미래에 있을  String 결과(또는 오류)를 제공 하는 것."  한편 현재 스레드를 차단하지 않고 바로 해제한다. 

역주 : Future 와 구분된다. 자세히 알고 싶으면 아래를 참고하시라.
Promise 란 ? http://hamait.tistory.com/771

Future 란? http://hamait.tistory.com/763

Enumerator 로 생산되는 것을 소비할 소비자가 필요하다.


넌블럭 속성으로 인해 아무도 이러한 청크를 사용하지 않으면 Enumerator 는 아무 것도 차단하지 않고 또한 아무 숨겨진 런타임 리소스를 소비하지도 않게된다.따라서 Enumerator는 소비 할 사람이 있는 경우에만 데이터 청크를 생성 하는게 의미 있어 진다.

그렇다면 Enumerator 에 의해 생성 된 데이터 청크들을 소비하는 것은 무엇일까?

빙고!!!  : Iteratee이다.


><> Iteratee 에 대하여 ><>

Iteratee 는 Enumerator를 반복 할 수 있는 일반적인 "도구" 이다. 


한 문장으로 뽑아본다면:

Iteratee는 순수 함수 프로그래밍에서 반복 개념의 일반화로 볼 수 있다.

Iterator는 반복되는 컬렉션에서 만들어 지지만 Iteratee는 반복되는 Enumerator에 대해 존재하는 일반 엔터티이다.


Iterator와 Iteratee의 차이점에 대해서 기억이 납니까?  아니라구요.....: 헐..

  •  Iteratee는 Enumerator (또는 다른 것)에 의해 생성된 데이터를 반복 할 수있는 일반적인 엔티티이다.
  •  Iteratee는 반복될 Enumerator와는 별도로 생성되고 Enumerator 가 제공됩니다.
  •  Iteratee immutable 이고 stateless 이며 서로 다른 enumerators 를 위해 재사용될 수 있다.

한마디로

Iteratee는 Enumerator에 적용되거나 Enumerator를 통해 실행됩니다.


Enumerator [Int]의 모든 요소의 합계를 계산하는 위의 예제가 기억나는지 모르겠다. Iteratee를 한 번 만들고 다른 Enumerators 에서 여러 번 재사용 할 수 있다는 것을 보여주는 동일한 코드가 있다. 

아래 코드를 보자.

val iterator = Iteratee.fold(0){ (total, elt) => total + elt }

val e1 = Enumerator(1, 234, 455, 987)
val e2 = Enumerator(345, 123, 476, 187687)


e1(iterator)      // or e1.apply(iterator)
e2(iterator)


val result1 = e1.run(iterator) // or e1 run iterator
val result2 = e2.run(iterator)

Enumerator.apply와 Enumerator.run은 약간 다른 함수이며 나중에 설명 할 것이니 안심하시라.

Iteratee 는 active 한 데이터 소비자이다.

기본적으로 Iteratee는 첫 번째 데이터 청크를 기다리고 바로 다음에 반복 메커니즘을 시작한다. Iteratee는 계산이 끝났다고 생각할 때까지 데이터 소비를 계속하며 초기화가 완료되면 Iteratee는 전체 반복 프로세스를 완전히 담당하고 중지 시기를 결정한다.


val iterator = Iteratee.fold(0){ (total, elt) => total + elt }

val e = Enumerator(1, 234, 455, 987)

// this injects the enumerator into the iteratee 

// = pushes the first chunk of data into the iteratee

enumerator(iterator)

// the iteratee then consumes as many chunks as it requires

// don't bother about the result of this, we will explain later

위에서 설명한 것처럼 Enumerator는 데이터 청크 생산자이며 소비자가 이러한 데이터 청크를 소비 할 것으로 기대된다. 소비되고 반복되는것을 위해서는 Enumerator는 Iteratee 에 삽입 되거나 플러그드 되야 하며, 보다 정확하게는 첫 번째 데이터 묶음을 Iteratee에 주입 / 푸시 해야한다.

당연히 Iteratee는 Enumerator의 생산 속도에 의존된다. 느린 경우 Iteratee도 느리다.

종속성주입및 제어역전 패턴과 비슷하게 Iteratee / Enumerator 관계를 생각해 볼 수 있습니다.

Iteratee Iteratee는 ”1-chunk-loop” 함수이다.

Iteratee는 iteration이 끝났다고 생각할 때까지 하나씩 청크를 소비한다. 사실, Iteratee의 실제 범위는 덩어리 하나의 처리로 제한되며 이것이 하나의 데이터 청크를 소비 할 수 있는 함수로 정의 할 수 있는 이유이다.

Iteratee 정적 타입된 청크를 허용하고 정적 타입된 결과를 계산한다.

Iterator는 생성된 컬렉션에서 오는 데이터 청크를 반복하는 반면,  Iteratee는 좀 더 야심적입니다. 즉, 데이터를 소비하면서 무언가를 계산할 수 있습니다.

 Iteratee 모습이 이러기 때문인데 :

trait Iteratee[E, +A]

// E 는 데이터 청크들의 데이터의 타입.  따라서 오직 Enumerator[E] 에 의해 적용 됩니다.

// A 는 iteration 의 결과 타입 입니다. (역주: +A 는 C[T’]는 C[T]의 하위 클래스이다라는 공변성을 말합니다.)


첫 번째 샘플로 돌아가 봅시다 : Enumerator[Int] 가 생성한 모든 정수의 합계를 계산합니다.


val iterator = Iteratee.fold(0){ (total, elt) => total + elt }
val e = Enumerator(1, 234, 455, 987)

// runs the iteratee over the enumerator and retrieve the result

val total: Promise[Int] = enumerator run iterator


run의 사용법에 주목하자 : 우리는 비동기적인 세계에 있기 때문에 합계 자체 value 가 아니라 합계가 Promise [Int]임을 알 수있다. 실제 합계를 얻으려면 scala concurrent blocking 인 Await._ 함수를 사용할 수 있다. 그러나 이것은 블로킹 API 이기 때문에 반드시 필요한 곳이 아니라면 권장하지 않는다.  Play2는 완전히 비동기 / 논블럭이므로 Promise.map / flatMap을 사용하여 전파하는 것이 가장 좋겠다.


하나의 결과값을 얻기 위한 행위 뿐 아니라, 소비되는 모든 청크를 println 할 수 도 있고  

val e = Enumerator(1, 234, 455, 987)
e(Iteratee.foreach( println _ ))


e.apply(Iteratee.foreach( println _ ))


모든 청크를 List로 연결 할 수 도 있다. 예를 들어:

val enumerator = Enumerator(1, 234, 455, 987)

val list: Promise[List[Int]] = enumerator run Iteratee.getChunks[Int]


Iteratee 는 반복을 통해 불변 컨텍스트 및 상태를 전파 할 수 있다.

최종 합계를 계산할 수 있으려면 Iteratee는 부분 합계를 반복 단계를 따라 전파해야 한다.

즉, Iteratee는 이전 단계에서 컨텍스트 (이전의 총합)를 수신 한 다음 현재 데이터 덩어리로 새 컨텍스트를 계산할 수 있다. (새 총계 = 이전의 총계 + 현재 요소)  그리고 이 컨텍스트를 다음 단계 (다음 단계가 필요할 경우) 로 전파한다. 


Iteratee 는 간단한 상태 머신이다.

좋아, 멋지다. 하지만 Iteratee가 iterating을 멈추어야 한다는 것을 어떻게 알 수 있을까? 오류 혹은 EOF가 있거나 Enumerator 의 끝에 도달하면 어떻게 될까?

따라서 컨텍스트 외에도 Iteratee는 이전 상태를 받아야 할 일을 결정하고 다음 단계로 보내질 새 상태를 잠재적으로 계산해야 한다.

이제 위에서 설명한 고전적인 반복 상태를 기억하라. Iteratee의 경우 거의 동일한 2 가지 반복 상태가 있다.

  • State Cont : 반복은 다음 청크로 계속 진행될 수 있으며 잠재적으로 새로운 컨텍스트를 계산할 수 있다.
  • State Done : 프로세스가 끝났음을 알리고 결과 컨텍스트 값을 반환 할 수 있다.

그리고 꽤 논리적으로 보이는 제 3의 것:

  • State Error : 현재 단계에서 오류가 발생했음을 알리고 반복 반복을 중지합니다.


이러한 관점에서 우리는 Iteratee가  상태를 Done 또는 Error 로 전환하기 위한 조건을 탐지 할 때까지 상태 Cont를 반복하는 것을 담당하는 상태 기계라고 생각할 수 있게된다.

Iteratee 상태인 Done/Error/Cont 또한  Iteratee이다.

Iteratee는1-chunk-loop 함수로 정의되며 주요 목적은 하나의 상태에서 다른 상태로 변경하는 것임을 기억하시라. 그것들도 Iteratee 라고 생각해야한다.

우리는 3가지 상태의  Iteratees 를 가지고 있다.

Done[E, A](a: A, remaining: Input[E])

  • a:A 이전 단계로 부터 받은 컨텍스트
  • remaining: Input[E] 다음 청크를 표현 


Error[E](msg: String, input: Input[E])

이건 매우 이해하기 쉽습니다 : 오류 메시지와 실패한 입력.


Cont[E, A](k: Input[E] => Iteratee[E, A])

이것은 Input [E]를 취하고 또 다른 Iteratee [E, A]를 반환하는 함수에서 만들어지는 가장 복잡한 상태이다. 이 이론을 너무 깊이 생각하지 않고도 Input [E] => Iteratee [E, A]는 하나의 입력을 소비하고 다른 입력을 소비하고 다른 상태를 반환 할 수있는 새로운 상태 / iteratee를 리턴하는 방법이라는 것을 쉽게 이해할 수 있을 것이다. / iteratee 등 ... 상태가 완료 또는 오류에 도달 할 때까지.

이 구조는 (전형적인 기능적 방식으로) 반복 메커니즘을 공급(feeding)하는 것을 보장합니다.


Enumerator [Int]에 두 개의 첫 번째 요소의 총계를 계산하는 Iteratee를 작성해 보자.


def total2Chunks: Iteratee[Int, Int] = {
  
  def step(idx: Int, total: Int)(i: Input[Int]): Iteratee[Int, Int] = i match {
  
    case Input.EOF | Input.Empty => Done(total, Input.EOF)
  
    case Input.El(e) =>
  
      if(idx < 2) Cont[Int, Int](i => step(idx+1, total + e)(i))
  
      else Done(total, Input.EOF)
  }
 
  ( Cont[Int, Int](i => step(0, 0)(i)) )
}


val promiseTotal = Enumerator(10, 20, 5) run total2Chunks
promiseTotal.map(println _)

=> prints 30



이 예제를 사용하면 Iteratee를 작성하는 것이 받은 Chunk의 유형과 새 State / Iteratee를 반환하는 방법에 따라 각 단계에서 수행 할 작업을 선택하는 것과 크게 다르지 않다는 것을 이해할 수 있게 된다.



아직 졸리지  않은 사람들을 위한 몇 가지 수면제

Enumerator 는 단지 Iteratee를 다루는 도우미 일 뿐이다.

보시다시피, Iteratee API에는 Enumerator에 대한 언급이 없다.

이것은 Enumerator가 Iteratee와 상호작용하는데 필요한 단순한 헬퍼 일 뿐이기 때문에 Iteratee에 자체적으로 연결하여 첫 번째 데이터 청크를 주입 할 수 있다. 그러나 Iteratee가 Play2의 모든 곳에서 정말 쉽고 잘 통합되어 있기 때문에 Enumerator가 필요 없다.


 Enumerator.apply(Iteratee) 와 Enumerator.run(Iteratee) 차이점

앞에서 언급했던이 지점으로 돌아가 보자. Enumerator에서 주요 API의 서명을 살펴보면 

trait Enumerator[E] {

  def apply[A](i: Iteratee[E, A]): Promise[Iteratee[E, A]]
  ...
  def run[A](i: Iteratee[E, A]): Promise[A] = |>>>(i)
}

apply 는 마지막  Iteratee/State 를 리턴한다.

apply 함수는 청크를 소비하고 해당 작업을 수행하고 Iteratee의 Promise를 반환하는 Iteratee에  Enumerator를 주입한다. 이전 설명에서, 반환된 Iteratee가  Enumerator에서 요구한 청크를 소비 한 후 마지막 상태 일 수 있음을 스스로 판단 할 수 있다.


run 는 Promise[Result] 를 리턴한다.

run 3가지 단계를 가진다.

  1. 이전 apply 호
  2. Input.EOF 를 끝났음을 확인하기위해 Iteratee 에 삽입 
  3. Iteratee 로 부터 promise 로써 마지막 컨텍스트를 얻는다. 

예를 보면 :

val iterator = Iteratee.fold(0){ (total, elt) => total + elt }
val e = Enumerator(1, 234, 455, 987)

// just lets the iterator consume all chunks but doesn't require result right now

val totalIteratee: Promise[Iteratee[Int, Int]] = enumerator apply iterator

// runs the iteratee over the enumerator and retrieves the result as a promise

val total: Promise[Int] = enumerator run iterator


한줄 요약 

Iteratee의 결과가 필요할 때 run을 사용해야합니다.

결과를 검색하지 않고 Enumerator를 통해 Iteratee를 적용해야하는 경우 apply



Iteratee 는 Promise[Iteratee] 이다.(매우 중요) 

One more thing to know about an Iteratee is that Iteratee is a Promise[Iteratee] by definition.

Iteratee에 대해 알아야 할 또 하나의 사실은 Iteratee가 정의에 의한 약속 [Iteratee]이라는 것이다.

// converts a Promise[Iteratee] to Iteratee

val p: Promise[Iteratee[E, A]] = ...
val it: Iteratee[E, A] = Iteratee.flatten(p)

// converts an Iteratee to a Promise[Iteratee]

// pure promise

val p1: Promise[Iteratee[E, A]] = Promise.pure(it)

// using unflatten

val p2: Promise[Iteratee[E, A]] = it.unflatten.map( _.it )

// unflatten returns a technical structure called Step wrapping the Iteratee in _.it

Iteratee <=> Promise[Iteratee]

즉, Iteratee에서 매우 게으른 방식으로 코드를 작성할 수 있습니다. Iteratee를 사용하면 Promise로 전환하고 원하는대로 되돌릴 수 있습니다.



Enumeratee 에 대하여..

2 번째 충고 :  다 왔어. 당황하지마… Enumeratee 개념은 정말 단순해

Enumeratee 는 단지  Enumerator 와 Iteratee 사이의 파이프 어탭터일뿐 


Enumerator [Int]와 Iteratee [String, List [String]]가 있다고 상상해보라.

Int를 String으로 변환 할 수 있다. 그렇지 않나?하지만 그렇게 하기 위해서는 

Int의 덩어리를 String의 덩어리로 변환 한 다음 Iteratee에 삽입해야 한다.

val enumerator = Enumerator(123, 345, 456)
val iteratee: Iteratee[String, List[String]] =val list: List[String] = enumerator through Enumeratee.map( _.toString ) run iteratee

무슨일이 벌어졌나?
Enumerator [Int]와 Enumeratee [Int, String]를 Iteratee [String, List [String]]로 연결 했을뿐이다.

2번째 스텝:

val stringEnumerator: Enumerator[String] = enumerator through Enumeratee.map( _.toString )
val list: List[String] = stringEnumerator run iteratee

따라서 Enumeratee는 사용자 지정 열거자를 Play2 API에서 제공하는 일반 Iteratee와 함께 사용하도록 변환하는 데 매우 유용한 도구라는 것을 이해할 수 있게 되었다.

Enumerator / Iteratee로 코딩하는 동안 이것이 가장 많이 사용할 도구라고 확신 한다.


Enumeratee 는 Iteratee 없이 Enumerator 로 될 수 있다.


이는 Enumeratee의 매우 유용한 기능입니다. 열거 형 [From]을 열거 형 [To, From]으로 열거 형 [To]으로 변형 할 수 있습니다.

Signature of Enumeratee is quite explicit:

Enumeratee[From, To]

다음처럼 사용 할 수 있다.

val stringEnumerator: Enumerator[String] = enumerator through Enumeratee.map( _.toString )

Enumeratee 는 Iteratee로 변환 할 수 있다.

이것은 약간의 낯선 기능일 것인데 반복문 [To, A]을 반복자 [From, A]에서 Enumeratee[From, To]로 변형 할 수 있기 때문이다.


val stringIteratee: Iteratee[String, List[String]] =val intIteratee: Iteratee[Int, List[String]] = Enumeratee.map[Int, String]( _.toString ) transform stringIteratee

Enumeratee 는 Enumeratee로 구성될 수 있다.

Yes, this is the final very useful feature of Enumeratee.

val enumeratee1: Enumeratee[Type1, Type2] =val enumeratee2: Enumeratee[Type2, Type3] =val enumeratee3: Enumeratee[Type1, Type3] = enumeratee1 compose enumeratee2

다시 한번 말하지만, 일반적인 Enumeratees를 만든 다음 이를 사용자 지정 Enumerator / Iteratee에 필요한 사용자 지정 Enumeratee로 작성할 수 있다는 것을 쉽게 알 수 있습니다.


결론 


어쨌든 Iteratee / Enumerator / Enumeratee를 사용해야하는 이유는 무엇일까?


최신 웹 응용 프로그램은 더 이상 동적으로 생성 된 페이지가 아닙니다. 이제 다른 소스에서 가져온 데이터 흐름을 다양한 형식으로 서로 다양하게 조작하고 있습니다. 수많은 고객에게 엄청난 양의 데이터를 제공하고 분산 된 환경에서 작업해야 할 수도 있습니다.

Iteratee는 실시간으로 데이터 흐름을 처리하기에 안전하고 변경 불가능하며 매우 유용하기 때문에 이러한 경우에 매우 적합하다고 볼 수 있습니다. 


Note : 이상한 연산자들 

&>, >> >>, >> >>> 및 유명한 물고기 연산자> <>와 같은 Iteratee / Enumerator / Enumeratee를 기반으로하는 코드에서 이러한 연산자를 많이 볼 수 있습니다. through, apply, applyOn 또는 compose와 같은 실제 명시적인 단어의 별칭이 있습니다. 어떤 사람들은 오퍼레이터가 더 명확하고 더 간결하며, 어떤 사람들은 단어를 선호 할 것입니다.

예를들면  &> 는  through의 약자이다. 필터링을 한다는 뜻이다.




Streaming HTTP responses

Standard responses and Content-Length header

HTTP 1.1부터 여러 HTTP 요청과 응답을 제공하기 위한 단일 연결을 유지하기 위해, 서버는 적절한 Content-Length HTTP 헤더를 응답과 함께 보내야합니다.

기본적으로 아래와 같지요. 

public Result index() {
    return ok("Hello World");
}

특별히 Content-Length 헤더를 지정하지 않았습니다. 물론, 보내는 콘텐츠가 잘 알려져 있기 때문에 Play는 콘텐츠 크기를 계산하고 적절한 헤더를 생성 할 수 있습니다.

참고 : 텍스트 기반 콘텐츠의 경우 Content-Length header가 문자를 바이트로 변환하는 데 사용되는 인코딩에 따라 계산되어야 하므로 모양이 단순하지 않습니다.

Content-Length 헤더를 올바르게 계산하려면 Play가 전체 응답 데이터를 소비하고 해당 내용을 메모리에 로드해야합니다.

Serving files

간단한 콘텐츠를 위해 전체 콘텐츠를 메모리에 로드하는 것 말고  대용량 데이터는 어떻게 할까요? 우리가 큰 파일을 웹 클라이언트로 되돌려 보내려한다고 가정 해 봅시다.

Play는 로컬 파일을 제공하는 일반적인 작업에 사용하기 쉬운 도우미를 제공합니다.

public Result index() {
    return ok(new java.io.File("/tmp/fileToServe.pdf"));
}

또한 이 헬퍼는 파일 이름에서 Content-Type 헤더를 계산합니다. 그리고 Content-Disposition 헤더를 추가하여 웹 브라우저가 이 응답을 처리하는 방법을 지정합니다. 기본값은 Content-Disposition : attachment를 사용하여 , 이 파일을 다운로드 하도록 웹 브라우저에 요청하는 것이죠. filename = fileToServe.pdf.

Chunked responses

지금까지는 스트리밍 하기 전에 콘텐츠 길이를 계산할 수 있기 때문에 스트리밍 파일 콘텐츠와 잘 작동합니다. 그러나 콘텐츠 크기가 없는 동적으로 계산된 콘텐츠는 어떻게 할까요?

이러한 종류의 응답을 위해서는 청크 분할 전송 인코딩을 사용해야합니다.

참고: 청크 분할 전송 인코딩은 웹 서버가 일련의 청크로 콘텐츠를 제공하는 HTTP 1.1 버전의 데이터 전송 메커니즘입니다. 이것은 프로토콜이 필요로 하는 Content-Length 헤더 대신 Transfer-Encoding HTTP 응답 헤더를 사용합니다. Content-Length 헤더가 사용되지 않기 때문에 서버는 클라이언트 (일반적으로 웹 브라우저)에 대한 응답 전송을 시작하기 전에 내용의 길이를 알 필요가 없습니다. 웹 서버는 동적으로 생성 된 컨텐트를 사용하여 응답을 전송하기 전에 해당 컨텐트의 전체 크기를 알 수 있습니다.

각 청크의 크기는 청크 자체 바로 전에 보내 지므로 클라이언트는 청크에 대한 데이터 수신이 완료된 시점을 알 수 있습니다. 데이터 전송은 길이가 0 인 최종 청크에 의해 종료됩니다.

https://en.wikipedia.org/wiki/Chunked_transfer_encoding

장점은 우리가 데이터를 실시간으로 제공 할 수 있다는 것입니다. 즉, 우리는 준비된 데이터를  가능한 빨리 데이터 덩어리로 보냅니다. 단점은 웹 브라우저가 콘텐츠 크기를 알지 못하기 때문에 적절한 다운로드 진행률 표시 줄을 표시 할 수 없다는 것입니다.

일부 데이터를 계산하는 동적인 InputStream을 제공하는 어딘가에 서비스가 있다고 가정 해 보겠습니다. 우리는 청크 응답을 사용하여 Play에 이 콘텐츠를 직접 스트리밍 하도록 요청할 수 있습니다.

public Result index() {
    InputStream is = getDynamicStreamSomewhere();
    return ok(is);
}

자체 청크 응답 빌더를 설정할 수도 있습니다.  (EvenSource 등) 

public Result index() {
    // Prepare a chunked text stream
    Source<ByteString, ?> source = Source.<ByteString>actorRef(256, OverflowStrategy.dropNew())
        .mapMaterializedValue(sourceActor -> {
            sourceActor.tell(ByteString.fromString("kiki"), null);
            sourceActor.tell(ByteString.fromString("foo"), null);
            sourceActor.tell(ByteString.fromString("bar"), null);
            sourceActor.tell(new Status.Success(NotUsed.getInstance()), null);
            return null;
        });
    // Serves this stream with 200 OK
    return ok().chunked(source);
}

Source.actorRef 메서드는 ActorRef에 구체화하는 Akka stream소스를 만듭니다. 그런 다음 액터로 메시지를 보내 스트림에 요소를 게시 할 수 있습니다. 다른 방법은 ActorPublisher를 확장하고 Stream.actorPublisher 메서드를 사용하여 ActorPublisher를 만드는 액터를 만드는 것입니다.

서버가 보낸 HTTP 응답을 검사 할 수 있습니다.

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked

4
kiki
3
foo
3
bar
0

우리는 응답을 닫는 세 개의 청크와 하나의 마지막 빈 청크를 얻습니다.



Angular2 , 웹팩을  Play 와 함께 사용하기 

http://www.lightbend.com/activator/template/play-ng2-webpack2

아래 템플릿들을 얻기 위한 방법은 매우 다양하다.


방법 1:  Activator UI 에서 play-ng2-webpack2 를 선택해서 시작.


이미 Lightbend Activator (get it here) 는 제공하고 있는데 UI 를 시작한후에  play-ng2-webpack2를 템플릿
리스트에서 찾아보시라~


방법2:  play-ng2-webpack2 프로젝트를 zip 으로 다운로드


 Activator 없으면 그냥 코드 자체를  play-ng2-webpack2. 다운로드 받을 수 있다.

  1. Download the Template Bundle for "Play Framework with Angular 2 and Webpack 2"

다운로드 한 zip 파일을 시스템에 압축 해제하시고~번들에는 Activator를 시작할 수있는 작은 부트 스트랩 스크립트가 포함되어 있습니다. Lightbend Activator의 UI를 시작하려면 :

파일 탐색기에서 템플릿이 추출 된 디렉토리로 이동하고 "activator.bat"파일을 마우스 오른쪽 버튼으로 클릭 한 다음 "열기"를 선택하고 경고 메시지가 나타나면 계속하려면 클릭.

  1. 또는 커맨드 라인에서 : 

     C:\Users\typesafe\play-ng2-webpack2> activator ui 

    Lightbend Activator 는 브라우저에서 해당 템플릿을 실행 시킬 것 입니다.

방법 3: 커맨드라인에서  play-ng2-webpack2 project 생성


커맨드라인에서 activator new PROJECTNAME play-ng2-webpack2  


방법 4: template source 보기 


 https://github.com/lbialy/play-ng2-webpack2.


방법 5:  아래 튜토리얼 참고하기 


이 템플릿의 튜토리얼 텍스트는 아래에 포함되어 있지만 컴퓨터의 Activator에서 볼 경우 더 효과적 일 수 있습니다. 액티베이터 튜토리얼은 대개 상호 작용하도록 설계되었습니다.

Preview the tutorial


Play 2.5.4 + Angular 2 RC4 seed with Webpack 2 + SBT - based build

이 템플릿에는 Angler 2 (현재 RC4)로 작성된 UI와 함께 Play Framework 2.5.4의 표준 배포가 포함되어 있습니다.

Angular  2 응용 프로그램은 별도의 디렉토리 UI에 포함되어 있으며 Angular-CLI를 사용하여 스캐 폴딩됩니다. Angular-CLI webpack 기반 빌드가 아직 준비되지 않았기 때문에 프론트 엔드 응용 프로그램을 위한 자체 빌드 시스템을 제공하기로 결정했으며 Webpack 2를 선택했습니다 (대부분 응용 프로그램 크기를 줄이기위한  tree shaking 모듈 해상도의 새로운 기능을 활용할 수있었습니다). ). 또한 WebJars 및 Sbt-Web 기반 프론트 엔드 빌드가 너무 느리고 선호도가 제한적이라는 사실을 발견했습니다.


Build requirements

  • node: ^6.3.0
  • npm: ^3.10.0

이 두 가지는 적어도 webpack 설치를 처리하고 sbt-js-engine으로 실행될 때까지 전역적으로 필요합니다. sbt-js-engine에서 js 빌드를 기반으로하는 데 성공 했더라도 더 나은 성능을 위해 노드를 사용하는 것이 좋습니다.


Changes and things to know


PF 2.5.4의 표준 배포판과는 약간의 차이가 있지만, 대부분은 sbt 빌드 및 테스트 장치와 관련이 있습니다.

  • 스칼라 버전이 2.11.7에서 2.11.8로 변경되었습니다.
  • Webpack이 이제 프론트 엔드 빌드를 처리하기 때문에 모든 sbt-web 플러그인이 project / plugins.sbt에서 제거되었습니다.
  • 재생 실행 후크 UIBuildHook가 프로젝트에 추가되었습니다. - 작업은 npm 및 webpack을 통한 노드 모듈 설치를 처리합니다. - Play가 dev 모드에서 실행될 때 감시합니다.
  • build.sbt 파일에는 UI 빌드 작업이 포함되어 있습니다. npm 모듈 설치 및 Play 테스트와 함께 실행되는 카르마 테스트
  • ui / dist 폴더는 관리되지 않는 리소스 디렉토리로 표시되므로 dev 모드에서 실행중인 Play와 stage / dist 패키지 모두 Assetscontroller를 통해 내용에 액세스 할 수 있습니다. 그 폴더는 Webpack 빌드의 출력 디렉토리입니다.
  • IntegrationSpec은 제거되었습니다. Selenium Webdriver 구동 E2E 테스트는 angular 통합으로 인해 쓸모가 없습니다. 하나는 브라우저 기반의 Play 테스트 클래스를 사용하여 Angular2가없는 응용 프로그램의 부분을 테스트 할 수 있지만, Webdriver가 Angular 2 응용 프로그램의 'vendor.js'부분을 다운로드하려고 시도 할 때 Cryptic 오류로 계속 충돌하고 있음을 경고해야합니다.
  • angular 테스트는 ProtractorSpec 테스트 클래스를 사용하여 테스트합니다. 나는 SBT에서 angular 테스트를 실행하고 실패 했으므로 이미 그 이름을 쓰지 않은 중국 코더에게 그 부분을 빚지고있다. 그래서 나는 그럴 필요가 없었다. 고마워, 친구!
  • ui 폴더의 변경으로 인해 재생 재로드가 발생하지 않지만 재생이 개발 모드에서 실행 중일 때 Webpack에서 관찰 (및 처리)됩니다.

About the Angular 2 app:

scaffolders (ng generate _) 이외의 다른 CLI 명령을 사용하지 마십시오. webpack은 모든 건물을 처리합니다.
play없이 Angular 2 앱을 제공하려면 npm run devin ui / directory를 실행하면됩니다. devpacker는 devpacket mode에서 작동하도록 webpack-dev-server를 실행합니다
npm dev / dependencies는 shrinkwrapped이므로 고정되어 있습니다. 여기에서는 출혈이 있습니다. 일부 베타 종속성에서 BC가 전체 프론트 엔드 빌드를 망칠 수는 없으므로 미안한 것보다 안전합니다 (실제로이 패키지를 준비 할 때 발생 함). 가장 중요한 라이브러리 (즉, Angular 및 Webpack)를 업데이트하려고 노력할 것입니다.하지만이 기능이 작동하는 한 멋지고 통제 된 방식으로 업데이트 할 것입니다. docs에서 shrinkwrapped 프로젝트에 npm 패키지를 추가하는 방법에 대해 읽어보십시오.


  • 중요 : 저는 Angular 2 Webpack 가이드가 권고 한대로 angular2-template-loader를 사용하여 템플릿과 스타일을 인라인합니다. 구성 요소의 디렉토리 구조의 경우 :

        component/some-funny.component.ts
        component/some-funny.component.html
        component/some-funny-component.scss
    

annotation syntax in file some-funny.component.ts:


        @Component({
            templateUrl: 'some-funny.component.html',
            styleUrls: ['some-funny.component.scss']
        })
    


스타일과 템플릿을 올바르게 인라인합니다. 주의 : 분명히 angular2-template-loaderresolves 템플릿과 스타일 URL은 참조 어노테이션을 포함하고있는 컴포넌트 파일에 상대적이다. 이것은 경로가 구성 요소가 아닌 문서 루트에 상대적이어야하는 Angular 2 (System.JS에서의 의미)가 정상적으로 작동하는 것과 다릅니다.

  • As you probably noticed I am also using SASS via sass-loader (cause it's awesome!)




Action 과 Action.async 의 차이점 


개인적으로 Play 를 하면서 가장 헥깔렸던 것 중 하나가 Action { ... } 과 Action.async { ... } 의 차이점이다.
아마 다른 사람들도 마찬가지리라~~그래서 이 포스트를 통해서 확실히 해소해 보려 한다. 

보통 Play 는 주로 Scala 로 사용되는데 , 아시다시피 Scala 는 자바에 비해 문법적으로 생략되는 것들이 무지 많다. 그것이 코드를 단순화하고 보기 좋게 만든다고 주장도 하겠지만 반대급부로 도대체 이게 먼소리야 하게 되며 코드 가독성이 떨어질 경우도 많다. (특히 implicit )

이게 먼소리야~~ 라는 부분에 대해서, 그냥 그려려니 하고 "구현" 에 집중해서 코딩을 하는것이라면 뭐 간편하게 좋겠지만, 인사이드에 대해 호기심을 느끼는 순간부터 지옥이 될 수도.. 너무 생략이 많아서...;;

게다가 Scala 나 Play 나 Akka 나 모두 급변하고 있기 때문에 자료의 신빙성도 믿기 어려우며 한번 이해했다고 해도 바로 바뀌는 경우도 있고, 모두 공통적으로 사용하는 표준도 없으며 (특히 deploy ) 아무튼 산넘어 산이요..

너무 부정적으로 썼나 싶다;;; 장점은 그 보다 훨씬 많으니 스칼라를 선택함은 축복이 될 것이다.

자~ 각설하고 이제 부터 여행을 떠나볼까나~!


무엇이 문제인가? 

Action 과  Action.async 이라 음 하나는 동기,하나는 비동기로 사용하라는 것인가? 문서를 한번 찾아보자.

Actions are asynchronous by default


먼소리여~ 그냥 모두 비동기라구?

Note: Both Action.apply and Action.async create Action objects that are handled internally in the same way. There is a single kind of Action, which is asynchronous, and not two kinds (a synchronous one and an asynchronous one). The .async builder is just a facility to simplify creating actions based on APIs that return a Future, which makes it easier to write non-blocking code.

둘다 비동기이고 .async 는 좀 더 넌블럭킹 코드를 쉽게 사용하게 만들어 준것이라고??  
그럼 어떻게 사용하면 불편한거고, 어떻게 사용하면  편한건지 비교해서 예를 들어주라규~~~ 이놈들아 ㅜㅜ

(여기서 부터 함정에 빠지게 되는데....즉 쉬운걸 어렵게 생각하기 시작한다..) 


초보자와의 선문답 

다음 내용은 스택오버플로우에 있는 내용을 좀 각색해서 쉽게 풀어나간다.

초보자 김군 :  아니 Play 에는 Action 과 Action.async 라고 있는데 , 둘다 똑같이 브라우저에서 결과 반응이 오던데요? 이게 뭡미?? 비동기 코드는 바로 리턴이 와야 하는거 아닌가요?  코드도 첨부해드릴께요

def func(): Int = {
    Thread.sleep(5000)
    return 1
}

//동기
def A = Action {
   Ok("sync" + func())
} //비동기 def B = Action.async { val futureint = scala.concurrent.Future { func() }
futureint.map(i => Ok("async" + i)) }

A 함수를 호출해도 브라우저에서는 5초후에 연락이 오고 ,
B 함수를 호출해서 브라우저에서는 5초후에 연락이 오고..

B 함수는 Future 를 통해서 바로 리턴되었을 텐데 -.-?

경력자 박씨:

하나씩 짚어보자구.  일단 "브라우저로 리턴이 바로 와야하는게 아닌가요? " 이 부분 부터 보자고.

먼저 답부터 알려주면 비동기라는 것은 서버쪽의 얘기지 클라이언트와 연관된 말이 아니야. 
즉 클라이언트는 서버쪽에서 5초간 일을 한다면  5초 후에 리턴을 받을 수 있다는 말이지. 

즉 5초동안 브라우저가 멍때릴 수 있다는 말이야. Play가 비동기건 , 동기건 상관없이~ 그래서 그게 싫다면 브라우저쪽 자바스크립트 코드에서 자신만의 비동기코드를 구현해야하는것이지. 

위의 그림을 설명해보면,  클라이언트의 접속을 받을 수 있는 쓰레드는 3개 뿐이야. (fork-join 풀 같은게 내부적으로 사용되겠지) 근데 그 쓰레드 3개가 처음부터 끝까지 모든 일을 책임지면 어떻게 될까?? 그래 3명 이상 부터의 접속자는 그냥 멍때려야해..

따라서 일단 접수 받자마자 워커쓰레드에게 일을 넘기고 , 자신은 다시 접수 받을 준비를 해야하는거지. 이게 비동기식 방법이야. 근데 여기서 브라우저에게 바로 리턴해주는게 아니라, 워커쓰레드가 일을 마칠때까지 기다리다가 (그림의 주황색 블럭) 모두 완료되면 response 를 보내게 되지. 

이게 일반적인 작동 원리야. 코드로 말해볼께. 

def func(): Int = {
  Logger.info("1")
  Thread.sleep(5000)
  Logger.info("2")
  return 1
}

def A = Action {
  val result = Ok("sync" + func())
Logger.info("3") result } def B = Action.async { val futureint = scala.concurrent.Future { func() }
val f = futureint.map(i => Ok("async" + i)) Logger.info("3") f }

이 코드는 어떤 순서대로 로그가 찍힐까? 

A 는 

2014-05-14 17:23:39,359 [info] application - 1
2014-05-14 17:23:44,359 [info] application - 2
2014-05-14 17:23:44,359 [info] application - 3

B 는

2014-05-14 17:23:39,359 [info] application - 1
2014-05-14 17:23:44,359 [info] application - 3
2014-05-14 17:23:44,359 [info] application - 2

예상했던 대로 A 는 Action 안에서  func() 함수가 모두 완료(5초) 될 때까지 기다리기 때문에 3이 마지막에 찍혔고 B 는 바로 리턴하기 때문에 3이 먼저 찍히고 리턴해주는 것이지 ( 즉 그림에서 쓰레드를 클라이언트를 맞이하는 용도로 전환 시켜 주기 위해 ) 


초보자 김군 : 아..이해했습니다 ^^  근데 Play 문서에서는 둘 다 Future 를 만들게 되며 둘다 비동기라고 하는데요? 그건 무엇인가요? 

경력자 박씨:  아 그래??  이건 나도 잘 모르겠는데 -.-a


멘붕의 시작 

그렇다!!  Action 과 Action.async 둘다 결국엔 Future 로 감싸지는 비동기란다. 이 말을 유추해보면  

//동기
def A = Action {
   Ok("sync" + func())
}

Action 의 apply 메소드의 내부에 파라미터로 들어가는 OK("sync" + func()) 가 apply 메소드 자체에서 사용 될 때 Future 로 감싸진다는 말이다. 이렇게 되면 async 안해도 된다는 말인데 ?? async 는  Future 를 두번 사용하는 낭비를 할리는 없고, 결국 Action 은 내부에서 Future 로 감아주고 , async 는 사용자가 준 Future 를 직접 사용하는것이라고 봐야하는데 왜 ??흠흠...

* 외부 웹서비스 api 호출시

def users = Action.async { implicit request =>
  WS.url("http://www.cloudusers.com/123/list").get().map { response =>
    Ok(response.json)
  }
}

위의 코드처럼 외부소스에 접근하는 콜들이 병목되는것을 막기 위해, async 와 Future 를 사용하는데 애초에 그냥 Action 또한 내부에서 Future 로 감싸져서 바로 리턴된다면 굳이 왜 async 를 사용해야 한다는 것인가?

이 의문을 파헤치려면 결국 인사이드 코드를 봐야겠다.


인사이드의 시작

* val echo = Action { request =>
* Ok("Got request [" + request + "]")
* }
final def apply(block: R[AnyContent] => Result): Action[AnyContent] = apply(BodyParsers.parse.default)(block)

* val hello = Action {
* Ok("Hello!")
* }
final def apply(block: => Result): Action[AnyContent] = apply(_ => block)


위의 함수는 requset 를 매개변수로 받는 익명함수가 매개변수로 들어 간 것이다. 둘다 Result 를 리턴한다.
또 쫒아가보자. 으잉?? async 역시 호출한다. 

final def apply[A](bodyParser: BodyParser[A])(block: R[A] => Result): Action[A] = async(bodyParser) { req: R[A] =>
Future.successful(block(req))
}

그냥 Action 도 결국 Future.successful 을 이용해서 Future[Result] 를 리턴하는 구나. 
그럼 Action.async 를 쫒아가보자.
 

final def async(block: R[AnyContent] => Future[Result]): Action[AnyContent] = async(BodyParsers.parse.default)(block)

final def async[A](bodyParser: BodyParser[A])(block: R[A] => Future[Result]): Action[A] = composeAction(new Action[A] {

오~~ 예상이 맞았어. 결국 같은 함수를 마지막에 같이 호출하는거였군. 

굳이 차이점을 찾자면 Future.successful 인거 같다. 

인사이드를 통해 먼가는 확인했지만 아직까지 why 가 부족하다. 다시 구글링을 좀 더 해보자.

http://stackoverflow.com/questions/23997418/are-there-any-benefits-in-using-non-async-actions-in-play-framework-2-2
https://groups.google.com/forum/#!topic/play-framework/WWQ0HeLDOjg

두개의 관련된 글을 찾았다.

2가지의 데이터베이스가 있다고하자 : A 는 블럭되는 DB,  B 는 논블럭되는 DB 

A 디비 (블럭)

def read(id: Long): User

DB에서 사용자 정보를 읽어오는 메소드이다.

def read(id: Long) = Action {
    Ok(Json.toJson(User.read(id))
}

다음처럼 Action 을 사용 할 수 있으며 이것은  Action.apply 로 풀어보자면 아래와 같다.

def read(id: Long) = Action.async {
    Future.successful(Ok(Json.toJson(User.read(id)))
}

여기서 깊이 있는 상상을 해야한다. 결국 Future로 감싸져서, 사용자 accept 를 위한 쓰레드의 제어권이 풀리긴 한다. 하지만 워커쓰레드는 여전히 블럭 상태이다.


B 디비 (논블럭)

def read(id: Long): Future[User]

비동기DB의 호출은 아래와 같을 것이고 

def read(id: Long) = Action.async {
    User.read(id).map(user => Ok(Json.toJson(user)))
}

다음 처럼 자연스럽게 Future 를 함수합성 할 것이다. 워커쓰레드도 논블럭이다. 


그럼 마지막 의문점!

그럼 그냥 Action 에다가, 비동기 디비를 사용하면 안되나? 
불가능하다. (너무 단순하게 바라보면 그렇다는 이야기이고, Await 같은것으로 비동기작업들의 완료를 기다려서 값으로 뽑아낸후에 리턴할 수도 있습니다.) 비동기DB 사용의 경우 애초에 Future 를 리턴했는데 그것을 굳이 Result 로 만들 수는 없지 않나? 


피날레

그렇다. 사용자를 맞이하는 쓰레드를 해제해 주기 위함이 아니라 , (비동기 스프링에서 서블릿 쓰레드풀에 다가 쓰레드를 release 하는 것처럼 ) 비지니스 로직에서의 Future 합성등을 통해 비동기를 더 많이 보장받기 위해서 Action.async 를 사용한 것이었다. (추가: Action 도 비동기작업을 할 수 없는것은 아니다) 

아래와 같이 정리된다. 소스복잡도와 동시성 효율성을 잘 트레이트오프해서 선택하면 될거 같다.
Action : 그냥 비지니스로직은 별개 없고, RDBMS 및 동기적 외부API에 접속해서 데이터 가져오는 것들.
Action.async : 비지니스로직이 동시성 여지가 있는 것들 및 외부 API 에 비동기로 커뮤니케이션 하면서 내부 비지니스 로직을 돌리는 경우등

자 이제 아래 멘트를 통해 꼬였던 뇌를 다시 풀어보자.


"그냥 프레임워크 입장에서 사용자가 그냥 값을 반환할지 `Future`를 반환할 지 모르니까 두 가지 방식을 따로 제공"  
- okky.kr fender 님 코멘트- 


결론은 매우 단순하게 나버렸지만 , 처음에 쓸때없이(?) 힘들었던 사람이라면 (나처럼) 아주 속이 시원하리라본다. ^^  





  Iteratee Enumerator / Enumeratee 란?


번역  :  http://qiita.com/sunny4381/items/a711fa72db26c9263b3f

Play Framework의 Iteratee / Enumerator / Enumeratee 는 공식 문서를 읽어도 잘 모르고, 또한 수학적 설명이 적혀 있기도 해서 불필요하게 이해하기 어렵다.  따라서  몇 가지 예를 통해 이해를 하려합니다.

또한, Understanding Play2 Iteratees for Normal Humans (한글번역)
를 참고하며 Play Framework 2.1.0, Scala 2.10을 대상으로하고 있습니다.  

Iteratee / Enumerator / Enumeratee을 간단하게 말하면 :

이름설명
Iteratee [E, A]

루프의 내용. 형태 E에서 형태 A를 생성한다.  (소비자역할)

Enumerator [E]컬렉션을 일반화 한 것으로 형태 E를 열거한다. 무한 열거 (Streaming) 할 수도있다. (생산자역할)
Enumeratee [E, A]

많이 사용하지 않기 때문에 지금은 생각하지 좋다.

Iterator와 Iteratee의 차이

Scala는 Iterator [E]라는 친숙한 인터페이스가 있습니다. 
Iterator [E]와 Iteratee [E, A]의 차이는 Iterator [E]는 어떤 컬렉션에서 생성되는 반면 Iteratee [E, A]는 순수 함수형 프로그래밍에서 iteration 컨셉의 일반화 입니다. 컬렉션 요소들과 독립하며 불변 ( immutable)에서 논블럭 비동기 처리하에 입력 형식 E출력 형식 A가 정적으로 정의되어 있다는 특징 외에도 결과가 필요할 때까지 아무런 실행되지 않는다는 특징이 있습니다.

먼저 iterator 를 상기시켜 봅시다.

1
2
3
4
5
6
7
8
9
10
val l = List(1, 234, 455, 987)

var total = 0 // will contain the final total
var it = l.iterator
while( it.hasNext ) {
  total += it.next
}

total
=> resXXX : Int = 1677

예 1 : Int 형 요소와 Enumerator [Int]과 그 합계를 계산하는 Iteratee [Int, Int]

val initialValue : Int = 0 val sumIteratee : Iteratee [ Int , Int ] = Iteratee . fold ( initialValue ) { ( total , e ) => total + e } val intEnumerator1 : Enumerator [ Int ] = Enumerator ( 1 , 234 , 455 , 987 ) val futureTotal1 : Future [ Int ] = intEnumerator1 . run ( sumIteratee ) val total1 : Int = Await . result ( futureTotal1 , Duration . Inf ) println ( "total =" + total1 . toString ) val intEnumerator2 : Enumerator [ Int ] = Enumerator ( 431 , 57 , 668 ) val futureTotal2 : Future [ Int ] = intEnumerator2 . run ( sumIteratee ) val total2 : Int = Await . result ( futureTotal2 , Duration . Inf ) println ( "total =" + total2 . toString )

다음과 같은 결과가 출력됩니다.

total = 1677
total = 1156

Iteratee [E, A] 는 컬렉션과 독립하고 있기 때문에, intEnumerator1과 intEnumerator2 라는 각각 다른 컬렉션에 대해 수행 할 수 있으며 Iteratee [E, A] 는 비동기적으로 작업을 수행하기 위해 Future [Int ]를 리턴받지요.

Future [Int]에서 결과를 꺼내기 위해서 일부러 Await.result () 메소드를 사용하지 않으면 안되기 때문에 조금 귀찮은 느낌일 것입니다. 저도 그렇게 생각합니다.

예 2 : 무한 Enumerator (Streaming Enumerator)

Enumerator는 무한히 열거 할 수 있습니다.

// 500 밀리 초마다 문자열 "current time % s"를 생성하고 계속 Enumerator val stringGenerator : Enumerator [ String ] = Enumerator . generateM ( play . api . libs . concurrent . Promise . timeout ( Some ( "current time % s " . format (( new java . util . Date ()))) 500 ) ) // 콘솔에 출력하는 Iteratee val printIteratee : Iteratee [ String , Unit ] = Iteratee . foreach { println _ } // 5 초 기다리고 그 동안 stringGenerator가 생성한 요소 각각에 대해 printIteratee를 실행하는 val future : Future [ Unit ] = stringGenerator . run ( printIteratee ) Await . result ( future , Duration ( 5 , "seconds" )

다음과 같은 결과가 5초동안 출력됩니다.

current time Tue Feb 26 11:55:58 JST 2013
current time Tue Feb 26 11:55:58 JST 2013
current time Tue Feb 26 11:55:59 JST 2013
current time Tue Feb 26 11:55:59 JST 2013
current time Tue Feb 26 11:56:00 JST 2013
current time Tue Feb 26 11:56:00 JST 2013
current time Tue Feb 26 11:56:01 JST 2013
current time Tue Feb 26 11:56:01 JST 2013
current time Tue Feb 26 11:56:02 JST 2013
current time Tue Feb 26 11:56:02 JST 2013

예 3 : Iteratee [E, A]와 Future [Iteratee [E, A]]

Iteratee [E, A]에서 Future [Iteratee [E, A]]로 변환하고 반대로 Future [Iteratee [E, A]]에서 Iteratee [E, A]로 변환 할 수 있습니다. 양측은 교환 법칙이 성립합니다.

val  initialValue :  Int  =  0 
val  sumIterator :  Iteratee [ Int , Int ]  =  Iteratee . fold ( initialValue )  {  ( total ,  e )  =>  total  +  e  } 
val  futureSumIterator :  Future [ Iteratee [ Int , Int ]]  =  sumIterator . unflatten . map ( _ . it ) 
val  sumIteratorAgain :  Iteratee [ Int , Int ]  =  Iteratee . flatten ( futureSumIterator )

예 4 : Iteratee [E, A] .feed와 Input

Iteratee[E, A].feed() 메소드를 사용하면 Enumerator [E]를 사용하지 않고 요소를 하나씩 Iteratee [E, A]에 전달 할 수 있습니다. Iteratee[E, A].feed() 메서드는 요소를 일반화 한 Input 클래스의 서브클래스인 El, Empty, EOF 중 하나를 제공합니다.  다음은 Input 3 개의 서브 El, Empty, EOF의 개요를 보여줍니다.

이름설명
Input.El [E]형태 E 요소를 나타냅니다.
Input.Empty빈 요소를 나타냅니다.
Input.EOF반복을 종료합니다.

다음의 예는 Enumerator를 사용하지 않고, Iteratee[E, A].feed() 메소드를 사용하여 요소 열 1, 234, 455, 987을 처음부터 순서대로 하나씩줍니다.

val initialValue : Int = 0 var sumIteratee : Iteratee [ Int , Int ] = Iteratee . fold ( initialValue ) { ( total , e ) => total + e } var futureSumIterator : Future [ Iteratee [ Int , Int ] = null // 1을 Iteratee에 공급하는 futureSumIterator = sumIteratee . feed ( Input . El ( 1 )) sumIteratee = Iteratee . flatten ( futureSumIterator ) futureSumIterator = sumIteratee . feed ( Input . El ( 234 )) sumIteratee = Iteratee . flatten ( futureSumIterator ) futureSumIterator = sumIteratee . feed ( Input . El ( 455 )) sumIteratee = Iteratee . flatten ( futureSumIterator ) futureSumIterator = sumIteratee . feed ( Input . El ( 987 )) sumIteratee = Iteratee . flatten ( futureSumIterator ) // 합계를 계산하는 val futureTotal : Future [ Int ] = sumIteratee . run val total : Int = Await . result ( futureTotal , Duration . Inf ) println ( "0 + 1 + 234 + 455 + 987 =" + total . toString )

다음과 같은 결과가 출력됩니다.

0 + 1 + 234 + 455 + 987 = 1677

만약 당신이 Future 사랑한다면

Future [Iteratee [E, A]]를 Iteratee [E, A] 로 일부러 변환없이 Future [Iteratee [E, A]] 그대로 동일한 작업을 수행 할 수 있습니다.

val initialValue : Int = 0 var sumIteratee : Iteratee [ Int , Int ] = Iteratee . fold ( initialValue ) { ( total , e ) => total + e } var futureSumIteratee : Future [ Iteratee [ Int , Int ] = null futureSumIteratee = sumIteratee . feed ( Input . El ( 1 )) futureSumIteratee = futureSumIteratee . flatMap ( _ . feed ( Input . El ( 234 ))) futureSumIteratee = futureSumIteratee . flatMap ( _ . feed ( Input . El ( 455 ))) futureSumIteratee = futureSumIteratee . flatMap ( _ . feed ( Input . El ( 987 ))) val totalFuture : Future [ Int ] = futureSumIteratee . flatMap ( _ . run ) val total = Await . result ( totalFuture , Duration . Inf ) println ( "0 + 1 + 234 + 455 + 987 =" + total . toString )

예 5 : Iteratee [E, A]의 불변성 (Immutability)

Iteratee [E, A]는 인스턴스화 된 후 불변의 내부 상태를 변화시키지 않습니다. 

val initialValue : Int = 0 val sumIteratee0 : Iteratee [ Int , Int ] = Iteratee . fold ( initialValue ) { ( total , e ) => total + e } val sumIteratee1 : Iteratee [ Int , Int ] = Iteratee . flatten ( sumIteratee0 . feed ( Input . El ( 1 )))

val sumIteratee2 : Iteratee [ Int , Int ] = Iteratee . flatten ( sumIteratee1 . feed ( Input . El ( 234 )))

val sumIteratee2_1 : Iteratee [ Int , Int ] = Iteratee . flatten ( sumIteratee2 . feed ( Input . El ( 583 )))

val sumIteratee2_2 : Iteratee [ Int , Int ] = Iteratee . flatten ( sumIteratee2 . feed ( Input . El ( 162 ))) println ( "0 =" + Await . result ( sumIteratee0 . run , Duration . Inf )) println ( "0 + 1 =" + Await . result ( sumIteratee1 . run , Duration . Inf )) println ( "0 + 1 + 234 = " + Await . result ( sumIteratee2 . run , Duration . Inf )) println ( "0 + 1 + 234 + 583 = " + Await . result ( sumIteratee2_1 . run , Duration . Inf )) println ( "0 + 1 + 234 + 162 = " + Await . result ( sumIteratee2_2 . run , Duration . Inf ))

다음과 같은 결과가 출력됩니다.

0 = 0
0 + 1 = 1
0 + 1 + 234 = 235
0 + 1 + 234 + 583 = 818
0 + 1 + 234 + 162 = 397

예 6 : Iteratee [E, A]는 결과가 필요할 때까지 아무런 실행되지 않는다

Iteratee [E, A]는 정말 결과가 필요할 때까지 아무것도 실행되지 않습니다.

val  initialValue :  Int  =  0 
val  sumIteratee0 :  Iteratee [ Int , Int ]  =  Iteratee . fold ( initialValue )  {  ( total ,  e )  =>  { 
    println ( "e ="  +  e . toString ) 
    total  +  e 
  } 
} 
// feed 한 것만으로는 계산되지 않는 
val  sumIteratee1  =  Iteratee . flatten ( sumIteratee0 . feed ( Input . El ( 1 ))) 
val  sumIteratee2  =  Iteratee . flatten ( sumIteratee1 . feed ( Input . El ( 234 ))) 
val  sumIteratee3  =  Iteratee . flatten ( sumIteratee2 . feed ( Input . El ( 455 ))) 
val  sumIteratee4  =  Iteratee . flatten ( sumIteratee3 . feed ( Input . El ( 987 ))) 

// 아직 계산되지 않는 
val  futureTotal :  Future [ Int ]  =  sumIteratee4 . run

println ( "caclulate result" ) 
val  total  =  Await . result ( futureTotal ,  Duration . Inf ) 
println ( "0 + 1 + 234 + 455 + 987 ="  +  total . toString )

다음과 같은 결과가 출력됩니다.

caclulate result
e = 1
e = 234
e = 455
e = 987
0 + 1 + 234 + 455 + 987 = 1677

예 7 : Input.Empty과 Input.EOF

Input.Empty을 Iteratee [E, A] 에 부여하면 무시됩니다. 
Input.EOF을 Iteratee [E, A]에 공급하면 반복을 중지합니다.

val  initialValue :  Int  =  0 
val  sumIteratee0 :  Iteratee [ Int , Int ]  =  Iteratee . fold ( initialValue )  {  ( total ,  e )  =>  total  +  e  }

val  sumIteratee1  =  Iteratee . flatten ( sumIteratee0 . feed ( Input . El ( 1 ))) 
val  sumIteratee2  =  Iteratee . flatten ( sumIteratee1 . feed ( Input . Empty )) 
val  sumIteratee3  =  Iteratee . flatten ( sumIteratee2 . feed ( Input . El ( 234 ))) 
val  sumIteratee4  =  Iteratee . flatten ( sumIteratee3 . feed ( Input . EOF )) 
val  sumIteratee5  =  Iteratee . flatten ( sumIteratee4 . feed ( Input . El ( 455 ))) 
val  sumIteratee6  =  Iteratee . flatten ( sumIteratee5 . feed ( Input . El ( 987 ))) 

// Input.Empty를 feed 후 feed 값은 계산되는 
println ( "0 + 1 + 234 ="  +  Await . result ( sumIteratee3 . run ,  Duration . Inf )) 

// Input.EOF를 feed 한 이후에 feed 값은 계산되지 않는 
println ( "0 + 1 + 234 + 455 ="  +  Await . result ( sumIteratee5 . run ,  Duration . Inf )) 
println ( "0 + 1 + 234 + 455 + 987 = "  +  Await . result ( sumIteratee6 . run ,  Duration . Inf ))

이 프로그램을 실행하면 다음과 같은 결과가 출력됩니다.

0 + 1 + 234 = 235
0 + 1 + 234 + 455 = 235
0 + 1 + 234 + 455 + 987 = 235

결과에서 알 수 있듯이, Input.EOF을 Iteratee [E, A에 미치는 반복을 멈추기 때문에 Input.EOF을 준 후 Input.El (455)과 Input.El (987)는 주어서 총합은 변화하지 않습니다. 또한 Input.EOF을 공급하면 반복을 중지하는 것을 확인할 수 있습니다.

+ Recent posts