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

게시:     수정

카테고리:

태그:

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

말랑말랑 퀴즈 — 해답지 ✅

날짜: 2026-09-16 문제 수: 6문제 총점: 53.6/60 (89점) ✅ 잘했어요

📊 채점 결과

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

총평

89점, 최고 기록입니다. 🏆 훌륭해요(90점)에 1점 모자랍니다. 만점이 세 문항이고, 나머지 셋도 전부 7점 이상입니다.

Q3 빈칸이 만점입니다. 8월 26일에 BSONinsert를 비워 6.0점이었던 단원인데, 이번엔 여섯 칸을 전부 채우셨습니다. Q5(1)도 만점입니다 — 변환 불가·private화·is-implemented-in-terms-of 세 요소를 정확히 쓰셨습니다. 이틀 전 객관식에서 확인된 정의가 서술형에서도 그대로 나왔습니다.

Q4 오답노트는 9.0점으로 또 한 번 만점을 놓쳤습니다. X 판단과 3번 단락 반박(“같은 LAN 안에서는 IP가 필요 없고 MAC으로 통신”)은 정확한데, 2번 단락까지 틀렸다고 판정하셨습니다. 2번 단락은 원문 그대로입니다 — 스위치 한계 → MAC 관리 어려움 → 그래서 WAN이 등장. 이 항목의 개념 자체가 “스위치 한계의 해법은 WAN”이라서, 그 문장을 거짓이라 한 건 짚고 넘어가야 합니다. 이 1점만 있었으면 오답노트 통과에 총점 90점이었습니다.

Q2(4)에서 순서가 뒤집혔습니다. “일단 보여주고 나중에 해결”에서 먼저 시작하는 건 모션이고, 나중에 오는 게 사운드와 이펙트입니다. ㉠㉡ 모두 “이펙트를 먼저”라고 쓰셔서 일관되게 반대였습니다. Q5(2)㉡의 EBO 이름과 단일 상속 조건도 문제가 명시적으로 물었는데 비어 있었습니다 — 8월 26일 0점이었던 그 항목입니다.


Q1. 🟡 보통 — ⭕ 10/10

문제: Customer에 복사 생성자·복사 대입 연산자를 직접 정의해 둔 상태에서 데이터 멤버 lastTransaction을 추가했지만 복사 함수는 손대지 않았다. 이 상황에 대한 설명으로 가장 올바른 것은?

Customer::Customer(const Customer& rhs)
    : name(rhs.name)                     // lastTransaction은 없다
{ logCall("Customer 복사 생성자"); }

Customer& Customer::operator=(const Customer& rhs)
{
    logCall("Customer 복사 대입 연산자");
    name = rhs.name;                     // lastTransaction은 없다
    return *this;
}
  • A. 컴파일러가 새 멤버 lastTransaction의 복사를 기존 복사 함수에 자동으로 끼워 넣어 주므로 문제없다. 복사 함수를 직접 정의했더라도 멤버별 복사는 컴파일러가 보장한다.
  • B. 복사 함수가 새 멤버를 다루지 않으므로 컴파일 에러가 나서 곧바로 발견된다. 그래서 실무에서는 이 실수가 거의 일어나지 않는다.
  • C. 컴파일은 문제없이 되지만 복사가 부분적으로만 이루어져, 복사된 객체의 lastTransaction은 원본 값이 아니라 초기값을 그대로 갖는다. 그래서 데이터 멤버를 추가하면 복사 함수도 함께 수정해야 하며, 이것이 “객체의 모든 부분을 빠짐없이 복사하라”는 규칙의 첫 번째 의미다. 상속 관계에서 기본 클래스 부분을 명시적으로 복사해야 하는 것은 같은 규칙의 두 번째 의미다.
  • D. 복사 함수를 직접 정의했더라도 컴파일러가 만든 기본 복사 함수가 뒤이어 함께 실행되어 나머지 멤버를 채워 준다. 그래서 직접 정의한 함수에서는 특별한 처리가 필요한 멤버만 다루면 된다.

📝 내 선택: C — A: 기존 복사 함수에 자동으로 끼워 넣어 주지 않는다. 멤버의 생성자만 호출되어 초기값이 된다. / B: 생성자를 호출해 주기에 에러 나진 않고, 조용히 넘어간다. / D: 복사 함수를 직접 정의하면 기본 복사 함수가 생성되지 않는다. 생성자만 호출된다.

정답: C

해설:

컴파일러는 복사 생성자와 복사 대입 연산자를 자동으로 생성해 줄 수 있다. 그러나 그건 사용자가 정의하지 않았을 때의 이야기다. 일단 직접 정의하면 컴파일러는 그 함수에 손을 대지 않는다 — 멤버를 하나 빠뜨렸다고 채워 주지도, 경고하지도 않는다.

