스칼라의 동시성 프로그래밍 - 비동기 방식을 지원하는 Monad의 대표적인 함수 Future와 flatMap, for comprehension의 활용

View: 266 2 0
작성자: 달빛제이크
카테고리: Scala Language
발행: 2024-07-28 수정 2024-10-28

Monad의 대표적인 함수인 Future와 flatMap, for comprehension 사용 방법에 대해서 알아보겠습니다.

Future는 스칼라에서 제공하는 동시성 프로그래밍을 위한 객체로 비동기 방식을 통해 동시성 처리 방식을 제공하고 있습니다.
동시성 프로그래밍과 동기화(Synchronous) 방식, 비동기(Asynchronous) 방식에 대해서는 글 하나에 모두 담을 수 없는 주제이기 때문에 추후에 다른 글에서 심도 있게 다루기로 하고 이 글에서는 프로그래밍에서 자주 사용하는 Callbacks 위주의 사용 방법으로 그 주제를 제한해서 실제로 프로그래밍에서 사용할 수 있는 내용으로 이야기 하겠습니다.

이 글의 내용은 Scala Document의 Futures and Promises의 내용을 바탕으로 작성되었습니다.

1. Future

스칼라 라이브러리에서 제공하고 있는 Future는 비동기 프로그래밍을 위해 사용하는 객체입니다. 주로 응답이 늦게 이루어지는 입출력 작업에 많이 사용되며, 데이터베이스, 네트워크 요청, 파일 시스템 접근 등 응답 대기가 발생하는 작업을 수행합니다. 비동기 방식으로 연산을 수행하기 때문에 일반적으로 새로운 Thread를 만들어서 실행이 되며, 문법 상으로 Future는 위치지정자(placeholder)의 역할을 담당합니다.

Scala Document에는 Future를 다음과 같이 정의합니다.
A Future is an object holding a value which may become available at some point. This value is usually the result of some other computation.
Future는 어느 시점에서 이용 할 수 있는 값을 가지고 있는 객체이다. Future가 가지고 있는 값은 일반적으로 다른 연산들에 대한 결과이다.

Future는 scala.concurrent에 trait로 존재하고, Companion Object의 apply method를 통해 Instance를 생성합니다.

/**
 * Future.scala
 */
package scala.concurrent

trait Future[+T] extends Awaitable[T]

object Future:
  final def apply[T](body: => T)(implicit executor: ExecutionContext): Future[T]

scala.concurrent.ExecutionContext는 비동기 방식으로 실행되는 Future에 Thread pool을 제공합니다. 비동기 방식으로 연산이 이루어지는 다양한 메커니즘이 존재하지만 스칼라의 Future는 비동기 연산을 처리하기 위해 기본적으로 쓰레드 풀을 사용합니다. 따라서 위의 예제와 같이 Future Instance를 생성하기 위해서는 By-name parameter 형태로 제공해 주는 작업 내용과 함께 ExecutionContext도 제공해 주어야 합니다. ExecutionContext 트레이트를 확장하면 다양한 방식으로 커스터마이징이 가능하지만, 대부분의 경우에 메커니즘 변경없이 그대로 사용하며, import 문을 사용해 전역 컨텍스트로 지정하거나, given(Scala 2에서는 implicit) 키워드를 사용해서 명시적으로 정의하는 것으로 ExecutionContext를 암묵적으로 사용할 수 있습니다. 메소드나 클래스에서 Future를 활용하기 위해서는 using 키워드를 사용해서 매개변수로 선언해 줍니다.

import scala.concurrent.{Future, ExecutionContext}

/**
 * 전역 컨텍스트
 * 전역 변수로 활용되기 때문에 ExecutionContext 매개변수를 추가할 필요가 없습니다.
 */
import ExecutionContext.Implicits.global

val f: Future[String] = Future {
  "Hello, future!"
}

def someMethod(): Future[String] =
  Future {
    "Hello, Future!"
  }

