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

게시:     수정

카테고리:

태그:

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

말랑말랑 퀴즈 — 해답지 ✅

날짜: 2026-08-26 문제 수: 5문제 총점: 24.1/50 (48점) 🔴 다시 공부해요

📊 채점 결과

문항 유형 난이도 결과 점수
Q1 객관식 🟡 보통 ⭕ 정답 10 / 10
Q2 서술형 🔴 어려움 🔺 부분 정답 2.8 / 10
Q3 빈칸 🟢 쉬움 🔺 부분 정답 6.0 / 10
Q4 OX 🔴 어려움 ❌ 오답 0 / 10
Q5 서술형 🟡 보통 🔺 부분 정답 5.3 / 10
합계       24.1 / 50

총평

Q1은 만점입니다. 어제 ⑤에서 “네트워크 계층”을 “IP”로 답했던 걸 생각하면, 오늘 “LAN 안은 스위치만으로 되고 그 밖은 안 된다” 는 경계를 정확히 짚어 C를 고른 건 확실한 진전입니다. 보기 D가 바로 어제의 그 오해를 그대로 담은 함정이었는데 걸리지 않았습니다.

Q2는 점수는 낮지만 핵심 개념 하나는 정확히 갖고 있었습니다 — (3)에서 초기화 리스트에 기본 클래스 복사를 넣어야 한다는 걸 바로 썼습니다(super는 C++ 키워드가 아니라 Customer로 써야 하지만 개념은 맞습니다). 문제는 (1)과 (2)의 답이 서로 뒤바뀐 것입니다. 새 객체를 만드는 복사 생성자에서 "Alice"(= 대입받는 쪽의 기존 값)가 나올 수는 없습니다. 이 두 상황의 차이가 이 항목의 전부라 해도 과언이 아니니 아래 표를 꼭 보세요.

Q5는 (2)에서 “다양한 분기에서의 흐름”이라고 정확히 짚었습니다. 다만 (나)를 상태 다이어그램이라 부른 게 아쉬운데, UML에는 상태 다이어그램이 따로 있어서 이름이 틀리면 다른 다이어그램을 가리키게 됩니다. 정답은 액티비티 다이어그램입니다.

Q4는 어제 Q2와 똑같은 형태의 함정이었습니다 — 앞 문단이 전부 참이고 마지막 한 문장만 거짓. 이 패턴이 이틀 연속 나왔으니, OX 문제를 만나면 마지막 문장에만 밑줄을 긋고 따로 검사하는 것을 습관으로 만드세요. 이번 회차에서 가장 확실하게 점수를 올릴 수 있는 지점입니다.


Q1. 🟡 보통 — ⭕ 10/10

문제: 컴퓨터 네트워크는 통신의 주체인 단말기와, 단말기 간의 연결을 담당하는 네트워크 기기로 구성된다. 아래는 단말기를 잇는 두 가지 위상(topology)이다.

[ 링 위상 ]                    [ 별 위상 ]

[💻 A]  -  [💻 B]              [💻 A]        [💻 B]
   |          |                       \     /
[💻 D]  -  [💻 C]                    [📁  ?  ]
                                       /     \
                               [💻 C]        [💻 D]

이 두 구조와, 그 위에서 이루어지는 통신에 대한 설명으로 가장 올바른 것은?

  • A. 링 위상에서 A가 C에게 데이터를 보내면 중앙 기기가 목적지를 판별해 곧바로 C에게 전달하므로, 경유하는 단말기 없이 한 번에 도착한다.
  • B. 요즘 네트워크는 대부분 링 위상으로 구성된다. 단말기 하나가 고장 나도 반대 방향으로 우회할 수 있어, 중앙 기기 한 대에 의존하는 별 위상보다 안정적이기 때문이다.
  • C. 별 위상에서 ? 자리에 오는 기기는 네트워크 스위치이며, 이렇게 구성된 근거리 네트워크를 LAN이라 부른다. 같은 LAN 안의 단말기끼리는 스위치만으로 통신이 되지만, 스위치 한 대가 연결할 수 있는 단말기 수에는 한계가 있어 LAN을 넘어서는 통신에는 별도의 기기가 필요하다.
  • D. 별 위상에서 ? 자리에 오는 기기는 라우터다. 같은 LAN 안이든 밖이든 모든 통신은 IP 주소를 보고 경로를 정해야 하므로, LAN 내부 통신에도 라우터가 반드시 있어야 한다.

