본문 바로가기

Programmer Jinyo/Scala & FP

Scala Future에 대해 기본은 배워보자! 튜토리얼 ~~


투명한 기부를 하고싶다면 이 링크로 와보세요! 🥰 (클릭!)

바이낸스(₿) 수수료 평생 20% 할인받는 링크로 가입하기! 🔥 (클릭!)

본 글은

http://allaboutscala.com/tutorials/chapter-9-beginner-tutorial-using-scala-futures/

의 글을 번역(그렇지만 전부 하지는 않고 필요한 부분만)한 것임을 먼저 알립니다!

 

 

 

 

 

Introduction.

Official Scala Future에 대한 문서는 여기서 보시면 됩니다.

Scala Future에 대해 짧은 코드 스니펫과 함께 Scala Future을 통한 asynchronous non-blocking operations에 익숙해질 수 있게 도와주겠다. 충성충성^^7. Scala에 대한 기본적인 이해가 있는 독자를 대상으로 하며, 일단은 Future은 일종의 code wrapper이라고 간략하게 알면 된다.

 

 

 

Method with future as return type

1. Define a method which returns a Future

빠꾸없이 donutStock()이라는 메소드를 정의 할 것이다. 이 메소드는 우리가 도넛의 타입을 알려주면 해당 도넛의 개수를 리턴하기 위하여 Int type을 리턴할 것이다. 이때, 우리는 단순히 Int를 리턴하는 것이 아니라 Future의 Int인 Future[Int] 리턴하는 것임을 알고있도록 하자.

println("Step 1: Define a method which returns a Future")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Int] = Future {
  // assume some long running database operation
  println("checking donut stock")
  10
}

Note:

- 우리는 Future type을 import하기 위해서 import scala.concurrent.Future를 해야 함.

- import scala.concurrent.ExecutionContext.Implicits.global은 Future가 비동기적으로 실행될 scope에 thread pool 을 배치하게 한다. ExecutionContext에 익숙하지 않다면 괜찮다. 앞으로 추가적인 예시가 나올 것이다.

 

2. Call method which returns a Future

다음으로, 우리는 donutStock()메소드를 call 하여 vanilla donut을 input 파라미터로 받을 것이다. donutStock()메소드는 비동기적으로 실행되며 이 예제에서는 우리는 donutStock()메소드의 결과를 받아들기 위해서 Await.result()를 통해 우리의 main program을 block할 것이다.

println("\nStep 2: Call method which returns a Future")
import scala.concurrent.Await
import scala.concurrent.duration._
val vanillaDonutStock = Await.result(donutStock("vanilla donut"), 5 seconds)
println(s"Stock of vanilla donut = $vanillaDonutStock")

만약에 IntelliJ에서 Scala로 돌려보면 아래와 같은 결과를 얻을 것이다.

Step 2: Call method which returns a Future
checking donut stock
Stock of vanilla donut = 10

Note: 

- 일반적으로는 blocking을 피하자!

- 이 튜토리얼을 진행하면서, blocking을 future을 통해서 피하는 방법에 대해 보여 줄 것이다.

 

 

 

Non blocking future result

1.  Define a method which returns a Future

위에서, 우리는 Future return type을 추가함으로써 asynchronous method를 만드는 방법에 대해 알아보았다. 이 메소드를 다시 사용해 보자.

println("Step 1: Define a method which returns a Future")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Int] = Future {
  // assume some long running database operation
  println("checking donut stock")
  10
}

2. Non blocking future result

blocking 함수인 Await.result()대신, 우리는 Future.onComplete() 콜백 함수를 통해 Future의 result를 받아오기로 한다.

println("\nStep 2: Non blocking future result")
import scala.util.{Failure, Success}
donutStock("vanilla donut").onComplete {
  case Success(stock) => println(s"Stock for vanilla donut = $stock")
  case Failure(e) => println(s"Failed to find vanilla donut stock, exception = $e")
}
Thread.sleep(3000)

이때 3초를 쉬는 것은 결과가 끝나기도 전에 main이 끝나면 안되기 때문이다. 아래와 같은 결과가 나올 것이다.