그래서 위 코드는 컴파일이 잘 되지만 복사는 부분적으로만 이루어진다. name은 복사되고, lastTransaction은 복사되지 않아 초기값을 그대로 갖는다. 컴파일 에러도 경고도 없으니 테스트에서 우연히 걸리기 전까지는 발견되지 않는다. 이것이 이 실수가 위험한 이유다.

“객체의 모든 부분을 빠짐없이 복사하라”의 두 가지 의미

  놓치기 쉬운 부분 해야 할 일
① 데이터 멤버가 추가될 때 새로 추가한 멤버 복사 함수도 같이 수정한다
② 상속 관계일 때 기본 클래스로부터 물려받은 부분 초기화 리스트에서 Base(rhs), 대입 연산자에서 Base::operator=(rhs)명시적으로 호출한다

두 경우 모두 컴파일러는 침묵한다. 그래서 복사할 내용을 copyFrom() 같은 private 함수 하나에 모아 두면 멤버가 늘어날 때 고칠 곳이 한 군데로 좁혀져 실수를 줄일 수 있다.

  • A — 직접 정의한 복사 함수에 컴파일러는 아무것도 끼워 넣지 않는다. 자동 생성은 정의하지 않았을 때만 일어난다.
  • B — 컴파일 에러가 나지 않는다. 멤버를 복사하지 않는 것은 문법적으로 아무 문제가 없다. 이 실수가 위험한 이유가 바로 조용히 지나가기 때문이다.
  • D — 사용자 정의 복사 함수와 컴파일러 생성 복사 함수는 둘 중 하나만 존재한다. 함께 실행되는 일은 없다.

🔍 피드백: 만점입니다. 세 오답의 근거가 모두 정확하고, 특히 A와 B에서 “멤버의 생성자만 호출되어 초기값이 된다” 고 쓰신 게 이 실수의 메커니즘 그 자체입니다. 복사되지 않은 lastTransaction은 그냥 비어 있는 게 아니라 Date의 기본 생성자가 호출된 결과를 갖게 되니까요. 그래서 “조용히 넘어간다”는 겁니다 — 값이 하나 들어 있으니 컴파일러도 런타임도 이상을 눈치채지 못합니다.

이 항목은 이제 세 번 나왔고(기본 클래스 복사 누락 → 서로 호출 금지 → 멤버 추가 시 갱신), 세 규칙이 전부 확인됐습니다. 8월 26일 2.8점에서 시작한 단원이 이제 정리된 셈입니다.

출처: game_dev/cpp/chapter2/2025-06-08-cpp_2_12.md


Q2. 🔴 어려움 — 🔺 7.0/10

문제: 온라인 게임의 캐릭터 이동 처리와 레이턴시 마스킹에 대해 답하시오.

(1) 기본 과정대로만 하면 왜 답답한가 (2) “사소한 것은 클라이언트에서” — ㉠ 값 검증 ㉡ 예측/보정의 세 단계 (3) 예측/보정의 한계 (4) “일단 보여주고 나중에 해결” — ㉠ 세 단계 ㉡ 고레이턴시에서의 연출 순서와 사용 예

📝 내 풀이:

(1) 움직이는 건 RTT 이후에 반영된다. 1번에서 먼저 움직임을 예측하지 않기 때문이다.

(2) ㄱ: 움직이는 속도가 정상적인 값인지 검사하고, 아니라면 보정시킨다. ㄴ: 1번: 움직이는 방향과 속도를 서버에게 전달한다. 2번: 먼저 예측하여 클라이언트에 반영한다. 3번: 서버에서 시뮬레이션을 돌려보고 정상이면 그대로, 아니라면 클라이언트를 재조정시킨다.

(3) 정확히 얼마라는 값은 없지만 레이턴시가 높아지면 예측이 자주 빗나가게 되어, 자주 재조정된다. 레이턴시가 낮은 환경에서 사용하면 좋다.

(4) ㄱ: 1. 클라에서 이펙트, 소리를 먼저 실행시키고 서버로 전송한다. 2. 서버에서 판단하고 클라에게 결과를 보낸다. 3. 클라에서 결과를 반영한다. (e.g. 체력 감소) ㄴ: 레이턴시가 높아지면 이펙트가 보이고 나중에 결과가 반영된다. 스타크래프트가 예시다.

정답:

(1) 답답한 이유 — 내 입력이 서버를 한 바퀴 돌아와야 화면이 움직이기 때문

① 클라이언트 → 서버 : 명령 전송
② 서버           : 이동 연산
③ 서버 → 클라이언트 : 이동 정보 전송      ← 여기까지 와야
④ 클라이언트      : 위치 갱신             ← 비로소 움직인다

