[말랑 퀴즈] 26/08/30 해답

게시:     수정

카테고리:

태그:

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

말랑말랑 퀴즈 — 해답지 ✅

날짜: 2026-08-30 문제 수: 5문제 총점: 34.5/50 (69점) 🟡 복습이 필요해요

📊 채점 결과

문항 유형 난이도 결과 점수
Q1 객관식 🔴 어려움 ⭕ 정답 10 / 10
Q2 서술형 🔴 어려움 🔺 부분 정답 4.6 / 10
Q3 빈칸 🟢 쉬움 🔺 부분 정답 6.1 / 10
Q4 OX 🟡 보통 🔺 부분 정답 9.0 / 10
Q5 서술형 🟡 보통 🔺 부분 정답 4.8 / 10
합계       34.5 / 50

총평

69점 — “잘했어요”(70점)에 1점 모자랍니다. 아깝게 들리겠지만 뒤집어 보면, 어디 하나만 채웠어도 넘었다는 뜻입니다. Q3의 ③(STL) 한 칸이나 Q5(3)의 “3가지 → 2가지” 하나만 맞았어도 70점을 넘습니다.

객관식과 OX는 이제 안정적입니다. Q1은 어려움 난이도인데 정답을 골랐을 뿐 아니라, A·C·D를 각각 왜 배제했는지 근거를 적어 두셨습니다 — 특히 D를 “TMP에서 if문과 반복문을 사용하지 못했던 것으로 기억난다”로 쳐낸 건 정확합니다. Q4도 X를 맞혔습니다. 이걸로 최근 세 회차의 OX가 X → O → X였는데 전부 내용으로 판별하셨습니다. 패턴이 아니라 문장을 읽고 계십니다.

약한 곳은 여전히 서술형 두 문항(4.6, 4.8)입니다. 그런데 안을 뜯어보면 “몰라서 못 쓴 것”보다 “묻는 것과 다른 걸 답한 것” 이 더 큽니다. Q2(3)은 힌트를 “병목 지점”으로 읽고 “로비 서버를 분산해야 한다”고 답하셨는데, 문제는 이미 로비 서버를 분산한 뒤 그 서버들 사이 통신량을 줄이는 방법을 묻고 있었습니다. Q5(2)도 “외부 코드 입장에서 두 구현의 차이는?”이라는 질문에 A와 B의 성능 트레이드오프를 답하셨습니다. 정답은 “차이가 없다” 였습니다.

반대로 Q2(2)의 “지연 시간동안 방이 생성되고, 이동했기 때문”은 정답의 핵심을 정확히 짚은 문장입니다. 아는 건 정확히 쓰고 계시니, 답을 쓰기 전에 질문의 마지막 어절을 한 번 다시 읽는 습관만 붙이면 이 두 문항의 점수는 크게 달라집니다.


Q1. 🔴 어려움 — ⭕ 10/10

문제: 반복자에는 여러 종류가 있고, 그중 임의 접근 반복자(vector, deque 등)만 it += n 연산을 지원한다. list의 반복자는 양방향 반복자라 ++ / --만 되고 +=는 지원하지 않는다.

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
    if (typeid(typename std::iterator_traits<IterT>::iterator_category)
        == typeid(std::random_access_iterator_tag)) {
        iter += d;                                  // 임의 접근 반복자용 빠른 경로
    } else {
        if (d >= 0) { while (d--) ++iter; }         // 그 외 반복자용
        else        { while (d++) --iter; }
    }
}
std::list<int>::iterator iter;   // list는 양방향 반복자
advance(iter, 10);               // ← 컴파일 에러

이 상황에 대한 설명으로 가장 올바른 것은?

  • A. list의 반복자는 임의 접근이 아니므로 typeid 비교가 실패해 런타임에 else 분기로 간다. iter += d는 실행되지 않으므로 사실 이 코드는 문제없이 컴파일되며, 에러가 났다면 헤더를 빠뜨린 것이다.
  • B. if의 조건이 런타임에 판정되더라도, 컴파일러는 인스턴스화 시점에 두 분기의 코드를 모두 생성해야 한다. list의 반복자는 +=를 지원하지 않으므로 실행되지도 않을 iter += d 때문에 컴파일 에러가 난다. 이를 피하려면 typeid 대신 특성정보(traits)를 이용한 함수 오버로딩으로 타입 판정을 컴파일 타임으로 옮겨야 한다.
  • C. typeid 방식의 문제는 타입 점검 코드가 실행 파일에 들어가 크기가 커지는 것뿐이고, 컴파일 자체는 정상이다. -O2 같은 최적화 옵션을 켜면 도달 불가능한 분기가 제거되므로 해결된다.
  • D. 템플릿 메타프로그래밍(TMP)은 일반적인 if문과 반복(iteration) 루프를 그대로 쓸 수 있으므로, 위 코드를 TMP로 옮기려면 ifwhile 재시도 루프로 바꾸기만 하면 된다.