Step 2: Non blocking future result
checking donut stock
Stock for vanilla donut = 10

Note:

- Future.onComplete()를 통해서 우리는 더이상 Future결과를 위해 blocking하지 않는 대신 Success나 Failure에 대한 callback을 받게 된다.

- 그러기 위해 우리는 import scala.util.{Failure, Success} 해야 한다.

- 실제 코드에서는 Thread.sleep()와 같은 행동은 하지 않아도 된다. 단순히 future에서 return된 결과에 react하기만 하면 된다.

 

 

Chain futures using flatMap

이 파트에서는 flatMap() 메소드를 사용해서 Future을 chain하는 방법에 대해서 다뤄 볼 것이다.

1. Define a method which returns a Future

이전 예제와 비슷하게, 우리는 Future[Int]를 리턴하는 donutStock()를 정의 해볼 것이다.

println("Step 1: Define a method which returns a Future")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Int] = Future {
  // assume some long running database operation
  println("checking donut stock")
  10
}

2. Define another method which returns a Future

다음으로, buyDonuts()를 정의한다. 이것은 future를 리턴하지만 Boolean type이다. 우리는 도넛 재고 수량이 buyDonuts() 메소드에 파라미터로 넘어 갈 것이라고 가정한다.

println("\nStep 2: Define another method which returns a Future")
def buyDonuts(quantity: Int): Future[Boolean] = Future {
  println(s"buying $quantity donuts")
  true
}

3. Chaining Futures using flatMap

연속적인 여러 futures를 순서대로 실행하려면, flatMap()메소드를 사용할 수 있다. 우리의 예제에서는, 두개의 future operations (donutStock()과 buyDonuts())를 flatMpa()메소드를 이용하여 chain해 줄 것이다.

println("\nStep 3: Chaining Futures using flatMap")
val buyingDonuts: Future[Boolean] = donutStock("plain donut").flatMap(qty => buyDonuts(qty))
import scala.concurrent.Await
import scala.concurrent.duration._
val isSuccess = Await.result(buyingDonuts, 5 seconds)
println(s"Buying vanilla donut was successful = $isSuccess")

위 소스에서 하는 행위는, donutStock으로부터 나온 결과를 Future을 벗겨내어 Int 형만 남겨 다시 buyDonuts안에 집어넣고 리턴을 받아오는 행위이다. (buyDonuts는 Boolean을 input으로 받지, Future Boolean을 받는 것이 아니다)

이를 통해 아래의 결과를 받아볼 수 있다.

Step 3: Chaining Futures using flatMap
checking donut stock
buying 10 donuts
Buying vanilla donut was successful = true

 

 

Chain futures using for comprehension

이전 예시에서, 우리는 당신이 flatMap() 메소드를 사용해서 여러 future들을 chain할 수 있다는 것을 보여주었다. 스칼라는 for comprehension을 통해 future들을 chain할 수 있도록 flatMap()을 위한 syntactic sugar를 제공한다.

 

1. Define a method which returns a Future

Int type의 Future를 리턴하는 donutStock()메소드를 정의하자.

println("Step 1: Define a method which returns a Future")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Int] = Future {
  // assume some long running database operation
  println("checking donut stock")
  10
}

 

2. Define another method which returns a Future

우리의 이전 예시와 비슷하게, 우리는 buyDouts()라는 boolean type의 Future를 리턴하는 새로운 메소드를 정의 할 것이다.

println("\nStep 2: Define another method which returns a Future")
def buyDonuts(quantity: Int): Future[Boolean] = Future {
  println(s"buying $quantity donuts")
  true
}

 

3. Chaining Futures using for comprehension

donutStock()과 buyDonuts() 메소드를 chain하기 위해서 당신은 아래의 for comprehension syntax를 통해 더욱 쉽게 프로그래밍이 가능하다.

println("\nStep 3: Chaining Futures using for comprehension")
for {
  stock     <- donutStock("vanilla donut")
  isSuccess <- buyDonuts(stock)
} yield println(s"Buying vanilla donut was successful = $isSuccess")

