반응형

[백엔드 8편] Java Virtual Thread — Thread-per-Request 모델의 한계를 극복하는 새로운 동시성 패러다임

 

높은 동시성을 확보하기 위해 그동안 Java 개발자들은 리소스 소모가 큰 Thread-per-Request 모델과 코드 복잡도가 높은 Reactive Programming 사이에서 고민해 왔습니다.
Java 21에서 도입된 Virtual Thread(Project Loom)는 이러한 고민에 대한 새로운 해답을 제시합니다. 본 아티클에서는 Virtual Thread의 내부 메커니즘과 도입 시 고려해야 할 사항들을 분석합니다.


1. Platform Thread의 한계와 진화

기존 Java의 Thread는 OS 스레드를 래핑한 Platform Thread입니다.

  • 높은 리소스 비용: OS 스레드는 생성 시 약 1MB의 스택 메모리를 할당받으며 컨텍스트 스위칭 비용이 발생합니다.
  • I/O 블로킹 문제: I/O 작업 동안 OS 스레드는 대기 상태가 되어 리소스가 낭비됩니다. 이를 해결하기 위해 등장한 리액티브 스택은 성능은 좋으나 학습 곡선이 높고 디버깅이 어렵다는 단점이 있습니다.

2. Virtual Thread의 내부 구조: Carrier Thread와 Continuation

Virtual Thread는 OS 스레드에 직접 매핑되지 않는 경량 사용자 수준 스레드입니다.

2.1 Carrier Thread와 스케줄링

Virtual Thread는 실제로 ForkJoinPool 기반의 Carrier Thread 위에서 실행됩니다. Virtual Thread가 I/O 작업을 만나 블로킹되면, 런타임은 해당 Virtual Thread의 상태를 힙(Heap) 메모리에 저장하고 Carrier Thread에서 언마운트(Unmount)합니다. Carrier Thread는 즉시 다른 Virtual Thread를 실행할 수 있게 되어 소수의 OS 스레드로 많은 동시 요청을 처리할 수 있습니다.

2.2 Continuation

Virtual Thread의 핵심은 실행 중인 코드 지점과 로컬 변수 상태를 캡처하여 저장했다가 나중에 다시 실행할 수 있게 해주는 저수준 기술입니다.


3. 실무 도입 시의 핵심 이슈: Pinning (피닝)

Virtual Thread 도입 시 가장 주의해야 할 현상이 Pinning입니다.

  • 발생 조건: synchronized 블록 내부에서 I/O 작업을 수행하거나 Native Method를 호출할 때 발생합니다.
  • 현상: Virtual Thread가 Carrier Thread에서 언마운트되지 못하고 OS 스레드를 점유하게 되어 시스템 가용성이 떨어집니다.
  • 해결책: synchronized 대신 ReentrantLock을 사용하여 블로킹 시 언마운트가 원활하게 일어나도록 코드를 수정해야 합니다.

4. 성능 분석: WebFlux vs Virtual Thread

  • 처리량: I/O 밀집형 작업에서 Virtual Thread는 WebFlux와 대등하거나 그 이상의 성능을 보여줍니다.
  • 메모리 효율: 수만 개의 요청이 몰려도 Virtual Thread는 힙 메모리를 유연하게 사용하므로 효율적입니다.
  • 개발 경험: Virtual Thread의 이점은 가독성입니다. 리액티브 연산자 없이 익숙한 코드로 높은 성능을 구현할 수 있습니다.

5. 실무 도입 및 운영 가이드라인

  1. 스레드 풀 지양: Virtual Thread는 생성 비용이 저렴하므로 기존처럼 스레드 풀에 가두는 대신 필요할 때마다 생성하여 사용하는 방식이 권장됩니다.
  2. ThreadLocal 사용 주의: 수백만 개의 스레드가 무거운 ThreadLocal 변수를 가지게 되면 메모리 압박이 올 수 있으므로 Scoped Values 도입을 검토해야 합니다.
  3. 라이브러리 호환성 체크: 사용하는 라이브러리가 내부적으로 synchronized를 사용하는지 확인해야 하며, 최근 라이브러리들은 점진적으로 Virtual Thread를 지원하고 있습니다.

 
Java21, VirtualThread, Loom프로젝트, 동시성모델, 성능최적화, 처리량향상, 백엔드설계

반응형
반응형

[백엔드 7편] 합의 알고리즘과 쿼럼(Quorum) 설계 — 분산 시스템의 신뢰성을 지탱하는 합의의 원리는 무엇인가?

 

분산 시스템에서 물리적으로 떨어진 노드들이 하나의 상태에 동의하는 과정은 아키텍처의 성패를 좌우하는 핵심 과제입니다.
특히 네트워크 단절 상황에서 시스템이 정합성과 가용성 중 무엇을 우선할지 결정하는 메커니즘은 매우 중요합니다. 본 아티클에서는 Split Brain 현상을 방지하는 쿼럼 원리와 현대 분산 시스템의 근간인 Raft 알고리즘을 분석합니다.


1. 네트워크 파티션과 Split Brain

분산 클러스터에서 노드 간 통신이 끊기면 클러스터가 두 개 이상의 독립된 그룹으로 나뉘는 네트워크 파티션이 발생합니다. 이때 나뉜 그룹들이 각각 자신을 리더로 간주하고 데이터 쓰기를 허용하면 데이터 정합성이 깨지는 Split Brain 현상이 일어납니다. 합의 알고리즘은 이러한 극한 상황에서 데이터의 일관성을 유지하기 위한 보루입니다.


2. 쿼럼(Quorum) 메커니즘: 과반수의 법칙

쿼럼은 의사결정을 위해 필요한 최소한의 투표수입니다. 분산 시스템에서 쿼럼은 동시에 두 개의 리더가 존재할 수 없음을 보장하는 역할을 합니다.

2.1 정족수 구성

전체 노드 수가 N일 때, 쿼럼은 보통 N/2 + 1로 정의됩니다. 3대(정족수 2)와 4대(정족수 3) 구성은 모두 단 1대의 장애까지만 허용하므로, 효율성 측면에서 3, 5, 7대의 홀수 구성을 선호합니다.

2.2 읽기/쓰기 쿼럼 (R + W > N)

일관성을 보장하기 위해 쓰기 쿼럼(W)과 읽기 쿼럼(R)의 합이 전체 노드 수(N)보다 커야 합니다. 이는 읽기 요청 시 최소한 하나 이상의 노드는 최신 데이터를 가지고 있음을 보장하는 장치입니다.


3. Raft 알고리즘: 리더 선출과 로그 복제

Raft는 이해 가능성을 목표로 설계되어 현대의 Etcd, Consul, TiDB 등에 널리 채택된 알고리즘입니다.

3.1 리더 선출 (Leader Election)

  • 임기(Term): 시간은 단조 증가하는 숫자로 나뉘며, 각 임기에는 최대 한 명의 리더만 존재합니다.
  • 랜덤 타임아웃: 노드마다 서로 다른 타임아웃을 가져 투표 분산 가능성을 최소화합니다.
  • 데이터 안전성: 자신보다 최신 로그를 가진 노드에게만 투표함으로써 데이터 유실을 방지합니다.

3.2 로그 복제 (Log Replication)

