관리 메뉴

HAMA 블로그

예제로 보는 아카(akka) - 20. Akka HTTP [번역] 본문

Akka

예제로 보는 아카(akka) - 20. Akka HTTP [번역]

[하마] 이승현 (wowlsh93@gmail.com) 2017. 3. 27. 10:39


AKKA HTTP

[번역]  https://sachabarbs.wordpress.com/2016/11/16/akka-http/


지난 번에 우리는 Akka 에서의 라우팅에 대해 이야기했다. 이번에는 Akka의 http 지원에 대해 알아 보겠는데, 그 전에 약간의 역사에 대해 살펴보자.  Akka.Http가 있기 전에 스칼라 개발자들은 이미 Spray라는 Akk 기반 http 라이브러리를 사용할 수 있었다. 여기에 Spray설명서가 있으니 참고 하시고. http://spray.io/

이 프레임워크는 매우 잘 작성되어 있기 때문에 이 팀이 수행해 많은 놓은 작업을 이용해서 Akka.Http에 대한 많은 코드베이스가 형성되어 졌다. 실제로 Spray에 익숙하다면 Akka.Http에서서 route 와 JSON이 처리되는 방식을 보고 많은 유사점을 발견 할 수 있을 것이다.

 

소개


Akka.Http에는 서버 측 라이브러리와 클라이언트 측 라이브러리가 있으며,  JSON / XML과 같은 표준 직렬화 및 네 자신의 직렬화 기능을 지원한다. 또한 Spray에서 수행 한 작업에 매우 영감을 받은 꽤 멋진 라우팅 DSL이 제공된다.

이 글에서는 HTTP를 사용하여 작업 할 때 발생할 수있는 일반적인 사용 사례에 중점을 둔다.

 

SBT 의존성

평소대로 우리는 올바른 JAR을 참조하도록 해야 한다. 그래서 여기에 서버 쪽 / 클라이언트 쪽과 그 사이에 전달되는 공통 메시지 모두에 사용되고 있는 SBT 파일을 소개한다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import sbt._
import sbt.Keys._
 
 
lazy val allResolvers = Seq(
  "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/",
  "Akka Snapshot Repository" at "http://repo.akka.io/snapshots/"
)
 
 
lazy val AllLibraryDependencies =
  Seq(
    "com.typesafe.akka" % "akka-actor_2.11" % "2.4.12",
    "com.typesafe.akka" % "akka-http_2.11" % "3.0.0-RC1",
    "com.typesafe.akka" % "akka-http-core_2.11" % "3.0.0-RC1",
    "com.typesafe.akka" % "akka-http-spray-json_2.11" % "3.0.0-RC1"
  )
 
 
lazy val commonSettings = Seq(
  version := "1.0",
  scalaVersion := "2.11.8",
  resolvers := allResolvers,
  libraryDependencies := AllLibraryDependencies
)
 
 
lazy val serverside =(project in file("serverside")).
  settings(commonSettings: _*).
  settings(
    name := "serverside"
  )
  .aggregate(common, clientside)
  .dependsOn(common, clientside)
 
lazy val common = (project in file("common")).
  settings(commonSettings: _*).
  settings(
    name := "common"
  )
 
lazy val clientside = (project in file("clientside")).
  settings(commonSettings: _*).
  settings(
    name := "clientside"
  )
  .aggregate(common)
  .dependsOn(common)

이 JAR에는 JSON 종속성이 포함되어 있음을 알 수 있다.

1
akka-http-spray-json_2.11

이미  AKKA.HTTP 는 spray 에 의해 영감을 받은 것이라고 말했던 것을 기억하라. 

 

서버 사이드 

이 섹션에서는 Akka.Http의 서버 측 요소에 대해 설명한다.

 

Hosting The Service

올바르게 구성된 / 호스트 가능한 서버 측을 가지려면 다음과 같은 것들이 필요하다.

  • An actor system
  • A materializer (Akka http uses flows which is the subject of the next and final post)
  • An execution context
  • Routing

일단 이러한 것들을 기술 하게 된다면 실제로 경로를 호스트 이름과 포트에 바인딩하는 것이 된다.

아래 소스를 보고 어떻게 생겼는지 명확히 이해하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import akka.NotUsed
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
import akka.http.scaladsl.server.Directives
import akka.stream.scaladsl.Flow
import common.{Item, JsonSupport}
import scala.io.StdIn
import scala.concurrent.Future
import akka.http.scaladsl.model.ws.{Message, TextMessage}
import akka.stream._
import akka.stream.scaladsl._
 
 
object Demo extends App with Directives with JsonSupport {
 