📝 내 선택: B — 특성 정보를 이용해서 순방향/양방향 등을 분기했던 것은 기억나지만 정확히 기억나진 않는다. / A: 문제없이 컴파일된다는 건 틀렸다. / C: typeid는 타입 점검 코드가 실행 파일에 들어가 문제가 됐던 것 같다. / D: TMP에서 if문과 반복문을 사용하지 못했던 것으로 기억난다.

정답: B

해설:

advance(iter, 10)을 호출하면 템플릿이 아래처럼 인스턴스화된다.

void advance(std::list<int>::iterator& iter, int d) {
    if (typeid(std::iterator_traits<std::list<int>::iterator>::iterator_category)
        == typeid(std::random_access_iterator_tag)) {
        iter += d;                       // ❌ 컴파일 에러!
    } else {
        ...
    }
}

핵심은 if는 런타임에 갈리지만, 코드 생성은 컴파일 타임에 끝난다는 점이다. 컴파일러는 실행 여부와 무관하게 두 분기 모두 문법적으로 유효한 코드여야 인스턴스화를 마칠 수 있다. list의 반복자는 양방향이라 += 연산자가 없으므로, 절대 실행되지 않을 줄 하나가 컴파일 전체를 막는다.

typeid 방식이 나쁜 이유는 이것 말고도 있다.

  • 타입 점검이 컴파일 타임이 아니라 런타임에 일어난다.
  • 그 런타임 타입 점검 코드가 실행 파일에 들어갈 수밖에 없다.

해결책은 판정을 컴파일 타임으로 옮기는 것이다. iterator_traits가 내주는 태그 타입을 인자로 받는 오버로딩된 함수들을 만들어 두면, 컴파일러가 오버로딩 해결로 알맞은 함수 하나만 고르고 나머지는 아예 인스턴스화하지 않는다. 이것이 이미 우리가 쓰고 있던 TMP다 — 템플릿 메타프로그래밍은 기존 작업을 런타임에서 컴파일 타임으로 옮기는 기법이고, 그래서 선행 에러 탐지(early error detection)높은 런타임 효율을 함께 얻는다.

  • A — 정반대다. 런타임에 그 분기로 가지 않는다는 사실은 컴파일러의 코드 생성과 아무 상관이 없다. 헤더 문제도 아니다.
  • C — 최적화 옵션은 컴파일이 성공한 뒤에 적용되는 단계다. 애초에 += 연산자가 없어 타입 검사에서 걸리는 코드는 최적화가 손댈 기회조차 없다. 또한 typeid의 문제가 “코드 크기뿐”이라는 것도 사실이 아니다.
  • D — TMP의 성격을 반대로 말하고 있다. TMP에는 반복(iteration) 의미의 루프가 없고 재귀(재귀식 템플릿 인스턴스화)로 루프 효과를 낸다. 분기도 일반 if가 아니라 템플릿 및 템플릿 특수화로 표현한다. 아래 팩토리얼이 그 전형이다.
template<unsigned n>
struct Factorial {                       // 재귀식 템플릿 인스턴스화 = 루프
    enum { value = n * Factorial<n-1>::value };
};

template<>
struct Factorial<0> {                    // 템플릿 특수화 = 분기(종료 조건)
    enum { value = 1 };
};

std::cout << Factorial<5>::value;        // 120 — 컴파일 시간에 이미 계산됨

덧붙임: TMP는 튜링 완전성을 가진다. 실용적인 활용으로는 ① 치수 단위 정확성 검증(속도 변수에 질량을 대입하면 컴파일 에러), ② 표현식 템플릿으로 행렬 연쇄 곱의 임시 객체 제거, ③ 정책 기반 설계로 디자인 패턴 구현을 조립하는 것 등이 있다.

🔍 피드백: 만점입니다. 어려움 난이도의 객관식에서 네 보기를 하나씩 판정하며 좁혀 들어간 방식이 좋았습니다.

특히 D를 쳐낸 근거가 정확합니다. “TMP에서 if문과 반복문을 사용하지 못했던 것으로 기억난다” — 맞습니다. TMP에는 반복 루프가 없어 재귀식 템플릿 인스턴스화로 루프를 흉내 내고, 분기는 if가 아니라 템플릿 특수화로 표현합니다. C를 쳐낸 근거도 절반은 정확합니다 — typeid는 실제로 타입 점검 코드가 실행 파일에 들어가는 게 문제입니다. 다만 C가 오답인 진짜 이유는 그 뒤 문장, “컴파일 자체는 정상이고 -O2면 해결된다” 는 부분입니다. 최적화는 컴파일이 성공한 뒤에 도는 단계라, 타입 검사에서 걸린 코드에는 손댈 기회조차 없습니다.

