Using Staged Architecture for Effects


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)
endpoint
와logic
은 Effect 실행 전까지 준비만 해 놓은 상태(Stage)진짜 네트워크 요청을 처리하는 건
HttpServer.start()
시점!
4. Database Migration 툴 (Flyway, Liquibase)
이들도 사실 "Migration Plan" 을 Stage로 쌓고
실제 DB에 적용하는 건 나중에 수행
즉, DDL문(ALTER TABLE 등)을 바로 실행하지 않고, 적용 계획을 만든 다음에, 한 번에 실행
Summary
- Staged Architecture 는 “해야 할 일 을 먼저 안전하게 정의” 하고, “나중에 통제된 방식으로 실행” 하는 것"
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.