• Scala Error Handling의 기본 컨셉은 선언적 처리(Declarative Error Handling)으로 기존 try-catch의 명시적인 방식(Imperative Error Handling)과는 방식이 다르다.
  • 선언적 에러 처리의 장점은 아래와 같다.
    • 기존 try-catch 방식은 경우에 따라 error의 발생 원인과 try-catch로 인한 로직의 변경 추적이 어렵다. 반면 선언적 처리는 참조 무결성(referentially transparent)이 보장된다.
    • Type-safety : 함수의 정의시 return type 뿐만 아니라 어떤 type의 error로 실패하는지 알 수 있게 되어 compile time에 type-safety에 대한 점검이 가능하다.
    • Exhaustivity Checking(에러 체크의 완전성) : 컴파일 타임에 반드시 처리해야 하는 error handling 여부에 대한 검사가 가능하다.
    • Error Model : ZIO는 Exit, Cause와 같은 에러의 정보를 담고있는 모델을 자체적으로 제공한다. 에러의 유실을 막을 수 있다.
 try {
    try throw new Error("e1")
    finally throw new Error("e2")
 } catch {
   case e: Error => println(e)
 }

// Output:
// e2

---

ZIO.fail("e1")
  .ensuring(ZIO.succeed(throw new Exception("e2")))
  .catchAll {
    case "e1" => Console.printLine("e1")
    case "e2" => Console.printLine("e2")
  }

// Output:
// e1

 

  • ZIO 전용의 Error를 저장 관리하기 위한 객체인 Cause에 대한 이해가 필수적이다.
    • https://zio.dev/reference/core/cause
    • It allows us to take a base type E that represents the error type and then capture the sequential and parallel composition of errors in a fully lossless fashion.
    • (번역) Cause는 에러(Exception)의 class type을 나타내는 E를 획득할 수 있게 하고, 에러의 누락 방직 차원에서 순차적인 혹은 병렬적인 에러의 포착을 제공한다.
    • Cause의 내부 구조는 다음과 같다
      • Empty : Cause의 초기상태로 ZIO.succeed(5)와 같이 error가 없는 상태를 의미한다.
      • Fail : expected error의 type E를 의미한다.
      • Die : defect(unexpected error)의 type E를 의미한다.
      • Iterrupt : fiber 등의 멀티 쓰레드 환경에서 interruption을 의미한다.
      • Stackless : stack trace 와 excution trace 정보를 담고 있다. (stack trace의 노출 레벨이 Die와는 다르다)
      • Both : 병렬 프로그램밍 상에서 2개 이상의 error가 발생하였을때의 정보를 담고 있다.
      • Then : 순차적 에러가 발생했을때 Error 객체 저장(고전 try-catch 모델 혹은 fail.ensuring 등)
sealed abstract class Cause[+E] extends Product with Serializable { self =>
  import Cause._
  def trace: Trace = ???

  final def ++[E1 >: E](that: Cause[E1]): Cause[E1] = Then(self, that)
  final def &&[E1 >: E](that: Cause[E1]): Cause[E1] = Both(self, that)
}

object Cause extends Serializable {
  case object Empty extends Cause[Nothing]
  final case class Fail[+E](value: E, override val trace: Trace) extends Cause[E]
  final case class Die(value: Throwable, override val trace: Trace) extends Cause[Nothing]
  final case class Interrupt(fiberId: FiberId, override val trace: Trace) extends Cause[Nothing]
  final case class Stackless[+E](cause: Cause[E], stackless: Boolean) extends Cause[E]
  final case class Then[+E](left: Cause[E], right: Cause[E]) extends Cause[E]
  final case class Both[+E](left: Cause[E], right: Cause[E]) extends Cause[E]
}
  • ZIO상에서 에러의 처리를 위해서는 아래의 관련 함수를 숙지해야 한다.
    • .catchAll
    • .catchSome
    • .either / .absolve
      • [R, E, A] --> [R, Either[E, A]]
      • [R, Nothing, Either[E, A]] --> [R, E, A]
    • .absorb  // defect to Failures (recover from both Die and Interruption)
      • [Any, Nothing, Nothing] --> [Any, Throwable, Nothing]
    • .resurrent // defect to Failures (recover from only from Die) 
      • [Any, Nothing, Nothing] --> [Any, Throwable, Nothing]
    •  .orDie
      • [R, Throwable, A] --> [R, Nothing, A]
    • .refineOrDie // defect to Failures (narraw down the type of the error channel from E)
      • [R, Throwable, A] --> [R, IOException, A]
    • .unrefine // defect to Failures (broadens the type of the error channel from E to the E1 and embeds some defects into it)
      • [R, Nothing, A] --> [R, E, A]
    • .sandbox (.unsandbox) // defect to Failures
      • [R, Nothing, A] --> [R, Cause[E], A]
    • .ensuring : catch block과 유사한 역할
      • [R, E, A] --> [R, ???, A]
    • .some
      • [R, E, Option[A]] --> [R, Option[E], A]
    • .cause (.uncause)
      • [R, E, A] --> [R, Nothing, Cause[E]]
    • .merge
      • [R, E, A] --> [R, super(A|E)]
    • .reject : success chanel의 값의 일부를 fail channel로 처리
      • [R, E, A] --> [R, E, A]

 

 

 

