C++ vs Go | 성능·동시성·선택 가이드 완전 비교 [#47-1]
이 글의 핵심
C++ vs Go에 대한 실전 가이드입니다. 성능·동시성·선택 가이드 완전 비교 [#47-1] 등을 예제와 함께 상세히 설명합니다.
들어가며: “C++로 갈까, Go로 갈까” 고민되는 순간
왜 비교하는가
백엔드·서버 개발에서 C++과 Go는 모두 고성능·동시성을 내세우는 언어입니다. C++은 스레드·이벤트 루프(Asio) 로 제어권을 개발자가 쥐고, Go는 고루틴(Goroutine—Go 런타임이 관리하는 경량 스레드)·채널(Channel—고루틴 간 데이터 통신)로 수만 개 경량 태스크를 런타임이 스케줄합니다. 이 글은 실제 겪는 문제 시나리오, 완전한 비교표, 자주 하는 실수, 선택 가이드, 프로덕션 패턴까지 포함해 두 언어를 실전 관점에서 비교합니다.
이 글에서 다루는 것:
- 문제 시나리오: 기술 선택 실패로 겪는 실제 상황
- 동시성 모델: C++ 스레드·Asio vs Go 고루틴·M:N 스케줄링
- 완전한 비교: 성능·메모리·타입·에코시스템·빌드
- 자주 하는 실수: C++·Go 각각에서 피해야 할 패턴
- 선택 가이드: 언제 무엇을 선택할지
- 프로덕션 패턴: 실전에서 쓰는 설계 패턴
관련 글: C++ 실전 가이드 #7 스레드, C++ 개발자의 뇌 구조로 이해하는 Go.
개념을 잡는 비유
이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.
목차
- 문제 시나리오: 기술 선택 실패로 겪는 상황
- 동시성 모델 비교
- 컨텍스트 스위칭·메모리 비용
- 성능·트레이드오프
- 완전한 C++ vs Go 비교
- 자주 하는 실수
- 선택 가이드: 언제 무엇을 선택할지
- 프로덕션 패턴
- 정리 및 체크리스트
1. 문제 시나리오: 기술 선택 실패로 겪는 상황
기술 선택을 잘못하면 아래와 같은 문제가 발생합니다.
flowchart LR
subgraph Mismatch["기술 선택 불일치"]
A[요구사항] -->|선택| B[언어/스택]
B -->|기대| C[특성]
A -.->|불일치| C
C --> D[성능 문제/개발 지연/유지보수 어려움]
end
시나리오 1: “C++로 웹 API를 만들다가 개발 속도가 밀렸다”
상황: 스타트업에서 “성능이 중요하다”며 C++로 REST API 서버를 만들었습니다. 연결당 스레드 모델로 설계했더니 동시 연결 1만 개에서 메모리 80GB를 넘었고, JSON 파싱·HTTP 파서·ORM을 직접 구현하다 출시 일정이 3개월 밀렸습니다.
원인: 웹·API 서버는 개발 속도·운영 단순성이 우선입니다. C++의 극한 성능은 필요하지만, 대부분의 CRUD·마이크로서비스는 Go·Node.js로 충분합니다. C++은 네트워크 스택·파싱·ORM 등 인프라를 직접 쌓아야 해서 초기 개발 비용이 큽니다.
해결 방향: “성능이 중요하다”는 요구가 나노초 단위 지연인지, 처리량인지 구분합니다. 수 ms 수준이면 Go·Rust가 더 적합합니다. C++은 게임 서버·HFT·임베디드처럼 극한 제어가 필요한 도메인에 집중합니다.
시나리오 2: “Go로 HFT 시스템을 만들다가 GC 지연이 문제됐다”
상황: 금융사에서 “Go가 동시성에 좋다”며 주문 라우팅 시스템을 Go로 구현했습니다. 피크 시간에 GC(가비지 컬렉션) 정지가 수 ms 발생해, “나노초 단위 지연” 요구를 충족하지 못했습니다.
원인: Go의 GC는 수 ms~수십 ms 수준의 정지를 가질 수 있습니다. HFT·저지연 트레이딩에서는 GC 정지가 없는 C++·Rust가 필수입니다. Go는 웹·API·마이크로서비스에 적합하고, 극저지연에는 맞지 않습니다.
해결 방향: 지연 요구가 마이크로초 이하이면 C++·Rust를 선택합니다. Go는 수 ms 수준의 API·배치 작업에 적합합니다.
시나리오 3: “연결당 스레드로 10만 동시 접속을 처리하려다 OOM이 났다”
상황: C++로 채팅 서버를 만들었습니다. 연결당 std::thread 하나씩 할당했더니, 동시 접속 1만 개에서 스레드 스택만 80GB를 넘어 OOM(Out of Memory)이 발생했습니다.
원인: OS 스레드는 스레드당 1~8MB 스택을 기본으로 가집니다. 10만 스레드 = 수백 GB 메모리. “연결당 스레드” 모델은 현실적이지 않습니다.
해결 방향: C++에서는 Asio 이벤트 루프로 소수 스레드만 두고, 수만 개 소켓을 논블로킹 I/O로 처리합니다. Go는 “연결당 고루틴”이 자연스럽고, 고루틴당 수 KB 스택으로 10만 개도 가능합니다.
시나리오 4: “Go에서 CPU 바운드 작업을 고루틴으로 쏟다가 느려졌다”
상황: Go로 이미지 리사이징 서비스를 만들었습니다. 요청당 고루틴 하나씩 생성해 CPU 집약 작업을 수행했더니, GOMAXPROCS(기본값 = CPU 코어 수)만큼만 병렬로 돌고 나머지는 대기했습니다. 처리량이 코어 수에 묶여 있었습니다.
원인: Go 고루틴은 M:N 스케줄링으로 OS 스레드 수는 보통 CPU 코어 수 수준입니다. CPU 바운드 작업은 고루틴을 아무리 많이 만들어도 실제 병렬 실행은 코어 수에 제한됩니다. I/O 바운드일 때 고루틴이 유리합니다.
해결 방향: CPU 바운드 작업은 워커 풀 패턴으로 코어 수만큼 고루틴을 두고 작업을 분배합니다. 또는 C++로 해당 부분만 네이티브 라이브러리로 분리해 cgo로 호출할 수 있습니다(다만 cgo 오버헤드 주의).
시나리오 5: “C++ Asio로 구현하다가 콜백 지옥에 빠졌다”
상황: C++ Asio로 HTTP 서버를 만들었습니다. 비동기 콜백이 중첩되면서 async_read → async_write → async_read 체인이 5단계 이상 이어졌고, 에러 처리·타임아웃 로직이 흩어져 유지보수가 어려워졌습니다.
원인: C++ Asio는 콜백 기반이라 복잡한 비동기 흐름에서 가독성이 떨어집니다. Go의 go func() + 채널은 동기 스타일로 비동기를 작성할 수 있어 코드가 단순합니다.
해결 방향: C++20 coroutines 또는 서드파티 코루틴 라이브러리를 검토합니다. 또는 해당 서비스가 I/O 바운드 위주라면 Go로 마이크로서비스 분리를 고려합니다.
시나리오 6: “Go로 빌드했는데 바이너리가 50MB가 넘었다”
상황: Go로 CLI 도구를 만들었습니다. go build 한 번에 단일 바이너리가 나오는 것은 좋았는데, 정적 링크된 런타임·표준 라이브러리 때문에 바이너리 크기가 15~30MB를 넘었습니다. 임베디드·람다에 배포하기에 부담이 됐습니다.
원인: Go는 기본적으로 정적 링크로 모든 의존성을 바이너리에 포함합니다. -ldflags="-s -w"로 스트립해도 런타임·GC·스케줄러는 포함됩니다.
해결 방향: 바이너리 크기가 극도로 중요하면 C++·Rust로 정적 링크 시 훨씬 작은 크기를 얻을 수 있습니다. Go는 -trimpath·-ldflags="-s -w"로 최소화한 뒤, 필요 시 UPX 압축을 검토합니다.
시나리오 7: “C++ 빌드가 30분 걸려서 CI가 터졌다”
상황: C++ 프로젝트에서 헤더 하나 수정할 때마다 전체 리빌드에 20~30분이 걸렸습니다. CI 파이프라인에서 매 커밋마다 풀 빌드를 돌리다 시간 제한에 걸렸습니다.
원인: C++은 헤더 의존성이 강합니다. 널리 쓰이는 헤더(<iostream>, Boost 등)를 포함하면 컴파일 단위가 커지고, 템플릿은 헤더에 구현이 있어 인스턴스화 비용이 큽니다.
해결 방향: PCH(Precompiled Header), 모듈(C++20), 빌드 캐시(ccache), 증분 빌드 최적화를 적용합니다. 또는 마이크로서비스로 분리해 변경이 잦은 부분만 작은 단위로 빌드합니다. Go는 패키지 단위 증분 빌드가 빠른 편입니다.
2. 동시성 모델 비교
C++: OS 스레드 + 이벤트 루프
- std::thread: OS 스레드 1:1 매핑. 스레드 생성/파괴 비용이 크고, 스레드당 스택(보통 1~8MB)을 가집니다. 수만 개 동시 연결을 스레드 하나당 하나로 만들기는 부담이 큽니다.
- Asio: 한 스레드(또는 소수 스레드) 가 이벤트 루프를 돌면서 수만 개의 소켓을 논블로킹 I/O로 처리합니다. 완료 핸들러가 스레드 풀에 분산되므로, “연결 수”에 비해 스레드 수는 적게 유지할 수 있습니다.
- 제어: 스케줄링·메모리·락을 개발자가 직접 설계합니다. 복잡도는 높지만, 지연·처리량을 세밀하게 조정할 수 있습니다.
flowchart TB
subgraph Cpp["C++ 모델"]
T1[OS 스레드 1]
T2[OS 스레드 2]
T3[OS 스레드 N]
E[이벤트 루프]
S[소켓 1만 개]
E --> S
T1 --> E
T2 --> E
T3 --> E
end
실행 가능 예제 (C++ 스레드 최소):
// 복사해 붙여넣은 뒤: g++ -std=c++17 -pthread -o cpp_concurrent cpp_concurrent.cpp && ./cpp_concurrent
#include <iostream>
#include <thread>
int main() {
std::thread t([]{ std::cout << "C++ OS thread\n"; });
std::cout << "main\n";
t.join();
return 0;
}
Go: 고루틴(Goroutine) + M:N 스케줄링
- 고루틴: 경량 코루틴. 수 KB 단위의 스택으로 시작해 필요 시 확장. 수만·수십만 개를 만들어도 OS 스레드 수는 보통 CPU 코어 수 수준으로 유지됩니다.
- M:N: 고루틴 N개가 OS 스레드(M개)에 매핑되어 스케줄됩니다. 컨텍스트 스위칭은 유저 공간에서 이루어져, OS 스레드 전환보다 비용이 적습니다.
- 채널: 고루틴 간 통신·동기화를 채널로 하는 관용구. “공유 메모리+락” 대신 “메시지 전달” 스타일을 권장합니다.
flowchart TB
subgraph Go["Go 모델"]
M1[OS 스레드 1]
M2[OS 스레드 2]
G1[고루틴 1]
G2[고루틴 2]
G3[고루틴 ...]
GN[고루틴 10만]
M1 --> G1
M1 --> G2
M2 --> G3
M2 --> GN
end
실행 가능 예제 (Go 고루틴):
// go run main.go
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Go goroutine")
}()
fmt.Println("main")
wg.Wait()
}
동시성 코드 비교: 같은 작업, 다른 스타일
작업: 10개의 URL을 동시에 요청하고 결과를 모은다.
C++ (std::async):
#include <future>
#include <vector>
#include <string>
std::vector<std::string> fetchAll(const std::vector<std::string>& urls) {
std::vector<std::future<std::string>> futures;
for (const auto& url : urls) {
futures.push_back(std::async(std::launch::async, [url]() {
return fetchUrl(url); // HTTP 요청
}));
}
std::vector<std::string> results;
for (auto& f : futures) {
results.push_back(f.get());
}
return results;
}
Go (고루틴 + 채널):
func fetchAll(urls []string) []string {
results := make([]string, len(urls))
var wg sync.WaitGroup
for i, url := range urls {
wg.Add(1)
go func(idx int, u string) {
defer wg.Done()
results[idx] = fetchURL(u)
}(i, url)
}
wg.Wait()
return results
}
차이점: C++은 std::future로 결과를 모으고, Go는 sync.WaitGroup으로 동기화합니다. Go는 인덱스 i를 인자로 넘겨 클로저 캡처 문제를 피합니다.
한 줄 비교
- C++: 스레드·이벤트 루프를 직접 설계. 제어권 최대, 복잡도 높음.
- Go: 고루틴을 많이 만들고 런타임이 스케줄. 개발 난이도 낮고, I/O 바운드·동시 연결 많을 때 유리.
3. 컨텍스트 스위칭·메모리 비용
C++ 스레드
- 스택: 스레드당 보통 1~8MB(기본값). 스레드 1만 개면 스택만 수십 GB 단위가 될 수 있어, 연결당 스레드 모델은 현실적이지 않습니다.
- 컨텍스트 스위칭: OS 커널이 개입. 캐시 미스·TLB 플러시 등으로 마이크로초 단위 비용이 나올 수 있습니다. 스레드 수가 많을수록 스위칭 비용이 커집니다.
Go 고루틴
- 스택: 초기 수 KB. 필요 시 증가. 고루틴 10만 개라도 스택 합계는 C++ 스레드 10만 개보다 훨씬 작습니다.
- 스위칭: 유저 공간 스케줄러가 전환. 커널 모드 전환이 없어 나노초~마이크로초 단위로 스레드 전환보다 가볍습니다. I/O 대기 시 해당 고루틴은 블록되고 다른 고루틴이 실행되므로, “연결당 고루틴” 패턴이 자연스럽습니다.
스택 크기 비교 (개념적)
| 항목 | C++ std::thread | Go 고루틴 |
|---|---|---|
| 초기 스택 | 1~8MB (기본값) | ~2KB |
| 확장 | 고정 (설정으로 변경 가능) | 동적 증가 |
| 1만 개 시 스택 합계 | ~20MB 수준 | |
| 10만 개 | 현실적이지 않음 | ~200MB 수준 |
정리
- 동시 연결이 많고 I/O 대기 비중이 크면: 고루틴이 메모리·스위칭 비용 면에서 유리합니다.
- CPU 바운드·지연 극소화가 목표면: C++에서 스레드 수를 조절하고, 이벤트 루프·lock-free 등으로 세밀하게 튜닝하는 편이 맞을 수 있습니다.
4. 성능·트레이드오프
CPU 바운드
- C++: 네이티브 코드, 컴파일 타임 최적화, 캐시·메모리 레이아웃을 직접 제어할 수 있어 순수 연산에서는 일반적으로 Go보다 유리합니다. 스레드 수 = 코어 수 근처로 맞추면 스위칭을 줄일 수 있습니다.
- Go: GC·런타임 오버헤드가 있고, 최적화 한계가 있어 극한의 CPU 성능이 필요하면 C++이 유리합니다.
I/O 바운드·많은 동시 연결
- C++: Asio로 이벤트 루프 + 소수 스레드만 쓰면 메모리·스위칭을 절약할 수 있습니다. 다만 콜백·Strand 설계 등 구현 부담이 있습니다.
- Go: 연결당 고루틴 하나만 만들어도 되고, 블로킹 I/O처럼 쓰면 됩니다. 코드가 단순하고, 대부분의 웹·API 서버 수준에서는 성능이 충분한 경우가 많습니다.
지연(Latency)
- 극저지연(마이크로초 이하)이 필요하면: C++에서 스레드·메모리·스케줄링을 통제하는 편이 유리합니다. GC 정지 구간이 없는 것도 장점입니다.
- 수 ms~수십 ms 수준이면: Go도 적절한 튜닝으로 목표를 달성할 수 있습니다.
벤치마크 예시 (개념적)
| 작업 유형 | C++ | Go | 비고 |
|---|---|---|---|
| 순수 연산 (1e9 회산) | ~100ms | ~150ms | C++ 유리 |
| HTTP 요청 처리 (1만 QPS) | 비슷 | 비슷 | 구현에 따라 |
| 동시 연결 10만 (에코) | Asio 필수 | 고루틴 자연스러움 | Go 개발 용이 |
| GC 정지 | 없음 | 수 ms 가능 | C++ 극저지연 유리 |
실전 비교: 에코 서버 (동시 연결 1만 개)
C++ Asio: 이벤트 루프 1개 + 스레드 풀 4~8개. 소켓 1만 개를 io_context에 등록하고, async_accept → async_read → async_write 체인으로 처리. 메모리: 스레드 8개 × 8MB + 소켓 버퍼 ≈ 수십 MB.
Go: 연결당 go handleConn(conn) 하나. 1만 개 고루틴 = 스택 합계 ~20MB. net.Conn을 블로킹 I/O처럼 사용. 코드가 단순하고, 런타임이 스케줄링.
결론: 둘 다 1만 동시 연결을 처리할 수 있지만, Go는 “연결당 고루틴”이 관용구라 구현이 빠릅니다. C++은 세밀한 제어가 가능하지만 설계·구현 부담이 있습니다.
5. 완전한 C++ vs Go 비교
종합 비교표
| 항목 | C++ | Go |
|---|---|---|
| 메모리 관리 | 수동/RAII/스마트 포인터 | GC |
| 동시성 모델 | std::thread, Asio | 고루틴, 채널 |
| 타입 시스템 | 정적, 강타입, 템플릿 | 정적, 강타입, 인터페이스 |
| 제네릭 | 템플릿 (컴파일 타임) | 제네릭 (Go 1.18+) |
| 예외 | try/catch | error 반환, panic/recover |
| 빌드 | 느림 (헤더 의존성) | 빠름 (패키지 단위) |
| 바이너리 | 정적/동적 링크 | 단일 바이너리 (정적 링크) |
| 배포 | 의존성 관리 복잡 | go build 한 번 |
| 학습 곡선 | 높음 | 상대적으로 낮음 |
| 에코시스템 | Boost, Qt, 수많은 라이브러리 | 표준 라이브러리 풍부, go get |
| 성능 | 극한 제어 가능 | 대부분 충분 |
| 지연 | GC 없음, 나노초 제어 | GC 정지 가능 |
타입·메모리 비교
C++:
- 명시적 메모리 제어.
new/delete또는 스마트 포인터. - RAII로 리소스 수명 관리.
- 템플릿으로 컴파일 타임 다형성.
Go:
- GC가 메모리 회수. 개발자는 할당만 신경 씀.
defer로 정리 로직.- 인터페이스로 런타임 다형성.
에코시스템 비교
C++: Boost.Asio, nlohmann/json, spdlog, gRPC, Protobuf 등. 빌드 시스템(CMake, vcpkg, Conan)이 필요합니다.
Go: net/http, encoding/json, log, context 등 표준 라이브러리가 풍부합니다. go get으로 의존성 추가가 간단합니다.
문법·관용구 퀵 레퍼런스
| 기능 | C++ | Go |
|---|---|---|
| 동시 실행 | std::thread t(f); t.join(); | go f() |
| 동기화 | std::mutex, std::atomic | sync.Mutex, 채널 |
| 비동기 결과 | std::future, std::async | 채널, errgroup |
| 리소스 정리 | RAII, 소멸자 | defer |
| 에러 처리 | try/catch, optional | error 반환 |
| 패키지/모듈 | #include, 네임스페이스 | import, 패키지 |
에러 처리 비교
C++: try/catch 예외 또는 std::expected(C++23), 에러 코드 반환. 예외 비용을 피하려면 에러 코드·optional 패턴을 씁니다.
// C++: 예외 또는 에러 코드
std::optional<int> parse(const std::string& s) {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt;
}
}
Go: error 반환. if err != nil 체크가 관용구입니다.
// Go: error 반환
func parse(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
return n, nil
}
빌드·배포 비교
| 항목 | C++ | Go |
|---|---|---|
| 빌드 시간 | 느림 (헤더·템플릿) | 빠름 (패키지 증분) |
| 의존성 관리 | vcpkg, Conan, 시스템 패키지 | go.mod, go.sum |
| 크로스 컴파일 | 툴체인별 설정 | GOOS, GOARCH 환경 변수 |
| 배포 | 동적 라이브러리 의존성 주의 | 단일 바이너리 |
| Docker 이미지 | 베이스 + 빌드 도구 | 스크래치 + 바이너리만 가능 |
6. 자주 하는 실수
C++ 쪽 실수
실수 1: 연결당 스레드 모델
// ❌ 나쁜 예: 연결 1만 개 = 스레드 1만 개
void handle_client(int fd) {
std::thread([fd]() {
// 스레드당 1~8MB 스택 → 1만 개 = 10~80GB
process_request(fd);
}).detach();
}
해결: Asio 이벤트 루프로 소수 스레드만 사용합니다.
// ✅ 좋은 예: 이벤트 루프 + 스레드 풀
boost::asio::io_context ioc;
// 스레드 수 = CPU 코어 수
for (int i = 0; i < std::thread::hardware_concurrency(); ++i) {
std::thread([&ioc]() { ioc.run(); }).detach();
}
// 수만 개 소켓을 ioc에 등록
실수 2: shared_ptr 남용으로 지연 증가
// ❌ 나쁜 예: 모든 객체에 shared_ptr
void process(std::shared_ptr<Request> req) {
auto resp = std::make_shared<Response>(); // atomic 참조 카운트
// ...
}
해결: 소유권이 한 곳에만 있으면 unique_ptr, 공유가 꼭 필요할 때만 shared_ptr을 사용합니다.
실수 3: 뮤텍스 없이 공유 변수 접근
// ❌ 나쁜 예: data race
int counter = 0;
std::thread t1([&]() { ++counter; });
std::thread t2([&]() { ++counter; });
해결: std::mutex 또는 std::atomic을 사용합니다.
Go 쪽 실수
실수 1: CPU 바운드 작업을 고루틴으로 무한 생성
// ❌ 나쁜 예: 고루틴 1만 개가 CPU 작업 → 스케줄링 오버헤드만 증가
for i := 0; i < 10000; i++ {
go cpuHeavyTask()
}
해결: 워커 풀 패턴으로 코어 수만큼만 고루틴을 두고 작업을 분배합니다.
// ✅ 좋은 예: 워커 풀
jobs := make(chan int, 100)
for w := 0; w < runtime.NumCPU(); w++ {
go func() {
for j := range jobs {
cpuHeavyTask(j)
}
}()
}
실수 2: 채널 닫기 누락으로 고루틴 누수
// ❌ 나쁜 예: 채널을 닫지 않아 range가 영원히 대기
ch := make(chan int)
go func() {
for v := range ch { // ch가 닫히지 않으면 영원히 대기
process(v)
}
}()
해결: 송신 측에서 작업 완료 시 close(ch)를 호출합니다.
실수 3: 고루틴에 슬라이스/맵 포인터 전달
// ❌ 나쁜 예: 같은 슬라이스 참조로 data race
for i := 0; i < 10; i++ {
go func() {
process(items[i]) // i는 루프 변수, 클로저에서 공유
}()
}
해결: 값으로 복사하거나 인자로 전달합니다.
// ✅ 좋은 예
for i := 0; i < 10; i++ {
go func(idx int) {
process(items[idx])
}(i)
}
실수 4: nil 채널에 송수신
// ❌ 나쁜 예: nil 채널은 영원히 블록
var ch chan int // nil
ch <- 1 // 데드락
<-ch // 데드락
해결: make(chan int)로 채널을 초기화한 뒤 사용합니다.
실수 5: 버퍼 없는 채널로 단방향 전달 시 데드락
// ❌ 나쁜 예: 수신자가 없으면 송신자가 블록
ch := make(chan int) // 버퍼 0
ch <- 1 // 수신자가 없으면 여기서 블록
해결: 버퍼를 두거나, 수신 고루틴을 먼저 실행합니다. go func() { <-ch }() 후 ch <- 1처럼요.
C++·Go 공통 실수
실수 6: 데드락 (상호 대기)
C++: 뮤텍스 A, B를 스레드 1은 A→B, 스레드 2는 B→A 순서로 잡으면 데드락.
Go: 채널 2개를 서로 대기하면 데드락.
해결: 뮤텍스는 항상 같은 순서로 잡기, 채널은 단방향 전달·select로 타임아웃 처리.
7. 선택 가이드: 언제 무엇을 선택할지
의사결정 플로우
flowchart TD
A[요구사항 분석] --> B{지연 요구?}
B -->|마이크로초 이하| C[C++]
B -->|수 ms 이상| D{동시 연결 수?}
D -->|수만 개 이상| E{개발 속도 우선?}
E -->|예| F[Go]
E -->|아니오| G[C++ Asio]
D -->|수천 개| H{팀 역량?}
H -->|C++ 숙련| I[C++]
H -->|다양| J[Go]
목표별 선택표
| 목표 | C++ | Go |
|---|---|---|
| 극한 CPU 성능·저지연 | ✅ | △ |
| 수만 동시 연결·개발 속도 | △(설계 필요) | ✅ |
| 메모리·스레드 수 제어 | ✅(이벤트 루프) | ✅(고루틴) |
| 팀 학습 곡선·유지보수 | 상대적으로 무거움 | 상대적으로 가벼움 |
| 단일 바이너리 배포 | △(정적 링크 설정) | ✅ |
| 기존 C/C++ 인프라 활용 | ✅ | △(cgo) |
도메인별 추천
- C++을 선택하는 경우: 게임 서버·HFT·임베디드·기존 C++ 인프라가 있는 서비스, “나노초 단위 제어”가 필요한 경우.
- Go를 선택하는 경우: 웹·API·마이크로서비스·빠른 출시·운영 단순성이 중요할 때.
마이그레이션 고려사항
C++ → Go: 기존 C++ 라이브러리를 cgo로 호출할 수 있지만, cgo 호출 비용·GC 정지 시 블록 가능성을 고려합니다. 점진적 마이그레이션은 gRPC·HTTP로 서비스를 나누어 C++ 코어와 Go API를 분리하는 방식이 일반적입니다.
Go → C++: 극한 성능·극저지연이 필요해질 때. 해당 모듈만 C++로 재작성하고 나머지는 Go로 유지하는 하이브리드가 현실적입니다.
팀 역량·채용 관점
- C++: 숙련자 채용이 어렵고, 학습 곡선이 높습니다. 레거시·게임·금융 도메인에서 수요가 있습니다.
- Go: 상대적으로 배우기 쉽고, 클라우드·마이크로서비스·DevOps 도메인에서 수요가 많습니다. 채용 풀이 넓은 편입니다.
실전 적용 시나리오 요약
| 시나리오 | 권장 언어 | 이유 |
|---|---|---|
| REST API·마이크로서비스 | Go | 빠른 개발, 표준 라이브러리, 단일 바이너리 |
| 게임 서버 (CCU 10만+) | C++ | 메모리·지연 제어, 기존 엔진 연동 |
| HFT·저지연 트레이딩 | C++ | GC 없음, 나노초 제어 |
| Kubernetes·Docker 도구 | Go | 커뮤니티·에코시스템 |
| 이미지·비디오 처리 파이프라인 | C++ 또는 Go | CPU 바운드면 C++, I/O 위주면 Go |
| 채팅·실시간 알림 서버 | Go | 연결당 고루틴, 개발 용이 |
| 임베디드·IoT 에지 | C++ | 리소스 제약, 직접 제어 |
8. 프로덕션 패턴
C++ 프로덕션 패턴
패턴 1: Asio + 스레드 풀
// io_context를 여러 스레드에서 실행
boost::asio::io_context ioc;
boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);
signals.async_wait([&](auto, auto) { ioc.stop(); });
std::vector<std::thread> threads;
for (unsigned i = 0; i < std::thread::hardware_concurrency(); ++i) {
threads.emplace_back([&ioc]() { ioc.run(); });
}
// 모든 스레드 조인
for (auto& t : threads) t.join();
패턴 2: Strand로 순차 처리
// 같은 연결 내에서 핸들러 순서 보장
auto strand = boost::asio::make_strand(ioc);
boost::asio::async_read(socket, buffer, boost::asio::bind_executor(strand, {
// 이 핸들러는 strand 내에서 순차 실행
}));
패턴 3: 메모리 풀 + 커스텀 할당자
// 고빈도 할당/해제 구간에서 메모리 풀 사용
template<typename T>
using pool_alloc = boost::pool_allocator<T>;
std::vector<int, pool_alloc<int>> vec;
Go 프로덕션 패턴
패턴 1: context로 취소·타임아웃
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
// 타임아웃 처리
}
}
defer resp.Body.Close()
패턴 2: errgroup으로 고루틴 그룹
g, ctx := errgroup.WithContext(ctx)
for _, url := range urls {
url := url
g.Go(func() error {
resp, err := fetch(ctx, url)
if err != nil {
return err
}
return process(resp)
})
}
if err := g.Wait(); err != nil {
return err
}
패턴 3: worker pool
func worker(id int, jobs <-chan Job, results chan<- Result) {
for j := range jobs {
results <- process(j)
}
}
jobs := make(chan Job, 100)
results := make(chan Result, 100)
for w := 0; w < runtime.NumCPU(); w++ {
go worker(w, jobs, results)
}
// jobs에 작업 전달, results에서 수집
하이브리드 아키텍처
- C++ 코어 + Go API 레이어: 극한 성능이 필요한 코어(예: HFT 엔진)는 C++로, REST/gRPC API는 Go로 구현. gRPC·HTTP로 연동.
- Go 마이크로서비스 + C++ 라이브러리: Go 서비스가 cgo로 C++ 라이브러리 호출. 다만 cgo 오버헤드와 GC 정지 시 C++ 스레드 블록 가능성을 고려합니다.
패턴 4: C++ - RAII로 리소스 관리
class Connection {
boost::asio::ip::tcp::socket socket_;
public:
Connection(boost::asio::io_context& ioc) : socket_(ioc) {}
~Connection() {
boost::system::error_code ec;
socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
}
// 소멸 시 자동으로 소켓 정리
};
패턴 5: Go - defer로 정리
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // 함수 반환 시 자동 실행
// ... 처리
return nil
}
패턴 6: C++ - 스레드 로컬 스토리지
thread_local std::mt19937 rng(std::random_device{}());
// 스레드마다 독립적인 RNG
패턴 7: Go - sync.Once로 초기화
var once sync.Once
var config *Config
func getConfig() *Config {
once.Do(func() {
config = loadConfig()
})
return config
}
패턴 8: Go - 채널 vs 뮤텍스 선택
채널: 고루틴 간 데이터 전달이 주 목적일 때. “메시지 전달” 스타일. select로 다중 채널 대기 가능.
// 채널: 생산자-소비자
ch := make(chan int, 10)
go producer(ch)
go consumer(ch)
뮤텍스: 공유 상태를 보호할 때. 캐시·맵·카운터 등.
// 뮤텍스: 공유 상태 보호
var mu sync.Mutex
var cache map[string]string
func get(key string) string {
mu.Lock()
defer mu.Unlock()
return cache[key]
}
Go 격언: “Don’t communicate by sharing memory; share memory by communicating.” — 채널을 우선 고려하고, 꼭 필요할 때만 뮤텍스를 사용합니다.
패턴 9: C++ - atomic으로 lock-free 카운터
std::atomic<int> counter{0};
// 락 없이 스레드 안전 증가
counter.fetch_add(1, std::memory_order_relaxed);
패턴 10: C++ - shared_mutex로 읽기 다중 접근
std::shared_mutex mtx;
// 읽기는 여러 스레드 동시, 쓰기는 배타
std::shared_lock read_lock(mtx); // 읽기
std::unique_lock write_lock(mtx); // 쓰기
9. 정리 및 체크리스트
핵심 요약
| 항목 | C++ | Go |
|---|---|---|
| 동시성 | 스레드·Asio | 고루틴·채널 |
| 메모리 | 수동/RAII | GC |
| 지연 | 극저지연 가능 | GC 정지 있음 |
| 개발 속도 | 상대적으로 느림 | 상대적으로 빠름 |
| 배포 | 의존성 복잡 | 단일 바이너리 |
핵심 원칙:
- 지연·처리량·팀 역량을 먼저 분석한다.
- 극저지연이면 C++, 빠른 출시·운영 단순성이면 Go를 고려한다.
- 연결당 스레드는 피하고, C++은 Asio, Go는 고루틴을 활용한다.
- CPU 바운드는 워커 풀, I/O 바운드는 고루틴/이벤트 루프가 적합하다.
기술 선택 체크리스트
- 지연 요구가 마이크로초 이하인가? → C++ 검토
- 동시 연결이 수만 개 이상인가? → Go 고루틴 또는 C++ Asio
- 팀에 C++ 숙련자가 있는가? → 없으면 Go 우선
- 기존 C++ 인프라가 있는가? → C++ 유지 검토
- 단일 바이너리 배포가 중요한가? → Go 유리
- GC 정지가 허용되지 않는가? → C++
성능 측정·프로파일링 팁
C++: perf, vtune, gprof로 CPU 프로파일링. valgrind로 메모리 누수·data race 검사. std::chrono::high_resolution_clock으로 나노초 단위 지연 측정.
Go: go tool pprof로 CPU·메모리 프로파일링. -race 플래그로 data race 검사. runtime/debug로 GC 통계 확인.
// Go: GC 통계
import "runtime/debug"
var stats debug.GCStats
debug.ReadGCStats(&stats)
// stats.PauseTotal, stats.NumGC 등
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 서버·백엔드 기술 선택, 마이크로서비스 아키텍처 설계, 동시성 모델 비교 시 C++ vs Go 선택 가이드를 참고하세요. 위 본문의 문제 시나리오·선택 가이드·프로덕션 패턴을 활용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference, Go 공식 문서, Effective Go를 참고하세요. Boost.Asio, Go net 패키지 문서도 활용하면 좋습니다.
Q. C++와 Go를 함께 사용할 수 있나요?
A. 가능합니다. gRPC·HTTP로 연동하는 하이브리드 아키텍처가 일반적입니다. C++ 코어(엔진·HFT) + Go API 레이어 조합이 많이 쓰입니다. cgo로 Go에서 C++ 호출도 가능하지만, 호출 비용·GC 정지 시 블록 가능성을 고려해야 합니다.
다음 단계
- C++ 심화: C++ 스레드 기초, Data Race·Mutex·Atomic
- Go 입문: C++ 개발자의 뇌 구조로 이해하는 Go
- 시스템 설계: C++ 시스템 디자인
한 줄 요약: C++와 Go의 성능·동시성 모델 차이를 알면 기술 선택이 명확해집니다. 다음으로 Go 입문(#47-2)를 읽어보면 좋습니다.
다음 글: [C++ vs 타 언어 #47-2] C++ 개발자의 뇌 구조로 이해하는 Go 언어
이전 글: [C++ 면접·시스템 설계 #46-3] 회사·도메인별 C++ 요구 역량 차이: 네카라쿠배, 금융/HFT, 게임사
참고 자료
- cppreference - Thread support library
- Boost.Asio 공식 문서
- Go 공식 문서 - Concurrency
- Effective Go
- Go Blog - Share Memory By Communicating
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 개발자를 위한 2주 완성 Go 커리큘럼 · Go 시리즈 전체 목차
- [Go 심화 #09] context·타임아웃·우아한 종료 — Go 실무 패턴
- C++ std::thread 입문 | join 누락·디태치 남용 등 자주 하는 실수 3가지와 해결법
- C++ 개발자의 뇌 구조로 이해하는 Go 언어 [#47-2]
- C++ 도메인별 요구 역량 | 네카라쿠배·금융·게임 [#46-3]
이 글에서 다루는 키워드 (관련 검색어)
C++ vs Go, 언어 비교, 고루틴, 동시성, 성능 비교, Asio, 채널 등으로 검색하시면 이 글이 도움이 됩니다.
관련 글
- C++ 개발자의 뇌 구조로 이해하는 Go 언어 [#47-2]
- C++ vs Rust 완전 비교 | 소유권·메모리 안전성·에러 처리·동시성·성능 실전 가이드
- Rust vs C++ 메모리 안전성 | 컴파일러 오류 차이 [#47-3]
- Rust 메모리 안전성 완벽 가이드 | 소유권·Borrow checker·수명·unsafe 실전
- C++ Redis 클론 | Modern C++ 인메모리 KV 스토어 [#48-1]