주니어 C++ 준비

주니어 C++ 백엔드 면접 질문

언어 기초와 기본 소유권을 명확하게 설명해야 하는 백엔드 개발자를 위한 선별된 주니어 C++ 면접 질문 15개

주니어 C++ AI 면접 시작하기신용카드가 필요하지 않습니다. 무료 세션 1회 제공.
영어 기술 면접 연습비원어민이 기술 면접 통과를 연습할 수 있는 모드입니다.

Resource Management

1RAII (Resource Acquisition Is Initialization)의 개념과 C++ 백엔드 서비스에서 예외에 안전한 리소스 관리를 어떻게 가능하게 하는지 설명해 주세요.

RAII (Resource Acquisition Is Initialization)는 C++ 객체가 리소스의 소유권을 갖고 소멸자에서 이를 해제하도록 설계하는 기법입니다. 예외 발생에 따른 스택 해제(stack unwinding) 과정을 포함해 로컬 객체의 수명 또는 스코프가 끝나면 소멸자가 자동으로 호출되므로, RAII는 결정론적인 리소스 정리를 보장하고 에러 경로를 예외에 안전하게 만듭니다. 백엔드 서비스에서는 이러한 방식이 메모리뿐만 아니라 파일 디스크립터, 소켓, 뮤텍스 락, 데이터베이스 핸들, 트랜잭션, 기타 OS 및 애플리케이션 리소스 전반에 널리 적용됩니다.

class Fd {
public:
    explicit Fd(int fd = -1) noexcept : fd_(fd) {}
    ~Fd() { if (fd_ != -1) ::close(fd_); }

    Fd(const Fd&) = delete;
    Fd& operator=(const Fd&) = delete;

    Fd(Fd&& other) noexcept : fd_(std::exchange(other.fd_, -1)) {}
    Fd& operator=(Fd&& other) noexcept {
        if (this != &other) {
            if (fd_ != -1) ::close(fd_);
            fd_ = std::exchange(other.fd_, -1);
        }
        return *this;
    }

    int get() const noexcept { return fd_; }

private:
    int fd_;
};

void handle_request() {
    Fd sock(::accept(...));
    if (sock.get() == -1) throw std::runtime_error("accept failed");
    // If any later operation throws, sock is still closed by ~Fd().
}
AI 코치와 함께 이 질문에 답해 보세요

메모리 관리

2std::unique_ptr와 std::shared_ptr를 비교하고 백엔드 API(Application Programming Interface)에서 각각 언제 사용하는 것이 적절한지 설명해 주세요.

`std::unique_ptr`는 독점 소유권을 나타냅니다. 비용이 저렴하고 이동(move)은 가능하지만 복사는 불가능하므로, 단일 소유자만 필요하거나 소유권을 이전하는 API에 적합합니다. `std::shared_ptr`는 공유 소유권을 나타냅니다. 복사가 가능하며 마지막 강한 소유자가 리소스를 해제할 때까지 참조 카운팅을 통해 객체 수명을 유지합니다. 백엔드 API에서는 소유권을 이전할 때 `unique_ptr`를 사용하고, 독립적인 여러 소유자가 객체의 수명을 연장해야 하는 경우에만 `shared_ptr`를 사용하며, 소유권이 필요 없는 단순 접근에는 참조자(reference)나 원시 포인터(raw pointer)를 사용하는 것이 좋습니다. `std::make_shared`는 메모리 할당 효율성이 높고 예외 안전성을 보장하므로 `shared_ptr` 객체를 생성할 때 일반적으로 선호됩니다.

struct Session {};

std::unique_ptr<Session> create_session();      // caller receives ownership
void process(Session& s);                       // borrows, does not own
void maybe_process(const Session* s);           // nullable borrow

class Dispatcher {
public:
    void add(std::shared_ptr<Session> s) {      // Dispatcher shares lifetime
        sessions_.push_back(std::move(s));
    }
private:
    std::vector<std::shared_ptr<Session>> sessions_;
};
AI 코치와 함께 이 질문에 답해 보세요

타입 시스템

3이동 시맨틱(move semantics)이란 무엇이며, 올바른 이동 생성자와 이동 대입 연산자는 어떻게 구현하나요?