“특성 정보로 분기했던 건 기억나는데 정확히는 모르겠다”고 하셨는데, 그 기억이 정확합니다. 다만 한 가지만 분명히 해 두면 좋습니다 — traits로 바꾸는 것이 단순한 스타일 개선이 아니라 문제를 없애는 유일한 방법이라는 점입니다. if로 갈라 두면 컴파일러가 양쪽 분기를 모두 만들어야 하지만, 오버로딩된 함수로 갈라 두면 알맞은 함수 하나만 인스턴스화하고 나머지는 존재조차 하지 않게 됩니다. “실행 안 되는 코드”와 “생성 안 되는 코드”의 차이입니다.

출처: game_dev/cpp/chapter7/2025-11-03-cpp_7_48.md


Q2. 🔴 어려움 — 🔺 4.6/10

문제: 로비 서버를 여러 대로 분산했을 때의 통신 방식 선택과, 그로 인해 발생한 사고에 대해 답하시오.

(1) 로비 서버들이 서로 알아야 하는 데이터는? 세 방식 중 데이터 복제 기반 로컬 처리가 선택된 이유와, 나머지 둘이 탈락한 이유는? (2) ⑩ 시점에 로비 서버1은 C1의 취소를 이미 알고 있는데도 왜 되돌릴 수 없는가? 결국 어떻게 처리하는가? (3) 로비 서버끼리의 통신량 폭발을 줄이는 방법은?

📝 내 풀이:

(1) 클라이언트들의 매칭 대기 여부 동기 분산 처리: 클라이언트가 많아지면 통신량을 감당하기 힘들어지기에 탈락 비동기 분산 처리: 서버 사이의 일관성이 위배되기에 탈락 그러므로 데이터 복제 기반 로컬 처리를 선택한다.

(2) 무엇이라 하는가?: 기억나지 않는다. 되돌릴 수 없는 이유: 지연 시간동안 방이 생성되고, 이동했기 때문. 처리 방법: 매칭 전으로 롤백시키고, 기존 인원을 매칭 대기로 만들어 준다.

(3) 힌트에 있는 분산 여부를 판단하는 기준은 병목이 일어나는 지점을 말하는 것일 것이다. 병목이 일어나는 지점은 로비 서버기에, 로비 서버를 분산시켜야 한다. 이 방법에는 지역별 분산(미국 서버, 아시아 서버)가 대표적이다.

정답:

(1) 주고받아야 하는 데이터와 방식 선택

로비 서버들이 서로 알아야 하는 데이터는 두 가지다.

  • 플레이어의 실력 정보
  • 매칭 대기 중 여부
방법 결과 이유
동기 분산 처리 통신 횟수가 너무 많아진다. 매칭 후보를 한 명 검토할 때마다 다른 서버에 물어보고 응답을 기다려야 한다
비동기 분산 처리 응답을 받지 못한다. 매칭은 “이 사람 지금 대기 중인가?”에 대한 답이 있어야 성립하는데, 던져 놓고 기다리지 않으면 판단할 근거가 없다
데이터 복제 기반 로컬 처리 필요한 데이터를 서버 간 통신으로 미리 동기화해 두고, 매칭 판단은 자기 메모리에서 한다. 매칭 시도마다 원격 통신이 발생하지 않는다

(2) 스테일 데이터(데이터 불일치) 문제

로비 서버마다 복제본을 들고 판단하기 때문에, 원본이 바뀐 뒤 동기화가 도착하기 전까지는 낡은 데이터(stale data) 로 판단하게 된다. 대표적인 사례가 매칭 취소 직전에 매칭이 성사되는 경우다.

⑩ 시점에 로비 서버1이 취소 사실을 이미 알고 있는데도 되돌릴 수 없는 이유는, 되돌려야 할 대상이 로비 서버1이 아니기 때문이다.

  • 로비 서버2에게 “원상 복구하라”고 알리는 방법을 떠올릴 수 있다.
  • 그러나 그 시점에 클라이언트 2~10은 이미 배틀 서버로 이동 중이거나 이미 접속했다. 게임방은 만들어졌고 아홉 명이 그 안에 들어가 있다.
  • 즉 취소를 존중하려면 이미 진행된 아홉 명의 이동을 전부 취소해야 하는데, 이건 되돌리기가 더 비싸고 그들에게도 손해다.

그래서 결론은 자동으로 매치메이킹을 재시작하는 수밖에 없다. 취소한 C1은 빼고, 나머지에게 매칭을 다시 돌려 주는 것이다.

다른 접근: 메모리 저장 서버를 따로 두고 모든 로비 서버가 그곳에 접근하게 하면 복제본 자체가 사라져 불일치도 없다. 다만 데이터에 접근할 때마다 기기 간 통신이 발생하므로 비효율적일 수 있고, 로비 서버의 연산량이 많고 메모리 저장소 서버의 연산량이 적을 때 효과적이다.

(3) 통신량 폭발을 줄이는 방법 — 응집도가 낮은 데이터는 동기화하지 않는다

분산 여부를 판단할 때 쓰는 기준인 데이터 응집력을 여기에도 그대로 적용한다.

  • 응집도가 높은 플레이어들끼리만, 즉 같은 실력대의 플레이어끼리만 매칭시킨다.
  • 응집도가 낮은 플레이어의 데이터는 아예 동기화하지 않는다.