리더는 클라이언트 요청을 로컬 로그에 쓰고 팔로워들에게 전달합니다. 과반수의 팔로워가 성공 응답을 보내면 리더는 해당 로그를 커밋 상태로 확정하고 클라이언트에게 응답합니다. 이후 팔로워들에게도 커밋 소식을 전파합니다.


4. 일관성과 성능의 트레이드오프

합의 알고리즘은 강한 일관성을 제공하지만 성능 비용이 수반됩니다. 쓰기 요청이 쿼럼 노드에 복제될 때까지 대기해야 하므로 네트워크 왕복 시간이 필수적입니다. 또한 팔로워에서 읽기를 수행할 때 리더의 커밋 소식이 도달하지 않았다면 과거 데이터를 읽을 수 있으므로, Read Index나 Lease Read 기법을 통해 이를 보정해야 합니다.


5. 시스템 설계 시 고려사항

  • Etcd: Kubernetes 상태 저장소로 사용되며 고도의 신뢰성이 필요한 메타데이터 관리에 적합합니다.
  • Zookeeper: 분산 코디네이터로 검증되었으며 계층 구조의 데이터 관리에 강점이 있습니다.
  • Redis Sentinel: 리더 선출에는 쿼럼을 쓰지만 데이터 복제는 비동기식입니다. 따라서 가용성은 높으나 데이터 유실 가능성이 있음을 인지하고 설계해야 합니다.

 
합의알고리즘, Raft, Paxos, 쿼럼설계, 분산시스템, SplitBrain, 백엔드설계

반응형
반응형

[백엔드 6편] Saga 패턴의 설계와 운영 — 분산 환경에서 데이터 일관성을 어떻게 보장할 것인가?

 

마이크로서비스 아키텍처(MSA)로의 전환에서 가장 큰 기술적 부채는 분산 트랜잭션 관리입니다.
과거 모놀리식 구조에서는 단일 DB의 ACID 특성을 통해 데이터 정합성을 보장받았으나, 서비스별로 DB가 파편화된 환경에서는 기존의 2-Phase Commit(2PC) 방식이 성능 저하와 가용성 하락의 주범이 됩니다.

본 아티클에서는 분산 시스템에서 데이터 일관성을 보장하기 위한 실질적인 표준인 Saga 패턴의 메커니즘과 구현 전략, 그리고 실무적인 트레이드오프를 분석합니다.


1. 분산 트랜잭션의 딜레마: 2PC의 한계와 가용성 하락

전통적인 XA 리소스를 활용한 2PC 방식은 모든 참여 노드가 준비될 때까지 리소스를 점유합니다. 이는 다음과 같은 문제를 야기합니다.

  • 성능 저하: 네트워크 레이턴시가 누적되어 전체 트랜잭션 시간이 기하급수적으로 증가합니다.
  • 가용성 반비례: 트랜잭션에 참여하는 서비스 중 단 하나라도 장애가 발생하면 전체 시스템이 블로킹됩니다.
  • 데이터베이스 제약: 모든 DB가 XA 프로토콜을 지원해야 하므로 NoSQL이나 이종 DB 혼용이 어렵습니다.

이러한 한계를 극복하기 위해 분산 시스템은 최종 일관성(Eventual Consistency) 모델로 선회하게 되며, 그 중심에 Saga 패턴이 있습니다.


2. Saga 패턴의 두 가지 아키텍처: Choreography vs Orchestration

Saga 패턴은 각 단계별 로컬 트랜잭션을 실행하고, 실패 시 이전에 성공한 작업을 되돌리는 보상 트랜잭션을 실행하는 흐름입니다.

2.1 Choreography (이벤트 기반 탈중앙화)

각 서비스가 로컬 트랜잭션을 마치고 이벤트를 발행하면, 다음 서비스가 이를 구독하여 자신의 작업을 수행하는 방식입니다.

  • 장점: 중앙 제어자가 없어 결합도가 낮고 구성 요소가 단순하여 소규모 워크플로우에 유리합니다.
  • 단점: 워크플로우가 복잡해지면 전체 흐름을 한눈에 파악하기 어렵고, 서비스 간 이벤트를 주고받다 보면 순환 의존성이 발생할 위험이 있습니다. 또한 분산 로그를 추적하지 않으면 어느 단계에서 장애가 발생했는지 식별하기 어렵습니다.

2.2 Orchestration (중앙 집중식 제어)

별도의 오케스트레이터가 각 서비스에 명령을 내리고 상태를 관리하는 방식입니다.

  • 장점: 복잡한 비즈니스 로직과 상태 전이가 한곳에 모여 있어 가시성이 뛰어납니다. 실패 지점과 복구 전략을 오케스트레이터 내부 로직으로 정의할 수 있어 에러 핸들링이 명확합니다.
  • 단점: 오케스트레이터 장애 시 전체 트랜잭션이 중단될 수 있어 고가용성 구성이 필수적입니다. 또한 비즈니스 로직이 오케스트레이터에 과도하게 몰리면 서비스 자체가 빈약해질 수 있습니다.

3. 실무적 완성도: Transactional Outbox와 멱등성 보장

Saga 패턴을 안전하게 구현하기 위해서는 네트워크 장애나 프로세스 중단 상황에서도 데이터 누락이 없어야 합니다.

3.1 Transactional Outbox Pattern

로컬 트랜잭션과 메시지 발행이 원자적으로 일어나지 않는 문제를 해결합니다. DB의 동일한 트랜잭션 내에서 비즈니스 데이터와 메시지를 함께 저장하고, 별도의 릴레이 프로세스가 이 메시지를 메시지 큐로 전달합니다. 이를 통해 적어도 한 번(At-least-once) 전달을 보장합니다.

3.2 멱등성(Idempotency)의 필수성

적어도 한 번 전달은 중복 메시지 발생을 전제로 합니다. 따라서 보상 트랜잭션을 포함한 모든 Saga 참여 서비스는 중복 요청에도 결과가 변하지 않는 멱등성을 갖춰야 합니다. 보통 요청 ID를 통한 처리 여부 체크나 DB의 유니크 제약 조건을 활용합니다.


4. 보상 트랜잭션 설계의 기술

보상 트랜잭션은 이미 커밋된 데이터를 논리적으로 되돌리는 과정이므로 세심한 설계가 필요합니다.

  • 시맨틱 보상: 이미 결제가 완료된 건은 결제 취소 상태로 변경해야지 행 자체를 삭제해서는 안 됩니다.
  • 순서 관리: 보상 트랜잭션은 반드시 실행된 순서의 역순으로 진행되어야 합니다.
  • 실패 대응: 보상 트랜잭션 자체가 실패할 경우를 대비해 재시도 로직이나 관리자 알림 체계를 마련해야 합니다.

5. 아키텍처적 결론: 설계 시 고려사항

참여 서비스가 2~3개 내외이고 비즈니스 흐름이 선형적이라면 Choreography가 유리합니다. 반면 결제나 예약처럼 참여 서비스가 많고 예외 케이스가 복잡한 경우라면 Orchestration을 선택하는 것이 관리 효율 측면에서 좋습니다. 최근에는 Temporal이나 AWS Step Functions 같은 엔진을 활용하여 오케스트레이터 직접 구현 비용을 줄이는 추세입니다.