/**
 * given 키워드를 사용한 명시적 정의
 * 클래스나 메소드를 선언할 때 using 키워드를 사용해서 ExecutionContext를 매개변수로 지정합니다.
 * 클래스와 메소드를 사용하는 유저가 암묵적으로 ExecutionContext를 사용할 수 있습니다.
 */
given ec: ExecutionContext = ExecutionContext.global

val f: Future[String] = Future {
  "Hello, future!"
}

def someMethod()(using ec: ExecutionContext): Future[String] =
  Future {
    "Hello, future!"
  }

import 문이나 given 키워드를 통해 ExecutionContext를 사용하는 경우 모두 Future.apply에 ExecutionContext를 암묵적으로 제공합니다. 클래스와 메소드를 선언할 때 import 문을 사용할 경우 전역 변수로 지정 되어있기 때문에 ExecutionContext를 매개변수로 지정하지 않아도 클로저로서 정상 동작하며, given 키워드를 사용한다면 method에 명시적으로 매개변수를 지정해야 합니다. 어떤 방식을 사용하든 클래스와 메소드의 매개변수에 ExecutionContext를 명시적으로 작성하는 것이 코드를 이해하는 데 더 유리합니다.

2. 콜백 등록하기

먼저 Future를 사용할 때 필요한 용어에 대해서 설명하겠습니다.

  1. 연산이 완료되지 않았을 때 - not completed
  2. 연산이 완료되어 값이나 예외를 반환했을 때 - completed
  3. 연산이 완료되고 값을 반환했을 때 - successfully completed
  4. 연산이 완료되고 예외를 반환했을 때 - failed

일반적으로 동기화 방식에서는 특정 연산의 결과를 사용하기 위해서 메인 작업을 중지(Block)시킨 후 해당 연산을 완료한 후 다시 중지 시킨 작업을 재개해서 결과 값을 처리하는데, Scala Future에서 제공하는 비동기 방식은 메인 작업을 중지 시킬 필요 없이 콜백(callback)을 등록(register)한 후 해당 연산이 다른 Thread에서 완료되면 메인 Thread에서 콜백이 호출되어 실행됩니다. 이는 메인 연산을 중지 시킬 필요가 없기 때문에 Performance 측면에서 더 좋은 방식입니다.

Future를 통해 얻은 결과를 활용하기 위해 스칼라 Document에 나와있는 예시를 들어 설명하겠습니다.

import scala.util.{Sucess, Failure}

val f: Future[List[String]] = Future {
  session.getRecentPosts()
}

f.onComplete {
  case Sucess(posts) => for post <- posts do println(post)
  case Failure(t) => println("An error has occured: " + t.getMessage)
}

예제에서 onComplete이라는 새로운 함수가 사용되었습니다. onComplete은 가장 일반적으로 사용되는 콜백 등록 함수입니다. 이 함수는 Try[T] => U 타입의 콜백 함수를 필요로 합니다. Try[T]는 Option[T], Either[T, S]와 유사하게 하위 타입으로 Success[T]와 Failure[T]를 가지고 있습니다. Future에 제공된 연산이 successfully completed 되어 결과 값을 갖게 되면 Success[T] 타입의 값이 적용되고, failed 되어 예외를 반환하면 Failure[T] 타입의 예외를 반환합니다. onComplete를 사용해서 콜백 함수를 등록할 때 이 두 가지 경우를 모두 고려해 주어야 하기 때문에 case 키워드를 사용해서 패턴 매칭을 통해 SuccessFailure를 구분해 주었습니다. 만약 Success, 한 가지 경우에 대해서 결과 값을 처리해야 한다면 foreach 함수를 사용할 수 있습니다.

val f: Future[List[String]] = Future {
  session.getRecentPosts()
}

f.foreach { posts =>
  posts.foreach { post =>
    println(post)
  }
}

동일한 작업을 for comprehension을 활용해 작성할 수 있습니다.

val f: Future[List[String]] = Future {
  session.getRecentPosts()
}