📝 내 선택: C

정답: C

해설:

별 위상(Star Topology) 은 단말기 사이에 네트워크 스위치를 두고 각 단말기가 스위치와 랜선으로 연결되는 형태이며, 요즘 네트워크는 대부분 이렇게 구성된다. 이런 별 모양의 근거리 네트워크를 LAN(Local Area Network) 이라 부른다.

같은 LAN 안에서는 A → 스위치 → 대상 단말기 경로로 데이터가 오간다. 즉 스위치만으로 통신이 된다. 이는 OSI 2계층(데이터 링크 계층) 의 일이며, 단말기는 고유한 MAC 주소로 식별되고 데이터는 프레임 단위로 오간다.

그런데 이 방식에는 한계가 있다.

  • 스위치 하나가 연결할 수 있는 단말기 수에 한계가 있다.
  • 단말기가 많아지면 고유한 MAC 주소 관리가 어려워진다.

그래서 WAN(Wide Area Network) 과, 서로 다른 LAN을 이어 주는 라우터(OSI 3계층, 네트워크 계층)가 등장한다. C는 이 흐름을 그대로 담고 있다.

  • A — 링 위상에는 중앙 기기가 없다. A가 C에게 보내려면 A → B → C처럼 다른 단말기를 경유해야 한다. “중앙 기기가 판별해 곧바로 전달”하는 것은 별 위상의 스위치가 하는 일이다.
  • B — 앞뒤가 뒤바뀌었다. 요즘은 대부분 별 위상으로 구성한다.
  • D — 두 군데가 틀렸다. 별 위상의 중앙 기기는 스위치이지 라우터가 아니고, 같은 LAN 안의 통신에는 라우터가 필요 없다. 라우터는 LAN과 LAN 사이([단말기 A] - [스위치] - [라우터] - [스위치] - [단말기 B])를 이을 때 등장한다.

🔍 피드백: 만점입니다. 특히 보기 D를 피한 것을 짚고 싶습니다. D는 어제 빈칸 ⑤에서 3계층을 “IP”라고 답했던 것과 같은 오해 — “결국 통신은 다 IP로 하는 것 아닌가?” — 를 그대로 문장으로 만든 함정이었습니다. 오늘은 LAN 내부는 MAC 주소와 스위치(2계층), LAN을 넘어서면 IP와 라우터(3계층) 라는 경계를 정확히 지켰습니다. 이 경계만 잡고 있으면 이 단원 문제는 거의 다 풀립니다.

출처: server/game_server/2/2025-06-30-game_server_2_1.md


Q2. 🔴 어려움 — 🔺 2.8/10

문제: C++에서 객체를 복사하는 함수는 복사 생성자와 복사 대입 연산자 두 가지이며, 둘을 묶어 객체 복사 함수라 부른다. 아래 코드를 보자.

void logCall(const std::string& funcName);   // 로그 기록용

class Customer {
public:
    Customer();                              // 기본 생성자
    Customer(const Customer& rhs);
    Customer& operator=(const Customer& rhs);
private:
    std::string name;
    Date        lastTransaction;
};

class PriorityCustomer : public Customer {   // public 상속
public:
    PriorityCustomer(const PriorityCustomer& rhs);
    PriorityCustomer& operator=(const PriorityCustomer& rhs);
private:
    int priority;
};

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
    : priority(rhs.priority)
{
    logCall("PriorityCustomer 복사 생성자");
}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
    logCall("PriorityCustomer 복사 대입 연산자");
    priority = rhs.priority;
    return *this;
}

(1) PriorityCustomer b(a); 를 실행했을 때, b가 물려받은 Customer 부분은 어떤 값을 갖게 되는가? 그렇게 되는 이유도 함께 쓰시오. (2) 이미 만들어져 있던 cc = a; 로 대입할 경우, c가 물려받은 Customer 부분은 어떤 값을 갖게 되는가? (1)과 결과가 어떻게 다른지 대비해서 설명하시오. (3) 위 두 함수를 올바르게 고치시오. (4) 중복을 없애려고 복사 생성자를 *this = rhs; 한 줄로 구현하면 왜 안 되는가? 올바른 방법은?