Thread.sleep(3000)

직관적으로 이해할 수 있듯, 위 예제의 flatMap()을 활용한 예제와 똑같은 수행을 한다.

Step 3: Chaining Futures using for comprehension
checking donut stock
buying 10 donuts
Buying vanilla donut was successful = true

위와 같은 출력을 뱉는다.

 

Future option with for comprehension

어떤 때에는 당신의 future들이 어떤 type의 Option 타입을 리턴할지도 모르겠다, 이때, 우리가 Scala의 함수적인 측면에서 볼 때 monad transformers를 사용하여 훨씬 우아하게 다루는 법을 보여주도록 하겠다. 

더보기

monad????? 뭐???? 싶다면 아래 글을 살짝 참고하자.

2019/12/13 - [Programmer Jinyo/Scala & AKKA] - Scala의 모나드(Monad)에 대한 정리

이제부터, 우리는 계속 for comprehension을 사용 할 것이다.

 

1. Define a method which returns a Future Option

아래의 donutStock() 메소드는 Int type의 Future Option을 리턴 할 것이다. 다시말해, 그냥 Int를 리턴하는 것이 아니라 Future[Option[Int]]를 리턴 할 것이라는 얘기다.

 

println("Step 1: Define a method which returns a Future Option")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Option[Int]] = Future {
  // assume some long running database operation
  println("checking donut stock")
  if(donut == "vanilla donut") Some(10) else None
}

도넛이 바닐라 도넛이면 Some(10) 아니면 None을 리턴한다. (Option의 경우 Scala에서 사용하는 타입인데, 값이 존재하거나 존재하지 않을 수 있음을 다루는 타입으로 그 결과가 Some 이거나 None인 자료형이다.)

 

2. Define another method which returns a Future

이번에는,  이전에도 사용했던 buyDonuts() method를 재사용하며 Boolean type의 Future을 리턴 할 것이다. 

println("\nStep 2: Define another method which returns a Future")
def buyDonuts(quantity: Int): Future[Boolean] = Future {
  println(s"buying $quantity donuts")
  if(quantity > 0) true else false
}

3. Chaining Future Option using for comprehension

donutStock() 메소드가 Int type의 Future Option을 리턴하므로, someStock은 Int인 Option type 일 것이다. someStock()을 buyDonuts()에 인풋 파라미터로 넘겨주기 위해서  우리는 Option의 getOrElse()메소드를 사용 할 것이다.

println("\nStep 3: Chaining Future Option using for comprehension")
for {
  someStock  <- donutStock("vanilla donut")
  isSuccess  <- buyDonuts(someStock.getOrElse(0))
} yield println(s"Buying vanilla donut was successful = $isSuccess")

getOrElse의 경우 Option에 있는 메소드로, 만약 실제 값이 있다면 그 값을 사용하고, 없다면 인자로 넘긴 값을 (위 코드에서는 0) 사용하는 메소드이다.

아래와 같은 결과를 확인할 수 있다.

Step 3: Chaining Future Option using for comprehension
checking donut stock
buying 10 donuts
Buying vanilla donut was successful = true

 

Future option with map

이 예시에서는, 우리는 map function을 사용하여 Wrapped된 Option의 Future 안에 도달하는(access) 방법에 대해 보여 줄 것이다. 그러나 단순히 하나의 Future만을 가지고 프로그래밍 할 때는 map이 유용할지라도 여러 Future chain에 대해서는 그렇지 않다는 것을 명심하자.

 

1. Define a method which returns a Future Option

자 다시 Int type의 Future Option을 리턴하는 donutStock()메소드를 만들자.

 

println("Step 1: Define a method which returns a Future Option")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Option[Int]] = Future {
  // assume some long running database operation
  println("checking donut stock")
  if(donut == "vanilla donut") Some(10) else None
}

 

2. Access value returned by future using map() method

donutStock() 의 future method 에서 map() method를 부름으로써, (이 경우에는 Int type의 Option인) future에서 리턴된 값에 도달할 수 있다.