반응형
반응형

[백엔드 5편] SAGA 패턴 — 분산 트랜잭션 환경에서 보상 트랜잭션을 활용한 최종 정합성 구현

 

모놀리식 아키텍처에서는 하나의 데이터베이스 안에서 @Transactional 어노테이션 하나로 모든 작업의 원자성(Atomicity)을 보장할 수 있었다. 하지만 마이크로서비스 아키텍처(MSA)로 넘어오면 이야기가 달라진다. '주문', '결제', '배송', '재고' 서비스가 각각 독립된 DB를 소유하고 있는 환경에서, 사용자의 주문 한 번은 4개의 서로 다른 네트워크 요청으로 쪼개진다.

 

문제는 결제는 성공했는데 재고 차감에서 에러가 났을 때다. 이미 성공한 결제를 어떻게 취소할 것인가? 분산 DB 환경에서는 고전적인 2PC(2-Phase Commit) 방식이 성능과 가용성 문제로 인해 거의 쓰이지 않는다. 대신 현대적인 시스템은 SAGA 패턴을 통해 '최종 정합성(Eventual Consistency)'을 추구한다. 본 글에서는 SAGA 패턴의 핵심인 보상 트랜잭션의 원리와 두 가지 구현 전략을 상세히 분석한다.

 

 


 

1. SAGA 패턴의 본질: 로컬 트랜잭션의 연쇄

 

SAGA는 긴 비즈니스 프로세스를 여러 개의 작은 로컬 트랜잭션으로 쪼갠다. 각 서비스는 자신의 DB 작업만 완수하고 완료 이벤트를 발행한다.

 

  • 핵심: 모든 단계가 성공하면 전체 비즈니스 프로세스가 완료된다.
  • 에러 대응: 중간 단계에서 실패가 발생하면, 이전에 성공했던 단계들에 대해 거꾸로 보상 트랜잭션(Compensating Transaction)을 실행하여 마치 아무 일도 없었던 것처럼 데이터를 원상 복구시킨다.

 

 


 

2. 구현 전략 1: 코레오그래피 (Choreography) — 자율적 협업

 

중앙 제어자 없이 서비스들이 서로의 이벤트를 구독하여 연쇄적으로 반응하는 방식이다.

 

  • 흐름: 주문 서비스(주문 생성 이벤트 발행) → 결제 서비스(이벤트 수신 후 결제 처리, 결제 성공 이벤트 발행) → 재고 서비스...
  • 장점: 구조가 단순하고 서비스 간 결합도가 낮다. 작은 규모의 시스템에서 빠르게 도입하기 좋다.
  • 단점: 비즈니스 로직이 여러 서비스에 흩어져 있어 전체 흐름을 파악하기 어렵다. 서비스가 많아지면 어떤 이벤트가 어디로 튀는지 추적하는 '이벤트 지옥'에 빠질 수 있다.

 

 


 

3. 구현 전략 2: 오케스트레이션 (Orchestration) — 중앙 통제

 

전체 흐름을 관리하는 별도의 Saga Orchestrator를 두는 방식이다.

 

  • 흐름: 오케스트레이터가 "결제해!"라고 명령하면 결제 서비스가 결과를 보고한다. 성공하면 다음 서비스에 명령을 내리고, 실패하면 이전에 거쳐온 서비스들에게 "취소해!"라고 보상 트랜잭션을 지시한다.
  • 장점: 복잡한 비즈니스 로직을 한곳에서 관리하므로 가독성이 높고 상태 추적이 용이하다.
  • 단점: 오케스트레이터 자체가 병목이 되거나 단일 장애점(SPOF)이 될 수 있어 고가용성 설계가 필수적이다.

 

 


 

4. 실전 설계 주의사항: 멱등성(Idempotency) 확보

 

보상 트랜잭션 환경에서 가장 무서운 것은 '이벤트의 중복 전달'이다. 네트워크 순단으로 인해 동일한 '결제 취소' 요청이 두 번 들어올 수 있다.

 

  • Idempotent Consumer: 모든 서비스는 동일한 요청을 여러 번 받아도 단 한 번만 처리되도록 설계해야 한다.
  • 설계 기법: 데이터베이스에 processed_event_id 테이블을 두어 이미 처리된 이벤트인지 확인하거나, 업데이트 쿼리 시 WHERE status = 'PENDING'과 같은 조건절을 넣어 상태 전이의 안전성을 확보해야 한다.

 

 


 

결론: 정합성은 기술이 아닌 타협의 산물이다

 

분산 시스템에서 완벽한 실시간 정합성은 불가능에 가깝거나, 성능을 포기해야만 얻을 수 있는 비싼 대가다. SRE와 백엔드 엔지니어의 역할은 "절대 에러가 안 나게 하는 것"이 아니라, "에러가 났을 때 시스템이 스스로를 어떻게 복구하게 할 것인가"를 설계하는 데 있다.

 

SAGA 패턴은 데이터를 맞추기 위해 시스템을 멈추는 대신, '잠시 틀릴 수 있음'을 인정하고 우아하게 되돌리는 지혜로운 선택이다. 비즈니스의 복잡도에 따라 코레오그래피와 오케스트레이션을 전략적으로 선택하고, 멱등성이라는 방패를 통해 최종적인 데이터의 평화를 쟁취하라.

 

 


 

이것으로 인프라 플랫폼부터 AI 서빙, 그리고 고난도 백엔드 아키텍처에 이르는 총 15편의 기술 백서 연재를 마칩니다. 커널의 바닥부터 분산 시스템의 정점까지 훑어본 이 기록들이, 단순한 코더를 넘어 시스템 아키텍트로 성장하려는 모든 분에게 견고한 이정표가 되길 바랍니다.

 

 


 

SAGAPattern, 분산트랜잭션, 보상트랜잭션, MSA정합성, 최종정합성, 오케스트레이션, 멱등성, 백엔드설계

반응형
반응형

[백엔드 4편] gRPC와 Protobuf — HTTP/2 스트리밍 기반의 고성능 내부 통신 프로토콜 설계

 

마이크로서비스 아키텍처(MSA)에서 서비스 간 통신 비용은 전체 시스템의 지연 시간(Latency)을 결정짓는 핵심 변수다. 흔히 사용되는 REST API와 JSON의 조합은 인간이 읽기 편하다는 장점이 있지만, 기계 간의 통신(Machine-to-Machine)에서는 장황한 텍스트 페이로드와 매번 연결을 맺고 끊는 HTTP/1.1의 특성으로 인해 막대한 오버헤드를 발생시킨다.

 

구글이 공개한 gRPC는 이러한 한계를 돌파하기 위해 탄생했다. 바이너리 직렬화 포맷인 Protocol Buffers(Protobuf)HTTP/2의 멀티플렉싱 기능을 결합하여, 기존 REST 대비 수 배 이상의 성능 향상을 이뤄낸 gRPC의 내부 아키텍처를 심층 분석한다.

 

 


 

1. Protobuf: 바이너리의 압도적 효율성

 