for
  posts <- f
  post <- posts
do println(post)

session.getRecentPosts() 함수에서 예외가 발생했을 때 Future f는 failed 되고 foreach와 for comprehension 모두 내부 코드는 실행되지 않습니다. 이는 두 경우 모두 Future가 성공적인 결과가 있을 때만 동작하기 때문입니다. 따라서 예외가 발생하면 Future는 실패 상태가 되고 특별한 예외 처리 코드를 추가하지 않으면 프로그램은 예외 처리 없이 종료됩니다.
 

onCompleteforeach 메소드는 Unit 타입을 반환합니다. 이는 메소드의 반환 값이 없으며, 단지 부수 효과(side effect)를 가지는 코드 블록을 실행하는 것을 의미합니다. 또한 Unit 타입을 반환하기 때문에 f.onComplete(...).onComplete(...)와 같이 체인 방식으로 함수를 사용 할 수 없습니다. 마지막으로, onComplete와 foreach를 여러 번 사용해서 콜백 함수를 등록했을 때 등록된 콜백 순서에 따라 실행되지 않으며 각각의 콜백은 독립적으로 실행되고 순서를 보장하지 않습니다.

다음의 예제를 보겠습니다.

@volatile var totalA = 0

val text = Future {
  "na" * 16 + "BATMAN!!!"
}

text.foreach { txt =>
  totalA += txt.count(_ == 'a')
}

text.foreach { txt =>
  totalA += txt.count(_ == 'A')
}

foreach를 통해 서로 다른 연산을 하는 콜백 두 개를 등록하고 totalA라는 동일한 자원에 접근하도록 했습니다. 콜백의 순서가 보장되지 않기 때문에 두 콜백 함수는 순차적으로 또는 역순으로 또는 동시에 병렬로 실행될 수 있습니다. 시간에 대해 매우 높은 Resolution을 갖고 있지 않다면 어느 한계 치 이상에서는 순서를 구분할 수 없고 동시에 실행이 이루어져 초기 값이 0인 totalA 값은 16도 될 수 있고 2도 될 수 있습니다.

콜백의 특징을 다음과 같이 정리합니다.

  1. onComplete 콜백을 Future에 등록하는 것은 Future가 완료된 후에 해당 클로저가 결국 실행될 것임을 보장한다.

  2. foreach 콜백을 등록하는 것은 onComplete와 동일한 의미를 가지지만, Future가 성공적으로 완료(successfully completed)되어야 호출되는 차이가 있다.

  3. 이미 완료된 Future에 콜백을 등록하는 것은 1번 항목에서 암시하는 것처럼 콜백이 결국 실행된다는 것을 의미한다.

  4. 여러 콜백들이 Future에 등록된 경우, 그들이 실행되는 순서는 정의되지 않는다. 사실, 콜백은 서로 동시에(concurrently) 실행될 수 있다. 그러나, 단일 스레드로 동작하는 등의 특정한 ExecutionContext 구현을 통해 콜백의 실행 순서를 명확하게 정의할 수 있다.

  5. 일부 콜백들이 예외를 던진 상황에서도, 다른 콜백들은 상관없이 실행된다.

  6. 일부 콜백들이 무한루프를 포함하고 있는 등의 이유로 결코 완료되지 않는 상황에서, 다른 콜백들이 전혀 실행되지 않을 수 있다. 이러한 경우에 잠재적으로 블로킹되는 콜백은 반드시 블로킹 구조를 사용해야 한다.

  7. 콜백이 한번 실행되면, Future 객체에서 제거되어 GC(가비지 컬렉션)의 대상이 된다.

3. Future의 연산을 연결하는 flatMap

다음 예제를 먼저 보겠습니다.

val rateQuote = Future {
  connection.getCurrentValue(USD)
}