  implicit val system = ActorSystem("my-system")
  implicit val materializer = ActorMaterializer()
 
 
  val route = .....
 
  val (host, port) = ("localhost", 8080)
  val bindingFuture = Http().bindAndHandle(route, host, port)
 
  bindingFuture.onFailure {
    case ex: Exception =>
      println(s"$ex Failed to bind to $host:$port!")
  }
 
  println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
  StdIn.readLine() // let it run until user presses return
  bindingFuture
    .flatMap(_.unbind()) // trigger unbinding from the port
    .onComplete(_ => system.terminate()) // and shutdown when done
}

우리는 라우팅 DSL을 따로 볼 것 이다.

 

Routing DSL

명시된 바와 같이, Akka.Http는 Spray에 많은 빚이 있고, 라우팅 DSL은 실제로 Spray와 변한게 없으므로 Spray 라우팅 문서 ( http://spray.io/documentation/1.2.4/spray-routing/ )를 읽는 것이 좋다.
또한   Akka.Http docs 링크도 첨부한다. 
 http://doc.akka.io/docs/akka/2.4.7/scala/http/introduction.html#routing-dsl-for-http-servers


이 예제 중 일부는 다음 주제 인 JSON에 의존하기 때문에 일단 여기서는 JSON을 accept/return 하는 방법이 있다는 것을 염두해두자. 

다음 사용 사례를 고려해 보자.

  • 간단한 문자열을 반환하는 GET
  • 항목의 JSON 표현을 반환하는 GET
  • 새 항목을 허용하는 POST

이 모든 경우에 이것은 아이템이 보이는 것과 같다.

1
2
3
package common
 
final case class Item(name: String, id: Long)

위의 예제가 작동하도록하는 라우팅 DSL을 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
val route =
  path("hello") {
    get {
      complete(HttpEntity(
    ContentTypes.`text/html(UTF-8)`,
    "<h1>Say hello to akka-http</h1>"))
    }
  } ~
  path("randomitem") {
    get {
      // will marshal Item to JSON
      complete(Item("thing", 42))
    }
  } ~
  path("saveitem") {
    post {
      // will unmarshal JSON to Item
      entity(as[Item]) { item =>
        println(s"Server saw Item : $item")
        complete(item)
      }
    }
  }


거기에 몇 가지 일반적인 라우팅 DSL bits 와 bobs 이 있다는 것을 알 수 있다 :

  • path : which satisfies the route name part of the route
  • get : which tells us that we should go further into the route matching if it’s a GET http request and it matched the path route DSL part
  • post: which tells us that we should go further into the route matching if it’s a POST http request and it matched the path route DSL part
  • complete : This is the final result from the route


DSL의 이러한 부분을 지시문이라고 하며 지시문의 일반적인 내부는 다음과 같다.

1
2
3
name(arguments) { extractions =>
  ... // inner route
}


이름 및  0 개 이상의 인수 및 선택적으로 내부 경로를가진다. (RouteDirectives는 잎 수준 (leaf-level) 에서 항상 사용되고 내부 경로를 가질 수 없다는 점에서 특별하다). 또한 지시어는 여러 값을 "추출"하여 내부 경로에서 함수 인수로 사용할 수 있다. "외부에서"볼 때 내부 경로가있는 지시문은 Route 유형의 표현식을 형성한다.

 http://doc.akka.io/docs/akka/2.4.7/scala/http/routing-dsl/directives/index.html#directives  (날짜 : 15/11/16)에서 가져 왔다.


What Directives Do?

지시문은 다음 중 하나 이상을 수행 할 수 있다.

  • Transform the incoming RequestContext before passing it on to its inner route (i.e. modify the request)
  • Filter the RequestContext according to some logic, i.e. only pass on certain requests and reject others
  • Extract values from the RequestContext and make them available to its inner route as “extractions”
  • Chain some logic into the RouteResult future transformation chain (i.e. modify the response or rejection)
  • Complete the request


즉, 지시문이 내부 경로의 기능을 완전히 래핑하고 요청 및 응답 측면 모두에서 임의로 복잡한 변환을 적용 할 수 있음을 의미한다.

자, 이제 라우팅 DSL과 지시어에 대한 빠른 둘러보기 여행을 마쳤으므로 위에서 논의한 몇 가지 사항을 살펴 보도록 하겠다.


이 작업을 위해 나는 "Postman" Google 앱의 사용을 강력히 권한다.

https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=en


GET

이 route 는 다음과 같다.

1
2
3
4
5
path("hello") {
  get {
    complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to akka-http</h1>"))
  }
}


우리는 path를 사용하고 get 지시문을 사용하여 get route를 설정하며, 다음 complete 를 사용하여 반환 할 HTML을 나타내는 정적 문자열로 경로를 완성한다.

그럼 Postman 에서 이걸 GET 호출해서 확인해보자.

image

 

GET Item (as JSON)

이 route 는 다음과 같다.

1
2
3
4
5
6
path("randomitem") {
  get {
    // will marshal Item to JSON
    complete(Item("thing", 42))
  }
}


다시 path/get  지시문을 사용하지만 이번에는 Item을 complete 하는데 이는 우리에게 적합한 직렬화 데이터를 생성 할 수있는 JSON 지원으로 인해 수행된다. 다음 섹션에서 이것을 보게 될 것이다.


그럼 Postman 에서 이걸 보자.

image

POST Item

이 route 는 다음과 같다.

1
2
3
4
5
6
7
8
9
path("saveitem") {
  post {
    // will unmarshal JSON to Item
    entity(as[Item]) { item =>
      println(s"Server saw Item : $item")
      complete(item)
    }
  }
}


다시 path 지시문을 사용하지만 이번에는 post에서 JSON이라는 항목이 제공 될 것으로 기대하는post 을 사용한다. 들어오는 JSON 문자열을 항목으로 변환하는 작업은 Unmarshaller를 사용하여 수행된다. 


그럼 Postman 에서 이걸 보자.

image

 

JSON Support


Akka.http는 Maven Central Repo에서 얻을 수있는이 라이브러리 akka-http-spray-json-experimental을 사용하여 JSON 지원을 제공한다.

JsonProtocol

spray 를 사용할 때 SprayJsonProtocol 및 DefaultJsonProtocol을 사용하여 사용자 정의 객체에 대한 JSON protcol을 만들 수 있다.


지금까지 데모에서 본 Item 클래스를 고려해 보겠다.

1
2
3
package common
 
final case class Item(name: String, id: Long)


이것이 우리가 이 간단한 클래스를 위한 JSON 프로토콜 코드를 작성하는 방법이다.

1
2
3
4
5
6
7
8
package common
 
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import spray.json.DefaultJsonProtocol
 
trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
  implicit val itemFormat = jsonFormat2(Item)
}


