'play2 강좌'에 해당하는 글 2건



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)
}




WRITTEN BY
[前草] 이승현 (wowlsh93@gmail.com)
스타코프 (데이터지능플랫폼pd) (관심분야: 에너지IoT, 시계열(NILM) 데이터, 폴리글랏 프로그래밍 )

트랙백  0 , 댓글이 없습니다.
secret



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 님 코멘트- 


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





WRITTEN BY
[前草] 이승현 (wowlsh93@gmail.com)
스타코프 (데이터지능플랫폼pd) (관심분야: 에너지IoT, 시계열(NILM) 데이터, 폴리글랏 프로그래밍 )

트랙백  0 , 댓글이 없습니다.
secret