rateQuote.foreach { quote =>
  val purchase = Future {
    if isProfitable(quote) then connection.buy(amount, quote)
    else throw Exception("not profitable")
  }

  purchase.foreach { amount =>
    println(s"Purchased $amount USD")
}

현재 환율을 가져와서 rateQuote라는 Future 객체를 생성하고 successfully completed 되면 'foreach' 콜백에서 수익성 여부를 판단하고 구매 결정을 내리는 purchase Future 객체를 생성합니다. purchase가 successfully completed 되면 다시 foreach 콜백을 통해 알림 메세지를 출력합니다.

이 코드에는 다음과 같은 문제가 숨어 있습니다.

  1. USD 구매가 완료된 후 이어서 다른 통화를 거래하고 싶다면 purchase.foreach 콜백 내에서 동일한 코드를 반복해서 작성해야 하며, 이는 코드를 지나치게 들여쓰기 하게 되고, 부피가 커지고 이해하기 어렵게 만듭니다.

  2. purchase 객체는 rateQuote.foreach 콜백 내에서만 접근이 가능하기 때문에 애플리케이션의 다른 부분에서 purchase 객체가 보이지 않아 외부에서 다른 통화를 거래하고자 한다면 동일한 코드를 새로 작성해 주어야 합니다.

Future에서 제공하는 map combinator를 통해 개선된 코드는 아래와 같습니다.

val rateQuote = Future {
  connection.getCurrentValue(USD)
}

val purchase = rateQuote.map { quote =>
  if isProfitable(quote) then connection.buy(amount, quote)
  else throw Exception("not profitable")
}

purchase.foreach { amount =>
  println(s"Purchased $amount USD")
}

rateQuote Future 객체의 map을 사용해서 foreach 콜백 하나를 제거했고, foreach 내부 함수를 사용하지 않아 중첩을 피했습니다. 이제 새로운 통화를 거래하고 싶다면, purchase 객체의 map을 사용해서 rateQuote의 작업에 이어서 또 다른 비동기 작업을 등록할 수 있으며, rateQuote의 결과에 기반해서 후속 작업을 수행할 수도 있습니다.

코드를 더 자세히 들여다 보면 여기에서도 몇 가지 중요한 문제를 발견할 수 있습니다.

  1. rateQuote.map에 등록된 비동기 작업이 예외를 발생 시키면 purchase.map을 통해 추가로 비동기 작업을 등록 시킬 때에도 예외를 발생 시킵니다.
    rateQuote.map의 if 문에서 isProfitable(quote)가 false이거나 connection이 비정상적일 때 Failure 타입의 예외가 반환됩니다.

  2. connection이 깨지면 getCurrentValue가 예외를 발생 시키면서 rateQuote 객체가 fail 됩니다. fail 된 rateQuote 객체는 어떤 값도 갖지 않기 때문에 이후 진행되는 purchase 객체도 fail이 되고 rateQuote와 동일한 예외가 발생합니다.

  3. rateQuote.map 내부 구문에서 if 문 외에 추가 작업이 필요한 경우 응답 지연이 발생할 수 있는 connection.buy 또한 Future 객체로 만들어 주어야 합니다.

  4. Collection의 map 메소드 내부에서 동일한 Collection을 생성할 때 타입이 중복되는 것 처럼 내부 코드 블록에서 다시 동일한 타입인 Future를 사용해야 하는 상황이 발생하면 Future 타입의 중복 문제가 발생합니다. 이렇게 되면 후속 작업에서 Future의 결과 값을 처리할 때 코드가 다소 복잡해 질 수 있습니다.

1, 2번 항목을 정리하면 첫 번째로 실행되는 Future가 successfully competed 되면 그 값이 존재하기 때문에 후속 비동기 작업에 문제가 없지만, 예외가 발생하게 되면서 후속 Future에 동일한 예외가 전달됩니다. 이를 개선하기 위해 rateQuote객체의 recover 함수를 사용해서 예외 처리를 추가해 주어야 합니다. 또한 3번과 4번 항목에서, 첫 번째로 실행되는 Future의 map 함수 내부에서 다시 Future를 통해 비동기 연산을 등록하게 되면 Functor의 중복 연산 문제가 발생할 수 있습니다. 이는 Monad의 flatMap을 통해 해결할 수 있습니다.

다음은 개선된 코드를 통해 작성된 예제입니다.

val rateQuote = Future {
  connection.getCurrentValue(USD)
}

val rateQuoteWithRecovery = rateQuote.recover {
  case ex: Exception =>
    println(s"Failed to get rate quote: ${ex.getMessage}")
    0.0    // 예외 발생 시 기본값으로 0.0을 반환

val purchase = rateQuoteWithRecovery.flatMap { quote =>
  if isProfitable(quote) then
    Future { connection.buy(amount, quote) }
  else Future.failed(new Exception("not profitable"))
}

purchase.onComplete {
  case Success(amount) =>
    println(s"Purchased $amount USD")
  case Failure(ex) =>
    println(s"Failed to purchase: ${ex.getMessage}")
}

recover 함수를 사용해서 예외 처리를 추가해 주었고, flatMap을 통해 중복 연산 문제를 해결했습니다. 또한 purchase 객체의 결과에 대해서도 foreach 대신 onComplete을 사용해서 콜백 함수를 등록해 주었습니다. flatMap은 Future의 값을 다른 Future에 맵핑 시키기 때문에 flatMap 내부 블록의 연산을 모두 Future 타입으로 만들어 주었습니다. recover 외에도 recoverWith, fallbackTo, andThen 등 Future 객체에서 지원하는 다양한 Combinator들이 존재합니다. 각 Combinator들을 적절히 활용하여 다양한 작업을 처리할 수 있습니다.
Future 객체의 비동기 작업에 대한 결과 값을 처리하는 것은 도메인과 Context에 따라 여러가지 방법으로 작성될 수 있으며, 예외 처리와 재사용성을 고려해서 간결하게 작성하려는 노력이 필요합니다.

4. 병렬 처리를 위한 for comprehension

지금까지 Future 객체의 비동기 작업 및 콜백 함수에 대해서 알아보았습니다. onComplete와 foreach를 사용해서 콜백 함수를 등록할 수 있고 map 또는 flatMap을 사용해서 연속적인 비동기 작업을 수행할 수 있습니다. 또한 for comprehension을 사용해서 여러 단계의 비동기 작업을 더 간결하게 표현할 수 있습니다. 다음 예제를 살펴 보겠습니다.

val future1 = Future { 10 }
val future2 = Future { 20 }

// map을 사용해서 작업 등록. Future Type 중복 발생
val resultWithMap = future1.map { a =>
  future2.map { b =>
    a + b
  }
}

// flatMap을 사용해서 작업 등록. flatMap 내부에서는 map을 사용함.
val resultWithFlatMap1 = future1.flatMap { a =>
  future2.map { b => 
    a + b
  }
}

// flatMap을 사용해서 작업 등록. flatMap 내부에는 항상 Future 타입의 반환 값이 필요함.
val resultWithFlatMap2 = future1.flatMap { a =>
  future2.flatMap { b => 
    Future(a + b)
  }
}

// for comprehension 사용
// result = Future(30)
val result =
  for
    a <- future1
    b <- future2
  yield a + b

map과 flatMap을 통해 비동기 작업을 처리하는 코드와 for comprehension을 사용한 코드를 비교해 보았을 때 for comprehension을 사용한 코드가 좀 더 직관적인 것을 알 수 있습니다.

Scala Document에 나와있는 예제를 살펴 보겠습니다.

val usdQuote = Future { connection.getCurrentValue(USD) }
val chfQuote = Future { connection.getCurrentValue(CHF) }

val purchase = for
  usd <- usdQuote
  chf <- chfQuote
  if isProfitable(usd, chf)
yield connection.buy(amount, chf)

purchase.foreach { amount =>
  println(s"Purchased $amount CHF")
}

USD, CHF 두 환율을 가져와서 구매 결정을 한 후 실행하는 예제 입니다. 환율을 가져오는 비동기 작업이 for comprehension을 사용해서 병렬로 처리되고 if 문을 통해 수익이 나는 USD와 CHF 환율을 선택하게 됩니다. 콜백 함수는 foreach에 등록되어 successfully completed 된 경우 실행됩니다. 이 코드를 flatMap과 withFilter를 사용해서 다음과 같이 작성할 수 있습니다.

val purchase = usdQuote.flatMap {
  usd => 
    chfQuote
      .withFilter(chf => isProfitable(usd, chf))
      .map(chf => connection.buy(amount, chf))
}

usdQuote 객체의 flatMap을 통해 그 결과 값을 chfQuote 객체에 맵핑 시켜주었고, chfQuote의 withFilter를 통해서 USD와 CHF의 환율 수익성을 확인한 후 다시 chfQuote의 결과 값으로 구매를 실행하는 코드입니다. usdQuote의 flatMap과 chfQuote의 withFilter, map이 순차적으로 작성되었다고 해서 각각의 비동기 작업이 순차적으로 수행되는 것은 아닙니다. 각각의 작업은 병렬로 수행되며 작업이 완료되는 시점에 따라 후속 작업이 수행됩니다. withFilter로 제공되는 filter 연산은 for generator 안의 if 문과 대응되며, 조건을 만족하는 Future의 결과 값으로 새로운 Future를 만들고, 조건을 만족하지 않으면 NoSuchElementException을 반환합니다. for comprehension과 flatMap 연산을 사용한 예제를 비교했을 때 for comprehension을 사용했을 때 코드가 좀 더 간결하고 직관적인 것을 알 수 있습니다.

5. Future의 예외 처리

Future의 예외 발생 여부에 따라 onComplete 메소드를 통해 성공과 실패를 모두 한 번에 처리하거나, failed 되었을 경우 recover, recoverWith, fallbackTo, andThen 메소드를 사용해서 다양한 처리를 할 수 있습니다. 여기서는 Scala Documents에 나와있는 예제를 바탕으로 failed 되었을 경우의 처리 방법에 대해 알아 보겠습니다.

1) recover

val purchase: Future[Int] = rateQuote.map {
 quote => connection.buy(amount, quote)
}.recover {
 case QuoteChangedException() => 0
}

rateQuate라는 Future가 완료되면, 그 결과를 이용해서 connection.buy(amout, quote)를 호출하여 구매를 처리하는 코드입니다. Future[Int] 타입의 purchase가 rateQuote의 성공 여부에 따라 connection.buy(amout, quote)의 결과 값을 갖거나, QuoteChangedException 예외가 발생되면 0 값을 갖게 됩니다. 여기서 recover는 Future가 실패했을 때 예외를 처리하는 메소드로서, 특정 예외가 발생했을 때 기본값을 제공하도록 설계되었습니다. 즉 Future가 실패하지 않으면 recover는 호출되지 않고 map의 결과가 그대로 purchase에 전달되고 예외가 발생하면 패턴 매칭을 통해 QuoteChangedException 예외에 대해 0 값을 반환합니다.

2) recoverWith