어차피 실력 차가 큰 두 플레이어는 서로 매칭될 일이 없다. 그러니 그들의 정보를 서로에게 보낼 이유도 없다. 동기화 대상을 “매칭 가능성이 있는 범위”로 좁히는 것만으로 통신량이 크게 줄어든다.

참고 — 배틀 서버 분산이 먼저였던 이유: 서로 다른 게임방이나 다른 리전은 응집력이 낮으므로 나누기 좋은 지점이다. 게임방 처리를 배틀 서버로 떼면 플레이 중 레이턴시도 줄일 수 있다. 반면 로비 서버 클러스터는 한 리전에 두어도 된다. 그리고 클라이언트가 배틀 서버에 접속할 때는 로비 서버가 발급한 인증 정보(Credential) 를 사용한다.

🔍 피드백: 논점별로 (1) 2.0 / (2) 2.0 / (3) 0.6을 받아 4.6점입니다.

(1)동기 분산 처리의 탈락 이유는 정확합니다. “통신량을 감당하기 힘들다”가 맞습니다. 매칭 후보를 한 명 검토할 때마다 다른 서버에 물어보고 응답까지 기다려야 하니까요.

두 군데가 아쉽습니다. 첫째, 주고받아야 할 데이터를 “매칭 대기 여부” 하나만 쓰셨는데 하나가 더 있습니다 — 플레이어의 실력 정보입니다. 실력을 모르면 애초에 누구와 누구를 붙일지 정할 수 없으니 반드시 필요합니다. (그리고 이게 (3)의 답과도 이어집니다.)

둘째, 비동기의 탈락 이유가 바뀌었습니다. “서버 사이의 일관성이 위배되기에”라고 쓰셨는데, 일관성 문제는 오히려 채택된 방식인 데이터 복제 기반 로컬 처리의 단점입니다 — 그게 바로 (2)의 스테일 데이터죠. 비동기가 탈락한 이유는 더 단순합니다. 응답을 받지 못하기 때문입니다. 매칭은 “이 사람 지금 대기 중인가?”라는 질문에 답이 돌아와야 성립하는데, 비동기는 던져 놓고 기다리지 않으니 판단할 근거 자체가 없습니다.

(2)의 “되돌릴 수 없는 이유”가 이번 문항에서 가장 좋았습니다. “지연 시간동안 방이 생성되고, 이동했기 때문” — 정답의 핵심을 정확히 짚으셨습니다. 되돌려야 할 대상이 로비 서버1이 아니라, 이미 배틀 서버로 넘어간 아홉 명이라는 게 이 문항의 요지였습니다.

이름은 스테일 데이터(stale data), 즉 데이터 불일치 문제입니다. “복제본을 들고 판단하다가 원본이 바뀐 걸 아직 못 받은 상태”라고 기억하시면 됩니다.

처리 방법에서 쓰신 “매칭 전으로 롤백시키고 기존 인원을 매칭 대기로 만들어 준다”는 원문의 “자동으로 매치메이킹을 재시작한다” 와 사실상 같은 이야기라 대부분 인정했습니다. 다만 표현에 미묘한 충돌이 있습니다 — 바로 앞에서 “되돌릴 수 없다”고 하셨는데 여기서 “롤백”이라고 쓰면 읽는 사람이 헷갈립니다. 이미 만들어진 게임방과 이동한 아홉 명은 되돌리지 않고, 그 상태에서 매칭만 다시 돌려 준다는 게 정확한 서술입니다.

(3)은 힌트를 다르게 읽으셨습니다. 힌트가 가리킨 “분산 여부를 판단할 때 쓰는 기준”은 병목이 아니라 데이터 응집력입니다. 그리고 문제의 전제를 다시 보시면, 로비 서버는 이미 분산된 상태이고 묻는 것은 그 서버들 사이에 오가는 통신량을 줄이는 방법이었습니다. 그래서 “로비 서버를 분산시켜야 한다”는 답은 출발점으로 되돌아가 버립니다.

정답은 응집도가 낮은 데이터는 아예 동기화하지 않는 것입니다. 구체적으로는 같은 실력대의 플레이어끼리만 매칭시키고, 실력 차가 큰 플레이어의 정보는 서로에게 보내지 않습니다. 어차피 매칭될 일이 없는 상대의 정보를 실시간으로 주고받을 이유가 없으니까요. (1)에서 놓치신 “플레이어 실력 정보”가 바로 이 답의 열쇠였습니다.

덧붙이신 지역별 분산은 발상의 방향이 맞습니다 — 다른 리전은 응집력이 낮으니 나누기 좋은 지점이고, 실제로 배틀 서버를 그렇게 나눕니다. 다만 이 문항이 물은 건 이미 나눈 뒤의 통신량이라 점수에는 반영하지 못했습니다.

출처: server/game_server/9/2026-03-27-game_server_10_3.md


Q3. 🟢 쉬움 — 🔺 6.1/10

문제: 빈칸을 채우시오.