'Tech > ZIO' 카테고리의 다른 글

Scala ZIO의 error handling - (1)  (0) 2024.05.05

본 문서에 관하여

  • ZIO의 error handling 방식을 함수 위주로 정리하여 실제 Scala에서 error handling 전략을 살펴본다

ZIO는 기본적으로 아래와 같이 Input, Error, Output Type을 정의해야 한다.

ZIO[R, E, A]

Java와 다른 점은 실질적인 Return type이 Error와 실제 Value 를 정의한다는 것이다.

 

아래는 Error를 명시적으로 구현한 코드라 볼 수 있다.

val aFailedZIO: IO[String, Nothing] = ZIO.fail("Something went wrong")
val failedWithThroable: IO[RuntimeException, Nothing] = ZIO.fail(new RuntimeException("Bumb!"))
// RuntimeException의 Error Type을 String으로 변환
val failWithDescription: ZIO[Any, String, Nothing] = failedWithThroable.mapError(_.getMessage)

 

그러면 Java의 try-catch 처럼 명시적 혹은 비명시적으로 발생한 Error를 어떻게 처리할 것인가?

// 잘못된 사용 예시
val badZIO: ZIO[Any, Nothing, RuntimeFlags] = ZIO.succeed {
    println("Trying something")
    val string: String = null
    string.length
  }

// attempt 함수를 사용하여 Throwable을 error를 처리할 수 있다.
val anAttempt: ZIO[Any, Throwable, Int] = ZIO.attempt {
    println("Trying something")
    val string: String = null
    string.length
}

// catchAll과 catchSome을 활용한 catch error
val catchError: ZIO[Any, Throwable, Any] = 
	anAttempt.catchAll(a => ZIO.attempt(s"Returning a different value because $a"))

val catchServiceErrors: ZIO[Any, Throwable, Any] = anAttempt.catchSome {
    case e: RuntimeException => ZIO.succeed(s"Ignoring runtime excep[tion: $e")
    case _ => ZIO.succeed("Ignoring everything else")
}

 

 위의 코드와 같이 성공인지 실패인지 알 수 없는 코드를 실행하는 경우 ZIO.attempt 를 활용하고,

catchAll과 catchSome을 활용하여 발생한 exception에 전체에 대해 처리가 가능하다.

 

Scala의 일반적은 Error 처리 기법인 Try, Either, Option 등이 ZIO에도 동일하게 사용된다.

// Option / Try / Either to ZIO
  val aTryToZIO: ZIO[Any, Throwable, Int] = ZIO.fromTry(Try(42 / 0))

  // either -> ZIO
  val anEither: Either[Int, String] = Right("Success!")
  val anEitherToZIO: ZIO[Any, Int, String] = ZIO.fromEither(anEither)

  // ZIO -> ZIO with Either as the value channel
  val eitherZIO: URIO[Any, Either[Throwable, Int]] = anAttempt.either

  // reserve
  val anAttempt_v2 = eitherZIO.absolve

  // option -> ZIO
  val anOption: ZIO[Any, Option[Nothing], Int] = ZIO.fromOption(Some(42))

  // implements
  def try2ZIO[A](aTry: Try[A]): Task[A] = aTry match {
    case Failure(exception) => ZIO.fail(exception)
    case Success(value) => ZIO.succeed(value)
  }

  def either2ZIO[A, B](anEither: Either[A, B]): ZIO[Any, A, B] = anEither match {
    case Left(value) => ZIO.fail(value)
    case Right(value) => ZIO.succeed(value)
  }

  def option2ZIO[A](anOption: Option[A]): ZIO[Any, Option[Nothing], A] = anOption match {
    case Some(value) => ZIO.succeed(value)
    case None => ZIO.fail(None)
  }

  def zio2zioEither[R, A, B](zio: ZIO[R, A, B]): ZIO[R, Nothing, Either[A, B]] = zio.foldZIO(
    error => ZIO.succeed(Left(error)),
    value => ZIO.succeed(Right(value))
  )

  def absolveZIO[R, A, B](zio: ZIO[R, Nothing, Either[A, B]]): ZIO[R, A, B] = zio.flatMap {
    case Left(e) => ZIO.fail(e)
    case Right(v) => ZIO.succeed(v)
  }
 
 

계속..

'Tech > ZIO' 카테고리의 다른 글

ZIO의 Error 처리  (0) 2024.08.30

+ Recent posts