val purchase: Future[Int] = rateQuote.map {
  quote => connection.buy(amount, quote)
}.recoverWith {
  case QuoteChangedException() =>
    backupQuote.map { backupQuote =>
      connection.buy(amount, backupQuote)
    }
}

purchase.onComplete {
  case Success(result) => println(s"Purchase succeeded with result: $result")
  case Failure(ex) => println(s"Purchase failed: ${ex.getMessage}")
}

recover가 단순히 실패한 경우에 대체값을 반환하는 반면, recoverWith는 실패했을 경우 다른 비동기적인 Future로 대체할 수 있는 능력을 제공합니다. 예제 코드에서 rateQuote가 실패했을 때 단순히 결과 값 만을 반환하지 않고 백업 환율을 사용해서 다시 구매를 시도합니다. 최종적으로 purchase는 새로운 Future 값을 갖게 되고 onComplete 메소드를 통해 Success와 Failure에 대한 처리를 진행합니다. recover와 recoverWith를 문법적으로 쉽게 구분하자면 Future[Int] 타입에 대한 recover는 Int 타입의 값을 반환하고 recoverWith는 Future[Int] 타입의 값을 반환합니다.

3) fallbackTo

val usdQuote = Future {
  connection.getCurrentValue(USD)
}.map {
  usd => "Value: " + usd + "$"
}
val chfQuote = Future {
  connection.getCurrentValue(CHF)
}.map {
  chf => "Value: " + chf + "CHF"
}