📝 내 풀이:

(1) name은 Alice, lastTransaction은 기본 생성자로 인한 값을 가진다. priority는 PriorityCustomer의 값을 가진다.

(2) name, lastTransaction은 기본 생성자로 인한 값을 가진다. priority는 PriorityCustomer의 값을 가진다.

(3)

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
    : super(rhs), priority(rhs.priority)
{
    logCall("PriorityCustomer 복사 생성자");
}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
    logCall("PriorityCustomer 복사 대입 연산자");
    *this = super::operator=(rhs);
    priority = rhs.priority;
    return *this;
}

(4) 복사 대입 생성자가 먼저 컴파일되지 않을수 있기 때문이었던 것 같다. 초기화 리스트를 통해 부모의 생성자를 호출한다.

정답:

(1) 복사 생성자의 경우 — Customer 부분이 “초기값”이 된다

bname"John"이 아니라 빈 문자열, lastTransactiona의 값이 아니라 Date의 기본값이 된다.

이유는 기본 클래스의 복사가 자동으로 일어나지 않기 때문이다. 파생 클래스의 생성자가 실행되려면 기본 클래스 부분이 먼저 만들어져야 하는데, 초기화 리스트에 Customer(rhs)가 없으므로 컴파일러가 대신 Customer의 기본 생성자 Customer() 를 호출한다. 즉 Customer 부분은 “복사된” 것이 아니라 새로 기본 초기화된 것이다.

무서운 점은 이 코드가 컴파일 에러도, 경고도 내지 않는다는 것이다. 복사는 되는데 부분적으로만 된다.

(2) 복사 대입 연산자의 경우 — Customer 부분이 “손대지 않은 채 그대로” 남는다

cname은 대입 전 값인 "Alice" 그대로다.

(1)과의 차이가 여기에 있다. 대입 연산자는 이미 존재하는 객체에 대해 호출되므로, 기본 클래스 생성자를 호출할 일 자체가 없다. Customer::operator=를 명시적으로 부르지 않으면 Customer의 복사 대입 연산자는 아예 호출되지 않고, Customer 부분은 수정된 것 없이 원래 값을 유지한다.

정리하면 같은 “기본 클래스 복사 누락”이라도 증상이 다르다.

  기본 클래스 부분의 결과
복사 생성자 Customer()가 호출되어 기본값(초기값) 객체를 새로 만드는 중이라 기본 클래스 생성자가 반드시 하나는 불린다
복사 대입 연산자 대입 이전 값 그대로 아무 함수도 안 불리므로 손대지 않는다

(3) 올바르게 고친 코드 — 기본 클래스 복사를 명시적으로 호출

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
    : Customer(rhs),                 // ✅ 기본 클래스의 복사 생성자 호출
      priority(rhs.priority)
{
    logCall("PriorityCustomer 복사 생성자");
}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
    logCall("PriorityCustomer 복사 대입 연산자");
    Customer::operator=(rhs);        // ✅ 기본 클래스의 복사 대입 연산자 호출
    priority = rhs.priority;
    return *this;
}

핵심 원칙은 “객체의 모든 부분을 빠짐없이 복사하라” 이다. 여기서 “모든 부분”은 자기 클래스의 모든 데이터 멤버 + 기본 클래스로부터 물려받은 부분을 뜻한다. 데이터 멤버를 새로 추가할 때 복사 함수를 같이 고쳐 줘야 하는 것도 같은 이유다.

(4) *this = rhs; 가 안 되는 이유

복사 생성자는 아직 초기화되지 않은 객체를 만들고 있는 중이다. 반면 대입 연산자는 이미 초기화가 끝난 객체에 값을 넣는 함수다. 따라서 복사 생성자 안에서 대입 연산자를 부르는 것은 아직 만들어지지도 않은 객체에 대입을 수행하는 위험한 행동이다. (반대로 대입 연산자에서 복사 생성자를 부르는 것도 마찬가지로 말이 안 된다 — 이미 존재하는 객체를 다시 생성하려는 셈이다.)

