100. 다대다 (Many-to-Many) 스레드 모델

핵심 인사이트 (3줄 요약)

  1. 본질: 다대다 (Many-to-Many) 모델은 사용자 수준 스레드 (ULT, User-Level Thread) 여러 개를 그보다 작거나 같은 수의 커널 수준 스레드 (KLT, Kernel-Level Thread)에 다중화(Multiplexing)하여 연결하는 하이브리드 아키텍처다.
  2. 가치: 다대일 (Many-to-One) 모델의 블로킹 (Blocking) 문제와 일대일 (One-to-One) 모델의 커널 스레드 생성 오버헤드 (Overhead) 문제를 동시에 해결하여, 무한대의 동시성 (Concurrency)과 하드웨어 한도 내의 병렬성 (Parallelism)을 모두 제공한다.
  3. 융합: 운영체제 (OS, Operating System) 레벨에서는 구현 복잡성으로 인해 일대일 모델로 회귀했지만, 이 아키텍처의 설계 철학은 Go 언어의 고루틴 (Goroutine)과 같은 현대 프로그래밍 언어의 런타임 스케줄러 구조로 완벽하게 계승되어 대용량 동시 처리의 핵심이 되었다.

Ⅰ. 개요 및 필요성 (Context & Necessity)

  • 개념: 다대다 (Many-to-Many) 모델은 응용 프로그램이 원하는 만큼 사용자 수준 스레드 (ULT, User-Level Thread)를 생성할 수 있도록 허용하면서도, 이를 실행하기 위한 커널 수준 스레드 (KLT, Kernel-Level Thread)의 개수를 시스템(또는 프로세스)의 가용 자원에 맞춰 제한적으로 유지하는 방식이다. M개의 ULT가 N개의 KLT(M ≥ N)에 유연하게 매핑된다.
  • 필요성: 다대일 모델은 빠르지만 하나의 스레드가 블로킹되면 전체가 마비되었고, 일대일 모델은 병렬 처리가 가능하지만 스레드 생성 시 커널 모드 (Kernel Mode) 전환 비용이 크고 메모리 고갈 위험이 컸다. 개발자들은 문맥 교환 (Context Switching) 비용을 사용자 공간에서 처리할 수 있을 만큼 가벼우면서도, 멀티코어 (Multi-core) CPU (Central Processing Unit)의 자원을 모두 활용할 수 있는 "두 모델의 장점만 결합한" 완전한 해결책이 필요했다.
  • 💡 비유: 다대다 모델은 거대한 콜택시 회사와 같다. 승객(사용자 스레드)은 수만 명일 수 있지만, 회사가 보유한 실제 택시(커널 스레드)는 100대에 불과하다. 회사 상황실(스레드 라이브러리)은 빈 택시가 생길 때마다 기다리는 승객을 유연하게 배차시켜 한정된 차량으로 수많은 승객을 효율적으로 목적지로 실어 나른다.
  • 등장 배경: 과거 Solaris 운영체제나 IRIX 시스템 등에서 최적의 성능을 끌어내기 위해 Two-level 모델(다대다의 변형)을 도입했다. 이 모델은 성능 상 가장 이상적이었으나 운영체제 내부에 스케줄러 촉발 (Scheduler Activation)이라는 복잡한 메커니즘을 구현해야 했다.

다대다 모델이 어떻게 ULT와 KLT 사이의 완충 지대를 형성하여 다중화를 달성하는지 아키텍처 구조도로 파악할 수 있다.

┌────────────────────────────────────────────────────────────────┐
│                  사용자 공간 (User Space)                      │
│                                                                │
│  ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐               │
│  │ULT 1│ │ULT 2│ │ULT 3│ │ULT 4│ │ULT 5│ │ULT 6│ (M개)         │
│  └─┬───┘ └─┬───┘ └─┬───┘ └─┬───┘ └─┬───┘ └─┬───┘               │
│    │       │       │       │       │       │                   │
│    └───────┴───────┴───┬───┴───────┴───────┘                   │
│                    [스레드 라이브러리]                         │
│                    (다중화 스케줄러)                           │
├────────────────────────│───────────────────────────────────────┤
│                        ▼                                       │
│               ┌───────────────────┐                            │
│  LWP 영역      │ LWP 1 │ LWP 2 │ LWP 3 │  (중간 매개체)        │
│               └─┬─────────────────┬─┘                          │
├─────────────────│─────────────────│────────────────────────────┤
│                 ▼                 ▼                            │
│  커널 공간   ┌───────┐         ┌───────┐                       │
│             │ KLT 1 │         │ KLT 2 │  (N개, M ≥ N)          │
│             └───────┘         └───────┘                        │
│                 │                 │                            │
│             [Core 0]          [Core 1]                         │
└────────────────────────────────────────────────────────────────┘