val anyQuote = usdQuote.fallbackTo(chfQuote)

anyQuote.foreach { println(_) }

이 코드에서는 두 개의 비동기 작업 usdQuote와 chfQuote를 정의하고, usdQuote가 성공하면 그 결과를 반환하고 실패하면 fallbackTo 메소드를 통해 chfQuote의 결과를 반환합니다. 여기서 fallbackTo는 첫 번째 Future가 실패했을 때 두 번째 Future를 시도하는 방식으로 동작합니다.

4) andThen

val allPosts = mutable.Set[String]()

Future {
  session.getRecentPosts()
}.andThen {
  case Success(posts) => allPosts ++= posts
}.andThen {
  case _ =>
    clearAll()
    for post <- allPosts do render(post)
}

이 코드는 비동기적으로 getRecentPosts()를 통해 최근 게시물을 가져와 처리하는 흐름을 구현한 것입니다. 여기서 andThen은 Future가 완료된 후 실행할 코드를 지정하는 메소드입니다. 첫 번째 andThen에서 Future가 성공했을 경우 allPosts라는 집합에 최근 게시물을 저장하고, 두 번째 andThen에서 그 외의 경우, 즉 실패했을 때 clearAll()을 통해 화면을 초기화하거나 이전에 렌더링된 게시물 목록을 비우고 지금까지 allPosts에 저장한 모든 게시물을 화면에 출력합니다.

