[말랑 퀴즈] 26/08/30 문제

게시:     수정

카테고리:

태그:

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

말랑말랑 퀴즈 📝

날짜: 2026-08-30 문제 수: 5문제


Q1. 🔴 어려움

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

iterator_traits<IterT>::iterator_category는 그 반복자의 종류를 나타내는 타입이다. 이를 이용해 반복자를 d칸 전진시키는 advance를 아래처럼 작성했다.

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 재시도 루프로 바꾸기만 하면 된다.

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

📝 내 선택: B. 특성 정보를 이용해서 순방향, 양방향 등을 분기했던 것은 기억나지만 정확히 기억나진 않는다.
A: list는 어떤 접근자였는지 정확히 기억나지 않는다. 문제없이 컴파일되지 않는다는 틀렸다.
C: typeid은 타입 점검 코드가 실행 파일에 들어가 문제가 됐던 것 같다.
D: 템플릿 메타프로그래밍에서 if문과 반복문을 사용하지 못했던 것으로 기억난다.


Q2. 🔴 어려움

매치메이킹은 함께 플레이할 다른 플레이어를 찾는 일이고, 그걸 담당하는 서버를 로비 서버라 한다. 실제 게임방을 돌리는 서버는 배틀 서버로 따로 뗀 상태다.

접속자가 늘어 로비 서버도 여러 대로 분산하기로 했다. 그런데 로비 서버들은 서로 데이터를 알아야 매칭을 만들 수 있다.

분산 처리 방식은 세 가지가 있다.

방식 설명
동기 분산 처리 다른 서버에 요청하고 응답을 기다린다
비동기 분산 처리 다른 서버에 요청만 던지고 기다리지 않는다
데이터 복제 기반 로컬 처리 필요한 데이터를 서버끼리 동기화해 두고, 판단은 자기 메모리에서 한다

(1) 로비 서버들이 서로 알아야 하는 데이터는 무엇인가? 그리고 위 세 방식 중 데이터 복제 기반 로컬 처리가 선택된 이유를 쓰시오. 나머지 두 방식이 왜 탈락했는지도 각각 밝히시오.

(2) 아래는 실제로 발생한 사고다.

① 클라이언트1 ──▶ 로비 서버1 : 게임 시작 대기
② 클라이언트2~10 ─▶ 로비 서버2 : 게임 시작 대기
③ 클라이언트1, 매칭 대기를 취소
④ 클라이언트1 ──▶ 로비 서버1 : 매칭 대기 = FALSE
⑤ (아직 로비 서버1 → 로비 서버2 동기화 전. 서버2는 C1이 대기 중이라고 알고 있다)
⑥ 로비 서버2 ──▶ 배틀 서버 : 게임방 생성 (C1 + C2~10으로 매칭)
⑦⑧⑨ 클라이언트2~10, 배틀 서버로 이동
⑩ 로비 서버2 ──▶ 로비 서버1 : "클라이언트1도 게임방에 참여시켜라"
⑪ 로비 서버1 ──▶ 클라이언트1 : 게임방 참여 요청  ⚠️ 이미 취소했는데!
⑬ 로비 서버1 ──▶ 로비 서버2 : (뒤늦게) C1 매칭 대기 = FALSE

이 문제를 무엇이라 부르는가? 그리고 ⑩ 시점에 로비 서버1은 C1이 취소했다는 사실을 이미 알고 있는데도 왜 되돌릴 수 없는가? 결국 어떻게 처리할 수밖에 없는지까지 쓰시오.

(3) 플레이어와 로비 서버가 아주 많아지면 로비 서버끼리 엄청난 통신량이 발생한다. 이를 줄이는 방법은 무엇인가? (힌트: 분산 여부를 판단할 때 쓰는 기준과 같은 개념이다.)

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

📝 내 풀이:

(1) 클라이언트들의 매칭 대기 여부

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

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

(3) 힌트에 있는 분산 여부를 판단하는 기준은 병목이 일어나는 지점을 말하는 것일 것이다.
병목이 일어나는 지점은 로비 서버기에, 로비 서버를 분산시켜야 한다.

이 방법에는 지역별 분산(미국 서버, 아시아 서버)가 대표적이다.


Q3. 🟢 쉬움

빈칸을 채우시오.

아래 코드는 팩토리 함수가 돌려준 생 포인터를 직접 delete 하고 있다. 중간에 return·continue·예외가 끼어들면 delete가 실행되지 않아 자원이 누출된다.

void f() {
    Investment* pInv = createInvestment();
    ...                                    // ← 여기서 예외가 나면?
    delete pInv;                           // 실행되지 않는다
}

그래서 자원 해제를 소멸자에게 맡긴다. 자원을 확보하자마자 객체에 집어넣고, 그 객체가 소멸될 때 자원이 풀리게 하는 이 방식을 ① ___(이)라고 부른다. 함수가 어떤 경로로 끝나든 지역 객체의 소멸자는 호출되므로 안전하다.

void f() {
    std::auto_ptr<Investment> pInv(createInvestment());
    ...
}   // 함수 종료 시 pInv의 소멸자가 delete를 호출한다
  • auto_ptr은 자원의 유일 소유권을 보장하려 한다. 그래서 복사하면 원본 쪽을 ② ___ 로 만들어 버린다.
    • 이 때문에 내부적으로 복사를 요구하는 ③ ___ 컨테이너에는 auto_ptr을 넣을 수 없다.
  • shared_ptr은 같은 자원을 가리키는 포인터의 개수를 세다가 그 수가 0이 되면 해제하는 ④ ___ 방식이다. 복사해도 양쪽 모두 유효하므로 동작이 더 직관적이다.
    • 다만 가비지 컬렉션과 달리, A가 B를, B가 A를 가리키는 ⑤ ___ 상황은 해결하지 못한다.
  • 두 스마트 포인터 모두 배열에는 쓰면 안 된다. 내부적으로 delete를 호출하는데, 배열에는 ⑥ ___ 가 필요하기 때문이다.
std::auto_ptr<std::string> aps(new std::string[10]);   // ❌ 잘못된 해제
std::shared_ptr<int>       spi(new int[1024]);         // ❌ 잘못된 해제

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

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


Q4. 🟡 보통

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

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

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

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

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

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


Q5. 🟡 보통

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

class AccessLevels {
public:
    int value;          // ← 이렇게 두면 무엇이 문제인가?
};

(1) 데이터 멤버를 private에 두어야 하는 이유는 크게 세 가지다. 각각의 이름을 쓰고 한 줄씩 설명하시오.

(2) 아래 클래스를 보자. averageSoFar()의 내부 구현은 두 가지가 가능하다.

class SpeedDataCollection {
public:
    void   addValue(int speed);      // 값을 추가
    double averageSoFar() const;     // 지금까지의 평균
    ...
};
  • 구현 A: addValue를 호출할 때마다 평균을 갱신해 두고, averageSoFar는 저장된 값을 그냥 반환한다.
  • 구현 B: averageSoFar가 호출될 때 그 자리에서 평균을 계산한다.

이 클래스를 쓰는 외부 코드 입장에서 두 구현의 차이는 무엇인가? 그리고 이 사실이 설계에 어떤 이득을 주는지 (1)에서 답한 세 이유 중 하나와 연결해 설명하시오.

(3) “데이터 멤버를 public에 두는 건 위험하지만, protected에 두면 그래도 안전하다”는 말은 흔히 들을 수 있다. 이 주장이 왜 틀렸는지 설명하시오. 그리고 캡슐화 관점에서 접근 지정자를 실질적으로 몇 가지로 나눌 수 있는지도 쓰시오.

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

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

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

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

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

댓글남기기