paper-review

(Review) Towards Modern Development of Cloud Applications

date
Apr 11, 2024
slug
towards-modern-development-of-cloud-applications
author
status
Public
tags
Paper Review
summary
Google Service Weaver의 근간이 되는 논문 리뷰
type
Post
thumbnail
스크린샷 2024-04-11 오후 2.10.19.png
category
paper-review
updatedAt
Oct 1, 2025 06:23 AM
 
안녕하세요 Yureutae입니다. 오늘 리뷰할 논문은 Towards Modern Development of Cloud Applications라는 Google에서 HOTOS 2023에 발간한 논문입니다.
해당 논문은 작년 구글이 갑작스럽게 공개한 Google Service Weaver 에 기반이 되는 논문입니다. 기존 MSA를 구축하는데 드는 품을 줄이기 위해 문제점을 분석하고 새로운 모듈형 모놀리식 이라는 개발 패러다임을 제안하며 이에 대응하는 툴을 제공한다가 주 요지인 것 같습니다.
Google Service Weaver 웹사이트
해당 기술과 논문에 대해서 많은 엔지니어의 갑론을박이 있었는데, 어떤 기여와 문제점이 있는지 분석해보겠습니다.
 

Index

  • Background
  • Problem & Challenge of Current MSA
  • Proposed Solutions
  • Contributions
  • Problems of This Paper
  • Conclusion
 

Background

Monolithic Architecture vs Micro Service Architecture

notion image
 
현재 엔지니어간 논의가 가장 많은 주제 중에 하나인 “모놀리스 vs MSA”입니다.
  • 모놀리스 (Monolith) 아키텍처는 하나의 소프트웨어를 구성하는 모든 모듈과 코드를 하나의 프로젝트에서 관리하는 것입니다. 일반적으로 생각하는 웹 애플리케이션 개발 프로젝트를 하나 생각하면 될 것 같습니다.
  • MSA 아키텍쳐는 하나의 소프트웨어를 구성하는 컴포넌트들을 독립적인 프로젝트들로 분리하여 관리하는 것입니다. 위의 웹 애플리케이션을 예시로 생각했을 때, 구분이 되는 각 도메인(유저, 결제, 상품 등등)을 모두 별개의 (백엔드 애플리케이션 + DB) 환경의 조합으로 구분(node js, fastapi, spring 등의 Framework + MySQL, Postgres, Oracle 등의 DB)할 수 있다고 생각하면 될 것 같습니다.
 
모놀리스와 비교했을 때 MSA의 장점을 나열하면 다음과 같습니다.
  • High Scalability ⇒ 각 Micro Service는 독립적이므로 Scale up, out, down 등이 용이해서 resource 사용량을 최적화할 수 있습니다.
  • High Availability ⇒ Micro Service들은 독립적이고 Scalable하며 K8s나 Cloud Service 등으로 쉽게 Auto Scaling, Self Healing이 가능하고, 로드밸런싱 등으로 분산처리가 가능합니다.
  • Continuous Deployment ⇒ 각 Micro Service를 개별적으로 배포가 가능함으로써 전체 아키텍처를 재배포하지 않고 지속적으로 업데이트 가능합니다.
  • Small Codebase ⇒ 각 Micro Service는 전체 아키텍처에 대해서 상대적으로 작은 규모의 코드베이스로 개발, 유지보수 등이 용이합니다.
 

Must MSA?

당연히 MSA가 유리한 부분이 많으며, 많은 기업에서의 도입 사례로 유행을 탄 면이 있습니다.
ㅋㅋㅋ
ㅋㅋㅋ
 
하지만 위에서 MSA의 정의와 관련된 개념적인 측면이 아니라, 실제 MSA 도입을 고려하여 깊게 고려하면 난이도가 매우 높아집니다.
 

EDM (Event Driven Micro Service)

많은 분들이 광고로 인해, MSA라고하면 Kubernetes, Docker를 가지고 컨테이너화된 애플리케이션을 분산 노드 환경에 배포하는 것 정도만 생각하는 것 같습니다.
하지만 대부분 기업사례에서는 엄청난 트래픽을 소화하기 위해서 MSA 아키텍처 내에 EDA(Event Driven Architecture)를 거의 내포하고 있습니다. (a.k.a EDM - Event Driven MicroService)
 
