[말랑 퀴즈] 26/09/08 해답
카테고리: MallangQuiz
태그: Quiz
제 말랑말랑 퀴즈 생성기는 이곳에서 확인하실 수 있습니다.
말랑말랑 퀴즈 — 해답지 ✅
날짜: 2026-09-08 문제 수: 5문제 총점: 36.4/50 (73점) ✅ 잘했어요
📊 채점 결과
| 문항 | 유형 | 난이도 | 결과 | 점수 |
|---|---|---|---|---|
| Q1 | 서술형 | 🔴 어려움 | 🔺 부분 정답 | 3.1 / 10 |
| Q2 | 객관식 | 🟡 보통 | ⭕ 정답 | 10 / 10 |
| Q3 | OX | 🟡 보통 | 🔺 부분 정답 | 7.0 / 10 |
| Q4 | 빈칸 | 🟢 쉬움 | 🔺 부분 정답 | 8.5 / 10 |
| Q5 | 서술형 | 🔴 어려움 | 🔺 부분 정답 | 7.8 / 10 |
| 합계 | 36.4 / 50 |
총평
Q5에서 7.8점이 나왔습니다. 어려움 난이도 서술형 최고 기록을 하루 만에 다시 갱신했습니다. (4)는 네 매개변수를 전부 맞혀 만점이고, (1)의 “수신자의 ID / TCP·UDP / 페이로드”도 세 종류를 정확히 대응시킨 답입니다. 특히 Reliable·Unreliable을 TCP·UDP 기반이라고 짚으신 건 원문의 정리표와 같은 이야기입니다.
Q3으로 OX는 다섯 회차 연속 정답입니다. X → O → X → X 다음에 O를 배치했는데도 맞히셨습니다. 다만 근거를 한 줄도 쓰지 않아 3점을 통째로 놓쳤습니다. OX 배점은 판단 7점 + 근거 3점이라, 판단이 맞아도 근거가 비면 만점의 70%에서 멈춥니다. 이번엔 명제가 참이었으니 “두 사례가 각각 수직·수평 분산의 정의와 어떻게 맞아떨어지는지” 를 한두 줄 적었으면 10점이었습니다.
가장 낮은 곳은 Q1(3.1점) 입니다. Modern C++ 항목 6은 이번이 처음 나온 내용이라 낯설 만합니다. 그런데 답안을 보면 길은 반쯤 찾아 놓으셨습니다 — (3)에서 “암시적 변환이 성립되기 때문”이라고 정답의 핵심어를 정확히 쓰셨고, (2)에서도 features(w) → vector<bool> → [5] → 다른 형식까지 순서대로 따라가셨습니다. 딱 마지막 한 칸, “그 임시 객체가 문장 끝에서 죽는다”가 빠져 있습니다. 아래 피드백에서 그 한 칸을 중심으로 정리했습니다.
Q1. 🔴 어려움 — 🔺 3.1/10
문제: features()는 위젯이 지원하는 기능들을 vector<bool>로 돌려준다. 각 원소는 특정 기능의 지원 여부를 뜻한다.
std::vector<bool> features(const Widget& w);
Widget w;
bool highPriority = features(w)[5]; // ✅ 정상 동작
processWidget(w, highPriority);
형식만 auto로 바꾸면 컴파일은 되는데 미정의 동작이 된다.
auto highPriority = features(w)[5]; // ⚠️ 컴파일은 되지만 미정의 동작
processWidget(w, highPriority);
(1) auto가 추론하는 형식과, vector<bool>이 그것을 반환할 수밖에 없는 이유는?
(2) 미정의 동작이 되기까지의 과정을 ①~④ 순서대로.
(3) bool로 명시한 코드는 왜 안전한가?
(4) auto를 쓰면서 안전하게 만들려면? 그 방법의 이름은?
📝 내 풀이:
(1)
auto_initialize같은 이름의 형식으로 추론했던 것 같다. 이유는 잘 모르겠다.(2)
features(w)→vector<bool>→vector<bool>[5]→ 이것이 다른 형식으로 추론된다.(3) 암시적 변환이 성립되기 때문. 마지막 다른 형식으로 추론되는 과정
(4) 함수의 앞에 auto 관련된 지시자를 붙여줘야 했던것같다.
정답:
(1) bool이 아니라 std::vector<bool>::reference로 추론된다
vector<bool>은 다른 vector<T>와 취급이 다르다. 이유는 두 단계다.
vector<bool>은 메모리 효율을 위해 각bool을 1비트로 압축 저장한다.- 그런데 C++은 비트에 대한 참조를 금지한다. 그래서
vector<T>::operator[]가 원래 돌려주는T&, 즉bool&를 만들 수가 없다.
그래서 우회책으로 bool&처럼 동작하는 프록시 객체인 std::vector<bool>::reference를 반환한다. 명세를 보면 반환형이 T&가 아니라 reference임을 알 수 있다.
namespace std {
template <class Allocator>
class vector<bool, Allocator> {
public:
class reference { ... };
reference operator[](size_type n); // T&가 아니다!
};
}
auto는 초기화 표현식의 형식을 있는 그대로 추론하므로, bool이 아니라 이 프록시 형식을 그대로 붙잡는다.
(2) 미정의 동작이 되는 과정
auto highPriority = features(w)[5];
features(w)호출 →vector<bool>임시 객체가 생성된다.- 그 임시 객체에 대해
operator[](5)호출 →vector<bool>::reference가 반환된다. 이 프록시는 임시 객체 내부의 비트를 가리키는 포인터 + offset(5) 를 들고 있다. auto가highPriority를vector<bool>::reference형식으로 추론한다. 즉highPriority는 값이 아니라 “어딘가의 비트를 가리키는 것” 이 된다.- 문장이 끝나면서 1번의 임시 객체가 파괴된다. 그러면
highPriority가 들고 있던 포인터는 이미 사라진 메모리를 가리키는 댕글링 포인터가 된다.
그 다음 줄에서 processWidget(w, highPriority)가 highPriority를 bool로 변환하려 하면, 파괴된 임시 객체의 메모리를 읽게 되므로 미정의 동작이다.
(3) bool로 명시하면 왜 안전한가 — 변환 시점이 다르기 때문
bool highPriority = features(w)[5];
이렇게 쓰면 vector<bool>::reference → bool로의 암묵적 변환이 그 문장 안에서, 즉 임시 객체가 아직 살아 있는 동안 일어난다. 변환 결과로 얻은 것은 포인터가 아니라 true/false라는 값이다.
그래서 문장이 끝나 임시 객체가 파괴되어도 highPriority는 아무 영향을 받지 않는다. (2)의 4단계에서 문제가 되는 것은 “가리키는 대상이 사라지는 것”인데, 여기서는 애초에 가리키는 것이 아니라 값을 복사해 온 것이기 때문이다.
(4) 형식 명시 초기화 관용구 — static_cast
auto highPriority = static_cast<bool>(features(w)[5]);
features(w)[5] → reference → bool(캐스팅) 순으로 진행되고, 그 캐스팅이 임시 객체가 살아 있는 동안 끝난다. 그러면 auto도 정상적으로 bool로 추론된다.
이 방법을 형식 명시 초기화 관용구(explicitly typed initializer idiom) 라고 한다.
정리: “보이지 않는” 프록시 형식 때문에
auto가 초기화 표현식의 형식을 잘못 추론할 수 있다. 형식 명시 초기화 관용구는auto가 원하는 형식을 추론하도록 강제한다.
🔍 피드백: 논점별로 (1) 0.3 / (2) 1.0 / (3) 1.5 / (4) 0.3을 받아 3.1점입니다. 처음 나온 단원이라 낮게 나왔지만, 답안 안에 정답으로 가는 실마리가 두 개 들어 있습니다.
(3)의 “암시적 변환이 성립되기 때문”이 정확한 핵심어입니다. 여기에 시점만 붙이면 완성됩니다.
vector<bool>::reference→bool로의 암묵적 변환이 임시 객체가 아직 살아 있는 동안 일어나기 때문에 안전하다.
(2)도 뼈대는 맞습니다. features(w) → vector<bool> → [5] → 다른 형식 추론, 여기까지 순서대로 쓰셨습니다. 빠진 건 마지막 한 칸인데, 그게 하필 “왜 미정의 동작인가”의 답입니다.
④ 문장이 끝나면서
features(w)가 만든 임시vector<bool>이 파괴된다. 그러면highPriority가 들고 있던 포인터는 댕글링 포인터가 된다.
즉 위험한 이유는 “형식이 다르게 추론되어서”가 아니라, 그 형식이 값이 아니라 “어딘가를 가리키는 것”이고, 가리키던 대상이 곧바로 죽어 버리기 때문입니다. (3)에서 쓰신 암묵적 변환이 왜 구원이 되는지도 여기서 설명됩니다 — 변환하면 가리키는 것이 아니라 true/false라는 값을 복사해 오기 때문입니다. 두 소문항이 사실 한 이야기입니다.
(1)의 형식 이름은 std::vector<bool>::reference입니다. 그리고 vector<bool>만 이러는 이유가 이 항목의 출발점이라 꼭 기억해 두세요.
vector<bool>은 메모리를 아끼려고 각bool을 1비트로 압축 저장한다.- 그런데 C++은 비트에 대한 참조를 금지한다. 그래서
operator[]가bool&를 만들 수가 없다. - 그래서
bool&처럼 동작하는 프록시 객체를 대신 돌려준다.
(4) “무언가를 붙여야 했다”는 감각은 맞습니다. 붙이는 건 auto 쪽이 아니라 오른쪽 표현식입니다.
auto highPriority = static_cast<bool>(features(w)[5]);
이름은 형식 명시 초기화 관용구이고, 하는 일은 이름 그대로입니다 — auto에게 “이 형식으로 추론해라”라고 강제하는 것입니다. 이 관용구는 Q2에서 본 두 번째 용도(변환이 의도된 것임을 드러내기)로도 쓰이니, 두 문항을 묶어서 복습하시면 좋습니다.
출처:
game_dev/moderncpp/2/2026-04-08-moderncpp_2_6.md
Q2. 🟡 보통 — ⭕ 10/10
문제: 표현식 템플릿에서 operator+가 프록시 객체를 반환하는 상황과, 프록시와 무관한 float/double 변환 상황에 대한 설명으로 가장 올바른 것은?
Matrix sum = m1 + m2 + m3 + m4;
// 실제 표현식의 형식: Sum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix>
double calcEpsilon();
float ep = calcEpsilon(); // (가)
auto ep = static_cast<float>(calcEpsilon()); // (나)
- A. 표현식 템플릿에서
operator+가 프록시를 반환하는 것은 설계 실수다.Matrix를 직접 반환하도록 고쳐야auto가 안전해지고 코드도 단순해진다. - B. 프록시 클래스는 사용자가 그 존재를 모르도록 만드는 것이 좋고, 그래서
Matrix로의 암묵적 변환을 지원한다. 그런데auto는 그 암묵적 변환을 일으키지 않고 프록시 형식을 그대로 붙잡으므로static_cast<Matrix>(...)로 형식을 못 박아야 한다. 이 관용구는 프록시가 없는 (나)처럼 변환이 의도된 것임을 드러내는 데도 쓸 수 있다. - C.
auto가 위험해지는 것은 이런 프록시 클래스를 쓰는 외부 라이브러리에서만 생기는 문제다. 표준 라이브러리의 컨테이너만 쓴다면auto를 마음 놓고 써도 된다. - D.
static_cast는 런타임 변환 비용을 발생시키므로, 형식이 걱정될 때는auto를 쓰지 말고 (가)처럼 형식을 직접 적는 편이 항상 낫다.auto와static_cast를 같이 쓰는 것은 결국 형식을 두 번 적는 셈이다.
📝 내 선택: B — A는 암시적 변환이 되기에 상관없음 / C는 잘 모르겠지만, 어떤 라이브러리를 쓰는지와 관련이 없을 것 같음 / D는 잘 모르겠어서 B가 아니라면 D일 것 같음
정답: B
해설:
프록시 클래스는 버그가 아니라 최적화 장치다. 두 Matrix를 더할 때마다 Matrix를 새로 만들면 임시 객체가 계속 생기므로, 대신 “무엇을 더할 예정인가”만 들고 있는 가벼운 프록시(Sum<Matrix, Matrix>)를 반환하고 실제 계산은 마지막에 한 번에 한다.
그래서 설계 목표는 사용자가 프록시의 존재를 모르는 것이다. 그러기 위해 프록시 클래스는 Matrix로의 암묵적 변환을 지원한다. Matrix sum = ... 처럼 형식을 적어 두면 그 자리에서 변환이 일어나 사용자는 아무것도 눈치채지 못한다.
문제는 auto가 그 암묵적 변환을 일으키지 않는다는 점이다. auto는 표현식의 형식을 있는 그대로 가져오므로 Sum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix>를 붙잡아 버린다. 그래서 형식 명시 초기화 관용구로 못 박아 준다.
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4); // ✅
같은 관용구가 프록시와 무관한 곳에서도 쓸모가 있다. 변환이 의도된 것임을 코드에 드러내는 용도다.
float ep = calcEpsilon(); // double → float가 의도된 것인가, 실수인가?
auto ep = static_cast<float>(calcEpsilon()); // 의도된 것임이 분명하다
int idx = d * c.size(); // double → int가 의도된 것인가?
auto idx = static_cast<int>(d * c.size()); // 의도된 것임이 분명하다
(가)를 읽는 사람은 작성자가 정밀도 손실을 알고도 그렇게 쓴 것인지, 아니면 calcEpsilon()이 double을 반환한다는 걸 몰랐던 것인지 알 수 없다. (나)는 그 의문이 없다.
- A — 프록시 반환은 실수가 아니라 의도된 최적화다.
Matrix를 직접 반환하도록 되돌리면 임시 객체가 다시 생겨 비효율로 돌아간다. - C —
std::vector<bool>이 바로 표준 라이브러리의 프록시 사례다. 표준만 쓴다고 안전하지 않다. - D — 두 군데가 틀렸다.
static_cast<float>(double)같은 변환은 형식을 적어 놓았을 때도 어차피 일어나는 변환이므로 추가 비용이 아니다. (가)와 (나)의 런타임 동작은 같다. 그리고auto+static_cast는 “두 번 적는 것”이 아니라 한 번만 적되 그것이 의도임을 드러내는 것이다.
🔍 피드백: 만점입니다. C를 쳐낸 감각이 특히 좋았습니다. “어떤 라이브러리를 쓰는지와 관련이 없을 것 같다”고 하셨는데 정확합니다. 근거를 하나 붙여 드리면 확신이 생기실 겁니다 — 바로 옆 Q1의 std::vector<bool>이 표준 라이브러리의 프록시 사례입니다. 외부 라이브러리를 안 써도 표준 컨테이너 하나에서 이미 벌어지는 일이니, “표준만 쓰면 안전하다”는 성립할 수 없습니다.
A를 쳐낸 근거는 방향을 조금 조정하시면 좋겠습니다. “암시적 변환이 되기에 상관없다”고 하셨는데, A가 틀린 이유는 조금 다른 데 있습니다. A는 프록시를 반환하는 것 자체가 설계 실수라고 주장하는데, 사실은 의도된 최적화입니다. 매번 Matrix를 만들면 임시 행렬이 계속 생기니까 그걸 피하려고 프록시를 쓰는 거죠. A대로 “Matrix를 직접 반환하게 고치면” 원래의 비효율로 되돌아갑니다.
D는 찍으셨다고 하셨으니 근거를 남겨 둡니다. 두 군데가 틀렸습니다. 첫째, static_cast<float>(calcEpsilon())의 변환은 float ep = calcEpsilon();이라고 써도 어차피 일어나는 변환입니다. 두 코드의 런타임 동작은 같으니 “추가 비용”이 아닙니다. 둘째, auto + static_cast는 형식을 두 번 적는 것이 아니라 한 번만 적는 것입니다 — 왼쪽은 auto고 형식은 오른쪽에만 나오니까요. 오히려 float ep = calcEpsilon();이 “왼쪽에 float, 오른쪽에 double”로 형식이 둘 나오는 쪽입니다.
출처:
game_dev/moderncpp/2/2026-04-08-moderncpp_2_6.md
Q3. 🟡 보통 — 🔺 7.0/10
문제: 다음 명제가 참(O)인지 거짓(X)인지 판단하라.
어떤 게임이 채팅·전투·거래를 각각 다른 서버가 담당하도록 나눠 두었다. 어느 날 채팅 서버가 다운됐는데, 채팅만 되지 않을 뿐 전투와 거래는 정상적으로 돌아갔다. 이것은 수직 분산이 주는 안정성이다.
다른 게임은 똑같은 일을 하는 필드 서버를 여러 대 두고 플레이어를 나눠 담아 두었다. 필드 서버 2번이 다운되자 그 서버에 있던 플레이어들만 접속이 끊겼고, 그들은 다른 필드 서버에 접속해 계속 플레이할 수 있었다. 나머지 서버의 플레이어들은 아무 영향도 받지 않았다. 이것은 수평 분산이 주는 안정성이다.
두 사례는 분산 처리가 확장성뿐 아니라 안정성에도 효과를 준다는 것을 보여 준다.
📝 내 답: O (근거 없음)
정답: O
해설:
세 문단이 모두 참이다.
첫 문단 — 수직 분산 ✅
채팅·전투·거래를 기능별로 서로 다른 서버에 맡긴 것이 수직 분산이다. 그리고 그 결과도 정확히 서술되어 있다.
수직 분산의 경우, 중지된 서버가 처리하는 기능 “외” 다른 기능은 정상 작동한다.
채팅 서버가 죽었으니 채팅은 안 된다. 남은 서버들이 채팅을 대신 맡아 주지 않는다. 다만 전투와 거래는 다른 서버가 담당하므로 멀쩡하다. 명제가 딱 그렇게 쓰고 있다.
둘째 문단 — 수평 분산 ✅
같은 일을 하는 필드 서버를 여러 대 두고 플레이어를 나눈 것이 수평 분산이다.
수평 분산의 경우, 중지된 서버에 있는 플레이어들만 접속이 끊어지며, 다른 서버에 접속하면 된다.
서버들이 같은 일을 하므로 서로 대체가 가능하다. 그래서 끊긴 플레이어가 다른 서버로 옮겨 가면 그만이다.
셋째 문단 — 분산의 두 효과 ✅
분산 처리는 확장성뿐 아니라 안정성에도 효과를 준다. 서버가 한 대뿐이라면 그 한 대가 멈추는 순간 서비스 전체가 멈추지만, 나눠 두면 피해가 일부로 제한된다.
정리하면 이렇다.
| 나누는 기준 | 한 대가 죽으면 | 대체 가능한가 | |
|---|---|---|---|
| 수평 분산 | 같은 일을 하는 서버를 여러 대 | 그 서버의 플레이어만 끊긴다 | 가능 — 다른 서버로 옮기면 된다 |
| 수직 분산 | 기능별로 서로 다른 서버 | 그 기능만 멈춘다 | 불가능 — 다른 서버는 그 일을 못 한다 |
혼동하기 쉬운 지점은 “남은 서버가 대신 맡아 준다” 가 수평 분산에만 해당한다는 것이다. 수직 분산은 각 서버가 다른 일을 하므로 대체가 불가능하고, 대신 피해가 그 기능 하나로 격리되는 것이 이득이다.
🔍 피드백: 판단 7점 만점, 근거 3점 중 0점으로 7.0점입니다.
OX 다섯 회차 연속 정답입니다. 최근 정답이 X → O → X → X로 나와서 규칙을 찾으려 하면 오히려 헷갈리는 자리였는데 O를 맞히셨습니다. 게다가 이번 문항은 성격이 달랐습니다 — 어제까지는 “거짓인 문장을 찾아내는” 문제였고, 이번엔 두 시나리오가 각각 수직/수평 분산으로 올바르게 분류되었는지 검증하는 문제였습니다. 정의를 외운 것만으로는 안 되고 구체적 상황에 적용할 수 있어야 하는데, 그걸 하셨습니다.
그런데 근거를 한 줄도 안 쓰셔서 3점을 통째로 놓쳤습니다. 이게 이번 회차에서 가장 아까운 실점입니다 — 내용을 몰라서가 아니라 쓰지 않아서 빠진 점수니까요.
OX 배점은 판단 7점 + 근거 서술 3점입니다. 판단만 맞히면 아무리 잘해도 7점에서 멈춥니다. 지난 회차들에서는 “일단 나눠두는 것은 비용적인 측면이 너무 크므로 좋지 않다”, “수직 분산에서 기능을 나눠 맡는다는 것이 틀렸다”처럼 한 줄씩 적어 2점대를 챙기셨는데, 이번엔 그게 빠졌습니다.
O라고 판단했을 때 근거를 쓰는 요령은 X일 때와 다릅니다. X는 “틀린 곳”을 짚으면 되지만, O는 짚을 곳이 없으니 각 문단이 왜 참인지를 정의에 대응시키면 됩니다. 이번 문항이라면 이 정도로 충분했습니다.
채팅 서버가 죽어도 전투·거래는 다른 서버가 담당하므로 정상 작동한다 → 수직 분산의 정의와 일치. 필드 서버 2번이 죽으면 그 서버 플레이어만 끊기고 다른 서버로 옮겨 가면 된다 → 수평 분산의 정의와 일치. 따라서 참.
두 줄이면 만점입니다. 다음 OX부터는 답을 쓰고 반드시 한 줄을 덧붙이는 것을 규칙으로 삼으세요.
출처:
server/game_server/9/2026-03-22-game_server_9_11.md
Q4. 🟢 쉬움 — 🔺 8.5/10
문제: 빈칸을 채우시오.
게임 네트워크 엔진 프라우드넷의 네트워크 모듈은 크게 두 개다.
- ①
___— 게임 서버의 메인 모듈. 클라이언트의 연결을 받고 메시지를 주고받는다. 각 클라이언트의 ③___상황도 열람할 수 있다. - ②
___— 게임 클라이언트의 네트워크 모듈. 서버로 연결을 맺고 메시지를 주고받는다. 서버를 거치지 않고 다른 클라이언트와 직접 통신하는 것도 가능한데, 이런 방식을 ④___통신이라 한다.
📝 내 답: NetServer, NetClient, 연결, P2P
정답:
① NetServer ② NetClient ③ 네트워크 ④ P2P (Peer to Peer)
해설:
프라우드넷의 네트워크 모듈은 서버 쪽 하나, 클라이언트 쪽 하나로 짝을 이룬다.
① NetServer 클래스
- 게임 서버의 메인 모듈이다.
- 클라이언트 연결을 받고, 메시지를 주고받는 역할을 한다.
- 각 클라이언트의 네트워크 상황을 열람할 수 있다.
② NetClient 클래스
- 게임 클라이언트의 네트워크 모듈이다.
- 서버로 연결을 맺고 메시지를 주고받을 수 있다.
- 다른 클라이언트와 P2P 통신이 가능하다.
여기서 눈여겨볼 것은 ④ P2P다. 서버-클라이언트 구조에서 모든 통신이 서버를 경유한다면 클라이언트 A가 B에게 무언가 보낼 때 A → 서버 → B 두 번의 홉이 필요하다. P2P는 그 중계를 건너뛰고 A → B로 직접 보낸다. 지연이 줄고 서버 부하도 덜지만, 대신 클라이언트끼리 직접 연결되므로 신뢰 문제나 NAT 통과 같은 별도의 고려가 따라온다.
🔍 피드백: ①②④를 정답 처리하고 ③에 부분 점수를 주어 8.5점입니다.
①② NetServer / NetClient를 정확히 쓰셨고, ④ P2P도 맞혔습니다. 클래스 이름을 대소문자까지 정확히 기억하고 계신 게 좋습니다.
③ “연결”에는 부분 점수를 드렸습니다. 정답은 네트워크입니다. 원문은 NetServer가 “각 클라이언트의 네트워크 상황 열람 가능”이라고 적고 있습니다. “연결 상황”도 아주 틀린 말은 아니지만 범위가 좁습니다 — 연결 여부는 네트워크 상황의 일부일 뿐이고, 실제로 서버가 보고 싶은 것은 각 클라이언트의 핑, 패킷 손실률, 대역폭 같은 것들까지 포함하니까요.
바로 다음 문항인 Q5와 연결해 보시면 왜 그런지 보입니다. UnreliableSend로 위치를 보낼지, 얼마나 자주 보낼지 같은 판단을 하려면 연결됐는지 여부만으로는 부족하고 회선 품질을 알아야 합니다. NetServer가 “네트워크 상황”을 열람할 수 있다는 건 그런 판단 재료를 제공한다는 뜻입니다.
출처:
server/game_server/6/2026-02-18-game_server_6_2.md
Q5. 🔴 어려움 — 🔺 7.8/10
문제: 프라우드넷에서 SendUserMessage()로 바이너리 데이터를 주고받는 방식에 대해 답하시오.
c->SendUserMessage(HostID_Server, RmiContext::ReliableSend, data, 100);
s->SendUserMessage(ClientHostID, RmiContext::UnreliableSend, data, 50);
s->SendUserMessage(sendTo, 10, RmiContext::UnreliableSend, data, 30);
c->OnReceiveUserMessage =
[...](HostID sender, const RmiContext& rmiContext,
uint8_t* payload, int payloadLength) { ... };
(1) SendUserMessage()에 넘기는 인자 세 종류는 각각 무엇을 정하는가?
(2) 전송 옵션들을 담는 구조체의 이름과, 거기 담기는 옵션 두 가지 더는?
(3) ㉠ 아이템 구매 사실 ㉡ 매 틱 갱신되는 캐릭터 위치 — 각각 Reliable / Unreliable 중 무엇을?
(4) 수신 콜백의 매개변수 네 개는 각각 무엇을 뜻하는가?
📝 내 풀이:
(1) 수신자의 ID, TCP/UDP, 페이로드(데이터)
(2) RmiContext, 다른게 무엇이 있었는지는 모르겠다.
(3) ㄱ은 ReliableSend이며, 확실히 전달되어야 하는 사실 ㄴ은 UnreliableSend이며, 확실히 전달되지 않아도 큰 문제가 생기지 않는 데이터
(4) 송신자의 ID, 전송 설정, 데이터, 데이터 길이
정답:
(1) 세 종류의 인자
| 인자 | 정하는 것 |
|---|---|
| HostID 또는 HostID array | 누구에게 보낼 것인가 (대상). 배열로 넘기면 여러 클라이언트에게 한 번에 보낼 수 있다 |
| reliable / unreliable (+ 옵션들) | 어떻게 보낼 것인가. 전송을 보장할지, 보장하지 않는 대신 즉시 보낼지 |
| byte array | 무엇을 보낼 것인가. 보낼 메시지에 해당하는 바이너리 데이터 |
HostID_Server처럼 미리 정의된 값도 있어서 클라이언트가 서버로 보낼 때 쓴다.
(2) RmiContext 구조체
전송 방식과 관련된 옵션들을 통틀어 RmiContext 구조체라고 한다. ReliableSend/UnreliableSend 외에 담기는 옵션은 다음과 같다.
- 암호화 여부
- 압축 여부
- 오래된 메시지는 건너뛸지 여부
마지막 옵션이 특히 흥미롭다. 위치 갱신처럼 최신 값만 의미가 있는 데이터라면, 밀려 있는 낡은 메시지를 굳이 보낼 이유가 없기 때문이다.
(3) 무엇을 고를 것인가
㉠ 아이템 구매 사실 → ReliableSend
TCP 기반이라 전송이 보장된다. 아이템 구매는 한 번 일어나고 다시 오지 않는 사건이며, 이 메시지가 유실되면 “돈은 빠졌는데 아이템이 없다” 같은 상태 불일치가 생긴다. 뒤에 오는 메시지가 이 값을 덮어써 주지도 않는다. 그래서 반드시 도착해야 하는 데이터에는 Reliable을 쓴다. (아이템·채팅 등이 여기 속한다.)
㉡ 캐릭터 위치 → UnreliableSend
UDP 기반이라 보장은 없지만 빠르다. 위치는 매 틱 새 값이 오는 데이터이므로, 한 프레임의 위치가 유실되어도 다음 값이 곧 덮어쓴다. 오히려 Reliable로 보내면 유실된 패킷의 재전송을 기다리느라 뒤에 있는 최신 위치까지 늦게 도착해 캐릭터가 끊겨 보인다.
핵심 기준은 “이 메시지가 사라지면 뒤에 오는 것이 복구해 주는가” 다. 복구되지 않는 사건은 Reliable, 곧 덮어써질 상태 값은 Unreliable이다.
| 전송 방식 | 특징 | 사용 시기 |
|---|---|---|
| ReliableSend | TCP 기반, 전송 보장 | 중요한 데이터 (아이템, 채팅) |
| UnreliableSend | UDP 기반, 빠른 전송 | 실시간 데이터 (위치, 상태) |
(4) 수신 콜백의 매개변수
메시지를 수신하면 OnReceiveUserMessage 이벤트 함수가 호출된다.
| 매개변수 | 의미 |
|---|---|
HostID sender |
누가 보냈는가 |
const RmiContext& rmiContext |
어떠한 방식으로 받았는가 (reliable/unreliable, 압축·암호화 여부 등) |
uint8_t* payload |
받은 메시지 본문 (바이너리 데이터) |
int payloadLength |
그 메시지의 길이 |
보낼 때 넘긴 세 종류(대상·방식·데이터)가 받는 쪽에서 그대로 대응된다는 점을 보면 구조가 눈에 들어온다 — 보낸 사람, 어떻게 왔는지, 무엇이 왔는지, 얼마나 왔는지.
참고: 메시지 전송 방법은 두 가지이고, 이 문제에서 다룬 것은 전통적인 바이너리 데이터 주고받기다. 나머지 하나는 다른 컴퓨터에 있는 함수를 원격 호출하는 방식(RMI)이다.
🔍 피드백: 논점별로 (1) 2.3 / (2) 1.2 / (3) 1.8 / (4) 2.5를 받아 7.8점입니다. 어려움 난이도 서술형 최고 점수를 하루 만에 다시 갱신했습니다.
(4)는 만점입니다. 네 매개변수를 하나도 틀리지 않고 대응시키셨습니다. 여기에 구조를 하나 얹어 두면 외울 필요도 없어집니다 — 보낼 때 넘긴 세 종류(대상·방식·데이터)가 받는 쪽에 그대로 대응되고, 거기에 길이만 하나 더 붙습니다. 보낸 사람 / 어떻게 왔는지 / 무엇이 왔는지 / 얼마나 왔는지.
(1)도 거의 만점입니다. 특히 Reliable/Unreliable을 “TCP/UDP” 라고 쓰신 게 좋았습니다. 옵션 이름을 외운 게 아니라 그 실체를 알고 계신다는 뜻이니까요 — 원문 정리표도 ReliableSend = TCP 기반, UnreliableSend = UDP 기반으로 적고 있습니다. 0.2점만 빠진 건 문제가 “각각이 무엇을 정하는지“를 물었기 때문입니다. 세 인자를 누구에게 / 어떻게 / 무엇을 이라는 세 질문에 대응시켜 두면 순서까지 자연스럽게 기억됩니다.
(2) 구조체 이름 RmiContext는 정확합니다. 나머지 세 옵션을 채워 두세요.
- 암호화 여부
- 압축 여부
- 오래된 메시지는 건너뛸지 여부
마지막 것이 특히 기억할 만합니다. 최신 값만 의미가 있는 데이터(위치처럼)라면 밀려 있는 낡은 메시지를 굳이 보낼 이유가 없으니, 아예 건너뛰는 옵션을 두는 겁니다. 이게 (3)㉡의 사고방식과 정확히 같은 이야기입니다.
(3)은 선택이 둘 다 맞아 점수를 드렸는데, 이유가 질문을 되풀이하는 데 그쳤습니다.
㉠은 “확실히 전달되어야 하는 사실” → 그래서 Reliable ㉡은 “확실히 전달되지 않아도 큰 문제가 생기지 않는 데이터” → 그래서 Unreliable
이건 Reliable/Unreliable의 정의를 그대로 되돌려 놓은 것이라, “왜 위치는 유실돼도 괜찮은가?”라는 질문에는 아직 답하지 않은 상태입니다. 판별 기준을 하나 드리겠습니다.
“이 메시지가 사라지면, 뒤에 오는 것이 복구해 주는가?”
- 아이템 구매 — 한 번 일어나고 다시 오지 않는 사건입니다. 유실되면 “돈은 빠졌는데 아이템이 없다”가 되고, 뒤에 오는 어떤 메시지도 이걸 고쳐 주지 않습니다. → Reliable
- 캐릭터 위치 — 매 틱 새 값이 옵니다. 한 프레임 것이 사라져도 다음 값이 곧 덮어씁니다. → Unreliable
그리고 ㉡에는 한 걸음 더 있습니다. 위치를 Reliable로 보내면 단지 “낭비”인 게 아니라 오히려 나빠집니다. 유실된 패킷의 재전송을 기다리느라 그 뒤에 있는 최신 위치까지 발이 묶여 캐릭터가 뚝뚝 끊겨 보이거든요. 즉 Unreliable을 고르는 건 “덜 중요해서”가 아니라 “늦은 정답보다 이른 근사값이 나은 데이터라서” 입니다.
출처:
server/game_server/6/2026-02-23-game_server_6_4.md
📌 복습 포인트
- Q1
auto와 프록시 형식 (이번 회차 최우선) —vector<bool>은 1비트 압축 저장 → 비트 참조 금지 →bool&불가 → 프록시(vector<bool>::reference) 반환. 위험한 이유는 형식이 달라서가 아니라 가리키던 임시 객체가 문장 끝에서 죽어 댕글링이 되기 때문. 해법은static_cast<bool>(...), 이름은 형식 명시 초기화 관용구 →game_dev/moderncpp/2/2026-04-08-moderncpp_2_6.md - Q3 OX 근거 쓰기 (점수 직결) — 판단 7점 + 근거 3점. 답이 O일 때는 각 문단이 정의와 어떻게 맞아떨어지는지를 한 줄로 대응시키면 된다 →
server/game_server/9/2026-03-22-game_server_9_11.md - Q4 NetServer가 보는 것 — “연결”이 아니라 네트워크 상황(핑·손실률 등). 전송 방식을 판단하려면 연결 여부만으로는 부족하다 →
server/game_server/6/2026-02-18-game_server_6_2.md - Q5 RmiContext의 나머지 옵션 — 암호화 / 압축 / 오래된 메시지 건너뛰기 →
server/game_server/6/2026-02-23-game_server_6_4.md - Q5 Reliable vs Unreliable 판별 기준 — “이 메시지가 사라지면 뒤에 오는 것이 복구해 주는가?” 복구 안 되는 사건은 Reliable, 곧 덮어써질 상태 값은 Unreliable → 같은 문서
댓글남기기