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

게시:     수정

카테고리:

태그:

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

말랑말랑 퀴즈 — 해답지 ✅

날짜: 2026-09-10 문제 수: 6문제 총점: 42.9/60 (72점) ✅ 잘했어요

📊 채점 결과

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

총평

Q3 오답노트 문제를 만점으로 통과하셨습니다. 어제 두 번 놓쳤던 1:1세션을 이번엔 정확히 채우셨고, 문제 틀이 흐름도 → 비교표 → 회사 연표로 세 번 바뀌었는데도 흔들리지 않았습니다. 답의 위치를 외운 게 아니라 개념이 잡혔다는 뜻입니다. stage 1로 올라갔고, 한 번 더 만점이면 오답노트에서 사라집니다.

Q1은 8.3점으로 어려움 서술형 최고 기록을 또 갱신했습니다. 특히 (1)과 (2)가 만점입니다. (1)의 *를 기준으로 좌측이면 값, 우측이면 포인터`는 이 항목의 핵심을 한 문장으로 압축한 정리이고, (2)는 “예상과 달랐던 것 같다”며 자신 없어 하셨지만 네 가지가 전부 정확했습니다. 확신을 가지셔도 됩니다.

Q5(3)㉢에서 “배열의 주소는 재할당되기 때문”이라고 쓰신 것도 짚고 싶습니다. 이건 바로 어제 Q4(2)에서 힌트까지 드렸는데 놓치셨던 내용입니다. 하루 만에 자기 답으로 나왔습니다.

아쉬운 곳은 Q4입니다. OX 다섯 회차 연속 정답 뒤의 첫 오답인데, 이 한 문항 때문에 총점이 16점 내려갔습니다(맞혔다면 88점). 함정이 이전과 결이 달랐습니다 — 거짓인 문장을 심는 대신 참인 사실(CPU 0%)에서 잘못된 결론(그러니 손해 없음)으로 미끄러지게 만들었습니다. 아래 피드백에서 그 미끄러짐을 자세히 정리했습니다.


Q1. 🔴 어려움 — 🔺 8.3/10

문제: C++의 const 키워드에 대해 답하시오.

(1) 네 가지 포인터 선언의 의미와, const의 위치가 무엇을 가르는지 (2) const iteratorconst_iterator의 차이 (3) const 유무만 다른 operator[] 오버로딩과, 함수 뒤 const의 의미 (4) ㉠ length() const 안에서 멤버를 수정할 때 에러가 나는 근거가 되는 상수성 개념과 그 대비 개념 ㉡ 고치는 방법

📝 내 풀이:

(1) ㄱ: 포인터 O, 값 O / ㄴ: 포인터 O, 값 X / ㄷ: 포인터 X, 값 O / ㄹ: 포인터 X, 값 X *를 기준으로 좌측이면 값, 우측이면 포인터

(2) 이건 예상과 달랐던 것 같다. 아마 (가)는 T* const처럼 동작하며 *it = 10;만 가능, (나)가 const T*처럼 동작하며 ++it;만 가능했던 것으로 기억한다.

(3) 1번은 const TextBlock 타입의 객체, 2번은 TextBlock의 객체 const를 지정하면 내부에서 변수를 수정하지 못한다.

(4) ㄱ: 무슨 상수성 개념이 있었냐는 기억이 안난다. 이게 안되는 이유는 함수에 const가 붙여져 있기 때문이다. ㄴ: mutable 키워드를 pText, textLength의 타입 앞에 붙여야 한다.

정답:

(1) 네 가지 포인터 선언

선언 데이터(가리키는 값) 수정 포인터(주소) 수정
char * p
const char * p
char * const p
const char * const p

정리하면 이렇다.

const*의 앞에 있으면 “가리키는 데이터”가 상수, 뒤에 있으면 “포인터 자체”가 상수다.

한편 const와 타입의 위치는 서로 바뀌어도 상관없다. 아래 두 함수는 완전히 같다.

void f1(const Widget * pw);      // 둘 다 "상수 Widget 객체를 가리키는 포인터"
void f2(Widget const * pw);

(2) 반복자에서의 대응

// ㉮ T * const 처럼 동작한다 — 주소가 상수
const std::vector<int>::iterator iter = vec.begin();
*iter = 10;    // ⭕ 데이터 수정 가능
++iter;        // ❌ 주소 수정 불가능

// ㉯ const T * 처럼 동작한다 — 데이터가 상수
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10;   // ❌ 데이터 수정 불가능
++cIter;       // ⭕ 주소 수정 가능

헷갈리기 쉬운 지점은 const iterator가 “값을 못 바꾸는 반복자”가 아니라 “자기 자신을 못 바꾸는 반복자” 라는 것이다. 반복자 자체가 포인터 역할이므로, 그 앞에 붙인 const포인터를 상수로 만든다. 값을 보호하고 싶다면 const_iterator를 써야 한다.

(3) const 멤버 함수와 오버로딩

TextBlock tb("Hello");
std::cout << tb[0];      // (2)번, 비상수 버전 호출

const TextBlock ctb("World");
std::cout << ctb[0];     // (1)번, 상수 버전 호출
  • (1) 상수 버전 — 상수 객체에 대해 호출된다.
  • (2) 비상수 버전 — 비상수 객체에 대해 호출된다.

함수 뒤에 붙은 const“이 멤버 함수는 상수 객체에 대해 호출될 수 있다” 는 뜻이다. 즉 그 함수가 객체를 변경하지 않겠다는 선언이며, 이 차이만으로도 오버로딩이 성립한다.

이렇게 얻는 이점이 두 가지다.

  1. 클래스의 인터페이스를 이해하기 좋아진다. 사용자가 ‘객체를 변경할 수 있는 함수’와 ‘변경할 수 없는 함수’를 구분하기 쉬워진다.
  2. 상수 객체를 사용할 수 있게 된다. 성능을 위해 객체를 상수 객체에 대한 참조자(reference-to-const) 로 전달하려면, 그 참조자로 부를 수 있는 함수가 있어야 한다.
cout << tb[0];      // ⭕
tb[0] = 'x';        // ⭕
cout << ctb[0];     // ⭕
ctb[0] = 'x';       // ❌ operator[]의 반환 타입이 const char&이기 때문

(4)㉠ 비트수준(물리적) 상수성 vs 논리적 상수성

에러의 근거가 되는 것은 비트수준(물리적) 상수성이다.

비트수준 상수성: 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 멤버 함수가 const임을 인정한다.

length()const인데 textLengthlengthIsValid를 수정하려 하므로 이 기준에 걸려 에러가 난다.

대비되는 개념은 논리적 상수성이다.

논리적 상수성: 일부 몇 비트 정도는 수정할 수 있되, 그것을 사용자 측에서 알아채지 못하면 된다.

논리적 상수성이 필요한 이유는 비트수준 상수성이 완벽하지 않기 때문이다. 아래 코드는 비트수준으로는 const이지만 실제로는 객체 내용이 바뀐다.

class CTextBlock {
public:
    char& operator[](size_t position) const { return pText[position]; }   // 멤버를 안 건드림 → const 인정
private:
    char* pText;
};

const CTextBlock cctb("Hello");
char* pc = &cctb[0];
*pc = 'J';              // ⚠️ 상수 객체인데 내용이 바뀐다!

반환값이 주소라서, 그 주소가 가리키는 값을 밖에서 고칠 수 있는 것이다.

mutable 키워드를 데이터 멤버 앞에 붙인다

class CTextBlock {
public:
    size_t length() const;
private:
    char*          pText;
    mutable size_t textLength;      // ✅
    mutable bool   lengthIsValid;   // ✅
};

mutable을 붙인 멤버는 const의 제한에서 예외 대우를 받아, 상수 멤버 함수 안에서도 수정할 수 있다. 캐싱처럼 “겉으로 드러나는 상태는 그대로인데 내부적으로만 갱신하는” 경우에 쓴다.

덤 — 코드 중복 제거: const가 붙고 안 붙고 차이뿐인 두 함수를 만들 때, 비상수 버전이 상수 버전을 호출하게 하면 본문을 한 번만 쓸 수 있다.

char& operator[](size_t position) {
    return const_cast<char&>(                      // 3. const를 떼어냄
        static_cast<const TextBlock&>(*this)       // 1. const를 붙임
            [position]                             // 2. 상수 버전 호출
    );
}

순서가 중요하다 — const를 붙여 상수 버전을 부른 뒤, 결과에서 const를 떼어낸다. 반대 방향(상수 버전이 비상수 버전을 호출)은 상수 객체를 비상수로 만드는 셈이라 위험하다.

🔍 피드백: 논점별로 (1) 2.5 / (2) 2.5 / (3) 2.2 / (4) 1.1을 받아 8.3점입니다. 어려움 난이도 서술형 최고 기록을 다시 갱신하셨습니다.

(1)이 만점입니다. 네 선언의 수정 가능 여부를 하나도 틀리지 않으셨고, 무엇보다 마지막 한 줄이 좋았습니다.

*를 기준으로 좌측이면 값, 우측이면 포인터

이 항목이 하려는 말을 한 문장으로 압축한 정리입니다. 여기에 하나만 덧붙여 두시면 완결됩니다 — const와 타입의 위치는 서로 바뀌어도 상관없습니다. const Widget* pwWidget const* pw는 완전히 같은 선언이고, 둘 다 *왼쪽이므로 “가리키는 데이터가 상수”입니다.

(2)도 만점인데, “예상과 달랐던 것 같다”고 쓰셨습니다. 네 가지 판단이 전부 맞았으니 확신을 가지셔도 됩니다. 헷갈리셨던 이유가 짐작되는데, 아마 const iterator라는 이름이 “값을 못 바꾸는 반복자”처럼 읽혀서일 겁니다. 그런데 (1)에서 정리하신 규칙을 그대로 적용하면 답이 나옵니다 — 반복자 자체가 포인터 역할이므로, 그 앞에 붙은 constT* const, 즉 포인터를 상수로 만듭니다. 값을 보호하려면 이름에 _가 들어간 const_iterator를 써야 하고요.

(3) 어떤 객체에 대해 호출되는지는 정확합니다. 함수 뒤 const를 “내부에서 변수를 수정하지 못한다”고 설명하셨는데, 이건 결과이고 원문이 말하는 의미는 조금 다른 각도입니다.

“해당 멤버 함수가 상수 객체에 대해 호출된 함수”라는 것을 알려준다.

같은 동전의 양면이라 감점은 0.3에 그쳤습니다. 다만 “상수 객체에 대해 호출될 수 있다”로 기억해 두시면 왜 이 차이만으로 오버로딩이 성립하는지가 자연스럽게 따라옵니다 — 호출 대상이 다른 함수니까요.

(4)가 이번 문항에서 유일하게 약한 곳입니다. 두 상수성 개념의 이름을 꼭 기억해 두세요.

개념
비트수준(물리적) 상수성 멤버 함수가 어떤 데이터 멤버도 건드리지 않아야 const임을 인정
논리적 상수성 일부 비트는 수정하되, 사용자가 알아채지 못하면 된다

컴파일러가 채택한 기준이 비트수준 상수성이고, length()가 에러 나는 것도 그 때문입니다. 그런데 이 기준은 완벽하지 않습니다 — char& operator[](size_t) const처럼 주소를 반환하면 멤버를 직접 건드리지 않고도 밖에서 내용을 고칠 수 있거든요. 그 허점을 보완하려고 나온 개념이 논리적 상수성이고, 그것을 코드로 구현하는 수단이 바로 mutable입니다.

㉡의 mutable은 정확합니다. 다만 붙일 대상이 조금 어긋났습니다 — textLengthlengthIsValid 두 개입니다. pTextlength() 안에서 수정되지 않으므로(읽기만 합니다) mutable이 필요 없습니다. 에러 메시지가 난 줄에 등장하는 멤버만 붙이면 된다고 기억하세요.

출처: game_dev/cpp/chapter1/2025-04-06-cpp_1_3.md


Q2. 🔴 어려움 — ⭕ 10/10

문제: 게임에서 실제 레이턴시를 줄이는 방법에 대한 설명으로 가장 올바른 것은?

  • A. TCP는 패킷 유실이 발생하면 재전송을 위한 지연이 생겨 스터터링을 유발하므로, 캐릭터 이동 메시지처럼 실시간성이 중요한 데이터에는 UDP가 적합하다. 한편 메시지를 뭉쳐서 보낼 때는 뭉치는 시간이 길수록 패킷 수가 줄어 유리하므로, 100ms 이상 모았다가 한 번에 보내는 것이 권장된다.
  • B. C/S 네트워킹과 P2P 네트워킹은 언제나 함께 쓰는 것이 최선이다. 게임 서버를 여러 지역에 분포시켜 가까운 서버에 매칭시키더라도, P2P를 함께 써야 레이턴시를 조금이라도 더 줄일 수 있기 때문이다.
  • C. 레이턴시를 줄이는 방법은 크게 ① TCP 대신 UDP 사용 ② 적은 수의 패킷으로 전송 ③ 두 네트워킹 방식 혼용 세 가지다. 이 중 혼용은 서버는 멀고 두 클라이언트는 가까운 경우에 효과적이며, 이때 중요한 연산은 C/S로, 덜 중요한 연산은 P2P로 나누어 처리하면 효과적이다.
  • D. UDP는 패킷이 유실되어도 재전송 지연이 생기지 않는다. 따라서 아이템 획득이나 채팅처럼 반드시 도착해야 하는 데이터도 UDP로 보내는 편이 낫다. 유실되더라도 애플리케이션이 다시 요청하면 되므로, 어차피 TCP를 쓸 이유가 없다.

📝 내 선택: C — A: 이것은 레이턴시를 줄이는 법이 아닌, 통신량을 줄이는 방법이다. / B: 정확히 기억은 안나지만, 언제나는 아니었던 것 같다. / D: 정확한 전송이 필요한 데이터는 TCP로 보내야 한다. UDP는 전송을 보장하지 않는다.

정답: C

해설:

레이턴시를 줄이는 방법은 세 가지다.

① TCP 대신 UDP 사용

TCP는 패킷 유실이 발생하면 재전송을 위한 지연 시간이 생기고, 그 결과 스터터링(캐릭터가 뚝뚝 끊겨 보이는 현상)이 나타난다. UDP는 유실이 나도 지연이 생기지 않으므로 캐릭터 이동 메시지에 적합하다.

② 적은 수의 패킷으로 전송

메시지를 뭉쳐서 보내면 패킷 수가 줄어 효과적이다. 다만 무한정 모을 수는 없다 — 모으는 동안 그 메시지들이 대기하므로 그만큼 지연이 늘기 때문이다. 저자는 뭉치는 시간이 10ms 이하가 적당하다고 본다.

③ 두 가지 네트워킹 방식 혼용

  • C/S 네트워킹 — 클라이언트와 서버 간 통신
  • P2P 네트워킹 — 클라이언트끼리 통신

혼용이 항상 필요한 것은 아니다. C/S만으로도 게임 서버를 많은 지역에 분포시키고 가까운 지역의 서버에 매칭시키면 되므로, 그런 경우에는 혼용할 필요가 없다.

혼용이 효과적인 경우는 서버는 멀고, 두 클라이언트는 가까운 경우다. 이때는 중요한 연산은 C/S로, 덜 중요한 연산은 P2P로 나누어 처리하면 효과적이다.

  • A — 앞부분(TCP 재전송 지연 → 스터터링, 이동 메시지는 UDP)은 맞지만 뒷부분이 정반대다. 뭉치는 시간은 길수록 좋은 게 아니라 10ms 이하가 적당하다. 오래 모을수록 패킷 수는 줄지만 모으는 동안 쌓인 지연이 레이턴시를 줄이려던 목적을 깎아먹는다.
  • B — “언제나 최선”이 아니다. 가까운 지역 서버에 매칭시키는 것만으로 충분하다면 혼용할 필요가 없다. 혼용은 공짜가 아니라 구현 복잡도와 보안 고려를 함께 늘리는 선택이다.
  • D — UDP의 성질은 맞게 썼지만 결론이 틀렸다. 아이템 획득이나 채팅처럼 반드시 도착해야 하는 데이터에는 TCP(신뢰성 전송)가 필요하다. UDP가 유리한 것은 캐릭터 이동처럼 뒤에 오는 값이 앞의 값을 덮어써 주는 데이터에 한한다. “유실되면 다시 요청하면 된다”는 것은 곧 애플리케이션이 TCP가 하는 일을 직접 만들겠다는 뜻이라, 이득이 없다.
방법 효과 주의사항
UDP 사용 재전송 지연 제거 중요 데이터는 TCP 필요
패킷 묶기 오버헤드 감소 10ms 이하 권장
C/S + P2P 혼용 레이턴시 최소화 보안 고려 필요

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

D를 쳐낸 근거가 가장 정확합니다. “정확한 전송이 필요한 데이터는 TCP로 보내야 한다”는 9월 8일 회차에서 다룬 Reliable/Unreliable 판단과 정확히 같은 기준입니다. 그때 정리했던 “이 메시지가 사라지면 뒤에 오는 것이 복구해 주는가” 가 여기서도 그대로 작동합니다 — 아이템 획득은 복구되지 않고, 캐릭터 위치는 다음 값이 덮어씁니다.

A를 쳐낸 근거는 한 가지만 짚어 두겠습니다. “레이턴시를 줄이는 법이 아니라 통신량을 줄이는 방법”이라고 하셨는데, 사실 패킷 묶기도 레이턴시를 줄이는 세 방법 중 하나로 분류됩니다. 패킷 수가 줄면 각 패킷의 헤더 오버헤드와 처리 비용이 줄어 결과적으로 지연도 줄기 때문입니다.

A가 오답인 진짜 지점은 숫자입니다.

뭉치는 시간이 길수록 유리하므로 100ms 이상 모았다가 → ❌ 저자 권장은 10ms 이하 → ✅

이유가 재미있습니다. 오래 모을수록 패킷 수는 확실히 줄지만, 모으는 동안 그 메시지들이 그냥 앉아서 기다립니다. 즉 레이턴시를 줄이려고 한 일이 레이턴시를 만드는 셈이라, 어느 지점에서 멈춰야 합니다. 그 지점이 10ms입니다.

B는 “정확히 기억은 안 나지만”이라고 하셨는데 판단이 맞았습니다. 근거를 드리면 — C/S만으로도 서버를 여러 지역에 분포시켜 가까운 서버에 매칭시키면 충분합니다. 그럴 땐 혼용할 이유가 없습니다. 혼용이 빛나는 건 서버는 멀고 두 클라이언트는 가까운 특정 상황뿐이고, 그때조차 중요한 연산은 C/S, 덜 중요한 연산은 P2P 로 나눕니다. P2P에 중요한 판정을 맡기면 클라이언트가 조작할 여지가 생기니까요.

출처: server/game_server/5/2026-02-15-game_server_5_6.md


Q3. 🟢 쉬움 · 📌 오답노트 — ⭕ 10/10

문제: 빈칸을 채우시오.

1998년   네트워크 기능이 아예 없었다.
2001년   네트워크를 붙였지만 ① ___ 플레이만 가능했다.
2005년   최대 8명. 방을 만든 사람의 PC가 서버 역할을 겸했다.
         → ② ___ 방식, 그 사람을 ③ ___ 라 부른다.
2010년   전용 서버 프로그램을 24시간 가동. 렌더링·입력 처리를 하지 않고 연결만 처리.
         → ④ ___ 서버

2005년 방식에는 ③ 가 유리하다는 ⑤ ___ 문제와, ③ 가 나가면 판이 깨지는 문제가 있었다.

📝 내 답: 1:1, 세션, 호스트, 데디케이티드, 형평성

정답:

① 1:1 ② 세션 ③ 호스트 ④ 데디케이티드 (Dedicated) ⑤ 공정성

해설:

이 변천사는 게임 서버가 자리 잡기까지의 네 단계를 그대로 따라간다. 구분의 축은 참여 인원과 서버의 주체다.

단계 네트워크 서버 역할 렌더링 공정성 안정성
단독 플레이 없음 없음 각자
① 1:1 플레이 두 명 직접 연결 없음 각자
② 세션 방식 3명 이상 플레이어 중 한 명(③ 호스트) 호스트도 렌더링 호스트 유리 가능 호스트 이탈 시 문제
④ 데디케이티드 서버 다수 전용 프로그램 하지 않음 공정함 안정적

① 1:1 — 과거의 패키지 게임은 대부분 네트워크를 쓰지 않았고, 쓰더라도 1:1 플레이만 지원했다.

② 세션 방식 / ③ 호스트 — 세 명 이상이 인터넷으로 함께 플레이하되 별도의 서버가 없으므로, 플레이어 중 한 명이 호스트 역할을 맡아 게임 상태를 관리하고 나머지와 동기화한다. 한 판(세션)을 플레이어 한 명이 유지해 준다는 뜻이다.

⑤ 공정성 — 호스트도 플레이어이므로 자기 화면에서는 지연이 없어 유리해질 수 있다. 이것이 세션 방식의 첫 번째 약점이고, 두 번째 약점이 호스트 이탈 시 게임이 깨지는 것이다.

④ 데디케이티드 서버렌더링과 사용자 입력 처리를 하지 않고, 순전히 클라이언트의 연결을 처리하는 프로그램이다. 게임 플레이에 참여하지 않으므로 위 두 약점이 함께 사라진다.

  • 호스트 플레이어의 네트워크·성능에 영향받지 않음
  • 게임 진행의 일관성과 공정성 보장
  • 서버 전용 최적화 가능
  • 24시간 안정적인 서비스 제공 가능

한 줄 요약: 단독 플레이 → 1:1세션(3명 이상, 호스트 겸업) → 데디케이티드(전용 서버)

🔍 피드백: 만점입니다. 오답노트 재출제를 통과하셨습니다.

어제 놓치셨던 두 칸이 정확히 채워졌습니다.

빈칸 어제 오늘
“P2P” (부분 점수) 1:1
비워 둠 세션

특히 의미 있는 건 문제의 틀이 세 번 바뀌었는데도 흔들리지 않았다는 점입니다. 처음엔 위에서 아래로 흐르는 4단계 도식, 두 번째는 세 방식을 나란히 놓은 비교표, 이번엔 한 회사의 1998~2010년 연표였습니다. 형태가 매번 달라도 답이 나왔다는 건 답의 위치를 외운 게 아니라 개념이 잡혔다는 뜻입니다.

⑤ “형평성”은 이번에도 정답 처리했습니다. 원문 표현은 “공정성”이지만 같은 것을 가리키고, 호스트가 유리해지는 상황을 정확히 짚고 계십니다.

이 문항은 stage 1로 올라갔습니다. 오답노트는 연속 두 번 만점이어야 사라지므로, 다음에 한 번 더 나올 때 만점을 받으면 목록에서 제거됩니다. 반대로 그때 하나라도 놓치면 stage 0으로 리셋되니, 위의 한 줄 요약만 붙들고 계시면 됩니다.

출처: server/game_server/4/2026-02-08-game_server_4_1.md 오답노트: wn-2cb31e69 · 2026-09-09 최초 오답 · 이번이 2번째 복습


Q4. 🟡 보통 — ❌ 0/10

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

블로킹이란 디바이스에 처리 요청을 걸고 응답을 대기하는 함수를 호출할 때, 스레드에서 발생하는 대기 현상이다. 예를 들어 ReadFile()을 호출하면 디스크 처리가 완전히 끝날 때까지 애플리케이션은 대기 상태로 남는다.

이때 그 스레드는 waitable state가 되며, CPU 사용량이 0% 가 된다.

그런데 CPU 사용량이 0%라는 것은 그 스레드가 CPU를 조금도 쓰지 않는다는 뜻이므로, 블로킹은 성능 면에서 손해가 없다. 오히려 바쁜 대기(busy wait)와 달리 자원을 아끼는 방식이다. 신경 쓸 부분은 코드 가독성 정도이며, 네트워크 수신에서도 상대가 데이터를 보내지 않으면 함수가 적절히 타임아웃되어 리턴하므로 문제되지 않는다.

📝 내 답: O — Ready, Wait, 등이 있었던 것 같은데 정확히 기억이 안난다.

정답: X

해설:

앞의 두 문단은 참이다.

  • 블로킹의 정의 — 디바이스에 처리 요청을 걸고 응답을 대기하는 함수를 호출할 때 스레드에서 발생하는 대기 현상
  • ReadFile()을 호출하면 디스크 처리가 완전히 종료될 때까지 애플리케이션은 대기 상태로 남는다 ✅
  • 그 스레드는 waitable state가 되고 CPU 사용량이 0% 가 된다 ✅

틀린 곳은 마지막 문단이며, 두 군데가 어긋나 있다.

① “CPU 0%라서 손해가 없다”가 거짓 — 놀고 있는 것은 CPU가 아니라 스레드다

CPU 사용량이 0%라는 사실은 좋은 소식이 아니라 나쁜 소식이다. 그 스레드가 아무 일도 하지 못한 채 붙잡혀 있다는 뜻이기 때문이다.

바쁜 대기와 비교해 보면 차이가 분명해진다.

  하는 일 CPU 그 스레드는
바쁜 대기 조건이 참이 될 때까지 루프를 돈다 100% 묶여 있다
블로킹 커널이 깨워 줄 때까지 잠든다 0% 똑같이 묶여 있다

CPU를 태우지 않는다는 점에서 블로킹이 바쁜 대기보다 나은 것은 맞다. 하지만 “그 스레드가 그동안 다른 일을 못 한다”는 손해는 양쪽이 똑같다. 스레드는 공짜가 아니다 — 호출 스택 메모리를 차지하고, 개수가 늘면 컨텍스트 스위치 비용도 늘어난다. 디스크 응답을 기다리는 동안 CPU는 놀고 프로그램은 정지 상태처럼 느려지는 것이 블로킹의 실제 비용이다.

바로 이 손해 때문에 논블록 소켓Overlapped I/O 같은 방식이 존재한다. 기다리는 동안 그 스레드를 풀어 다른 일을 시키려는 것이다.

② “타임아웃되어 리턴한다”가 거짓 — 영원히 블로킹될 수 있다

원문이 명시하는 문제 상황은 정확히 그 반대다.

  1. 스레드에서 네트워크 수신을 하는 함수를 호출했으며
  2. 상대 컴퓨터에서 데이터를 보내지 않으면

영원히 블로킹이 발생한다.

디스크 I/O는 언젠가 끝나지만, 네트워크 수신은 상대가 보내 주지 않으면 끝날 이유가 없다. 기본 블로킹 소켓은 알아서 타임아웃되지 않으므로, 그 스레드는 프로그램이 끝날 때까지 그대로 묶여 있을 수 있다.

🔍 피드백: 판단·근거 모두 0점입니다. OX 다섯 회차 연속 정답 뒤의 첫 오답이고, 이 한 문항 때문에 총점이 16점 내려갔습니다(맞혔다면 88점이었습니다).

이번 함정은 이전과 결이 달랐습니다. 지난 회차들은 거짓인 문장을 심어 두는 방식이었습니다 — “수직 분산에서 남은 서버가 기능을 나눠 맡는다”처럼, 사실이 아닌 진술을 찾아내면 됐죠. 그래서 “마지막 문장을 따로 검사하라”는 요령이 통했습니다.

이번엔 거짓 진술이 없습니다. 대신 참인 사실에서 잘못된 결론으로 미끄러집니다.

CPU 사용량이 0%다                      ← 참 ✅
    ↓
그 스레드가 CPU를 쓰지 않는다            ← 참 ✅
    ↓
그러므로 손해가 없다                    ← ❌ 여기서 미끄러진다

미끄러지는 지점은 “누가 놀고 있는가” 입니다. CPU는 다른 스레드를 돌리면 그만이지만, 그 스레드 자신은 아무것도 못 한 채 묶여 있습니다. 바쁜 대기와 나란히 놓으면 분명해집니다.

  CPU 그 스레드는
바쁜 대기 100% 묶여 있다
블로킹 0% 똑같이 묶여 있다

CPU를 안 태운다는 점만 다르고, “그동안 다른 일을 못 한다”는 손해는 같습니다. 이 손해가 없다면 논블록 소켓Overlapped I/O도 존재할 이유가 없습니다. 그 방식들이 하는 일이 정확히 기다리는 스레드를 풀어 주는 것이니까요.

두 번째 거짓은 더 간단합니다. 명제는 “네트워크 수신도 적절히 타임아웃되어 리턴한다”고 했지만, 원문이 명시하는 문제 상황은 정반대입니다.

네트워크 수신 함수를 호출했는데 상대 컴퓨터가 데이터를 보내지 않으면 → 영원히 블로킹된다.

디스크는 언젠가 응답하지만, 네트워크는 상대가 보내 주지 않으면 끝날 이유가 없습니다. 이 “영원히”가 블로킹 소켓 단원의 핵심이라 꼭 기억해 두세요.

앞으로의 요령을 하나 갱신하시면 좋겠습니다. “마지막 문장 검사”에 더해, “참인 사실 뒤에 ‘그러므로’가 붙으면 그 연결을 의심하라” 를 추가하세요. 사실이 참이어도 결론이 따라 나오지 않을 수 있습니다.

출처: server/game_server/3/2025-12-16-game_server_3_1.md


Q5. 🟡 보통 — 🔺 6.6/10

문제: 멀티스레드 프로그래밍이 필요한 상황에 대해 답하시오.

(1) 멀티스레딩이 필요한 세 가지 상황 (2) 게임 로딩 예시 — ㉠ 싱글스레드 코드의 문제점 둘 ㉡ 두 스레드의 역할과 isStillLoading의 용도 (3) ㉠ 클록 속도와 코어 수의 추세 ㉡ 4코어에서 싱글스레드의 CPU 사용량 ㉢ 소수 구하기 코드의 문제

📝 내 풀이:

(1) 단일 작업이 아님, 입출력 필요, 모르겠다.

(2) ㄱ: 끊김 현상, 다른 작업 불가 ㄴ: Thread1은 업데이트 및 렌더, Thread2는 로딩을 맡는다. isStillLoading은 로딩중인지 판단한다. 정확히 모르겠다.

(3) ㄱ: 클럭 속도는 로그 함수의 형태로 한계에 도달했다. 그래서 코어 수를 올리는 추세다. ㄴ: 25%로 나타난다. 코어 하나밖에 쓰지 않기 때문 ㄷ: mutex를 사용하지 않아서 생긴 문제다. 가장 큰 문제는 배열의 주소는 재할당되기 때문에 생긴다.

정답:

(1) 멀티스레딩이 필요한 세 가지 상황

  1. 오래 걸리는 일 하나와, 빨리 끝나는 일 여러 개를 동시에 해야 할 때
  2. 어떤 긴 처리가 진행되는 동안 다른 짧은 작업을 처리해야 할 때
  3. 기기에 있는 모든 CPU 코어를 최대한 활용하고 싶을 때

(2)㉠ 싱글스레드 코드의 문제점

Render();  LoadScene();  Render();  LoadModel();  Render();  ...
  • 프레임이 끊긴다. 렌더링과 로딩을 번갈아 실행하므로, 리소스 하나를 로드하는 동안 화면이 멈춘다. 로드 작업의 길이가 제각각이라 프레임 간격도 들쭉날쭉해진다.
  • 로직이 복잡해진다. 로딩 단계를 하나 추가할 때마다 그 사이사이에 Render()를 끼워 넣어야 한다. 두 관심사가 한 함수 안에 뒤엉킨다.

㉡ 멀티스레드 구조

  역할
Thread1 렌더링 전담FrameMove()Render()를 계속 돌려 프레임을 부드럽게 유지한다
Thread2 리소스 로딩 전담 — Scene·Model·Texture·Animation·Sound를 순서대로 읽는다

isStillLoading은 두 스레드를 잇는 신호다. Thread2가 로딩을 모두 마치면 이 값을 false로 바꾸고, 그것을 보고 Thread1의 렌더링 루프가 종료된다. 즉 “언제까지 로딩 화면을 그릴 것인가”를 알려 주는 종료 조건이다.

역할이 분리되어 코드도 더 간결하고 효율적이 된다.

(3)㉠ 클록 속도는 멈췄고, 코어 수는 늘고 있다

  • 클록 속도는 4GHz 언저리에서 멈췄다.
  • 코어는 4개, 8개, 16개까지 계속 늘어난다.

그래서 성능을 더 얻으려면 “더 빠른 코어 하나”가 아니라 “여러 코어를 동시에 쓰는 것” 에 기대야 한다.

㉡ 약 25% (1/4)

4코어 CPU에서 싱글스레드 프로그램은 코어 하나만 쓰므로 성능의 1/4만 사용한다. 실제로 싱글스레드로 소수를 구했을 때 실행 시간 약 3920ms, CPU 사용량 전체의 25% 가 나왔다.

primes.push_back(n); 줄이 문제다

if (IsPrimeNumber(n)) {
    primes.push_back(n);      // ❗ 여러 스레드가 동시에 접근한다
}

primes모든 스레드가 공유하는 vector 인데, 아무 보호 없이 여러 스레드가 동시에 push_back을 호출하고 있다. 그래서 아래와 같은 에러가 난다.

An invalid parameter was passed to a function
that considers invalid parameters fatal.

참고: int n = num; num++; 부분도 같은 종류의 문제를 안고 있다. num++은 기계어 수준에서 읽기 → 더하기 → 쓰기 세 연산으로 쪼개지므로, 여러 스레드가 같은 값을 중복해서 가져가거나 어떤 값을 건너뛸 수 있다.

두 문제 모두 해결하려면 공유 데이터에 접근하는 순간만큼은 다른 스레드가 접근하지 못하도록 막아야 한다 — 즉 동기화(임계 영역 / 뮤텍스 / 잠금)가 필요하다.

🔍 피드백: 논점별로 (1) 1.2 / (2) 2.2 / (3) 3.2를 받아 6.6점입니다.

(3)이 거의 만점입니다. ㉠의 “클럭 속도는 한계에 도달했고 그래서 코어 수를 올리는 추세”와 ㉡의 “25%, 코어 하나밖에 쓰지 않기 때문”이 정확합니다.

그리고 ㉢에서 “배열의 주소는 재할당되기 때문”이라고 쓰신 것을 짚고 싶습니다. 이건 바로 어제 Q4(2)에서 힌트(“공간이 부족해지면”)까지 드렸는데 놓치셨던 내용입니다. 하루 만에 자기 답으로 나왔습니다. vector가 공간이 모자라 더 큰 메모리를 새로 잡으면 포인터가 가리키는 위치가 바뀌고, 그 사실을 모르는 다른 스레드가 예전 주소로 접근하면 이미 해제된 메모리를 건드리게 됩니다. 정확합니다.

한 가지만 더하면 만점이었습니다 — 문제가 “어느 줄이 문제인가” 를 물었으니 primes.push_back(n); 이라고 콕 집어 주시면 됩니다.

(1)이 가장 약합니다. 세 가지를 원문 표현으로 정리해 두세요.

  1. 오래 걸리는 일 하나 + 빨리 끝나는 일 여러 개를 동시에 해야 할 때 → 로딩 화면(로딩은 길고, 렌더링은 짧고 잦다)
  2. 긴 처리가 진행되는 동안 다른 짧은 작업을 처리해야 할 때 → 디스크 I/O 대기 중 다른 일
  3. 기기의 모든 CPU 코어를 최대한 활용하고 싶을 때 → 소수 구하기

쓰신 “입출력 필요”는 2번에 해당해 부분 점수를 드렸습니다. 그런데 3번을 “모르겠다”고 하신 게 아까운데, (3)에서 그 내용을 전부 정확히 답하셨습니다. 4코어에서 25%밖에 못 쓴다는 이야기가 바로 3번입니다. 같은 글 안에서 앞뒤가 이어져 있으니, (3)의 주제가 곧 (1)의 세 번째 항목이라는 걸 연결해 두시면 됩니다.

(2)㉠에서 “끊김 현상”은 정확합니다. 두 번째로 쓰신 “다른 작업 불가”는 원문이 드는 문제와 다릅니다. 정답은 “로직이 복잡해진다” 입니다 — 로딩 단계를 하나 추가할 때마다 그 사이에 Render()를 손으로 끼워 넣어야 하니, 코드가 두 관심사로 뒤엉킵니다. 멀티스레드가 주는 이득이 성능만이 아니라 코드가 간결해지는 것이기도 하다는 점이 이 예시의 요지입니다.

㉡의 두 스레드 역할은 정확합니다. isStillLoading을 “로딩중인지 판단한다”고 하셨는데 맞는 말이고, 한 걸음만 더 가면 됩니다 — 누가 그 값을 보고 무엇을 하는가입니다.

Thread1 { while (isStillLoading) { FrameMove(); Render(); } }   // ← 이 값을 보고 루프를 돈다
Thread2 { ... 로딩 ...; isStillLoading = false; }                // ← 이 값을 바꿔 알린다

Thread2가 “다 됐다”고 알리는 신호이자, Thread1의 렌더링 루프를 끝내는 종료 조건입니다. 두 스레드를 잇는 유일한 연결선이라 이 변수 하나가 구조 전체를 설명합니다.

출처: server/game_server/1/2025-05-28-game_server_1_3.md


Q6. 🟡 보통 — 🔺 8.0/10

문제: 빈칸을 채우시오.

분산 처리는 처리량을 늘리는 ① ___ 만이 아니라 ② ___ 에도 효과를 준다.

방식 나누는 기준 한 대가 중지되면
___ 분산 같은 일을 하는 서버를 여러 대 둔다 그 서버에 있던 플레이어들만 접속이 끊어진다
___ 분산 기능별로 서로 다른 서버에 맡긴다 중지된 서버가 처리하던 그 기능 외의 다른 기능은 정상 작동한다

📝 내 답: 서버 업그레이드, 안정성, 수평, 수직

정답:

① 확장성 ② 안정성 ③ 수평 ④ 수직

해설:

분산 처리는 확장성뿐만 아니라 안정성에도 효과를 준다. 서버가 한 대뿐이라면 그 한 대가 멈추는 순간 서비스 전체가 멈추지만, 나눠 두면 피해가 일부로 제한되기 때문이다.

다만 “일부로 제한된다”의 의미가 방식마다 다르다.

③ 수평 분산 — 같은 일을 하는 서버를 여러 대

중지된 서버에 있는 플레이어들만 접속이 끊어지며, 다른 서버에 접속하면 된다.

서버들이 같은 일을 하므로 서로 대체가 가능하다. 끊긴 플레이어는 다른 서버로 옮겨 가면 그만이고, 나머지 플레이어는 아무 영향도 받지 않는다.

④ 수직 분산 — 기능별로 서로 다른 서버

중지된 서버가 처리하는 기능 외 다른 기능은 정상 작동한다.

이쪽은 대체가 불가능하다. 각 서버가 서로 다른 일을 하므로, 채팅 서버가 죽으면 채팅은 안 된다. 남은 서버들이 채팅을 나눠 맡아 주지 않는다. 대신 전투·거래 같은 다른 기능은 멀쩡하므로 서비스 전체가 죽지는 않는다.

  나누는 기준 한 대가 죽으면 대체 가능한가
수평 분산 같은 일 × 여러 대 그 서버의 플레이어만 끊긴다 가능
수직 분산 기능별로 다른 서버 기능만 멈춘다 불가능

혼동하기 쉬운 지점은 “남은 서버가 대신 맡아 준다”가 수평 분산에만 해당한다는 것이다.

🔍 피드백: ②③④를 정답 처리하고 ①에 부분 점수를 주어 8.0점입니다.

②③④가 정확합니다. 특히 ③④의 수평/수직 구분은 이 개념이 등장한 네 번째 회차인데, 이번엔 표의 설명만 읽고 바로 채우셨습니다. 자리를 잡았다고 봅니다.

① “서버 업그레이드”는 방향이 어긋났습니다. 정답은 확장성입니다. 두 표현의 차이가 이 단원의 핵심과 맞닿아 있어 짚어 두겠습니다.

  무엇을 하는가 부르는 이름
서버 업그레이드 서버 한 대를 더 좋게 만든다 (CPU·RAM 증설) 수직 확장 / scale-up
확장성(분산) 서버를 여러 대로 늘린다 수평 확장 / scale-out

즉 “서버 업그레이드”는 분산 처리가 하려는 일의 반대편에 있습니다. 분산은 한 대를 강화하는 대신 여러 대에 일을 나누는 방식이고, 그렇게 얻는 성질이 확장성입니다.

문장을 다시 읽어 보시면 그 자리가 보입니다.

“흔히 처리량을 늘리는 ① 을 떠올리지만, 그것만이 아니다. … 분산은 에도 효과를 준다.”

①과 ②가 나란히 놓인 한 쌍입니다. ②에 “안정성”을 정확히 쓰셨으니, 그 짝이 되는 말이 무엇일지 되짚으면 확장성이 나옵니다. 이 단원 전체가 “분산 = 확장성 + 안정성” 이라는 한 문장으로 요약되니 둘을 붙여서 외워 두세요.

출처: server/game_server/9/2026-03-22-game_server_9_11.md


📌 복습 포인트

  • Q4 블로킹의 진짜 비용 (이번 회차 최우선) — CPU 0%는 좋은 소식이 아니다. 놀고 있는 것은 CPU가 아니라 그 스레드이며, “다른 일을 못 한다”는 손해는 바쁜 대기와 같다. 그리고 네트워크 수신은 상대가 안 보내면 영원히 블로킹된다(타임아웃 없음) → server/game_server/3/2025-12-16-game_server_3_1.md
  • OX 푸는 요령 갱신 — “마지막 문장 검사”에 더해, 참인 사실 뒤에 ‘그러므로’가 붙으면 그 연결을 의심할 것. 사실이 참이어도 결론이 따라 나오지 않을 수 있다
  • Q1 두 가지 상수성비트수준(물리적) = 데이터 멤버를 하나도 안 건드려야 인정 / 논리적 = 사용자가 못 알아채면 일부 수정 허용. mutable에러가 난 줄에 등장하는 멤버에만 붙인다 → game_dev/cpp/chapter1/2025-04-06-cpp_1_3.md
  • Q5 멀티스레딩이 필요한 세 상황 — ① 긴 일 + 짧은 일 여러 개 ② 긴 처리 중 짧은 작업 ③ 모든 코어 활용(= (3)에서 답한 25% 이야기). 싱글스레드 로딩의 두 문제는 프레임 끊김 + 로직 복잡화server/game_server/1/2025-05-28-game_server_1_3.md
  • Q6 확장성 vs 서버 업그레이드 — 분산은 한 대를 강화(scale-up)하는 게 아니라 여러 대로 나누는 것(scale-out)이다. 한 문장 요약: 분산 = 확장성 + 안정성server/game_server/9/2026-03-22-game_server_9_11.md

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

댓글남기기