플레이어가 키를 누른 순간(①)에는 화면이 움직이지 않는다. 그 명령이 서버까지 가서(①), 서버가 계산하고(②), 결과가 돌아와서(③) 그제서야 움직인다(④). 즉 왕복 레이턴시만큼 반응이 늦다. 키를 누르고 한 박자 뒤에 캐릭터가 움직이니 답답하게 느껴진다.

(2) 사소한 것은 클라이언트에서 — 치터 문제의 두 해법

클라이언트가 먼저 위치를 계산하고 서버에는 결과만 알려 주면 반응은 즉각적이다. 문제는 클라이언트를 믿을 수 없다는 것이다.

㉠ 값 검증

  • 클라이언트: 이동 정보(결과)를 서버에 보낸다.
  • 서버: 그 값이 정상적인 범위인지 검사한다. (한 프레임에 맵 반대편으로 갈 수는 없다 등)

구현이 간단하지만, 범위 안에서 조작하는 치터는 막지 못한다.

㉡ 예측/보정

클라이언트는 결과가 아니라 명령만 보낸다.

단계 무슨 일이 일어나는가
MyPlayer_Move() → 서버 클라이언트가 명령 정보를 서버에 보낸다 (결과가 아니다)
MyPlayer_Move() → 자신 서버 응답을 기다리지 않고 클라이언트가 먼저 움직인다 (예측)
Player_Move() → 클라이언트 서버가 명령 기반으로 이동을 시뮬레이션한 뒤 이동 정보를 보내고, 클라이언트는 그에 맞춰 자기 위치를 보정한다

판단 주체가 여전히 서버이므로 값 검증보다 보안이 우수하고, 클라이언트가 먼저 움직이므로 반응도 즉각적이다.

(3) 예측/보정의 한계 — 100ms를 넘으면 움직임이 손상된다

예측/보정은 해킹을 막는 데 제일 좋지만, 레이턴시가 100ms를 넘으면 캐릭터 움직임이 손상된다. 클라이언트가 예측한 위치와 서버가 돌려준 위치의 차이가 커져 보정 때마다 캐릭터가 튀기 때문이다.

그래서 레이턴시가 균일하고 낮을 때(50ms 이하) 쓰는 것이 제일 좋다.

(4) 일단 보여주고, 나중에 해결

㉠ 세 단계

  1. 클라이언트가 서버에 행동 명령 메시지를 전송하고, 동시에 행동 연출의 일부를 즉시 시작한다.
  2. 서버가 클라이언트에게 행동 명령 처리 결과를 전송한다.
  3. 클라이언트가 행동 연출의 나머지 부분을 진행한다.

핵심은 연출을 둘로 쪼개는 것이다 — 서버 확인 없이 시작해도 되는 부분(모션)과, 확인이 있어야 하는 부분(결과)으로.

㉡ 고레이턴시에서의 증상과 사용 예

레이턴시가 높으면 행동 모션은 즉시 취하지만, 사운드와 이펙트 연출은 그 뒤에서야 발생한다. 캐릭터가 팔을 휘두르고 한 박자 뒤에 타격음과 이펙트가 터지는 식이다.

사용 사례: 스타크래프트, 디아블로 3

세 기법 비교

기법 동작 방식 장점 단점 적합한 상황
값 검증 결과 전송 후 검증 구현 간단 보안 취약 신뢰 환경
예측/보정 즉시 이동 → 보정 보안 우수, 즉각 반응 고레이턴시 시 끊김 저레이턴시(50ms↓)
분할 연출 모션 먼저 → 이펙트 나중 고레이턴시 대응 시각적 위화감 가능 RTS, ARPG

🔍 피드백: 논점별로 (1) 2.3 / (2) 2.2 / (3) 1.5 / (4) 1.0을 받아 7.0점입니다.

(1)(2)가 거의 만점입니다. “RTT 이후에 반영된다”가 답답함의 정체이고, 예측/보정의 세 단계 — 명령 전달 → 먼저 움직임 → 서버 시뮬레이션 후 재조정 — 를 정확히 쓰셨습니다. 특히 ①에서 “방향과 속도”라고 쓰신 게 좋습니다. 값 검증은 결과(위치) 를 보내고 예측/보정은 명령(방향·속도) 을 보낸다는 게 두 방식의 갈림길이니까요.

(3)은 숫자가 빠졌습니다. “정확히 얼마라는 값은 없지만”이라 쓰셨는데, 원문에 명시되어 있습니다 — 100ms를 넘으면 캐릭터 움직임이 손상되고, 50ms 이하로 균일할 때 쓰는 게 제일 좋습니다. 메커니즘(“예측이 자주 빗나가 자주 재조정된다”)은 정확하니, 두 숫자만 붙이시면 됩니다. 이 두 숫자는 9월 10일 “패킷 뭉치기 10ms”처럼 이 단원의 몇 안 되는 수치라 외워 둘 만합니다.