[다이어그램 해설] 이 구조도의 핵심은 사용자 스레드 (ULT, User-Level Thread)와 커널 스레드 (KLT, Kernel-Level Thread) 사이에 경량 프로세스 (LWP, Lightweight Process)라는 가상의 매개체가 존재한다는 점이다. 스레드 라이브러리는 6개의 ULT를 커널이 제공하는 3개의 LWP(그리고 그에 연결된 KLT)에 동적으로 할당(다중화)한다. 만약 ULT 1이 I/O 작업을 요청하여 LWP 1이 블로킹 (Blocking)되더라도, 커널은 프로세스 전체를 멈추지 않고 나머지 LWP 2, LWP 3을 통해 ULT 2~6을 계속 스케줄링한다. 응용 프로그램은 메모리 한계 내에서 무한대의 ULT를 빠르고 가볍게 생성할 수 있으며, 운영체제는 KLT의 개수를 CPU (Central Processing Unit) 코어 수나 최적의 문맥 교환 한계치 이하로 유지하여 시스템 과부하를 막아낸다. 두 세계의 완벽한 타협점이다.

  • 📢 섹션 요약 비유: 은행에 고객(사용자 스레드)이 1,000명이 몰려와도 창구 직원(커널 스레드)을 무한정 늘리는 대신 최적의 인원인 5명만 배치하고, 로비 매니저(스레드 라이브러리)가 고객들을 유연하게 빈 창구로 안내하여 업무 마비와 인건비 낭비를 동시에 막아내는 시스템과 같습니다.

Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)

다대다 모델이 제대로 동작하기 위해서는 커널 공간과 사용자 공간이 스레드의 상태를 서로 주고받는 고도의 통신 메커니즘이 필요하다.

구성 요소역할내부 동작관련 개념비유
LWP (Lightweight Process)ULT와 KLT 사이의 매핑 인터페이스사용자 스레드를 실행하기 위한 가상 처리기가상 CPU (Virtual CPU)택시 운전석
스레드 라이브러리 (Thread Library)사용자 수준의 스케줄링 및 LWP 할당LWP 풀(Pool)을 관리하며 대기 중인 ULT를 배정N:M Scheduler콜택시 배차원
스케줄러 촉발 (Scheduler Activation)커널이 사용자 라이브러리에 상태 변화를 알림LWP 블로킹 시 새로운 LWP를 할당해 줌Upcall (업콜)상황실 긴급 무전
업콜 핸들러 (Upcall Handler)스케줄러 촉발을 수신하여 처리하는 루틴블로킹된 ULT의 상태를 저장하고 다른 ULT를 LWP에 배정콜백 함수 (Callback)사고 처리 대응반

특히 다대다 모델의 가장 중요한 혁신인 스케줄러 촉발 (Scheduler Activation)과 업콜 (Upcall) 메커니즘을 순차 흐름도로 살펴보면, 커널과 라이브러리가 어떻게 협력하여 블로킹 한계를 돌파하는지 이해할 수 있다.

[사용자 공간의 스레드 라이브러리]                   [운영체제 커널]

    ULT 1을 LWP 1에 할당하여 실행 ────────▶ (실행 중)
                                              │
    ULT 1이 I/O 시스템 콜 호출 ────────────▶ │ (KLT 1 블로킹 발생)
                                              │
                                              ├── 1. KLT 1 및 LWP 1 대기 상태 전환
                                              │
  ┌── 3. 업콜 핸들러(Upcall Handler) 실행 ◀────┼── 2. 스케줄러 촉발 (새로운 LWP 2 제공)
  │                                           │
  ├── 4. ULT 1의 상태를 TCB에 저장            │
  │                                           │
  ├── 5. 대기 중이던 ULT 2를 선택             │
  │                                           │
  └── 6. ULT 2를 새로운 LWP 2에 할당 ───────▶ │ (KLT 2를 통해 계속 실행)
                                              │
  (시간 경과 후 I/O 완료)                     ├── 7. I/O 완료 인터럽트 발생
  ┌── 9. 업콜 핸들러 실행 ◀──────────────────┼── 8. 스케줄러 촉발 (LWP 1 해제 알림)
  │                                           │
  └── 10. ULT 1을 다시 대기 큐(Ready)에 삽입  │

