C++ 도메인별 요구 역량: 네카라쿠배·금융(HFT)·게임 서버·임베디드 비교
들어가며: 도메인마다 “넘으면 안 되는 선”이 다르다
“C++을 쓴다”고 해도 웹·백엔드, 금융·HFT, 게임 서버, 임베디드에서는 요구하는 역량과 스택이 다릅니다. 같은 언어지만 절대 넘으면 안 되는 제약이 다르기 때문입니다. 게임은 한 프레임(60fps 기준 약 16.7ms) 안에 모든 처리를 끝내야 하고, HFT는 주문 처리 경로의 지연을 마이크로초 이하로 다투며, 임베디드는 수십~수백 KB의 RAM과 동적 할당 금지 같은 조건에서 동작해야 하고, 대형 IT 백엔드는 많은 개발자가 오래 유지보수할 수 있는 코드와 수평 확장을 요구합니다.
이 글은 대형 IT(흔히 네카라쿠배로 묶어 부르는 회사들)·금융/HFT·게임사 세 축을 중심으로 각 도메인에서 자주 보는 C++ 스택, 면접에서 강조되는 점, 최적화 방향을 정리하고, 임베디드·시스템 소프트웨어가 이들과 어떻게 다른지도 짚습니다. 커리어 전반은 #45-3 커리어 로드맵, 서버 설계는 #46-1 시스템 디자인에서 다룹니다.
나에게 맞는 도메인 찾기
어느 도메인이 더 좋다기보다, 어떤 종류의 문제를 오래 붙잡고 있어도 지치지 않는지가 기준이 됩니다.
flowchart TD
A[C++ 개발자로 어디로?] --> B{"실시간 그래픽·물리에 관심?"}
B -->|Yes| C[게임 개발]
B -->|No| D{"극한의 지연 최적화를 즐기나?"}
D -->|Yes| E[금융 HFT]
D -->|No| F{"하드웨어 제어에 관심?"}
F -->|Yes| G[임베디드]
F -->|No| H{"대규모 트래픽 처리에 관심?"}
H -->|Yes| I[서버 백엔드]
H -->|No| J[시스템 소프트웨어]
| 도메인 | 핵심 제약 | 주로 다루는 기술 |
|---|---|---|
| 게임 | 프레임 예산 (60fps ≈ 16.7ms) | 게임 엔진(Unreal 등), 그래픽 API, 메모리 풀 |
| 금융 HFT | 주문 경로 지연 (μs 이하) | lock-free, 캐시·메모리 레이아웃, 커널 바이패스 |
| 임베디드 | 수십~수백 KB RAM, 동적 할당 제한 | RTOS, bare-metal, MISRA C++ |
| 서버 백엔드 | 처리량·가용성·유지보수성 | 비동기 I/O, 분산 시스템, 관측성 |
| 시스템 소프트웨어 | OS·컴파일러 수준 정확성 | 커널, 드라이버, LLVM |
도메인 불일치에서 생기는 문제
도메인 사이의 차이를 무시하면 기술적으로 틀리지 않은 답과 코드도 “우리 문제를 모른다”는 평가로 이어집니다. 흔한 유형은 다음과 같습니다.
게임 서버에서 메모리 풀과 세션 설계를 오래 다룬 개발자가 HFT 회사에 지원하면, 면접에서는 캐시 라인 false sharing, seq_cst와 acquire-release 메모리 순서의 차이, 나노초 단위 지연 측정 같은 질문을 받습니다. 게임 서버가 안정성과 동시 접속자 수(CCU)를 먼저 본다면 HFT는 한 요청의 지연과 그 꼬리 분포를 먼저 보기 때문입니다. JD에 “지연·처리량·로우레벨” 같은 키워드가 있다면 캐시 친화적 코드, mutex와 동기화 원칙, 커스텀 할당자를 먼저 보강해야 합니다.
반대 방향도 있습니다. 대형 IT 백엔드 팀에 lock-free 큐, 커스텀 메모리 풀, std::atomic 기반 최적화를 측정 근거 없이 들고 오면, 코드 리뷰에서 가독성·테스트 가능성·표준 라이브러리 우선 원칙을 이유로 반려되기 쉽습니다. 이런 팀은 스케일 아웃·안정성·협업을 우선하고, 로우레벨 최적화는 측정된 병목이 있을 때 팀 합의를 거쳐 문서와 테스트를 함께 붙여 도입합니다.
HFT 코드에서 “메모리 안전”을 이유로 모든 객체를 std::shared_ptr로 다루는 것도 대표적인 불일치입니다. shared_ptr은 복사하고 소멸할 때마다 참조 카운트를 원자적으로 바꾸고, 여러 코어가 같은 객체를 공유하면 그 카운트가 있는 캐시 라인이 코어 사이를 오갑니다. 소유자가 하나라면 unique_ptr, 그 외에는 수명이 명확한 참조나 값 전달을 먼저 고려하는 것이 이 도메인의 관례입니다.
게임 서버에서 네트워크 오류나 패킷 파싱 실패를 정상 경로의 throw로 처리하는 것도 문제가 됩니다. 잘못된 패킷은 게임 서버에서 일상적으로 들어오는 입력이고, 악의적인 클라이언트가 깨진 패킷을 대량으로 보내면 예외 처리 비용이 그대로 서버 부하가 됩니다. 실시간 경로에서는 예외 대신 에러 값(std::optional, std::expected, 에러 코드)으로 실패를 반환하고, 예외는 복구할 수 없는 상황에 남겨 두는 것이 일반적입니다.
대형 IT 백엔드
스택과 환경
모던 C++(C++17/20)의 스마트 포인터, RAII, 표준 라이브러리를 중심으로 가독성과 유지보수성을 중시합니다. CMake와 vcpkg·Conan으로 빌드하고, CI에서 여러 플랫폼 빌드·테스트·정적 분석(Clang-Tidy 등)을 돌리는 문화가 있는 곳이 많습니다. 대규모 트래픽, 마이크로서비스, 로그·메트릭을 다루며, C++ 서비스가 Go나 Java 서비스와 함께 돌아가는 경우도 흔합니다.
면접에서 강조되는 점
대규모 동시 접속과 확장성을 다루는 시스템 설계 질문이 중심이고, 이벤트 루프·스레드 풀·캐시·DB 연동 수준에서 구조를 설명할 수 있으면 유리합니다. 코드 품질 측면에서는 메모리 안전성, 에러 처리, 테스트, 리뷰를 묻고 “왜 이렇게 짰는지”를 설명하길 기대합니다. API 설계, ABI·버전 호환, 문서화처럼 C++ 하나가 아니라 서비스 전체에서의 역할을 묻기도 합니다. 성능 질문이 나와도 “먼저 측정하고, 팀과 합의하고, 문서화한다”는 흐름을 함께 말하는 편이 좋습니다.
코드 스타일 예제
// 가독성·표준·테스트 용이성 우선
// g++ -std=c++17 -c bigtech_style.cpp
#include <cstddef>
#include <optional>
#include <string>
namespace service {
class RequestHandler {
public:
explicit RequestHandler(const std::string& config_path)
: config_(load_config(config_path)) {}
// 실패 가능성을 반환 타입으로 표현
std::optional<std::string> process(const std::string& input) const {
if (input.empty() || input.size() > config_.max_size) return std::nullopt;
return transform(input);
}
private:
struct Config {
int timeout_ms = 1000;
std::size_t max_size = 1024;
};
Config config_;
std::string transform(const std::string& s) const {
return s; // 실제로는 비즈니스 로직
}
static Config load_config(const std::string& /*path*/) {
return Config{};
}
};
} // namespace service
std::optional로 실패 가능성을 타입에 드러내고, explicit 생성자로 의도치 않은 변환을 막으며, 소유권이 분명한 값 멤버만 사용합니다. 새로운 기법보다는 동료가 바로 읽고 테스트할 수 있는 코드가 우선입니다.
금융·HFT
스택과 환경
지연과 처리량이 최우선이고, CPU 캐시와 메모리 레이아웃, 네트워크 스택까지 손대는 경우가 많습니다. lock-free 구조, 커스텀 할당자, SIMD, 커널 바이패스 네트워크(DPDK, Solarflare의 OpenOnload 등)를 다뤄 본 경험이 강점이 됩니다. 표준 C++을 쓰되 “이 한 줄이 어떤 어셈블리로 나가는지”, “여기서 캐시 미스가 나는지”까지 묻는 회사가 있습니다.
면접에서 강조되는 점
캐시 라인, false sharing, 메모리 순서, atomic 연산의 비용처럼 “이 코드가 왜 느린지”를 하드웨어 수준에서 설명할 수 있어야 합니다. 동시성 질문은 mutex보다 CAS, 메모리 모델, lock-free 큐 쪽으로 깊어지는 경우가 많습니다. 네트워크에서는 지연이 어디서 생기는지(시스템 콜, 커널 네트워크 스택, 인터럽트, 복사)를 말할 수 있으면 좋습니다. 무엇보다 체감이 아니라 측정 결과로 말하는 습관을 봅니다. 평균보다 99퍼센타일 같은 꼬리 지연을 먼저 보는 것도 이 도메인의 특징입니다.
할당 측면에서는 핫 경로의 new/delete를 풀이나 스레드 로컬 할당으로 바꾸는 것이 기본이며, #39-2 PMR과 핸들러 할당자가 같은 주제를 다룹니다.
코드 스타일 예제
// 지연 최소화, 캐시 친화, 할당 회피
// g++ -std=c++17 -O3 -c hft_style.cpp
#include <atomic>
#include <cstddef>
#include <cstdint>
// 캐시 라인(64B) 정렬로 false sharing 방지
struct alignas(64) CacheLinePaddedCounter {
std::atomic<std::uint64_t> value{0};
};
// 스레드별 카운터: 각자 다른 캐시 라인
struct PerThreadStats {
CacheLinePaddedCounter counters[8];
};
// 핫 경로: 힙 할당 없이 미리 확보한 버퍼를 순차 접근
inline std::uint64_t sum_prices(const std::uint64_t* buffer, std::size_t len) {
std::uint64_t sum = 0;
for (std::size_t i = 0; i < len; ++i) {
sum += buffer[i];
}
return sum;
}
// 시퀀스 번호 공개: 필요한 만큼만 동기화하는 release/acquire
void publish(std::atomic<std::uint64_t>& seq, std::uint64_t val) {
seq.store(val, std::memory_order_release);
}
std::uint64_t consume(const std::atomic<std::uint64_t>& seq) {
return seq.load(std::memory_order_acquire);
}
alignas(64)로 카운터마다 캐시 라인을 따로 두고, 핫 경로에서는 미리 확보한 연속 버퍼만 읽으며, 공개·소비 쌍에는 seq_cst보다 약한 release/acquire를 씁니다. x86에서 seq_cst 저장은 xchg처럼 비용이 큰 명령으로 컴파일되는 반면 release 저장은 일반 mov로 충분하다는 점이 이런 선택의 근거가 됩니다.
게임 서버
스택과 환경
수만 명 이상의 동시 접속, 틱 단위 응답, 이벤트 루프·스레드 모델·세션 설계가 핵심입니다. 객체를 대량으로 만들고 지우므로 오브젝트 풀과 메모리 풀로 할당 비용과 단편화를 줄이고, Windows·Linux 서버와 네트워크 프로토콜·직렬화·버전 호환을 함께 다룹니다.
면접에서 강조되는 점
Acceptor와 Worker 분리, 세션 단위 직렬화(Asio strand 등), 패킷·버퍼 설계 같은 네트워크 서버 설계가 자주 나오며, #46-1의 내용과 많이 겹칩니다. 대량 객체를 어떻게 할당하고 해제할지, use-after-free를 어떻게 막는지 같은 메모리 관리 질문과, 크래시 덤프 분석·재현 경로·로그와 메트릭 같은 운영 경험도 중요하게 봅니다. 팀에 따라 지연의 예측 가능성을 먼저 보기도 하고 처리량과 CCU를 먼저 보기도 하지만, 메모리 풀·세션 설계·네트워크 지연에 대한 이해는 공통입니다.
코드 스타일 예제
// 메모리 풀, 세션, 예외 회피
// g++ -std=c++17 -c game_style.cpp
#include <array>
#include <cstddef>
#include <cstdint>
#include <new>
#include <utility>
#include <vector>
// 오브젝트 풀: 세션·패킷 등 대량 생성/해제 객체용
template <typename T, std::size_t PoolSize = 1024>
class ObjectPool {
public:
ObjectPool() {
free_list_.reserve(PoolSize);
for (auto& slot : storage_) free_list_.push_back(&slot);
}
template <typename... Args>
T* acquire(Args&&... args) {
if (free_list_.empty()) return nullptr; // 풀 고갈 시 null (예외 대신)
Slot* slot = free_list_.back();
free_list_.pop_back();
return new (slot->bytes) T(std::forward<Args>(args)...); // 빈 슬롯에 생성
}
void release(T* obj) {
obj->~T();
free_list_.push_back(reinterpret_cast<Slot*>(obj));
}
private:
struct Slot { alignas(T) std::byte bytes[sizeof(T)]; }; // 생성되지 않은 원시 메모리
std::array<Slot, PoolSize> storage_;
std::vector<Slot*> free_list_;
};
// 패킷 버퍼: 고정 크기
struct Packet {
static constexpr std::size_t MAX_SIZE = 1024;
std::array<std::uint8_t, MAX_SIZE> data;
std::size_t len = 0;
};
// 세션: 연결당 하나, 풀에서 할당
struct Session {
int socket_fd = -1;
Packet recv_buf;
Packet send_buf;
int read_packet() { return 0; } // 예외 대신 에러 코드
int write_packet() { return 0; }
};
풀은 미리 확보한 원시 메모리(Slot)에 placement new로 객체를 만들고, 반납할 때는 소멸자만 호출합니다. 슬롯을 “아직 객체가 생성되지 않은 메모리”로 관리하는 것이 핵심입니다. 이미 생성된 객체 위에 다시 placement new를 하면 이전 객체의 소멸자가 불리지 않아 자원이 샙니다. 이 풀은 단일 스레드용이므로, 여러 스레드가 함께 쓰려면 스레드별 풀을 두거나 동기화를 추가해야 합니다. 또 storage_가 객체 안에 있어 Session처럼 큰 타입을 수천 개 담으면 풀 객체 자체가 수 MB가 되므로, 스택이 아니라 정적 저장소나 힙에 둡니다.
임베디드·시스템 소프트웨어는 무엇이 다른가
임베디드·펌웨어
위 세 도메인이 서버 한 대의 자원을 얼마나 잘 쓰느냐를 다룬다면, 임베디드는 자원 자체가 극히 적은 환경을 다룹니다. 마이크로컨트롤러는 RAM이 수십~수백 KB인 경우가 흔하고, 힙 단편화가 쌓이면 재부팅 말고는 회복할 방법이 없는 장비도 많습니다. 그래서 많은 임베디드 코드베이스는 부팅 시 한 번만 할당하거나 정적 배열만 쓰고, std::vector 대신 ETL 같은 고정 용량 컨테이너를 씁니다. -fno-exceptions -fno-rtti로 예외와 RTTI를 꺼 코드 크기와 예측 불가능성을 줄이고, 에러는 반환값으로 전달합니다. 자동차·산업용 소프트웨어는 MISRA C++이나 AUTOSAR C++14 같은 코딩 표준 준수를 요구받는 경우가 많습니다.
#include <cstdint>
// 메모리 매핑된 하드웨어 레지스터는 volatile로 접근 (주소는 칩마다 다름)
volatile std::uint32_t* const GPIO_ODR =
reinterpret_cast<volatile std::uint32_t*>(0x40020014);
void set_led(bool on) {
if (on) *GPIO_ODR |= (1u << 5);
else *GPIO_ODR &= ~(1u << 5);
}
// 인터럽트 핸들러는 최소한의 일만 하고 플래그로 메인 루프에 넘긴다
volatile bool g_tick = false;
extern "C" void TIM2_IRQHandler() {
// (타이머 인터럽트 플래그 클리어 - 칩 제조사 헤더의 레지스터 사용)
g_tick = true;
}
volatile은 “컴파일러가 이 읽기·쓰기를 생략하거나 합치지 말라”는 뜻일 뿐, 원자성이나 스레드 간 동기화를 보장하지 않는다는 점이 면접 단골 질문입니다. 위의 |=도 읽기-수정-쓰기 세 단계라서, 같은 레지스터를 ISR에서도 바꾼다면 그 사이에 인터럽트가 끼어들어 값이 덮어써질 수 있습니다. 이런 경우 인터럽트를 잠시 막거나, 칩이 제공하는 비트 단위 set/reset 레지스터를 쓰거나, 지원된다면 std::atomic을 써야 합니다(참고로 C++20은 volatile에 대한 복합 대입을 deprecated로 지정했다가 C++23에서 비트 연산 복합 대입만 다시 허용했습니다). 면접에서는 RTOS 태스크 스케줄링, 우선순위 역전, 동적 할당 없이 메모리를 관리하는 방법, JTAG 디버거 사용 경험 같은 질문이 자주 나옵니다. 하드웨어 없이 경험을 쌓기 어려운 것이 진입 장벽인데, 저렴한 개발 보드(STM32 Nucleo, ESP32 등)로 센서를 읽고 RTOS 태스크를 나눠 보는 작은 프로젝트가 포트폴리오로 효과적입니다.
시스템 소프트웨어 (OS·컴파일러·브라우저)
커널, 드라이버, 컴파일러·툴체인, 브라우저 엔진 같은 분야입니다. C++ 문법보다 그 아래 계층의 동작 원리를 깊게 묻는 경향이 있습니다. 페이지 테이블과 가상 메모리, 파일 시스템의 inode, 커널에서 TCP 연결이 처리되는 과정, 컴파일러의 SSA(Static Single Assignment) 형태 같은 CS 기초가 대표적인 면접 주제입니다. 리눅스 커널 자체는 C(일부 Rust)로 작성되지만, LLVM·Chromium처럼 거대한 C++ 코드베이스가 많아 대규모 코드를 읽고 빌드 시스템을 다루는 능력이 중요합니다. 이 분야는 오픈소스 기여 이력이 실력의 증거가 되는 경우가 많아, LLVM이나 Chromium에 작은 패치를 올려 본 경험이 이력서에서 큰 비중을 차지합니다(#45-1 오픈소스 기여 참고).
| 항목 | 임베디드 | 시스템 소프트웨어 |
|---|---|---|
| 핵심 제약 | 메모리·전력·실시간성 | 정확성·호환성·대규모 코드베이스 |
| C++ 사용 범위 | 서브셋(예외·RTTI·힙 제한) | 모던 C++ 전반, 자체 코딩 규칙 |
| 면접 주제 | volatile, ISR, RTOS, 메모리 맵 I/O | 가상 메모리, 파일 시스템, 컴파일러 IR |
| 디버깅 도구 | JTAG/SWD, 오실로스코프, 로직 분석기 | GDB, perf, 커널 트레이싱(ftrace, eBPF) |
같은 요구사항을 도메인별 스타일로 구현하기
요구사항은 “고빈도로 들어오는 이벤트를 받아 처리한다”입니다. 같은 요구를 세 도메인의 관례대로 구현하면 구조가 크게 달라집니다.
대형 IT 스타일: mutex 큐와 워커
// g++ -std=c++17 -pthread -c bigtech_events.cpp
#include <cstddef>
#include <functional>
#include <future>
#include <mutex>
#include <queue>
#include <vector>
class EventProcessor {
public:
using Event = std::vector<int>;
using Handler = std::function<void(const Event&)>;
void submit(Event event) {
std::lock_guard lock(mutex_);
queue_.push(std::move(event));
}
// 쌓인 이벤트를 워커 여러 개로 모두 처리하고 반환
void drain(std::size_t num_workers, const Handler& handler) {
std::vector<std::future<void>> futures;
for (std::size_t i = 0; i < num_workers; ++i) {
futures.push_back(std::async(std::launch::async, [this, &handler]() {
while (true) {
Event ev;
{
std::lock_guard lock(mutex_);
if (queue_.empty()) break;
ev = std::move(queue_.front());
queue_.pop();
}
handler(ev); // 처리는 락 밖에서
}
}));
}
for (auto& f : futures) f.get(); // 워커 예외를 호출자에게 전달
}
private:
std::mutex mutex_;
std::queue<Event> queue_;
};
mutex와 표준 컨테이너만으로 구성해 누구나 읽고 검증할 수 있습니다. 이벤트 하나의 처리 시간이 락 비용보다 충분히 길다면 이 구조로도 처리량이 부족해지는 일은 드뭅니다.
HFT 스타일: lock-free SPSC 링 버퍼
// g++ -std=c++17 -O3 -pthread -c hft_events.cpp
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
template <typename T, std::size_t Capacity>
class LockFreeSPSCQueue {
static_assert((Capacity & (Capacity - 1)) == 0, "Capacity는 2의 거듭제곱");
public:
bool push(const T& item) {
auto tail = tail_.load(std::memory_order_relaxed);
if (tail - head_.load(std::memory_order_acquire) >= Capacity)
return false; // 가득 참
buffer_[tail % Capacity] = item;
tail_.store(tail + 1, std::memory_order_release);
return true;
}
bool pop(T& out) {
auto head = head_.load(std::memory_order_relaxed);
if (head == tail_.load(std::memory_order_acquire))
return false; // 비어 있음
out = buffer_[head % Capacity];
head_.store(head + 1, std::memory_order_release);
return true;
}
private:
std::array<T, Capacity> buffer_;
alignas(64) std::atomic<std::uint64_t> head_{0}; // 소비자만 씀
alignas(64) std::atomic<std::uint64_t> tail_{0}; // 생산자만 씀
};
// 입력 스레드 하나가 넣고, 처리 스레드 하나가 꺼낸다
using MarketEvent = std::array<int, 64>; // 256바이트
using EventQueue = LockFreeSPSCQueue<MarketEvent, 65536>; // 약 16MB: 시작 시 힙에 한 번만 할당
head와 tail을 계속 증가하는 64비트 카운터로 두고 % Capacity로 칸을 찾으므로, 칸을 비워 두지 않고 전체 용량을 쓸 수 있습니다. 이 큐는 생산자와 소비자가 각각 하나일 때만 안전합니다. 생산자가 둘이면 두 스레드가 같은 tail 값을 읽고 같은 칸에 쓰게 되므로, CAS로 칸을 예약하는 MPSC 구조가 필요합니다. HFT 시스템은 이런 복잡도를 피하려고 “시장 데이터 수신 스레드 → 전략 스레드”처럼 스레드 쌍마다 전용 SPSC 큐를 두는 설계를 선호하는 경우가 많습니다. head_와 tail_을 alignas(64)로 떨어뜨린 것은 생산자와 소비자가 서로의 캐시 라인을 계속 무효화하지 않게 하기 위해서입니다.
게임 서버 스타일: 이벤트 루프와 strand
// g++ -std=c++17 -c game_events.cpp (Boost.Asio 필요)
#include <boost/asio.hpp>
#include <cstdint>
#include <vector>
namespace asio = boost::asio;
class GameSession {
public:
explicit GameSession(asio::io_context& io)
: strand_(asio::make_strand(io)) {}
// 어느 스레드에서 호출해도 같은 세션의 패킷은 순서대로 하나씩 처리됨
void submit(std::vector<std::uint8_t> packet) {
asio::post(strand_, [this, p = std::move(packet)]() mutable {
process_packet(std::move(p));
});
}
private:
asio::strand<asio::io_context::executor_type> strand_;
void process_packet(std::vector<std::uint8_t> p) {
// 같은 strand 안에서 순차 실행되므로 세션 상태에 락이 필요 없음
(void)p;
}
};
io_context를 여러 스레드가 함께 돌리더라도, 같은 strand에 올린 작업은 동시에 실행되지 않습니다. 세션마다 strand를 하나씩 두면 세션 상태를 락 없이 다루면서 서로 다른 세션은 병렬로 처리할 수 있습니다. 실제 코드에서는 람다가 실행되기 전에 세션이 사라지지 않도록 shared_from_this()로 수명을 붙잡는 것이 일반적입니다.
세 스타일 비교
| 항목 | 대형 IT | 금융/HFT | 게임 서버 |
|---|---|---|---|
| 동기화 | mutex, 표준 컨테이너 | lock-free, atomic | strand, 세션 단위 직렬화 |
| 할당 | 표준 할당, 스마트 포인터 | 풀, 스레드 로컬, 시작 시 선할당 | 오브젝트 풀, 메모리 풀 |
| 예외 | 허용 | 핫 경로에서 회피 | 실시간 경로에서 회피 |
| 지연 목표 | ms~수백 ms | μs 이하 | ms 단위, 예측 가능성 |
| 복잡도 수용 | 낮음(유지보수 우선) | 높음(지연 우선) | 중간(핫 경로만 최적화) |
| 로깅 | 구조화 로그, 메트릭 | 핫 경로에서는 바이너리 버퍼에 기록 후 별도 스레드가 출력 | 틱·세션 단위 집계, 샘플링 |
| 테스트 | 단위·통합 테스트, CI 정적 분석 | 지연 벤치마크 회귀, 리플레이 테스트 | 부하 테스트, CCU 시뮬레이션 |
대형 IT 쪽은 std::make_unique와 reserve 정도로 충분한지 먼저 확인하고, HFT와 게임은 풀을 처음부터 설계에 넣는 경향이 있습니다. 하지만 어느 도메인에서든 최적화의 출발점은 측정입니다. “이 코드가 느릴 것 같다”는 추측으로 lock-free로 바꿨는데 실제 시간은 I/O 대기에서 쓰이고 있었다면, 복잡도만 늘고 얻은 것은 없습니다. perf, VTune, 플레임 그래프로 병목을 확인한 뒤 그 구간만 손대는 것이 공통 원칙입니다.
도메인별 자주 하는 실수
false sharing을 막으려다 잘못 정렬하기
#include <atomic>
// 문제: 스레드별 카운터가 같은 캐시 라인
struct Bad {
std::atomic<int> count[8]; // 4B × 8 = 32B → 8개 전부 한 캐시 라인
};
// 흔한 오해: 배열 시작만 64B 정렬될 뿐, 원소는 여전히 붙어 있음
struct AlsoBad {
alignas(64) std::atomic<int> count[8];
};
// 원소 하나하나를 캐시 라인 크기로 정렬
struct alignas(64) PaddedCounter {
std::atomic<int> value{0};
};
struct Good {
PaddedCounter count[8]; // 각 카운터가 서로 다른 캐시 라인
};
서로 다른 스레드가 같은 캐시 라인을 수정하면, 캐시 일관성 프로토콜 때문에 쓰기마다 라인의 소유권이 코어 사이를 오가 성능이 크게 떨어집니다. 정렬은 배열이 아니라 원소 단위로 걸어야 합니다.
면접에서 도메인의 관심사를 놓치기
대형 IT 면접에서 성능 최적화, 캐시, SIMD만 이야기하면, 면접관이 실제로 궁금해하는 테스트 전략, 리뷰 문화, 장애 대응을 놓칠 수 있습니다. 반대로 HFT 면접에서 “스레드 풀과 mutex로 처리한다”고만 답하면 지연 문제를 고민해 본 적이 없다는 인상을 줍니다. 게임 서버에서는 “빠르게 돌아가면 된다”는 생각으로 세션·패킷 설계를 즉흥적으로 하면, 접속자가 늘었을 때 락 경합과 메모리 단편화로 확장이 막힙니다. Acceptor·Worker 분리, 세션 단위 직렬화, 풀 크기 산정은 처음부터 고려해야 하는 설계 항목입니다.
도메인 간 전환
flowchart LR
subgraph Entry[진입]
A[웹/백엔드]
B[게임 클라이언트]
C[임베디드]
end
subgraph Targets[목표 도메인]
T1[대형 IT 백엔드]
T2[금융/HFT]
T3[게임 서버]
end
A -->|시스템 설계·스케일| T1
A -->|로우레벨 보강| T2
B -->|네트워크·서버| T3
C -->|지연·하드웨어| T2
| 전환 | 그대로 쓸 수 있는 것 | 새로 채워야 하는 것 |
|---|---|---|
| 게임 → 서버 백엔드 | 멀티스레딩, 네트워크, 메모리 풀 | 데이터베이스, 분산 시스템, 관측성 |
| 서버 백엔드 → 금융 HFT | 네트워크, 동시성 | 캐시·메모리 모델, lock-free, 지연 측정 |
| 임베디드 → 게임 | 메모리 제약 하의 최적화 감각 | 그래픽 API, 엔진 구조, 물리 |
| 임베디드 → 시스템 소프트웨어 | 하드웨어·OS 저수준 이해 | 대규모 코드베이스, 커널·컴파일러 내부 |
가장 흔한 실패는 “C++은 같으니까”라며 이전 도메인의 습관을 그대로 가져가는 것입니다. 게임 서버 출신이 대형 IT 백엔드에서 커스텀 풀부터 만들거나, 백엔드 출신이 HFT 면접에서 표준 컨테이너와 mutex만으로 답하면, 기술적으로 틀리지 않아도 목표 도메인의 문제를 모른다는 인상을 줍니다. 전환 전에 목표 도메인의 제약(프레임 예산, 지연 예산, 메모리 예산, 유지보수 비용)을 자기 말로 설명할 수 있게 준비하세요.
준비는 세 단계로 나누면 현실적입니다. 먼저 모던 C++, 메모리, 동시성 같은 공통 기반을 점검합니다. 다음으로 목표 회사의 JD에서 반복되는 키워드를 정리해 격차를 확인하고, lock-free 큐 구현이나 Asio 기반 채팅 서버처럼 그 격차를 직접 다뤄 본 작은 프로젝트를 만듭니다. 마지막으로 면접에서는 “이론과 개인 프로젝트로 다뤄 봤고, 실무 경험은 아직”이라고 경험의 범위를 정확히 말할 수 있어야 합니다. 부풀린 경험은 꼬리 질문 몇 개로 금방 드러납니다.
도메인별 면접 강조 역량
| 도메인 | 강조 역량 | 자주 나오는 키워드 |
|---|---|---|
| 대형 IT | 설계·품질·스케일 | 모던 C++, 시스템 설계, 테스트, CI |
| 금융/HFT | 지연·로우레벨·측정 | 캐시, lock-free, 할당자, 벤치마크 |
| 게임 | CCU·메모리·네트워크 | 이벤트 루프, 풀, 세션, 패킷 |
메모리·포인터·RAII, data race와 mutex·atomic 같은 동시성 기초, 스레드 모델 수준의 시스템 설계는 어느 도메인에서나 묻습니다. 차이는 로우레벨과 성능을 어디까지 파고드는지, 설계와 협업을 얼마나 묻는지에 있으며, 이 강조점은 같은 도메인 안에서도 회사와 팀마다 다르므로 JD와 면접 질문에서 직접 읽어 내야 합니다.
같이 보면 좋은 글
- C++ 백엔드·게임 서버 시스템 디자인 | 대규모 동시 접속과 메모리 풀 [#46-1]
- C++ 개발자 기술 면접 | 자주 나오는 질문과 답변 스크립트(30초·90초)·꼬리 질문 대비
- C++ Lock-Free 프로그래밍 실전 | CAS·ABA·메모리 순서·고성능 큐 [#34-3]
- C++ 함수 객체 vs 람다 vs std::function
- C++ 오픈소스 기여: 유명 라이브러리 분석부터 첫 Pull Request까지 [#45-1]
- X-Macro로 중복 없애기
이전 글: C++ 개발자 기술 면접 | 자주 나오는 질문과 답변 스크립트(30초·90초)·꼬리 질문 대비 다음 글: [C++ vs 타 언어 #47-1] C++ vs Go: 성능 및 동시성 모델 실전 비교