이동 시맨틱(move semantics)은 C++에서 임시 객체나 더 이상 필요하지 않은 객체의 자원을 복사하는 대신 소유권을 이전할 수 있게 해 주는 기능입니다. T&&와 같은 rvalue 참조와 이동 오버로드가 선택되도록 캐스팅하는 std::move를 사용하지만, std::move 자체가 무언가를 직접 이동시키지는 않습니다. 올바른 이동 생성자는 원본 객체의 자원을 가져오고 원본 객체를 유효하면서도 소멸 및 대입이 가능한 상태로 남겨둠으로써 새 객체를 초기화합니다. 올바른 이동 대입 연산자는 기존 객체로 자원을 이전하며, 자기 대입(self-assignment)을 적절히 처리하거나 허용하고, 대상 객체의 기존 자원을 해제하거나 재사용한 후 원본 자원을 가져오고 원본을 안전한 상태로 남겨둡니다. 또한 이동 연산은 noexcept로 선언하는 것이 권장되며, 그래야 표준 컨테이너가 재할당 시 예외 안전성 보장을 유지하면서 이동 연산을 활용할 수 있습니다.

class Buffer {
public:
    Buffer() = default;
    explicit Buffer(std::size_t n) : size_(n), data_(new char[n]) {}
    ~Buffer() { delete[] data_; }

    Buffer(const Buffer&) = delete;
    Buffer& operator=(const Buffer&) = delete;

    Buffer(Buffer&& other) noexcept
        : size_(std::exchange(other.size_, 0)),
          data_(std::exchange(other.data_, nullptr)) {}

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            size_ = std::exchange(other.size_, 0);
            data_ = std::exchange(other.data_, nullptr);
        }
        return *this;
    }

private:
    std::size_t size_ = 0;
    char* data_ = nullptr;
};
AI 코치와 함께 이 질문에 답해 보세요

4C++ API에서 const 정확성(const-correctness)의 개념과 const 멤버 함수를 효과적으로 설계하는 방법을 설명해 주세요.

const 정확성(const-correctness)은 어떤 연산이 객체의 관찰 가능한 상태나 논리적 상태를 수정하지 않는지를 타입 시스템을 통해 명시하는 것을 의미합니다. const 멤버 함수는 `const` 한정자가 붙은 `this` 객체를 가지므로, `mutable`이 아닌 데이터 멤버를 수정하거나 동일한 객체의 비-const 멤버 함수를 호출할 수 없습니다. 훌륭한 API 설계는 읽기 전용 조회 메서드를 const로 지정하고, 상황에 맞게 값이나 const 참조/포인터를 반환하며, const 함수에서 수정 가능한 내부 상태를 외부에 노출하지 않는 것입니다. `mutable`은 캐시, 지연 평가된 값, 메트릭, 뮤텍스처럼 객체의 논리적 상태를 변경하지 않는 세부 구현에 한해 제한적으로 사용해야 합니다. const는 변경 가능성에 대한 API 계약일 뿐 스레드 안전성을 자동으로 보장하지 않으므로, 동시성 보장은 별도로 구현하고 문서화해야 합니다.

#include <mutex>
#include <optional>
#include <string>

class UserProfile {
public:
    const std::string& id() const { return id_; }

    std::string displayName() const {
        std::lock_guard<std::mutex> lock(mu_);
        if (!cached_display_name_) {
            cached_display_name_ = computeDisplayName();
        }
        return *cached_display_name_;
    }

private:
    std::string computeDisplayName() const { return id_; }

    std::string id_;
    mutable std::mutex mu_;
    mutable std::optional<std::string> cached_display_name_;
};
AI 코치와 함께 이 질문에 답해 보세요

5C++에서 std::byte란 무엇이며, 원시 바이너리 버퍼를 안전하게 표현하려면 어떻게 해야 하나요?

`std::byte`는 원시 바이너리 데이터를 문자나 산술 정수가 아닌 순수한 바이트 단위로 표현하기 위한 고유한 타입입니다. 비트 단위 연산을 지원하면서도 바이트 버퍼가 실수로 텍스트나 숫자 값으로 취급되지 않도록 하여 타입 안전성을 높입니다. 원시 바이너리 버퍼는 일반적으로 `std::vector<std::byte>`나 `std::array<std::byte, N>`과 같이 바이트 지향 스토리지를 사용해 표현해야 하며, 소유권을 갖지 않는 API에는 `std::span<std::byte>` 또는 `std::span<const std::byte>`로 전달해야 합니다. 직렬화 코드는 임의의 객체 메모리 배치에 의존하기보다는 명시적으로 값을 인코딩하고 디코딩해야 합니다.

