[말랑 퀴즈] 26/09/15 해답

게시:     수정

카테고리:

태그:

제 말랑말랑 퀴즈 생성기는 이곳에서 확인하실 수 있습니다.

말랑말랑 퀴즈 — 해답지 ✅

날짜: 2026-09-15 문제 수: 6문제 총점: 49.1/60 (82점) ✅ 잘했어요

📊 채점 결과

문항 유형 난이도 결과 점수
Q1 객관식 🔴 어려움 ⭕ 정답 10 / 10
Q2 서술형 🟡 보통 🔺 부분 정답 6.0 / 10
Q3 빈칸 🟢 쉬움 🔺 부분 정답 6.7 / 10
Q4 서술형 · 📌 오답노트 🟡 보통 🔺 부분 정답 9.4 / 10
Q5 객관식 🟡 보통 ⭕ 정답 10 / 10
Q6 OX 🟡 보통 🔺 부분 정답 7.0 / 10
합계       49.1 / 60

총평

82점, 어제(84점)에 이어 두 번째로 높은 점수입니다. 객관식 두 문항이 모두 만점이고, 어려움 난이도 객관식은 이제 다섯 회차 연속 정답입니다.

Q4 오답노트가 9.4점입니다. 9월 9일 4.8점에서 시작해 여기까지 왔고, 실행 순서·재할당·원자성/일관성 짝짓기가 전부 정확했습니다. 딱 하나, 동기화 기법 셋에서 임계 영역 자리에 atomic을 쓰셔서 만점을 놓쳤습니다. 오답노트는 만점이어야 stage가 오르므로 아쉽게 stage 0에 머뭅니다. 다음엔 임계 영역 / 뮤텍스 / 잠금 세 단어만 챙기면 통과입니다.

Q6에서 근거를 안 쓰셨습니다. 9월 8일에 같은 일로 3점을 잃은 뒤 세 회차 연속 잘 쓰셨는데, 이번에 다시 빠졌습니다. 정답이 O인 회차에서 근거가 빠지는 경향이 보입니다 — “틀린 곳이 없으니 쓸 게 없다”고 느끼신 것 같은데, O일 때는 각 문단이 원문의 어느 사실과 맞아떨어지는지를 한 줄로 대응시키면 됩니다.

Q2와 Q3은 새 단원과 재출제 단원이 섞여 있습니다. Q2의 “클라이언트에 저장해도 되는 데이터”에 현재 세션의 HP를 쓰신 건 방향이 반대입니다 — HP는 조작되면 치팅이 되는 값이라 서버가 쥐어야 합니다. 클라이언트에 둬도 되는 건 UI 설정, 그래픽 옵션, 키 바인딩 같은 것들입니다. Q3에서는 첫 칸 mongos를 “해시 함수”라 쓰셨는데, 8월 23일 객관식에서 이미 만점을 받았던 단원이라 이름만 다시 붙이면 됩니다.


Q1. 🔴 어려움 — ⭕ 10/10

문제: Person 헤더에서 #include를 빼고 전방 선언으로 바꾼 시도가 실패하는 이유에 대한 설명으로 가장 올바른 것은?

namespace std { class string; }     // string 전방 선언
class Date;
class Address;

class Person {
    ...
private:
    std::string theName;
    Date        theBirthDate;
    Address     theAddress;
};
  • A. 전방 선언만으로도 충분하다. 컴파일러는 Person 객체의 크기를 링크 타임에 알아내므로, 멤버 타입의 정의는 컴파일 시점에 필요하지 않다. 실패한 이유는 단지 date.h를 링크 대상에서 빠뜨렸기 때문이다.
  • B. DateAddress는 전방 선언으로 해결되지만, std::string이 문제다. namespace std { class string; }은 올바른 선언이므로, 실패 원인은 <string>을 빼면 name()의 반환형을 알 수 없게 되는 것뿐이다. <string>만 다시 포함하면 된다.
  • C. 실패 이유는 두 가지다. ① string은 클래스가 아니라 typedef 로 정의되어 있어 class string;처럼 선언할 수 없고, ② 컴파일러가 Person p; 를 위한 공간을 잡으려면 Person의 크기를 알아야 하는데, 그러려면 멤버인 Date·Address정의가 필요하다. 해법은 pimpl 관용구 — 구현부를 별도 클래스로 빼고 그것을 가리키는 포인터만 갖는 것이다. 포인터는 크기가 고정이라 가리키는 타입의 선언만 있으면 된다.
  • D. pimpl의 두 방식 중 인터페이스 클래스는 가상 함수를 쓰지 않으므로 호출 비용이 없고, 핸들 클래스는 vtable 포인터를 가져야 해서 객체마다 메모리가 커진다. 따라서 성능이 중요하면 인터페이스 클래스를 골라야 한다.

📝 내 선택: C — A: 컴파일 시점에 객체의 크기를 알아야 하기 때문에 헤더가 필요하다. / B: string은 문제가 없었던 것으로 알고 있다. 문제가 생기는 것은 Date와 Address다. / D: 인터페이스 클래스와 핸들 클래스의 설명이 반대다. 그런데 pimpl에 인터페이스 방식이 있었는지는 헷갈린다.

정답: C

해설:

전방 선언이 실패하는 두 가지 이유

  1. string은 클래스가 아니라 typedef로 정의된다. std::stringbasic_string<char>의 별칭이므로 class string;이라는 선언 자체가 틀렸다.
  2. 컴파일 도중 객체의 크기를 알아야 한다. 아래 코드를 컴파일하려면 컴파일러는 intPerson을 담을 공간을 잡아야 하고, 그러려면 Person 객체 하나의 크기를 알아야 한다. Person의 멤버로 Date·Address 객체가 들어 있으니, 그 크기를 알려면 결국 정의를 봐야 한다.
int main() {
    int x;
    Person p{ params };    // ← Person의 크기를 모르면 공간을 잡을 수 없다
}

해법 — pimpl 관용구 (pointer to implementation)

#include <string>
#include <memory>

class PersonImpl;      // 전방 선언
class Date;
class Address;

class Person {
public:
    Person(const std::string& name, const Date& birthday, const Address& addr);
    std::string name() const;
    ...
private:
    std::shared_ptr<PersonImpl> pImpl;    // 구현부를 가리키는 포인터
};

인터페이스와 구현이 분리된다. Person의 멤버는 이제 포인터 하나뿐이고, 포인터는 가리키는 타입이 무엇이든 크기가 같으므로 PersonImpl선언만 있으면 컴파일된다. 실제 Date·Address 객체는 PersonImpl 안에 숨어 있어, 그 구현을 고쳐도 Person을 쓰는 파일은 다시 컴파일할 필요가 없다.

컴파일 의존성 최소화 전략 세 가지

  1. 객체 참조자 및 포인터로 충분하면 객체를 직접 쓰지 않는다. 참조자·포인터를 정의할 때는 그 타입의 선언부만 필요하고, 객체를 정의할 때는 정의가 필요하다.
  2. 클래스 정의 대신 선언에 최대한 의존한다. 함수를 선언할 때는 반환·인자 타입이 클래스여도 선언만 있으면 된다.
class Date;                     // 선언만
Date today();                   // ✅ 반환해도
void clearAppointments(Date d); // ✅ 전달해도 — 선언만으로 OK
  1. 선언부와 정의부에 대해 별도의 헤더 파일을 제공한다. <iosfwd>가 그 예다 — iostream 관련 선언부만 모아 둔 헤더.
  • A — 객체 크기는 컴파일 시점에 필요하다. 링크 타임 이야기는 사실이 아니며, #include를 뺀 시점에서 이미 컴파일이 실패한다.
  • Bnamespace std { class string; }틀린 선언이다(typedef이므로). 그리고 <string>을 다시 포함해도 Date·Address 객체 멤버의 크기 문제는 그대로 남는다.
  • D — 정반대다. 인터페이스 클래스가 순수 가상 함수 기반이라 모든 호출이 가상 함수 → vtable 점프 비용이 들고, 파생 객체가 vtable 포인터를 가져 메모리가 커진다. 핸들 클래스의 비용은 구현부 포인터를 한 번 거치는 연산 한 단계 증가와 포인터만큼의 메모리, 그리고 동적 할당 오버헤드다.
  핸들 클래스 인터페이스 클래스
구조 구현 클래스를 가리키는 포인터 멤버 순수 가상 함수 + 팩토리 함수(create)
비용 포인터 경유(연산 +1), 포인터만큼 메모리, 동적 할당 모든 호출이 가상 함수(vtable 점프), vtable 포인터 메모리
공통 단점 인라인 함수의 도움을 받기 어렵다  

실무 원칙: 개발 도중에는 핸들 클래스나 인터페이스 클래스를 쓰고, 그 단점이 크게 손해가 될 때 구체 클래스로 바꾼다.

🔍 피드백: 만점입니다. 어려움 난이도 객관식 다섯 회차 연속 정답입니다.

A와 D를 쳐낸 근거가 정확합니다. “컴파일 시점에 객체의 크기를 알아야 한다”— 이게 전방 선언이 실패하는 두 이유 중 핵심입니다. D도 “설명이 반대”라고 정확히 잡으셨습니다.

두 가지만 바로잡아 두겠습니다. 첫째, string은 문제가 있습니다. 원문이 실패 이유로 첫 번째로 꼽은 것이 바로 string입니다 — std::string은 클래스가 아니라 basic_string<char>typedef 라서 class string;이라는 전방 선언 자체가 성립하지 않습니다. Date·Address는 “크기를 모른다”는 문제이고, string은 “선언 문법이 틀렸다”는 별개의 문제입니다. 보기 C가 두 이유를 나눠 쓴 게 그래서입니다.

둘째, pimpl에는 인터페이스 방식이 있습니다. 원문이 pimpl 사용 방법을 두 가지로 나눕니다 — 핸들 클래스(구현 클래스를 가리키는 포인터를 멤버로)와 인터페이스 클래스(순수 가상 함수 + create 팩토리 함수). 둘 다 “정의가 아니라 선언에 의존하게 만든다”는 같은 목적이지만 비용이 다릅니다. 핸들은 포인터 한 번 거치는 값을, 인터페이스는 가상 함수 호출 값을 치릅니다. D가 그 둘을 뒤바꿔 놓은 것이었고, 그걸 “반대”라고 잡으신 건 정확합니다.

출처: game_dev/cpp/chapter5/2025-10-07-cpp_5_31.md


Q2. 🟡 보통 — 🔺 6.0/10

