콘텐츠로 이동

2023 03 20

2023-03-20

Thread-Safe하게 진짜 랜덤한 13자리 숫자를 만드는 방법?

  • Random vs SecureRandom

Seq vs List

  • JSON 리스트 -> Seq[]가 유리하다
    1. 다형성: Seq[] 타입은 List[], Vector[], 그 외에도 잘 어울림. 유연한 코드 작성 가능
    2. 퍼포먼스: 많은 컬렉션을 다룰때, Seq[]의 성능이 List[]보다 좋음. Seq[]는 lazy evaluation을 지원하기에, 메모리/시간 측면에서 유리
    3. FP: Seq[] 타입이 List[] 보다 불변, 합성적인 측면에서 유리한가봄
  • Seq[] 장점
    • 더 유연함
    • 큰 컬렉션에서 더 나은 퍼포먼스
    • 불변/합성에 더 유리
  • Seq[] 단점
    • List[] 보다 덜 구체적임. 몇 개 메서드는 없을껄?
    • 타입 어노테이션이 필요할 수도

Play는 어떻게 비동기로 동작하지?

  • Netty는 싱글쓰레드로 동작하나?
    • Netty는 하나 이상의 보스 쓰레드를 두고 있음. 보스 쓰레드는 새로운 커넥션을 수립하는 책임을 가짐
    • Netty는 하나 이상의 워커 쓰레드를 두고 있음. 워커 쓰레드는 수립된 커넥션이 요구하는 I/O 이벤트 등을 수행함
    • 이러한 구조로 Netty는 많은 트래픽 동시성 처리를 가능하게 함
  • Play는?
    • Netty 위에 지어진 Play Framework의 경우 비슷한 쓰레드 모델을 채택중임
    • Play Framework는 고정된 쓰레드 풀을 사용해 들어오는 요청을 처리함
    • 쓰레드풀의 사이즈는 conf에서 변경할 수 있음
    • 따라서 Play는, 들어오는 요청을 다루는 thread를 thread pool에서 관리하여 여러개를 처리함
  • 쓰레드 몇 개로 관리함?
    • 기본적으로 몇 개를 쓰레드풀에 쟁여둘지는 ExecutionContext.global에 명시되어 있음
    • 기본적으로는 CPU cores 만큼 쓰레드를 만들 것임
      • quad-core라면 4개의 쓰레드를 생성할 것임
      • 물론 application.conf에서 조정할 수 있음
    • 어플리케이션 및 하드웨어의 속성에 따라 몇개의 쓰레드를 쓸지가 결정됨
      • 만약 어플리케이션이 CPU-Bound라면... (계산하는데에 많은 시간을 씀)
        • 쓰레드 갯수 증가가 멀티코어 환경에서 퍼포먼스 증대에 역할
      • 만약 어플리케이션이 IO-Bound라면...
        • 이러면 쓰레드 더 많이 생성한다고 퍼포먼스가 좋아질 이유는 없음
  • 작업 메인 쓰레드한테 끝났다고 어떻게 알려줘?
    • 플레이는 Akka를 기반으로 비동기 처리를 함
    • 해당 Task가 끝나면, main thread에게 일 끝났다고 알려줘
    • 커뮤니케이션은 Actor System을 기반으로 동작함 (Akka의 근본)
    • Akka actor는 가볍고, 동시성 처리에 유리함
      • 다른 쓰레드에서 Task가 끝나면, Actor에게 메시지를 보냄
      • Actor가 메시지 프로세스하고, Future의 결과값을 봐
      • Main Thread는 Future가 역할을 끝내줬길 기다리고 있었고, 결과값이 있다는 걸 확인하면, 실행을 마저 뚝딱
    • 정리
      • Action.async를 사용하면 매핑된 쓰레드가 처리하고 Actor 시스템에 메시지를 응답한다
      • 메시지는 Future의 Result를 담고있다
      • Main Thread는 Future에 Result가 있다는 사실로 Job이 끝났다는 것을 알게되며 이후 필요한 일을 처리한다
  • Play's Main Thread != Netty's Boss Thread
    • Play의 Main thread는 클라이언트의 요청을 핸들링 해주는 쓰레드임
    • Flow
      1. 클라이언트의 request 도착
      2. Netty의 worker thread pool 중 하나의 쓰레드가 해당 요청을 처리하라고 매핑됨
      3. 만약 컨트롤러가 Action.async를 달고 있다면, 해당 쓰레드가 Netty로 반환됨
      4. 비동기 오퍼레이션이 완료되면, Future에 Result를 담은 메시지가 Akka Actor로 돌아감
      5. Netty worker thread 중 다른 친구들을 핸들링 하고 있던 쓰레드가 그제서야 알림을 받음
  • 한 번 더 정리해보는 Play 흐름
    1. 클라이언트가 TCP Connection 수립하려 한다
    2. Netty의 Boss Thread가 Connection을 수립해 Worker Thread Pool 중 하나의 Worker Thread에 넘긴다
    3. Netty의 Worker Thread는 소켓으로 부터 넘어온 데이터를 읽으며, Play Framework의 Netty Server에게 프로세싱을 맡긴다
    4. Play Framework가 Request를 받으면 (이게 플레이 메인 쓰레드일 듯), Controller로 해당 요청을 매핑시킨다
    5. Action.async가 달려있는 거라면, 쓰레드풀에서 하나 쓰레드 할당해 비동기적으로 처리하도록 한다
    6. 비동기로 처리가 완료되고 나면, Akka로 결과값이 넘어가게 된다
    7. 플레이 메인쓰레드에게 Akka가 끝났다고 알려준다
    8. 플레이 메인쓰레드가 Netty의 Worker Thread Pool 중에 하나를 가져와 클라이언트에게 소켓으로 응답함
  • Play Framework의 Netty Server란건 또 뭐야?
    • Play Framework의 Netty Server는 Netty networking 라이브러리를 활용하여 구축되었음
    • Play의 상황에 알맞게 커스텀했음
    • Netty는 low-level networking library로써, async/event-driven 방식의 프레임워크임
    • Play Framework의 Netty Server는 Netty 위에서 higher-level abstraction으로 HTTP request/response를 다루기 위해 사용됨
  • 하나의 쓰레드는 여러개의 소켓 커넥션을 들고 있을 수 있다

    • Netty 서버가 소켓 커넥션을 다룸
    • Client가 요청 보냄 -> Netty Worker Thread가 요청을 받음 -> Play에게 Request 넘겨줌 ->
      Play 할일 하고 Netty Worker Thread에게 응답 줌 -> Client에게 응답 줌
    • 이 과정에서 Netty Worker Thread는 socket connection을 여전히 들고 있으면서 플레이의 응답을 기다림
      • 그러면서 non-blocking/async니까 딴일도 딴일 나름대로 처리 중
      • Netty Worker Thread가 요청 받으면, 해당 요청 처리를 Akka와 같은 non-blocking 라이브러리에 넘길 수 있음
    • 여러개의 소켓을 하나의 쓰레드에서 관리할 수 있다!