void write_frame(std::span<const std::byte> payload);

std::vector<std::byte> buf;
buf.push_back(std::byte{0x01});
buf.push_back(std::byte{0xFF});
write_frame(buf);
AI 코치와 함께 이 질문에 답해 보세요

객체 수명

60의 규칙(Rule of Zero), 3의 규칙(Rule of Three), 5의 규칙(Rule of Five)을 설명하고 각각 언제 적용되는지 설명해 주세요.

0의 규칙(Rule of Zero): 사용자 정의 소멸자, 복사 또는 이동 연산을 선언하지 않는 클래스를 지향하며, `std::string`, `std::vector`, `std::unique_ptr`, 파일/소켓 래퍼 등과 같은 RAII (Resource Acquisition Is Initialization) 멤버가 리소스를 관리하도록 합니다. 3의 규칙(Rule of Three): 클래스가 리소스를 수동으로 관리하여 사용자 정의 소멸자, 복사 생성자, 복사 대입 연산자 중 하나가 필요하다면, 올바른 복사 및 소유권 동작을 정의하기 위해 대개 세 가지 모두를 구현해야 합니다. 5의 규칙(Rule of Five): C++11 이상에서는 이러한 타입에 대해 이동 생성자와 이동 대입 연산자까지 함께 고려해야 합니다. 대부분의 애플리케이션 타입에는 0의 규칙을 적용하고, 타입이 리소스를 직접 소유하거나 복잡한 소유권/수명 제어가 필요한 경우에는 3의 규칙 또는 5의 규칙을 사용합니다.

struct Session {
    std::string user_id;
    std::vector<std::string> roles;
    std::unique_ptr<Connection> conn;

    // No custom destructor/copy/move operations.
};
AI 코치와 함께 이 질문에 답해 보세요

오류 처리

7C++ 예외 처리, 스택 되감기(stack unwinding), 소멸자와의 상호작용, 서비스 예외 경계에 대해 설명해 주세요.

C++ 예외는 `throw` 표현식에서 가장 가까운 일치하는 `catch` 블록으로 제어권을 전달합니다. 예외가 전파되는 동안 스택 되감기가 발생하여 완전히 생성된 자동 객체들을 역순으로 소멸시키므로, RAII (Resource Acquisition Is Initialization) 기반의 리소스 정리가 자동으로 수행됩니다. 소멸자는 일반적으로 예외를 던져서는 안 됩니다. 만약 `noexcept` 소멸자 외부로 예외가 빠져나가거나 이미 진행 중인 스택 되감기 도중에 또 다른 예외가 발생하면 프로그램은 `std::terminate`를 호출합니다. 객체 슬라이싱과 불필요한 복사를 방지하려면 예외는 일반적으로 참조, 주로 `const&`로 잡아야 합니다. 백엔드 서비스에서는 요청 핸들러, 워커 스레드 진입점, RPC/HTTP 프레임워크 콜백, `main` 함수 등 명확한 예외 경계를 정의해야 합니다. 이 경계에서 예외를 로깅하고, 에러 응답이나 상태 코드로 변환하며, C API, 소멸자, 스레드, `noexcept` 함수와 같이 부적절한 컨텍스트로 예외가 전파되지 않도록 차단해야 합니다.

struct Conn {
    ~Conn() noexcept { /* close or return connection to pool */ }
};

void handle() {
    Conn c;
    throw std::runtime_error("db failed");
} // c is destroyed while the exception propagates
AI 코치와 함께 이 질문에 답해 보세요

8소멸자에서 예외가 발생하면 어떻게 되며, 백엔드 타입은 정리(cleanup) 작업 실패를 어떻게 보고해야 하나요?