문제: 온라인 게임에서 플레이어 정보를 어디에 저장하느냐에 대해 답하시오.

(1) 클라이언트 저장의 문제 둘, 클라이언트에 저장해도 되는 데이터의 예 둘 (2) ㉠ 송금 1단계 후 중단되면? ㉡ 트랜잭션의 정의와 보장하는 성질 (3) ㉠ DB의 락은 멀티스레딩의 무엇과 비슷하며 무엇을 막는가 ㉡ 장애 내성의 장치와 그 역할

📝 내 풀이:

(1) 값을 마음대로 조작이 가능해진다. 다른 장치간 지속 플레이가 불가능하며, 잃어버리면 복구할 수 없다. 클라이언트에 저장해둬도 괜찮은 데이터는 현재 세션의 HP, 현재 세션의 시간 같은 값이다.

(2) ㄱ: 1의 계좌만 100만원 손해본다. ㄴ: 질의 명령 결과를 보장하는 것 / 원자성

(3) ㄱ: 뮤텍스, 값을 사용하는 동안 읽기/쓰기를 막아 일관성을 보장한다. ㄴ: 무슨 장치인지는 기억은 안나지만, 중단된 동안 들어온 질의를 기억해두었다가 재개되면 처리한다.

정답:

(1) 서버 저장의 이유와 예외

클라이언트에 저장하면 생기는 문제는 두 가지다.

  1. 해킹에 취약하다 — 클라이언트 측 데이터는 쉽게 조작할 수 있다.
  2. 크로스 플랫폼 플레이가 불가능하다 — 같은 사용자가 다른 기기를 쓸 때 이어서 할 수 없다.

그래서 온라인 게임은 서버에 저장한다. 온라인 게임이 아니더라도 서버에 저장하는 경우가 많은데, 스팀 클라우드가 그 예다.

다만 모든 것을 서버에 둘 필요는 없다. 사소한 데이터는 클라이언트에 둔다.

서버에 (중요한 데이터) 클라이언트에 (사소한 데이터)
게임 진행 상황, 아이템 인벤토리, 재화(골드·보석), 계정 정보 UI 설정, 그래픽 옵션, 키 바인딩, 사운드 볼륨

구분 기준은 조작되거나 잃어버려도 게임에 영향이 없는가다.

(2) 트랜잭션

㉠ 1단계만 끝난 뒤 중단되면 — 돈이 증발한다.

사용자 1의 계좌에서 100만 원은 빠져나갔는데, 사용자 2의 계좌에는 아직 들어가지 않았다. 두 계좌 어디에도 없는 100만 원이 생긴 것이다. 반대 순서였다면 100만 원이 복제되었을 것이다.

㉡ 정의와 성질

트랜잭션: 데이터베이스나 컴퓨터 시스템에서 논리적인 기능을 수행하기 위해, 더 이상 분할할 수 없는 최소한의 업무 처리 단위

트랜잭션이 보장하는 성질은 원자성이다 — 안에 든 작업이 모두 성공하거나, 모두 실패한다. 중간에 중단되면 이미 수행된 1단계를 되돌린다. 게임에서는 아이템 거래, 재화 이동, 계정 상태 변경, 중요 게임 이벤트에 트랜잭션이 필요하다.

(3) 락과 장애 내성

㉠ 락(Lock) — 멀티스레드의 임계 영역과 비슷한 개념

처리가 2개 이상 동시에 진행될 때, 한 데이터를 여러 처리가 동시에 액세스하면 이상한 결과가 나올 수 있다. 락은 이를 막는다 — 멀티스레딩에서 임계 영역이 공유 데이터를 한 번에 한 스레드만 만지게 하듯, DB의 락은 한 레코드를 한 번에 한 처리만 만지게 한다. 게임에서는 동시 거래 처리, 공유 리소스 접근, 경매장 입찰, 길드 자금 관리가 락이 필요한 상황이다.

㉡ 장애 내성 — 로그 버퍼

데이터베이스는 데이터를 미리 기록해 두는 로그 버퍼를 사용한다. 시스템이 중단되더라도 로그 버퍼가 남아 있으므로, 원래 해야 했던 일들을 찾아 복구할 수 있다.

DB를 쓰는 여섯 가지 이유 정리

이유 핵심 기능 게임에서의 활용
빠른 관리/분석 효율적인 쿼리 처리 플레이어 검색, 랭킹 조회
데이터 복원 사본 저장소로 빠른 복원 장애 발생 시 데이터 복구
트랜잭션 원자성 보장 아이템 거래, 재화 이동
데이터 일관성 제약 조건(중복 닉네임 금지, 돈 음수 금지) 유효성 검증
동시성 제어 동시 거래, 경쟁 상태 방지
장애 내성 로그 버퍼 기반 복구 서버 다운 시 데이터 보호

🔍 피드백: 논점별로 (1) 2.0 / (2) 2.6 / (3) 1.4를 받아 6.0점입니다.

(1)의 두 문제는 정확합니다. “조작 가능”이 해킹 취약, “다른 장치 간 지속 플레이 불가”가 크로스 플랫폼 문제입니다. 덧붙이신 “복구 불가”도 틀린 말은 아닙니다.