println(s"\nStep 2: Access value returned by future using map() method")
donutStock("vanilla donut")
  .map(someQty => println(s"Buying ${someQty.getOrElse(0)} vanilla donuts"))

위를 보면 map 메소드 안에 someQty라는 인자를 받아서 Option을 바로 받아왔다. 그러면 아래와 같은 결과를 얻을 수 있다.

 

Step 2: Access value returned by future using map() method
checking donut stock
Buying 10 vanilla donuts

 

Composing futures

우리는 future들을 flatMap을 이용하여 chain하는 방법이나 for comprehension을 통하여 future들을 chain하는 방법에 대해 알아보았다. 이 파트에서는 future들을 작성하는 추가적인 아이디어들에 대해 알아 볼 것이다.

 

1. Define a method which returns a Future Option

우리의 donutStock()을 정의하는것으로 부터 시작하자.

println("Step 1: Define a method which returns a Future Option")
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def donutStock(donut: String): Future[Option[Int]] = Future {
  // assume some long running database operation
  println("checking donut stock")
  if(donut == "vanilla donut") Some(10) else None
}

 

2. Define another method which returns a Future

다음으로 Future의 Boolean을 리턴하는 buyDonut()메소드를 정의한다.

println("\nStep 2: Define another method which returns a Future")
def buyDonuts(quantity: Int): Future[Boolean] = Future {
  println(s"buying $quantity donuts")
  if(quantity > 0) true else false
}

 

3. Calling map() method over multiple futures

당신은 donutStock() future메소드와 buyDonuts() future를 map() function을 사용하여 chain할 수 있을 것이다.

 

그렇지만 map()은 리턴 타입의 nesting ( 내포화.. 라고 번역된다 )을 만들어낸다. 아래의 예시에서 resultFromMap()은 Boolean type의 Future의 Future이다. 당신이 resultFromMap() type을 가지고 작업을 하고 싶으면 nesting(내포화)된 future을 unwrap하는 과정에서 막힐 수 밖에 앖다. 이 아래의 튜토리얼에서는 monadic transformers를 통해서 nesting을 다루는 방법에 대해서 알아 볼 것이다.

 

println(s"\nStep 3: Calling map() method over multiple futures")
val resultFromMap: Future[Future[Boolean]] = donutStock("vanilla donut")
  .map(someQty => buyDonuts(someQty.getOrElse(0)))
Thread.sleep(1000)

위 코드를 실행하면 아래와 같은 결과가 나온다.

Step 3: Calling map() method over multiple futures
checking donut stock
buying 10 donuts

 

4. Calling flatMap() method over multiple future

아래의 예시는 이전 section에서 보여준 flatMap()을 통한 연속적인 future을 다루는 것에 대한 예시이다. map()말고 flatMap()을 사용하면 어떤 차이가 생기는지 보는 것이 중요하다. flatMap()을 통하면 no nesting이며 resultFromFlatMap이 Boolean의 Future type이 된다.

 

println(s"\nStep 4: Calling flatMap() method over multiple futures")
val resultFromFlatMap: Future[Boolean] = donutStock("vanilla donut")
  .flatMap(someQty => buyDonuts(someQty.getOrElse(0)))
Thread.sleep(1000)

아래와 같은 결과가 나온다.

 

Step 4: Calling flatMap() method over multiple futures
checking donut stock
buying 10 donuts

 

 

Future sequence

이 section에서는 여러 future operation들을 실행하는 방법과 그 결과들을 Future.sequence() 함수를 통해 기다리는 방법에 대해서 다뤄본다. Scala API 공식 문서에서 언급하고 있듯, 여러 future들을 하나의 future로 줄이고 싶을 때 sequence 함수가 유용하다. 더해서, 이 future들은 non-blocking이며 병렬적으로 돌아갈 것이다. 다른말로 하면, 이것은 순서대로 종료되지 않을 수 있다는 말을 내포하고 있다.

 

1. Define a method which returns a Future Option of Int