소멸자는 일반적인 경우 암시적으로 `noexcept(true)`이므로, 소멸자 밖으로 예외가 빠져나가면 `std::terminate`가 호출됩니다. 소멸자를 명시적으로 `noexcept(false)`로 선언하더라도 스택 해제(stack unwinding) 도중에 예외를 던지는 것은 위험한데, 다른 예외가 이미 활성화된 상태에서 두 번째 예외가 탈출하면 마찬가지로 프로그램이 강제 종료되기 때문입니다. 따라서 소멸자는 최선의 정리 작업을 수행해야 하며 예외가 외부로 탈출하지 않도록 해야 합니다. 백엔드 타입은 정리 작업 실패를 소멸 전에 에러나 `std::expected`를 반환하거나 예외를 던지는 `close()`, `flush()`, `commit()`, `stop()`, `shutdown()` 같은 명시적인 연산을 통해 보고해야 합니다. 소멸자에서는 로깅, 메트릭 전송, 에러 억제 또는 안전한 대체 정리를 수행할 수 있지만, 조치 가능한 실패에 대한 주요 에러 보고 수단이 되어서는 안 됩니다.

class FileWriter {
public:
    std::expected<void, Error> close() noexcept; // reports flush/close failure explicitly

    ~FileWriter() noexcept {
        if (!closed_) {
            auto r = close();
            if (!r) log_error(r.error()); // destructor does not throw
        }
    }
private:
    bool closed_ = false;
};
AI 코치와 함께 이 질문에 답해 보세요

표준 라이브러리

9`std::vector`는 용량(capacity), 확장(growth), 재할당(reallocation) 및 반복자 유효성(iterator stability)을 어떻게 관리하나요?

`std::vector`는 요소를 연속된 메모리 공간에 저장하며, 크기(size)와 용량(capacity)을 모두 추적합니다. `size`는 생성된 요소의 개수이며, `capacity`는 추가 할당 없이 사용할 수 있도록 이미 확보된 저장 공간의 크기입니다. 요소를 추가할 때 용량을 초과하면, `vector`는 일반적으로 구현체에 정의된 기하급수적 확장 전략에 따라 더 큰 메모리 블록을 할당하고, 기존 요소를 새 위치로 이동하거나 복사한 뒤 이전 요소를 소멸시키고 기존 메모리를 해제합니다. `reserve(n)`은 크기 변경 없이 용량만 늘리며, `resize(n)`은 요소를 생성하거나 소멸시켜 크기를 변경합니다. 재할당이 발생하면 기존 요소에 대한 모든 반복자, 참조, 포인터가 무효화됩니다. 재할당이 발생하지 않더라도 `insert`나 `erase` 같은 연산은 변경 지점 및 그 이후의 위치를 가리키는 반복자 등을 무효화할 수 있습니다.

std::vector<int> v;
v.reserve(100);          // capacity >= 100, size == 0
v.push_back(1);          // size == 1
v.resize(10);            // size == 10, adds nine zero-initialized ints

std::cout << v.size() << " " << v.capacity() << "\n";
AI 코치와 함께 이 질문에 답해 보세요

10연속 메모리 기반 표준 컨테이너와 노드 기반 표준 컨테이너에 대해 숙지해야 할 무효화 규칙은 무엇인가요?

무효화 규칙은 컨테이너와 수행하는 연산에 따라 달라집니다. vector 및 string과 같은 연속(contiguous) 컨테이너는 반복자/참조 안정성이 취약합니다. 요소가 추가되어 크기가 늘어날 때 재할당이 발생하면 모든 반복자, 참조, 포인터가 무효화됩니다. 또한 재할당이 일어나지 않더라도 insert나 erase 연산은 요소를 이동시키므로 변경이 발생한 위치 및 그 이후 위치의 요소들이 무효화됩니다. list, map, set 및 이들의 multi 변형과 같은 노드 기반 정렬 컨테이너는 insert 연산 시 삭제되지 않은 기존 요소에 대한 반복자와 참조가 일반적으로 유지됩니다. 요소를 삭제(erase)하면 삭제된 해당 요소에 대한 반복자/참조만 무효화됩니다. 순서가 없는(unordered) 컨테이너 역시 요소를 노드에 저장하므로 재해시(rehash)가 발생해도 요소에 대한 참조와 포인터는 대체로 유지되지만, 재해시는 반복자를 무효화합니다. deque는 분할 메모리 구조(segmented-storage)에 따른 독자적인 규칙을 따릅니다. 실무에서는 컨테이너 수정 작업을 거치면서 반복자나 참조를 유지해야 할 때 해당 컨테이너와 연산별 세부 규칙을 반드시 확인해야 합니다.