올바른 방법은 두 복사 함수가 서로를 호출하는 대신, 공통 동작을 별도의 private 함수로 분리해 공유하는 것이다.

class Customer {
public:
    Customer(const Customer& rhs) {
        logCall("복사 생성자");
        copyFrom(rhs);
    }

    Customer& operator=(const Customer& rhs) {
        logCall("복사 대입 연산자");
        if (this != &rhs) {          // 자기 대입 방어
            copyFrom(rhs);
        }
        return *this;
    }

private:
    void copyFrom(const Customer& rhs) {
        name            = rhs.name;
        lastTransaction = rhs.lastTransaction;
    }

    std::string name;
    Date        lastTransaction;
};

이렇게 하면 중복도 줄고, 멤버가 늘어날 때 고칠 곳이 copyFrom 한 군데로 모인다.

🔍 피드백: 논점 4개 중 (1) 1.0점, (3) 1.8점을 받아 2.8점입니다.

(1)과 (2)의 답이 서로 뒤바뀌었습니다. (2)에 쓴 “name, lastTransaction은 기본 생성자로 인한 값”이 바로 (1)의 정답이고, (1)에 쓴 “name은 Alice”가 (2)의 정답입니다. 문제 조건을 다시 보면 갈림길이 보입니다 — PriorityCustomer b(a);b이 줄에서 처음 태어나는 객체입니다. 태어나기도 전이었으니 "Alice"라는 값을 가진 적이 없고, 가질 수도 없습니다. 반대로 c이미 "Alice"로 채워져 존재하던 객체이고, c = a; 는 그 객체에 값을 덮어쓰려는 시도일 뿐입니다.

여기서 판별 기준 하나만 기억하세요. “이 객체는 지금 태어나는 중인가, 이미 살아 있는가?”

  • 태어나는 중(복사 생성자) → 기본 클래스 부분도 반드시 무언가로 만들어져야 하므로, 안 적으면 컴파일러가 Customer()를 대신 부릅니다 → 기본값
  • 이미 살아 있음(대입 연산자) → 만들 게 없으니 아무 함수도 안 불립니다원래 값 그대로