우리의 앞선 예제와 비슷하게, Int type의 Future Option을 리턴하는 donutStock()메소드를 정의 할 것이다. 우리가 method 안에 Thread.sleep()을 적어 놓아서 연산이 오래 걸리는걸 시뮬레이팅 하는 중이라는 것을 알아두자.

println("Step 1: Define a method which returns a Future Option of Int")
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
def donutStock(donut: String): Future[Option[Int]] = Future {
  println("checking donut stock ... sleep for 2 seconds")
  Thread.sleep(2000)
  if(donut == "vanilla donut") Some(10) else None
}

 

2. Define another method which returns a Future[Boolean]

Step 2에서, Boolean의 Future을 리턴하는 buyDonuts() 메소드를 만든다. Step1과 비슷하게, Thread.sleep()을 넣었다.

println("\nStep 2: Define another method which returns a Future[Boolean]")
def buyDonuts(quantity: Int): Future[Boolean] = Future {
  println(s"buying $quantity donuts ... sleep for 3 seconds")
  Thread.sleep(3000)
  if(quantity > 0) true else false
}

 

3. Define another method for processing payments and returns a Future[Unit]

조금 더 복잡하게 만들어 보기 위해서, 우리는 processPayment()라는 Unit type의 Future을 리턴하는 메소드를 만들어 볼 것이다. (FYI, Unit type은 void와 같은 역할을 한다.) 다시, method block안에 Thread.sleep()을 사용 해 줄 것이다.

 

println("\nStep 3: Define another method for processing payments and returns a Future[Unit]")
def processPayment(): Future[Unit] = Future {
  println("processPayment ... sleep for 1 second")
  Thread.sleep(1000)
}

 

4. Combine future operations into a List

step 4에서 우리는 step 1 2 3의 futures를 변경 불가능한(Immutable) List로 만들어 볼 것이다. List안의 future은 type Any를 가진다.

println("\nStep 4: Combine future operations into a List")
val futureOperations: List[Future[Any]] = List(donutStock("vanilla donut"), buyDonuts(10), processPayment())

 

5. Call Future.sequence to run the future operations in parallel

마지막으로, 우리는 Future.sequence를 call 하고 future들의 List를 넘김으로써 그들이 병렬적으로 실행되게 할 것이다. 

연산들은 서로 사이사이에 실행됨을 기억하자.

println(s"\nStep 5: Call Future.sequence to run the future operations in parallel")
val futureSequenceResults = Future.sequence(futureOperations)
futureSequenceResults.onComplete {
  case Success(results) => println(s"Results $results")
  case Failure(e)       => println(s"Error processing future operations, error = ${e.getMessage}")
}

 

아래 결과가 나올 것이다.

Step 4: Combine future operations into a List
checking donut stock ... sleep for 2 seconds
processPayment ... sleep for 1 second
buying 10 donuts ... sleep for 3 seconds

왜 onComplete의 결과는 안나올까? (원 글에는 이 이유가 안나온다;;)

그 이유는 main이 끝나버린 후에 Future들이 끝나기 때문이다.

onComplete의 결과도 같이 보고 싶다면

 

futureSequenceResults.onComplete {
    case Success(results) => println(s"Results $results")
    case Failure(e)       => println(s"Error processing future operations, error = ${e.getMessage}")
  }
  Thread.sleep(7000)

 

위와 같이 sleep을 적당히 길게 해 준다면 아래와 같은 결과가 추가로 뜬다.

Results List(Some(10), true, ())

이 결과를 보면 success의 result들이 Some(10) , true , () 이 각각 리스트에 담겨서 리턴 된 것을 볼 수 있다. 이는 끝난 시간과 상관 없이 우리가 List에 넣은 순서임을 주목하자.

 

 

 

 

 

나는 개인적으로 이정도 공부 했으면 기본은 했다고 생각해서 이쯤까지만 번역하고자 한다. (ㅎㅎ..ㅋㅋ!!) 순서대로 번역 하였으니 남은 내용을 따로 보기 편할 것이다.

이것 외에도 많은 추가적인 활용 방법이 원본 링크에 있으니 더 자료가 필요하면 개인적으로 공부하시길 바라며 :)