JSON은 텍스트 포맷이기에 필드 이름("user_id")이 매번 반복되고, 숫자 데이터도 문자열로 인코딩되어 전송된다. 반면 Protobuf는 미리 정의된 스키마(.proto)를 바탕으로 데이터를 바이너리로 변환한다.

 

  • 메커니즘: 필드 이름을 보내는 대신, 미리 약속된 태그 번호(Index)만을 사용한다. 예를 들어 "user_id": 12345라는 데이터는 단 몇 바이트의 바이너리 숫자로 압축된다.
  • 성능: JSON 파싱은 문자열을 스캔하고 객체로 변환하는 무거운 작업이지만, Protobuf는 메모리 상의 데이터를 거의 그대로 직렬화하므로 CPU 소모량이 극히 적다. 이는 모바일 기기나 고빈도 통신이 발생하는 백엔드 서버에서 압도적인 이점이 된다.

 

 


 

2. HTTP/2 기반의 양방향 스트리밍

 

gRPC는 전송 계층으로 HTTP/2를 사용한다. 이는 단순히 최신 프로토콜을 쓰는 것 이상의 의미를 갖는다.

 

  • 멀티플렉싱(Multiplexing): 하나의 TCP 연결 위에서 수많은 요청과 응답이 동시에 오갈 수 있다. HTTP/1.1처럼 앞선 요청이 끝날 때까지 기다리는 'Head-of-Line Blocking' 문제가 사라진다.
  • 서버 푸시 및 양방향 스트리밍: 클라이언트가 요청을 보내기 전에도 서버가 데이터를 보낼 수 있으며, 대용량 파일 전송이나 실시간 채팅 등에서 연결을 유지한 채 데이터를 '조각(Chunk)' 단위로 계속 흘려보내는 스트리밍 통신이 가능하다.
  • 헤더 압축(HPACK): 반복되는 HTTP 헤더를 압축하여 네트워크 대역폭 낭비를 최소화한다.

 

 


 

3. IDL (Interface Definition Language) 중심의 개발 경험

 

gRPC의 가장 큰 특징은 코드 생성(Code Generation)이다. .proto 파일에 서비스 명세를 정의하면, 자바, 노드, 파이썬, 고(Go) 등 다양한 언어의 클라이언트/서버 코드가 자동으로 생성된다.

 

// user.proto
service UserService {
  rpc GetUser (UserRequest) returns (UserResponse) {}
}

message UserRequest {
  int32 id = 1;
}

message UserResponse {
  string name = 1;
  string email = 2;
}

 

  • 강점: 서로 다른 언어로 작성된 서비스 간에도 데이터 규격 불일치로 인한 런타임 에러가 발생하지 않는다. 컴파일 타임에 타입 체크가 완료되기 때문이다. 이는 대규모 엔지니어링 조직에서 '문서와 코드의 괴리'를 막아주는 강력한 가드레일이 된다.

 

 


 

4. 실전 도입 시 고려사항: 브라우저와 로드 밸런싱

 

gRPC가 만능은 아니다. 실무 도입 시 다음 두 가지 문제를 해결해야 한다.

 

  1. 브라우저 지원: 웹 브라우저는 HTTP/2의 로우 레벨 제어권이 제한적이라 gRPC를 직접 호출할 수 없다. grpc-web 프록시를 전면에 두거나, 외부 노출용으로는 여전히 REST/GraphQL을 유지해야 한다.
  2. L7 로드 밸런싱: gRPC는 TCP 연결을 오래 유지(Long-lived)하므로, 일반적인 L4 로드 밸런서는 특정 서버로 트래픽을 몰아주는 경향이 있다. 반드시 요청 단위로 부하를 분산할 수 있는 L7 로드 밸런서(Envoy, Linkerd)를 함께 구축해야 한다.

 

 


 

결론: MSA의 혈관, gRPC

 

마이크로서비스 아키텍처에서 네트워크는 서비스의 일부다. gRPC는 네트워크 지연이라는 물리적 한계를 직렬화 효율성과 프로토콜 최적화로 극복한다.

 

내부 서비스 간 호출이 잦고 초당 수만 건의 트래픽을 견뎌야 하는 시스템이라면, 더 이상 JSON에 미련을 두지 마라. 엄격한 스키마 기반의 gRPC 아키텍처로 전환하는 순간, 인프라 비용 절감과 시스템 안정성이라는 두 마리 토끼를 잡을 수 있을 것이다.

 

 


 

다음 편 예고

 

[백엔드 5편] SAGA 패턴 — 분산 트랜잭션 환경에서 보상 트랜잭션을 활용한 최종 정합성 구현

 

MSA에서 가장 고통스러운 순간은 '결제는 성공했는데 재고 차감에 실패했을 때'다. 물리적으로 분리된 여러 DB 사이에서 ACID 트랜잭션을 보장할 수 없는 한계. 이를 해결하기 위해 각 단계를 순차적으로 실행하고, 실패 시 이전에 성공한 작업을 되돌리는 보상 트랜잭션(Compensating Transaction) 기법인 SAGA 패턴의 두 가지 구현 방식(Choreography vs Orchestration)을 심층 분석한다.

 

 


 

gRPC, Protobuf, HTTP2, MSA통신, 직렬화최적화, 프로토콜버퍼, 양방향스트리밍, 백엔드아키텍처

반응형
반응형

[백엔드 3편] OAuth 2.0 & OIDC — 단순 인증을 넘어 보안 허점을 노리는 공격과 방어 시나리오

 

오늘날 대부분의 웹 서비스는 구글, 카카오, 애플 계정을 통한 '소셜 로그인'을 제공한다. 개발자들은 라이브러리가 제공하는 passport-google이나 Spring Security OAuth2 설정 몇 줄로 이 기능을 구현하곤 하지만, 정작 그 이면에서 오가는 Access TokenID Token의 차이, 그리고 프로토콜 흐름 속에 숨겨진 보안 취약점에 대해서는 무지한 경우가 많다.

 

잘못 설계된 OAuth 연동은 해커에게 사용자 계정 권한을 통째로 넘겨주는 하이웨이가 된다. 본 글에서는 OAuth 2.0과 OIDC(OpenID Connect)의 기술적 차이를 명확히 짚어보고, 실무에서 흔히 저지르는 보안 실수와 이를 악용하는 공격 시나리오를 분석한다.

 

 


 

1. OAuth 2.0 vs OIDC: 권한 부여와 신원 확인의 분리

 

  • OAuth 2.0: 자원에 대한 '권한(Authorization)'을 위임받는 프로토콜이다. "사용자의 구글 드라이브 파일 목록을 읽어도 좋다"는 승인을 받는 것이 목적이다. 사용자가 누구인지는 관심 밖이다.
  • OIDC: OAuth 2.0 레이어 위에 '신원(Authentication)' 확인 기능을 얹은 것이다. OAuth가 호텔 방 열람권을 주는 것이라면, OIDC는 투숙객의 신분증을 확인하는 과정이다. 결과물로 JWT 형태의 ID Token을 반환받는다.

 

 


 

2. 치명적 취약점 1: state 파라미터와 CSRF 공격

 

OAuth 인증 요청 시 전달하는 state 값은 단순한 옵션이 아니다. 이는 로그인 CSRF(Cross-Site Request Forgery) 공격을 막는 핵심 방패다.

 

