C++ 개발자 기술 면접 | 자주 나오는 질문과 답변 스크립트(30초·90초)·꼬리 질문 대비

이 글의 핵심

같은 내용을 알아도 결론, 메커니즘, 실무, 한계의 순서로 말하느냐에 따라 면접 평가가 달라집니다. 네 블록 답변 프레임을 먼저 잡고, 12개 주제별 스크립트와 "이 코드 문제점은?" 유형에 쓸 체크리스트로 실제 면접에서 말할 문장을 준비할 수 있습니다.

들어가며: 같은 답변도 “순서”가 다르면 점수가 갈린다

C++ 기술 면접에서 자주 지는 패턴은 지식 부족이 아니라 말하는 순서입니다. 정의를 길게 늘어놓다 시간이 지나면, 면접관은 “이 사람이 핵심을 짚는 사람인가?”를 확인하지 못한 채 끝나 버립니다.

이 글은 기술 면접 질문 30선·자주 틀리는 질문 50선처럼 목차형 정리는 다른 글에 맡기고, 여기서는 답변 골격과 말로 바로 쓸 수 있는 스크립트를 드립니다. 면접 전에 소리 내어 읽어 보며, 본인 경험 한 문장만 끼워 넣으면 그대로 현장에서 쓸 수 있습니다.

먼저 네 블록짜리 답변 프레임을 잡고, 고빈도 주제 12개에 대해 30초·90초 스크립트와 이어질 꼬리 질문을 정리합니다. 마지막에는 코드 리뷰형 질문에 쓰는 점검 순서와, 면접에서 자주 부정확하게 말하는 문장을 짚습니다.


답변 프레임: 네 블록만 기억하면 된다

면접관이 듣고 싶은 정보는 대부분 아래 네 덩어리 안에 있습니다.

  1. 결론(10초): “한 문장으로 무엇인가 / 언제 쓰는가”
  2. 메커니즘(30초): 컴파일러·런타임·메모리 모델 중 관련된 층만
  3. 실무(20초): 팀 코드에서의 선택 기준, 본인이 겪은 증상
  4. 한계(10초): 비용, UB, ABI, 예외 상황 중 하나만 짚고 마무리

모르는 질문이면 1번을 “확실히 아는 범위에서만” 짧게 말하며, 2번을 추측 대신 확인 방법(cppreference, 컴파일러 경고, Sanitizer, 최소 재현)으로 바꿉니다.


주제별 스크립트

각 항목은 30초는 거의 그대로 외워도 되고, 90초는 숫자·경험만 바꿔 넣으면 됩니다. 꼬리는 면접관이 이어서 묻기 좋은 방향입니다.

RAII와 스마트 포인터(소유권)

30초: “RAII는 생성자에서 자원을 잡고 소멸자에서 푸는 패턴이라서, 스코프를 벗어나면 해제 시점이 보장됩니다. C++에서는 기본이 unique_ptr로 단일 소유를 표현하며, 정말로 공유가 필요할 때만 shared_ptr을 씁니다.”

90초: “수동 new/delete는 예외 경로에서 누락되기 쉬워서 RAII로 감쌉니다. unique_ptr은 복사 없이 이동만 되니 소유권이 명확하며, shared_ptr은 참조 카운트 비용과 순환 참조 이슈가 있어서 기본값으로 두지 않습니다. weak_ptr은 캐시나 옵저버처럼 소유하지 않고 관찰만 할 때 씁니다.”

꼬리: “make_shared가 왜 shared_ptr 생성에 유리한가요?” → “제어 블록과 객체를 한 번에 할당해 할당 횟수와 캐시 친화를 노린다” 정도면 충분합니다. 한 단계 더 들어오면 “대신 weak_ptr이 남아 있는 동안에는 제어 블록과 함께 객체 메모리도 해제되지 않는다”는 단점까지 말할 수 있으면 좋습니다. 큰 객체를 weak_ptr 캐시로 관찰하는 구조에서는 이 차이 때문에 new로 따로 할당하는 쪽을 고르기도 합니다.