[다이어그램 해설] 다대일 모델에서는 스레드 하나가 블로킹되면 커널이 이를 사용자 라이브러리에 알려주지 않아 전체가 마비되었다. 다대다 모델은 이 문제를 스케줄러 촉발 (Scheduler Activation)이라는 커널의 통지(업콜, Upcall) 메커니즘으로 해결한다. 흐름도를 보면, ULT 1이 I/O 요청으로 인해 LWP 1(그리고 연결된 KLT)이 블로킹될 때, 커널은 이 프로세스 전체를 멈추는 대신 "너희 스레드 하나가 막혔으니 다른 걸 실행할 수 있게 새 LWP 2를 줄게"라고 사용자 공간의 업콜 핸들러를 호출(Upcall)한다. 스레드 라이브러리는 이 핸들러 내에서 ULT 1의 상태를 보존하고, 아직 실행 대기 중이던 ULT 2를 새로 받은 LWP 2에 배정하여 즉시 실행을 이어간다. 훗날 I/O가 완료되면 커널은 다시 업콜을 통해 이를 알리고 LWP를 수거해 간다. 이러한 완벽한 통신 구조 덕분에 블로킹 현상이 다른 스레드로 전파되지 않으면서도 매우 빠른 사용자 수준 문맥 교환이 가능해진다.

  • 📢 섹션 요약 비유: 달리는 택시(LWP)가 타이어 펑크(블로킹)로 멈췄을 때 승객 전체가 기다리는 것이 아니라, 본사(커널)가 즉시 대체 택시(새로운 LWP)를 보내어 다른 대기 승객(다른 ULT)이 지연 없이 여행을 계속할 수 있게 지원하는 첨단 관제 시스템과 같습니다.

Ⅲ. 융합 비교 및 다각도 분석 (Comparison & Synergy)

과거에서 현재까지의 세 가지 주요 스레드 모델 간의 성능 교환 조건(Trade-off)을 비교 분석하면, 다대다 모델이 시스템 자원 측면에서 가지는 이론적 우위성을 증명할 수 있다.

특성 비교다대일 (Many-to-One)일대일 (One-to-One)다대다 (Many-to-Many)
병렬성 (Parallelism)X (단일 코어만 사용)O (코어 수만큼 N개 병렬)O (할당된 KLT 수만큼 병렬)
블로킹 독립성X (하나 블로킹 시 전체 중단)O (상호 독립적 생존)O (업콜을 통한 타 스레드 생존)
스레드 생성 비용매우 낮음 (사용자 공간)높음 (시스템 콜 필수)낮음 (최초 LWP 생성 후에는 빠름)
제한 없는 생성O (가벼운 TCB만 소모)X (커널 메모리 한계 존재)O (수십만 개 생성 가능)
구현 복잡도단순 (라이브러리 단독)중간 (OS 커널 내장)매우 높음 (OS와 라이브러리 협력 필수)

스레드 수의 증가에 따른 '문맥 교환 오버헤드 (Context Switch Overhead)'와 '실제 처리량 (Throughput)'의 관계를 그래프로 비교하면 각 모델이 무너지는 한계점을 알 수 있다.

처리량(Throughput)
  ▲
  │                       [다대다(M:M) 모델]
  │                      /‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\
  │                     /                         \ (CPU 캐시 최적화 지속)
  │                    /
  │                   /   [일대일(1:1) 모델]
  │                  /    /‾‾‾‾‾\
  │                 /    /       \
  │                /    /         \ (스레드가 1000개 이상 시 스래싱 급증)
  │               /    /           \
  │              /    /             \
  │             /    /
  │ [다대일(M:1)]   /
  │ ‾‾‾‾‾‾‾‾‾‾‾\__/
  │ (코어1 한계) 
  └────────────────────────────────────────────────────────▶
                                         스레드 개수 (N)

