Using Staged Architecture for Effects

Gyuhang ShimGyuhang Shim
3 min read

What is Staged Architecture?

  • 모든 걸 한꺼번에 처리하지 않고, 단계(Stage) 를 나누어 점진적으로 진행하는 아키텍처

  • 각 Stage마다 책임을 분리해서, Effects 를 처리하는 시점과 방식도 명확히 관리

What is Effects?

  • Program 이 외부 세계와 상호 작용할 때 생기는 것들

  • 예를 들면: File I/O, Network 통신, Database Query, Console output

  • 즉, 순수 함수(Pure Function) 와는 다르게, 외부 상태를 바꾸거나 영향을 받는 작업을 통틀어 Effects라고 부름

What does “Using Staged Architecture for Effects” mean?

  • 코드를 작성할 때,

    • Effects (Network, File I/O)를 막 아무 데서나 호출하게 되면, 복잡성 폭발, 테스트 어려움, 유지보수가 엄청 어려워짐

    • 그래서 Effects 를 다루는 흐름 자체를 "단계(Stage)" 별로 구조화해서

      • 어디까지는 순수 코드로 계산만 수행

      • 그 다음 단계에서만 실제 Effects 를 모아서 실행하는 구조를 만들어서 작성

  • 쉽게 설명하자면

    • “순수 계산(Pure)과 부수효과(Effect)를 섞지 말고, 단계(Stage) 를 나눠서 깔끔하게 관리하자.” 라는 프로그래밍 Paradigm

예제 코드

// (1) 순수 계산 단계
fun calculateSomething(input: Int): Int {
    return input * 2 + 1
}

// (2) Effect를 '명령'으로 모아두는 단계
data class WriteToFileCommand(val path: String, val content: String)

// (3) Effect를 실제로 실행하는 단계
fun executeCommand(cmd: WriteToFileCommand) {
    File(cmd.path).writeText(cmd.content)
}

// 사용 예
val result = calculateSomething(10) // 순수 계산
val command = WriteToFileCommand("/tmp/output.txt", "Result is $result") // 이펙트를 준비
executeCommand(command) // 준비한 이펙트를 실행

왜 이런 식으로 작성해야 할까?

  • 테스트가 쉬워진다: 순수 함수는 그냥 input/output 만 검증하면 충분함

  • 버그가 줄어든다: 어디서 Effects 가 발생하는지 명확히 파악이 가능

  • 확장성과 유지 보수가 좋아진다: 나중에 Effects 만 교체하거나 Mocking 하기 쉬워짐

Staged Architecture Examples

1. ZIO (Scala) — “Effect Staging” 의 교과서

  • ZIO 는 Scala 기반 Async/Concurrent Framework 임

  • ZIO[R, E, A] 타입이 Effect 를 "값" 으로 표현하고, 실행은 나중으로(Stage) 미룸

  • 즉, Effect 를 바로 실행하는 게 아니라, 조합해서 "계획(plan)"을 만들고, 마지막에 한 번에 실행하는 구조(Lazy Evaluation)

Example Code

val program: ZIO[Any, Nothing, Unit] = 
  for {
    _ <- Console.printLine("Starting...")
    _ <- Console.printLine("Doing something important...")
    _ <- Console.printLine("Finished!")
  } yield ()

Runtime.default.unsafeRun(program) // 마지막에 "한방"에 실행
  • Console.printLine 같은 것도 Effect 를 직접 실행하는 게 아니라, "해야 할 일" 을 기록

  • 진짜 실행은 unsafeRun 을 호출하는 최종 Stage 에서만 일어남

즉, "순수한 Business Logic"과 "언제/어떻게 Effect 를 실행할지" 를 명확히 분리한 구조(전형적인 staged architecture)

2. React (Frontend) — "Virtual DOM Staging"

  • React 도 사실 "staging" 을 핵심에 두고 있음

  • Component 가 바로 DOM 을 조작하지 않고, Virtual DOM이라는 Fake Tree 를 준비(Stage)

  • 그런 다음, 최적화된 방법으로 실제 DOM 에 적용(Patch)

요약 흐름

1. render() -> Virtual DOM 생성 (준비 Stage)
2. Diff 계산 (변경사항 Stage)
3. DOM 실제 업데이트 (Effect 실행 Stage)
  • render 함수가 직접 document.createElement 를 호출하지 않는 이유가 바로 staging 을 적용하기 때문임

3. Functional Programming (FP) 기반의 API 설계

  • HTTP 서버, Database 접근에서도 staging 개념이 존재

  • 예를 들어, http4k (Kotlin), Tapir (Scala) 같은 FP 스타일 API 라이브러리들은

    • Request 를 받으면 바로 실행하지 않음

    • 필요한 처리 단계를 조합해서 실행 준비만 수행

    • 그걸 실행(Serve)하는 건 나중에 서버 Boot 시점에 함

Tapir 예제

val endpoint = endpoint.get.in("hello").out(stringBody)

val logic = endpoint.serverLogicSuccess[Unit](_ => Future.successful("Hello, world!"))

HttpServer.start(logic)
  • endpointlogicEffect 실행 전까지 준비만 해 놓은 상태(Stage)

  • 진짜 네트워크 요청을 처리하는 건 HttpServer.start() 시점!

4. Database Migration 툴 (Flyway, Liquibase)

  • 이들도 사실 "Migration Plan" 을 Stage로 쌓고

  • 실제 DB에 적용하는 건 나중에 수행

  • 즉, DDL문(ALTER TABLE 등)을 바로 실행하지 않고, 적용 계획을 만든 다음에, 한 번에 실행

Summary

  • Staged Architecture 는 “해야 할 일 을 먼저 안전하게 정의” 하고, “나중에 통제된 방식으로 실행” 하는 것"
0
Subscribe to my newsletter

Read articles from Gyuhang Shim directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Gyuhang Shim
Gyuhang Shim

Gyuhang Shim Passionate about building robust data platforms, I specialize in large-scale data processing technologies such as Hadoop, Spark, Trino, and Kafka. With a deep interest in the JVM ecosystem, I also have a strong affinity for programming in Scala and Rust. Constantly exploring the intersections of high-performance computing and big data, I aim to innovate and push the boundaries of what's possible in the data engineering world.