왜 Kafka, Rabbit MQ, AWS MSK, SQS, SNS 등의 Message Queue를 MSA에서 필수로 고려하게 될까요? Micro Service로 애플리케이션이 쪼개졌기 때문입니다. 즉, 기존과 달리 트랜잭션 처리를 분산환경에서 깊게 고려할 필요가 있습니다. (배민에서 음식을 주문했는데 트래픽이 급증했다고 돈만 보내지고 주문이 음식점에 도착하지 않은 상황을 생각하면 좋을 것 같습니다.)
MSA에서 비즈니스 로직(트랜잭션)을 처리하는 방법은 아래 이미지와 같이 2PC, SAGA - Choreography, SAGA - Orchestration이 있습니다.
2PC 기반 트랜잭션 처리. Atomicity를 보장하지만 겁나 비효율적
2PC 기반 트랜잭션 처리. Atomicity를 보장하지만 겁나 비효율적
SAGA - Orchestration. 당연히 Orchestrator에 병목이 발생할 수 있음
SAGA - Orchestration. 당연히 Orchestrator에 병목이 발생할 수 있음
SAGA - Choreography. 분산 조율되기 때문에 병목은 없겠지만 SAGA Flow 추적이 어려울수도..?
SAGA - Choreography. 분산 조율되기 때문에 병목은 없겠지만 SAGA Flow 추적이 어려울수도..?
이런 구조로 Modern Architecture에서는 Kafka 등의 Message Broker를 도입해서 Event Driven으로 비즈니스 로직을 보다 효율적으로 처리하게 됩니다.
이러다보니 MSA 도입 고려 시에는 EDA를 추가로 고려할 수 밖에 없고, 당연히 학습, 개발, 관리 등에서 비용이 추가 발생할 수 밖에 없습니다.
 

DDD (Domain Driven Architecture)

앞서 말했듯이 MSA를 위해 Domain 단위로 구분해야합니다. 많은 예제로 쓰이는 User, Product 등의 도메인 외에도 Use-Case에 따라 도메인이 달라질 수 있으며 도메인 내부 구성요소가 달라질 수 있습니다.
이렇게 도메인을 구분하는 행위는 코드베이스 상에서 도메인 별 모듈화가 용이하기에 모놀리스에서부터 DDD로 개발하는 추세도 생겼습니다.
DDD 때문에 아래와 같은 작업이 추가적으로 수반됩니다. 당근 더 머리가 아프게됩니다. 문제는 이것을 기반으로 인프라에 올리는 작업은 또 구분되므로 MSA가 확 쉬워지는 것은 아닙니다.
진짜로 Miro 같은 사이트에서 위와 같이 스티커 붙여서 도메인 나눈다고 함
진짜로 Miro 같은 사이트에서 위와 같이 스티커 붙여서 도메인 나눈다고 함

효율?

비용적인 문제도 MSA가 절대적으로 유리하지는 않습니다. 요즘 엔지니어 사이에서 무조건적인 MSA 찬양보다는 옛날 방식으로 애플리케이션 설계자체를 잘하면 겁나 좋다라는 얘기가 나오는 것 같습니다.. 즉, 비즈니스 성장부터하고 트래픽에 부하가 오면 그때 작업하라는 거죠. 대표적인 사례가 다음과 같습니다.
반대의 의견도 존재합니다. 서비스 초기에 어느정도 성장세와 MSA migration을 고려하지 않고 만들면 나중에 아키텍처 변경 비용이 더 나온다는 것입니다.
 

플랫폼, 인프라 학습 비용

