관리 메뉴

HAMA 블로그

[Play2] Action 과 Action.async 의 차이점 본문

PlayFramework2

[Play2] Action 과 Action.async 의 차이점

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



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


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




Comments