소켓 이론 3/3 · 언어별 구현 — Python·Node.js·Go·Rust
소켓 API는 같아도 이벤트 루프, goroutine, Tokio task가 기다림과 계산을 처리하는 방식은 다릅니다. Node.js까지 더해 속도·메모리·동시성 안전을 비교합니다.

목차
TCP 서버가 하는 일은 어느 언어든 비슷합니다. 포트에서 기다리다가 연결을 받고,
바이트를 읽고, 응답을 쓰고, 연결을 닫습니다. 운영체제까지 내려가면 결국 같은
accept, read, write 같은 기능을 사용합니다.
그런데 연결이 1개에서 1만 개로 늘거나 메시지 처리에 무거운 계산이 섞이면 결과는 달라집니다. 차이를 만드는 것은 소켓 함수 자체보다 기다리는 작업을 어떤 단위로 쪼개고, 어느 스레드에서 다시 깨우며, 잘못된 공유를 언제 발견하는가입니다.
먼저 결론부터 말하면 이렇습니다.
- I/O 대기가 대부분이면 Python
asyncio, Node.js, Go, Rust/Tokio 모두 좋은 서버를 만들 수 있습니다. - CPU 계산이 섞이면 이벤트 루프에서 계산을 분리했는지가 언어 이름보다 중요합니다.
- Rust는 여러 메모리·동시성 실수를 컴파일 때 막지만, timeout이나 backpressure까지 자동으로 만들지는 않습니다.
- Go는 연결당 goroutine 모델이 단순하지만, 공유 메모리의 data race는 channel·mutex와 race detector로 관리해야 합니다.
먼저 세 질문으로 좁히세요
- 메시지를 기다리는 시간이 대부분인가요? 그렇다면 네 언어 모두 후보입니다.
- 요청마다 무거운 계산이 있나요? 그렇다면 언어보다 먼저 계산 풀과 큐 상한을 설계합니다.
- 마지막으로 팀의 주 언어, 배포 환경, 메모리 제어와 컴파일 검사의 우선순위를 비교합니다.
이 순서를 거꾸로 하면 “어느 언어가 가장 빠른가?”라는 답하기 어려운 질문에 갇히기 쉽습니다. 데모에서도 서버의 일 → 언어 → 안전장치 순서로 바꿔 보세요.
10초 선택표
| 우선순위 | 먼저 검토할 언어 | 이유 |
|---|---|---|
| 짧은 코드로 빠르게 반복 | Python | 코드가 간결하고 asyncio로 I/O 대기를 다루기 쉬움 |
| 웹 API·WebSocket을 한 생태계에서 개발 | Node.js | 프런트엔드와 언어·도구를 공유하기 쉬움 |
| 많은 연결과 단순한 운영 코드의 균형 | Go | 연결당 goroutine 구조가 읽기 쉽고 배포가 단순함 |
| 런타임 GC 없는 메모리 제어·컴파일 검사 | Rust | 소유권과 Send·Sync를 컴파일 때 확인 |
이 표는 벤치마크 순위가 아닙니다. 팀의 경험과 라이브러리, 실제 메시지 크기, CPU 작업 비율에 따라 답은 달라집니다. 먼저 데모에서 서버가 하는 일을 고른 뒤 언어 탭을 바꿔 보면 선택 이유가 더 분명해집니다.
“가장 빠른 언어”보다 먼저 물어야 할 것
소켓 서버에서 속도는 하나의 숫자가 아닙니다.
| 물어볼 것 | 뜻 |
|---|---|
| 동시 연결 수 | 대부분 기다리는 소켓을 얼마나 가볍게 유지하는가 |
| 처리량 | 1초 동안 메시지를 몇 개 끝내는가 |
| p95·p99 지연 | 느린 요청까지 포함해 응답이 얼마나 고른가 |
| 메모리 | 연결·버퍼·작업 하나가 차지하는 비용은 얼마인가 |
| 과부하 행동 | 한계를 넘었을 때 큐가 무한히 자라는가, 요청을 거절하는가 |
예를 들어 채팅 서버는 연결이 많아도 각 연결이 대부분 다음 메시지를 기다립니다. 반면 받은 이미지를 압축하는 서버는 연결이 적어도 CPU를 오래 씁니다. 두 서버에 같은 “동시 접속 1만”이라는 숫자를 붙여 비교하면 중요한 차이를 놓칩니다.
Python asyncio — 기다림을 간결하게 겹칩니다
async def handle(reader, writer):
data = await reader.read(1024)
writer.write(data)
await writer.drain()
server = await asyncio.start_server(handle, "0.0.0.0", 8080)
asyncio는 I/O 중심 네트워크 코드에 잘 맞습니다. 한 coroutine이
await reader.read()에서 기다리면 이벤트 루프가 준비된 다른 소켓을 처리합니다.
스레드를 연결마다 만들지 않고도 많은 대기를 겹칠 수 있고, 구현 속도도 빠릅니다.
주의할 곳은 CPU 작업입니다. async def 안에서 1초짜리 계산을 해도 그 계산에
await 지점이 없다면 이벤트 루프는 1초 동안 멈춥니다. 다른 coroutine과 소켓 I/O도
같이 밀립니다. 이런 작업은 ProcessPoolExecutor 같은 실행 자원이나 별도 서비스로
분리하고, 결과를 다시 비동기로 받아야 합니다.
Node.js — 작은 콜백이 많을 때 강합니다
net.createServer((socket) => {
socket.on("data", (data) => {
const writable = socket.write(data)
if (!writable) socket.pause()
})
socket.on("drain", () => socket.resume())
}).listen(8080)
Node.js도 이벤트 루프가 네트워크 이벤트를 조정합니다. 웹 API와 WebSocket을 같은 JavaScript 생태계에서 빠르게 만들기 좋고, 각 콜백의 일이 작다면 적은 스레드로 많은 클라이언트를 다룰 수 있습니다.
반대로 큰 JSON 파싱, 압축, 암호 계산을 JavaScript 콜백 안에서 오래 수행하면 그동안
다른 연결도 차례를 받지 못합니다. worker_threads, 작업 큐, 별도 프로세스 중 하나로
계산을 넘겨야 합니다. await를 붙였다는 사실만으로 동기 계산이 worker로 이동하지는
않습니다.
Go — 연결 하나를 goroutine 하나로 읽기 쉽게 표현합니다
ln, _ := net.Listen("tcp", ":8080")
for {
conn, err := ln.Accept()
if err != nil { continue }
go handle(conn)
}
Go에서는 연결마다 go handle(conn)을 실행하는 코드가 자연스럽습니다. goroutine은
작은 스택에서 시작하고 런타임이 여러 OS 스레드에 나눠 실행합니다. 한 goroutine이
I/O를 기다려도 다른 goroutine은 계속 움직일 수 있습니다. 네트워크 코드가 순차 코드처럼
읽히면서도 동시성을 얻는 것이 큰 장점입니다.
하지만 goroutine이 가볍다는 말은 무제한이라는 뜻이 아닙니다. 요청이 들어올 때마다 CPU 작업과 메시지 큐까지 끝없이 만들면 메모리와 tail latency가 커집니다. 크기가 정해진 worker pool과 bounded queue로 상한을 만들어야 합니다.
또 여러 goroutine이 같은 map이나 카운터를 수정하면 data race가 생길 수 있습니다.
channel로 상태 소유자를 하나 두거나 mutex로 보호하고, go test -race를 CI와 실제에
가까운 부하 테스트에 포함하는 것이 좋습니다.
Rust + Tokio — 실행 전 안전 검사의 범위가 넓습니다
let listener = TcpListener::bind("0.0.0.0:8080").await?;
loop {
let (socket, _) = listener.accept().await?;
tokio::spawn(async move {
process(socket).await
});
}
Tokio는 연결마다 가벼운 async task를 만들고, 준비된 task를 런타임 스레드에서 실행합니다.
Rust는 런타임 GC 없이 소유권으로 메모리 수명을 관리하고, 스레드 사이로 옮길 수 없는
값이나 안전하지 않은 참조를 Send, Sync, 수명 검사로 컴파일 때 거부합니다.
그 대가는 설계 비용입니다. 공유 상태는 Arc<Mutex<T>> 같은 타입으로 의도를 명시해야
하고, 비동기 작업이 어떤 값을 소유하는지 이해해야 합니다. 컴파일이 성공했다고 서버가
절대 멈추지 않는 것도 아닙니다. lock 순서를 잘못 잡은 deadlock, 논리 오류, 무제한 큐,
timeout 누락은 여전히 프로그램과 운영 설계의 책임입니다.
CPU 작업도 async task 안에서 오래 붙잡지 않는 것이 좋습니다. Tokio에서는
spawn_blocking이나 전용 계산 풀로 분리하고 동시에 실행할 작업 수를 제한합니다.
안정성은 세 층으로 나눠 봐야 합니다
“Rust는 안전하고 Python은 불안정하다”처럼 한 줄로 말하면 실제 선택에 도움이 되지 않습니다. 서로 다른 문제를 나눠야 합니다.
- 메모리 안전 — 해제된 메모리, 잘못된 포인터와 수명의 문제입니다. Rust는 이 층의 많은 오류를 컴파일 때 막습니다. Python·Node.js·Go는 자동 메모리 관리와 GC가 큰 부분을 맡습니다.
- 동시성 안전 — data race, 잘못된 갱신 순서, deadlock의 문제입니다. Rust의 타입 검사가 넓게 도와주지만 어떤 언어에서도 상태 소유권과 동기화 설계가 필요합니다.
- 운영 안정성 — timeout, backpressure, 연결·큐 상한, 재시도, graceful shutdown, 관측성의 문제입니다. 이 층은 언어가 대신 완성해 주지 않습니다.
느린 클라이언트를 예로 들어 보겠습니다. 상대가 초당 1KB만 읽는데 서버가 초당 1MB를
계속 write하면 차이는 결국 큐에 쌓입니다. Rust로 작성해도 큐에 상한이 없다면 메모리가
자랍니다. Python이어도 drain, timeout, bounded queue와 연결별 한도를 제대로 두면
느린 연결을 격리할 수 있습니다.
여기서는 네 런타임에 공통으로 필요한 운영 원칙까지만 확인합니다. 버퍼 상한과 멈춤·재개 신호, 청크 이어받기는 이론 3단계를 마친 뒤 소켓 응용 2/5 · 흐름 제어에서 상세히 다룹니다.
표보다 마지막에 확인할 것
팀이 이미 잘 다루는 언어, 장애를 추적할 관측 도구, 라이브러리 성숙도와 배포 환경이 미세한 성능 차이보다 더 큰 결과를 만들기도 합니다. 처음에는 익숙한 언어로 프로토콜을 검증하고, 실제 프로파일에서 병목이 확인된 부분만 다른 언어로 옮기는 방법도 현실적입니다.
최종 결정 전에는 “연결 수가 몇 개인가?”만 묻지 말고 메시지를 기다리는 시간과 CPU로 계산하는 시간의 비율, 허용할 p99 지연, 연결당 메모리, 팀이 운영할 수 있는 복잡도를 함께 적어 보세요. 그러면 위 10초 선택표를 어디까지 믿어도 되는지가 보입니다.
언어를 바꾸기 전에는 같은 서버에서 먼저 병목을 분리합니다.
- CPU 프로파일로 이벤트 루프나 handler가 오래 계산하는지 봅니다.
- 열린 연결 수와 연결당 RSS 메모리를 함께 기록합니다.
- 큐 길이와 거절 수를 측정해 과부하가 숨은 대기로 바뀌지 않는지 확인합니다.
- 평균이 아니라 p95·p99 지연과 timeout 비율을 봅니다.
- 정상 부하뿐 아니라 느린 클라이언트와 종료·재시작 상황을 시험합니다.
이 결과가 있어야 “런타임 한계”와 “현재 코드의 무제한 큐·동기 계산”을 구분할 수 있습니다. 구현을 그대로 옮기면 다른 언어에서도 같은 병목을 다시 만들 수 있습니다.
데모에서 확인할 것
- 기본값인 채팅·대부분 기다림에서 Python이 I/O 대기 사이에 다른 연결을 처리하는지 봅니다.
- 압축·계산이 많음으로 바꾸고 안전장치를 끄면 Python과 Node.js의 이벤트 루프가 어떻게 막히는지 봅니다.
- 안전장치 적용을 눌러 계산을 worker로 분리한 뒤 흐름이 달라지는지 확인합니다.
- 상대가 천천히 받음에서 설정을 끄면 네 언어 모두 위험해지는 것이 핵심입니다.
- 여럿이 같은 값 수정 예제에서는 Rust가 안전하지 않은 공유를 컴파일 때 막고, Go는 race detector로 실행 중 경합을 찾는 차이를 봅니다.
이 데모의 상태는 실제 벤치마크 수치가 아니라 런타임 실행 모델을 설명하는 시뮬레이션입니다. 실제 선택 전에는 같은 프로토콜, 같은 메시지 크기, 같은 연결 재사용 조건에서 워밍업과 p95·p99 지연, RSS 메모리, CPU 사용률, 오류율을 함께 측정해야 합니다.
소켓의 기본 흐름이 아직 낯설다면 먼저 소켓 이론 1/3에서 IP·포트·서버·클라이언트와 한 번의 요청·응답을 보고 돌아오면 이 비교가 더 선명해집니다. 아웃바운드 연결에서는 그 연결을 방화벽 안쪽 PC가 먼저 여는 방향과 성립한 세션의 양방향 데이터 흐름을 볼 수 있습니다.
다음: 소켓 응용 5단계
여기까지가 소켓 이론 3단계입니다. 이제 소켓 응용 1/5 · 전달 보장에서 ACK·메시지 ID·재시도로 끊긴 연결 위의 메시지를 안전하게 처리하는 규칙을 시작합니다.