Future 다루는 방법

  • map
    • Future 클래스의 메서드로, 새로운 Future를 리턴함
    • 사이드 이펙트 없이 현재 Future를 새로운 Future로 변환
    • 다른 map, flatMap 등으로 체이닝이 가능
    • 다만, Future가 성공적으로 수행되었는지, 실패했는지는 알 길이 없음
  • onSuccess
    • Future가 성공적으로 완수되면, 콜백 함수가 Future의 결과값을 가지고 지지고 볶는다
    • 하지만 사이드 이펙트 있을 수 있음
    • Future가 성공했을떄, 무슨 콜백으로 어떤 오퍼레이션 뚝딱 할 것인지 제공
    • 사이드 이펙트 발생가능 : shared state, logging etc
    • 새로운 Future를 반환하진 않음

Action

  • Action 만들기
    • BaseController를 extend한 컨트롤러의 경우, Action이 기본 탑재되어 있음
    • 여러가지 Action 빌더들을 통해 Action을 생성할 수 있음
  • 예시 1. Request 참조 없이
    • 가장 간단한 방법 중에 하나
    • 하지만 incoming request에 대한 참조를 얻을 수 없다
      Action {
          OK("Hello World")
      }
      
  • 예시 2. Request => Result

    • Request => Result를 아규먼트로 받아 반환하는 친구가 있음
      Action { request => 
          Ok("Got Request [ " + request + "]")
      }
      
    • 여기서 request를 implicit으로 받으면, 다른 API에서 받아서 처리할 수 있음
      Action { implicit request =>
          Ok("Got Request [ " + request + "]")
      }
      
      def action = Action { implicit request =>
          anotherMethod("Some para value")
          Ok("Got Request [ " + request + "]")
      }
      
      def anotherMethod(p: String)(implicit request: Request[_]) = {
          // do sth that needs access
      }
      
  • 예시 3. additional BodyParser
    • 다른 BodyParser 기능을 통해 Action 객체를 만들수 있나봄
    • 현재로썬 그냥 Any content body parser가 있나보다~
      Action(parse.json) { implicit request =>
          Ok("Got request [ " + request + "]")
      }
      
  • Simple Results
    def index = Action {
        Result(
            header = ResponseHeader(200, Map.empty),
            body = HttpEntity.Strict(ByteString("Hello world!"), Some("text/plain"))
        )
    }
    
    // 위에꺼랑 아래꺼랑 똑같아
    def index = Action {
        Ok("Hello World!")
    }
    
    val ok           = Ok("Hello world!")
    val notFound     = NotFound
    val pageNotFound = NotFound(<h1>Page not found</h1>)
    val badRequest   = BadRequest(views.html.form(formWithErrors))
    val oops         = InternalServerError("Oops")
    val anyStatus    = Status(488)("Strange response type")
    
  • Redirect도 그저 간단한 Result 일 뿐
    def index = Action {
      Redirect("/user/home")
    }
    
    def index = Action {
        Redirect("/user/home", MOVED_PERMANENTLY)
    }
    

Action.async

  • 개요
    • 비동기적으로 다른 쓰레드 풀에서 non-blocking 코드를 실행하도록 지원함
      • 따라서 해당 코드가 long-run operation에 들어가더라도, 서버는 다른 incoming request 처리가 가능하다
  • 클라이언트에게 응답이 어떻게 가능하지?
    • Action.asyncFuture[Result]를 반환한다
    • Action.async가 있다면, 플레이가 Http request를 받는 쓰레드가 아닌, 다른 쓰레드에 해당 function 수행을 매핑시켜 실행시킬 거임
      • Future[Result]가 완료된다면, 플레이는 Http request를 통해 클라이언트에게 해당 응답을 줄 수 있도록 할 것임
  • 예시
    def delayResponse: Action[AnyContent] = Action.async {
        Akka.system.scheduler.scheduleOnce(1.second) {
            Future.successful(Ok("Response after 1 second delay!"))
        }
    }
    
  • Action.async vs Action
    • 시간을 많이 잡아먹는 작업이 request를 받는 thread에서 수행된다면 => Action.async가 좋음
    • 다만, Action.async에는 단점이 있어서 필요할 때만 사용하는 것도 좋아보인다.
      • Future를 관리하는 비용
      • 쓰레드풀 관리 비용
    • 벤치마크 확인하고, 코드 프로파일링하고, 병목/최적화 하는 것이 좋아보임