자원을 확보하자마자 객체에 집어넣고, 그 객체가 소멸될 때 자원이 풀리게 하는 방식을 ① ___ 라고 한다.

  • auto_ptr은 복사하면 원본 쪽을 ② ___ 로 만들어 버린다.
    • 이 때문에 내부적으로 복사를 요구하는 ③ ___ 컨테이너에는 넣을 수 없다.
  • shared_ptr은 가리키는 포인터의 개수를 세다가 0이 되면 해제하는 ④ ___ 방식이다.
    • 다만 A가 B를, B가 A를 가리키는 ⑤ ___ 상황은 해결하지 못한다.
  • 두 스마트 포인터 모두 배열에는 쓰면 안 된다. 내부적으로 delete를 호출하는데, 배열에는 ⑥ ___ 가 필요하기 때문이다.

📝 내 답: 스마트 포인터, 널 값, ?, 참조, 루프, delete []

정답:

① RAII (Resource Acquisition Is Initialization, 자원 획득 즉 초기화) ② null (널 포인터) ③ STL ④ 참조 카운팅 (reference counting) ⑤ 순환 참조 ⑥ delete[]

해설:

① RAII — 생성자 안에서 자원을 획득하고 소멸자에서 해제하는 객체를 쓰는 패턴이다. 핵심은 소멸자는 함수가 어떤 경로로 끝나든 호출된다는 점이다. return으로 빠져나가든, 루프에서 continue·goto로 튀든, 예외가 던져지든 스택 되감기 과정에서 지역 객체의 소멸자가 불린다. 그래서 사람이 delete 위치를 고민할 필요가 없어진다.

②③ auto_ptr의 유일 소유권 — 한 자원을 두 auto_ptr이 가리키면 delete가 두 번 불릴 수 있다. 이를 막기 위해 auto_ptr복사되는 순간 원본을 null로 만든다.

auto_ptr<Investment> pInv1(createInvestment());
auto_ptr<Investment> pInv2(pInv1);   // pInv1은 null, pInv2만 유효
pInv1 = pInv2;                       // pInv2는 null, pInv1만 유효

“복사했는데 원본이 비어 버리는” 이 동작 때문에, 내부적으로 원소를 복사하는 STL 컨테이너에는 auto_ptr을 넣을 수 없다.

④⑤ shared_ptr — 같은 자원을 가리키는 포인터의 수를 세다가 마지막 하나가 소멸될 때 자원을 해제한다. 복사해도 양쪽 다 유효하므로 동작이 직관적이고, 그래서 auto_ptr보다 낫다.

shared_ptr<Investment> pInv1(createInvestment());
shared_ptr<Investment> pInv2(pInv1);   // 둘 다 같은 객체를 가리킨다

가비지 컬렉션처럼 알아서 정리해 주지만 원리가 다르다. 참조 카운팅이라서 순환 참조를 해결하지 못한다 — A가 B를, B가 A를 가리키면 둘 다 카운트가 1 이상으로 남아 영영 해제되지 않는다.

⑥ 배열 — 스마트 포인터는 기본적으로 delete를 호출하는데, 배열은 delete[]로 해제해야 한다. auto_ptrshared_ptr은 이를 구분하지 못하므로 배열에 쓰면 안 된다.

auto_ptr<string> aps(new string[10]);   // ❌ delete[]가 아니라 delete가 불린다
shared_ptr<int>  spi(new int[1024]);    // ❌ 마찬가지

덧붙임: 근본 원인은 createInvestment()생 포인터(raw pointer) 를 반환한다는 데 있다. 반환 타입 자체가 사람의 실수를 유도하는 셈이다.

🔍 피드백: ②와 ⑥은 정답, ①④⑤에 부분 점수를 주어 6.1점입니다.

① “스마트 포인터” — 빈칸이 있던 문장은 “자원을 확보하자마자 객체에 집어넣고, 그 객체가 소멸될 때 자원이 풀리게 하는 이 방식을 ① 라고 부른다” 였습니다. 즉 패턴의 이름을 묻는 자리이고, 정답은 RAII(Resource Acquisition Is Initialization)입니다. 스마트 포인터는 그 패턴을 구현한 도구입니다 — 원문도 “스마트 포인터는 … RAII 객체이다”라고 쓰고 있습니다. 방향은 맞아 부분 점수를 드렸지만, 둘의 관계를 “RAII는 원칙, 스마트 포인터는 그 원칙을 지키는 물건” 으로 정리해 두시면 좋습니다.

③은 빈칸으로 두셨습니다. 정답은 STL입니다. 바로 앞 문장이 답을 알려 줍니다 — auto_ptr복사하면 원본이 null이 되므로, 내부적으로 원소를 복사하는 컨테이너에 넣으면 원소가 사라져 버립니다. vector, list 같은 STL 컨테이너가 정확히 그런 일을 합니다. ②를 맞히셨으니 ③은 그 논리적 귀결이었습니다.

