개요
1-1. Go 동시성, 정말 장점일까? - 주요 언어 성능 비교
GoLang의 사용처를 묻는 질문이 들어온다면 블록체인, 게임서버, 네트워크, 실시간 데이터 처리, 고성능 API 등 중에서 아마 대답이 나올 겁니다.
결국 고루틴(goroutine) 기반의 강력한 동시성을 GoLang의 핵심으로 보고 이를 장점으로 이야기 하는 것이죠. 이는 GoLang에 대해 공부하면 경량화된 쓰레드 자체 특징에 더해 채널, 락관리 등도 매우 편리하게 할 수 있는 걸 알고 있죠.
하지만 한 번더 꼬리물기 질문을 한다면?
- 진짜 이게 장점일까?
- 다른 언어에 비해 월등히 좋을까?
- Rust도 좋다고들 하던데 Rust 보다도 좋을까?
- Python은 느리다던데 유의미한 퍼포먼스 차이가 있을까?
이런 질문들에 대해서는 실제로 해본적은 없으니 “메모리 사용량은 약간 적고 실행 속도는 더 빠를 것”이라는 막연한 생각만이 떠오릅니다. 그래서 이번 포스트는 여러 언어들에서 동시성 로직을 다룰 때 무슨 특징들이 있는 지를 다뤄볼겁니다.
1-2. 동시성 로직 비교 대상 언어
이번 포스트에서는 같은 동시성 로직을 두고 여러 언어들은 다음과 같습니다. 개발자 커뮤니티에서 주로 보이는 백엔드 언어를 우선순위로 삼았습니다, 각자 자신의 영역에서 대표적인 언어들이죠.
Go
- 고성능 API 서버, 마이크로서비스, 클라우드/인프라(Kubernetes·Docker·Terraform), 블록체인 등에 쓰이는 언어
- 경량 쓰레드라는 특징 덕분에 대규모 트래픽 서비스 모두에서 점유율 향상 중
Python
- AI/ML, 데이터 분석, 웹 백엔드(Django/Flask), 자동화 스크립트 분야에서 절대적인 입지
- 동시성은 약하지만 생태계와 생산성 덕분에 빠른 개발이 필요한 곳에서 자주 사용
Java
- 엔터프라이즈 서버, 금융권, 대규모 트래픽 플랫폼의 대표 언어
- 안정성·성능·JVM 생태계 덕분에 기업 환경에서 가장 널리 쓰이고 있음
C
- OS, 커널, 임베디드, 네트워크 드라이버 등 시스템 프로그래밍에서 대표적인 언어
- 로우 레벨에서의 쓰레드 제어가 가능
Rust
- 메모리 안전성과 고성능을 강조하는 현대적인 언어, 시스템 프로그래밍에서 사용
- WebAssembly, 고성능 서버, CLI, 블록체인 등에서 C/C++의 영역을 일부 대체하는 중
본론
2-1. 동시성 알고리즘 정의
비교할 떄 적용해볼 문제는 숫자 1을 1,000,000번 더하는 작업 10개를 동시에 실행하는 로직을 만들어볼 겁니다. 언어별로 뮤텍스나 세마포어 처리가 프로세스에 대해 명확하게 아는 것이 아니기에 공유 메모리에 대한 비교는 다루지 않을 예정입니다.
2-2. 동시성 구현 코드 비교
각 언어는 동일한 로직을 수행하는 다음 코드로 비교를 해봤습니다.
C Code
#include <pthread.h>
#include <stdio.h>
#define JOBS 10
#define ITERS 1000000
int results[JOBS];
void* worker(void* arg) {
long idx = (long)arg;
int sum = 0;
for (int i = 0; i < ITERS; i++) {
sum++;
}
results[idx] = sum;
return NULL;
}
int main(void) {
pthread_t threads[JOBS];
for (long i = 0; i < JOBS; i++) {
pthread_create(&threads[i], NULL, worker, (void*)i);
}
int total = 0;
for (int i = 0; i < JOBS; i++) {
pthread_join(threads[i], NULL);
total += results[i];
}
printf("lang=c total=%d\n", total);
return 0;
}
- pthread.h: C에서 멀티스레딩을 할 때 사용하는 표준 라이브러리
- pthread_t: 스레드 표시타입
- pthread_join(): 스레드 종료까지 대기
Rust Code
use std::thread;
const JOBS: usize = 10;
const ITERS: i32 = 1_000_000;
fn main() {
let mut handles = Vec::with_capacity(JOBS);
for _ in 0..JOBS {
handles.push(thread::spawn(|| {
let mut sum = 0;
for _ in 0..ITERS {
sum += 1;
}
sum
}));
}
let mut total = 0;
for h in handles {
total += h.join().unwrap();
}
println!("lang=rust total={}", total);
}
- use std::thread: Rust 표준 라이브러리의 스레드(thread) 모듈을 사용
- Vec::with_capacity(): 레드 핸들을 저장할 벡터(Vec)를 미리 capacity 확보하여 생성
- h.join().unwrap(): 스레드 종료까지 대기
GoLang Code
package main
import (
"fmt"
"sync"
)
const (
JOBS = 10
ITERS = 1_000_000
)
func main() {
var wg sync.WaitGroup
ch := make(chan int, JOBS)
for i := 0; i < JOBS; i++ {
wg.Add(1)
go func() {
defer wg.Done()
sum := 0
for j := 0; j < ITERS; j++ {
sum++
}
ch <- sum
}()
}
go func() {
wg.Wait()
close(ch)
}()
total := 0
for v := range ch {
total += v
}
fmt.Printf("lang=go total=%d\n", total)
}
- goroutine: Go는 OS 스레드가 아닌 경량 스레드(goroutine) 사용
- WaitGroup: 고루틴 종료 대기
- 채널을 통해서 공유 메로리를 받는 게 아닌 메시지로 값을 받음
Java Code
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class Main {
static final int JOBS = 10;
static final int ITERS = 1_000_000;
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newFixedThreadPool(JOBS);
List<Future<Integer>> futures = new ArrayList<>();
for (int i = 0; i < JOBS; i++) {
futures.add(exec.submit(() -> {
int sum = 0;
for (int j = 0; j < ITERS; j++) {
sum++;
}
return sum;
}));
}
int total = 0;
for (Future<Integer> f : futures) {
total += f.get();
}
exec.shutdown();
System.out.printf("lang=java total=%d%n", total);
}
}
- ExecutorService: Java는 스레드를 직접 생성하지 않고 풀 기반으로 os 스레디를 미리 만들어둬 작업에 투입하는 방식
- Future: 종료 대기
- exec.shutdown(): 실행이 끝났음을 스레드 풀에 알림
Python Code
from concurrent.futures import ThreadPoolExecutor
JOBS = 10
ITERS = 1_000_000
def worker():
s = 0
for _ in range(ITERS):
s += 1
return s
def main():
with ThreadPoolExecutor(max_workers=JOBS) as ex:
total = sum(ex.map(lambda _: worker(), range(JOBS)))
print(f"lang=python total={total}")
if __name__ == "__main__":
main()
- ThreadPoolExecutor: Python의 스레드 풀(thread pool) 구현
- ex.map(): 스레드 풀의 Future 들을 자동으로 관리해줌
2-3 테스트 환경
도커로 테스트를 진행하였고 도커 환경은 다음과 같습니다
Ram : 2GB
CPU: 4개로 제한
이 도커 환경을 돌리는 제 pc는 다음과 같아요
CPU: AMD Ryzen 7 5800X 8-Core Processor
RAM: 32GB
언어 버전은 다음과 같아요.
C: gcc (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0
Rust: rustc 1.91.1 (ed61e7d7e 2025-11-07)
Go: go version go1.18.1 linux/amd64
Java
- openjdk version “11.0.28” 2025-07-15
- OpenJDK Runtime Environment (build 11.0.28+6-post-Ubuntu-1ubuntu122.04.1)
- OpenJDK 64-Bit Server VM (build 11.0.28+6-post-Ubuntu-1ubuntu122.04.1, mixed mode, sharing)
- Python: 3.10.12
다음은 이를 구성할 때 사용한 Dockerfile 입니다.
FROM ubuntu:22.04
# Install base packages and language runtimes/compilers
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
build-essential \
curl \
ca-certificates \
golang-go \
default-jdk \
python3 \
python3-pip \
time \
&& rm -rf /var/lib/apt/lists/*
# Install Rust using rustup (non-interactive)
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
WORKDIR /app
# Copy source code and benchmark script
COPY go ./go
COPY python ./python
COPY java ./java
COPY c ./c
COPY rust ./rust
COPY run_bench.sh ./run_bench.sh
# Remove CRLF and grant execute permission
RUN sed -i 's/\r$//' /app/run_bench.sh && chmod +x /app/run_bench.sh
# Directory for storing benchmark results
RUN mkdir -p /results
# Run benchmark script when container starts
CMD ["/app/run_bench.sh"]
그리고 각 코드들을 실행한 shell은 다음과 같습니다.
#!/usr/bin/env bash
set -euo pipefail
# How many times to run (configurable via env var, default: 3)
RUNS="${RUNS:-3}" # Number of runs (can be changed via env var)
OUT="/results/results.txt" # Output file path (inside container)
echo "========== LANGUAGE VERSIONS ==========" | tee -a "$OUT"
# Go
echo "[Go]" | tee -a "$OUT"
go version 2>/dev/null | tee -a "$OUT"
# Python
echo "[Python]" | tee -a "$OUT"
python3 --version 2>/dev/null | tee -a "$OUT"
# Java
echo "[Java]" | tee -a "$OUT"
java -version 2>&1 | tee -a "$OUT"
# C (GCC)
echo "[C / GCC]" | tee -a "$OUT"
gcc --version | head -n 1 | tee -a "$OUT"
# Rust
echo "[Rust]" | tee -a "$OUT"
rustc --version 2>/dev/null | tee -a "$OUT"
echo "" | tee -a "$OUT"
echo "==== Building binaries ===="
mkdir -p /app/bin
echo "[C] Building with gcc..."
gcc -O3 -pthread -o /app/bin/bench_c /app/c/main.c
echo "[Go] Building with go build..."
cd /app/go && go build -o /app/bin/bench_go main.go
echo "[Rust] Building with rustc..."
cd /app/rust && rustc -O main.rs -o /app/bin/bench_rust
echo "[Java] Compiling with javac..."
cd /app/java && javac Main.java
echo "[Python] Python version:"
python3 --version
cd /app
echo
echo "==== Running benchmarks (concurrent only, RUNS=$RUNS) ===="
echo "Results will be stored in $OUT."
echo > "$OUT"
bench() {
local label="$1"; shift
local cmd=("$@")
echo "============================" | tee -a "$OUT"
echo "BENCH: $label (runs=$RUNS)" | tee -a "$OUT"
echo "Command: ${cmd[*]}" | tee -a "$OUT"
for i in $(seq 1 "$RUNS"); do
tmpfile=$(mktemp)
start_ms=$(date +%s%3N)
/usr/bin/time -v "${cmd[@]}" 1>/dev/null 2>"$tmpfile"
end_ms=$(date +%s%3N)
elapsed_ms=$((end_ms - start_ms))
user_time_s=$(grep "User time (seconds)" "$tmpfile" | awk '{print $4}')
sys_time_s=$(grep "System time (seconds)" "$tmpfile" | awk '{print $4}')
max_rss_kb=$(grep "Maximum resident set size" "$tmpfile" | awk '{print $6}')
rm -f "$tmpfile"
user_time_ms=$(awk -v s="$user_time_s" 'BEGIN { printf "%.0f", s * 1000 }')
sys_time_ms=$(awk -v s="$sys_time_s" 'BEGIN { printf "%.0f", s * 1000 }')
printf 'RESULT label="%s" elapsed_ms=%d user_ms=%s sys_ms=%s max_rss_kb=%s\n' \
"$label" "$elapsed_ms" "$user_time_ms" "$sys_time_ms" "$max_rss_kb" | tee -a "$OUT"
done
}
bench "c concurrent" /app/bin/bench_c
bench "rust concurrent" /app/bin/bench_rust
bench "go concurrent" /app/bin/bench_go
bench "java concurrent" java -cp /app/java Main
bench "python concurrent" python3 /app/python/main.py
echo
echo "==== DONE ===="
echo "Full results saved at: $OUT"
2-4. 벤치마킹 값
벤치 마킹은 각 언어별로 동일한 코드를 10번 실행했습니다.
아래는 10번 돌리고 나온 지표입니다.
============================
BENCH: c concurrent (runs=10)
Command: /app/bin/bench_c
RESULT label="c concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=1696
RESULT label="c concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=1784
RESULT label="c concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=1728
RESULT label="c concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=1688
RESULT label="c concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=1716
RESULT label="c concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=1740
RESULT label="c concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=1764
RESULT label="c concurrent" elapsed_ms=3 user_ms=0 sys_ms=0 max_rss_kb=1688
RESULT label="c concurrent" elapsed_ms=3 user_ms=0 sys_ms=0 max_rss_kb=1804
RESULT label="c concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=1796
============================
BENCH: rust concurrent (runs=10)
Command: /app/bin/bench_rust
RESULT label="rust concurrent" elapsed_ms=5 user_ms=0 sys_ms=0 max_rss_kb=2208
RESULT label="rust concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=2316
RESULT label="rust concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=2332
RESULT label="rust concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=2296
RESULT label="rust concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=2328
RESULT label="rust concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=2312
RESULT label="rust concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=2280
RESULT label="rust concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=2164
RESULT label="rust concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=2236
RESULT label="rust concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=2332
============================
BENCH: go concurrent (runs=10)
Command: /app/bin/bench_go
RESULT label="go concurrent" elapsed_ms=5 user_ms=0 sys_ms=0 max_rss_kb=3420
RESULT label="go concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=3416
RESULT label="go concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=3428
RESULT label="go concurrent" elapsed_ms=5 user_ms=0 sys_ms=0 max_rss_kb=3428
RESULT label="go concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=3420
RESULT label="go concurrent" elapsed_ms=5 user_ms=0 sys_ms=0 max_rss_kb=3428
RESULT label="go concurrent" elapsed_ms=5 user_ms=0 sys_ms=0 max_rss_kb=3416
RESULT label="go concurrent" elapsed_ms=4 user_ms=0 sys_ms=0 max_rss_kb=3424
RESULT label="go concurrent" elapsed_ms=5 user_ms=0 sys_ms=0 max_rss_kb=3420
RESULT label="go concurrent" elapsed_ms=5 user_ms=0 sys_ms=0 max_rss_kb=3420
============================
BENCH: java concurrent (runs=10)
Command: java -cp /app/java Main
RESULT label="java concurrent" elapsed_ms=165 user_ms=470 sys_ms=30 max_rss_kb=42508
RESULT label="java concurrent" elapsed_ms=51 user_ms=70 sys_ms=0 max_rss_kb=44040
RESULT label="java concurrent" elapsed_ms=119 user_ms=270 sys_ms=50 max_rss_kb=44160
RESULT label="java concurrent" elapsed_ms=51 user_ms=50 sys_ms=20 max_rss_kb=42480
RESULT label="java concurrent" elapsed_ms=51 user_ms=80 sys_ms=0 max_rss_kb=42816
RESULT label="java concurrent" elapsed_ms=54 user_ms=70 sys_ms=0 max_rss_kb=44560
RESULT label="java concurrent" elapsed_ms=53 user_ms=60 sys_ms=20 max_rss_kb=44068
RESULT label="java concurrent" elapsed_ms=51 user_ms=70 sys_ms=10 max_rss_kb=42412
RESULT label="java concurrent" elapsed_ms=82 user_ms=160 sys_ms=10 max_rss_kb=44448
RESULT label="java concurrent" elapsed_ms=51 user_ms=60 sys_ms=10 max_rss_kb=42384
============================
BENCH: python concurrent (runs=10)
Command: python3 /app/python/main.py
RESULT label="python concurrent" elapsed_ms=432 user_ms=420 sys_ms=10 max_rss_kb=12248
RESULT label="python concurrent" elapsed_ms=426 user_ms=410 sys_ms=10 max_rss_kb=12376
RESULT label="python concurrent" elapsed_ms=437 user_ms=410 sys_ms=10 max_rss_kb=12072
RESULT label="python concurrent" elapsed_ms=429 user_ms=400 sys_ms=10 max_rss_kb=12440
RESULT label="python concurrent" elapsed_ms=430 user_ms=410 sys_ms=0 max_rss_kb=12172
RESULT label="python concurrent" elapsed_ms=440 user_ms=430 sys_ms=10 max_rss_kb=12392
RESULT label="python concurrent" elapsed_ms=430 user_ms=410 sys_ms=0 max_rss_kb=12220
RESULT label="python concurrent" elapsed_ms=425 user_ms=420 sys_ms=0 max_rss_kb=12228
RESULT label="python concurrent" elapsed_ms=422 user_ms=390 sys_ms=20 max_rss_kb=10684
RESULT label="python concurrent" elapsed_ms=433 user_ms=410 sys_ms=20 max_rss_kb=12044
2-5. 정리
다음 기준으로 정리했습니다.
- elapsed_ms: 실제로 얼만큼의 시간이 걸려서 끝났는 지
- user_ms: CPU 에서 얼마나 많은 리소스를 차지하는 지
- sys_ms: OS 커널에서 오버헤드 비용이 발생해서 별도의 시간이 소모 되는 지
- max_rss_kb: 최대 메모리 사용량으로 GC 힙이나 언어 런타임 무게, 스레드 수에 대한 지표
요약 테이블
| Language | avg_elapsed_ms | avg_user_ms | avg_sys_ms | avg_rss_kb |
|---|---|---|---|---|
| c | 3.8 | 0 | 0 | 1740.4 |
| rust | 4.1 | 0 | 0 | 2270.4 |
| go | 4.6 | 0 | 0 | 3422 |
| java | 72.8 | 136 | 15 | 43387.6 |
| python | 420.4 | 411 | 9 | 12117.6 |
평균 시간 경과 (avg_elapsed_ms)
- C, Rust, Go는 매우 낮은 평균 시간을 보여주지만 확실히 성능면에서 C가 제일 좋네요.
- Rust와 Go가 큰 차이가 발생하지 않는 다는 점과 이 정도의 차이면 좋은 PC에서는 차이가 더 미미할걸로 보입니다.
- 예상대로 파이썬은 매우 느리군요
- 평균 시간 경과 (avg_elapsed_ms)
평균 사용자 시간 및 시스템 시간 (user_ms, sys_ms)
- C, Rust, Go는 사용자 시간과 시스템 시간 모두 평균 0.0ms를 기록
- Java는 Raw 데이터를 보시면 알 수 있듯이 처음과 세번째에서 user_ms가 이상하게 튀는 현상이 발생하는 데 이는 JVM 부팅이나 GC 문제로 추측됩니다.
평균 최대 메모리 (max_rss_kb)
- C와 Rust는 메모리를 거의 비슷하게 점유하고 Go는 살짝 더 많이 먹는데 아마 GC와 고루틴 스케줄러로 인해 Rust 보다는 많이 먹습니다.
- Java는 JVM 으로 인해 메모리를 많이 먹을 수 밖에 없군요
처음에 꼬리물기 질문을 했던 것들에 답한다면
- 진짜 이게 장점일까?
C, Rust 보다는 느리지만 이는 유의미한 차이는 아니며 그 대신에 GoLang의 강력한 키워드들(go, chan, select, sync package)로 더 안전하게 동시성을 관리할 수 있다면 장점이다.
- 다른 언어에 비해 월등히 좋을까?
Go는 C, Rust 같은 네이티브 언어와 견줄 수 있는 성능을 지닌다.
- Rust도 좋다고들 하던데 Rust 보다도 좋을까?
당연하게도 Rust 보다는 메모리나 속도 면에서 느리다.
- Python은 느리다던데 유의미한 퍼포먼스 차이가 있을까?
Python은 진짜 느리다.
글의 목적은 Go의 장점을 정량적인 지표를 토대로 확인해보는 것이며 언어의 우위를 매기는 것이 아닙니다.
Go의 동시성은 강력하지만 항상 최고의 선택지는 아닐 것이며, 메모리 최적화가 매우 필요할 경우에는 Rust, C를 고려해봐야하고 생산성이 중요한 경우는 Python을, 탄탄한 생태계와 안정성을 원하면 Java를 상황에 선택해서 써야합니다.
💬 댓글