아주 간단한 경우에 사용할 수있는 jsonFormatXX 도우미가 있다는 것을 알 수 있다. 이 경우 jsonFormat2는 항목 클래스가 2 개의 매개 변수를 가졌기 때문에 사용된다는 것을 알 수 있을 것이다.

이 내장 된 도우미는 대부분 우리가 필요로하는 모든 것입니다. 그러나 좀 더 정교한 것을 원하면 독자적인  jsonFormat 읽기 / 쓰기 메소드를 자유롭게 만들 수 있습니다.

Marshalling

마샬링은 객체를 가져 와서 JSON 문자열 표현을 만들어 와이어를 통해 전송하는 sprays 프로세스이며Akka Spray JAR에는 커스텀 클래스를 가져 와서 JSON으로 바꿀 수있게 해주는 많은 기본 마샬러가 있다.

가장 자주 사용하는 일반적인 기본 마셜러는 아래와 같다.

1
2
3
4
type ToEntityMarshaller[T] = Marshaller[T, MessageEntity]
type ToHeadersAndEntityMarshaller[T] = Marshaller[T, (immutable.Seq[HttpHeader], MessageEntity)]
type ToResponseMarshaller[T] = Marshaller[T, HttpResponse]
type ToRequestMarshaller[T] = Marshaller[T, HttpRequest]


여기에 대한 자세한 내용은 여기를 참조하라.

http://doc.akka.io/docs/akka/2.4.7/scala/http/common/marshalling.html

다행히도 라우팅 DSL이 작업을 수행 할 때 라우팅 DSL이 대부분의 작업을 수행 할 때 이러한 작업을 수반하지 않아도 된다. 암시적으로 찾을 수 있는 마샬러가 있으면 이 작업을 처리 할 수 있게 된다.


