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().
}
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` 객체를 생성할 때 일반적으로 선호됩니다.
3스마트 포인터에서 사용자 정의 삭제자는 어떻게 동작하며, C/C++ 상호 운용성 측면에서 언제 유용한가요?
사용자 정의 삭제자는 스마트 포인터가 소유한 리소스를 해제할 때 기본 delete 연산 대신 실행되는 호출 가능한 정리 로직입니다. fclose, close, curl_easy_cleanup, SSL_free, free 또는 라이브러리 전용 해제 함수처럼 특정 함수로 리소스를 정리해야 하는 C/C++ 상호 운용 작업에서 유용합니다. unique_ptr의 경우 삭제자 타입이 unique_ptr 타입의 일부가 되므로 포인터 크기에 영향을 줄 수 있습니다. 상태가 없는 삭제자는 최적화되어 크기를 차지하지 않을 수 있지만, 함수 포인터나 상태를 지닌 삭제자는 추가 메모리를 차지합니다. shared_ptr의 경우 삭제자가 제어 블록에 저장되며, 마지막 강한 소유자가 객체를 해제할 때 실행됩니다. 사용자 정의 삭제자를 사용하면 C 스타일 리소스도 RAII(Resource Acquisition Is Initialization) 패턴에 안전하게 참여시킬 수 있습니다.
4순환 참조, enable_shared_from_this, 참조 카운트 비용을 포함하여 weak_ptr와 shared_ptr의 제어 블록(control block) 동작 메커니즘을 설명해 주세요.
`shared_ptr`는 강한 참조 카운트(strong reference count), 약한 참조 카운트(weak reference count), 그리고 삭제자(deleter)나 할당자(allocator)와 같은 정리 정보를 포함하는 제어 블록을 통해 공유 소유권을 관리합니다. `shared_ptr` 객체를 복사하거나 소멸시키면 일반적으로 원자적 연산을 통해 강한 참조 카운트가 증가하거나 감소하므로, 서로 다른 스레드에서 개별 `shared_ptr` 인스턴스를 안전하게 조작할 수 있지만 매 참조 카운트 갱신마다 비용이 발생합니다. 강한 참조 카운트가 0에 도달하면 관리 대상 객체가 소멸하지만, 제어 블록은 약한 참조 카운트까지 모두 0이 될 때까지 메모리에 유지됩니다. `weak_ptr`는 객체 수명을 연장하지 않고 동일한 제어 블록을 가리킵니다. `lock()` 메서드는 객체가 여전히 유효하면 `shared_ptr`를 반환하고, 이미 소멸했다면 빈 `shared_ptr`를 반환합니다. `shared_ptr`로만 구성된 순환 참조는 강한 참조 카운트가 0에 도달하지 않아 메모리 누수가 발생하므로, 역방향 포인터나 관찰자(observer) 링크에는 `weak_ptr`를 사용해야 합니다. `enable_shared_from_this`는 이미 `shared_ptr`로 관리 중인 객체가 기존 제어 블록을 재사용하여 자기 자신을 가리키는 새 `shared_ptr`를 안전하게 생성할 수 있도록 하여, 독립된 제어 블록이 중복 생성되는 위험을 방지합니다. 참조 카운트 비용으로는 원자적 증감 연산, 캐시 경합(cache contention), 제어 블록 할당 오버헤드, 성능상 중요한 코드 경로에서의 실행 지연 등이 있습니다.
struct Parent;
struct Child {
std::weak_ptr<Parent> parent; // non-owning back-reference
};
struct Parent {
std::vector<std::shared_ptr<Child>> children; // owns children
};
void example() {
auto p = std::make_shared<Parent>();
auto c = std::make_shared<Child>();
c->parent = p;
p->children.push_back(c);
if (auto locked = c->parent.lock()) {
// safe use while Parent is still alive
}
}
5이동 시맨틱(move semantics)이란 무엇이며, 올바른 이동 생성자와 이동 대입 연산자는 어떻게 구현하나요?
이동 시맨틱(move semantics)은 C++에서 임시 객체나 더 이상 필요하지 않은 객체의 자원을 복사하는 대신 소유권을 이전할 수 있게 해 주는 기능입니다. T&&와 같은 rvalue 참조와 이동 오버로드가 선택되도록 캐스팅하는 std::move를 사용하지만, std::move 자체가 무언가를 직접 이동시키지는 않습니다. 올바른 이동 생성자는 원본 객체의 자원을 가져오고 원본 객체를 유효하면서도 소멸 및 대입이 가능한 상태로 남겨둠으로써 새 객체를 초기화합니다. 올바른 이동 대입 연산자는 기존 객체로 자원을 이전하며, 자기 대입(self-assignment)을 적절히 처리하거나 허용하고, 대상 객체의 기존 자원을 해제하거나 재사용한 후 원본 자원을 가져오고 원본을 안전한 상태로 남겨둡니다. 또한 이동 연산은 noexcept로 선언하는 것이 권장되며, 그래야 표준 컨테이너가 재할당 시 예외 안전성 보장을 유지하면서 이동 연산을 활용할 수 있습니다.
6C++ API에서 const 정확성(const-correctness)의 개념과 const 멤버 함수를 효과적으로 설계하는 방법을 설명해 주세요.
const 정확성(const-correctness)은 어떤 연산이 객체의 관찰 가능한 상태나 논리적 상태를 수정하지 않는지를 타입 시스템을 통해 명시하는 것을 의미합니다. const 멤버 함수는 `const` 한정자가 붙은 `this` 객체를 가지므로, `mutable`이 아닌 데이터 멤버를 수정하거나 동일한 객체의 비-const 멤버 함수를 호출할 수 없습니다. 훌륭한 API 설계는 읽기 전용 조회 메서드를 const로 지정하고, 상황에 맞게 값이나 const 참조/포인터를 반환하며, const 함수에서 수정 가능한 내부 상태를 외부에 노출하지 않는 것입니다. `mutable`은 캐시, 지연 평가된 값, 메트릭, 뮤텍스처럼 객체의 논리적 상태를 변경하지 않는 세부 구현에 한해 제한적으로 사용해야 합니다. const는 변경 가능성에 대한 API 계약일 뿐 스레드 안전성을 자동으로 보장하지 않으므로, 동시성 보장은 별도로 구현하고 문서화해야 합니다.
7이동 전용 타입(move-only type)이란 무엇이며, 소켓, 파일 핸들, 락(lock)의 API(Application Programming Interface) 경계에 어떤 영향을 미치나요?
이동 전용 타입은 복사는 불가능하지만 이동은 가능한 타입을 의미합니다. 소켓, 파일 디스크립터, 파일 핸들, 락, `std::unique_ptr`처럼 리소스의 단독 소유권이 필요한 대상에 주로 사용됩니다. 소유권의 중복과 이중 해제를 방지하기 위해 복사 연산은 삭제(delete)되며, 이동 연산은 소유권을 이전하고 원본 객체를 유효하지만 일반적으로 비어 있거나 소유권이 없는 상태로 남겨둡니다. API는 소유권 경계를 명확히 해야 합니다. 팩토리 함수는 이동 전용 객체를 값으로 반환할 수 있고, 소유권을 넘겨받는 함수는 값이나 rvalue 참조로 전달받을 수 있으며, 단순히 조회만 하는 함수는 참조, 포인터, 기타 대여 핸들을 사용해야 합니다. 컨테이너의 경우 요소가 이동 방식으로 삽입되거나 재배치될 때 이동 전용 값을 저장할 수 있습니다.
#include <utility>
class Fd {
public:
explicit Fd(int fd = -1) noexcept : fd_(fd) {}
~Fd() { close_if_valid(); }
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) {
close_if_valid();
fd_ = std::exchange(other.fd_, -1);
}
return *this;
}
int get() const noexcept { return fd_; }
explicit operator bool() const noexcept { return fd_ != -1; }
private:
void close_if_valid() noexcept {
if (fd_ != -1) {
// ::close(fd_); // real POSIX code would close here
fd_ = -1;
}
}
int fd_;
};
Fd open_socket(); // factory returns ownership by value
void take_socket(Fd fd); // takes ownership
void inspect_socket(const Fd&); // borrows only
void example() {
Fd s = open_socket();
inspect_socket(s);
take_socket(std::move(s));
}
8일반적인 백엔드 코드에서 auto, decltype, decltype(auto), 그리고 템플릿 인자 추론(template argument deduction)을 비교해 주세요.
auto는 변수에 대해 템플릿과 유사한 추론 방식을 사용합니다. 단순히 auto만 사용하면 auto&, const auto&, auto&&와 같이 선언에서 명시적으로 요구하지 않는 한 참조와 최상위(top-level) const가 보통 탈락됩니다. decltype(expr)은 선언된 타입이나 표현식 타입을 더 정확하게 조사합니다. 괄호가 없는 식별자 표현식(id-expression)은 선언된 타입을 그대로 반환하며, 그 외의 lvalue 표현식은 T&, xvalue는 T&&, prvalue는 T를 생성합니다. decltype(auto)는 decltype의 규칙을 사용하여 타입을 추론하며, 참조를 보존해야 하는 반환 타입에 주로 사용됩니다. 템플릿 인자 추론은 auto와 유사하지만 T, T&, const T&, T&&와 같은 매개변수 형태에 따라 달라지며 고유한 규칙을 갖습니다. 중괄호 초기화 목록(braced initializer)이 흔한 차이점 중 하나입니다. auto x = {1,2}는 std::initializer_list<int>로 추론되지만, 일반 템플릿 매개변수는 매개변수가 initializer_list나 다른 적절한 타입을 요구하지 않는 한 단순한 중괄호 초기화 목록으로부터 T를 추론할 수 없습니다.
int x = 1;
const int cx = 2;
int& rx = x;
auto a = rx; // int, copy of x
auto b = cx; // int, top-level const dropped
const auto& c = cx; // const int&, reference preserved by declaration
a = 10; // does not change x
90의 규칙(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의 규칙을 사용합니다.
10initializer_list 및 집합체를 포함한 C++ 초기화 방식과 흔한 함정에 대해 설명해 주세요.
C++에는 여러 가지 초기화 방식이 존재합니다. `T x;`와 같은 기본 초기화(default initialization)는 클래스 타입의 경우 기본 생성자를 호출하지만, 자동 저장 기간을 갖는 기본 타입(fundamental type) 변수는 초기화되지 않은 상태로 둡니다. `T x{};` 또는 `T()`와 같은 값 초기화(value initialization)는 생성자나 멤버 초기화가 실행되기 전에 해당되는 부분을 0으로 초기화(zero-initialize)합니다. 중괄호를 사용하는 리스트 초기화(list initialization)는 좁히기 변환(narrowing conversion)을 허용하지 않으며, 사용 가능한 `std::initializer_list` 생성자를 강력하게 우선 선택하는 등 고유한 오버로드 확인 규칙을 가집니다. 집합체 초기화(aggregate initialization)는 중괄호를 사용해 집합체 멤버를 직접 초기화하며, C++20부터는 집합체에 대해 선언 순서대로 지정된 초기화(designated initializers)도 지원합니다. 흔히 겪는 함정으로는 초기화되지 않은 지역 스칼라 변수, `initializer_list` 오버로드의 의도치 않은 우선 선택, 중괄호 사용 시의 좁히기 변환 에러, 소괄호 사용 시의 최악의 파싱(most vexing parse), 그리고 특정 타입이 더 이상 집합체가 아니게 되었을 때 발생하는 동작 변경 등이 있습니다.
#include <iostream>
struct S {
int x;
};
int main() {
int a; // default-initialized: indeterminate value
int b{}; // value/list-initialized: zero
S s{}; // aggregate/value initialization: s.x is zero
std::cout << b << ' ' << s.x << '\n';
(void)a; // reading a would be undefined behavior
}
11C++에서 정의되지 않은 동작(undefined behavior)이란 무엇이며, 백엔드 프로덕션 장애에서 어떻게 나타날 수 있나요?
정의되지 않은 동작(UB, Undefined Behavior)은 유효하지 않은 연산이 발생한 후 C++ 표준이 아무런 요구 사항도 규정하지 않는 동작을 의미합니다. 프로그램이 정상적으로 작동하는 것처럼 보일 수도 있고, 충돌(crash)이 발생하거나, 데이터가 손상되거나, 보안 취약점이 노출되거나, 컴파일러 최적화로 인해 예상치 못한 동작으로 이어질 수도 있습니다. 컴파일러는 UB가 발생하지 않는다고 가정하고 최적화를 수행하기 때문에, 릴리스 빌드나 프로덕션 트래픽 환경에서만 문제가 표면화되기도 합니다. 백엔드 장애는 댕글링 포인터/참조(dangling pointer/reference), use-after-free, 객체 수명 위반, 경계 초과 접근(out-of-bounds access), 부호 있는 정수 오버플로, 데이터 레이스(data race), 잘못된 캐스팅, 초기화되지 않은 메모리 읽기, 이중 해제(double free), strict-aliasing 위반 등에서 비롯될 수 있습니다. 완화 대책으로는 RAII (Resource Acquisition Is Initialization) 및 명확한 소유권/수명 설계, 안전한 추상화 및 경계 검사, 테스트/퍼징(fuzzing), 코드 리뷰, 정적 분석, 그리고 ASan(AddressSanitizer), UBSan(UndefinedBehaviorSanitizer), TSan(ThreadSanitizer)과 같은 새니타이저(sanitizer) 활용 등이 있습니다.
int grow(int x) {
if (x + 1 < x) {
return 0; // compiler may assume this is unreachable
}
return x + 1;
}
12가상 함수, 가상 함수 테이블(vtable), 동적 디스패치 비용, 객체 슬라이싱, 가상 소멸자 위험에 대해 설명해 주세요.
가상 함수는 런타임 다형성을 가능하게 합니다. 기반 클래스의 포인터나 참조를 통해 호출될 때 객체의 동적 타입에 맞는 구현이 선택됩니다. 대부분의 구현에서는 각 다형성 객체 내에 숨겨진 vptr를 저장하여 해당 동적 타입의 가상 함수 주소들이 담긴 vtable을 가리키도록 합니다. 일반적인 비용으로는 객체 내 추가 포인터 공간, 간접 호출 오버헤드, 캐시 및 분기 예측 영향 가능성, 인라이닝 기회의 감소 등이 있지만 컴파일러가 때때로 역가상화(devirtualization)를 수행하기도 합니다. 객체 슬라이싱(object slicing)은 파생 클래스 객체가 기반 클래스 객체로 값 복사되거나 값으로 저장될 때 발생하며, 이 과정에서 파생 부분과 동적 동작이 잘려 나가 손실됩니다. 기반 클래스 포인터를 통해 객체를 삭제하려는 경우 기반 클래스의 소멸자는 반드시 가상 소멸자여야 합니다. 그렇지 않으면 기반 포인터를 통해 파생 객체를 삭제할 때 정의되지 않은 동작(undefined behavior)이 발생합니다.
13서비스 타임아웃, 메트릭, 타임스탬프를 다룰 때 std::chrono의 시계(clock), time_point, duration의 역할을 설명해 주세요.
`std::chrono`는 시계(clock), `time_point`, `duration`을 통해 시간을 모델링합니다. `duration`은 밀리초나 초와 같은 단위를 가진 시간 간격입니다. `time_point`는 특정 시계의 시간선상에 있는 한 지점을 나타냅니다. `steady_clock`은 단조 시계(monotonic clock)이므로 시스템 시간(wall-clock) 변경의 영향을 받지 않아 경과 시간 측정, 서비스 타임아웃, 마감 시한(deadline), 지연 시간 측정에 사용해야 합니다. `system_clock`은 일상/시스템 시간을 나타내며 타임스탬프, 로깅, 영속화, 달력 변환에 적합하지만 시스템 시간이 조정되면 시간이 건너뛸 수 있습니다. 타임아웃은 보통 `steady_clock::now() + duration`을 기반으로 설정해야 하며, 머신 타임스탬프는 명확히 문서화된 시스템 시간 형식(일반적으로 API나 스토리지 경계에서는 UTC)을 사용해야 합니다.
using namespace std::chrono_literals;
auto deadline = std::chrono::steady_clock::now() + 500ms;
while (!done()) {
if (std::chrono::steady_clock::now() >= deadline) {
throw std::runtime_error("timeout");
}
poll_once();
}
14iostreams 및 printf 스타일 API와 비교하여 std::format과 최신 포매팅 기능을 설명해 주세요.
`std::format`은 fmtlib에서 영감을 받아 C++20에 도입된 타입 안전한 포매팅 기능입니다. iostreams의 상태 기반 삽입 구문이나 `printf` 스타일의 C 가변 인자(varargs) 없이도 `{}` 대체 필드와 포맷 지시자를 사용해 포맷된 텍스트를 생성합니다. `printf`와 비교하면 타입 불일치 문제를 대폭 방지할 수 있으며, iostreams와 비교하면 가독성이 높고 조합하기 쉽습니다. fmtlib은 `std::format`에 앞서 널리 쓰이며 영향을 준 라이브러리로, 더 폭넓은 지원과 최신 기능을 제공하기도 합니다. 사용자 정의 타입 또한 커스텀 포매터 지원을 구현하여 포매팅할 수 있습니다. 대규모 로깅 환경에서는 불필요한 포매팅, 변환, 할당을 방지하는 것(특히 비활성화된 로그 레벨에 대해)이 성능을 좌우하므로, 포매팅을 지연 처리하거나 로그 레벨을 먼저 확인하는 API를 사용하는 것이 바람직합니다.
15C++의 값 범주(value category)와 완벽한 전달(perfect forwarding)을 설명하고, 이것이 효율적인 제네릭 API에서 왜 중요한지 설명해 주세요.
C++의 값 범주는 표현식(expression)의 특성을 나타냅니다. lvalue는 고유한 식별성(identity)을 가지며 표현식 평가 이후에도 참조될 수 있습니다. prvalue는 다수의 임시 객체나 계산된 값과 같은 순수 rvalue입니다. xvalue는 자원을 재사용할 수 있는 만료 직전의 객체(expiring object)입니다. 완벽한 전달은 전달 참조(forwarding reference, 보통 타입 추론이 일어나는 T&&)를 받고 std::forward<T>(arg)를 사용하여 호출자의 값 범주를 그대로 유지하면서 전달하는 템플릿 기법입니다. 이를 통해 lvalue는 lvalue로, rvalue는 rvalue로 유지되며, 이는 참조 축약(reference collapsing) 규칙 덕분에 가능합니다. 래퍼(wrapper), 팩토리(factory), 디스패처(dispatcher), emplace 스타일 함수 등에서 불필요한 복사를 방지하고 오버로드 선택 및 이동 동작(move behavior)을 온전히 보존할 수 있으므로 제네릭 백엔드 API에서 매우 중요합니다.
16`std::scoped_lock` 및 `std::lock`을 활용한 교착 상태(deadlock) 방지 및 다중 뮤텍스 락 전략에 대해 설명해 주세요.
교착 상태는 순환 대기를 피함으로써 방지할 수 있습니다. 가능한 경우 일관된 전역 순서로 락을 획득하거나, 교착 상태 방지 알고리즘을 사용하는 `std::lock` 또는 `std::scoped_lock`을 통해 여러 뮤텍스를 한 번에 획득합니다. `std::scoped_lock lock(a, b, ...)`은 여러 뮤텍스를 잠그고 스코프를 벗어날 때 자동으로 해제하는 가장 간결한 RAII (Resource Acquisition Is Initialization) 방식입니다. `std::lock`을 사용할 때는 먼저 뮤텍스들을 잠근 후 `std::adopt_lock`을 사용하여 RAII 래퍼에 소유권을 넘기거나, `std::defer_lock`과 함께 `std::unique_lock`을 사용할 수 있습니다. 임계 영역은 짧게 유지하고 락을 쥐고 있는 동안에는 블로킹 작업이나 예측할 수 없는 콜백 함수 호출을 피해야 합니다. 단, 교착 상태 회피가 공정성(fairness)을 보장하거나 모든 라이브락/기아(starvation) 현상을 완전히 방지하지는 않습니다.
struct Account {
std::mutex m;
int balance{};
};
void transfer(Account& from, Account& to, int amount) {
if (&from == &to) return;
std::scoped_lock lock(from.m, to.m);
if (from.balance >= amount) {
from.balance -= amount;
to.balance += amount;
}
}
뮤텍스로 보호되는 상태 조건(predicate)의 변경을 대기할 때 `std::condition_variable`을 사용합니다. 동일한 뮤텍스를 획득한 상태에서 공유 상태를 수정한 후, `notify_one`이나 `notify_all`을 호출해 대기 중인 스레드를 깨웁니다. 가짜 깨어남(spurious wakeup)이 발생할 수 있고 조건 상태와 별개로 알림이 보존되지 않으므로, 대기하는 측에서는 `cv.wait(lock, predicate)` 또는 이에 상응하는 루프를 사용해야 합니다. `notify_one`은 대기 중인 하나의 스레드를 깨우며, `notify_all`은 모든 대기 스레드를 깨우므로 종료 처리와 같은 브로드캐스트 형태의 상태 변경에 적합합니다.
18표준 C++ 기본 요소를 사용하여 스레드 안전한 유한 큐(bounded queue)를 설계하고 해당 API 보장 사항을 정의하세요.
스레드 안전한 유한 큐는 `std::mutex`, 조건 변수(condition variable), 고정 용량 컨테이너, 그리고 종료/닫힘(closed) 플래그를 사용하여 구현할 수 있습니다. `push`는 큐가 가득 찼을 때 대기(block), 타임아웃, 또는 실패 처리되어 배압(backpressure)을 제공하며, `pop`은 큐가 비어 있을 때 대기합니다. 두 연산 모두 `size < capacity || closed` 및 `!empty || closed`와 같은 조건 술어(predicate)를 기준으로 대기해야 합니다. 종료(shutdown/close) 시에는 대기 중인 생산자와 소비자를 모두 깨우고 새 `push`를 거부해야 하며, 소비자가 남아 있는 항목을 모두 비우고 종료할지 즉시 중단할지를 정의해야 합니다. API에는 블로킹 동작 방식, 종료 시점의 동작 규칙, 반환값, 동시성 보장 사항을 명시해야 합니다.
template<class T>
class BoundedQueue {
public:
explicit BoundedQueue(std::size_t cap) : cap_(cap) {}
// Returns false if the queue has been closed before the item is accepted.
bool push(T value) {
std::unique_lock<std::mutex> lk(m_);
not_full_.wait(lk, [&] { return q_.size() < cap_ || closed_; });
if (closed_) return false;
q_.push_back(std::move(value));
lk.unlock();
not_empty_.notify_one();
return true;
}
// Returns nullopt when closed and drained.
std::optional<T> pop() {
std::unique_lock<std::mutex> lk(m_);
not_empty_.wait(lk, [&] { return !q_.empty() || closed_; });
if (q_.empty()) return std::nullopt;
T value = std::move(q_.front());
q_.pop_front();
lk.unlock();
not_full_.notify_one();
return value;
}
void close() {
{
std::lock_guard<std::mutex> lk(m_);
closed_ = true;
}
not_empty_.notify_all();
not_full_.notify_all();
}
private:
const std::size_t cap_;
std::mutex m_;
std::condition_variable not_empty_;
std::condition_variable not_full_;
std::deque<T> q_;
bool closed_ = false;
};
단계적 확장-축소(expand-contract) 마이그레이션 방식을 사용합니다. 먼저 null 허용 컬럼이나 새 테이블 추가처럼 현재 실행 중인 C++ 서비스에 문제를 일으키지 않는 하위 호환 가능한 스키마 변경을 적용합니다. 구 버전과 신 버전 표현을 모두 처리할 수 있는 호환 코드를 배포하고, 기존 데이터를 쓰로틀링이 적용된 작은 배치 단위로 백필한 뒤 정합성을 검증합니다. 그 후 읽기 작업을 새 스키마로 전환하고, 배포된 모든 버전이 더 이상 이전 스키마에 의존하지 않게 되었을 때 비로소 이전 컬럼이나 코드를 제거합니다. 대용량 테이블의 경우 긴 락을 유발하는 블로킹 DDL을 피하고 지원되는 환경에서는 온라인/동시성(concurrent) 작업을 사용해야 하며, 락, 복제 지연, 리소스를 모니터링하고 코드가 부분적으로 배포되었거나 데이터가 일부만 마이그레이션된 상태에서도 작동하는 롤백 경로를 마련해 두어야 합니다.
1. Expand: add new nullable column user_display_name.
2. Deploy compatible code: write both name and user_display_name; read new if present, else old.
3. Backfill: copy name -> user_display_name in small batches.
4. Validate: compare counts/checksums/null rates and monitor errors.
5. Switch: read only user_display_name after confidence.
6. Contract: stop writing old name, then drop old column in a later release.
20C++ 서비스에서 오래된 데이터 읽기(stale read)를 관리하고 자신이 작성한 내용 읽기(read-your-writes) 보장을 유지하면서 데이터베이스 복제본(replica)으로부터 데이터를 읽도록 하려면 어떻게 설계해야 합니까?
복제본 읽기는 명시적인 일관성 절충(trade-off)으로 다루어야 합니다. 데이터 지연(staleness)을 허용할 수 있는 읽기 작업은 정상 상태의 복제본으로 보낼 수 있지만, 자신이 작성한 내용 읽기(read-your-writes), 트랜잭션 보장 또는 최신성이 중요한 읽기는 기본(primary) 데이터베이스로 보내야 합니다. 단, 선택한 복제본이 LSN(Log Sequence Number), 타임스탬프 또는 버전과 같은 관련 쓰기 위치를 이미 반영(replay)했음이 확인된 경우는 예외입니다. 복제 지연 시간(replica lag)과 상태를 지속적으로 추적하고, 엔드포인트나 요청별로 필요한 일관성 수준을 명시해야 합니다. 복제본의 지연이 허용 범위를 초과할 때는 기본 노드로 폴백(fallback)하거나, 복제 동기화를 기다리거나, 요청을 실패 처리하도록 구현합니다. 또한 호출자가 어떤 읽기 작업에서 지연된 데이터가 반환될 수 있는지 알 수 있도록 일관성 모델을 명확히 문서화해야 합니다.
ReadPolicy EVENTUAL(max_staleness=2s):
choose healthy replica with lag <= 2s; else primary or error depending on SLA
ReadPolicy READ_YOUR_WRITES(session_lsn):
choose replica only if replay_lsn >= session_lsn;
otherwise wait briefly or route to primary
ReadPolicy STRONG:
route to primary