[다이어그램 해설] 이 성능 그래프는 요구되는 논리적 동시성(스레드 개수)이 증가할 때 시스템 처리량이 어떻게 변하는지를 보여준다. 다대일 모델은 처음부터 단일 코어의 성능 한계에 부딪혀 일정 수준 이상 처리량이 오르지 않는다. 일대일 모델은 코어 수에 비례하여 처리량이 선형적으로 증가하지만, 스레드 개수가 커널이 감당할 수 있는 한계(수천~수만 개)를 넘어서는 순간 컨텍스트 스위칭 오버헤드와 TLB/L1 캐시 미스가 폭증하는 스래싱 (Thrashing) 현상이 발생하여 그래프가 급격히 추락한다. 반면, 다대다 모델은 10만 개의 사용자 스레드(ULT)를 만들더라도 실제 커널 영역에서 스위칭되는 KLT의 수는 하드웨어 코어 수(예: 8개, 16개)에 맞춰 엄격히 통제된다. 따라서 스레드가 기하급수적으로 늘어나더라도 스래싱이 발생하지 않고 높은 처리량을 안정적으로 유지(고점의 평탄한 유지)할 수 있는 독보적인 확장성을 증명한다.

  • 📢 섹션 요약 비유: 소형차(다대일)는 짐을 조금밖에 못 싣고, 대형 트럭 수백 대(일대일)는 도로를 꽉 막히게 하여 옴짝달싹 못 하게 만들지만, 기차(다대다)는 앞의 기관차(KLT) 개수는 고정해 두고 뒤에 화물칸(ULT)만 끝없이 이어 붙임으로써 교통체증 없이 최대의 짐을 나르는 원리와 같습니다.

Ⅳ. 실무 적용 및 기술사적 판단 (Strategy & Decision)

이론적으로 완벽해 보이는 다대다 모델이지만, 실무와 운영체제 발전사에서는 흥미로운 의사결정의 변화를 겪었다.

실무 시나리오 1. 대규모 I/O 처리 서버 (Golang 채택): 초당 수백만 건의 네트워크 패킷을 처리해야 하는 분산 시스템 환경. 기존의 C++이나 Java(일대일 스레드 기반)로 개발할 경우 스레드를 수만 개 띄우면 메모리가 부족해진다. 아키텍트는 언어 차원에서 다대다 모델(M:N 스케줄러)을 내장하고 있는 Go 언어를 선택한다. 수십만 개의 고루틴 (Goroutine)을 생성해도 내부적으로는 논리 프로세서(P)와 OS 스레드(M)에 매핑되어 가볍게 스위칭되므로, 적은 하드웨어 자원으로 극한의 동시성을 달성할 수 있다.

기술사적 판단: OS 레벨에서의 M:M 포기: 과거 Sun Solaris 운영체제는 다대다 모델을 전면 도입했으나, 결국 나중에는 일대일 모델로 회귀했다. 그 이유는 "구현의 극단적인 복잡성" 때문이었다. OS가 사용자 라이브러리와 통신하는 업콜(Upcall) 구조는 커널의 안정성을 해치고 유지보수를 극도로 어렵게 만들었다. 하드웨어 성능이 좋아지고 커널 내부의 스레드 처리 로직(리눅스 NPTL 등)이 고도화되면서, "복잡한 M:M을 쓰느니 무거운 1:1을 하드웨어 힘으로 짓누르거나 스레드 풀(Thread Pool)로 감싸서 쓰는 게 낫다"는 실용주의적 결론이 현대 OS의 표준이 되었다.

이러한 기술적 트레이드오프와 발전사를 보여주는 의사결정 트리를 살펴보면 실무적 판단 기준이 명확해진다.

    [ 극단적 수준의 다중 동시성 처리 아키텍처 설계 ]
                       │
                       ▼
      운영체제(OS) 수준에서 다대다(M:M) 모델을 지원하는가?
         ├── 예 ──▶ 과거 Solaris 등 일부 유닉스 (현재는 거의 사장됨)
         │           └─▶ 잦은 커널 패닉 및 업콜 관리의 복잡도 감수
                       │
         └── 아니오 (현대 Linux / Windows)
                       │
               ▼
      일대일(1:1) 스레드의 한계(C10K 등 메모리 고갈)를 극복할 방법은?
         ├── 방법 A ─▶ 비동기 이벤트 루프 (Event Loop) 모델 사용
         │             (Node.js, Nginx, Redis) - 콜백 지옥 주의
                       │
         └── 방법 B ─▶ 언어 런타임 층에서 다대다(M:M) 모델을 자체 구현한
                       언어 도입 (Go의 Goroutine, Erlang/Elixir)
                       (가장 직관적인 동기식 코드 작성 + 최고 성능)