현재는 MSA 구축을 위해서 전문 인력을 많은 비용으로 채용하거나, 도입을 위해서 엔지니어들이 많은 학습비용을 지출하게 됩니다.
아래 무신사 SRE 엔지니어 분들의 사례(https://bit.ly/platformEngKorea240321_1)처럼 추상화된 인프라 도구를 도입할 경우 개발자들에게 가중되는 부담이 증가하는 것을 MSA의 약점으로 들 수 있겠습니다.
notion image
notion image
 
 
 

Problem & Challenge of Current MSA

해당 논문은 출판된 당시 제안 방법에 대해 상당히 많이 까인 논문이지만, 현재 아키텍처 구조 방식이 잘못됐다는 것에는 모두가 동의하는 것 같습니다.
대차게 깐다
위에서 검토했으니 현재 MSA의 주요 문제점만 간단하게 다시 정리해보겠습니다.
 
  • Performance
데이터 송/수신 시 발생되는 serializing/deserializing이 애플리케이션이 더 쪼개질수록 병목현상이 증가
  • Correctness
시스템 상에 존재하는 수 많은 버전의 Micro Service 간 interaction 추적의 어려움. (8개 시스템의 100건의 catastrophic한 장애가 버전 간 interaction 추적의 어려움으로 발생)
  • Manage
다양한 Micro Service에 대해 build, test, deploy를 위해서 별개의 binary를 운용해야 함
  • Freeze APIs
Micro Service간 의존성이 강하면(tight coupling) API가 “Freezing”(교착 상태)가 될 수 있음. (한 번 수정에 드는 품이 많다는 얘기)
  • Development
여러 Micro Service에 영향을 미칠 수 있다면 atomically하게 구현/배포가 어려움
 
당근 구글 쯤되니 초대형 아키텍쳐에서 MSA 운용의 어려움을 정확하게 뽑아냈다고 볼 수 있을 것 같습니다.
 
그럼 기존 연구들이나 사례의 문제점을 어떻게 제시했는지 보면
  • 완화하는 것을 목표로 Framework와 Process를 개발 중이지만 완전히는 개선x (Terraform과 Jenkins 등을 예시로 드는데 음…뭔가 최신은 아니네요)
  • gRPC, Protocol Buffer 등으로 개선 중이었다하는데 이건 본인들 연구 한번 참조해준 느낌입니다.
 
어쨌든 지금까지의 솔루션이 위의 문제점(Challenge)를 해결 못하는데는 2가지 이유가 있다고 합니다.
  • 개발자가 메뉴얼하게 애플리케이션을 여러 바이너리로 분할한다고 “전제”하기 때문
즉 개발자에 network 형태가 의존하는 형태가 되어, 개발자에 따라 통신 코드/과정이 달라져 “harden”(굳어지는?)되게 됩니다. 이 경우 굳은만큼 수정이 어렵다는 얘기가 됩니다. (Performance, Correctness, Freeze APIs가 속한다는데… 사실 상 Freeze APIs로 나머지가 다 영향을 받는다로 해석할 수 있을 것 같습니다.)
  • 애플리케이션 바이너리가 개별적으로 Production에 Release된다고 “전제”하기 때문
즉 독립적으로 개발은 하되 타 바이너와의 관계를 고려해야하므로, Cross binary Protocol을 변경하기 어렵습니다. 즉, 바이너리 간 검증 문제가 발생하고 다양한 데이터 형식을 사용함에 따라 비효율적일 수 있습니다. (Performance, Correctness, Manage, Freeze APIs, Development 모두 속함)
 
 
그래서 해당 논문이 제안하는 방법은 아래와 같습니다.
  • 애플리케이션을 Modularized Monolithic으로 작성 (logical component로 구분)
  • Dynamically / Automatically하게 logical component를 physical process로 전환/할당할 수 있는 Runtime
  • 애플리케이션을 atomically하게 배포해서 버전간 interaction 발생 방지
해당 논문의 실제 구현 결과물이 Google Service Weaver입니다.
 
 
 
 

 

Proposed Solution

  • 개발자가 비즈니스 로직에만 신경을 쓰도록, 단일 바이너리 Module형 애플리케이션을 작성할 수 있는 추상화가 포함된 Programming Model
  • 위의 Programming Model 맞춤 build, deploy, optimize 모두 포함된 Runtime
이 핵심 파츠라고 합니다.
 

Programming Model

Component

Component는 본 연구에서 제시한 핵심 추상화 단위입니다. 프론트엔드에서 말하는 Component와 뭔가 유사하면서도 MicroService와 연결되어있습니다.
notion image
 
  • 애플리케이션은 독립적인 Component들로 구성
  • Interface로 정의되며, 외부에서는 Interface의 Method로만 Component에 접근가능
  • State를 가지며 Long-lived한 독립적 Computational agent 단위 (대충 Micro Service 백엔드를 잘 돌려말한 것 같습니다.)
  • 개발자는 Component 내부 구현만 신경쓰고, Physical Process에는 신경 안쓰도록
  • Component는 여러 node에 hosting될 수 있음 (중복 존재 가능)
  • 동일한 node간 Component 통신은 Local Procedure Call로 이루어짐
  • Runtime이 Component의 분산 실행, 배치, Scaling 등을 결정 (K8s의 Control Plane 역할?)
  • node가 다를 때는 Runtime에 의해 LPC → RPC (Remote Procedure Call)로 전환
 

API

Google Service Weaver는 Go로 만들어진 Component API를 제공합니다. (언어에 구애받지 않고 MSA에서 Go 백엔드랑 gRPC가 많이 쓰이긴 하지만…좀 더 대중적인 프레임워크를 썼으면 어땠을까)
(Golang의 polymorphism을 먼저 이해하면 좋을 것 같습니다. https://mingrammer.com/gobyexample/interfaces/ )
논문에서의 Pseudo-code 예제. Go struct 및 interface 사용을 모르면 좀 난해한듯?
논문에서의 Pseudo-code 예제. Go struct 및 interface 사용을 모르면 좀 난해한듯?
 
// reverser.go
package main

import (
    "context"

    "github.com/ServiceWeaver/weaver"
)

// Reverser component.
type Reverser interface {
    Reverse(context.Context, string) (string, error)
}

// Implementation of the Reverser component.
type reverser struct{
    weaver.Implements[Reverser]
}

func (r *reverser) Reverse(_ context.Context, s string) (string, error) {
    runes := []rune(s)
    n := len(runes)
    for i := 0; i < n/2; i++ {
        runes[i], runes[n-i-1] = runes[n-i-1], runes[i]
    }
    return string(runes), nil
}

// main.go
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/ServiceWeaver/weaver"
)

func main() {
    if err := weaver.Run(context.Background(), serve); err != nil {
        log.Fatal(err)
    }
}

type app struct{
    weaver.Implements[weaver.Main]
    reverser weaver.Ref[Reverser]
}

func serve(ctx context.Context, app *app) error {
    // Call the Reverse method.
    var r Reverser = app.reverser.Get()
    reversed, err := r.Reverse(ctx, "!dlroW ,olleH")
    if err != nil {
        return err
    }
    fmt.Println(reversed)
    return nil
}
Google Service Weaver의 docs에 나와있는 예제. 얘가 좀 더 이해하기 쉬운 것 같다.
 
위 예시를 활용해서 조금 더 실용적인 비즈니스 로직 메서드를 짜면 아래와 같을 것 같습니다. 이제야 장점이 조금 이해가는 것 같습니다. 단순하네요. 이 호출방식이 나중에 원하는대로 LPC가 될 수도 있고 RPC가 될 수도 있다고 하는 것 같습니다. (Correctness, Manage, FreezeAPIs 정도?)
일단 통신 방식은 local이든 remote든 gRPC 방식으로 작성하도록 통일만하고 나중에 런타임으로 처리하려는 것 같습니다.
func (g *greeterComponent) Greet(ctx context.Context, name string) (string, error) {
    // 데이터베이스에서 사용자 정보 가져오기
    user, err := g.userRepo.GetByName(ctx, name)
    if err != nil {
        return "", err
    }

    // 외부 API 호출하여 인사말 생성
    greeting, err := g.greetingService.Generate(ctx, user.Language)
    if err != nil {
        return "", err
    }

    // 결과 가공 및 반환
    return fmt.Sprintf("%s, %s!", greeting, user.Name), nil
}
 
 

Runtime

Runtime이 배포하는 방식
Runtime이 배포하는 방식
본 연구에서의 Runtime은 Component의 배포와 실행을 담당합니다. Component가 어디에 배치될 지, 얼마나 복제될 지 결정하는 등의 high-level decision + 실제 physical resource 할당 및 self healing을 결정하는 등의 low-level decision을 처리하며 Atomic rollout을 통해 해당 버전 애플리케이션의 컴포넌트의 원자성을 보장합니다. (이거 거진 k8s 아닌가요)
Runtime의 핵심 요소는 다음과 같습니다.
  • Code Generation
Implements[T] 로 작성된 embedding을 검사하고 모든 Component Interface와 implementation set을 계산. 이후 mashal/unmashal 코드 및 RPC 코드 생성 (대충 proto 얘기인 것 같습니다). 비즈니스 로직 코드와 단일 바이너리로 컴파일 됨.
  • Application-Runtime Interaction
범용적인 배포환경을 고려하여 Proclet 이라는 API를 제공. 모든 애플리케이션 바이너리는 proclet을 데몬으로 실행해서 바이너리를 관리.(이건 kube-proxy..?) 뒷 부분을 읽어보니 Runtime이 Proclet의 Control-Plane 역할을 한다고 적혀있네요. 단 Runtime이 SSH든 Cloud든, K8s든 가리지 않는 것이 핵심인 것 같습니다. Global Manager로 Proclet을 조율하고, Proclet에 대한 지시는 Envelope로 내린다고합니다. Envelope로 proclet agent와 소통하며 단순 application lifecycle 뿐만 아니라 할당된 환경 및 Component의 상태, 정보, Metric, Log를 모두 수집합니다.
  • Atomic Rollout
읽어보니 대충 Blue-Green으로, 한번에 전체 아키텍처 복사본 띄어서 테스트하고 전환하는 시간을 최소화하는 것 같습니다.
 
 
 

Contribution

주제가 주제인만큼 실험 관련이 빈약할 것은 예상했는데 상당히 빈약한 논문입니다. 대신 해당 아키텍처의 기여될 부분과 실제 기여를 쭉 늘여놓았는데 빠르게 살펴보겠습니다.
  • Transport Placement, Scaling
대충 런타임으로 Component나 Component간이나 환경에서 발생하는 모든 정보를 파악할 수 있어서 이를 바탕으로 최적화가 가능하고 RDMA와 같은 유선 통신 기술 등도 특정 병목 부분에서 파악하여 도입이 가능하다고 하는데….그냥 너무 뜬 구름 잡는 소리 같습니다.
  • Routing
Slicer라는 연구를 인용하면서 Caching을 Routing에 적용하는 알고리즘을 Service Weaver에 포함했다는데, “Runtime could also learn”이라는 문장을 봤을 때 무슨 ML 알고리즘처럼 얘기했지만 아무런 보충 자료나 결과가 없어서 무슨소리를 하는지 잘 모르겠습니다. 나중에 Service Weaver Docs보면서 핸즈온 해봐야할 것 같습니다.
  • Automated Testing
얘는 얼추 이해가는게, 기존 MicroService가 모두 별개의 binary여서 전체 아키텍처에 대한 end-to-end 테스트가 어려운 편이지만 제안한 방법으로는 Single binary만 생성되므로 테스트가 쉽다는 얘기인 것 같습니다.
 
  • Prototype Implementation
이제 Google Service Weaver를 링크주면서 설명합니다. 앞서 논문에 설명한 모든 내용이 포함됐다고합니다.
GKE Multi-Region 배포를 blue-green Rollout으로 구현하며 HPA를 사용해서 auto scaling을 테스트한 것 같습니다. Baseline Appliication은 gRPC와 K8s를 사용했다고하며 Go 기반으로 포팅된 11개의 Micro Service 기반 애플리케이션을 Component로만 래핑하여 동일하게 사용했습니다. Workload 생성은 Locust를 사용해서 일정 비율의 HTTP 요청을 전송하고 로드에 따라 container replicaSet을 autoscaling하는 Scenario로 구성되었습니다.
매우 빈약하다. 이해는 가는데, 구글급이 실험 하나로 쫑냈다…
매우 빈약하다. 이해는 가는데, 구글급이 실험 하나로 쫑냈다…
Baseline (k8s + gRPC) 대비 Google Service Weaver가 같은 QPS일 때, 78 → 28 (약 2.78배)의 적은 평균 코어를 사용했으며, Median Latency는 5.47 → 2.66 (약 2.05배) 줄었습니다.
컴파일 + atomicity rollout으로 이루어낸 성과로 보이는데, 논문에서 Serialization Format이 불필요한 정보를 사용하지 않았다고 하기 때문입니다. (encoder/decoder가 정확히 동일한 버전에서 실행되고 field set와 encoding/decoding 순서가 사전에 정해졌으므로 field 번호, type 정보 등을 사용하지 않기 때문에)
정확한 비교를 위해 Component를 동일 환경에 배치하지 않았다고 하는데, 11개 모두 같은 공간에 배치하면 core가 9개, latency는 0.38ms까지 줄어든다고 합니다. (근데 이건 당연한 거 아닌가)
 
  • Networking Abstraction
끝에 애매하게 적어둔 느낌인데 장점이라면 장점으로 볼 수 있을 것 같습니다.
 
마지막으로는 대충 Orleans, Akka, K8s, Helm, DOcker, Istio 등등과 비교하는데 특정 서비스와 개발 플랫폼 도구를 비교하는게 좀 애매하네요.
 
 

Problems of This Paper

Disscusion으로 먼저 선수를 쳤습니다. 예상가능한 단점을 쭉 써놨네요.

Discussion

  • Multiple Application Binary
결국 규모가 클 때 어떻게 개발, 관리할 것이냐에 대한 문제입니다. 틀릴 수도 있음을 인정하고 갑니다. 현재 Go뿐만 아니라 여러 언어의 Micro Service로 구성된 Application에 관해 수용점을 모색하고 있답니다.
  • Integration with External Services
무조건적으로 Component를 쓰지 않아도 되지만 쓰면 유용하다고 합니다.
  • Distributed Systems Challenge
편의성을 증가시켰지 분산 시스템의 근본적인 문제를 해결하진 못한다고 합니다. 아키텍처 잘못 만들면 삑날 수 있다는 것 같습니다.
  • Programming Guide
Monolith vs MSA 언급해주면서 정답은 없지만, 본인들은 Monolith → MSA로 점진적 변화가 맞다고 합니다.
 

Problem

  • Business Logic 구현 (Logical)과 Physical Process를 구분했으나 실제 Component(Micro Service)간 Coupling은 없어지지 않음.
  • 사용자 편의성 및 통신 성능 관련 기여 대부분이 gRPC(Google Remote Procedure Call)를 최대한 활용한 것에 의존
  • 테스트 Application을 Go로 통일하고 RPC 개념 자체에 의존하기 때문에, 본인들이 제시하는 “language-agnostic”이 와닿지 않음.
  • 실험이 단순 HPA이기 때문에 “Cloud Application”이라는 거대한 범주를 고려했다고 보기 힘듦
  • Runtime, Proclet은 K8s를 한번 더 구현한 느낌.
 

Conclusion

Modularized Monolith라는 개념을 제안하고 gRPC를 극한으로 활용해서 최대한 개발자에게 편의성을 증가시키면서 통신과정에서 resource 낭비를 줄여 효율을 높인 것이 주 기여인 것 같습니다.
MSA의 단점을 명확히 부각시킨게 인상적인 논문이지만, 이외에는 매우 별로인 논문인 것 같습니다. 딱 앞부분까지만 보라고 추천할 논문인 것 같아요.
이와 별개로 현재 MSA 개발과정을 개선할 더 특별한 패러다임과 도구가 나와야 할 것 같긴합니다. 사실상 모호한 개념을 use-case에 맞춰 최적화하는 것 같아요. 어떤 사례를 봐도 확 공통적인 느낌이 없고 가지각생인 느낌이 듭니다.