(4)에서 순서가 뒤집혔습니다. “일단 보여주고 나중에 해결”의 핵심은 연출을 둘로 쪼개는 것인데, 어느 쪽이 먼저인지가 반대입니다.

  쓰신 답 원문
먼저 (서버 확인 전) 이펙트, 소리 행동 모션
나중 (서버 확인 후) 결과(체력 감소) 사운드와 이펙트

원문의 문장은 이렇습니다 — “레이턴시가 높으면 행동 모션을 취하지만, 이후에서야 사운드와 이펙트 연출이 발생한다.” 왜 이 순서인지 생각해 보면 기억하기 쉽습니다. 모션은 “내가 공격 버튼을 눌렀다”는 사실만으로 시작할 수 있지만, 타격음과 이펙트는 “정말 맞았는가”를 서버가 확인해 줘야 틀 수 있습니다. 맞지도 않았는데 타격음이 나면 안 되니까요. 스타크래프트에서 유닛이 팔을 휘두르고 한 박자 뒤에 소리가 나는 게 그 장면입니다. 예시로 스타크래프트를 드신 건 정확합니다.

출처: server/game_server/5/2026-02-14-game_server_5_3.md


Q3. 🟢 쉬움 — ⭕ 10/10

문제: 빈칸을 채우시오.

MongoDB API는 ① ___ (Binary JSON) 객체를 이용해 도큐먼트에 액세스한다. { "Key", "Value" } 구조는 JSON과 같지만 ② ___ 구조라 값 자리에 다른 도큐먼트를 중첩할 수 있다.

단계 코드 넘기는 매개변수
1. 연결 객체 생성 new client("mongodb://localhost:27017") ___
2. 액세스 획득 client["mydb"] ___ 이름
3. 액세스 객체 획득 db["mycollection"] ___ 이름
4. 도큐먼트 삽입 coll.⑥ ___(doc)  

📝 내 답: Bson, 트리, 엔드포인트, 인스턴스, 컬렉션, insert

정답:

① BSON ② 트리 ③ 엔드포인트 ④ DB 인스턴스 ⑤ 컬렉션 ⑥ insert

해설:

① BSON(Binary JSON) — MongoDB API가 도큐먼트에 액세스할 때 쓰는 객체 형식이다. JSON과 같은 역할이고 { "Key", "Value" } 구조도 동일하다.

② 트리 구조 — BSON 객체는 트리 구조를 사용하므로, 값 자리에 또 다른 BsonDocument를 넣어 중첩할 수 있다. "address" 안에 "City", "Street"가 들어가는 것이 그 예다. 이것이 NoSQL의 레코드가 트리·구조체 형태를 가질 수 있다는 말의 실체다.

③ 엔드포인트 — 연결 객체를 만들 때 넘기는 매개변수. mongodb://localhost:27017처럼 주소와 포트다.

④ DB 인스턴스 이름 / ⑤ 컬렉션 이름 — 순서대로 범위를 좁혀 들어간다. 컬렉션은 DB 인스턴스 안에 있으므로 순서를 바꿀 수 없다.

⑥ insert — 만들어 둔 도큐먼트를 컬렉션에 넣는다. RDBMS의 SQL에서도 넣는 동작은 insert였다.

엔드포인트 → DB 인스턴스 → 컬렉션 → 도큐먼트 순으로 범위가 좁아진다고 기억하면 순서를 헷갈리지 않는다.

🔍 피드백: 만점입니다. ④ “인스턴스”는 “DB 인스턴스”의 축약이고 문장이 “④ 이름”이라 “인스턴스 이름”으로 자연스럽게 읽히므로 정답 처리했습니다.

이 단원이 세 번째인데 흐름이 분명합니다 — 8월 26일 빈칸에서 BSONinsert를 비워 6.0점, 어제 객관식에서 BSON의 트리 구조까지 붙여 만점, 오늘 빈칸에서 여섯 칸 전부 정답. 그때 못 채운 두 칸이 이제는 첫 번째로 나옵니다.

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


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

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

OSI 2계층인 데이터 링크 계층은 LAN 안에서의 통신을 담당한다. 단말기마다 고유한 MAC 주소가 있고, 데이터는 헤더(송·수신자 주소)와 페이로드(실제 데이터)로 이루어진 프레임 단위로 오간다. 요즘 LAN은 대부분 별 위상이라 단말기들이 스위치 하나에 연결된다.

그런데 스위치 하나가 연결할 수 있는 단말기 수에는 한계가 있고, 단말기가 많아지면 고유한 MAC 주소를 관리하기도 어려워진다. 이 문제를 해결하려고 WAN이 등장하며, 여러 LAN 사이의 경로를 설정하는 것이 3계층 네트워크 계층라우터다.