[다이어그램 해설] 이 의사결정 흐름은 현대 소프트웨어 엔지니어가 다대다 모델의 장점을 어떻게 실무에 적용하고 있는지를 보여준다. OS 단에서는 다대다 모델이 사장되었지만, 그 훌륭한 개념은 버려지지 않고 프로그래밍 언어의 런타임으로 이식(방법 B)되었다. 개발자는 스레드 폭증 문제가 예상될 때, 억지로 C/C++로 OS 스레드를 쥐어짜는 대신 다대다 모델의 최신 구현체인 고루틴(Go)이나 액터 모델(Erlang)을 채택함으로써 아키텍처 레벨에서 문제를 우아하게 회피하는 전략을 취해야 한다. 기술사는 이러한 모델의 변천사를 이해하고 적재적소에 올바른 언어와 런타임을 배치하는 역할을 수행해야 한다.

  • 📢 섹션 요약 비유: 너무 정교하고 완벽한 기계 시계(OS 레벨의 다대다 모델)는 잦은 고장과 수리비 폭탄을 불렀지만, 그 시계의 핵심 톱니바퀴 설계도(M:M 철학)는 최신형 스마트워치 소프트웨어(Go 언어 등 런타임)에 그대로 이식되어 현대 기술의 승리를 이끈 것과 같습니다.

Ⅴ. 기대효과 및 결론 (Future & Standard)

다대다 모델의 철학은 동시성 프로그래밍 패러다임을 혁신적으로 변화시켰으며 그 가치는 현재 진행형이다.

구분일대일(OS 스레드) 의존 시언어 레벨 다대다(M:N) 도입 시기대 효과 (가치 창출)
메모리 풋프린트스레드당 약 1MB 이상 (스택)고루틴 당 약 2KB메모리 효율 500배 향상
코딩 패러다임콜백, 퓨처(Future) 등 복잡순차적(동기식) 코딩 가능개발 생산성 극대화 및 유지보수성 향상
도메인 적합성연산 집약(CPU-bound) 작업마이크로서비스, 웹소켓 등 I/O 집약MSA 기반 클라우드 네이티브 아키텍처에 최적화

미래 전망: 클라우드 컴퓨팅과 마이크로서비스 아키텍처 (MSA, Microservices Architecture)의 대중화로 수만 개의 동시 접속을 가볍게 처리하는 것이 필수 조건이 되었다. 다대다 모델의 유산인 Go의 스케줄러, Java 21의 가상 스레드 (Virtual Threads - Project Loom) 등 사용자 공간 중심의 M:N 매핑 기술이 향후 백엔드 개발의 절대적인 표준 (Standard)으로 자리 잡게 될 것이다. 하드웨어(코어 수)와 소프트웨어(루틴 수) 간의 임피던스 불일치를 해소하는 가장 이상적인 해답이기 때문이다.

  • 📢 섹션 요약 비유: 다대다 모델은 과거에 잠시 반짝이고 사라진 구형 엔진이 아니라, 오늘날 수백만 대의 서버 위에서 거대한 구름(클라우드)을 떠받치고 있는 보이지 않는 최첨단 서스펜션(완충 장치)으로 화려하게 부활했습니다.

📌 관련 개념 맵 (Knowledge Graph)

개념 명칭관계 및 시너지 설명
경량 프로세스 (LWP)다대다 모델에서 ULT와 KLT를 연결해 주는 커널의 자료구조로, 스케줄링의 버퍼 역할을 수행한다.
스케줄러 촉발 (Scheduler Activation)블로킹 발생 시 커널이 사용자 라이브러리에 개입할 수 있도록 상태 정보를 통지하는 핵심 메커니즘.
업콜 (Upcall)운영체제가 응용 프로그램 영역의 핸들러(콜백)를 역으로 호출하여 문맥 교환을 지시하는 구조.
고루틴 (Goroutine)다대다(M:N) 모델을 현대적으로 재해석하여 Go 언어에 내장한 초경량 사용자 수준 동시성 처리 단위.
가상 스레드 (Virtual Thread)Java 21에 도입된 다대다 모델 구현체로, JVM이 수백만 개의 가상 스레드를 적은 수의 플랫폼 스레드(KLT)에 매핑한다.

👶 어린이를 위한 3줄 비유 설명

  1. 놀이공원(컴퓨터)에 1,000명의 아이들(사용자 스레드)이 롤러코스터를 타러 왔는데, 롤러코스터 기차(커널 스레드)는 딱 10대밖에 없어요.
  2. 옛날에는 기차가 고장 나면 줄 서 있던 아이들이 전부 다 기다려야 했지만, 다대다 모델은 아주 똑똑한 안내원(스레드 라이브러리)이 있어서 다른 멀쩡한 기차로 바로바로 갈아태워 준답니다!
  3. 덕분에 롤러코스터를 무작정 많이 만들지 않아도(돈 절약!), 수많은 아이들이 끊기지 않고 재미있게 놀이공원을 즐길 수 있는 최고의 방법이에요.