For comprehension을 사용해서 Future를 다루는 경우에도 동일하게 위의 메소드들을 사용해서 상황에 맞게 적절히 예외 처리를 진행할 수 있습니다.
Future의 Projection(Future.successful 또는 Future.failed)은 Scala 2.x에서 사용되었지만, Scala 2.13 이후로는 비권장(deprecated)되었습니다.

 
 

스칼라 표준 라이브러리에서 제공하는 Future에 대해서 알아보았습니다. Future는 비동기 방식으로 동시성 프로그래밍을 지원하는 스칼라의 대표적인 모나드 객체 중 하나입니다. 비동기 방식을 채택하였기 때문에 응답 지연이 발생할 수 있는 모든 입출력 작업에 사용할 수 있으며, 다중 사용자 시스템, 병렬 연산 처리 등 여러 작업을 동시에 처리해야 하는 다양한 상황에서 효율적인 자원 활용과 성능 최적화를 위해 사용됩니다. Future는 스칼라의 기본 구성 요소로 외부 라이브러리의 추가 없이 사용이 가능하지만, 오류 처리, 취소 가능성, 제어 가능한 실행 컨텍스트 등에서 제한적이고, 특히 오류가 발생했을 때 예외를 던지는 방식으로만 처리하는 한계가 있습니다. Future의 한계를 넘어서 더욱 강력하고 유연한 기능을 제공하기 위한 노력이 진행 중이며, 특히 Cats와 Zio는 순수 함수형 프로그래밍을 지향하는 스칼라 라이브러리로 부작용을 관리하고 보다 강력하고 유연한 비동기 처리 및 동시성 제어 기능을 제공하기 때문에 스칼라 프로그래머들에게 매우 각광 받는 라이브러리로 입지를 다지고 있습니다.

스칼라의 함수형 프로그래밍에 대한 글은 Future를 마지막으로 일단락 하고, 앞으로는 스칼라의 타입 시스템에 대해 이야기하려고 합니다. 감사합니다.

comments 0