다만 실제로는 같은 LAN 안의 두 단말기가 통신할 때도 스위치가 IP 주소를 보고 경로를 정한다. 스위치는 프레임의 MAC 주소를 IP 주소로 변환한 뒤 라우팅 테이블을 참조하므로, 2계층 장비라 해도 결국 3계층 정보 없이는 동작할 수 없다.

📝 내 답: X — 2, 3번 단락이 잘못되었다. WAN이 등장한 것은 여러 LAN을 커버하기 위해, 넓은 범위의 인터넷을 IP 주소를 통해 사용하기 위한 것이다. 같은 LAN 안의 단말기끼리 통신할 때는 IP 주소가 필요 없고 MAC으로 통신한다.

정답: X

해설:

앞의 두 문단은 전부 참이다. 특히 둘째 문단은 지난 복습에서 놓쳤던 부분 — 스위치의 한계는 주소 공간이 아니라 “포트 수와 MAC 주소 관리 부담”이고, 그 해법은 IPv6가 아니라 WAN과 라우터 — 를 정확히 서술하고 있다.

틀린 곳은 마지막 문단이다. 원문이 그리는 같은 LAN 내 통신은 이렇다.

[단말기 A] - [스위치] - [단말기 B]
<————————————— LAN —————————————>
→ 스위치만으로 통신 가능

같은 LAN 안에서는 스위치만으로 통신이 된다. 스위치는 프레임 헤더의 MAC 주소를 보고 대상 단말기에게 전달하며, IP 주소나 라우팅 테이블은 관여하지 않는다. IP와 라우터가 등장하는 것은 LAN을 넘어갈 때뿐이다.

[단말기 A] - [스위치] - [라우터] - [스위치] - [단말기 B]
<—————— LAN 1 —————>              <————— LAN 2 ——————>
→ 라우터를 통해 다른 네트워크로 전달

“MAC 주소를 IP 주소로 변환한 뒤 라우팅 테이블을 참조한다”는 것은 2계층과 3계층의 일을 뒤섞은 것이다. 두 계층은 각자의 주소와 각자의 장비로 각자의 범위를 담당한다.

  2계층 (데이터 링크) 3계층 (네트워크)
범위 LAN 내부 LAN과 LAN 사이 (WAN)
장비 스위치 라우터
주소 MAC 주소 IP 주소
단위 프레임 패킷

지난 복습의 함정은 “3계층의 숫자(32비트)를 2계층에 갖다 붙인 것”이었고, 이번 함정은 “3계층의 동작(IP 라우팅)을 2계층 안으로 끌어들인 것“이다. 방향만 다를 뿐 같은 혼동이다 — LAN 내부는 MAC과 스위치, LAN 사이는 IP와 라우터라는 경계를 지키면 둘 다 걸러진다.

🔍 피드백: 판단 7점 만점, 근거 3점 중 2.0점으로 9.0점입니다. 두 번째 복습에서도 만점을 아깝게 놓쳐 stage 0에 머뭅니다.

맞은 부분부터. X 판단이 정확하고, 마지막 단락에 대한 반박 — “같은 LAN 안의 단말기끼리 통신할 때는 IP 주소가 필요 없고 MAC으로 통신한다” — 는 이 함정의 급소를 정확히 찌른 문장입니다. 지난 복습(9월 9일)에서 “32비트는 MAC이 아니라 IP”라고 잡으셨고, 이번엔 반대 방향의 함정도 잡으셨으니 2계층/3계층 경계 자체는 확실히 서 있습니다.

감점은 2번 단락을 거짓이라 판정한 데서 나왔습니다. “2, 3번 단락이 잘못되었다”고 쓰셨는데, 2번 단락은 원문 그대로 참입니다.

스위치 하나가 연결할 수 있는 단말기 수에 한계가 있고, 단말기가 많아지면 고유한 MAC 주소 관리가 어려워진다. 이 문제를 해결하기 위해 등장하는 것이 바로 WAN이다.

이건 원문의 문장을 거의 그대로 옮긴 것이고, 이 오답노트 항목의 concept이 정확히 이 내용입니다 — “스위치의 단말기 수 한계는 주소 공간이 아니라 포트 수·MAC 관리 부담의 문제이고 그 해법은 WAN과 라우터”. 지난 복습 피드백에서 이 부분을 “원인 진단”으로 짚어 드렸는데, 이번 명제에는 그게 참으로 들어가 있었습니다.

쓰신 반박 — “WAN이 등장한 것은 여러 LAN을 커버하기 위해” — 는 틀린 말은 아닙니다. 다만 그것이 2번 단락과 모순되지 않습니다. 스위치 한 대로는 단말기를 다 못 담으니 LAN을 여러 개 두게 되고, 그 여러 LAN을 잇는 것이 WAN이니까요. “스위치의 한계 때문에”와 “여러 LAN을 커버하기 위해”는 같은 이야기의 원인과 결과입니다.

