zio/zio2

ZLayer 를 이용한 dependency injection

wefree 2022. 7. 27. 20:05

문제

zio1 에서의 zlayer example 을 zio2 의 zlayer 를 이용해 구현해 보자

 

코드

아래처럼는 공식 가이드를 이용해 작성한 코드이지만.. 개인적으로 생각하는 코드는 바로 아래에 추가했다.

import zio._

import java.util.concurrent.TimeUnit

object ZLayerTest extends zio.ZIOAppDefault {
  trait Logging {
    def log(line: String): UIO[Unit]
  }

  object Logging {
    def log(line: String): URIO[Logging, Unit] =
      ZIO.serviceWithZIO[Logging](_.log(line))
  }

  case class LoggingLive(ref: Ref[Long]) extends Logging {
    override def log(line: String): UIO[Unit] =
      for {
        current <- Clock.currentDateTime
        count   <- ref.modify(x => (x + 1, x + 1))
        _       <- Console.printLine(current.toString + s"\t[$count]\t" + line).orDie
      } yield ()
  }

  object LoggingLive {
    val layer: ZLayer[Ref[Long], Nothing, LoggingLive] = ZLayer.fromFunction(LoggingLive.apply _)

    //    val layer: ZLayer[Ref[Long], Nothing, LoggingLive] =
    //      ZLayer {
    //        for {
    //          ref <- ZIO.service[Ref[Long]]
    //        } yield LoggingLive(ref)
    //      }
  }

  // passthrough 를 사용하면 R 부분을 그대로 유지하면서 A 쪽으로 정보를 그대로 넘길 수 있다.
//  val layerPassthrough: ZLayer[Ref[Long], Nothing, Ref[Long] with LoggingLive] = LoggingLive.layer.passthrough

  // launch = creates a ZIO that uses the services and never finishes
//  val layerLaunch: ZIO[Ref[Long], Nothing, Nothing] = LoggingLive.layer.launch

  // fresch
//  val layerFresh = LoggingLive.layer.fresh

  val program: ZIO[Logging, Nothing, Unit] = Logging.log("Hi") *> Logging.log("Hello")

  val refLayer: ZLayer[Any, Nothing, Ref[Long]] = ZLayer(Ref.make(0L))

  
//  val loggingZIO: ZIO[Any, Nothing, Logging] = LoggingLive.layer.provideSomeLayer(refLayer)
//  아래처럼 ZLayer.make() 를 이용해 따로 만들 수도 있다.
//  val loggingLiveLayer: ZLayer[Any, Nothing, Logging] = ZLayer.make[Logging](LoggingLive.layer, refLayer)
//  def run = program.provide(loggingLiveLayer)

  // 개발할 때 ZLayer.Debug.mermaid 또는 ZLayer.Debug.tree 를 추가하면 tree 형태로 dependency 를 보여줌
  def run = program.provide(LoggingLive.layer, refLayer, ZLayer.Debug.mermaid)

  /*
     Already provides services: Clock, Random, System, Console
   */
//  val getTime = Clock.currentTime(TimeUnit.SECONDS)
//  val randomValue = Random.nextInt
//  val sysVariable = System.env("HADOOP_HOME")
}

 

공식 가이드는 위와 같이 case class 를 활용을 권장하지만, zlayer 로 변환 그리고 ZIO 의 R 과 결합되면서 매우 복잡해진다. 개인적으로는 아래처럼 코드를 작성하는 것이 더 심플하다고 생각한다. program 전반에 걸쳐 dependency 는 ZIO 의 R 로 관리하고, 마지막 결합에서 ZIO 를 Zlayer 로 변환해 provide 한다.

 

import zio._

object ZioApp extends zio.ZIOAppDefault {
  trait Logging {
    def log(line: String): UIO[Unit]
  }

  object Logging {
    def log(line: String): URIO[Logging, Unit] = ZIO.serviceWithZIO(_.log(line))
  }

  val someLoggingZIO: ZIO[Ref[Long], Nothing, Logging] = for {
    ref <- ZIO.service[Ref[Long]]
  } yield new Logging {
    override def log(line: String): UIO[Unit] =
      for {
        current <- Clock.currentDateTime
        count <- ref.modify(x => (x + 1, x + 1))
        _ <- Console
          .printLine(current.toString + s"\t[$count]\t" + line)
          .orDie
      } yield ()
  }

//  val someLoggingZIO: ZIO[Ref[Long], Nothing, Logging] =
//    ZIO.serviceWith[Ref[Long]] { ref =>
//      new Logging {
//        override def log(line: String): UIO[Unit] =
//          for {
//            current <- Clock.currentDateTime
//            count <- ref.modify(x => (x + 1, x + 1))
//            _ <- Console
//              .printLine(current.toString + s"\t[$count]\t" + line)
//              .orDie
//          } yield ()
//      }
//    }

  val program: ZIO[Logging, Nothing, Unit] =
    Logging.log("Hi") *> Logging.log("Hello")

  override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] = {
    val refLive: UIO[Ref[Long]] = Ref.make(0L)
    val loggingLive: ZIO[Ref[Long], Nothing, Logging] = someLoggingZIO

    val refLayer = ZLayer(refLive)
    val loggingLayer = ZLayer(loggingLive)

    program.provide(loggingLayer, refLayer, ZLayer.Debug.mermaid)
  }
}

 

Scope 활용

Scope 를 이용한 resource 관리가 필요할 경우 ZLayer.scoped 를 사용한다.

import zio._

import java.io.IOException
import scala.io._

object ZLayerTest extends zio.ZIOAppDefault {
  trait Show {
    def print(line: String): Task[Unit]
  }

  object Show {
    def print(line: String): ZIO[Show, Throwable, Unit] =
      ZIO.serviceWithZIO(_.print(line))
  }

  case class ShowLive(source: Source) extends Show {
    override def print(line: String): Task[Unit] =
      Console.printLine(source.getLines().toList.headOption.getOrElse("") + "\n" + line)
  }

  object ShowLive {
    val layer: ZLayer[Source, Nothing, ShowLive] = ZLayer.fromFunction(ShowLive.apply _)
  }

  val source: ZIO[Scope, IOException, Source] = {
    def acquire(name: => String): ZIO[Any, IOException, Source] = 
      ZIO.attemptBlockingIO(Source.fromFile(name))
    def release(source: => Source): ZIO[Any, Nothing, Unit] = 
      ZIO.succeedBlocking(source.close())
      
    ZIO.acquireRelease(acquire("app.conf"))(release(_))
  }

  val program: ZIO[Show, Throwable, Unit] = Show.print("Show me the money")

  // val sourceZIO: ZIO[Any, IOException, Source] = ZIO.scoped(source)
  val sourceLayer: ZLayer[Any, IOException, Source] = ZLayer.scoped { source }

  def run = program.provide(ShowLive.layer, sourceLayer)
}