④ “참조” — 절반입니다. 정답은 참조 카운팅(reference counting) 이고, “카운팅”이 핵심어입니다. 문장 자체가 “가리키는 포인터의 개수를 세다가 0이 되면 해제” 라고 정의를 그대로 풀어 놓았으니, “센다”에 해당하는 말이 들어가야 합니다.

⑤ “루프” — 순환 구조를 가리키는 말이라 개념 방향은 맞아 부분 점수를 드렸습니다. 정확한 용어는 순환 참조입니다. 여기서 중요한 건 용어보다 왜 해결이 안 되는가입니다. A가 B를, B가 A를 가리키면 서로가 서로의 카운트를 1로 유지해 주기 때문에, 바깥에서 아무도 안 쓰게 되어도 둘 다 0이 되지 않습니다. shared_ptr이 가비지 컬렉션과 다른 지점이 정확히 여기입니다.

②와 ⑥은 정확합니다. 특히 ⑥ delete[]는 배열용 해제 연산자를 정확히 쓰셨습니다.

출처: game_dev/cpp/chapter3/2025-06-09-cpp_3_13.md


Q4. 🟡 보통 — 🔺 9.0/10

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

서버 한 대가 할 일을 여러 대로 나누는 것이 분산 처리다. 분산 처리 방식에는 동기 분산 처리, 비동기 분산 처리, 데이터 동기화 기반 로컬 처리 세 가지가 있다.

어디를 분산할지 정할 때는 성능 분석을 통해 지점을 엄선해야 하고, 나누려는 데이터의 응집력도 확인해야 한다.

다만 분산 처리는 확장성이라는 이점이 워낙 크므로, 응집력이 높아 나누기 애매한 부분이라도 일단 나눠 두는 편이 안전하다. 나중에 병목이 드러난 뒤에 구조를 뜯어고치는 것보다는 처음부터 분산해 두는 쪽이 비용이 적기 때문이다.

📝 내 답: X — 일단 나눠두는 것은 비용적인 측면이 너무 크므로 좋지 않다.

정답: X

해설:

앞의 두 문단은 참이다.

  • 분산 처리 방식이 동기 분산 처리 / 비동기 분산 처리 / 데이터 동기화 기반 로컬 처리 세 가지인 것 ✅
  • 성능 분석으로 분산 지점을 엄선하고, 데이터 응집력을 확인해야 한다는 것 ✅

틀린 곳은 마지막 문단이다. 분산 처리 전략의 첫 번째 원칙은 정반대다.

분산 처리를 피할 수 있다면 피하자.

“일단 나눠 두면 안전하다”는 발상이 위험한 이유는, 분산 자체가 공짜가 아니기 때문이다. 응집력이 높은 데이터를 억지로 나누면 서버 간 통신이 폭증하고, 앞선 로비 서버 사례처럼 복제본 사이의 데이터 불일치(스테일 데이터) 같은 새로운 문제가 따라온다. 이런 문제들은 단일 서버였다면 존재하지도 않았을 것들이다.

그래서 순서는 이렇다.

  1. 피할 수 있으면 피한다.
  2. 피할 수 없다면 성능 분석으로 진짜 병목인 지점을 엄선한다.
  3. 그 지점의 데이터 응집력을 확인한다 — 응집력이 낮은 곳(다른 게임방, 다른 리전 등)이 나누기 좋은 지점이다.
  4. 그제서야 세 가지 방식 중 하나를 고른다.

“나중에 고치기 어려우니 미리 나눠 둔다”가 아니라, “정말 필요한 곳만 골라 나눈다” 가 이 항목의 결론이다.

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

최근 세 회차의 OX가 X → O → X였는데 전부 맞히셨습니다. 답이 번갈아 나오도록 냈는데도 다 맞혔다는 건, 패턴을 외운 게 아니라 매번 문장을 실제로 검사하고 계시다는 뜻입니다. 이건 확실히 자리 잡았습니다.

근거로 쓰신 “일단 나눠두는 것은 비용적인 측면이 너무 크므로 좋지 않다”는 방향이 정확합니다. 1점만 더 채우려면 원문의 원칙 문장 자체를 인용하시면 됩니다.

분산 처리를 피할 수 있다면 피하자.

그리고 “비용”이 구체적으로 무엇인지 한 가지만 예로 들면 완결됩니다. 응집력이 높은 데이터를 억지로 나누면 서버 간 통신이 폭증하고, 바로 앞 Q2에서 본 스테일 데이터 같은 문제가 새로 생깁니다. 단일 서버였다면 아예 존재하지도 않았을 문제들이죠. 이번 회차에서 Q2와 Q4가 사실 같은 이야기의 앞뒤였다는 걸 알아 두시면 좋습니다 — Q2는 “분산했더니 생긴 문제”, Q4는 “그러니 함부로 분산하지 마라”입니다.

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


Q5. 🟡 보통 — 🔺 4.8/10

문제: C++에서 데이터 멤버를 어느 접근 영역에 둘 것인지에 대한 문제다.

