Company
교육 철학

객체지향 프로그래밍 연습(상속, 가상함수) - Part 2

C++ 상속 연습문제 50선 (동적할당 X) - Part 2

// C++ 상속 연습문제 50선 (virtual + 다형성 + 정적할당 + 주소값 배열 + for 루프)
// 난이도를 5단계로 세분화하여 완만한 학습 곡선 제공
// Part 1: 문제 1-25 | Part 2: 문제 26-50

Part 2 문제를 풀기 위한 필수 C++ 문법 가이드

중요: Part 1의 기초 문법(클래스, 상속, 가상함수)을 먼저 이해하고 오시는 것을 추천합니다!

1. 순수 가상 함수와 추상 클래스 (Abstract Class)

순수 가상 함수란 구현부가 없고 = 0으로 선언된 가상 함수를 말합니다.
순수 가상 함수를 하나라도 포함하는 클래스는 추상 클래스가 되며, 이는 객체를 직접 생성할 수 없습니다.

기본 문법

class Shape { public: virtual void Function() = 0; // = 0이 순수 가상 함수 };
C++
복사
= 0을 붙이면 구현이 없는 순수 가상 함수
순수 가상 함수를 하나라도 가지면 추상 클래스가 됨
추상 클래스는 객체를 직접 생성할 수 없음 (인터페이스 역할)
2. 형변환 (dynamic_cast)
if (Mammal* m = dynamic_cast<Mammal*>(animal)) { m->Sleep(); // 변환 성공시에만 실행 }
C++
복사
부모 포인터를 자식 타입으로 안전하게 변환
변환 실패시 nullptr 반환
3. 다형성 (Polymorphism)
Base* arr[] = { &child1, &child2 }; // 부모 포인터로 자식 참조 arr[0]->VirtualFunc(); // 자식의 함수가 호출됨
C++
복사
레벨 3 (문제 26-30) - 필수 문법
1. 순수 가상 함수와 추상 클래스
class Shape { public: virtual double Area() const = 0; // 순수 가상 함수 virtual double Perimeter() const = 0; }; class Circle : public Shape { private: double radius; public: Circle(double r) : radius(r) {} // 반드시 모든 순수 가상 함수를 구현해야 함 double Area() const override { return 3.14159 * radius * radius; } double Perimeter() const override { return 2 * 3.14159 * radius; } };
C++
복사

왜 사용하는가?

인터페이스 정의: 자식 클래스가 반드시 구현해야 할 함수를 강제합니다
다형성 보장: 부모 포인터로 여러 자식 객체를 동일한 인터페이스로 다룰 수 있습니다
설계 명확성: "이 클래스는 직접 사용할 수 없고, 상속받아 구현해야 한다"는 의도를 명확히 표현합니다
// Shape s; // 오류! 추상 클래스는 객체 생성 불가 Circle c(5.0); // OK! 모든 순수 가상 함수를 구현했으므로 Shape* shape = &c; // OK! 부모 포인터로 참조 가능
C++
복사
핵심: 추상 클래스는 "설계도"입니다. 직접 사용할 수 없지만, 이를 기반으로 구체적인 클래스를 만들 수 있습니다.

2. dynamic_cast (동적 형변환)

dynamic_cast런타임에 안전하게 타입을 변환하는 연산자입니다.
특히 상속 관계에서 부모 포인터를 자식 타입으로 변환할 때 사용합니다.

기본 문법과 동작

class Animal { public: virtual void Eat() { cout << "Eating..." << endl; } virtual ~Animal() {} }; class Mammal : public Animal { public: void Sleep() { cout << "Sleeping..." << endl; } }; class Dog : public Mammal { public: void Bark() { cout << "Woof!" << endl; } };
C++
복사
Dog d; Animal* animal = &d; // 업캐스팅 (자식 → 부모): 항상 안전 // 다운캐스팅 (부모 → 자식): dynamic_cast로 안전하게 if (Mammal* m = dynamic_cast<Mammal*>(animal)) { m->Sleep(); // 변환 성공! Mammal이 맞음 } else { cout << "변환 실패" << endl; // nullptr 반환 } // 더 구체적인 타입으로 변환 if (Dog* dog = dynamic_cast<Dog*>(animal)) { dog->Bark(); // 변환 성공! Dog가 맞음 }
C++
복사

왜 사용하는가?

타입 안전성: 잘못된 변환을 시도하면 nullptr을 반환하여 런타임 오류를 방지합니다
타입 확인: 현재 객체가 특정 타입인지 확인할 수 있습니다
선택적 기능 사용: 특정 타입일 때만 추가 기능을 호출할 수 있습니다
// 배열 순회하며 타입별로 다른 동작 Animal* animals[] = { &dog, &cat, &bird }; for (int i = 0; i < 3; ++i) { animals[i]->Eat(); // 공통 기능 // Mammal인 경우에만 Sleep() 호출 if (Mammal* m = dynamic_cast<Mammal*>(animals[i])) { m->Sleep(); } }
C++
복사
주의: dynamic_cast를 사용하려면 부모 클래스에 가상 함수가 최소 1개 있어야 합니다 (vtable이 필요하므로)

3. 포인터 멤버 변수와 컴포지션 (Composition)

컴포지션은 "has-a" 관계를 표현합니다. 즉, 한 클래스가 다른 클래스의 객체를 멤버로 포함하는 것입니다.
상속("is-a")과 달리, 컴포지션은 더 유연하고 결합도가 낮은 설계를 가능하게 합니다.

기본 패턴

class Engine { public: Decorator(Base* obj) : wrapped(obj) {} };
C++
복사
클래스가 다른 객체의 포인터를 멤버로 보관
생성자에서 포인터를 초기화
5. 재귀 함수
void Display() { for (auto child : children) { child->Display(); // 자식도 재귀 호출 } }
C++
복사
트리 구조에서 모든 자식 노드를 순회할 때 사용
6. 정적 배열과 포인터 관리
Component* children[10]; // 최대 10개 int count = 0; children[count++] = &newChild;
C++
복사
동적 할당 없이 배열로 여러 객체 관리
레벨 4 (문제 31-40) - 필수 문법
4. 포인터 멤버 변수 (컴포지션)
class Decorator { Base* wrapped; // 다른 객체를 포함 public: void Start() { cout << "Engine started" << endl; } void Stop() { cout << "Engine stopped" << endl; } }; class Car { private: Engine* engine; // 다른 객체를 포함 public: Car(Engine* e) : engine(e) {} void Drive() { if (engine) { engine->Start(); cout << "Driving..." << endl; } } void Park() { if (engine) { engine->Stop(); cout << "Parked" << endl; } } };
C++
복사
// 사용 예시 Engine myEngine; Car myCar(&myEngine); myCar.Drive(); // Engine의 기능을 내부적으로 사용
C++
복사

데코레이터 패턴에서의 활용

class Beverage { public: void Render() override { } void Update() override { } };
C++
복사
여러 인터페이스를 동시에 상속
각 인터페이스의 함수를 모두 구현해야 함
8. 함수 오버로딩
class Visitor { public: virtual void Visit(TypeA* obj) = 0; virtual void Visit(TypeB* obj) = 0; // 같은 이름, 다른 타입 };
C++
복사
같은 함수 이름에 다른 매개변수 타입
9. 포인터 체이닝
class Handler { Handler* next; // 다음 핸들러 public: void SetNext(Handler* h) { next = h; } };
C++
복사
연결 리스트처럼 다음 객체를 가리키는 포인터
레벨 5 (문제 41-50) - 필수 문법
7. 다중 상속 (인터페이스 패턴)
class GameObject : public IRenderable, public IUpdatable { public: virtual int GetCost() = 0; virtual string GetDescription() = 0; virtual ~Beverage() {} }; class Coffee : public Beverage { public: int GetCost() override { return 2000; } string GetDescription() override { return "Coffee"; } }; class AddOn : public Beverage { protected: Beverage* beverage; // 다른 음료를 "감싸는" 패턴 public: AddOn(Beverage* b) : beverage(b) {} }; class Milk : public AddOn { public: Milk(Beverage* b) : AddOn(b) {} int GetCost() override { return beverage->GetCost() + 500; // 기존 비용에 추가 } string GetDescription() override { return beverage->GetDescription() + " + Milk"; } };
C++
복사
// 중첩해서 래핑 가능 Coffee coffee; Milk milkCoffee(&coffee); Sugar sweetMilkCoffee(&milkCoffee); cout << sweetMilkCoffee.GetDescription() << ": " << sweetMilkCoffee.GetCost() << "원" << endl; // 출력: Coffee + Milk + Sugar: 3000원
C++
복사
핵심: 컴포지션은 "객체를 다른 객체 안에 넣는다"는 개념입니다. 상속보다 유연하며, 런타임에 동작을 변경할 수 있습니다.

4. 재귀 함수와 트리 구조

재귀 함수는 자기 자신을 호출하는 함수입니다.
트리 구조(부모-자식 관계)에서 모든 노드를 순회할 때 매우 유용합니다.

컴포짓 패턴에서의 재귀

class Component { public: virtual void Display() = 0; virtual ~Component() {} }; class Leaf : public Component { private: string name; public: Leaf(string n) : name(n) {} void Display() override { cout << "Leaf: " << name << endl; } }; class Composite : public Component { private: string name; Component* children[10]; int childCount; public: Composite(string n) : name(n), childCount(0) {} void Add(Component* c) { children[childCount++] = c; } void Display() override { cout << "Composite: " << name << endl; // 재귀적으로 모든 자식의 Display() 호출 for (int i = 0; i < childCount; ++i) { children[i]->Display(); // 여기서 재귀 발생 } } };
C++
복사
// 사용 예시: 폴더 구조 Leaf file1("file1.txt"); Leaf file2("file2.txt"); Composite folder1("Folder1"); folder1.Add(&file1); folder1.Add(&file2); Leaf file3("file3.txt"); Composite rootFolder("Root"); rootFolder.Add(&folder1); // 폴더 안에 폴더 rootFolder.Add(&file3); rootFolder.Display(); // 출력: // Composite: Root // Composite: Folder1 // Leaf: file1.txt // Leaf: file2.txt // Leaf: file3.txt
C++
복사

재귀의 핵심 원리

1.
종료 조건: Leaf에 도달하면 재귀 종료
2.
재귀 호출: Composite는 자식들의 Display()를 호출
3.
자동 백트래킹: 함수가 종료되면 호출한 곳으로 자동 복귀
핵심: 재귀는 트리 구조를 탐색하는 가장 자연스러운 방법입니다. 각 노드가 자신의 자식들에게 동일한 작업을 요청하는 패턴입니다.

5. 다중 상속 (Multiple Inheritance)

다중 상속은 하나의 클래스가 여러 부모 클래스를 동시에 상속받는 것입니다.
C++에서는 주로 인터페이스 패턴으로 사용됩니다.

기본 문법

// 인터페이스 정의 (순수 가상 함수만 포함) class IRenderable { public: virtual void Render() = 0; virtual ~IRenderable() {} }; class IUpdatable { public: virtual void Update() = 0; virtual ~IUpdatable() {} }; class ICollider { public: virtual void CheckCollision() = 0; virtual ~ICollider() {} }; // 여러 인터페이스를 동시에 구현 class GameObject : public IRenderable, public IUpdatable, public ICollider { private: string name; public: GameObject(string n) : name(n) {} // 모든 인터페이스의 함수를 구현해야 함 void Render() override { cout << name << " rendering" << endl; } void Update() override { cout << name << " updating" << endl; } void CheckCollision() override { cout << name << " collision check" << endl; } };
C++
복사

인터페이스별로 분리해서 사용

class Player : public GameObject { public: Player(string n) : GameObject(n) {} void Render() override { cout << "Player rendering with animation" << endl; } }; class Enemy : public GameObject { public: Enemy(string n) : GameObject(n) {} void Update() override { cout << "Enemy AI update" << endl; } };
C++
복사
// 사용: 인터페이스별로 배열 관리 가능 Player p("Hero"); Enemy e("Goblin"); // 렌더링만 필요한 경우 IRenderable* renderables[] = { &p, &e }; for (int i = 0; i < 2; ++i) { renderables[i]->Render(); } // 업데이트만 필요한 경우 IUpdatable* updatables[] = { &p, &e }; for (int i = 0; i < 2; ++i) { updatables[i]->Update(); }
C++
복사

다중 상속 주의사항

1.
다이아몬드 문제: 같은 조상을 여러 경로로 상속받을 때 발생 (virtual 상속으로 해결)
2.
인터페이스만 사용: 데이터 멤버가 있는 클래스를 다중 상속하면 복잡해짐
3.
명확한 목적: 보통 "이 객체는 여러 역할을 할 수 있다"는 의미로 사용
핵심: 다중 상속은 "여러 능력을 가진 객체"를 표현합니다. 게임에서 GameObject가 렌더링도 되고, 업데이트도 되고, 충돌도 체크하는 것처럼요.

6. 함수 오버로딩과 비지터 패턴

함수 오버로딩은 같은 이름의 함수를 매개변수 타입만 다르게 여러 개 정의하는 것입니다.
비지터 패턴에서 타입별로 다른 동작을 구현할 때 유용합니다.
class ConcreteElementA; class ConcreteElementB; class Visitor { public: virtual void Visit(ConcreteElementA* element) = 0; virtual void Visit(ConcreteElementB* element) = 0; // 오버로딩 virtual ~Visitor() {} }; class Element { public: virtual void Accept(Visitor* visitor) = 0; virtual ~Element() {} }; class ConcreteElementA : public Element { public: void Accept(Visitor* visitor) override { visitor->Visit(this); // 자신의 타입을 전달 } void OperationA() { cout << "Element A specific operation" << endl; } }; class ConcreteElementB : public Element { public: void Accept(Visitor* visitor) override { visitor->Visit(this); // 자신의 타입을 전달 } void OperationB() { cout << "Element B specific operation" << endl; } }; class ConcreteVisitor : public Visitor { public: void Visit(ConcreteElementA* element) override { cout << "Visiting A: "; element->OperationA(); } void Visit(ConcreteElementB* element) override { cout << "Visiting B: "; element->OperationB(); } };
C++
복사
핵심: 오버로딩을 통해 타입별로 다른 동작을 깔끔하게 분리할 수 있습니다.

7. 포인터 체이닝 (Chain of Responsibility)

포인터 체이닝은 객체들을 연결 리스트처럼 연결하여, 요청을 순차적으로 전달하는 패턴입니다.
class Handler { protected: Handler* nextHandler; public: Handler() : nextHandler(nullptr) {} void SetNext(Handler* handler) { nextHandler = handler; } virtual void HandleRequest(int level) { if (nextHandler) { nextHandler->HandleRequest(level); // 다음으로 전달 } else { cout << "요청을 처리할 수 없습니다" << endl; } } virtual ~Handler() {} }; class LowLevelHandler : public Handler { public: void HandleRequest(int level) override { if (level <= 1) { cout << "Low level handler 처리" << endl; } else { Handler::HandleRequest(level); // 다음 핸들러로 } } }; class MidLevelHandler : public Handler { public: void HandleRequest(int level) override { if (level <= 2) { cout << "Mid level handler 처리" << endl; } else { Handler::HandleRequest(level); // 다음 핸들러로 } } }; class HighLevelHandler : public Handler { public: void HandleRequest(int level) override { if (level <= 3) { cout << "High level handler 처리" << endl; } else { Handler::HandleRequest(level); // 다음 핸들러로 } } };
C++
복사
// 체인 구성 LowLevelHandler low; MidLevelHandler mid; HighLevelHandler high; low.SetNext(&mid); mid.SetNext(&high); // 요청 전달 low.HandleRequest(1); // Low가 처리 low.HandleRequest(2); // Mid가 처리 low.HandleRequest(3); // High가 처리
C++
복사
핵심: 포인터로 다음 객체를 가리켜 연결하면, 책임을 순차적으로 전달할 수 있습니다.

공부 순서 추천

1.
레벨 3 (문제 26-30): 추상 클래스와 dynamic_cast 연습
2.
레벨 4 (문제 31-35): 포인터 멤버와 컴포지션 패턴
3.
레벨 4 (문제 36-40): 디자인 패턴 기본 구조
4.
레벨 5 (문제 41-45): 복잡한 패턴 조합
5.
레벨 5 (문제 46-50): 다중 상속과 고급 패턴

가장 중요한 핵심 개념 Top 7

1.
순수 가상 함수 (= 0) - 인터페이스 정의, 자식 클래스가 반드시 구현하도록 강제
2.
dynamic_cast - 안전한 타입 변환, 런타임에 타입 확인 가능
3.
포인터 멤버 변수 - 객체 조합 (컴포지션), 상속보다 유연한 설계
4.
재귀 함수 - 트리 구조 탐색, 자기 자신을 호출하여 모든 노드 방문
5.
다중 상속 - 여러 인터페이스 구현, 하나의 객체가 여러 역할 수행
6.
함수 오버로딩 - 같은 이름, 다른 타입의 함수로 타입별 동작 분리
7.
포인터 체이닝 - 객체들을 연결하여 요청을 순차적으로 전달

각 개념이 사용되는 디자인 패턴

개념
주로 사용되는 패턴
핵심 아이디어
순수 가상 함수
Strategy, State, Command
동작을 교체 가능하게
dynamic_cast
Composite, Visitor
런타임에 타입 확인
포인터 멤버
Decorator, Proxy, Adapter
객체를 감싸거나 위임
재귀
Composite, Interpreter
트리 구조 순회
다중 상속
Bridge, Facade
여러 인터페이스 구현
함수 오버로딩
Visitor
타입별 동작 분리
포인터 체이닝
Chain of Responsibility
책임 연쇄 전달

레벨 3: 추상 클래스와 인터페이스 패턴 (문제 26-30)

문제 26. 계층적 상속과 형변환 활용

기획 요구사항:
동물원 관리 시스템을 만들려고 합니다. 모든 동물은 먹을 수 있고, 일부 포유류는 잠을 잘 수 있으며, 개와 고양이는 각자의 소리를 낼 수 있습니다. 동물들을 하나의 배열로 관리하면서, 각 동물의 타입에 따라 가능한 행동들을 모두 수행할 수 있어야 합니다.
구현 가이드:
Animal (Eat() 가상함수) → Mammal (Sleep() 가상함수 추가) → Dog/Cat (Speak() 가상함수 추가, 모든 함수 오버라이딩)
Animal 포인터 배열로 순회하며 Eat()만 호출 후, Mammal 이상인지 확인하여 Sleep()도 호출하도록 구현하세요.
Dog d; Cat c; Animal* animals[] = { &d, &c }; for (int i = 0; i < 2; ++i) { animals[i]->Eat(); // Mammal인 경우 Sleep()도 호출 (dynamic_cast 활용) }
C++
복사

문제 27. 스킬 시스템 with 추상 클래스

기획 요구사항:
RPG 게임의 스킬 시스템을 구현합니다. 모든 스킬은 실행 가능하고 쿨다운 시간이 있어야 합니다. 공격 스킬, 방어 스킬, 회복 스킬이 있으며, 각각 다른 효과를 가집니다. 스킬들을 동일한 방식으로 관리하고 사용할 수 있어야 합니다.
구현 가이드:
Skill (순수 가상: Execute(), GetCooldown()) → AttackSkill/DefenseSkill/HealSkill (각각 구현)
각 스킬을 실행하고 쿨다운을 출력하세요.
AttackSkill fireball("Fireball", 5); DefenseSkill shield("Shield", 10); HealSkill heal("Heal", 8); Skill* skills[] = { &fireball, &shield, &heal }; for (int i = 0; i < 3; ++i) { skills[i]->Execute(); std::cout << "Cooldown: " << skills[i]->GetCooldown() << std::endl; }
C++
복사

문제 28. 배송 시스템

기획 요구사항:
쇼핑몰의 배송비 계산 시스템을 만듭니다. 일반 배송과 빠른 배송 두 가지 옵션이 있습니다. 일반 배송은 고정 요금이고, 빠른 배송은 거리에 따라 요금이 달라집니다. 각 배송 방식의 정보를 출력하고 배송비를 계산할 수 있어야 합니다.
구현 가이드:
Delivery (순수 가상: Calculate(), GetInfo()) → StandardDelivery/ExpressDelivery (각각 구현)
StandardDelivery는 3000원, ExpressDelivery는 거리 × 500원으로 계산합니다.
StandardDelivery std; ExpressDelivery exp(10); Delivery* deliveries[] = { &std, &exp }; for (int i = 0; i < 2; ++i) { deliveries[i]->GetInfo(); std::cout << deliveries[i]->Calculate() << std::endl; }
C++
복사

문제 29. 인증 시스템

기획 요구사항:
보안 시스템에서 여러 가지 인증 방법을 지원해야 합니다. 비밀번호 인증과 생체 인증 두 가지 방식이 있으며, 각각 다른 방식으로 사용자를 인증합니다. 모든 인증 방식은 동일한 인터페이스로 사용할 수 있어야 합니다.
구현 가이드:
Authenticator (순수 가상: Authenticate(string username, string password)) → PasswordAuth/BiometricAuth (각각 구현)
각 인증 방식으로 로그인을 시도하고 결과를 출력하세요.
PasswordAuth passAuth; BiometricAuth bioAuth; Authenticator* auths[] = { &passAuth, &bioAuth }; for (int i = 0; i < 2; ++i) auths[i]->Authenticate("user123", "pass456");
C++
복사

문제 30. 필터 시스템

기획 요구사항:
텍스트 처리 프로그램에서 다양한 필터를 적용할 수 있어야 합니다. 대문자 변환, 소문자 변환, 문자열 뒤집기 등의 필터가 있으며, 각 필터는 입력된 텍스트를 변환하여 반환합니다. 여러 필터를 동일한 방식으로 사용할 수 있어야 합니다.
구현 가이드:
Filter (순수 가상: Apply(string input)) → UpperCaseFilter/LowerCaseFilter/ReverseFilter (각각 구현)
각 필터를 적용한 결과를 출력하세요.
UpperCaseFilter upper; LowerCaseFilter lower; ReverseFilter reverse; Filter* filters[] = { &upper, &lower, &reverse }; for (int i = 0; i < 3; ++i) std::cout << filters[i]->Apply("Hello") << std::endl;
C++
복사

레벨 4: 복잡한 계층 구조와 디자인 패턴 기초 (문제 31-40)

문제 31. 전투 시스템 (다단계 상속 + 복합 동작)

기획 요구사항:
게임의 전투 시스템을 구현합니다. 모든 유닛은 체력을 가지며, 전투에 참여하는 유닛은 공격과 방어를 할 수 있습니다. 전사, 마법사, 궁수는 각각 다른 방식으로 공격하고 방어하며, 각자 고유한 스킬도 사용할 수 있습니다. 모든 전투 유닛을 하나의 배열로 관리하면서 전투 행동을 수행할 수 있어야 합니다.
구현 가이드:
Entity (HP 멤버) → Combatant (Attack(), Defend() 가상함수) → Warrior/Mage/Archer (각자 Attack()과 Defend() 오버라이딩, UseSkill() 가상함수 추가)
Combatant 배열로 순회하며 각 유닛이 공격, 방어를 사용하도록 구현하세요.
Warrior w(100, 30); Mage m(80, 50); Archer a(90, 25); Combatant* units[] = { &w, &m, &a }; for (int i = 0; i < 3; ++i) { units[i]->Attack(); units[i]->Defend(); }
C++
복사

문제 32. 계층적 메뉴 시스템

기획 요구사항:
UI 메뉴 시스템을 만듭니다. 개별 메뉴 항목도 있고, 여러 항목을 그룹으로 묶는 폴더도 있습니다. 폴더 안에는 다른 항목들이나 폴더가 들어갈 수 있습니다. 메뉴를 표시할 때 개별 항목이든 폴더든 동일한 방식으로 다루면서, 폴더는 자동으로 내부의 모든 항목을 표시해야 합니다.
구현 가이드:
Component (순수 가상: Display()) → Leaf (구현), Composite (자식 배열 보관, 재귀 Display())
Composite는 여러 Leaf를 포함할 수 있으며, Display()는 모든 하위 항목을 출력합니다.
Leaf l1("Item1"); Leaf l2("Item2"); Composite comp("Folder"); comp.Add(&l1); comp.Add(&l2); Component* components[] = { &l1, &comp }; for (int i = 0; i < 2; ++i) components[i]->Display();
C++
복사

문제 33. 파일 시스템 계층 구조

기획 요구사항:
파일 탐색기를 만듭니다. 파일과 폴더가 있으며, 폴더 안에는 파일이나 다른 폴더가 들어갈 수 있습니다. 파일은 자신의 크기를 가지고, 폴더는 내부의 모든 파일과 하위 폴더들의 크기를 합친 전체 크기를 계산해야 합니다. 파일이든 폴더든 동일한 방식으로 정보를 표시하고 크기를 조회할 수 있어야 합니다.
구현 가이드:
FileSystemNode (이름, GetSize() 순수 가상, Display() 순수 가상) → File (크기 저장, 구현), Folder (자식 노드 배열 저장, 재귀적으로 크기 합산 및 표시)
Folder는 여러 File/Folder를 포함할 수 있으며, GetSize()는 모든 하위 항목의 크기를 합산합니다.
File f1("doc.txt", 100); File f2("image.png", 500); Folder folder("MyFolder"); folder.Add(&f1); folder.Add(&f2); FileSystemNode* nodes[] = { &f1, &folder }; for (int i = 0; i < 2; ++i) { nodes[i]->Display(); std::cout << "Size: " << nodes[i]->GetSize() << std::endl; }
C++
복사

문제 34. 정렬 알고리즘 선택 시스템

기획 요구사항:
데이터를 정렬하는 프로그램을 만듭니다. 버블 정렬, 퀵 정렬 등 여러 정렬 알고리즘이 있으며, 실행 중에 사용자가 원하는 알고리즘으로 변경할 수 있어야 합니다. 정렬 로직은 쉽게 교체 가능하도록 설계하되, 정렬을 실행하는 코드는 변경되지 않아야 합니다.
구현 가이드:
SortStrategy (순수 가상: Sort()) → BubbleSort/QuickSort (구현), Sorter 클래스는 SortStrategy 포인터를 멤버로 가지며 SetStrategy()로 전략 교체 가능.
BubbleSort bubble; QuickSort quick; Sorter sorter(&bubble); sorter.PerformSort(); // BubbleSort 사용 sorter.SetStrategy(&quick); sorter.PerformSort(); // QuickSort 사용
C++
복사

문제 35. 음료 옵션 추가 시스템

기획 요구사항:
카페 주문 시스템을 만듭니다. 기본 음료(커피)가 있고, 우유 같은 추가 옵션을 선택할 수 있습니다. 추가 옵션은 기본 음료 가격에 옵션 가격을 더한 총 금액을 계산해야 합니다. 기본 음료든 옵션이 추가된 음료든 동일한 방식으로 가격을 조회할 수 있어야 합니다.
구현 가이드:
Beverage (GetCost() 가상함수) → Coffee (기본 구현), AddOn은 Beverage를 상속하면서 내부에 다른 Beverage 포인터를 가지며, Milk는 AddOn을 상속하여 기존 비용에 추가.
Coffee coffee; Milk milkCoffee(&coffee); Beverage* drinks[] = { &coffee, &milkCoffee }; for (int i = 0; i < 2; ++i) std::cout << drinks[i]->GetCost() << std::endl;
C++
복사

문제 36. 로깅 시스템

기획 요구사항:
시스템 로그를 기록하는 프로그램을 만듭니다. 콘솔에 출력, 파일에 저장, 네트워크로 전송 등 여러 방식으로 로그를 기록할 수 있어야 합니다. 로그를 기록하는 코드는 어떤 방식을 사용하든 동일하게 작성되어야 하며, 나중에 새로운 로깅 방식을 추가하기 쉬워야 합니다.
구현 가이드:
Logger (순수 가상: Log(string message)) → ConsoleLogger/FileLogger/NetworkLogger (각각 구현)
각 로거로 메시지를 기록하고 출력 방식을 다르게 구현하세요.
ConsoleLogger console; FileLogger file; NetworkLogger network; Logger* loggers[] = { &console, &file, &network }; for (int i = 0; i < 3; ++i) loggers[i]->Log("System started");
C++
복사

문제 37. 기기 상태 관리 시스템

기획 요구사항:
기기의 상태를 관리하는 시스템을 만듭니다. 기기는 대기 중, 실행 중, 정지됨 등 여러 상태를 가질 수 있으며, 각 상태에서 요청을 받았을 때 다르게 동작합니다. 현재 상태에 따라 자동으로 적절한 동작이 수행되어야 합니다.
구현 가이드:
State (순수 가상: Handle()) → IdleState/RunningState/StoppedState (각각 구현)
각 상태에서의 동작을 출력하세요.
IdleState idle; RunningState running; StoppedState stopped; State* states[] = { &idle, &running, &stopped }; for (int i = 0; i < 3; ++i) states[i]->Handle();
C++
복사

문제 38. 게임 명령 실행/취소 시스템

기획 요구사항:
게임에서 플레이어의 행동을 관리하는 시스템을 만듭니다. 이동, 공격 등의 행동을 수행할 수 있고, 각 행동은 실행 취소(Undo)가 가능해야 합니다. 모든 행동은 동일한 방식으로 실행하고 취소할 수 있어야 하며, 나중에 새로운 행동을 추가하기 쉬워야 합니다.
구현 가이드:
Command (순수 가상: Execute(), Undo()) → MoveCommand/AttackCommand (각각 구현)
각 명령을 실행하고 취소하는 동작을 출력하세요.
MoveCommand move("North"); AttackCommand attack("Enemy"); Command* commands[] = { &move, &attack }; for (int i = 0; i < 2; ++i) { commands[i]->Execute(); commands[i]->Undo(); }
C++
복사

문제 39. 복잡한 객체 조립 시스템

기획 요구사항:
복잡한 제품을 단계별로 조립하는 시스템을 만듭니다. 제품은 여러 부품으로 구성되며, 부품 조립 순서는 정해져 있습니다. 같은 조립 과정이지만 다른 부품을 사용하여 다양한 종류의 제품을 만들 수 있어야 합니다. 조립 과정은 동일하되, 실제 부품 선택은 조립 방식에 따라 달라집니다.
구현 가이드:
Builder (순수 가상: BuildPart1(), BuildPart2(), GetResult()) → ConcreteBuilderA/B (각각 구현)
각 빌더로 객체를 생성하고 결과를 출력하세요.
ConcreteBuilderA builderA; ConcreteBuilderB builderB; Builder* builders[] = { &builderA, &builderB }; for (int i = 0; i < 2; ++i) { builders[i]->BuildPart1(); builders[i]->BuildPart2(); builders[i]->GetResult(); }
C++
복사

문제 40. 작업 처리 프로세스 시스템

기획 요구사항:
정해진 순서로 작업을 처리하는 시스템을 만듭니다. 모든 작업은 1단계, 2단계를 순서대로 거쳐야 하지만, 각 단계에서 하는 구체적인 작업 내용은 작업 유형마다 다릅니다. 전체 작업 흐름은 고정하되, 각 단계의 세부 구현은 유연하게 바꿀 수 있어야 합니다.
구현 가이드:
AbstractClass (가상함수: TemplateMethod(), 순수 가상: Step1(), Step2()) → ConcreteClassA/B (Step1, Step2 구현)
TemplateMethod()는 Step1(), Step2()를 순서대로 호출합니다.
ConcreteClassA a; ConcreteClassB b; AbstractClass* classes[] = { &a, &b }; for (int i = 0; i < 2; ++i) classes[i]->TemplateMethod();
C++
복사

레벨 5: 고급 디자인 패턴과 복합 시스템 (문제 41-50)

문제 41. 다중 옵션 음료 시스템

기획 요구사항:
카페 주문 시스템을 확장합니다. 커피나 차 같은 기본 음료에 우유, 설탕, 휘핑크림 등 여러 옵션을 동시에 추가할 수 있습니다. 옵션은 여러 개를 중첩해서 추가할 수 있으며, 각 옵션은 이전 단계의 가격에 자신의 가격을 더합니다. 최종 음료의 설명과 총 가격을 계산할 수 있어야 합니다.
구현 가이드:
Beverage (GetCost() 가상함수, GetDescription() 가상함수) → Coffee/Tea (기본 구현), CondimentDecorator는 Beverage를 상속하면서 내부에 다른 Beverage 포인터를 가지며, Milk/Sugar/Whip은 CondimentDecorator를 상속하여 기존 비용에 추가 비용을 더합니다.
Coffee coffee; Milk milkCoffee(&coffee); Sugar sweetMilkCoffee(&milkCoffee); Whip fancyCoffee(&sweetMilkCoffee); Beverage* drinks[] = { &coffee, &milkCoffee, &sweetMilkCoffee, &fancyCoffee }; for (int i = 0; i < 4; ++i) { std::cout << drinks[i]->GetDescription() << ": " << drinks[i]->GetCost() << std::endl; }
C++
복사

문제 42. 제품 생산 시스템

기획 요구사항:
공장에서 여러 종류의 제품을 생산합니다. 각 공장은 자신만의 방식으로 제품을 만들지만, 제품 생성 과정은 동일한 인터페이스로 처리되어야 합니다. 어떤 공장에서 만들어진 제품이든 동일한 방식으로 사용할 수 있어야 하며, 새로운 공장과 제품 종류를 추가하기 쉬워야 합니다.
구현 가이드:
Creator (CreateProduct() 순수 가상함수 반환 Product*) → ConcreteCreatorA/B (각각 ProductA/ProductB 생성)
Product (Use() 가상함수) → ProductA/ProductB (Use() 오버라이딩)
Creator 배열을 순회하며 각 Creator가 Product를 생성하고, 생성된 Product의 Use()를 호출하세요. (정적 할당 제약 하에서 해결)
ProductA productA; ProductB productB; ConcreteCreatorA creatorA(&productA); ConcreteCreatorB creatorB(&productB); Creator* creators[] = { &creatorA, &creatorB }; for (int i = 0; i < 2; ++i) { Product* product = creators[i]->CreateProduct(); product->Use(); }
C++
복사

문제 43. 이벤트 구독 시스템

기획 요구사항:
뉴스 발행 시스템을 만듭니다. 뉴스 발행자가 새 뉴스를 발행하면, 구독자들에게 자동으로 알림이 전송되어야 합니다. 구독자는 여러 명일 수 있으며, 각 구독자는 알림을 받았을 때 자신만의 방식으로 반응합니다. 발행자는 누가 구독하고 있는지 몰라도 되고, 구독자는 언제든 추가되거나 제거될 수 있어야 합니다.
구현 가이드:
Observer (OnNotify() 순수 가상함수), Subject (Notify() 가상함수, Observer 배열 보관)
ConcreteObserverA/B는 Observer를 상속하며 각각 다르게 OnNotify() 구현. Subject가 Notify()를 호출하면 등록된 모든 Observer의 OnNotify()가 실행됩니다.
Subject subject; ConcreteObserverA obsA; ConcreteObserverB obsB; subject.RegisterObserver(&obsA); subject.RegisterObserver(&obsB); subject.Notify(); // 모든 Observer에게 통지
C++
복사

문제 44. 게임 이벤트 시스템

기획 요구사항:
게임에서 중요한 이벤트가 발생하면 여러 시스템이 동시에 반응해야 합니다. 예를 들어 게임 오버가 되면, 플레이어는 동작을 멈추고, UI는 게임 오버 화면을 표시하고, 사운드 매니저는 게임 오버 음악을 재생해야 합니다. 이벤트가 발생했을 때 어떤 시스템들이 반응해야 하는지 미리 정하지 않고, 각 시스템이 관심 있는 이벤트에 등록하여 알림을 받을 수 있어야 합니다.
구현 가이드:
Observer (OnNotify(string event) 순수 가상함수), Subject (Notify(string event) 가상함수, Observer 배열 보관), ConcreteSubject는 상태 변화시 모든 Observer에게 알림.
Player/UI/SoundManager는 Observer를 상속하며 각각 다르게 OnNotify() 구현. Subject가 Notify()를 호출하면 등록된 모든 Observer의 OnNotify()가 실행됩니다.
ConcreteSubject subject; Player player; UI ui; SoundManager sound; subject.RegisterObserver(&player); subject.RegisterObserver(&ui); subject.RegisterObserver(&sound); subject.ChangeState("GameOver"); subject.Notify("GameOver"); // 모든 Observer에게 통지
C++
복사

문제 45. 단계별 요청 처리 시스템

기획 요구사항:
고객 지원 시스템을 만듭니다. 문의가 들어오면 먼저 1차 상담원이 처리를 시도하고, 처리할 수 없으면 2차 상담원에게 전달하고, 그래도 안 되면 관리자에게 전달됩니다. 각 단계의 처리자는 자신이 처리할 수 있는 난이도의 문의만 처리하고, 나머지는 다음 단계로 넘깁니다. 새로운 처리 단계를 추가하거나 순서를 변경하기 쉬워야 합니다.
구현 가이드:
Handler (순수 가상: HandleRequest(int level), SetNext(Handler*)) → ConcreteHandlerA/B/C (각각 구현)
각 핸들러는 자신이 처리할 수 있는 레벨의 요청을 처리하고, 처리할 수 없으면 다음 핸들러에게 전달합니다.
ConcreteHandlerA handlerA; ConcreteHandlerB handlerB; ConcreteHandlerC handlerC; handlerA.SetNext(&handlerB); handlerB.SetNext(&handlerC); handlerA.HandleRequest(1); handlerA.HandleRequest(3);
C++
복사

문제 46. 접근 제어 시스템

기획 요구사항:
민감한 리소스에 대한 접근을 제어하는 시스템을 만듭니다. 실제 리소스에 직접 접근하는 대신, 중간 단계를 거쳐서 접근 권한을 확인하거나 로그를 기록한 후에 실제 리소스를 사용할 수 있어야 합니다. 클라이언트 코드는 실제 리소스를 사용하는지 중간 단계를 거치는지 알 필요가 없어야 합니다.
구현 가이드:
Subject (순수 가상: Request()) → RealSubject/Proxy (각각 구현)
Proxy는 내부에 RealSubject 포인터를 가지며, Request() 호출 시 접근 제어 후 RealSubject의 Request()를 호출합니다.
RealSubject real; Proxy proxy(&real); Subject* subjects[] = { &real, &proxy }; for (int i = 0; i < 2; ++i) subjects[i]->Request();
C++
복사

문제 47. 인터페이스 변환 시스템

기획 요구사항:
기존에 사용하던 외부 라이브러리가 있는데, 현재 시스템의 인터페이스와 맞지 않습니다. 외부 라이브러리의 코드를 수정할 수 없으므로, 중간에서 인터페이스를 변환해주는 역할이 필요합니다. 시스템의 다른 부분은 변경하지 않고, 외부 라이브러리를 마치 원래 시스템의 일부인 것처럼 사용할 수 있어야 합니다.
구현 가이드:
Target (순수 가상: Request()) → Adapter (Adaptee를 포함하여 Request() 구현)
Adaptee (SpecificRequest() 구현)
Adapter는 Adaptee의 SpecificRequest()를 Target의 Request() 인터페이스로 변환합니다.
Adaptee adaptee; Adapter adapter(&adaptee); Target* targets[] = { &adapter }; for (int i = 0; i < 1; ++i) targets[i]->Request();
C++
복사

문제 48. 문서 처리 시스템

기획 요구사항:
여러 종류의 문서 요소(텍스트, 이미지 등)가 있고, 이들에 대해 다양한 작업(출력, 저장, 변환 등)을 수행해야 합니다. 새로운 작업을 추가할 때마다 모든 문서 요소 클래스를 수정하는 것은 비효율적입니다. 문서 요소의 코드는 변경하지 않으면서 새로운 작업을 자유롭게 추가할 수 있어야 하며, 각 작업은 문서 요소의 타입에 따라 다르게 동작해야 합니다.
구현 가이드:
Element (순수 가상: Accept(Visitor*)) → ConcreteElementA/B (각각 구현)
Visitor (순수 가상: Visit(ConcreteElementA), Visit(ConcreteElementB)) → ConcreteVisitor (구현)
각 요소가 비지터를 받아들이고, 비지터가 각 요소를 방문하며 동작을 수행합니다.
ConcreteElementA elementA; ConcreteElementB elementB; ConcreteVisitor visitor; Element* elements[] = { &elementA, &elementB }; for (int i = 0; i < 2; ++i) elements[i]->Accept(&visitor);
C++
복사

문제 49. 상태 저장/복원 시스템

기획 요구사항:
문서 편집기에서 실행 취소 기능을 구현합니다. 사용자가 문서를 수정할 때마다 현재 상태를 저장해두었다가, 실행 취소를 누르면 이전 상태로 돌아갈 수 있어야 합니다. 문서 객체의 내부 구조를 외부에 노출하지 않으면서도 상태를 저장하고 복원할 수 있어야 합니다.
구현 가이드:
Originator (CreateMemento(), RestoreMemento(Memento*)), Memento (상태 저장), Caretaker (Memento 보관)
Originator의 상태를 저장하고 복원하는 기능을 구현하세요.
Originator originator; originator.SetState("State1"); Memento memento1 = originator.CreateMemento(); originator.SetState("State2"); originator.ShowState(); originator.RestoreMemento(&memento1); originator.ShowState();
C++
복사

문제 50. 복합 게임 시스템 (종합)

기획 요구사항:
게임 엔진의 오브젝트 관리 시스템을 만듭니다. 게임에는 캐릭터, 아이템, 이펙트 등 다양한 오브젝트가 있으며, 모든 오브젝트는 매 프레임마다 업데이트되고 화면에 렌더링됩니다. 게임 매니저는 모든 오브젝트를 동일한 방식으로 관리해야 합니다. 추가로, 캐릭터는 입력을 받을 수 있고, 아이템은 수집될 수 있는 등 각 오브젝트 타입마다 고유한 기능이 있습니다. 특정 오브젝트가 여러 역할을 동시에 수행할 수 있어야 합니다.
구현 가이드:
GameObject (가상: Update(), Render()) → Character/Item/Effect (각각 구현)
GameManager는 GameObjectPool을 가지며, 모든 GameObject를 순회하며 Update()와 Render()를 호출합니다.
Character는 IControllable 인터페이스를 구현하여 OnInput() 추가, Item은 ICollectable 구현하여 OnCollect() 추가.
Character player("Hero"); Item sword("Sword"); Effect explosion("Boom"); GameObject* objects[] = { &player, &sword, &explosion }; for (int i = 0; i < 3; ++i) { objects[i]->Update(); objects[i]->Render(); } // Character의 경우 IControllable로 캐스팅하여 OnInput() 호출
C++
복사

Part 2 난이도별 문제 분포 (문제 26-50)

레벨
난이도
문제 번호
문제 수
핵심 개념
3
26-30
5문제
추상 클래스, 인터페이스 심화, 형변환
4
31-40
10문제
복잡한 계층, 디자인 패턴 기초
5
41-50
10문제
고급 디자인 패턴, 복합 시스템

전체 50문제 요약

레벨
난이도
Part 1
Part 2
총 문제 수
1
10문제 (1-10)
-
10문제
2
10문제 (11-20)
-
10문제
3
5문제 (21-25)
5문제 (26-30)
10문제
4
-
10문제 (31-40)
10문제
5
-
10문제 (41-50)
10문제
합계
25문제
25문제
50문제