2-1. 공격 시나리오

  1. 해커가 자신의 계정으로 '카카오 로그인' 인증 요청을 시작한다.
  2. 카카오로부터 Authorization Code를 발급받는 직전 단계에서 요청을 가로챈다.
  3. 이 유효한(하지만 해커 계정인) 코드가 담긴 리다이렉트 URL을 일반 사용자가 클릭하게 만든다.
  4. 사용자의 브라우저는 해커의 코드를 들고 우리 서비스 서버로 접속한다.
  5. 결과: 사용자의 서비스 세션에 해커의 카카오 계정이 연동되어 버린다. 이제 사용자가 개인 정보를 입력하면 해커는 자신의 카카오 계정으로 로그인하여 그 정보를 고스란히 훔쳐볼 수 있다.

 

2-2. 방어책

요청 시 브라우저 세션에 난수(Nonce)를 저장하고 이를 state 파라미터에 담아 보낸다. 콜백 시 돌아온 state 값이 세션에 저장된 값과 일치하는지 반드시 검증해야 한다.

 

 


 

3. 치명적 취약점 2: ID Token 서명 검증 누락

 

가장 흔한 사고는 백엔드 서버가 Authorization Code를 Token으로 교체한 뒤, 결과로 받은 ID Token(JWT)의 서명을 검증하지 않고 내부 정보(email, sub)를 그대로 믿어버리는 경우다.

 

  • 공격: 해커가 자신의 로컬 서버에 가짜 인증 서버를 띄우거나, 유효 기간이 지난 토큰을 조작하여 전송한다.
  • 위험: 서버가 서명을 확인하지 않으면, 해커는 sub(사용자 고유 ID) 값을 희생자의 ID로 조작하여 해당 사용자로서 로그인을 성공시킬 수 있다. (인증 우회)
  • 방어: 반드시 IdP(Identity Provider)가 제공하는 공개키(JWKS 엔드포인트)를 가져와 JWT의 서명 유효성, aud(Audience) 필드가 우리 서비스 앱 ID가 맞는지, exp(만료 시간)가 지나지 않았는지를 엄격하게 검증해야 한다.

 

 


 

4. 실전 팁: PKCE (Proof Key for Code Exchange) 도입

 

모바일 앱이나 SPA(Single Page Application)처럼 클라이언트 시크릿(Client Secret)을 안전하게 보관할 수 없는 환경에서는 PKCE가 필수다.
코드 교환 시점에 임의의 검증 코드(Code Verifier)를 동적으로 생성하여 전송함으로써, 중간에 코드가 탈취되더라도 해커가 실제 토큰으로 교환하는 것을 원천 차단한다. 현대적인 OAuth 연동에서는 서버 사이드 환경에서도 PKCE 사용을 강력히 권장하는 추세다.

 

 


 

결론: 편의성은 보안의 적이다

 

OAuth와 OIDC는 복잡한 인증 로직을 외부 전문 업체에 위임하여 개발 생산성을 높여주지만, 그 구현 과정에서의 사소한 생략이 거대한 보안 구멍을 만든다.

 

라이브러리의 기본 설정을 맹신하지 마라. 프로토콜의 각 단계에서 데이터의 무결성이 어떻게 보장되는지 이해하고, 특히 외부에서 들어오는 모든 토큰은 '오염되었을 것'이라는 가정하에 검증 절차를 거쳐야 한다. 안전한 로그인이 서비스 신뢰의 시작이다.

 

 


 

다음 편 예고

 

[백엔드 4편] gRPC와 Protobuf — HTTP/2 스트리밍 기반의 고성능 내부 통신 프로토콜 설계

 

JSON과 REST API의 느린 속도와 장황한 페이로드에 지쳤다면? 구글이 만든 gRPC로 눈을 돌릴 때다. 바이너리 포맷인 Protocol Buffers의 직렬화 효율성과 HTTP/2 멀티플렉싱을 활용한 양방향 스트리밍 통신 아키텍처를 분석한다. MSA 내부 통신에서 gRPC가 왜 필수적인지 벤치마크 데이터와 함께 파헤친다.

 

 


 

OAuth2, OIDC, 보안취약점, JWT검증, CSRF방어, PKCE, 소셜로그인, 백엔드보안

반응형
반응형

[백엔드 2편] 분산 락(Distributed Lock) — Redis Redlock과 Zookeeper 락의 알고리즘 및 정합성 한계

 

단일 서버 환경에서는 synchronizedReentrantLock으로 공유 자원을 보호할 수 있다. 하지만 서버가 수십 대인 마이크로서비스 아키텍처(MSA) 환경에서 선착순 이벤트나 재고 차감 로직을 구현해야 한다면? 로컬 락은 무용지물이 된다.

 

엔지니어는 여러 서버 노드가 공유하는 외부 저장소를 활용한 분산 락(Distributed Lock)을 도입해야 한다. 가장 대중적인 Redis의 Redlock 알고리즘과 강한 정합성을 자랑하는 Zookeeper 기반 락을 비교 분석하고, 특히 네트워크 파티션이나 GC Pause 상황에서 분산 락이 어떻게 깨질 수 있는지 그 치명적인 한계를 해부한다.

 

 


 

1. Redis 기반 분산 락의 두 얼굴

 

1-1. 단순 SetNX 방식의 위험성

초보적인 방식은 Redis의 SET resource_name my_id NX PX 30000 명령어를 사용하는 것이다.

  • 문제점: 만약 락을 획득한 프로세스가 로직 수행 중 30초를 넘기면 락이 자동으로 해제(Expire)된다. 이때 다른 프로세스가 락을 가로채고, 이전 프로세스가 뒤늦게 작업을 마치고 락을 해제(Delete)하려 할 때 남의 락을 지워버리는 불상사가 발생한다. (Lock Fencing 토큰이 필요한 이유다.)

 

1-2. Redlock 알고리즘: 다수결의 원칙

Redis 창시자 안티레즈(antirez)가 제안한 Redlock은 독립적인 5개의 Redis 노드를 운영한다.

  1. 현재 시간을 기록한다.
  2. 5개 노드에 순차적으로 락 획득을 시도한다.
  3. 과반수(3개 이상) 노드에서 락 획득에 성공하고, 소요된 시간이 유효 시간보다 짧다면 락을 획득한 것으로 간주한다.
    이 방식은 단일 노드 장애 시에도 가용성을 보장하지만, 시스템 시계(Clock Drift)가 노드마다 미세하게 다를 경우 정합성이 깨질 수 있다는 논란이 여전히 존재한다.

 

 


 

2. Zookeeper 기반 락: 강한 정합성의 선택

 

Zookeeper는 분산 코디네이션 서비스답게 락 구현에 최적화된 Ephemeral Sequential Node 기능을 제공한다.

 

  • 작동 원리: 클라이언트가 특정 경로 아래에 임시 순번 노드를 생성한다. 자신의 순번이 가장 낮으면 락을 획득한 것이다. 아니면 바로 앞 순번 노드에 Watch를 걸고 대기한다.
  • 장점: 클라이언트와 Zookeeper 사이의 세션이 끊기면 임시 노드가 자동으로 삭제된다. 타임아웃을 계산할 필요 없이 즉각적인 장애 감지가 가능하다.
  • 비교: 성능(TPS)은 Redis보다 떨어지지만, 데이터 일관성과 정합성 면에서는 훨씬 견고하다. 결제나 금융 거래 등 1원의 오차도 허용되지 않는 곳에는 Zookeeper(또는 이를 추상화한 Curator 프레임워크)가 권장된다.

 

 


 