한 가지 요령을 덧붙이면, 거짓인 단락은 보통 하나입니다. 명제 전체를 X로 판단한 뒤에도 어느 단락이 거짓인지는 단락마다 따로 검사하세요. 3번 단락 하나만 짚으셨으면 근거도 만점이었고, 이 항목은 stage 1로 올라갔을 것이며, 총점은 90점이었습니다.

출처: server/game_server/2/2025-06-30-game_server_2_1.md 오답노트: wn-34e57a32 · 2026-09-09 최초 오답 · 이번이 2번째 복습


Q5. 🔴 어려움 — 🔺 7.6/10

문제: private 상속에 대해 답하시오.

(1) eat(s)가 컴파일되지 않는 이유, 물려받은 public 멤버의 접근 지정자, 그리고 private 상속의 의미 (2) ㉠ HoldsAnInt_A(객체 합성)의 크기와 이유 ㉡ HoldsAnInt_B(private 상속)의 크기, 현상의 이름과 조건 (3) private 상속을 써야만 하는 세 가지 경우

📝 내 풀이:

(1) private 상속인 경우 파생 클래스에서 부모 클래스로의 변환을 허용하지 않는다. public 멤버들은 private로 변한다. private 상속의 의미는 is-implemented-in-terms-of로써, 무엇을 통해 구현할 때 사용한다.

(2) ㄱ: 전자가 더 큰 공간을 차지한다. 상속하여 객체로 사용하기 위해선 클래스의 크기가 0이 되면 안 되기 때문에, 컴파일러가 내부에 더미 데이터를 넣는다. ㄴ: 상속만 하고 객체를 만들진 않았기에 empty는 공간을 차지하지 않는다. 이 경우 동일한 크기가 된다

(3) 1. 상속한 클래스에서 특정 함수만을 사용하고자 하는 경우 2. 클래스의 크기를 0으로 만들어 사용하는 기법이 필요한 경우 3. 함수를 오버라이드하여 사용하고 싶은 경우

정답:

(1) private 상속의 의미 — is-implemented-in-terms-of

class Student : private Person { ... };
eat(s);     // ❌

eat(s)가 안 되는 이유는 private 상속이면 컴파일러가 파생 클래스 객체(s)를 기본 클래스 객체(p)로 변환하지 않기 때문이다. StudentPerson의 일종이 아니다. public 상속의 is-a 관계가 성립하지 않는다.

그리고 기본 클래스로부터 물려받은 멤버는 파생 클래스에서 모조리 private 멤버가 된다. Person의 public 함수가 Student에서는 밖에서 부를 수 없게 된다.

이 두 사실을 묶으면 private 상속의 의미가 나온다.

private 상속의 의미는 is-implemented-in-terms-of(…는 …를 써서 구현된다)다. 구현만 물려받을 수 있고, 인터페이스는 물려받지 못한다.

(2) 공백 기본 클래스 최적화(EBO)

sizeof(HoldsAnInt_A) > sizeof(int)

C++에는 “독립 구조의 객체는 반드시 크기가 0이 넘어야 한다” 는 규칙이 있다. Empty는 비정적 데이터 멤버가 없어 메모리를 쓰지 말아야 할 것 같지만, 멤버 변수 e로 품는 순간 독립된 객체가 되므로 이 규칙에 걸린다.

그래서 Empty e 자리에 char 한 개가 들어가고, 바이트 정렬이 필요하다고 판단되면 패딩까지 붙는다. 결과적으로 int 하나만 담으면 되는 클래스가 그보다 커진다.

sizeof(HoldsAnInt_B) == sizeof(int)

private 상속으로 바꾸면 Empty독립된 객체가 아니라 기본 클래스 부분이 된다. 기본 클래스 부분에는 “크기 0 초과” 규칙이 적용되지 않으므로 공간을 차지하지 않고, int 하나만큼의 크기가 된다.

이것을 공백 기본 클래스 최적화(EBO, Empty Base Optimization) 라 한다. 적용 조건은 단일 상속이다 — 기본 클래스를 두 개 이상 상속하면 적용되지 않는다. STL에도 unary_function, binary_function 같은 공백 클래스가 이 최적화를 염두에 두고 존재한다.

(3) private 상속을 써야만 하는 세 가지 경우

  1. 기본 클래스의 protected 멤버에 접근해야 할 때
  2. 상속받은 가상 함수를 재정의해야 할 때 (객체 합성으로 복잡해질 경우)
  3. 공백 기본 클래스 최적화(EBO) 를 활성화해야 할 때

이 셋이 아니면 객체 합성을 쓴다. 객체 합성 쪽이 파생 클래스의 재정의를 막는 설계 제어가 가능하고, 정의를 분리해 컴파일 의존성도 줄일 수 있기 때문이다.