(3)은 절반 이상 맞았습니다. 복사 생성자 쪽에서 초기화 리스트에 기본 클래스 복사를 넣는다는 핵심을 정확히 짚었습니다(super는 Java/C#의 키워드이고 C++에는 없으니 Customer(rhs)로 쓰면 됩니다 — 개념은 맞으므로 감점하지 않았습니다). 다만 대입 연산자 쪽 *this = super::operator=(rhs);실행하면 무한 재귀에 빠집니다. *this = ... 자체가 PriorityCustomer::operator=를 다시 부르기 때문입니다. 여기서는 반환값을 어디에 담을 필요 없이 Customer::operator=(rhs); 한 줄만 호출하면 됩니다. 기본 클래스 부분을 갱신하는 게 목적이지, 무언가를 받아오는 게 아니니까요.

(4)는 0점입니다. “컴파일 순서” 문제가 아닙니다. 이유는 훨씬 단순합니다 — 복사 생성자 안의 *this는 아직 완성되지 않은 객체인데, 대입 연산자는 “이미 완성된 객체”를 전제하고 동작하는 함수라서 그렇습니다. 그리고 (4)가 물은 “올바른 방법”은 (3)의 답인 초기화 리스트가 아니라, 두 복사 함수가 서로를 부르는 대신 공통 코드를 copyFrom() 같은 private 함수로 빼내 둘이 함께 쓰는 것입니다.

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


Q3. 🟢 쉬움 — 🔺 6.0/10

문제: 빈칸을 채우시오.

게임 서버에서 MongoDB 명령을 실행하는 전형적인 순서다. (의사코드)

// 1단계: 연결 객체 생성 — 매개변수로 ① ___ 를 넘긴다
client = new client("mongodb://localhost:27017");

// 2단계: ② ___ 이름을 넘겨 액세스 획득
db = client["mydb"];

// 3단계: ③ ___ 이름을 넘겨 액세스 객체 획득
coll = db["mycollection"];

// 4단계: 도큐먼트를 만들어 넣는다
doc = new  ___ Document { ... };

coll. ___ (doc);

📝 내 답: 끝점 주소, 데이터베이스, 콜렉션, ?, addDocument

정답:

① 엔드포인트 ② DB 인스턴스 ③ 컬렉션 ④ Bson (BSON, Binary JSON) ⑤ insert

해설:

MongoDB는 C++, C#, 자바, 루비, 파이썬 등 다양한 언어에서 실행할 수 있는 클라이언트를 제공한다. 게임 서버에서 명령을 실행하는 순서는 바깥에서 안쪽으로 한 단계씩 좁혀 들어가는 형태다.

  1. 연결 객체 생성 — 매개변수로 엔드포인트(mongodb://localhost:27017처럼 주소와 포트)를 넘긴다.
  2. DB 인스턴스 액세스 획득 — 매개변수는 DB 인스턴스 이름(mydb).
  3. 컬렉션 액세스 객체 획득 — 매개변수는 컬렉션 이름(mycollection).
  4. 도큐먼트 추가 후 삽입insert()로 컬렉션에 넣는다.

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

④ BSON(Binary JSON) 은 MongoDB API가 도큐먼트에 액세스할 때 쓰는 객체 형식이다. JSON과 같은 역할이고 { "Key", "Value" } 구조도 동일하지만, BSON 객체의 트리 구조를 사용한다. 아래처럼 값 자리에 또 다른 BsonDocument를 넣어 중첩시킬 수 있다는 점이 관계형 데이터베이스의 평면적인 레코드와 대비되는 지점이다.

doc = new BsonDocument
{
    { "name", "John" },
    { "address", new BsonDocument
        {
            { "City",   "Seoul" },
            { "Street", "Nambu-street" }
        }
    }
};

coll.insert(doc);

🔍 피드백: 빈칸 5개 중 ①②③이 정확해 6.0점입니다. “끝점 주소”(엔드포인트), “데이터베이스”(DB 인스턴스), “콜렉션”(컬렉션) 은 표기만 다를 뿐 같은 개념이라 모두 정답 처리했습니다.

④는 문제 안에 답이 들어 있었습니다. 지문에 “Binary JSON의 약자”라고 적혀 있었으니, 그 두 단어의 머리글자를 그대로 조합하면 BSON입니다(new BsonDocument { ... }). MongoDB가 JSON 대신 BSON을 쓰는 이유도 함께 기억해 두세요 — { "Key", "Value" } 구조는 같지만 트리 구조라서 값 자리에 또 다른 문서를 통째로 중첩할 수 있고, 이게 그저께 배운 “NoSQL은 레코드가 트리·구조체 형태를 가질 수 있다”와 정확히 같은 이야기입니다.

addDocument 는 이름만 그럴듯한 오답입니다. MongoDB에서 도큐먼트를 컬렉션에 넣는 함수는 insert 입니다. 그저께 SQL의 CRUD를 insert / where / set으로 맞히셨는데, 관계형이든 NoSQL이든 “넣는” 동작의 이름은 insert로 같다고 묶어서 외우면 좋습니다.

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


Q4. 🔴 어려움 — ❌ 0/10

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

C++에는 “독립 구조의 객체는 반드시 크기가 0이 넘어야 한다” 는 규칙이 있다. 그래서 비정적 데이터 멤버가 하나도 없는 공백 클래스라 하더라도, 다른 클래스의 멤버 변수로 품으면 크기를 차지한다.

class Empty {};

class HoldsAnInt {
private:
    int   x;
    Empty e;                    // 객체 합성
};
// → sizeof(HoldsAnInt) > sizeof(int)

Empty 자리에 char 한 개가 들어가고, 바이트 정렬이 필요하다고 판단되면 패딩까지 붙기 때문이다.

이때 객체 합성 대신 private 상속으로 바꾸면 이 낭비가 사라진다.

class HoldsAnInt : private Empty {
private:
    int x;
};
// → sizeof(HoldsAnInt) == sizeof(int)

이것을 공백 기본 클래스 최적화(EBO) 라고 한다. 그리고 이 최적화는 기본 클래스를 여러 개 상속해도 똑같이 적용되므로, 품어야 할 공백 클래스가 여러 개일 때 특히 이득이 크다.

📝 내 답: O (잘 모르겠다)

정답: X

해설:

마지막 문단 직전까지는 전부 참이다.

  • C++에는 “독립 구조의 객체는 반드시 크기가 0이 넘어야 한다”는 규칙이 있다. ✅
  • 그래서 공백 클래스를 멤버로 품으면 char 한 개가 들어가고, 정렬이 필요하면 패딩까지 붙어 sizeof(HoldsAnInt) > sizeof(int) 가 된다. ✅
  • private 상속으로 바꾸면 sizeof(HoldsAnInt) == sizeof(int) 가 되고, 이를 공백 기본 클래스 최적화(EBO) 라고 부른다. ✅

틀린 곳은 마지막 문장 하나다. EBO는 단일 상속에만 적용된다. 기본 클래스를 두 개 이상 상속하면 적용되지 않으므로, “공백 클래스가 여러 개일 때 특히 이득”이라는 결론은 성립하지 않는다. 오히려 EBO를 노린다면 공백 기본 클래스는 하나만 두어야 한다.

이 명제가 헷갈리는 이유는 앞 문단들이 전부 사실이라 결론까지 딸려 오게 만들기 때문이다. 이런 형태의 명제는 마지막 문장만 따로 떼어 검사하는 습관이 필요하다.

한 걸음 더 — private 상속을 언제 쓰는가

EBO는 private 상속을 정당화하는 세 가지 예외 중 하나일 뿐이다.

  1. 기본 클래스의 protected 멤버에 접근해야 할 때
  2. 상속받은 가상 함수를 재정의해야 할 때
  3. 공백 기본 클래스 최적화(EBO) 가 필요할 때

private 상속의 의미는 is-a가 아니라 is-implemented-in-terms-of(…는 …를 써서 구현된다)이며, 구현만 물려받고 인터페이스는 물려받지 못한다. 그래서 기본 원칙은 “할 수 있다면 객체 합성을 쓰고, 꼭 해야만 한다면 private 상속을 쓴다” 이다. 객체 합성 쪽이 파생 클래스의 재정의를 막는 설계 제어가 가능하고, 정의를 분리해 컴파일 의존성도 줄일 수 있기 때문이다.

🔍 피드백: 판단(7점)·근거(3점) 모두 0점입니다.

이 문제는 어제 Q2와 완전히 같은 구조였습니다. 앞의 여러 문단이 전부 참이라 그대로 읽어 내려가면 O로 보이고, 마지막 한 문장에만 거짓이 심어져 있습니다. 이틀 연속 같은 함정에 걸렸으니 이번엔 방법을 하나 고정해 두세요.

OX 문제를 만나면 마지막 문장을 손가락으로 가리고 시작하세요. 앞부분이 참이라는 건 그 문장이 참이라는 근거가 전혀 되지 못합니다. 오히려 앞이 술술 맞을수록 마지막 문장을 의심해야 합니다 — 출제자가 굳이 참인 문단을 길게 깔아 둔 이유가 있으니까요.

이번 문항에서 그 마지막 문장은 “기본 클래스를 여러 개 상속해도 똑같이 적용된다” 였고, 정답은 EBO는 단일 상속에만 적용된다입니다. 이유도 직관적입니다 — 상속받은 공백 클래스가 둘이면 두 기본 클래스가 같은 주소를 갖게 되어 서로 구분할 수 없어집니다. C++이 “독립 구조의 객체는 크기가 0을 넘어야 한다”고 요구하는 이유 자체가 객체마다 고유한 주소를 보장하기 위해서이기 때문입니다.

private 상속은 ① protected 멤버 접근, ② 가상 함수 재정의, ③ EBO 이 세 경우에만 쓰는 예외적 도구이고, 그 밖에는 객체 합성을 쓴다 — 이 세 줄만 외워 두면 이 항목은 충분합니다.

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


Q5. 🟡 보통 — 🔺 5.3/10

문제: UML은 프로그램의 구조 명세를 표현하는 표기법이며, 여러 종류의 다이어그램이 있다. 채팅 기능을 두 가지 다이어그램 (가), (나)로 표현한 그림을 보고 답하시오.

(1) (가)와 (나)는 각각 어떤 다이어그램인가? 그리고 각 다이어그램의 구성 요소(무엇을 무엇으로 그리는지)를 쓰시오. (2) (가)만 보고서는 알 수 없지만 (나)를 보면 알 수 있는 정보가 무엇인지, 그림에서 구체적인 예를 하나 짚어 설명하시오. (3) 작업 A(통신 프로토콜 설계)와 작업 B(게임 로직 흐름 설계)에 각각 어느 다이어그램이 더 적합한지 고르고 이유를 쓰시오.

📝 내 풀이:

(1) 시퀀스 다이어그램, 상태 다이어그램. 각 구성 요소를 뭐라 하는지 잊어버렸다.

(2) 다양한 분기에서의 흐름

(3) 시퀀스, 상태 다이어그램

정답:

(1) 각 다이어그램의 정체와 구성 요소

  • (가) = 시퀀스 다이어그램. 구성 요소는 두 가지다.
    • 객체(Object) — 상호 작용의 주체로, 네모 상자로 표현한다. (클라이언트1, 서버, 클라이언트2, 클라이언트3)
    • 메시지(Message) — 상호 작용의 내용으로, 화살표로 표현한다. (Chat(), ShowChat())
  • (나) = 액티비티 다이어그램. 구성 요소는 다음과 같다.
    • 타원 도형활동 상태를 의미한다. (사용자 입력 텍스트, 유효성 검사, 로그 경고 등)
    • 최초 도형 — 각 서브 다이어그램 내 최초 도형은 최초 상태를 의미한다.
    • 조건 분기 — 분기문을 표현할 수 있다.

(2) (가)에는 없고 (나)에만 있는 정보 — 메시지를 받은 뒤의 “처리 방식”

시퀀스 다이어그램은 주고받은 메시지의 처리 방식에 대한 표현에 한계가 있다. (가)를 보면 “클라이언트1이 Chat()을 보냈고 서버가 ShowChat()을 두 클라이언트에게 보냈다”는 순서는 알 수 있지만, 그 사이에 서버가 무엇을 했는지는 전혀 드러나지 않는다.

구체적인 예로 “유효성 검사”와 그 뒤의 조건 분기를 들 수 있다. (나)에는 서버가 RMI Chat 호출을 받은 뒤 유효성 검사를 하고, 그 결과가 해커이면 “로그 경고”로, 실제 사용자이면 “다른 클라이언트 목록 얻기 → RMI ShowChat”으로 갈라진다는 사실이 그려져 있다. (가)에는 정상 경로만 화살표로 남아 있어, 해커로 판정되어 아무에게도 전달되지 않는 경로가 존재한다는 것 자체를 알 수 없다.

(3) 작업별 적합한 다이어그램

  • 작업 A(통신 프로토콜 설계) → 시퀀스 다이어그램. 프로토콜 설계에서 중요한 것은 “누가 누구에게, 어떤 순서로 무엇을 보내는가”인데, 시퀀스 다이어그램은 시간적 순서가 명확하다는 것이 강점이다. 객체별 세로선과 화살표만 보면 패킷 순서가 그대로 읽힌다.
  • 작업 B(게임 로직 흐름 설계) → 액티비티 다이어그램. 로직 흐름에는 조건 분기가 필수인데(예: 인벤토리가 가득 찼는가, 획득 권한이 있는가), 액티비티 다이어그램은 조건 분기와 병렬 처리 표현이 강점이다. 반면 시퀀스 다이어그램은 복잡한 로직 표현이 어렵다.
구분 시퀀스 다이어그램 액티비티 다이어그램
목적 시간 순서에 따른 상호작용 표현 처리 흐름과 로직 표현
표현 방식 객체 간 메시지 전달 활동 상태와 전환
강점 시간적 순서 명확 조건 분기, 병렬 처리 표현
약점 복잡한 로직 표현 어려움 시간적 순서 불명확
사용 시기 통신 프로토콜 설계 게임 로직 흐름 설계

🔍 피드백: 논점 3개에서 (1) 1.1점, (2) 2.0점, (3) 2.2점을 받아 5.3점입니다.

(1) (가)를 시퀀스 다이어그램이라고 맞힌 건 정확합니다. 다만 (나)는 액티비티 다이어그램입니다. “상태 다이어그램”이라고 쓰셨는데, UML에는 상태 다이어그램(State Diagram)이 별개로 존재하기 때문에 이름을 바꿔 부르면 다른 종류를 가리키게 됩니다. 구분은 이렇습니다 — 상태 다이어그램은 하나의 객체가 어떤 상태에 머무는지(대기 중 → 전투 중 → 사망)를 그리고, 액티비티 다이어그램은 일이 어떤 순서로 처리되는지(입력 → 검사 → 분기 → 전송)를 그립니다. 위 그림은 “활동이 이어지는 흐름”이므로 액티비티 쪽입니다.

구성 요소는 어렵지 않습니다. 그림에 그려진 모양 이름을 그대로 말하면 됩니다.

  • 시퀀스: 네모 상자 = 객체(상호 작용의 주체), 화살표 = 메시지(상호 작용의 내용)
  • 액티비티: 타원 = 활동 상태, 각 구역의 첫 도형 = 최초 상태, 갈라지는 지점 = 조건 분기

(2)가 이번 문항에서 가장 좋았습니다. “다양한 분기에서의 흐름”은 정답의 핵심을 정확히 짚은 표현입니다. 다만 문제가 “그림에서 구체적인 예를 하나 짚어” 설명하라고 요구했는데 그 부분이 비어 있어 만점은 아닙니다. 여기서 짚을 예는 유효성 검사 뒤의 “해커 → 로그 경고” 갈래입니다. (가)에는 정상 경로만 화살표로 남아 있어서, 해커로 걸러져 아무에게도 전달되지 않는 경로가 있다는 사실 자체가 보이지 않습니다. 이렇게 “그림의 이 부분”까지 지목하면 같은 답이 만점이 됩니다.

(3) 작업 A → (가), 작업 B → (나)라는 짝짓기는 정확합니다. 이유를 안 적어 감점했습니다. 이유도 표에 그대로 있습니다 — 시퀀스는 시간적 순서가 명확해서 패킷 순서 설계에 맞고, 액티비티는 조건 분기·병렬 처리 표현이 강점이라 로직 흐름 설계에 맞습니다. 서로의 약점이 상대의 강점이라는 대칭 구조로 기억하면 편합니다.

출처: server/game_server/5/2026-02-12-game_server_5_1.md


📌 복습 포인트

  • Q2 기본 클래스 복사 누락 — “이 객체는 지금 태어나는 중인가, 이미 살아 있는가?”로 복사 생성자(→ 기본값)와 대입 연산자(→ 원래 값 그대로)를 가르기. 그리고 *this = rhs; 대신 copyFrom() 분리 → game_dev/cpp/chapter2/2025-06-08-cpp_2_12.md
  • Q3 MongoDB 실행 순서엔드포인트 → DB 인스턴스 → 컬렉션 → BSON 도큐먼트, 넣는 함수는 insertserver/game_server/8/2026-03-14-game_server_8_9.md
  • Q4 EBO와 단일 상속 제약 — EBO는 단일 상속에만 적용. private 상속을 쓰는 세 가지 예외(protected 접근 / 가상 함수 재정의 / EBO) → game_dev/cpp/chapter6/2025-10-26-cpp_6_39.md
  • Q4 OX 푸는 법 — 앞 문단이 전부 참이어도 근거가 되지 않는다. 마지막 문장만 따로 떼어 검사할 것 (어제 Q2와 동일 패턴)
  • Q5 UML 두 다이어그램 — (나)는 상태가 아니라 액티비티 다이어그램. 시퀀스 = 객체(네모) + 메시지(화살표) / 액티비티 = 활동 상태(타원) + 조건 분기 → server/game_server/5/2026-02-12-game_server_5_1.md

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

댓글남기기