3. 분산 락의 치명적 결함: Fencing 토큰의 필요성

 

분산 락은 만능이 아니다. 마틴 클레프만(Martin Kleppmann)이 지적했듯, 모든 분산 락은 STW(Stop-The-World) GC Pause 앞에서 무력하다.

 

  1. 클라이언트 A가 락을 획득한다.
  2. 클라이언트 A에 아주 긴 GC Pause가 발생하여 락의 유효 시간이 만료된다.
  3. Redis는 락을 해제하고, 클라이언트 B가 동일한 락을 획득한다.
  4. GC에서 깨어난 클라이언트 A는 자신이 여전히 락을 쥐고 있다고 착각하고 DB 쓰기 작업을 수행한다.
  5. 결과: 자원 정합성 파괴.

 

이를 방지하기 위해 저장소(DB) 레벨에서 Fencing Token(점진적으로 증가하는 ID)을 검증하는 로직이 동반되어야 한다. 락을 획득할 때마다 받은 토큰 번호가 저장소의 마지막 토큰보다 작다면 쓰기를 거부하는 최종 방어선이 필요하다.

 

 


 

결론: 락은 알고리즘이 아니라 정책이다

 

분산 락 도입 시 엔지니어는 스스로에게 물어야 한다. "우리는 가용성(Availability)이 중요한가, 정합성(Consistency)이 중요한가?"

 

단순한 중복 실행 방지가 목적이라면 Redis 기반의 가벼운 락으로 충분하다. 하지만 비즈니스 정합성이 절대적이라면 Zookeeper를 고려하고, 그것으로도 부족한 예외 상황을 대비해 DB 레벨의 낙관적 락(Optimistic Lock)이나 Fencing 토큰을 결합한 다중 방어 체계를 설계해야 한다. 분산 환경에 완벽한 락은 없으며, 오직 '통제된 위험'만이 있을 뿐이다.

 

 


 

다음 편 예고

 

[백엔드 3편] OAuth 2.0 & OIDC — 단순 인증을 넘어 보안 허점을 노리는 공격과 방어 시나리오

 

카카오, 구글 로그인을 연동할 때 우리는 정말 안전하게 코딩하고 있는가? Authorization Code Grant 방식에서 발생하는 State 파라미터 누락 취약점과 CSRF 공격, 그리고 ID Token 서명 검증을 생략했을 때 벌어지는 인증 우회(Authentication Bypass) 사례를 실제 페이로드 분석과 함께 심층적으로 다룬다.

 

 


 

분산락, DistributedLock, Redis, Redlock, Zookeeper, 동시성제어, 정합성, FencingToken

반응형
반응형

[백엔드 1편] Kafka의 Zero-copy 전송 — 어떻게 초당 수백만 메시지를 지연 없이 처리하는가?

 

아파치 카프카(Apache Kafka)가 링크드인(LinkedIn)에서 처음 탄생했을 때, 업계는 경악했다. 기존의 메시지 큐들이 초당 수만 건의 메시지 처리에 허덕일 때, 카프카는 단일 클러스터에서 초당 수백만 건의 이벤트를 지연(Latency) 없이 쏟아냈다. 많은 이들이 카프카가 특수한 하드웨어나 마법 같은 알고리즘을 쓴다고 생각했지만, 정답은 의외로 가까운 곳에 있었다.

 

카프카는 운영체제(OS) 커널의 기능을 극한으로 활용한다. 본 글에서는 데이터를 디스크에서 네트워크 카드로 옮길 때 발생하는 불필요한 복사 과정을 제거하는 Zero-copy 기술과, 커널의 Page Cache를 활용한 로그 구조 파일 시스템의 비밀을 파헤친다.

 

 


 

1. 전통적인 데이터 전송의 비효율성: Context Switch와 복사의 지옥

 

일반적인 파일 전송 프로그램(예: FTP, 구형 웹 서버)이 디스크의 파일을 읽어 네트워크로 보낼 때, 커널 내부에서는 다음과 같은 4번의 복사가 일어난다.

 

  1. 디스크 → 커널 Read Buffer: 하드웨어에서 OS 메모리로 데이터 로드 (DMA 복사).
  2. 커널 Read Buffer → 유저 어플리케이션 버퍼: 앱이 데이터를 처리하기 위해 자신의 메모리 영역으로 복사 (CPU 복사).
  3. 유저 어플리케이션 버퍼 → 커널 Socket Buffer: 전송을 위해 다시 커널 영역으로 복사 (CPU 복사).
  4. 커널 Socket Buffer → NIC(네트워크 카드) 버퍼: 실제 전송 수행 (DMA 복사).

 

이 과정에서 유저 모드와 커널 모드를 오가는 Context Switch가 4번 발생하며, CPU는 데이터를 이리저리 옮기느라 정작 중요한 비즈니스 연산에 집중하지 못한다. 메시지 양이 많아질수록 CPU는 복사 지옥에 빠져 성능이 수직 하락한다.

 

 


 

2. 카프카의 해결책: sendfile() 시스템 콜과 Zero-copy

 

카프카는 메시지를 가공하지 않고 그대로(Immutable) 전송하는 특성을 활용해, 유저 공간(User-space)을 아예 거치지 않는 Zero-copy 방식을 채택했다.

 

2-1. 하드웨어 간의 직접 데이터 패스

리눅스 커널의 sendfile() 시스템 콜을 사용하면, 커널은 유저 어플리케이션에 데이터를 넘겨주지 않고 커널 Read Buffer에서 Socket Buffer로 데이터를 즉시 복사한다. 최신 커널에서는 이마저도 최적화되어, 데이터의 위치 정보(Descriptor)만 전달하고 실제 복사는 NIC가 DMA를 통해 직접 커널 Buffer에서 가져가게 만든다.

 

  • 복사 횟수: 4회 → 2회 (사실상 CPU 복사는 0회).
  • Context Switch: 4회 → 2회.
  • 결과: CPU 부하가 거의 없이 네트워크 대역폭 전체를 메시지 전송에만 쏟아부을 수 있게 된다.

 

 


 

3. Page Cache 활용과 순차 I/O의 승리

 

카프카는 스스로 메모리를 관리하려 하지 않는다. 대신 운영체제의 Page Cache에 모든 것을 맡긴다.

 

3-1. OS의 기억력을 믿어라

카프카로 들어오는 모든 메시지는 디스크의 'Append-only 로그 파일'에 순차적으로 기록된다.

  • 쓰기 최적화: 임의 접근(Random Access)이 아닌 순차 쓰기(Sequential Write)는 현대의 NVMe/SSD 환경에서 디스크 연산 중 가장 빠르다.
  • 읽기 최적화: 컨슈머가 메시지를 읽으려 할 때, 커널은 해당 데이터가 이미 Page Cache(남는 RAM 영역)에 있음을 발견한다. 카프카 어플리케이션은 디스크 I/O를 일으키지 않고 메모리에서 데이터를 즉시 뽑아 네트워크로 쏜다.

 