특징 객체 합성 private 상속
의미 has-a 또는 is-implemented-in-terms-of is-implemented-in-terms-of
설계 제어 재정의 방지 가능 재정의 방지 불가
컴파일 의존성 최소화 가능 증가
EBO 불가 가능
protected 접근 불가 가능

🔍 피드백: 논점별로 (1) 3.3 / (2) 2.1 / (3) 2.2를 받아 7.6점입니다.

(1)은 만점입니다. 변환 불허·public → private·is-implemented-in-terms-of 세 요소를 하나도 빠짐없이 쓰셨습니다. 이틀 전 객관식에서 A를 쳐낼 때 쓰신 근거가 서술형에서도 그대로 나왔으니, private 상속의 정의는 완전히 잡혔습니다.

(2)㉠도 거의 만점입니다. “크기가 0이 되면 안 되기 때문에 더미 데이터를 넣는다”— 원문의 “char 한 개가 들어간다”를 자기 말로 쓰신 겁니다. 한 가지만 덧붙이면 그 규칙의 원문 표현입니다 — “독립 구조의 객체는 반드시 크기가 0이 넘어야 한다.” Empty e처럼 멤버로 품으면 독립된 객체가 되어 이 규칙에 걸리는 것이고, 필요하면 패딩까지 붙어 더 커집니다.

㉡에서 이름과 조건이 빠졌습니다. “상속만 하고 객체를 만들진 않았기에 공간을 차지하지 않는다”는 정확한 이유입니다 — 기본 클래스 부분은 독립 객체가 아니라 규칙이 적용되지 않으니까요. 그런데 문제가 “이 현상의 이름과, 적용되기 위한 조건” 을 명시적으로 물었는데 둘 다 비어 있습니다.

  • 이름: 공백 기본 클래스 최적화(EBO, Empty Base Optimization)
  • 조건: 단일 상속에만 적용된다. 기본 클래스를 두 개 이상 상속하면 적용되지 않습니다.

이 조건이 바로 8월 26일 OX에서 0점이었던 그 지점입니다(“EBO는 여러 개 상속해도 적용된다”가 거짓). 이번엔 원리를 정확히 설명하셨으니, 이름과 조건만 붙이면 그 항목은 완결됩니다.

(3)은 셋 중 둘이 맞습니다. “크기를 0으로 만드는 기법”이 EBO, “오버라이드”가 가상 함수 재정의로 정확합니다. 첫 번째로 쓰신 “특정 함수만 사용하고자 하는 경우” 는 이 항목이 아니라 항목 33(이름 가림)의 전달 함수 이야기입니다 — private 상속에서 특정 오버로드만 물려받을 때 쓰는 기법이죠. 여기서 빠진 세 번째는 protected 멤버에 접근해야 할 때입니다. 객체 합성으로는 기본 클래스의 protected 멤버를 건드릴 수 없으니 그때는 private 상속이 유일한 길입니다.

출처: game_dev/cpp/chapter6/2025-10-26-cpp_6_39.md


Q6. 🟡 보통 — ⭕ 10/10

문제: RmiContext와 수신 처리에 대한 설명으로 가장 올바른 것은?

  • A. RmiContext에 담기는 옵션은 ReliableSendUnreliableSend 딱 두 가지뿐이다. 암호화나 압축이 필요하면 애플리케이션이 data를 직접 가공한 뒤 넘겨야 한다.
  • B. RmiContext에는 “오래된 메시지는 건너뛸지” 옵션이 있는데, 이것은 채팅처럼 순서가 중요한 데이터에 켜야 한다. 밀린 메시지를 건너뛰어야 최신 대화부터 표시할 수 있기 때문이다.
  • C. RmiContext에는 reliable/unreliable 외에 암호화 여부, 압축 여부, 오래된 메시지는 건너뛸지 같은 옵션이 함께 담긴다. 오래된 메시지 건너뛰기는 위치 갱신처럼 최신 값만 의미 있는 데이터에 적합하다. 수신 콜백은 sender(누가), rmiContext(어떤 방식으로), payload·payloadLength(무엇을 얼마나)를 받으므로, 받는 쪽도 그 메시지가 어떤 옵션으로 왔는지 알 수 있다.
  • D. SendUserMessage()에 HostID 배열을 넘겨 여러 클라이언트에게 보낼 때는 RmiContext가 무시되고 자동으로 Reliable이 강제된다. 다수 전송에서 일부만 못 받으면 상태가 어긋나기 때문이다.

📝 내 선택: C — A: 두 가지만이 아니라 다른 옵션도 있었으며, 암호화나 압축을 위한 옵션도 존재한다. / B: 오래된 메시지 건너뛰기 옵션은 있지만, 채팅은 순서가 중요하기에 오히려 건너뛰면 안 된다. / D: 솔직히 이게 무엇이 잘못됐는지 모르겠다. Reliable로 보내질 것 같지만 RmiContext 자체가 무시될 것 같진 않다.

정답: C