이동 의미론·복사·RVO

30초: “이동은 ‘깊은 복사’ 대신 내부 버퍼 포인터 같은 자원 핸들만 넘겨서 O(1)에 가깝게 옮기는 것입니다. 복사 생성자가 무거운 타입에서 특히 중요합니다.”

90초: “rvalue는 보통 임시값이며, 이동 생성자·이동 대입이 있으면 그 임시의 자원을 빼앗아 옵니다. 반환값 최적화(RVO)가 되는 형태에서는 return에 std::move를 붙이면 오히려 NRVO를 막을 수 있어서 조심합니다. std::move는 캐스트에 가깝으며, 실제 이동은 이동 연산이 있을 때만 일어난다고 말합니다.”

꼬리: “다섯 특수 멤버에서 Rule of 0/3/5는?” → “직접 자원을 잡으면 규칙을 맞추고, 스마트 포인터로 위임하면 0으로 둔다” 수준.


가상 함수·vtable·디스패치 비용

30초: “가상 함수는 동적 디스패치입니다. 객체에 vptr이 있고 vtable을 통해 실제 구현을 찾습니다. 정적 디스패치보다 간접 호출 비용이 있지만, 인터페이스 치환과 확장에 씁니다.”

90초: “컴파일 타임에 타입이 고정되면 인라인·디버추얼라이제이션(devirtualization)이 가능하지만, 인터페이스 뒤에 구현이 바뀌면 런타임에 함수 포인터 테이블을 탑니다. 따라서 핫 루프 안에서 가상 호출을 남발하면 캐시 미스와 분기 예측 실패가 아프다는 식으로 트레이드오프를 말합니다.”

꼬리: “소멸자는 왜 virtual이어야 하나요?” → “기본 클래스 포인터로 파생 객체를 지울 때 파생 소멸자까지 타려면 필요하다”.


const correctness

30초: “const는 컴파일 타임 계약입니다. 인자를 const T&로 받으면 호출 측에 수정 안 한다는 신호를 주고, 멤버 함수에 const를 붙이면 그 객체 상태를 바꾸지 않겠다는 약속입니다.”

90초: “mutable은 논리적 const와 물리적 const가 갈릴 때 쓰며, const_cast는 상수성 제거라 UB 위험이 있어서 면접에서는 ‘정말 필요한가’를 같이 말합니다.”

꼬리: “const 반복자와 반복자 차이?” → “가리키는 요소를 바꿀 수 있느냐”로 한 줄.


Data race·mutex·atomic

30초: “Data race는 둘 이상의 스레드가 동기화 없이 같은 메모리에 쓰기·읽기를 겹치는 것이며, C++에서는 정의되지 않은 동작입니다. 공유 상태는 mutex로 임계 구역을 보호하거나, 단순 카운터처럼 조건이 맞으면 atomic을 씁니다.”

90초: “mutex는 여러 필드를 한 번에 일관되게 바꿀 때 유리하며, atomic은 단일 변수·단순 연산에 맞을 때 고릅니다. false sharing, memory order는 포지션에 따라 깊이를 조절하면 됩니다.”

꼬리: “lock_guard vs unique_lock?” → “지연 잠금(defer_lock), 조건 변수 대기, try_lock, 락 소유권 이동이 필요하면 unique_lock”. 여기서 “재진입(같은 스레드가 두 번 잠금)”을 unique_lock의 기능으로 말하는 실수가 흔한데, 그건 recursive_mutex의 영역입니다.


예외 안전성(기본기)

30초: “예외가 나도 리소스가 새지 않고 불변식이 깨지지 않아야 합니다. RAII가 기본 도구고, 강한 보장까지 못 가더라도 기본 보장은 지키려고 설계합니다.”