Unmarshalling


언 마샬링은 와이어 형식 (이 예제에서는 JSON 문자열)을 다시 스칼라 클래스 (이 경우 Item 클래스)로 가져 오는 프로세스이다.

이에 대한 자세한 내용은 Akka docs 공식 페이지에서 읽을 수 있습니다.

http://doc.akka.io/docs/akka/2.4.7/scala/http/common/unmarshalling.html

다행히도 라우팅 DSL이 라우팅 작업의 대부분을 수행 할 때 라우팅 DSL의이 부분을 사용하는 경우가 종종 있다. 즉, 라우팅 마샬러가 항목 작성을 위해 언 마샬러를 사용한다.

1
entity(as[Item]) { item =>

WebSockets

Akka HTTP도 웹 소켓을 지원하는데, 라우팅 DSL 관점에서 무엇이 필요한지 살펴보면서 이 조사를 시작할 수 있다.


1
2
3
4
5
path("websocket") {
  get {
    handleWebSocketMessages(websocketFlow)
  }
} ~


이 특별한 지시어를 좀 더 살펴보면 handleWebSocketMessages 지시어 정확히 무엇 같은지 알게 될 것이다.

다음과 같습니다.

1
def handleWebSocketMessages(handler: Flow[Message, Message, Any]): Route


우리는 flow을 제공해야 하는데  A Flow는 다음 부분에서 볼 수있는 반응형 스트림의 일부이다. 그러나 이제는 Sink / Source 및 Materializer에서 Flow를 생성하여 흐름을 구체화 할 수 있다는 사실을 기억하라.

이 websocket 예제는 Flow가 어떻게 생겼는지 보여준다.

1
2
3
4
5
6
7
8
9
10
val (websocketSink, websocketSource) =
  MergeHub.source[String].toMat(BroadcastHub.sink[String])(Keep.both).run()
 
val websocketFlow: Flow[Message, Message, NotUsed] =
  Flow[Message].mapAsync(1) {
    // transform websocket message to domain message (string)
    case TextMessage.Strict(text) =>       Future.successful(text)
    case streamed: TextMessage.Streamed => streamed.textStream.runFold("")(_ ++ _)
  }.via(Flow.fromSinkAndSource(websocketSink, websocketSource))
    .map[Message](string => TextMessage(string))


아이디어는 websocket 클라이언트가 연결하여 초기 메시지를 보내면 Websocket을 통해 TextMessage가 보낸 답장을 받게된다는 것이다.


이것은 꽤 새로운 akka 스트림 단계를 사용한다.

  • MergeHub : Creates a Source that emits elements merged from a dynamic set of producers.
  • Broadcast : Emit each incoming element each of n outputs

 

서버를 실행 한 다음이 "WebSocketTestClient.html"페이지를 여는 것으로 시작하라.

image

image

페이지가 열리면 텍스트 상자에 내용을 입력하고 '보내기'버튼을 누르면

image


지금까지 꽤 일반적인 예제들은 모두 웹 페이지 클라이언트 측에서 서버로 메시지를 보내고 서버는 우리가 보낸 텍스트로 응답 하는 것이 었습니다.

그러나 우리가 요청에 따라 클라이언트에게 메시지를 보내고 싶다면, 클라이언트에게 websocket을 알려주는 명령을 내릴 수있는 다른 route에 대해서 말해 보자면 어떨까?

이 흐름을 통해 우리는 websocket의 클라이언트쪽에 메시지를 push  할 수 있다.

어떤 작업을 시뮬 레이팅 할 또 다른 라우트를 볼 수 있는데 결과적으로 메시지가 클라이언트에 websocket으로 전송된다. (여전히 연결되어있는 경우)

1
2
3
4
5
6
7
8
9
path("sendmessagetowebsocket" / IntNumber) { msgCount =>
  post {
    for(i <- 0 until msgCount)
    {
      Source.single(s"sendmessagetowebsocket $i").runWith(websocketSink)
    }
    complete("done")
  }
}

websocket이 사용하는 Flow의 일부인 기존 Sink로 실행되는 새로운 소스를 간단히 생성한다는 것을 알 수 있습니다

이것이 postman에서 보이는 모습입니다.

image

그리고 위의 경로가 호출 된 후 웹 페이지 클라이언트 사이드 웹 소켓 예제가 어떻게 생겼는지 보자.

image

 

 

Client Side


Akka http 에는 사용할 수있는 3 가지 클라이언트 API가 함께 제공 된다.


이 기사에서는 필자가 생각하기에 가장 현명한 클라이언트 쪽 선택이므로 이 API의 마지막 부분 만 사용하게 될 것이다.

그러면 Request level 클라이언트 API는 어떻게 생겼을까?

GET

이 요청을 수행하기를 원할 경우

http://localhost:8080/randomitem

postman을 통해 실행될 때 다음 JSON 응답을 제공합니다.

image

따라서 request level 클라이언트 API를 사용하여 코드가 어떻게 보이는지 확인할 수 있다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.marshalling.Marshal
import akka.http.scaladsl.model._
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
import akka.stream.scaladsl._
import scala.concurrent.{Await, Future}
import concurrent.ExecutionContext.Implicits.global
import common.{Item, JsonSupport}
import concurrent.duration._
import scala.io.StdIn
 
class RegularRoutesDemo extends JsonSupport {
 
  def Run() : Unit = {
    implicit val system = ActorSystem()
    implicit val materializer = ActorMaterializer()
 
    val httpClient = Http().outgoingConnection(host = "localhost", port = 8080)
 
    //+++++++++++++++++++++++++++++++++++++++++++++++
    //+++++++++++++++++++++++++++++++++++++++++++++++
    val randomItemUrl = s"""/randomitem"""
    val flowGet : Future[Item] =
      Source.single(
        HttpRequest(
          method = HttpMethods.GET,
          uri = Uri(randomItemUrl))
        )
        .via(httpClient)
        .mapAsync(1)(response => Unmarshal(response.entity).to[Item])
        .runWith(Sink.head)
    val start = System.currentTimeMillis()
    val result = Await.result(flowGet, 5 seconds)
    val end = System.currentTimeMillis()
    println(s"Result in ${end-start} millis: $result")
 
  }
}

위의 코드에는 몇 가지 문제가 있습니다.

  • We use a Source which is a HttpRequest, where we can specify the HTTP verb and other request type things
  • We use Unmarshal to convert the incoming JSON string to an Item. We discussed Marshalling/Unmarshalling above.
  • This obviously relies on the Spray JSON support that we discussed above.

 

POST

이 요청을 수행하기를 원할 경우

http://localhost:8080/saveitem

postman을 통해 실행될 때 다음 JSON 응답을 제공합니다.

image

따라서 요청 수준 클라이언트 API를 사용하여 코드가 어떻게 보이는지 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.marshalling.Marshal
import akka.http.scaladsl.model._
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
import akka.stream.scaladsl._
import scala.concurrent.{Await, Future}
import concurrent.ExecutionContext.Implicits.global
import common.{Item, JsonSupport}
import concurrent.duration._
import scala.io.StdIn
 
class RegularRoutesDemo extends JsonSupport {
 
  def Run() : Unit = {
    implicit val system = ActorSystem()
    implicit val materializer = ActorMaterializer()
 
    val httpClient = Http().outgoingConnection(host = "localhost", port = 8080)
 
    //+++++++++++++++++++++++++++++++++++++++++++++++
    //+++++++++++++++++++++++++++++++++++++++++++++++
    val saveItemUrl = s"""/saveitem"""
    val itemToSave = Item("newItemHere",12)
    val flowPost = for {
      requestEntity <- Marshal(itemToSave).to[RequestEntity]
      response <-
      Source.single(
        HttpRequest(
          method = HttpMethods.POST,
          uri = Uri(saveItemUrl),
          entity = requestEntity)
        )
        .via(httpClient)
        .mapAsync(1)(response => Unmarshal(response.entity).to[Item])
        .runWith(Sink.head)
    } yield response
    val startPost = System.currentTimeMillis()
    val resultPost = Await.result(flowPost, 5 seconds)
    val endPost = System.currentTimeMillis()
    println(s"Result in ${endPost-startPost} millis: $resultPost")
  }
}


이번에 유일하게 다른 점은 우리가 전달한 Item의 JSON 문자열 표현을 HttpRequest에 전달해야한다는 것 이다.

이것은 암시적으로 범위 내에 있어야하는 JSON 마샬러를 사용하여 수행된다.

 

코드 예제는 아래에서 찾을 수 있으며, 보강 될 예정이다.

https://github.com/sachabarber/SachaBarber.AkkaExamples


Comments