std::map<int, std::string> m = {{1,"a"}, {2,"b"}, {3,"c"}};
for (auto it = m.begin(); it != m.end(); ) {
    if (it->first % 2 == 1)
        it = m.erase(it); // returns next iterator
    else
        ++it;
}
AI 코치와 함께 이 질문에 답해 보세요

11백엔드 룩업 테이블 관점에서 std::map, std::unordered_map, 그리고 플랫 맵(flat-map) 스타일 컨테이너를 비교해 주세요.

std::map은 보통 트리 기반으로 구현된 정렬 연관 컨테이너로, 탐색·삽입·삭제에 로그 시간이 소요됩니다. 정렬된 순회, 범위 쿼리, 또는 순서 보장이 중요할 때 유용합니다. std::unordered_map은 해시 테이블 기반으로 키 순서를 보장하지 않지만 평균적으로 상수 시간의 정확한 키 연산을 제공하며, 해싱 품질이 좋을 때 대규모 가변 룩업 테이블의 좋은 기본 선택지가 됩니다. 플랫 맵 스타일 컨테이너는 정렬된 키/값 쌍을 연속된 메모리에 저장하여 캐시 지역성이 우수하고 순회 및 이진 탐색이 빠르지만, 중간 삽입과 삭제에는 선형 시간이 소요됩니다. 백엔드 룩업 테이블용 컨테이너를 고를 때는 순서/범위 지원 필요 여부, 단순 조회 중심인지, 잦은 변경이 발생하는지, 지연 시간의 예측 가능성, 메모리 오버헤드, 캐시 동작 특성 등을 고려해야 합니다.

// Exact lookup, frequently updated:
std::unordered_map<std::string, User> users_by_id;

// Need sorted iteration or lower_bound/range queries:
std::map<std::string, User> users_by_id_ordered;

// Build once, query many times: vector sorted by key is a common flat-map style.
std::vector<std::pair<std::string, User>> users;
std::sort(users.begin(), users.end(), [](auto const& a, auto const& b) {
    return a.first < b.first;
});

auto it = std::lower_bound(users.begin(), users.end(), std::string_view{"u123"},
    [](auto const& p, std::string_view key) { return p.first < key; });
AI 코치와 함께 이 질문에 답해 보세요

12std::optional과 값이 없는 상태를 표현하기 위해 백엔드에서 일반적으로 활용되는 사용 사례를 설명해 주세요.

`std::optional<T>`는 포함된 T 값 또는 값이 없는 상태 중 하나를 나타냅니다. 비어 있는 상태는 `std::nullopt`로 표현되며, 코드에서는 `has_value()`를 확인하거나 불리언(boolean) 문맥에서 optional을 평가할 수 있고, `*`나 `value()`로 값에 접근하며, `value_or()`를 통해 기본값을 제공할 수 있습니다. 백엔드 코드에서는 null이 허용되는 데이터베이스 필드, 선택적 요청/설정 필드, 부재가 예상되는 캐시나 리포지토리 미스, 그리고 -1이나 빈 문자열 같은 센티널 값이 모호할 수 있는 도메인 상태를 다룰 때 유용합니다. 이는 다형성이나 상세한 오류 정보를 모델링하기 위한 것이 아니라, 오직 값의 부재만을 모델링합니다.

struct UserProfile {
    std::string id;
    std::optional<std::string> display_name; // absent if user has not set it
};

std::string label(UserProfile const& u) {
    return u.display_name.value_or("anonymous");
}
AI 코치와 함께 이 질문에 답해 보세요

13std::string_view와 std::span은 무엇이며, 비소유 뷰(non-owning view)는 어떤 수명 위험을 유발하나요?