그런데 “클라이언트에 저장해도 되는 데이터”의 예가 방향이 반대입니다. 현재 세션의 HP를 드셨는데, HP는 조작되면 곧바로 치팅이 되는 값이라 서버가 쥐고 있어야 하는 대표적인 데이터입니다. 첫 문제로 “조작 가능”을 직접 쓰셨으니, 그 기준을 그대로 적용하시면 됩니다 — 조작되거나 잃어버려도 게임에 아무 영향이 없는 것만 클라이언트에 둡니다. 원문의 예시는 UI 설정, 그래픽 옵션, 키 바인딩, 사운드 볼륨입니다. HP나 시간처럼 게임 진행에 얽힌 값은 전부 서버 쪽입니다.

(2)㉠은 정확합니다. “1의 계좌만 손해”가 곧 100만 원이 어디에도 없어졌다는 뜻이니까요. ㉡의 “원자성”도 맞습니다. 다만 정의가 부정확합니다 — “질의 명령 결과를 보장하는 것”은 트랜잭션이 하는 일이지 정의가 아닙니다. 정의는 이렇습니다.

더 이상 분할할 수 없는 최소한의 업무 처리 단위

“분할할 수 없다”에서 원자성이 나옵니다. 송금의 두 단계를 하나의 단위로 묶어 두면, 그 안에서 반만 실행되는 일이 없어지는 겁니다.

(3)㉠ “뮤텍스”는 원문의 “임계 영역”과 같은 계열이라 인정했습니다. 어제 Q4에서 임계 영역·뮤텍스·잠금이 동기화의 세 기법이라고 배우셨으니, DB의 락이 그 셋 중 어느 것과 닮았다고 해도 뜻은 통합니다. “읽기/쓰기를 막아 일관성 보장”이라는 역할 설명도 좋습니다.

㉡의 장치 이름은 로그 버퍼입니다. 역할 설명은 방향이 맞았는데(“기억해 두었다가 재개되면 처리”), 시점이 하나 어긋났습니다 — 중단된 동안 들어온 질의가 아니라, 중단되기 전에 미리 기록해 둔 것입니다. DB는 작업을 실행하기 전에 “이걸 할 예정”이라고 로그 버퍼에 먼저 적어 두고, 시스템이 중단되면 재기동 후 그 로그를 보고 원래 해야 했던 일을 찾아 마무리합니다. 9월 8일 DB 단원에서 나온 “게임 서버가 크래시 대비로 로그를 남겨 두었다가 성공하면 지운다”와 같은 원리입니다.

출처: server/game_server/4/2026-02-11-game_server_4_7.md


Q3. 🟢 쉬움 — 🔺 6.7/10

문제: 빈칸을 채우시오.