이 때문에 카프카를 운영할 때는 앱 프로세스에 메모리를 많이 할당(Heap Size)하기보다, OS가 Page Cache로 쓸 수 있도록 시스템 메모리 여유분을 많이 남겨두는 것이 성능 튜닝의 핵심이다.

 

 


 

4. 실전 엔지니어링 교훈: Immutable의 가치

 

카프카가 Zero-copy를 구현할 수 있었던 근본적인 이유는 "한 번 쓰인 메시지는 절대 변하지 않는다"는 불변성(Immutability) 원칙 덕분이다. 메시지를 중간에 수정해야 했다면 유저 공간으로 데이터를 불러와야 했고, Zero-copy의 이점은 사라졌을 것이다.

 

대규모 분산 시스템을 설계할 때, 데이터를 가공하는 지점과 전송하는 지점을 명확히 분리하여 '전송 경로의 순수성'을 확보하는 것이 얼마나 압도적인 성능 차이를 만드는지 카프카 아키텍처는 증명하고 있다.

 

 


 

다음 편 예고

 

[백엔드 2편] 분산 락(Distributed Lock) — Redis Redlock과 Zookeeper 락의 알고리즘 및 정합성 한계

 

서버가 수십 대인 분산 환경에서 특정 자원(예: 선착순 쿠폰)에 대한 동시성 제어는 어떻게 할까? Redis를 활용한 Redlock 알고리즘의 작동 원리와, 네트워크 지연(GC Pause) 상황에서 발생할 수 있는 '락 유실' 문제의 실체를 파헤친다. 또한 강한 정합성을 보장하는 Zookeeper의 Ephemeral Node 기반 락 메커니즘과 비교 분석한다.

 

 


 

카프카, Kafka, ZeroCopy, sendfile, 페이지캐시, 리눅스커널, 순차IO, 메시지브로커

반응형
반응형

[AI 서빙 5편] 모델 양자화(Quantization) — 가벼우면서 똑똑한 모델을 만드는 수학적 원리

 

70B 모델을 FP16(16비트 부동소수점) 정밀도로 서빙하려면 가중치 값만 저장하는 데 약 140GB의 VRAM이 필요하다. 이는 단일 GPU 1장에 올리는 것이 불가능하며, 다중 GPU 분산 추론 환경을 강제하여 비용을 폭증시킨다. 하지만 우리가 이 모델을 4비트(INT4)로 압축할 수 있다면? 메모리 점유율은 35GB로 줄어들어 한 장의 GPU에서도 쌩쌩하게 돌아가게 된다.

 

이것이 바로 모델 양자화(Quantization)의 힘이다. 본 글에서는 숫자의 정밀도를 낮추면서도 모델의 지능(정확도)을 유지하는 수학적 메커니즘과, 최신 양자화 기법인 GPTQ, AWQ의 내부 작동 원리를 심층 분석한다.

 

 


 

1. 양자화의 본질: 매핑과 스케일링

 

양자화는 넓은 범위의 연속적인 값(부동소수점)을 좁은 범위의 이산적인 값(정수)으로 매핑하는 과정이다.

 

  • 수식: Q = clip(round(V / S) + Z, q_min, q_max)
    • V: 원래의 부동소수점 값.
    • S: 스케일(Scale) 인자.
    • Z: 제로 포인트(Zero-point).
  • 문제점: 32비트의 정보를 4비트로 압축하면 필연적으로 정보의 손실, 즉 양자화 오차(Quantization Error)가 발생한다. 이 오차를 어떻게 줄이느냐가 양자화 기술의 핵심이다.

 

 


 

2. 주요 양자화 전략: PTQ vs QAT

 

2-1. PTQ (Post-Training Quantization)

학습이 완료된 모델을 사후에 압축하는 방식이다. 추가 학습 비용이 거의 들지 않아 실무에서 가장 선호된다.

  • 방법: 대표적인 가중치 샘플(Calibration Dataset)을 모델에 통과시켜 각 레이어의 값 분포를 파악하고, 그 분포에 최적화된 스케일링 인자를 결정한다.

 

2-2. QAT (Quantization-Aware Training)

학습 단계부터 양자화로 인한 오차를 모델이 스스로 보정하도록 훈련하는 방식이다.

  • 방법: 학습 중에는 고정밀도 연산을 수행하되, 중간중간 양자화된 것처럼 값을 변형(Fake Quantization)하여 모델이 낮은 정밀도 환경에서도 높은 성능을 내도록 적응시킨다. 결과물은 최상이지만 비용이 매우 높다.

 

 


 

3. 최신 알고리즘의 진화: GPTQ와 AWQ

 

단순히 반올림하는 수준을 넘어, 모델의 구조적 특성을 이용한 알고리즘들이 등장했다.

 

3-1. GPTQ (Generative Pre-trained Transformer Quantization)

레이어별로 '이차 형식의 최적화 문제'를 풀어 오차를 최소화한다.

  • 원리: 특정 가중치를 양자화할 때 발생하는 오차를, 아직 양자화되지 않은 나머지 가중치들이 보정하도록 업데이트한다. 수천억 개의 파라미터를 단 몇 시간 만에 4비트로 정밀하게 압축할 수 있다.

 

3-2. AWQ (Activation-aware Weight Quantization)

모든 가중치가 똑같이 중요한 것은 아니다.

  • 원리: 추론 과정에서 특정 '채널'의 활성화 값(Activation)이 매우 크다면, 해당 채널의 가중치는 모델의 성능에 결정적인 역할을 한다(Salient weights). AWQ는 이러한 중요한 가중치들만 선별적으로 보호하고 나머지를 압축하여, 정확도 하락을 사실상 0%에 가깝게 방어한다.

 

 


 

4. 실전 엔지니어링: 양자화 모델 서빙의 이점

 

양자화 모델을 서빙하면 얻는 이득은 단순히 '메모리 절약'만이 아니다.

 

  1. 대역폭 이득: GPU 코어가 연산을 수행하기 위해 가중치를 메모리로부터 읽어오는 시간(Memory Bandwidth)이 1/4로 줄어든다. 이는 LLM 추론의 고질적 병목인 메모리 지연을 해결한다.
  2. 연산 가속: 최신 GPU(텐서 코어)는 정수 연산(INT8, INT4)을 부동소수점 연산보다 훨씬 빠르게 처리할 수 있는 하드웨어 가속기를 내장하고 있다.

 

 


 

결론: 작지만 강한 모델이 세상을 바꾼다

 

모델 양자화는 단순히 기술적인 보조 장치가 아니다. 거대 AI를 누구나 스마트폰에서, 혹은 저렴한 서버 한 대에서 돌릴 수 있게 만드는 'AI 민주화'의 핵심 열쇠다.

 

엔지니어는 단순히 모델을 배포하는 것에 그치지 말고, 수학적 오차 분석을 통해 우리 도메인에 맞는 최적의 양자화 기법을 선택해야 한다. 1%의 정확도를 포기하고 400%의 속도 향상을 얻을 것인가? 이 정교한 트레이드 오프를 관리하는 능력이 바로 시니어 AI 인프라 엔지니어를 가르는 척도다.

 

 


 