90초: “복사 대입에서 자기 대입·부분 실패 시 상태를 말할 수 있으면 좋습니다. 실무에서는 noexcept가 이동 컨테이너 최적화에 영향을 준다는 정도까지 연결하면 플러스입니다.”

꼬리: “소멸자에서 예외?” → “C++11부터 소멸자는 기본적으로 noexcept라서, 예외가 소멸자 밖으로 빠져나가면 std::terminate가 호출됩니다. 그래서 소멸자 안에서는 예외를 잡아 로그만 남기고, 실패를 호출자에게 알려야 하는 정리 작업은 close()·flush() 같은 별도 함수로 노출합니다.”


깊은 복사 vs 얕은 복사

30초: “얕은 복사는 포인터 값만 복사해서 같은 힙을 가리키고, 깊은 복사는 새 메모리를 잡아 독립된 내용을 만듭니다. 기본 복사가 위험한 이유를 예로 들 수 있어야 합니다.”

90초: “Rule of Three/Five 시절에는 직접 자원을 잡으면 복사·이동·소멸을 같이 설계했으며, 지금은 스마트 포인터와 컨테이너로 위임하는 방법이 흔합니다.”

꼬리: “복사 대입 연산자에서 자기 대입?” → “먼저 검사하거나 copy-and-swap”.


std::vector와 반복자 무효화

30초: “vector에 삽입하다 capacity를 넘으면 저장 공간이 통째로 옮겨져 모든 반복자·참조가 무효화됩니다. 재할당이 없어도 삽입·삭제 지점 이후의 반복자는 무효화되므로, erase는 반환된 반복자로 이어 받아야 합니다.”

90초: “범위 for 안에서 erase하면 UB로 이어지기 쉬워서, 클래식 for 루프나 remove-erase 관용구를 말합니다.”

꼬리: “reserve 후 push_back?” → “reserve한 용량 안에서 push_back하면 재할당이 일어나지 않으므로 기존 반복자·참조가 유지됩니다(end()만 무효화). 단 용량을 넘는 순간 다시 모두 무효화된다”. 더 들어오면 재할당 시 원소의 이동 생성자가 noexcept가 아니면 강한 예외 보장을 위해 복사가 선택된다는 점(move_if_noexcept)까지 연결할 수 있습니다.


템플릿 인스턴스화·컴파일 시간

30초: “템플릿은 사용되는 타입마다 코드가 생성됩니다. 인스턴스화하는 번역 단위마다 정의가 보여야 해서 보통 헤더에 정의를 두고, 인스턴스가 많으면 바이너리와 빌드 시간이 늘 수 있습니다.”

90초: “명시적 인스턴스화나 모듈(C++20)로 완화할 수 있다는 정도까지 말하면 충분합니다.”

꼬리: “typename이 필요한 이유?” → “의존 이름 해석” 한 줄.


PIMPL

30초: “구현 세부를 포인터 뒤로 숨겨 헤더 의존을 줄이는 관용구입니다. 바이너리 호환과 컴파일 시간에 도움이 됩니다.”

90초: “헤더에는 전방 선언만 두며, 실제 멤버는 cpp의 impl 클래스에 둡니다. unique_ptr 특수성 때문에 소멸자를 cpp에 두는 패턴도 같이 말하면 좋습니다.”

꼬리: “비용은?” → “간접 접근 한 번, 힙 할당”.


undefined behavior 대표 사례

30초: “컴파일러는 프로그램에 UB가 없다고 가정하고 최적화하므로, UB가 있으면 소스만 봐서는 예상하기 어려운 기계어가 나올 수 있습니다. 대표적으로 null 역참조, 배열 벗어남, 미초기화 읽기, data race가 있습니다.”

90초: “UB를 줄이려면 경계 검사·Sanitizer·정적 분석·코드 리뷰를 조합한다고 말합니다.”

꼬리: “implementation-defined vs UB?” → “구현이 동작을 정하고 문서화해야 하는 것 vs 표준이 아무 요구도 하지 않는 것”.


링크 단계·ODR·inline

