C++ vs Go | 성능·동시성·선택 가이드 완전 비교 [#47-1]

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.

개념을 잡는 비유

이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.


목차

  1. 문제 시나리오: 기술 선택 실패로 겪는 상황
  2. 동시성 모델 비교
  3. 컨텍스트 스위칭·메모리 비용
  4. 성능·트레이드오프
  5. 완전한 C++ vs Go 비교
  6. 자주 하는 실수
  7. 선택 가이드: 언제 무엇을 선택할지
  8. 프로덕션 패턴
  9. 정리 및 체크리스트

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_readasync_writeasync_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::threadGo 고루틴
초기 스택1~8MB (기본값)~2KB
확장고정 (설정으로 변경 가능)동적 증가
1만 개 시 스택 합계1080GB~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~150msC++ 유리
HTTP 요청 처리 (1만 QPS)비슷비슷구현에 따라
동시 연결 10만 (에코)Asio 필수고루틴 자연스러움Go 개발 용이
GC 정지없음수 ms 가능C++ 극저지연 유리

실전 비교: 에코 서버 (동시 연결 1만 개)

C++ Asio: 이벤트 루프 1개 + 스레드 풀 4~8개. 소켓 1만 개를 io_context에 등록하고, async_acceptasync_readasync_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/catcherror 반환, 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::atomicsync.Mutex, 채널
비동기 결과std::future, std::async채널, errgroup
리소스 정리RAII, 소멸자defer
에러 처리try/catch, optionalerror 반환
패키지/모듈#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++ 또는 GoCPU 바운드면 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고루틴·채널
메모리수동/RAIIGC
지연극저지연 가능GC 정지 있음
개발 속도상대적으로 느림상대적으로 빠름
배포의존성 복잡단일 바이너리

핵심 원칙:

  1. 지연·처리량·팀 역량을 먼저 분석한다.
  2. 극저지연이면 C++, 빠른 출시·운영 단순성이면 Go를 고려한다.
  3. 연결당 스레드는 피하고, C++은 Asio, Go는 고루틴을 활용한다.
  4. 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, 게임사


참고 자료


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • 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]