다음 편 예고 (고급 백엔드 아키텍처 시리즈로 이어집니다)

 

[백엔드 1편] Kafka의 Zero-copy 전송 — 어떻게 초당 수백만 메시지를 지연 없이 처리하는가?

 

다시 백엔드의 심장부로 돌아온다. 분산 메시징 시스템의 절대 강자 Kafka. 데이터를 디스크에서 네트워크 카드로 옮길 때 발생하는 CPU 부하를 제거하는 Zero-copy 기술과, 커널의 Page Cache를 극한으로 활용하는 로그 구조 파일 시스템의 비밀을 파헤친다.

 

 


 

모델양자화, Quantization, GPTQ, AWQ, PTQ, LLM최적화, GPU메모리관리, AI성능튜닝

반응형
반응형

[AI 서빙 4편] Triton Inference Server — 다이나믹 배칭(Dynamic Batching)을 통한 GPU 효율 극대화

 

GPU는 대규모 병렬 연산에 특화된 하드웨어다. 하지만 실시간 추론(Inference) 서비스 환경에서는 개별 사용자의 요청이 무작위 시점에 하나씩 들어오게 된다. 만약 GPU가 요청 하나가 들어올 때마다 연산을 수행한다면, 수천 개의 코어(CUDA Cores) 중 극히 일부만 사용되고 나머지는 놀게 되는 심각한 리소스 낭비가 발생한다.

 

이 문제를 해결하기 위해 NVIDIA가 내놓은 정답이 바로 Triton Inference Server다. Triton의 핵심 기능인 다이나믹 배칭(Dynamic Batching)이 어떻게 서로 다른 시점의 요청을 찰나의 순간에 하나로 묶어 GPU 가동률을 100%에 가깝게 끌어올리는지, 그 내부 스케줄링 메커니즘을 심층 분석한다.

 

 


 

1. 배칭(Batching)의 딜레마: 처리량(Throughput) vs 지연 시간(Latency)

 

추론 효율을 높이는 가장 간단한 방법은 요청을 모아서 한꺼번에 처리하는 것이다.

  • 배치 크기(Batch Size)가 클수록: 한 번의 연산으로 많은 사용자를 처리하므로 전체 처리량(Throughput)은 비약적으로 상승한다.
  • 문제점: 배치를 채우기 위해 기다리는 시간(Queueing Delay) 동안 첫 번째 사용자의 대기 시간(Latency)이 길어진다.

 

실시간 서비스에서는 1초의 지연도 치명적이다. Triton은 이 상충하는 가치를 '최대 대기 시간(Max Queue Delay)'이라는 파라미터로 조율한다.

 

 


 

2. 다이나믹 배칭 스케줄러의 동작 원리

 

Triton의 다이나믹 배칭은 요청이 들어오는 즉시 큐(Queue)에 쌓고, 설정된 기준에 따라 배치를 형성한다.

 

2-1. 스케줄링 파라미터 제어

Triton 설정 파일(config.pbtxt)에서 핵심적인 두 설정은 다음과 같다.

  • max_batch_size: GPU로 한 번에 보낼 수 있는 최대 요청 수.
  • max_queue_delay_microseconds: 배치가 가득 차지 않더라도, 최대 몇 마이크로초(us)까지 기다린 후 GPU로 연산을 명령할 것인지에 대한 임계값.

 

2-2. 적응형 배치 형성

예를 들어 max_batch_size가 8이고 max_queue_delay가 100ms라고 가정하자.

  1. 첫 번째 요청이 들어오면 타이머가 시작된다.
  2. 100ms 이내에 8개의 요청이 모두 차면, 즉시 배치를 형성하여 GPU로 전달한다.
  3. 만약 100ms가 지났는데 3개의 요청만 들어왔다면, 더 기다리지 않고 현재 3개만 묶어서 GPU로 보낸다.

 

이 매커니즘 덕분에 트래픽이 적을 때는 낮은 지연 시간을 유지하고, 트래픽이 폭주할 때는 자동으로 배치를 꽉 채워 GPU 파워를 풀가동하는 유연한 대응이 가능해진다.

 

 


 

3. 모델 동시성(Model Concurrency)과 인스턴스 그룹

 

Triton은 하나의 GPU 메모리 내에 동일한 모델의 복제본(Instance)을 여러 개 띄울 수 있다.

 

  • Instance Group: GPU 연산 유닛이 여유롭다면, 첫 번째 배치가 GPU에서 연산 되는 동안 두 번째 배치를 준비하여 바로 다음 사이클에 밀어 넣는 파이프라이닝이 가능하다.
  • 효과: 이를 통해 하드웨어 가속기(Accelerator)의 유휴 시간(Idle time)을 0에 가깝게 수렴시킬 수 있다. 특히 모델의 크기가 작고 연산량이 적은 경우, 인스턴스 그룹 최적화만으로도 단일 서버의 처리량을 수십 배 이상 높일 수 있다.

 

 


 

4. 실전 팁: 모델 아키텍처별 배치 최적화

 

  • CNN/RNN 계열: 고정된 입력 크기를 가지므로 다이나믹 배칭의 효과가 매우 명확하다.
  • LLM (Transformer) 계열: 입력 문장의 길이가 제각각이므로, 짧은 문장이 긴 문장 때문에 기다려야 하는 Padding 문제가 발생한다. 이를 해결하기 위해 Triton은 최근 In-flight Batching(연산 도중 완료된 요청을 즉시 빼고 새 요청을 끼워 넣는 기술) 기능을 도입하여 최적화 수준을 한 단계 더 끌어올렸다.

 

 


 

결론: GPU 가성비의 마침표, Triton

 

H100 한 장의 가격이 수천만 원을 호가하는 시대에 GPU를 놀리는 것은 엔지니어링적 직무 유기다.

 

사용자의 실시간 경험(Latency)을 보장하면서도 인프라 비용(Throughput)을 최소화하는 최적의 교차점. 그 접점을 수학적 스케줄링으로 찾아주는 Triton Inference Server는 AI 모델 서빙 아키텍처의 중추다. 모델의 성능을 측정할 때 단순히 한 번의 추론 속도만 보지 마라. 다이나믹 배칭을 적용했을 때의 초당 쿼리 처리량(QPS)이야말로 비즈니스 성패를 가르는 진짜 지표다.

 

 


 

다음 편 예고

 

[AI 서빙 5편] 모델 양자화(Quantization) — FP16에서 INT4로, 성능 하락 없이 용량을 줄이는 수학적 원리

 

100GB가 넘는 모델을 어떻게 1/4 크기로 줄여서 서빙할 수 있을까? 가중치의 정밀도를 낮추는 Post-Training Quantization (PTQ)과 학습 과정에서 이를 보정하는 Quantization-Aware Training (QAT)의 차이를 분석한다. 소수점 데이터를 정수로 변환할 때 발생하는 오차(Quantization Error)를 최소화하는 아웃라이어 관리 전략과 최신 AWQ, GPTQ 알고리즘의 실체를 파헤친다.

 

 


 

Triton, InferenceServer, DynamicBatching, GPU최적화, 모델서빙, 처리량개선, 지연시간단축, AI인프라

반응형

+ Recent posts

목차