30초: “ODR은 하나의 정의 규칙입니다. 같은 함수가 여러 번 정의되면 링크 에러나 UB가 될 수 있어서, 헤더에는 inline 함수나 템플릿을 두는 식으로 관리합니다.”

90초: “inline은 ‘인라인 확장’보다 ODR 예외에 가깝다는 면접용 한 마디가 있으면 좋습니다.”

꼬리: “static vs anonymous namespace?” → “내부 링크·번역 단위 한정” 수준.


“이 코드 문제점은?” 스타일 답변 체크리스트

코드를 주고 토론하는 라운드에서는 아래 순서로 훑으면 빈칸이 줄어듭니다.

순서볼 것
1수명: 댕글링 참조, 반환 지역 변수 주소, 캡처된 포인터
2소유권: 누가 delete하는지, 이중 해제, 스마트 포인터 혼용
3예외 경로: 중간 return에서 누수 없는지
4동시성: 읽기/쓰기 겹침, 락 순서(데드락)
5성능: 불필요한 복사, O(n²) 삽입, 시스템 콜 반복
6API 계약: nullptr, 빈 컨테이너, 오버플로

한 가지를 짚을 때는 “증상 → 원인 → 수정 → 검증 방법” 순으로 말하면 신뢰가 쌓입니다.


면접에서 자주 틀리게 말하는 문장

스크립트를 외워도 한 문장이 틀리면 꼬리 질문에서 무너집니다. 아래는 실제로 자주 나오는, 반쯤 맞지만 정확하지 않은 표현들입니다.

  • “std::move가 값을 옮깁니다.” std::move는 rvalue 참조로 바꾸는 캐스트일 뿐이고, 이동은 그 결과를 받은 이동 생성자·이동 대입이 합니다. const 객체에 std::move를 쓰면 이동 생성자가 선택되지 않아 조용히 복사가 일어난다는 예를 들 수 있으면 이해도가 드러납니다.
  • “shared_ptr은 스레드 안전합니다.” 참조 카운트 증감이 원자적이라는 것이지, 가리키는 객체에 대한 접근이나 같은 shared_ptr 변수를 여러 스레드가 동시에 대입하는 것까지 안전하다는 뜻이 아닙니다. 후자는 C++20의 std::atomic<std::shared_ptr<T>>(그 이전에는 std::atomic_load/std::atomic_store 자유 함수)가 필요합니다.
  • “volatile을 붙이면 스레드 간 공유가 안전합니다.” C++에서 volatile은 메모리 매핑 I/O처럼 컴파일러가 접근을 생략하면 안 되는 경우를 위한 것이고, 원자성이나 순서를 보장하지 않습니다. 스레드 간 공유는 std::atomic이나 mutex입니다.
  • “inline을 붙이면 인라인됩니다.” 인라인 여부는 컴파일러가 정하고, inline 키워드의 실질적 의미는 여러 번역 단위에 같은 정의가 있어도 ODR 위반이 아니게 해 주는 것입니다.
  • “가상 함수 호출은 항상 느립니다.” 객체를 값으로 직접 호출하거나 final로 동적 타입이 확정되면 컴파일러가 직접 호출로 바꿉니다. 비용이 문제 되는 곳은 타입이 섞인 컨테이너를 도는 핫 루프 정도입니다.

면접 준비를 하면서 흔히 겪는 일은, 모범 답안을 문장 그대로 외웠다가 꼬리 질문 한 번에 막히는 것입니다. 위 문장들처럼 “어디까지가 사실이고 어디부터 과장인가”를 스스로 설명할 수 있는지 점검해 보면, 외운 답과 이해한 답의 차이가 드러납니다. 각 주제마다 직접 돌려 본 최소 예제 하나(예: const 객체에 std::move를 쓰고 복사 생성자에 로그를 찍어 보기)를 준비해 두면 90초 답변에 “직접 확인해 보니”라는 근거를 붙일 수 있습니다.


같이 보면 좋은 글