ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [도전] Scala로 REST API 만들려면 어떻게 하나? #1
    기술 관련/Scala 2021. 2. 4. 17:35

    Java 개발자 입장에서 스칼라에 대해 맛보기를 했다면, 이번에는 간단 REST API를 만들어 보고 싶었다.

     

    Python에는 Django나 Flask가 주로 많이 사용되고, Node.js 기반의 Javascript에서는 Express.js나 Nest.js 정도를 이용하는게 일반적이다. Scala에서도 HTTP 요청을 처리하는 기능을 쓸 수 있을 텐데 이를 그냥 사용하지는 않을 것 같다. 그렇다면 과연 Scala를 이용한 REST API 서비스를 만들려면 어떤 것을 이용해야 할까?

     

    스칼라용 웹프레임워크가 뭐가 있을까?

    스칼라를 위한 REST API나 Web Service를 지원하는 프레임 워크를 좀 찾아봤는데, 생각보다 많지는 않았고, 아래 링크를 통해 몇 가지를 확인 할 수 있었다. 

     

     

    더보기

    1. Play Framework

     

    www.playframework.com/

     

    Backend 지원을 위한 다양한 구조를 제공하고 

    커뮤니티를 통해 수 많은 플러그인이 제공되지만 안정성 여부가 검증된 것은 아님

     

    2. Finch

    github.com/finagle/finch

     

    Twitter Finagle HTTP를 Wrapping 한 것.

    빠르고 간편하므로 소규모 스타트업에게 유리하지만, 최소한의 것만 구현되어 있어서 확장 및 Full Stack 솔루션으로 부적합

     

    3. Akka HTTP

    doc.akka.io/docs/akka-http/current/index.html

     

    Lightbend에서 제공하는 akka-actor와 akka-stream을 기반으로 서버 및 클라이언트용 HTTP 스택

    다양한 문서 및 지원을 받을 수 있으며 Akka를 기반으로 병렬 커맨드와 고급 계산 처리을 할 수 있는 기능 제공

    다른 프레임워크에 비해 속도가 떨어지며, lightbend에 vendor lock-in이 단점

     

    4. Chaos

    github.com/mesosphere/chaos

     

    Mesosphere에서 만든 스칼라용 REST services 서비스 경량 프레임워크

    사용이 쉽고 지원이 잘 됨

    다양한 라이브러리를 조합해서 사용하므로 해당 라이브러리의 영향을 받을 수 있다. 

    딱 REST 만을 위해 만들었기 때문에 다른 기능을 통합하는 기본 라이브러리로 선택하기 어려움

     

    5. Lift

    liftweb.net/  

     

    보안 기능에 충실

    커뮤니티가 작고 문서 업데이트가 잘 안되고 있다.

    내부에 stateful 한 형태로 구성 된 부분이 있어 serialize 되어 공유되거나 하면 문제가 있을 수 있다.

    2018/07/21이 마지막 릴리즈

     

    6. BlueEyes

    github.com/jdegoes/blueeyes

     

    스칼라용 경량 Web 3.0 프레임웍

    서버사이드 생성 리소스에 대한 지원이 없다 (HTML, CSS, or JavaScript)

    전체가 비동기 방식이므로 동기식이 필요한 경우 문제가 된다

    2013/10/30 마지막 커밋

     

    7. Slick

    github.com/slick/slick

    스칼라에서 DB를 접근하기 위한 라이브러리

     

    다양한 DB를 지원하지만 MySQL에 대해 성능이 나쁘다.

    DBIO 에 특화되어 있다

     

    8. Scalatra

    scalatra.org

     

    단순하면서 가벼운 프레임워크

    Ruby의 Sinatra에 영감을 받아 만듬

     

    빠른 성능으로 RESTful 애플리케이션에 효과적

    Akka를 사용 하여 HTTP, Atmosphere를 이용하여 WebSocket이 구현되어 있다.

     

    2020/03/01에 마지막 업데이트

    동기 방식을 쓰는 경우 부적합

    기본적인 기능만 제공

     

    9. Spray

    spray.io/

     

    Elegant, high-performance HTTP for your Akka Actors.

    Akka HTTP로 대체되어 더이상 지원 안함

     

    10. Skinny

    skinny-framework.org/

     

    Scala on Rails를 표방 

    Full Stack 기능을 제공

     

    lasted updated :  12 Feb, 2020

     

    그래서 선택한 것은?

    위의 후보들 중에 선택된 것은 Play Framework, Scalatra 그리고 Akka HTTP 이다.

     

    사람들이 가장 많이 사용하는 것은 Play Framework이지만, Scalatra가 좀 더 사용에 편리하다는 의견이 있어서 Scalatra를 공부해 보려고 했는데, Akka HTTP가 계속 눈에 들어 왔다.

     

    사실, Scalatra에서도 Akka HTTP를 이용할 수 있다 하고, Play Framework도 Akka Framework의 부분이라고 하는 것을 보면, Akka HTTP가 이런 서비스들의 기본이 되는 것이 아닐까 하는 생각이 든다. 특히, Lightbend가 스칼라를 만든 Martin Odersky과 Akka를 만든 Jonas Bonér가 만든 회사라는 점에서, 우선 Akka를 먼저 배워 보고 그 다음에 Play Framework 그리고, Scalatra를 하나씩 살펴봐야겠다는 생각이 들었다.

     

    AKKA로 만들어 보는 Hello World

    여지 없이 여기도 Hello World 튜토리얼을 제공하고 있다.

    developer.lightbend.com/guides/akka-quickstart-scala

     

    우선 예제 프로젝트를 Lightbend 홈페이지에서 다운로드 받는다.

    developer.lightbend.com/start/?group=akka&project=akka-quickstart-scala

     

    Get Started with Lightbend Technologies | Lightbend Tech Hub

    Still loading, please wait...

    developer.lightbend.com

    압축 파일을 풀면 다음과 같은 파일들이 나온다. 아래 보이는 것 중에 sbt 라는 스크립트 파일이 있는데, sbt는 simple build tool로서  maven과 비슷하게 프로젝트를 빌드하는 툴이다. Scala 뿐만 아니라 Java도 빌드 할 수 있다. 추가적인 정보는 sbt 홈페이지에서 확인 해 볼 수 있다.

     

     

    프로젝트를 디렉토리에서 처음 sbt 실행 스크립드를 실행하면  Error: Unable to access jarfile 이란 메시지가 출력되고, 잠시 기다리면 필요한 파일들을 자동으로 다운로드 받는다. (환경에 따라 약간 시간이 더 걸리 수 있다). 프로젝트에 필요한 파일들을 다 받고 나면 sbt shell 화면이 나온다.

    sbt shell에서 reStart 라는 명령을 입력하고 엔터키를 누르면, 현재 project를 빌드하고 Hello World를 실행시킨다. 정상적으로 진행된 경우 다음과 같이 환영 메시지가 출력되는 것을 볼 수 있다.

     

    음.. 일단 실행은 했는데 구조가 어떻게 된걸까? 튜토리얼에는 이 예제가 다음 세 가지 Actor로 되어 있다고 설명한다.

     

    • Greet: 누군를 환영하는 명령을 받고 인사가 제대로 받아졌는지 확인을 위한 응답으로 인사를 보낸다
    • GreeterBot: 환영자로 부터 회신을 받아 추가적인 환영 메시지들을 몇 개 보내며, 전달된 메시지가 수가 주어진 최대 수가 되기 까지 회신을 수집한다.
    • GreeterMain: 가디언 액터(guardian actor)로서 처음 시작 부분이다

    음.. 액터는 뭐고 가디언은 또 뭔가? AKKA 만의 새로운 용어들이 등장하기 시작한다.

     

    액터 (Actor)

    액터(Actor)는 특정한 상태와 행동이 정의된 객체로 볼 수 있다. 공식 문서에는 액터를 사람처럼 바라보는게 더 낫다고 이야기는데, 사람이 특정 일을 하는 사람들이 조직을 갖추어 움직이는 것처럼 액터도 액터 시스템(Actor System)이란 조직을 바탕으로 구성된다고 한다.

     

    액터 시스템 (Actor System)

    액터 시스템은 JVM 기반으로 실행되며, 서비스 스케쥴링, 설정, 로깅 등이 포함된 공용 기능을 관리하는 단위이다. Thread가 여러 개 생성될 수 있는 있는 heavy weight 구조이므로 하나의 JVM에 하나의 액터 시스템을 구성하도록 안내하고 있다. 논리적인 애플리케이션의 단위로 액터 시스템을 인식하면 될 듯 하다. 

     

    액터 시스템의 본질적인 기능으로 child 액터를 이용하여 parent 액터의 업무를 작게 분산하는 것을 이야기하고 있다. 그렇게 함으로써, 액터가 수행할 업무 즉, 어떤 메시지를 처리해야하는지, 그것을 잘 처리하는 것과 오류가 발생했을 때는 어떻게 처리해야 하는지가 명확하게 하는 것이 중요하다고 강조 한다.

     

    가디언 Actor (Guardian Actor)

    AKKA에서는 Actor는 자신으로부터 child Actor를 만들어 낼 수 있다. 그렇다면, Actor 시작하는 가장 상위에 있는 Actor가 필요하게 되는데 그런 Actor를 Guardian(보호자) Actor라 부른다.

     

    다시, 튜토리얼 예제로 돌아와서, 가디언 액터인 GreeterMain 을 보자.

     

    GreeterMain은 akka-quickstart-scala/src/main/scala/com/example 경로에 AkkaQuickstart.scala 파일에 scala object로 정의되어 있다. 모든 액터 Object는 자신의 생성 parameter를 넘겨 받아 Behavior를 생성하는 apply() method를 가지고 있어야 한다. 아래 GreeterMain는 SayHello 케이스 클래스 형태로 "인사말" 문자열 정보로 넘겨 받는 Behavior를 생성한다.

    //#full-example
    package com.example
    
    
    import akka.actor.typed.ActorRef
    import akka.actor.typed.ActorSystem
    import akka.actor.typed.Behavior
    import akka.actor.typed.scaladsl.Behaviors
    import com.example.GreeterMain.SayHello
    
    // ...
    
    //#greeter-main
    object GreeterMain {
    
      final case class SayHello(name: String)
    
      def apply(): Behavior[SayHello] =
        Behaviors.setup { context =>
          //#create-actors
          val greeter = context.spawn(Greeter(), "greeter")
          //#create-actors
    
          Behaviors.receiveMessage { message =>
            //#create-actors
            val replyTo = context.spawn(GreeterBot(max = 3), message.name)
            //#create-actors
            greeter ! Greeter.Greet(message.name, replyTo)
            Behaviors.same
          }
        }
    }
    //#greeter-main

     

     

    GreeterMain이 Guardian Actor로서 처음 생성되는 Actor긴 하지만 어디선가는 이 Actor를 만드는 것에서 시작하게 되어 있으며, App 트레잇을 상속 받은 AkkaQuickstart에서 시작한다.

     

    AkkaQuickstart에서 ActorSystem을 이용 GreeterMain Actor를 생성하고, 생성된 instance에 GreeterMain.SayHello 케이스 클래스를 메시지로 전송한다.

    //#main-class
    object AkkaQuickstart extends App {
      //#actor-system
      val greeterMain: ActorSystem[GreeterMain.SayHello] = ActorSystem(GreeterMain(), "AkkaQuickStart")
      //#actor-system
    
      //#main-send-messages
      greeterMain ! SayHello("Charles")
      //#main-send-messages
    }
    //#main-class

    여기서는 "Charles" 에게 인사하는 것으로 시작하게 되는데, 이러면 GreeterMain에 메시지가 전달된다.  

     

    다시 GreeterMain을 보자.

    //#greeter-main
    object GreeterMain {
    
      final case class SayHello(name: String)
    
      def apply(): Behavior[SayHello] =
        Behaviors.setup { context =>
          //#create-actors
          val greeter = context.spawn(Greeter(), "greeter")
          //#create-actors
    
          Behaviors.receiveMessage { message =>
            //#create-actors
            val replyTo = context.spawn(GreeterBot(max = 3), message.name)
            //#create-actors
            greeter ! Greeter.Greet(message.name, replyTo)
            Behaviors.same
          }
        }
    }
    //#greeter-main

     

    Behaviors.setup에서 context를 이용해 child 액터인 Greeter 액터를 만들었고, GreeterMain이 메시지를 받으면 GreeterBot라는 child  액터를 만든 후 Greeter 액터에게 응답하는 메시지를 보낸다.

     

    이 때 회신하는 메시지는 Greeter.Greet 이라는 케이스 클래스이다. GreeterMain이 받는 메시지는 앞서 정의했던 SayHello 케이스 클래스이므로, 넘겨 받은 message에는 name이라는 필드에는 "Charles" 라는 값이 들어 있다.

     

    이제 이 메시지를 받는 Greeter 클래스를 살펴 보자.

    //#greeter-actor
    object Greeter {
      final case class Greet(whom: String, replyTo: ActorRef[Greeted])
      final case class Greeted(whom: String, from: ActorRef[Greet])
    
      def apply(): Behavior[Greet] = Behaviors.receive { (context, message) =>
        context.log.info("Hello {}!", message.whom)
        //#greeter-send-messages
        message.replyTo ! Greeted(message.whom, context.self)
        //#greeter-send-messages
        Behaviors.same
      }
    }
    //#greeter-actor

    Greeter도 GreeterMain처럼 Actor를 위한  apply 메소드가 있으며 Behaviors.receive를 통해 메시지를 전달 받는다. 전달 받는 메시지가 Greet 케이스 클래스이므로 message.who은 앞서 전달 받았던 "Charles"가 되며, message.replyTo는 GreeterBot이 된다.

     

    Greeter 액터는 메시지를 받아서 "Hello Charles!" 라고 출력하고, 뒤이어 GreeterBot 액터에게 Greeted 케이스 클래스를 보낸다. 

    //#greeter-bot
    object GreeterBot {
    
      def apply(max: Int): Behavior[Greeter.Greeted] = {
        bot(0, max)
      }
    
      private def bot(greetingCounter: Int, max: Int): Behavior[Greeter.Greeted] =
        Behaviors.receive { (context, message) =>
          val n = greetingCounter + 1
          context.log.info("Greeting {} for {}", n, message.whom)
          if (n == max) {
            Behaviors.stopped
          } else {
            message.from ! Greeter.Greet(message.whom, context.self)
            bot(n, max)
          }
        }
    }
    //#greeter-bot

    GreeterBot 액터는 GreeterMain 가디언 액터가 만들어서 전달한 것으로 max는 3이라는 값을 주어진 상태로 생성된다. 그리고 bot 이라는 메소드를 구성하여 메시지를 자신이 수신하는 메시지를 처리한다.

     

    메시지를 받을 때 마다 메시지를 출력하는데, 처음 메시지를 받으면 n 값이 greetingCounter + 1 이 1이므로  "Greeting 1 for Charles" 메시지를 출력한다. n 값이 최대 값 3이 아니므로, 메시지를 보냈던 Greeter 액터에게 메시지를 다시 보낸다.

    그러면, Greeter 액터는 메시지를 받아서 다시 "Hello Charles!" 라고 출력하고, 뒤이어 GreeterBot 액터에게 Greeted 케이스 클래스를 보낸다.

     

    그래서, bot() 메소드를 이용하여 메시지를 서로 주고 받는 코드를 재귀적으로 구성했고, greetingCounter가 max값인 3이 되면 Behaviors.stopped를 리턴하면서 메시지 수신이 종료 된다.

    Hello Charles!
    Greeting 1 for Charles
    Hello Charles!
    Greeting 2 for Charles
    Hello Charles!
    Greeting 3 for Charles

     

    이걸 그림으로 그려보면 이 정도가 될 것 같다.

     

     

    설명에 메시지를 보낸다고 이야기 했지만 greeterMain ! SayHello("Charles") 이런 모양이 좀 이상하게 느껴졌을 수도 있는데, !은 메소드로 정의된 것으로, AKKA Actor 공식 문서에는 !를 액터에게 메시지를 전송하기 위한 메소드라고 나와 있다. 참고로, ! 메소드는 Fire-and-Forget 방식으로 메시지를 비동기로 전송하되 결과와 상관 없이 바로 리턴되며(tell 방식), ? 메소드는 Request-Response 방식으로 비동기로 전송하고 Future를 반환하여 그 결과를 확인 할 수 있다(ask 방식). 

     

    테스트 해보기

    Hello World 예제가 잘 실행되는 것은 확인되었고, 프로젝트에는 단순 실행 예제만 들어있는것이 아니라 테스트 코드도 들어 있다. AKKA에서는 다양한 테스트 방법을 제공하고 있지만, 이 예제에서는 ScalaTest라는 프레임워크로 구성되어 있다. 프레임워크라 그런지 모양이 독특하게 되어 있다. 

    //#full-example
    package com.example
    
    import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
    import com.example.Greeter.Greet
    import com.example.Greeter.Greeted
    import org.scalatest.wordspec.AnyWordSpecLike
    
    //#definition
    class AkkaQuickstartSpec extends ScalaTestWithActorTestKit with AnyWordSpecLike {
    //#definition
    
      "A Greeter" must {
        //#test
        "reply to greeted" in {
          val replyProbe = createTestProbe[Greeted]()
          val underTest = spawn(Greeter())
          underTest ! Greet("Santa", replyProbe.ref)
          replyProbe.expectMessage(Greeted("Santa", underTest.ref))
        }
        //#test
      }
    
    }
    //#full-example
    

     

    akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit을 확장해서 사용하고 있는 AkkaQuickstartSpec 오브젝트가 test suite이 되며 "in" 뒤에 코드 블럭이 하나의 testcase가 된다. kkaQuickstartSpec은 AnyWordSpecLike 트레잇을 확장한 것으로 when, must, should, can, which와 같은 메소드가 포함되어 있다.  각각의 것은 코드 블럭으로 묶음 형태로 관리된다. 이러한 것을 Behavior-driven 스타일 개발(BDD)이라 하며, 테스트 케이스에 대한 가독성을 높혀준다. (물론 영어일 경우에 해당한다) 관련하여 추가 설명은 AnyWordSpec 클래스에 잘 나와 있다.

     

    createTestProbe()는 ActorTestKit에서 제공하는 메소드로서 주어진 Actor를 생성하고 결과 확인을 위한 다양한 기능을 제공하는 트레잇인 TestProbe를 리턴한다.

     

    Greeted로 Test Probe를 생성하고 spawn() 메소드를 이용해 Greeter 액션을 만든다. 해당 액션에서 Greet 케이스 클래스로 메시지와 replyProbe 액션에 대한 reference를 전달하고, replyProbe의 expectMessage() 를 이용해 Greeter 액션이 replyProbe로 예상하는 메시지를 보냈는지 확인한다.

     

    이제 sbt로 다시 가서 test란 명령을 입력하면, 테스트가 실행된다.

    [info] compiling 1 Scala source to /Volumes/Works/Project/DevWorks/akka/scala/akka-quickstart-scala/target/scala-2.13/test-classes ...
    SLF4J: A number (1) of logging calls during the initialization phase have been intercepted and are
    SLF4J: now being replayed. These are subject to the filtering rules of the underlying logging system.
    SLF4J: See also http://www.slf4j.org/codes.html#replay
    [2021-02-04 12:03:22,112] [INFO] [akka.event.slf4j.Slf4jLogger] [AkkaQuickstartSpec-akka.actor.default-dispatcher-3] [] - Slf4jLogger started
    [2021-02-04 12:03:23,344] [INFO] [com.example.Greeter$] [AkkaQuickstartSpec-akka.actor.default-dispatcher-3] [akka://AkkaQuickstartSpec/user/$a] - Hello Santa!m.example.AkkaQuickstartSpec 3s
    [info] AkkaQuickstartSpec:
    [info] A Greeter
    [info] - must reply to greeted
    [info] Run completed in 4 seconds, 963 milliseconds.
    [info] Total number of tests run: 1
    [info] Suites: completed 1, aborted 0
    [info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
    [info] All tests passed.
    [success] Total time: 11 s, completed 2021. 2. 4. 오후 12:03:24

     

    AkkaQuickstartSpec Testsuite에서 "A Greeter must reply to greeted" 라는 테스트 케이스 1개가 정상 실행되었다는 것을 볼 수 있다.

    빌드 정의(Build Definition)

    앞서 sbt는 maven 과 비슷하게 프로젝트를 관리하는 도구라고 했었다. 그렇다면 maven의 pom.xml과 같은 프로젝트 정보와 필요한 의존성 정보가 기입되어 있는 파일이 있어야 하는데 그 파일이 build.sbt라는 파일이다. 이 파일에는 현재 프로젝트에 대한 정보 및 빌드에 필요한 모듈 정보를 가지고 있다.

     

    name := "akka-quickstart-scala"
    
    version := "1.0"
    
    scalaVersion := "2.13.1"
    
    lazy val akkaVersion = "2.6.12"
    
    libraryDependencies ++= Seq(
      "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion,
      "ch.qos.logback" % "logback-classic" % "1.2.3",
      "com.typesafe.akka" %% "akka-actor-testkit-typed" % akkaVersion % Test,
      "org.scalatest" %% "scalatest" % "3.1.0" % Test
    )

    필요한 라이브러리는 libraryDependencies 에 정의되어 있으며, groupID, artifactID, revision 를 groupID % artifactID % revision 와 같이 표기한다. 예제에서는 com.typesafe.akkagroupID, akka-actor-typed가 artifactID로 그리고 마지막으로 revision akkaVersion은 위에 정의되어 있긴 하지만 2.6.12 이다.

     

    그런데, libraryDependence에서 %가 하나 더 붙어 있는 것도 있는 것을 볼 수 있는데, 이런 경우는 configuration 정보까지 포함되는 것으로 akka-actor-testkit-typed와 scalatest 의 경우는  Test configuration 이다.

     

    그리고 %%라고 되어 있는 부분은 artifact id에 postfix로 붙는 스칼라 버젼을 build.sbt 파일에서 정의한 scalaVersion 키 값을 이용하는 것을 의미한다. 위의 build.sbt 내용을 다시 쓰면 다음과 동일하다.

    name := "akka-quickstart-scala"
    
    version := "1.0"
    
    scalaVersion := "2.13.1"
    
    lazy val akkaVersion = "2.6.12"
    
    libraryDependencies ++= Seq(
      "com.typesafe.akka" % "akka-actor-typed_2.13" % "2.6.12",
      "ch.qos.logback" % "logback-classic" % "1.2.3",
      "com.typesafe.akka" % "akka-actor-testkit-typed_2.13" % "2.6.12" % Test,
      "org.scalatest" % "scalatest_2.13" % "3.1.0" % Test
    )
    

     

     

    정리

    Scala를 이용해서 REST API 서비스를 만들어 보려고 시작했지만, 모르는 것이 너무 많기는 하다. 물론, 새로운 것을 배우는 것이 쉬운 일은 아닐 것이다. 스칼라를 개발 언어로 접근하는 것도 또한 함수형 프로그래밍이라는 방법론으로 접근하는 것도 낯선것은 시간이 지나서 이걸 익히고 또 써 보는 시간이 필요할 것 같다.

     

    새로운 생각 또 그걸 구현한 것들은 분명한 시간이 흐르고 쌓이면서 성장하고 발전해 왔던건데 이걸 한 순간에 배운다는 것은 정말 기적과도 같은 일일테니까..

     

    지금 까지 간략히 맛보기를 했으니. 이제 좀 더 깊게 들어가 봐야지..

    댓글

Designed by Tistory.