(1) 데이터 멤버를 private에 두어야 하는 이유 세 가지를 쓰고 한 줄씩 설명하시오. (2) SpeedDataCollection::averageSoFar()의 두 구현(미리 갱신 / 호출 시 계산)이 외부 코드 입장에서 갖는 차이는? 그것이 주는 설계상 이득은? (3) “protectedpublic보다 안전하다”는 주장이 왜 틀렸는가? 캡슐화 관점에서 접근 지정자는 실질적으로 몇 가지인가?

📝 내 풀이:

(1) 캡슐화: 외부에서 값을 직접 건들지 못하도록 한다. 나머지는 기억나지 않는다.

(2) 평균을 자주 구해야 하는 상황에선 A가 유리하고, 그렇지 않다면 B가 유리하다. 내부 값을 직접 건드리지 않기에 캡슐화 측면에서 이득을 본다.

(3) 파생 클래스에서 직접 건드릴 수 있게 되기 때문이다. 캡슐화 관점에서 접근 지정자를 3가지로 나눌 수 있다.

정답:

(1) private에 두어야 하는 세 가지 이유

  1. 문법적 일관성 — 데이터 멤버가 전부 private에 있으면, 클래스에 대한 모든 접근이 반드시 public 멤버 함수를 통해 이루어진다. 사용자는 “이건 직접 접근해야 하나, 함수를 불러야 하나?”를 고민할 필요가 없다. 괄호를 붙일지 말지 헷갈릴 일이 사라진다.

  2. 세밀한 접근 제어public에 두면 누구나 읽기와 쓰기를 모두 할 수 있어 제어의 여지가 없다. private에 두고 함수로 감싸면 멤버마다 권한을 따로 줄 수 있다.

class AccessLevels {
public:
    int  getReadOnly()  const      { return readOnly; }
    void setWriteOnly(int value)   { writeOnly = value; }
    void setReadWrite(int value)   { readWrite = value; }
    int  getReadWrite() const      { return readWrite; }

private:
    int noAccess;    // 접근 불가
    int readOnly;    // 읽기 전용
    int writeOnly;   // 쓰기 전용
    int readWrite;   // 읽기/쓰기
};
  1. 캡슐화 — 내부 구현을 숨겨 두면 나중에 구현을 바꿔도 외부 코드가 영향을 받지 않는다. 클래스의 불변속성(invariant) 도 함수 안에서 지킬 수 있다.

(2) 두 구현의 차이 — 외부에서는 차이가 없다

외부 코드는 averageSoFar()를 호출해 double을 받을 뿐이므로, 구현 A(미리 갱신)든 구현 B(호출 시 계산)든 사용법도 결과도 똑같다.

여기서 얻는 이득이 바로 캡슐화다. 두 방식은 각각 다른 상황에 유리하다.

  • A(미리 갱신)averageSoFar 호출이 잦으면 유리하다. 다만 평균을 담을 멤버가 필요해 객체가 커진다.
  • B(호출 시 계산) — 객체는 작지만 호출할 때마다 계산 비용이 든다.

public 인터페이스가 그대로인 한, 나중에 A에서 B로 갈아타도 이 클래스를 쓰는 코드는 한 줄도 고칠 필요가 없다. 만약 데이터 멤버를 public에 노출해 두었다면, 외부 코드가 그 멤버를 직접 참조하고 있을 것이므로 구현을 바꾸는 순간 그 코드들이 전부 깨진다. 즉 캡슐화란 “감춘다”가 아니라 “나중에 바꿀 자유를 확보한다” 는 뜻이다.

(3) protected가 public보다 안전하지 않은 이유

protected 멤버는 파생 클래스에서 마음대로 접근할 수 있다. 그리고 파생 클래스가 몇 개나 만들어질지, 누가 만들지는 클래스 작성자가 통제할 수 없다.

누군가 protected 멤버에 의존하는 파생 클래스를 작성해 두면, 그 멤버를 바꾸거나 없애는 순간 그 파생 클래스들이 전부 깨진다. 이는 public 멤버를 바꿀 때와 파급 효과의 성격이 같다. “외부에 노출된 범위”가 세상 전체냐 파생 클래스 전체냐의 차이일 뿐, “내 마음대로 바꿀 수 없다”는 점에서는 동일하다.

그래서 캡슐화 관점에서 쓸모 있는 구분은 두 가지뿐이다.

privateprivate이 아닌 나머지(public·protected)

한 줄 요약: 접근 지정자는 문법상 셋이지만, 캡슐화의 관점에서는 둘이다. protectedpublic보다 결코 더 안전하지 않다.

🔍 피드백: 논점별로 (1) 1.1 / (2) 1.7 / (3) 2.0을 받아 4.8점입니다.