애플리케이션 서버
      │
   ① ___              ← 명령을 받아 적절한 샤드로 송수신
      │
      ├── mongod (② ___)     ← 샤드 클러스터 전체의 메타데이터 저장
      ├── mongod (③ ___ #1)  ← 도큐먼트를 나눠서 보관
      ├── mongod (③ ___ #2)
      └── mongod (③ ___ #3)
  • 어느 ③ 에 저장할지는 ④ ___ 를 해시 함수에 넣어 정한다.
  • ④ 가 없는 질의는 모든 ③ 에 ⑤ ___ 한다.
  • ② 가 죽으면 모든 샤드가 죽으므로 ⑥ ___ (replica set)를 한다.

📝 내 답: 해시 함수, config, shard, 데이터, broadcast, 복제

정답:

① mongos ② ConfigDB ③ 샤드 (Shard) ④ 샤드 키 ⑤ 브로드캐스트 ⑥ 이중화

해설:

수평 확장(샤딩) 은 서버 한 대가 할 일을 여러 대로 분산하는 것이다. MongoDB에서는 서버가 세 종류로 나뉜다.

① mongos — 애플리케이션 서버에서 명령을 받고, 적절한 샤드로 송수신하는 라우터다.

② mongod (ConfigDB) — 다른 mongod와 같은 데이터베이스 프로그램이지만 하는 역할이 다르다. 샤드 클러스터 전체의 메타데이터 — 샤드 목록, 컬렉션 샤딩 키 정보, 청크 분포 등 — 를 저장한다. 그래서 MongoDB가 제공하는 관리 명령어(sh.addShard(), sh.enableSharding(), sh.shardCollection() 등)로만 다뤄야 하며, db.config.update(…) 같은 일반 질의 구문으로 수정하면 안 된다.

③ mongod (Shard#n)샤드 키로 구분되는 도큐먼트를 분배해서 갖고 있다.

④ 샤드 키 — 이것을 해시 함수에 넣고 나온 정수 값으로 저장할 샤드를 정한다. (John → 해시 함수 → 1~4)

⑤ 브로드캐스트 — 샤드 키가 없는 질의 명령은 어느 샤드에 있는지 알 수 없으므로, mongos가 모든 샤드에 브로드캐스트해 질의를 수행한다. 그만큼 비용이 크므로 질의에는 가능한 한 샤드 키를 포함시켜야 한다.

⑥ 이중화ConfigDB가 죽으면 모든 샤드가 죽는다. 그래서 별도 서버 기기에 설치하기를 권장하고, 예비 데이터베이스를 갖추는 이중화(replica set) 를 하도록 되어 있다. MongoDB는 이중화 기능을 제공한다.

반면 샤드 하나가 죽더라도 다른 샤드는 사용할 수 있고, 각 샤드에도 이중화를 적용해 고가용성을 추구할 수 있다.

🔍 피드백: 6칸 중 4칸 정답으로 6.7점입니다. ②③⑤⑥은 정답 처리했습니다 — “config”은 ConfigDB, “shard”는 샤드, “broadcast”는 브로드캐스트, “복제”는 replica(이중화)의 직역이라 모두 같은 것을 가리킵니다.

① “해시 함수”는 서버의 이름이 아닙니다. 빈칸이 있던 자리는 애플리케이션 서버 바로 아래, “명령을 받아 적절한 샤드로 송수신” 하는 서버입니다. 그 이름이 mongos입니다. 해시 함수는 mongos가 샤드를 고를 때 쓰는 도구이지 mongos 자체가 아닙니다. 8월 23일 객관식에서 이 단원을 만점 받으셨을 때 mongos ← 명령을 받아 적절한 샤드로 송수신 이라는 그림이 문제에 있었으니, 그 이름만 다시 붙여 두시면 됩니다.

④ “데이터”는 너무 넓습니다. 해시 함수에 넣는 건 데이터 전체가 아니라 샤드 키 — 도큐먼트 중에서 “이 도큐먼트를 어느 샤드에 둘지”를 결정하는 데 쓰기로 정한 특정 필드입니다. 원문의 예에서 John이 그것입니다. 어제 Q3에서 DB 수평 확장의 샤드 키를 “해시 키”라고 쓰셨던 것과 같은 자리라, 이 용어를 한 번 정리해 두시면 두 단원이 함께 잡힙니다.

샤드 키(파티션 키) = 해시 함수에 넣어 어느 샤드로 갈지 정하는 키. RDBMS 샤딩(10.2)에서도, MongoDB 샤딩(8.8)에서도 같은 이름입니다.

출처: server/game_server/8/2026-03-13-game_server_8_8.md


Q4. 🟡 보통 · 📌 오답노트 — 🔺 9.4/10

문제: 여러 워커 스레드가 동시에 호출하는 채팅 서버 코드다.

int            messageCount = 0;
Array<Message> history;          // 내부에 [배열 포인터]와 [크기 정보]

void OnChatReceived(Message m) {
    history.Add(m);
    messageCount++;
}

(1) messageCount++의 세 연산과, 10에서 두 스레드가 호출했는데 11이 되는 순서. 무엇이 사라졌는가? (2) ㉠ history.Add(m)에서 벌어질 수 있는 더 위험한 일 ㉡ (1)과 (2)가 각각 어떤 성질의 위반인가? (3) 두 성질을 보장하는 기술의 통칭과 대표 기법 셋. 어느 구간을 어떻게 보호하는가?

📝 내 풀이:

(1) 임시변수 = 원래변수, 계산, 원래변수 = 임시변수 1-1, 1-2, 2-1, 2-2, 2-3, 1-3 (이전에도 있었던 문제라 이렇게 적는다) 무엇이 사라졌는지 쓰라는건 무슨 말인지 모르겠다. 다른 스레드가 계산했던 값이 사라졌다는 것?

(2) ㄱ: 배열 크기가 재할당되어 댕글링 포인터 문제. 예외가 발생한다. ㄴ: (1) 원자성, (2) 일관성

(3) 동기화. mutex/lock/atomic onChatReceived 함수 시작에 mutex를 선언한다.

정답:

(1) messageCount++가 11이 되는 과정

messageCount++는 기계어 수준에서 세 연산이다.

r = messageCount      // 읽기
r = r + 1             // 더하기
messageCount = r      // 쓰기

두 스레드가 messageCount = 10에서 동시에 호출하면 이렇게 될 수 있다.

messageCount = 10

# 스레드 A
rA = messageCount     // rA = 10
rA = rA + 1           // rA = 11
                               컨텍스트 스위치! 아직 쓰지 않았다

# 스레드 B
rB = messageCount     // rB = 10   ← A의 결과를 아직 못 봤다
rB = rB + 1           // rB = 11
messageCount = rB     // messageCount = 11

# 다시 스레드 A
messageCount = rA     // messageCount = 11   ← ❗ B가 쓴 값을 그대로 덮어쓴다

메시지는 두 개 왔는데 카운트는 하나만 올랐다. 사라진 것은 스레드 B의 증가 작업 하나 — 즉 메시지 한 통이 세어지지 않았다. 읽기와 쓰기 사이의 틈에 다른 스레드가 통째로 끼어들었기 때문이다. 이것이 경쟁 상태(데이터 레이스) 다.

(2)㉠ history.Add(m) — 재할당 중에 끼어들면 충돌한다

Array<Message>는 내부에 배열을 가리키는 포인터배열의 크기 정보 두 멤버를 갖는다. 원소를 넣다가 공간이 부족해지면 메모리를 재할당하는데, 이때 포인터가 가리키는 위치가 바뀐다.

스레드 A: 공간 부족 → 새 메모리 확보 → 포인터 교체 중 …
스레드 B:            ← 그 사이에 예전 포인터로 접근
                     → 이미 해제된 메모리를 건드린다 → 충돌(크래시)

(1)은 결과가 틀리는 문제지만, (2)는 프로그램이 죽는 문제라 더 위험하다. 포인터와 크기 정보가 서로 어긋난 순간에 다른 스레드가 접근하기 때문이다.

㉡ 두 성질의 위반

문제 위반한 성질
(1) messageCount++ 원자성(Atomicity) 하나의 작업 단위가 절대 쪼개지지 않아야 하는데, 세 연산 사이에 끼어들었다
(2) history.Add(m) 일관성(Consistency) 여러 멤버 변수(포인터·크기)가 항상 서로 어울리는 상태여야 하는데, 재할당 도중 어긋난 순간에 접근했다

(3) 동기화

두 성질을 보장하는 기술을 통틀어 동기화(Synchronization) 라 하며, 대표적인 기법은 세 가지다.

  • 임계 영역 (Critical Section)
  • 뮤텍스 (Mutex)
  • 잠금 (Lock)

이 코드에서 보호할 구간은 history.Add(m)messageCount++ 두 줄 전체다 — 공유 데이터에 접근하는 순간만큼은 다른 스레드가 접근하지 못하도록 잠그고, 끝나면 푼다.

void OnChatReceived(Message m) {
    lock.Acquire();        // ← 잠금
    history.Add(m);
    messageCount++;
    lock.Release();        // ← 해제
}

🔍 피드백: 논점별로 (1) 3.3 / (2) 3.3 / (3) 2.8을 받아 9.4점입니다. 9월 9일 4.8점에서 시작한 항목이 여기까지 왔습니다.

(1)은 만점입니다. 세 연산도, 1-1, 1-2, 2-1, 2-2, 2-3, 1-3 순서도 정확하고, “무엇이 사라졌는가”에 물음표를 붙이셨지만 “다른 스레드가 계산했던 값이 사라졌다”가 바로 정답입니다. 스레드 B가 올린 +1을 스레드 A가 덮어써서 없앤 것이고, 이 코드의 맥락으로 말하면 메시지 한 통이 세어지지 않은 것입니다. 자신 있게 쓰셔도 됩니다.

(2)도 만점입니다. “재할당 → 댕글링 포인터”가 정확히 원문의 “포인터가 가리키는 위치가 바뀌어 이미 해제된 메모리에 접근”입니다. 9월 9일에 힌트를 드려도 못 채우셨던 자리인데 이제는 자기 말로 나옵니다. ㉡의 (1) 원자성 / (2) 일관성 짝짓기도 정확합니다 — 이 문항의 핵심이 그 대응이었습니다. “예외가 발생한다”는 원문의 “충돌”과 뉘앙스가 조금 다르지만(예외는 잡을 수 있는 것, 충돌은 프로그램이 죽는 것) 점수에는 반영하지 않았습니다.

(3)에서 0.5점이 빠졌고, 그 때문에 오답노트 만점을 놓쳤습니다. 동기화라는 통칭은 맞고 보호 구간도 맞습니다. 빠진 건 기법 세 가지 중 하나입니다 — mutex / lock / atomic이라 쓰셨는데, 원문의 셋은 임계 영역(Critical Section) / 뮤텍스(Mutex) / 잠금(Lock) 입니다. atomic은 원자성을 보장하는 다른 수단이긴 하지만 이 단원이 든 세 가지에는 없습니다. 어제 빈칸 문제에서 이 셋을 전부 맞히셨으니, 임계 영역을 첫 자리에 두는 것만 기억해 두세요.

보호 구간은 “함수 시작에 mutex”라고 하셨는데 방향이 맞습니다. 한 가지만 덧붙이면 — 끝에서 풀어 주는 것까지가 한 쌍입니다. 잠그기만 하고 풀지 않으면 두 번째 스레드부터 영원히 기다립니다.

오답노트는 만점이어야 stage가 오르므로 이 항목은 stage 0에 머뭅니다. 다음에 한 번 더 나올 때 임계 영역 / 뮤텍스 / 잠금만 챙기면 통과입니다.

출처: server/game_server/1/2025-05-30-game_server_1_5.md 오답노트: wn-2de515c0 · 2026-09-09 최초 오답 · 이번이 2번째 복습


Q5. 🟡 보통 — ⭕ 10/10

문제: 게임 서버에서 MongoDB에 도큐먼트를 넣는 의사코드에 대한 설명으로 가장 올바른 것은?

client = new client("mongodb://localhost:27017");     // ①
db     = client["mydb"];                              // ②
coll   = db["mycollection"];                          // ③
doc    = new BsonDocument { ... { "address", new BsonDocument { ... } } };   // ④
coll.insert(doc);                                     // ⑤
  • A. ②와 ③의 순서는 바뀌어도 된다. 컬렉션 이름은 클러스터 안에서 고유하므로 client["mycollection"]으로 먼저 컬렉션을 얻은 뒤, 그 컬렉션이 속한 DB를 나중에 고를 수 있다.
  • B. ④의 BSON은 JSON과 역할이 같지만 평면 구조라서 값 자리에 다른 도큐먼트를 넣을 수 없다. 따라서 위 코드의 "address" 부분처럼 계층적인 데이터는 별도 컬렉션으로 분리하고 참조 키로 연결해야 한다.
  • C. ①②③은 엔드포인트 → DB 인스턴스 이름 → 컬렉션 이름 순으로 범위를 좁혀 들어가는 구조다. ④의 BSON(Binary JSON)은 { "Key", "Value" } 구조가 JSON과 같지만 트리 구조를 사용하므로, "address"의 값 자리에 또 다른 BsonDocument중첩할 수 있다. 이 도큐먼트를 ⑤ insert로 컬렉션에 넣는다.
  • D. ⑤에서 insert에 넘기는 것은 BSON 객체가 아니라 JSON 문자열이다. MongoDB API는 문자열을 받아 서버 쪽에서 파싱하므로, ④처럼 객체를 만드는 과정은 불필요하며 coll.insert("{ name: 'John' }")으로 충분하다.

📝 내 선택: C — A: 안 된다. C의 말처럼 좁혀 나가는 구조이다. / B: BSON은 트리 구조며, 도큐먼트를 넣을 수 있다. / D: 아니다. BSON이 아마 바이너리 형태로 저장했던 것 같다. JSON을 쓰면 안 된다.

정답: C

해설:

MongoDB는 C++, C#, 자바, 루비, 파이썬 등 다양한 언어에서 실행할 수 있는 클라이언트를 제공한다. 명령 실행 순서는 바깥에서 안쪽으로 한 단계씩 좁혀 들어가는 형태다.

단계 코드 매개변수
① 연결 객체 생성 new client("mongodb://localhost:27017") 엔드포인트
② DB 인스턴스 액세스 client["mydb"] DB 인스턴스 이름
③ 컬렉션 액세스 객체 db["mycollection"] 컬렉션 이름
④ 도큐먼트 생성 new BsonDocument { ... } BSON 객체
⑤ 삽입 coll.insert(doc)  

④ BSON(Binary JSON) — MongoDB API는 BSON 객체를 이용해 도큐먼트에 액세스한다. JSON과 같은 역할이고 { "Key", "Value" } 구조도 동일하지만, BSON 객체의 트리 구조를 사용한다. 그래서 값 자리에 또 다른 BsonDocument를 넣어 중첩할 수 있다.

doc = new BsonDocument
{
    { "name", "John" },
    { "address", new BsonDocument            // ← 값 자리에 도큐먼트가 들어간다
        {
            { "City",   "Seoul" },
            { "Street", "Nambu-street" }
        }
    }
};

이것이 NoSQL의 레코드가 트리·구조체 형태를 가질 수 있다는 말의 실체다. RDBMS라면 address를 별도 테이블로 빼고 외래 키로 이어야 했을 것이다.

  • A — 순서는 바뀔 수 없다. 컬렉션은 DB 인스턴스 안에 존재하므로 client에서 바로 컬렉션을 얻을 수 없다. 범위는 반드시 클러스터 → DB → 컬렉션 순으로 좁혀진다.
  • B — 정반대다. BSON은 트리 구조라 중첩이 가능하고, 위 코드의 "address"가 바로 그 예다. “별도 컬렉션으로 분리하고 참조 키로 연결”은 RDBMS의 사고방식이다.
  • Dinsert에 넘기는 것은 BSON 객체다. MongoDB API가 BSON 객체를 이용해 도큐먼트에 액세스한다고 명시되어 있으며, ④의 객체 생성 과정이 필요하다.

🔍 피드백: 만점입니다. 세 오답을 쳐낸 근거가 전부 정확합니다. 특히 B의 “BSON은 트리 구조며 도큐먼트를 넣을 수 있다”는 8월 26일 빈칸에서 비워 두셨던 바로 그 칸(BSON)의 핵심 성질입니다. 그때 못 쓴 이름을 이번엔 성질까지 붙여서 답하셨습니다.

D의 근거 “BSON이 바이너리 형태”도 맞습니다 — Binary JSON이라는 이름 자체가 그 뜻입니다. 여기에 한 가지만 얹어 두면, D가 틀린 결정적 이유는 저장 형식이 바이너리라서라기보다 API가 받는 것이 문자열이 아니라 BSON 객체라는 점입니다. 문자열을 넘기려면 그걸 BSON으로 바꿔 줄 단계가 어차피 필요하니, ④의 객체 생성을 생략할 수 없습니다.

출처: server/game_server/8/2026-03-14-game_server_8_9.md


Q6. 🟡 보통 — 🔺 7.0/10

문제: 다음 명제가 참(O)인지 거짓(X)인지 판단하라.

단일 서버 구조는 클라이언트 — 방화벽 — 스위치/라우터 — 서버 — 데이터베이스로 이어진다. 이 중 라우터/스위치, 방화벽, CPU, 스토리지 가운데 하나라도 한계를 넘어서면 분산 처리가 필요하며, 처음에는 네 가지 중 몇 가지에만 분산을 적용해도 괜찮다.

어디를 분산할지는 성능 분석으로 정한다. 예컨대 게임 서버의 CPU 사용량이 높다면 거기서 멈추지 않고 CPU 안에서 어느 함수가 많은 처리량을 차지하는지까지 더 자세히 분석한다. 그리고 분산에 앞서 코드 최적화를 먼저 진행하고, 그래도 부족할 때 비로소 분산한다.

분산 단위는 데이터 단위 분산기능 단위 분산으로 나뉘고, 게임 로직의 분산 처리 방식은 동기 분산 처리 / 비동기 분산 처리 / 데이터 복제 및 로컬 처리 세 가지다.

📝 내 답: O (근거 없음)

정답: O

해설:

세 문단이 모두 참이다.

첫 문단 ✅ — 단일 서버는 클라이언트 — 방화벽 — 스위치/라우터 — 서버 — 데이터베이스로 구성되고, 라우터/스위치, 방화벽, CPU, 스토리지 중 하나라도 한계를 넘으면 분산이 필요하다. 처음엔 네 가지 중 몇 가지에만 해도 괜찮으며, 성능 분석을 한 후 엄선한 지역에 분산하는 것이 경제적이다.

둘째 문단 ✅ — 성능 분석의 과정이 정확히 서술되어 있다.

  1. 성능 분석으로 과부하 지점을 찾으면 더 자세한 성능 분석을 진행한다. (CPU 사용량이 높다면 → CPU 안에서 어느 부분이 높은지)
  2. 어떤 함수가 많은 처리량을 차지하는지 확인한다. (저자는 함수 소요시간 + 하위 함수의 소요시간을 본다)
  3. 코드 최적화를 진행한다.
  4. 분산을 진행한다.

특히 “코드 최적화가 분산보다 먼저” 라는 순서가 이 단원의 요지다. 분산은 공짜가 아니므로(네트워크 부하, 디버깅 난이도, 스테일 데이터…), 한 대 안에서 해결할 수 있는 것은 먼저 해결하고 그래도 안 될 때 분산한다.

도구는 플랫폼마다 다르다 — 윈도는 비주얼 스튜디오 프로파일러·윈도 성능 도구, 리눅스는 perf.

셋째 문단 ✅ — 분산 단위는 데이터 단위 / 기능 단위, 게임 로직 분산 방식은 동기 / 비동기 / 데이터 복제 및 로컬 처리 세 가지다.

이 명제는 “분산 처리를 피할 수 있다면 피하자”(9.10), “지나친 분산의 네 가지 문제”(9.9) 와 같은 갈래에 있다. 세 단원이 함께 말하는 것은 하나다 — 분산은 마지막 수단이고, 어디를 나눌지는 측정으로 정한다.

🔍 피드백: 판단 7점 만점, 근거 3점 중 0점으로 7.0점입니다.

판단은 맞았습니다. 최근 세 회차의 OX가 연속 X였는데 반사적으로 X를 찍지 않고 O를 고르셨고, 특히 “코드 최적화를 먼저 하고 분산”이라는 문장이 거짓처럼 읽힐 수 있는 자리였는데 통과하셨습니다.

그런데 근거를 한 줄도 안 쓰셨습니다. 9월 8일에 같은 일로 3점을 잃은 뒤 세 회차(9/9, 9/10, 9/14) 연속 근거를 잘 쓰셨는데, 이번에 다시 빠졌습니다. 그리고 그 9월 8일도 정답이 O였습니다. 정답이 O일 때 근거가 빠지는 경향이 있는 것 같습니다 — 틀린 곳을 짚을 필요가 없으니 쓸 게 없다고 느끼셨을 겁니다.

O일 때의 근거는 “각 문단이 원문의 어느 사실과 맞아떨어지는가” 를 대응시키면 됩니다. 이번 문항이라면 이 정도면 충분했습니다.

과부하 지점 넷(라우터/스위치·방화벽·CPU·스토리지)이 맞고, 성능 분석 → 함수 단위 분석 → 코드 최적화 → 분산 순서가 원문 그대로이며, 분산 단위 둘·방식 셋도 맞다. 따라서 참.

세 줄이면 만점입니다. X든 O든 답 뒤에 반드시 한 줄을 붙이는 것을 규칙으로 고정하세요. 이 3점은 몰라서 잃는 점수가 아닙니다.

출처: server/game_server/9/2026-03-18-game_server_9_4.md


📌 복습 포인트

  • Q4 동기화의 세 기법 (오답노트 통과 조건)임계 영역 / 뮤텍스 / 잠금. atomic이 아니라 임계 영역이 첫 자리다. 그리고 잠갔으면 끝에서 풀기까지가 한 쌍 → server/game_server/1/2025-05-30-game_server_1_5.md
  • Q6 OX 근거 쓰기 (O일 때도) — 정답이 O일 때는 각 문단을 원문의 사실에 대응시키는 한 줄이면 된다. 판단이 맞아도 근거가 비면 7점에서 멈춘다 → server/game_server/9/2026-03-18-game_server_9_4.md
  • Q2 클라이언트에 둬도 되는 데이터 — HP·시간처럼 게임 진행에 얽힌 값은 전부 서버. 클라이언트는 UI 설정, 그래픽 옵션, 키 바인딩, 사운드 볼륨처럼 조작돼도 무해한 것만. 트랜잭션의 정의는 “더 이상 분할할 수 없는 최소한의 업무 처리 단위”, 장애 내성의 장치는 로그 버퍼(중단 전에 미리 기록) → server/game_server/4/2026-02-11-game_server_4_7.md
  • Q3 mongos와 샤드 키 — 명령을 받아 샤드로 보내는 서버의 이름은 mongos(해시 함수는 그것이 쓰는 도구). 해시에 넣는 것은 데이터 전체가 아니라 샤드 키 — RDBMS 샤딩(10.2)과 같은 용어 → server/game_server/8/2026-03-13-game_server_8_8.md
  • Q1 string도 전방 선언이 안 되는 이유std::string은 클래스가 아니라 typedefclass string; 자체가 틀린 선언. Date·Address의 “크기를 모른다”와는 별개의 이유 → game_dev/cpp/chapter5/2025-10-07-cpp_5_31.md

MallangQuiz 카테고리 내 다른 글 보러가기

댓글남기기