해설:

SendUserMessage()의 두 번째 인자는 “어떻게 보낼 것인가” 를 정한다. 여기에 들어가는 옵션을 통틀어 RmiContext 구조체라 한다.

옵션
reliable / unreliable 전송을 보장할지, 보장하지 않는 대신 즉시 보낼지
암호화 여부 메시지를 암호화해 보낼지
압축 여부 메시지를 압축해 보낼지
오래된 메시지는 건너뛸지 밀려 있는 낡은 메시지를 보내지 않고 건너뛸지

마지막 옵션이 이 구조체의 성격을 잘 보여 준다. 위치 갱신처럼 최신 값만 의미 있는 데이터라면, 대역폭이 몰려 밀려 있는 낡은 위치를 굳이 보낼 이유가 없다 — 어차피 다음 값이 덮어쓸 테니까. 이건 Unreliable을 고르는 이유와 같은 판단이다: “이 메시지가 사라지면 뒤에 오는 것이 복구해 주는가?”

수신 쪽은 OnReceiveUserMessage 이벤트 함수로 처리하며, 매개변수가 송신 쪽의 세 종류와 그대로 대응한다.

매개변수 의미
HostID sender 누가 보냈는가
const RmiContext& rmiContext 어떠한 방식으로 받았는가
uint8_t* payload 받은 메시지 본문
int payloadLength 그 메시지의 길이

받는 쪽이 rmiContext를 함께 받으므로, 그 메시지가 reliable이었는지 압축되었는지 등을 수신 측에서도 알 수 있다.

  • A — 두 가지뿐이 아니다. 암호화·압축·오래된 메시지 건너뛰기가 RmiContext 안에 옵션으로 있으므로 애플리케이션이 직접 가공할 필요가 없다.
  • B — 정반대다. 채팅은 한 통도 빠지면 안 되는 데이터라 건너뛰기를 켜면 대화가 유실된다. 이 옵션은 곧 덮어써질 상태 값(위치 등)에 켠다.
  • D — HostID 배열로 다수에게 보낼 때도 RmiContext는 그대로 적용된다. 원문의 예제가 정확히 그것이다.
HostID sendTo[10];
s->SendUserMessage(sendTo, 10, RmiContext::UnreliableSend, data, 30);   // 다수 + Unreliable

🔍 피드백: 만점입니다. A와 B의 근거가 정확하고, 특히 B의 “채팅은 순서가 중요하기에 오히려 건너뛰면 안 된다”는 이 옵션의 적용 기준을 뒤집어서 정확히 짚은 겁니다. 9월 8일에 못 쓰셨던 암호화·압축·건너뛰기 세 옵션이 이번엔 A의 근거로 자연스럽게 나왔습니다.

D에 대해 “솔직히 모르겠다”고 하셨는데, 판단 방향(“RmiContext 자체가 무시될 것 같진 않다”)이 맞습니다. 근거를 채워 드리면 — 원문의 예제 코드에 답이 있습니다.

HostID sendTo[10];
s->SendUserMessage(sendTo, 10, RmiContext::UnreliableSend, data, 30);

배열로 열 명에게 보내면서 UnreliableSend를 쓰고 있습니다. 다수 전송이라고 Reliable이 강제된다면 이 코드가 존재할 수 없죠. 실제로 다수에게 보내는 대표적인 데이터가 다른 플레이어들의 위치인데, 그건 오히려 Unreliable이 맞는 데이터입니다. D는 “다수 전송 = 중요한 전송”이라는 그럴듯한 연상을 심어 둔 함정이었습니다.

출처: server/game_server/6/2026-02-23-game_server_6_4.md


📌 복습 포인트

  • Q4 OX 근거 쓰는 법 (오답노트 통과 조건) — 명제가 X여도 거짓인 단락은 보통 하나다. 단락마다 따로 검사하고, 참인 단락을 거짓이라 하지 말 것. 2번 단락(“스위치 한계 → WAN 등장”)은 원문 그대로 참이었다 → server/game_server/2/2025-06-30-game_server_2_1.md
  • Q5 EBO의 이름과 조건 — 원리는 잡혔으니 이름 공백 기본 클래스 최적화(EBO) 와 조건 단일 상속에만 적용을 붙일 것. private 상속의 세 경우는 protected 접근 / 가상 함수 재정의 / EBO — “특정 함수만 사용”은 항목 33의 전달 함수 이야기 → game_dev/cpp/chapter6/2025-10-26-cpp_6_39.md
  • Q2 분할 연출의 순서먼저 모션, 나중에 사운드·이펙트. 맞았는지 서버가 확인해 줘야 타격음을 틀 수 있다. 예측/보정의 한계 수치는 100ms 넘으면 손상, 50ms 이하에서 적합server/game_server/5/2026-02-14-game_server_5_3.md

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

댓글남기기