(1) 세 가지 중 캡슐화 하나만 쓰셨습니다. 나머지 둘은 이름만 알면 바로 떠오르는 것들입니다.

  • 문법적 일관성 — 데이터가 전부 private이면 모든 접근이 public 멤버 함수를 거칩니다. 그러면 이 클래스를 쓰는 사람은 “이건 그냥 .value인가, .getValue()인가?”를 매번 헷갈릴 일이 없습니다. 괄호를 붙일지 말지 고민하지 않아도 되는 것, 그 자체가 이득입니다.
  • 세밀한 접근 제어public int value;읽기와 쓰기가 한 덩어리로 열립니다. 반면 함수로 감싸면 멤버마다 읽기 전용 / 쓰기 전용 / 읽기·쓰기 / 접근 불가를 따로 줄 수 있습니다. 접근을 막느냐 여느냐가 아니라, 의도에 맞는 인터페이스를 제공할 수 있다는 뜻입니다.

(2)에서 질문을 다르게 읽으셨습니다. 쓰신 “평균을 자주 구하면 A, 아니면 B가 유리하다”는 그 자체로는 정확한 분석이고 해답지에도 들어 있는 내용입니다. 다만 문제가 물은 것은 “이 클래스를 쓰는 외부 코드 입장에서 두 구현의 차이는 무엇인가” 였고, 정답은 “차이가 없다” 입니다.

이 “차이가 없다”가 시시한 답처럼 보이지만 사실 핵심입니다. 외부에서 구분할 수 없다는 것은 곧 내가 언제든 A에서 B로 갈아타도 아무도 안 깨진다는 뜻이니까요. 만약 데이터 멤버를 public으로 열어 두었다면 외부 코드가 그 멤버를 직접 참조하고 있을 테고, 구현을 바꾸는 순간 그 코드들이 전부 깨집니다.

그래서 캡슐화는 “감춘다”가 아니라 “나중에 바꿀 자유를 확보한다” 로 이해하시는 게 정확합니다. “내부 값을 직접 건드리지 않기에 캡슐화 측면에서 이득”이라고 연결하신 건 맞았는데, 그 이득의 정체가 미래의 변경 자유라는 데까지 가면 완성됩니다.

(3)의 앞부분은 정확합니다. “파생 클래스에서 직접 건드릴 수 있게 되기 때문”이 바로 정답의 핵심입니다. 여기에 그래서 어떤 결과가 오는가를 한 문장 붙이면 좋았습니다 — 누군가 그 protected 멤버에 의존하는 파생 클래스를 만들어 두면, 그 멤버를 바꾸는 순간 파생 클래스들이 전부 깨집니다. public 멤버를 바꿀 때와 파급 효과의 성격이 똑같습니다. 노출 범위가 “세상 전체”냐 “파생 클래스 전체”냐만 다를 뿐, “내 마음대로 못 바꾼다”는 점에서 동일한 겁니다.

뒷부분 “3가지”는 오답입니다. 정답은 2가지입니다. 문법상 접근 지정자는 public·protected·private 셋이지만, 방금 확인했듯 protectedpublic과 실질적으로 같은 처지입니다. 그래서 캡슐화의 잣대로 나누면 이렇게 둘이 됩니다.

private vs private이 아닌 나머지

(3)의 앞 문장에서 이미 그 근거를 정확히 대셨으니, 자기가 쓴 답을 한 번 더 읽고 결론을 맞춰 보는 것만으로 잡을 수 있었던 칸입니다.

출처: game_dev/cpp/chapter4/2025-06-20-cpp_4_22.md


📌 복습 포인트

  • Q2 로비 서버 분산 — ① 동기화 대상은 실력 정보 + 매칭 대기 여부 둘 ② 비동기가 탈락한 이유는 일관성이 아니라 응답을 못 받아서 ③ 통신량을 줄이는 방법은 데이터 응집력 — 같은 실력대끼리만 동기화 → server/game_server/9/2026-03-27-game_server_10_3.md
  • Q3 RAII와 스마트 포인터RAII는 원칙, 스마트 포인터는 그 원칙을 지키는 물건. auto_ptr은 복사 시 원본이 null → STL 컨테이너 불가. shared_ptr참조 카운팅이라 순환 참조를 못 푼다 → game_dev/cpp/chapter3/2025-06-09-cpp_3_13.md
  • Q4 분산 처리 전략 (근거 보강) — 원칙 문장은 “분산 처리를 피할 수 있다면 피하자.” 비용의 실체는 서버 간 통신 폭증과 스테일 데이터 → server/game_server/9/2026-03-22-game_server_9_10.md
  • Q5 데이터 멤버는 private — 세 이유는 문법적 일관성 / 세밀한 접근 제어 / 캡슐화. 두 구현은 외부에서 차이가 없고, 그래서 바꿀 자유가 생긴다. 캡슐화 관점의 접근 지정자는 2가지(private vs 나머지) → game_dev/cpp/chapter4/2025-06-20-cpp_4_22.md
  • 문제 읽는 법 — Q2(3)과 Q5(2)는 아는 내용을 썼는데도 묻는 것과 다른 답이라 감점됐습니다. 답을 쓰기 전에 질문의 마지막 어절을 한 번 다시 읽어 보세요.

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

댓글남기기