std::string_view는 연속된 문자 시퀀스를 가리키는 비소유 뷰이며, std::span<T>는 T 타입의 연속된 시퀀스를 가리키는 비소유 뷰입니다. 두 타입 모두 메모리를 할당하거나 소유하지 않고 포인터와 길이 정보만 전달하므로, 제로 카피 매개변수나 버퍼 API에 매우 유용합니다. 주요 위험 요소는 객체 수명입니다. 뷰가 참조하는 원본 스토리지는 뷰 자체보다 수명이 길어야 하며, 뷰를 사용하는 동안 무효화(invalidation)되지 않아야 합니다. 임시 객체, 지역 객체, 이미 소멸된 객체, 또는 재할당된 컨테이너를 가리키는 뷰를 반환하거나 저장하면 댕글링 뷰(dangling view)가 발생할 수 있습니다.

std::string_view bad() {
    std::string s = "hello";
    return std::string_view{s}; // dangling after return
}

void ok(std::string_view name) {
    // safe only during this call if caller's data outlives the call
}
AI 코치와 함께 이 질문에 답해 보세요

동시성

14std::mutex, std::shared_mutex, std::recursive_mutex의 차이점은 무엇이며, 각각 어떤 상황에서 선택해야 합니까?

std::mutex는 배타적 락(exclusive lock)을 제공합니다. 한 번에 하나의 스레드만 획득할 수 있으므로, 변경 가능한 공유 상태를 보호할 때 기본적으로 선택합니다. std::shared_mutex는 공유 읽기 락과 배타적 쓰기 락을 지원합니다. 여러 읽기 작업자가 동시에 락을 보유할 수 있지만, 쓰기 작업자는 배타적 접근 권한이 필요합니다. 읽기 작업의 동시성이 오버헤드를 감수할 만큼 가치 있고, 쓰기 작업자의 공정성이나 기아 상태(starvation)를 허용하거나 처리할 수 있는 읽기 위주의 데이터에 적합합니다. std::recursive_mutex는 동일한 스레드가 여러 번 락을 획득할 수 있고 획득한 횟수만큼 해제해야 하는 배타적 뮤텍스입니다. 미흡한 락 설계를 감추게 될 위험이 있으므로, 주로 레거시 코드나 재진입(re-entrant) 코드를 다룰 때처럼 제한적인 경우에만 드물게 사용해야 합니다.

std::mutex m;
std::shared_mutex sm;
std::recursive_mutex rm;

// Default exclusive protection
{
    std::lock_guard<std::mutex> lk(m);
    // modify shared state
}

// Read-mostly protection
{
    std::shared_lock<std::shared_mutex> read_lk(sm); // many readers allowed
    // read shared state
}
{
    std::unique_lock<std::shared_mutex> write_lk(sm); // exclusive writer
    // modify shared state
}

// Recursive locking by same thread, usually avoid if possible
void legacy_api() {
    std::lock_guard<std::recursive_mutex> lk(rm);
    // may call another function that also locks rm
}
AI 코치와 함께 이 질문에 답해 보세요

언어 기능

15람다 캡처 모드와 콜백에서 캡처가 객체 수명과 상호작용하는 방식을 설명해 주세요.

람다는 변수를 값으로 캡처(`[x]` 또는 `[=]`), 참조로 캡처(`[&x]` 또는 `[&]`), `this`를 캡처하거나 `[p = std::move(ptr)]`와 같은 초기화 캡처(init-capture)를 사용할 수 있습니다. 값 캡처는 람다가 생성될 때 캡처 대상 객체를 클로저 내부로 복사합니다. 반면 참조 캡처는 참조를 보관하므로 람다가 호출되는 모든 시점 동안 원본 객체가 살아 있어야 합니다. 콜백, 저장된 람다 또는 비동기 작업에서는 호출 전에 지역 변수나 대상 객체가 소멸할 수 있어 참조 캡처와 `this` 캡처가 위험합니다. 필요한 데이터는 값으로 캡처하고, 소유권 이전에는 이동/초기화 캡처를 사용하며, 객체 수명을 연장하거나 유효성을 검사해야 할 때는 명시적으로 `shared_ptr`/`weak_ptr` 패턴을 사용하는 것이 좋습니다.

std::function<void()> make_bad_callback() {
    std::string name = "job";
    return [&] {
        // BUG: name is destroyed when make_bad_callback returns.
        std::cout << name << '\n';
    };
}

std::function<void()> make_good_callback() {
    std::string name = "job";
    return [name] {
        std::cout << name << '\n';
    };
}
AI 코치와 함께 이 질문에 답해 보세요