관리 메뉴

HAMA 블로그

Play 해부 : 웹서버 이야기 본문

PlayFramework2

Play 해부 : 웹서버 이야기

[하마] 이승현 (wowlsh93@gmail.com) 2017. 4. 19. 10:42


역주: 몇년 지난 이 글에 오류가 있을 수 있으며, 겉핣기 수준의 플레이 지식에 기반한 제 번역에도 오류기 있을 수 있음을 알려드립니다. 수정해야할 부분에 대해서 코멘트 주시면 반영하겠으며, 많은 정보 공유가 필요합니다. 더 정확한 이해를 위해서 직접 소스를 보는 방법이 가장 확실합니다. 저도 나중에 시간만 된다면 전체 소스리딩을 하고 싶네요. 지난 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 가 전달됩니다.




Comments