C++ 상속 연습문제 50선 (동적할당 X) - Part 2
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++
복사
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:
void Start() { cout << "Engine started" << endl; }
void Stop() { cout << "Engine stopped" << endl; }
};
class Car
{
public:
Car(Engine* e) : engine(e) {}
void Drive()
{
if (engine)
{
engine->Start();
cout << "Driving..." << endl;
}
}
private:
Engine* engine; // 다른 객체를 포함
};
int main()
{
Engine engine;
Car car(&engine); // Engine 객체를 Car 객체에 전달
car.Drive(); // Car 객체의 Drive 메서드 호출
return 0;
}
C++
복사
•
클래스가 다른 객체의 포인터를 멤버로 보관
•
생성자에서 포인터를 초기화
•
외부에서 생성된 객체를 받아서 사용 (의존성 주입)
데코레이터 활용
데코레이터은 기존 객체에 새로운 기능을 동적으로 추가하는 패턴입니다.
컴포지션을 활용하여 원본 객체를 감싸고, 기능을 확장합니다.
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric> // iota 함수를 위해 필요
using namespace std;
class Beverage
{
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
{
public:
AddOn(Beverage* b) : beverage(b) {}
protected:
Beverage* beverage; // 다른 음료를 "감싸는" 패턴
};
class Milk : public AddOn
{
public:
Milk(Beverage* b) : AddOn(b) {}
int GetCost() override
{
return beverage->GetCost() + 500; // 기존 비용에 추가
}
string GetDescription() override
{
return beverage->GetDescription() + " + Milk";
}
};
int main()
{
Coffee* myCoffee = new Coffee();
cout << myCoffee->GetDescription() << " costs " << myCoffee->GetCost() << " won." << endl;
Beverage* myCoffeeWithMilk = new Milk(myCoffee);
cout << myCoffeeWithMilk->GetDescription() << " costs " << myCoffeeWithMilk->GetCost() << " won." << endl;
delete myCoffeeWithMilk; // Milk 객체 삭제
delete myCoffee; // Coffee 객체 삭제
return 0;
}
C++
복사
Composite(컴포지트) 패턴
게임 프로그래밍에서 Composite는 “단일 오브젝트(Leaf)와 오브젝트 묶음(Composite)을 같은 타입으로 취급”해서, 트리 구조를 한 번의 Update/Render 호출로 전체에 전파할 때 가장 빛납니다.
핵심: 컴포지션은 "객체를 다른 객체 안에 넣는다"는 개념입니다. 상속보다 유연하며, 런타임에 동작을 변경할 수 있습니다.
•
씬 그래프(Scene Graph): 부모-자식 Transform 계층(캐릭터 → 손 → 무기)처럼, 부모의 작업이 자식에게 자연스럽게 전달됩니다.
•
UI 트리: Panel(Composite) 안에 Button/Text(Leaf)가 들어가고, Panel의 Draw/HitTest가 자식들에게 재귀로 전달됩니다.
•
그룹 단위 처리: “이 오브젝트 묶음 전체를 숨김, 비활성화, 이동” 같은 요구를 그룹 1번 호출로 처리합니다.
•
공통 인터페이스(예: Update(), Render(), SetActive(bool))를 Component에 둡니다.
•
Leaf는 자기 동작만 수행합니다.
•
Composite는 자기 동작(선택) + 자식들에게 같은 메시지를 재귀 호출합니다.
•
부모 포인터 보관: 자식이 월드 변환 계산, 삭제 요청, 이벤트 버블링을 하려면 부모 참조가 필요합니다.
•
순회 중 수정 금지: Update 중에 Add/Remove하면 iterator/인덱스 꼬임이 생기기 쉬워서, 큐에 모아 프레임 끝에 반영합니다.
•
OnEnter/OnExit: 활성화 토글은 SetActive만으로 끝나지 않고, 사운드/애니메이션 시작·정지를 위해 진입/이탈 콜백을 두는 경우가 많습니다.
•
Composite는 계층(트리) 을 다루는 패턴이고,
•
ECS는 데이터/동작 분리 + 대량 처리에 강합니다.
정리: 게임에서는 “부모가 Update/Render/Transform을 호출하면 자식 전체가 따라오는 구조”가 필요할 때 Composite가 가장 직관적입니다.
예제 (Leaf/Composite 명확히 분리)
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric> // iota 함수를 위해 필요
using namespace std;
class Component
{
public:
virtual void Display() = 0;
virtual ~Component() {}
};
class Transform : public Component
{
private:
string name;
public:
Transform(string n) : name(n) {}
void Display() override
{
cout << "Transform: " << name << endl;
}
};
class Renderer : public Component
{
private:
string name;
public:
Renderer(string n) : name(n) {}
void Display() override
{
cout << "Renderer: " << name << endl;
}
};
class GameObject : public Component
{
private:
string name;
Component* components[10];
int childCount;
public:
GameObject(string n) : name(n), childCount(0) {}
void Add(Component* c)
{
components[childCount++] = c;
}
void Display() override
{
cout << "Composite: " << name << endl;
// 재귀적으로 모든 자식의 Display() 호출
for (int i = 0; i < childCount; ++i)
{
components[i]->Display(); // 여기서 재귀 발생
}
}
};
int main()
{
Transform tr("file1.txt");
Renderer renderer("file2.txt");
GameObject player("Folder1");
player.Add(&tr);
player.Add(&renderer);
player.Display();
return 0;
}
C++
복사
5. 다중 상속 (Multiple Inheritance)
다중 상속은 하나의 클래스가 여러 부모 클래스를 동시에 상속받는 것입니다.
C++에서는 주로 인터페이스 패턴으로 사용됩니다.
기본 문법
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 인터페이스 정의 (순수 가상 함수만 포함)
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;
}
};
int main()
{
GameObject player("Player");
player.Render();
player.Update();
player.CheckCollision();
return 0;
}
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. 함수 오버로딩과 비지터 패턴
함수 오버로딩은 같은 이름의 함수를 매개변수 타입만 다르게 여러 개 정의하는 것입니다.
비지터 패턴에서 타입별로 다른 동작을 구현할 때 유용합니다.
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric> // iota 함수를 위해 필요
using namespace std;
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();
}
};
int main()
{
ConcreteElementA elementA;
ConcreteElementB elementB;
ConcreteVisitor visitor;
elementA.Accept(&visitor); // A를 방문
elementB.Accept(&visitor); // B를 방문
return 0;
}
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++
복사
핵심: 포인터로 다음 객체를 가리켜 연결하면, 책임을 순차적으로 전달할 수 있습니다.
8. 정적 배열과 포인터 관리
동적 할당 없이 객체들을 관리하는 방법입니다.
Component* children[10]; // 최대 10개
int count = 0;
children[count++] = &newChild;
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*이므로 Sleep()을 바로 호출할 수 없습니다. 이럴 때 "이 동물이 포유류인지" 안전하게 확인하고 맞다면 Sleep()을 호출하려면 어떻게 해야 할까요?
구현 가이드:
Animal (Eat() 가상함수) → Mammal (Sleep() 가상함수 추가) → Dog/Cat (Speak() 가상함수 추가, 모든 함수 오버라이딩)
Animal 포인터 배열로 순회하며 Eat()만 호출 후, Mammal 이상인지 확인하여 Sleep()도 호출하도록 구현하세요.
핵심 포인트:
•
Animal은 모든 동물의 공통 기능(Eat)을 정의합니다
•
Mammal은 포유류만의 특별한 기능(Sleep)을 추가합니다
•
Dog와 Cat은 각자의 고유한 소리(Speak)를 냅니다
•
dynamic_cast<Mammal*>를 사용하면 "이 동물이 포유류인가?"를 런타임에 확인할 수 있습니다
•
변환에 성공하면 해당 포인터를 사용하고, 실패하면 nullptr이 반환됩니다
Dog d; Cat c;
Animal* animals[] = { &d, &c };
for (int i = 0; i < 2; ++i) {
animals[i]->Eat();
// Mammal인 경우 Sleep()도 호출 (dynamic_cast 활용)
if (Mammal* m = dynamic_cast<Mammal*>(animals[i])) {
m->Sleep();
}
}
C++
복사
문제 27. 스킬 시스템 with 추상 클래스
기획 요구사항:
RPG 게임의 스킬 시스템을 구현합니다. 모든 스킬은 실행 가능하고 쿨다운 시간이 있어야 합니다. 공격 스킬, 방어 스킬, 회복 스킬이 있으며, 각각 다른 효과를 가집니다. 스킬들을 동일한 방식으로 관리하고 사용할 수 있어야 합니다.
생각해볼 문제:
공격, 방어, 회복 스킬은 완전히 다른 효과를 가지지만, 게임 시스템에서는 "스킬"이라는 하나의 개념으로 다뤄야 합니다. 스킬 배열을 만들어서 반복문으로 처리하려면 어떻게 설계해야 할까요? 모든 스킬이 공통적으로 가져야 하는 기능은 무엇이고, 각 스킬마다 다르게 구현해야 하는 부분은 무엇일까요?
구현 가이드:
Skill (순수 가상: Execute(), GetCooldown()) → AttackSkill/DefenseSkill/HealSkill (각각 구현)
각 스킬을 실행하고 쿨다운을 출력하세요.
핵심 포인트:
•
Skill은 추상 클래스로서 모든 스킬이 가져야 할 공통 인터페이스를 정의합니다
•
Execute()는 스킬마다 다른 효과를 냅니다 (공격, 방어, 회복)
•
GetCooldown()은 각 스킬의 재사용 대기 시간을 반환합니다
•
스킬 배열을 순회하면서 어떤 종류의 스킬인지 몰라도 Execute()와 GetCooldown()을 호출할 수 있습니다
•
이렇게 하면 나중에 새로운 스킬(버프, 디버프 등)을 추가할 때도 기존 코드를 수정할 필요가 없습니다
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원으로 계산합니다.
학습 목표:
•
추상 클래스를 사용하여 다양한 알고리즘(배송비 계산 방식)을 동일한 인터페이스로 다루는 방법을 배웁니다
•
각 구현체가 서로 다른 데이터(거리)를 필요로 할 때 어떻게 설계하는지 익힙니다
•
실제 비즈니스 로직을 객체지향적으로 모델링하는 경험을 쌓습니다
핵심 포인트:
•
Delivery 추상 클래스는 모든 배송 방식의 공통 인터페이스를 정의합니다
•
StandardDelivery는 생성자에서 특별한 매개변수가 필요 없지만, ExpressDelivery는 거리 정보가 필요합니다
•
Calculate() 메서드가 각 배송 방식에 따라 다른 로직으로 구현됩니다
•
클라이언트 코드는 배송비가 어떻게 계산되는지 몰라도 됩니다
설계의 장점:
배송비 계산 로직이 각 배송 방식 클래스 안에 캡슐화되어 있습니다. 나중에 새로운 배송 방식(당일 배송, 새벽 배송 등)을 추가하거나 기존 배송비를 변경할 때 다른 코드에 영향을 주지 않습니다.
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 (각각 구현)
각 인증 방식으로 로그인을 시도하고 결과를 출력하세요.
학습 목표:
•
보안 시스템처럼 중요한 기능에서 추상화가 어떻게 코드의 안정성을 높이는지 이해합니다
•
같은 매개변수를 받지만 내부 처리 로직이 완전히 다른 함수들을 추상 클래스로 묶는 방법을 배웁니다
•
새로운 인증 방식(지문 인식, 얼굴 인식 등)을 쉽게 추가할 수 있는 확장 가능한 설계를 경험합니다
핵심 포인트:
•
Authenticator는 인증이라는 행위의 인터페이스를 정의합니다
•
PasswordAuth는 username과 password를 비교하여 인증합니다
•
BiometricAuth는 생체 정보를 검증하는 로직을 구현합니다 (실제로는 password 매개변수를 생체 데이터로 사용)
•
클라이언트 코드는 어떤 인증 방식을 사용하든 Authenticate() 하나만 호출하면 됩니다
실무 응용:
이런 설계는 OAuth, JWT, Session 등 다양한 인증 방식을 지원하는 실제 웹 서비스에서 자주 사용됩니다. 각 인증 방식의 구현을 독립적으로 관리할 수 있기 때문입니다.
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 (각각 구현)
각 필터를 적용한 결과를 출력하세요.
학습 목표:
•
입력을 받아 변환된 출력을 반환하는 함수형 프로그래밍 스타일을 객체지향으로 구현하는 방법을 배웁니다
•
여러 필터를 체인처럼 연결하여 순차적으로 적용할 수 있는 확장 가능한 구조를 이해합니다
•
동일한 인터페이스를 가진 다양한 변환 로직을 구현하는 경험을 쌓습니다
핵심 포인트:
•
Filter는 문자열 변환이라는 행위의 인터페이스를 정의합니다
•
Apply() 메서드는 입력 문자열을 받아 변환된 문자열을 반환합니다
•
각 필터는 독립적으로 동작하며, 필요에 따라 조합할 수 있습니다
•
새로운 필터(공백 제거, 특수문자 제거 등)를 추가하기 매우 쉽습니다
실무 응용:
이미지 처리, 데이터 정제(Data Cleaning), 파이프라인 처리 등에서 이와 유사한 설계가 사용됩니다. 여러 필터를 순차적으로 적용하는 것도 가능합니다.
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 배열로 순회하며 각 유닛이 공격, 방어를 사용하도록 구현하세요.
학습 목표:
•
다단계 상속에서 각 레벨이 담당하는 책임을 명확히 분리하는 설계 방법을 배웁니다
•
여러 개의 가상 함수를 가진 복잡한 인터페이스를 구현하는 경험을 쌓습니다
•
게임 개발에서 실제로 사용되는 엔티티-컴포넌트 계층 구조의 기초를 이해합니다
핵심 포인트:
•
Entity는 모든 게임 객체의 기본 속성(체력)을 담당합니다
•
Combatant는 전투 관련 행동(공격, 방어)을 추가합니다
•
Warrior/Mage/Archer는 각자의 전투 스타일을 구현합니다
•
같은 Combatant 인터페이스를 사용하므로 전투 로직이 단순해집니다
디자인 관점:
이런 계층 구조는 게임 엔진에서 GameObject → Character → PlayerCharacter/NPCCharacter 같은 형태로 자주 사용됩니다.
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. 계층적 메뉴 시스템 (Composite Pattern)
기획 요구사항:
UI 메뉴 시스템을 만듭니다. 개별 메뉴 항목도 있고, 여러 항목을 그룹으로 묶는 폴더도 있습니다. 폴더 안에는 다른 항목들이나 폴더가 들어갈 수 있습니다. 메뉴를 표시할 때 개별 항목이든 폴더든 동일한 방식으로 다루면서, 폴더는 자동으로 내부의 모든 항목을 표시해야 합니다.
구현 가이드:
Component (순수 가상: Display()) → Leaf (구현), Composite (자식 배열 보관, 재귀 Display())
Composite는 여러 Leaf를 포함할 수 있으며, Display()는 모든 하위 항목을 출력합니다.
학습 목표:
•
Composite Pattern: 개별 객체와 객체들의 집합을 동일하게 다루는 강력한 디자인 패턴을 배웁니다
•
재귀 함수를 사용하여 트리 구조를 순회하는 방법을 익힙니다
•
계층 구조를 표현하고 관리하는 객체지향적 접근법을 경험합니다
핵심 포인트:
•
Component는 개별 항목(Leaf)과 그룹(Composite) 모두의 공통 인터페이스입니다
•
Leaf는 더 이상 하위 항목이 없는 단말 노드입니다
•
Composite는 여러 Component를 포함할 수 있으며, 다른 Composite도 포함 가능합니다 (재귀적 구조)
•
Display()를 호출하면 Composite는 자신의 모든 자식에 대해 재귀적으로 Display()를 호출합니다
디자인 패턴: Composite Pattern
이 패턴은 파일 시스템, UI 컴포넌트 트리, 조직도 등 계층 구조가 필요한 모든 곳에서 사용됩니다.
재귀의 이해:
•
Composite.Display()가 호출되면 → 자식들의 Display()를 호출
•
자식이 또 Composite라면 → 그 자식의 자식들의 Display()를 호출
•
자식이 Leaf라면 → 그냥 출력하고 종료 (재귀의 종료 조건)
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. 파일 시스템 계층 구조 (Composite Pattern 심화)
기획 요구사항:
파일 탐색기를 만듭니다. 파일과 폴더가 있으며, 폴더 안에는 파일이나 다른 폴더가 들어갈 수 있습니다. 파일은 자신의 크기를 가지고, 폴더는 내부의 모든 파일과 하위 폴더들의 크기를 합친 전체 크기를 계산해야 합니다. 파일이든 폴더든 동일한 방식으로 정보를 표시하고 크기를 조회할 수 있어야 합니다.
구현 가이드:
FileSystemNode (이름, GetSize() 순수 가상, Display() 순수 가상) → File (크기 저장, 구현), Folder (자식 노드 배열 저장, 재귀적으로 크기 합산 및 표시)
Folder는 여러 File/Folder를 포함할 수 있으며, GetSize()는 모든 하위 항목의 크기를 합산합니다.
학습 목표:
•
Composite 패턴을 실제 파일 시스템에 적용하여 더 깊이 이해합니다
•
재귀적으로 데이터를 집계(aggregate)하는 방법을 배웁니다
•
트리 구조에서 부분(파일)과 전체(폴더)를 동일하게 다루는 강력함을 경험합니다
핵심 포인트:
•
File의 GetSize()는 자신의 크기를 반환합니다
•
Folder의 GetSize()는 모든 자식의 GetSize()를 재귀적으로 호출하여 합산합니다
•
이 재귀적 합산 덕분에 폴더 안의 폴더 안의 파일 크기까지 모두 계산됩니다
•
클라이언트는 File인지 Folder인지 신경 쓰지 않고 GetSize()를 호출할 수 있습니다
디자인 패턴: Composite Pattern (심화)
문제 32보다 한 단계 발전하여, 단순 표시뿐 아니라 데이터 집계까지 수행합니다.
재귀 집계의 예:
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. 정렬 알고리즘 선택 시스템 (Strategy Pattern)
기획 요구사항:
데이터를 정렬하는 프로그램을 만듭니다. 버블 정렬, 퀵 정렬 등 여러 정렬 알고리즘이 있으며, 실행 중에 사용자가 원하는 알고리즘으로 변경할 수 있어야 합니다. 정렬 로직은 쉽게 교체 가능하도록 설계하되, 정렬을 실행하는 코드는 변경되지 않아야 합니다.
구현 가이드:
SortStrategy (순수 가상: Sort()) → BubbleSort/QuickSort (구현), Sorter 클래스는 SortStrategy 포인터를 멤버로 가지며 SetStrategy()로 전략 교체 가능.
학습 목표:
•
Strategy Pattern: 알고리즘을 캡슐화하고 런타임에 교체할 수 있는 패턴을 배웁니다
•
컴포지션(has-a)을 활용하여 동작을 외부에서 주입받는 설계를 익힙니다
•
클라이언트 코드의 변경 없이 알고리즘을 교체하는 개방-폐쇄 원칙(OCP)을 경험합니다
핵심 포인트:
•
SortStrategy는 정렬 알고리즘의 인터페이스를 정의합니다
•
Sorter는 내부에 SortStrategy 포인터를 가지며, 이를 통해 정렬을 위임합니다
•
SetStrategy()를 통해 런타임에 알고리즘을 변경할 수 있습니다
•
Sorter 클래스는 정렬 알고리즘이 어떻게 동작하는지 몰라도 됩니다 (의존성 역전)
디자인 패턴: Strategy Pattern (전형적인 예제)
이 문제는 Strategy 패턴의 교과서적인 예시입니다. GoF 디자인 패턴 책에서도 정렬 알고리즘으로 설명합니다.
장점:
•
알고리즘의 독립적인 변경 가능
•
조건문 제거 (if-else로 알고리즘 선택 대신 객체 교체)
•
새로운 알고리즘 추가가 기존 코드에 영향을 주지 않음
BubbleSort bubble; QuickSort quick;
Sorter sorter(&bubble);
sorter.PerformSort(); // BubbleSort 사용
sorter.SetStrategy(&quick);
sorter.PerformSort(); // QuickSort 사용
C++
복사
문제 35. 음료 옵션 추가 시스템 (Decorator Pattern 기초)
기획 요구사항:
카페 주문 시스템을 만듭니다. 기본 음료(커피)가 있고, 우유 같은 추가 옵션을 선택할 수 있습니다. 추가 옵션은 기본 음료 가격에 옵션 가격을 더한 총 금액을 계산해야 합니다. 기본 음료든 옵션이 추가된 음료든 동일한 방식으로 가격을 조회할 수 있어야 합니다.
구현 가이드:
Beverage (GetCost() 가상함수) → Coffee (기본 구현), AddOn은 Beverage를 상속하면서 내부에 다른 Beverage 포인터를 가지며, Milk는 AddOn을 상속하여 기존 비용에 추가.
학습 목표:
•
Decorator Pattern: 기존 객체에 동적으로 기능을 추가하는 패턴을 배웁니다
•
상속과 컴포지션을 동시에 사용하는 고급 기법을 익힙니다
•
객체를 "감싸서" 기능을 확장하는 래핑(Wrapping) 개념을 이해합니다
핵심 포인트:
•
Beverage는 음료의 공통 인터페이스입니다
•
Coffee는 기본 음료로 독립적으로 동작합니다
•
AddOn은 Beverage를 상속받으면서 동시에 다른 Beverage를 포함합니다 (is-a + has-a)
•
Milk는 내부의 Beverage에게 GetCost()를 위임하고, 자신의 비용을 추가합니다
디자인 패턴: Decorator Pattern (기초)
이 패턴의 핵심은 "객체를 감싸서 기능을 추가한다"는 것입니다. 상속으로 모든 조합을 만들면 클래스가 폭발적으로 증가하지만, 데코레이터는 조합을 런타임에 구성합니다.
왜 상속이 아닌 컴포지션인가?
•
우유 커피, 설탕 커피, 우유 설탕 커피... 모든 조합마다 클래스를 만들 수 없습니다
•
데코레이터를 중첩하면 무한한 조합이 가능합니다
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. 로깅 시스템 (Strategy Pattern 응용)
기획 요구사항:
시스템 로그를 기록하는 프로그램을 만듭니다. 콘솔에 출력, 파일에 저장, 네트워크로 전송 등 여러 방식으로 로그를 기록할 수 있어야 합니다. 로그를 기록하는 코드는 어떤 방식을 사용하든 동일하게 작성되어야 하며, 나중에 새로운 로깅 방식을 추가하기 쉬워야 합니다.
구현 가이드:
Logger (순수 가상: Log(string message)) → ConsoleLogger/FileLogger/NetworkLogger (각각 구현)
각 로거로 메시지를 기록하고 출력 방식을 다르게 구현하세요.
학습 목표:
•
Strategy 패턴을 로깅이라는 실무적인 문제에 적용하는 방법을 배웁니다
•
같은 데이터(로그 메시지)를 다양한 방식으로 처리하는 유연한 설계를 익힙니다
•
실제 소프트웨어에서 자주 사용되는 로깅 시스템의 구조를 이해합니다
핵심 포인트:
•
Logger 인터페이스는 로그를 기록하는 방법을 추상화합니다
•
ConsoleLogger는 stdout에 출력합니다
•
FileLogger는 파일에 쓰기 작업을 수행합니다 (실제로는 시뮬레이션)
•
NetworkLogger는 원격 서버로 전송합니다 (실제로는 시뮬레이션)
•
로그를 남기는 코드는 Logger* 인터페이스만 사용하므로 구현체를 몰라도 됩니다
실무 응용:
실제 서비스에서는 개발 환경에서는 콘솔로, 운영 환경에서는 파일이나 클라우드 로깅 서비스로 전환합니다. 이때 Strategy 패턴을 사용하면 로깅 코드를 전혀 수정하지 않고 전략만 교체하면 됩니다.
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 Pattern)
기획 요구사항:
기기의 상태를 관리하는 시스템을 만듭니다. 기기는 대기 중, 실행 중, 정지됨 등 여러 상태를 가질 수 있으며, 각 상태에서 요청을 받았을 때 다르게 동작합니다. 현재 상태에 따라 자동으로 적절한 동작이 수행되어야 합니다.
구현 가이드:
State (순수 가상: Handle()) → IdleState/RunningState/StoppedState (각각 구현)
각 상태에서의 동작을 출력하세요.
학습 목표:
•
State Pattern: 객체의 상태에 따라 행동이 달라지는 패턴을 배웁니다
•
복잡한 조건문(if-else, switch)을 객체로 대체하는 방법을 익힙니다
•
상태 전이(State Transition)를 객체지향적으로 관리하는 경험을 쌓습니다
핵심 포인트:
•
State 인터페이스는 상태에서 수행할 행동을 정의합니다
•
각 구체적인 상태(Idle, Running, Stopped)는 State를 구현합니다
•
같은 Handle() 호출이지만 현재 상태 객체에 따라 다른 동작이 실행됩니다
•
상태 객체를 교체하면 자동으로 동작이 바뀝니다
디자인 패턴: State Pattern
이 패턴은 Strategy 패턴과 비슷해 보이지만, 목적이 다릅니다:
•
Strategy: 알고리즘을 교체 가능하게 (외부에서 선택)
•
State: 상태에 따라 동작이 변경 (내부 상태에 따라 자동 전환)
실무 응용:
게임 캐릭터의 상태(Idle, Walking, Running, Jumping), 네트워크 연결 상태(Disconnected, Connecting, Connected), UI 상태(Normal, Disabled, Focused) 등에서 사용됩니다.
IdleState idle; RunningState running; StoppedState stopped;
State* states[] = { &idle, &running, &stopped };
for (int i = 0; i < 3; ++i)
states[i]->Handle();
C++
복사
문제 38. 게임 명령 실행/취소 시스템 (Command Pattern)
기획 요구사항:
게임에서 플레이어의 행동을 관리하는 시스템을 만듭니다. 이동, 공격 등의 행동을 수행할 수 있고, 각 행동은 실행 취소(Undo)가 가능해야 합니다. 모든 행동은 동일한 방식으로 실행하고 취소할 수 있어야 하며, 나중에 새로운 행동을 추가하기 쉬워야 합니다.
구현 가이드:
Command (순수 가상: Execute(), Undo()) → MoveCommand/AttackCommand (각각 구현)
각 명령을 실행하고 취소하는 동작을 출력하세요.
학습 목표:
•
Command Pattern: 행동을 객체로 캡슐화하는 패턴을 배웁니다
•
실행 취소(Undo) 기능을 구현하는 방법을 익힙니다
•
행동의 히스토리를 관리하고 재실행할 수 있는 구조를 이해합니다
핵심 포인트:
•
Command는 하나의 행동을 나타내는 인터페이스입니다
•
Execute()는 행동을 수행합니다
•
Undo()는 Execute()의 효과를 되돌립니다
•
명령 객체를 배열에 저장하면 명령 히스토리를 관리할 수 있습니다
디자인 패턴: Command Pattern
이 패턴은 행동을 데이터처럼 다룰 수 있게 합니다. 함수 호출을 객체로 만들어 저장, 전달, 취소가 가능합니다.
실무 응용:
•
문서 편집기의 실행 취소/다시 실행 (Undo/Redo)
•
게임의 리플레이 시스템
•
트랜잭션 관리
•
매크로 기록
Undo 구현 팁:
MoveCommand.Execute()가 "북쪽으로 이동"이라면, Undo()는 "남쪽으로 이동"이어야 합니다. 이를 위해 Command는 이전 상태를 기억해야 할 수 있습니다.
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 Pattern)
기획 요구사항:
복잡한 제품을 단계별로 조립하는 시스템을 만듭니다. 제품은 여러 부품으로 구성되며, 부품 조립 순서는 정해져 있습니다. 같은 조립 과정이지만 다른 부품을 사용하여 다양한 종류의 제품을 만들 수 있어야 합니다. 조립 과정은 동일하되, 실제 부품 선택은 조립 방식에 따라 달라집니다.
구현 가이드:
Builder (순수 가상: BuildPart1(), BuildPart2(), GetResult()) → ConcreteBuilderA/B (각각 구현)
각 빌더로 객체를 생성하고 결과를 출력하세요.
학습 목표:
•
Builder Pattern: 복잡한 객체의 생성 과정을 단계별로 분리하는 패턴을 배웁니다
•
동일한 생성 과정으로 다른 결과물을 만드는 방법을 익힙니다
•
객체 생성 로직을 캡슐화하여 코드의 가독성을 높이는 경험을 쌓습니다
핵심 포인트:
•
Builder 인터페이스는 객체 생성의 각 단계를 메서드로 정의합니다
•
ConcreteBuilder들은 같은 단계를 거치지만 각 단계에서 다른 부품을 선택합니다
•
GetResult()는 완성된 제품을 반환합니다
•
복잡한 생성 과정을 숨기고 간단한 인터페이스만 제공합니다
디자인 패턴: Builder Pattern
이 패턴은 객체 생성이 복잡할 때 사용합니다. 생성자에 매개변수가 너무 많아지거나, 생성 과정이 여러 단계로 나뉠 때 유용합니다.
실무 응용:
•
게임 캐릭터 생성 (외형, 능력치, 장비 등 단계별 설정)
•
문서 생성 (헤더, 본문, 푸터 등 단계별 작성)
•
SQL 쿼리 빌더 (SELECT, FROM, WHERE 등 단계별 구성)
Template Method와의 차이:
Builder는 같은 과정으로 다른 결과를 만드는 데 집중하고, Template Method는 알고리즘의 골격을 정의하는 데 집중합니다.
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. 작업 처리 프로세스 시스템 (Template Method Pattern)
기획 요구사항:
정해진 순서로 작업을 처리하는 시스템을 만듭니다. 모든 작업은 1단계, 2단계를 순서대로 거쳐야 하지만, 각 단계에서 하는 구체적인 작업 내용은 작업 유형마다 다릅니다. 전체 작업 흐름은 고정하되, 각 단계의 세부 구현은 유연하게 바꿀 수 있어야 합니다.
구현 가이드:
AbstractClass (가상함수: TemplateMethod(), 순수 가상: Step1(), Step2()) → ConcreteClassA/B (Step1, Step2 구현)
TemplateMethod()는 Step1(), Step2()를 순서대로 호출합니다.
학습 목표:
•
Template Method Pattern: 알고리즘의 골격을 정의하고 세부 단계를 자식 클래스에 위임하는 패턴을 배웁니다
•
변하지 않는 부분(전체 흐름)과 변하는 부분(각 단계)을 분리하는 설계를 익힙니다
•
상속을 활용한 코드 재사용과 확장의 균형을 경험합니다
핵심 포인트:
•
AbstractClass의 TemplateMethod()는 전체 알고리즘의 골격을 정의합니다 (변경 불가)
•
Step1(), Step2() 등은 순수 가상 함수로 선언되어 자식이 반드시 구현해야 합니다 (변경 가능)
•
자식 클래스는 각 단계의 세부 구현만 제공하면 됩니다
•
전체 흐름은 부모가 제어하므로 일관성이 보장됩니다
디자인 패턴: Template Method Pattern
이 패턴은 "할리우드 원칙(Hollywood Principle)"을 따릅니다: "우리가 당신을 호출할게요, 당신이 우리를 호출하지 마세요."
Builder Pattern과의 차이:
•
Template Method: 전체 알고리즘의 순서와 구조를 고정하고, 각 단계만 커스터마이징
•
Builder: 생성 과정은 같지만 결과물이 다름
실무 응용:
•
데이터 처리 파이프라인 (읽기 → 검증 → 변환 → 저장)
•
게임 AI의 행동 트리 (인식 → 판단 → 행동)
•
테스트 프레임워크 (Setup → Test → Teardown)
ConcreteClassA a; ConcreteClassB b;
AbstractClass* classes[] = { &a, &b };
for (int i = 0; i < 2; ++i)
classes[i]->TemplateMethod();
C++
복사
레벨 5: 고급 디자인 패턴과 복합 시스템 (문제 41-50)
문제 41. 다중 옵션 음료 시스템 (Decorator Pattern 심화)
기획 요구사항:
카페 주문 시스템을 확장합니다. 커피나 차 같은 기본 음료에 우유, 설탕, 휘핑크림 등 여러 옵션을 동시에 추가할 수 있습니다. 옵션은 여러 개를 중첩해서 추가할 수 있으며, 각 옵션은 이전 단계의 가격에 자신의 가격을 더합니다. 최종 음료의 설명과 총 가격을 계산할 수 있어야 합니다.
구현 가이드:
Beverage (GetCost() 가상함수, GetDescription() 가상함수) → Coffee/Tea (기본 구현), CondimentDecorator는 Beverage를 상속하면서 내부에 다른 Beverage 포인터를 가지며, Milk/Sugar/Whip은 CondimentDecorator를 상속하여 기존 비용에 추가 비용을 더합니다.
학습 목표:
•
Decorator Pattern을 여러 레이어로 중첩하여 사용하는 방법을 배웁니다
•
객체를 감싸고 또 감싸서 기능을 누적적으로 추가하는 강력한 기법을 익힙니다
•
상속의 폭발(모든 조합마다 클래스 생성)을 피하고 조합을 런타임에 구성하는 방법을 이해합니다
핵심 포인트:
•
CondimentDecorator는 Beverage를 상속받으면서 동시에 포함합니다
•
각 데코레이터는 내부 Beverage에게 작업을 위임하고 자신의 기능을 추가합니다
•
Whip(Sugar(Milk(Coffee))) 같은 중첩 구조가 가능합니다
•
GetCost()와 GetDescription()이 재귀적으로 호출되어 누적됩니다
디자인 패턴: Decorator Pattern (심화)
문제 35의 확장판으로, 여러 데코레이터를 중첩하는 실전 활용법을 다룹니다.
중첩 호출의 흐름:
실무 응용:
•
I/O 스트림 (BufferedReader(FileReader(file)))
•
GUI 컴포넌트 (ScrollPane(Border(Panel)))
•
미들웨어 체인 (로깅 → 인증 → 압축 → 응답)
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. 제품 생산 시스템 (Factory Method Pattern)
기획 요구사항:
공장에서 여러 종류의 제품을 생산합니다. 각 공장은 자신만의 방식으로 제품을 만들지만, 제품 생성 과정은 동일한 인터페이스로 처리되어야 합니다. 어떤 공장에서 만들어진 제품이든 동일한 방식으로 사용할 수 있어야 하며, 새로운 공장과 제품 종류를 추가하기 쉬워야 합니다.
구현 가이드:
Creator (CreateProduct() 순수 가상함수 반환 Product*) → ConcreteCreatorA/B (각각 ProductA/ProductB 생성)
Product (Use() 가상함수) → ProductA/ProductB (Use() 오버라이딩)
Creator 배열을 순회하며 각 Creator가 Product를 생성하고, 생성된 Product의 Use()를 호출하세요. (정적 할당 제약 하에서 해결)
학습 목표:
•
Factory Method Pattern: 객체 생성을 서브클래스에 위임하는 패턴을 배웁니다
•
생성할 객체의 타입을 결정하는 로직을 캡슐화하는 방법을 익힙니다
•
객체 생성과 사용을 분리하여 결합도를 낮추는 설계를 경험합니다
핵심 포인트:
•
Creator는 Product를 생성하는 팩토리 메서드를 정의합니다
•
각 ConcreteCreator는 어떤 Product를 만들지 결정합니다
•
클라이언트는 Creator만 알면 되고, 구체적인 Product 타입을 몰라도 됩니다
•
정적 할당 제약 때문에 Creator가 미리 생성된 Product의 포인터를 반환하는 방식으로 구현할 수 있습니다
디자인 패턴: Factory Method Pattern
이 패턴은 객체 생성의 책임을 서브클래스로 넘깁니다. "어떤 클래스의 인스턴스를 만들까?"라는 결정을 서브클래스가 합니다.
실무 응용:
•
UI 라이브러리 (WindowsButton, MacButton 생성)
•
데이터베이스 연결 (MySQLConnection, PostgreSQLConnection 생성)
•
파일 파서 (JSONParser, XMLParser 생성)
정적 할당 제약 해결:
동적 할당이 금지되어 있으므로, Creator의 생성자에서 미리 만들어진 Product를 받거나, Creator 내부에 Product 멤버 변수를 두고 그 주소를 반환할 수 있습니다.
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 Pattern 기초)
기획 요구사항:
뉴스 발행 시스템을 만듭니다. 뉴스 발행자가 새 뉴스를 발행하면, 구독자들에게 자동으로 알림이 전송되어야 합니다. 구독자는 여러 명일 수 있으며, 각 구독자는 알림을 받았을 때 자신만의 방식으로 반응합니다. 발행자는 누가 구독하고 있는지 몰라도 되고, 구독자는 언제든 추가되거나 제거될 수 있어야 합니다.
구현 가이드:
Observer (OnNotify() 순수 가상함수), Subject (Notify() 가상함수, Observer 배열 보관)
ConcreteObserverA/B는 Observer를 상속하며 각각 다르게 OnNotify() 구현. Subject가 Notify()를 호출하면 등록된 모든 Observer의 OnNotify()가 실행됩니다.
학습 목표:
•
Observer Pattern: 일대다 의존 관계를 정의하여 상태 변화를 자동으로 전파하는 패턴을 배웁니다
•
느슨한 결합(Loose Coupling)을 통해 확장 가능한 시스템을 설계하는 방법을 익힙니다
•
이벤트 주도 프로그래밍의 기초를 이해합니다
핵심 포인트:
•
Subject는 Observer들의 리스트를 관리합니다
•
Observer는 알림을 받을 수 있는 인터페이스를 정의합니다
•
Subject의 상태가 변경되면 Notify()를 호출하여 모든 Observer에게 알립니다
•
Subject는 Observer의 구체적인 타입을 몰라도 됩니다 (인터페이스만 사용)
디자인 패턴: Observer Pattern (기초)
이 패턴은 MVC 아키텍처의 핵심이며, 이벤트 시스템의 기본입니다.
실무 응용:
•
GUI 이벤트 리스너 (버튼 클릭 시 여러 핸들러 실행)
•
데이터 바인딩 (모델 변경 시 뷰 자동 업데이트)
•
주식 가격 알림 (가격 변동 시 구독자들에게 통지)
왜 배열로 관리하는가?
구독자가 여러 명이므로 배열로 저장하고, Notify()에서 반복문으로 모두에게 알립니다.
Subject subject;
ConcreteObserverA obsA; ConcreteObserverB obsB;
subject.RegisterObserver(&obsA);
subject.RegisterObserver(&obsB);
subject.Notify(); // 모든 Observer에게 통지
C++
복사
문제 44. 게임 이벤트 시스템 (Observer Pattern 심화)
기획 요구사항:
게임에서 중요한 이벤트가 발생하면 여러 시스템이 동시에 반응해야 합니다. 예를 들어 게임 오버가 되면, 플레이어는 동작을 멈추고, UI는 게임 오버 화면을 표시하고, 사운드 매니저는 게임 오버 음악을 재생해야 합니다. 이벤트가 발생했을 때 어떤 시스템들이 반응해야 하는지 미리 정하지 않고, 각 시스템이 관심 있는 이벤트에 등록하여 알림을 받을 수 있어야 합니다.
구현 가이드:
Observer (OnNotify(string event) 순수 가상함수), Subject (Notify(string event) 가상함수, Observer 배열 보관), ConcreteSubject는 상태 변화시 모든 Observer에게 알림.
Player/UI/SoundManager는 Observer를 상속하며 각각 다르게 OnNotify() 구현. Subject가 Notify()를 호출하면 등록된 모든 Observer의 OnNotify()가 실행됩니다.
학습 목표:
•
Observer 패턴에 이벤트 타입(문자열) 정보를 추가하여 더 유연한 시스템을 만드는 방법을 배웁니다
•
여러 독립적인 시스템들이 이벤트를 통해 느슨하게 결합되는 아키텍처를 익힙니다
•
게임 엔진에서 실제로 사용되는 이벤트 시스템의 구조를 이해합니다
핵심 포인트:
•
OnNotify()가 이벤트 타입(string)을 매개변수로 받아 어떤 이벤트인지 알 수 있습니다
•
각 Observer는 관심 있는 이벤트에만 반응하도록 구현할 수 있습니다
•
하나의 이벤트로 여러 시스템이 동시에 반응합니다 (디커플링)
•
새로운 시스템을 추가할 때 기존 코드를 수정하지 않아도 됩니다
디자인 패턴: Observer Pattern (심화)
문제 43보다 발전하여 이벤트 데이터를 전달하는 방법을 다룹니다.
게임 엔진에서의 활용:
모든 시스템이 독립적으로 동작하므로 한 시스템의 버그가 다른 시스템에 영향을 주지 않습니다.
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. 단계별 요청 처리 시스템 (Chain of Responsibility Pattern)
기획 요구사항:
고객 지원 시스템을 만듭니다. 문의가 들어오면 먼저 1차 상담원이 처리를 시도하고, 처리할 수 없으면 2차 상담원에게 전달하고, 그래도 안 되면 관리자에게 전달됩니다. 각 단계의 처리자는 자신이 처리할 수 있는 난이도의 문의만 처리하고, 나머지는 다음 단계로 넘깁니다. 새로운 처리 단계를 추가하거나 순서를 변경하기 쉬워야 합니다.
구현 가이드:
Handler (순수 가상: HandleRequest(int level), SetNext(Handler*)) → ConcreteHandlerA/B/C (각각 구현)
각 핸들러는 자신이 처리할 수 있는 레벨의 요청을 처리하고, 처리할 수 없으면 다음 핸들러에게 전달합니다.
학습 목표:
•
Chain of Responsibility Pattern: 요청을 처리할 핸들러를 체인으로 연결하는 패턴을 배웁니다
•
포인터 체이닝을 통해 연결 리스트 같은 구조를 만드는 방법을 익힙니다
•
요청 처리의 책임을 분산시키고 각 단계를 독립적으로 관리하는 설계를 경험합니다
핵심 포인트:
•
각 Handler는 next 포인터로 다음 Handler를 가리킵니다
•
HandleRequest()에서 자신이 처리할 수 있으면 처리하고, 아니면 next에게 전달합니다
•
SetNext()로 체인을 동적으로 구성할 수 있습니다
•
요청을 보내는 쪽은 누가 처리할지 몰라도 됩니다
디자인 패턴: Chain of Responsibility Pattern
이 패턴은 요청의 발신자와 수신자를 분리합니다. 요청을 보내는 쪽은 누가 처리할지 결정하지 않습니다.
실무 응용:
•
로그 레벨 필터링 (DEBUG → INFO → WARN → ERROR)
•
예외 처리 체인
•
GUI 이벤트 버블링 (자식 → 부모로 이벤트 전파)
•
승인 워크플로우 (직원 → 팀장 → 부서장 → 임원)
체인의 장점:
•
체인의 순서를 쉽게 변경 가능
•
새로운 핸들러 추가 용이
•
각 핸들러는 독립적으로 테스트 가능
ConcreteHandlerA handlerA; ConcreteHandlerB handlerB; ConcreteHandlerC handlerC;
handlerA.SetNext(&handlerB);
handlerB.SetNext(&handlerC);
handlerA.HandleRequest(1);
handlerA.HandleRequest(3);
C++
복사
문제 46. 접근 제어 시스템 (Proxy Pattern)
기획 요구사항:
민감한 리소스에 대한 접근을 제어하는 시스템을 만듭니다. 실제 리소스에 직접 접근하는 대신, 중간 단계를 거쳐서 접근 권한을 확인하거나 로그를 기록한 후에 실제 리소스를 사용할 수 있어야 합니다. 클라이언트 코드는 실제 리소스를 사용하는지 중간 단계를 거치는지 알 필요가 없어야 합니다.
구현 가이드:
Subject (순수 가상: Request()) → RealSubject/Proxy (각각 구현)
Proxy는 내부에 RealSubject 포인터를 가지며, Request() 호출 시 접근 제어 후 RealSubject의 Request()를 호출합니다.
학습 목표:
•
Proxy Pattern: 실제 객체에 대한 대리자를 제공하여 접근을 제어하는 패턴을 배웁니다
•
투명성(Transparency)을 유지하면서 추가 기능(로깅, 캐싱, 보안)을 넣는 방법을 익힙니다
•
실제 작업의 지연(Lazy Loading)이나 원격 접근을 구현하는 기법을 이해합니다
핵심 포인트:
•
Subject 인터페이스를 RealSubject와 Proxy가 모두 구현합니다
•
Proxy는 RealSubject를 내부에 포함하고, Request()를 위임합니다
•
Proxy는 위임 전후에 추가 작업(접근 제어, 로깅 등)을 수행할 수 있습니다
•
클라이언트는 Subject 인터페이스만 사용하므로 Proxy인지 RealSubject인지 모릅니다
디자인 패턴: Proxy Pattern
이 패턴은 실제 객체의 대리인 역할을 합니다. Decorator와 비슷해 보이지만 목적이 다릅니다:
•
Decorator: 기능 추가가 목적
•
Proxy: 접근 제어, 지연 로딩, 원격 접근이 목적
Proxy의 종류:
•
Protection Proxy: 접근 권한 확인 (이 문제의 예시)
•
Virtual Proxy: 실제 객체 생성을 지연 (필요할 때만 생성)
•
Remote Proxy: 원격 객체를 로컬처럼 사용 (네트워크 통신 숨김)
•
Caching Proxy: 결과를 캐싱하여 성능 향상
실무 응용:
•
이미지 로딩 (썸네일 먼저 표시, 원본은 필요할 때)
•
데이터베이스 연결 (실제 사용 전까지 연결 지연)
•
RPC (원격 프로시저 호출)
RealSubject real;
Proxy proxy(&real);
Subject* subjects[] = { &real, &proxy };
for (int i = 0; i < 2; ++i)
subjects[i]->Request();
C++
복사
문제 47. 인터페이스 변환 시스템 (Adapter Pattern)
기획 요구사항:
기존에 사용하던 외부 라이브러리가 있는데, 현재 시스템의 인터페이스와 맞지 않습니다. 외부 라이브러리의 코드를 수정할 수 없으므로, 중간에서 인터페이스를 변환해주는 역할이 필요합니다. 시스템의 다른 부분은 변경하지 않고, 외부 라이브러리를 마치 원래 시스템의 일부인 것처럼 사용할 수 있어야 합니다.
구현 가이드:
Target (순수 가상: Request()) → Adapter (Adaptee를 포함하여 Request() 구현)
Adaptee (SpecificRequest() 구현)
Adapter는 Adaptee의 SpecificRequest()를 Target의 Request() 인터페이스로 변환합니다.
학습 목표:
•
Adapter Pattern: 호환되지 않는 인터페이스를 연결하는 패턴을 배웁니다
•
기존 코드를 수정하지 않고 재사용하는 방법을 익힙니다
•
레거시 시스템과 새 시스템을 통합하는 실무적인 기법을 이해합니다
핵심 포인트:
•
Target은 시스템이 기대하는 인터페이스입니다
•
Adaptee는 호환되지 않는 인터페이스를 가진 기존 클래스입니다
•
Adapter는 Target을 구현하면서 내부에서 Adaptee를 사용합니다
•
Adapter가 인터페이스를 변환하여 클라이언트가 Target 인터페이스로 Adaptee를 사용할 수 있게 합니다
디자인 패턴: Adapter Pattern
이 패턴은 실제 세계의 전원 어댑터와 같습니다. 한국 플러그(220V)를 미국 콘센트(110V)에 꽂을 수 있게 변환합니다.
실무 응용:
•
외부 라이브러리 통합 (서드파티 API를 내부 인터페이스에 맞춤)
•
레거시 시스템 연동
•
데이터 형식 변환 (JSON → XML)
Adapter vs Decorator vs Proxy:
•
Adapter: 인터페이스 변환 (호환성)
•
Decorator: 기능 추가 (확장)
•
Proxy: 접근 제어 (보호, 지연)
Adaptee adaptee;
Adapter adapter(&adaptee);
Target* targets[] = { &adapter };
for (int i = 0; i < 1; ++i)
targets[i]->Request();
C++
복사
문제 48. 문서 처리 시스템 (Visitor Pattern)
기획 요구사항:
여러 종류의 문서 요소(텍스트, 이미지 등)가 있고, 이들에 대해 다양한 작업(출력, 저장, 변환 등)을 수행해야 합니다. 새로운 작업을 추가할 때마다 모든 문서 요소 클래스를 수정하는 것은 비효율적입니다. 문서 요소의 코드는 변경하지 않으면서 새로운 작업을 자유롭게 추가할 수 있어야 하며, 각 작업은 문서 요소의 타입에 따라 다르게 동작해야 합니다.
구현 가이드:
Element (순수 가상: Accept(Visitor*)) → ConcreteElementA/B (각각 구현)
Visitor (순수 가상: Visit(ConcreteElementA), Visit(ConcreteElementB)) → ConcreteVisitor (구현)
각 요소가 비지터를 받아들이고, 비지터가 각 요소를 방문하며 동작을 수행합니다.
학습 목표:
•
Visitor Pattern: 데이터 구조와 알고리즘을 분리하는 고급 패턴을 배웁니다
•
함수 오버로딩을 활용하여 타입별로 다른 동작을 구현하는 방법을 익힙니다
•
Double Dispatch라는 고급 기법을 이해합니다
핵심 포인트:
•
Element는 Accept(Visitor*)만 구현하고, 실제 작업은 Visitor에 위임합니다
•
Visitor는 각 Element 타입에 대한 Visit() 오버로드를 가집니다
•
Accept() 안에서 visitor->Visit(this)를 호출하면 올바른 Visit() 오버로드가 선택됩니다
•
새로운 작업(새 Visitor)을 추가해도 Element 코드는 변경되지 않습니다
디자인 패턴: Visitor Pattern
이 패턴은 GoF 패턴 중 가장 복잡하지만, 강력한 패턴입니다.
Double Dispatch:
일반적인 다형성은 Single Dispatch입니다 (호출되는 객체의 타입에 따라 메서드 선택).
Visitor는 Double Dispatch를 사용합니다 (객체 타입 + 방문자 타입 모두 고려).
왜 이렇게 복잡한가?
Element에 모든 작업(출력, 저장, 변환 등)을 메서드로 넣으면, 새 작업 추가 시 모든 Element를 수정해야 합니다.
Visitor를 사용하면 Element는 변경 없이 새 Visitor만 추가하면 됩니다.
실무 응용:
•
컴파일러 (AST 노드에 대한 다양한 작업: 타입 검사, 코드 생성, 최적화)
•
문서 처리 (Export to PDF, HTML, Markdown 등)
•
그래프 알고리즘 (BFS, DFS, Shortest Path 등)
ConcreteElementA elementA; ConcreteElementB elementB;
ConcreteVisitor visitor;
Element* elements[] = { &elementA, &elementB };
for (int i = 0; i < 2; ++i)
elements[i]->Accept(&visitor);
C++
복사
문제 49. 상태 저장/복원 시스템 (Memento Pattern)
기획 요구사항:
문서 편집기에서 실행 취소 기능을 구현합니다. 사용자가 문서를 수정할 때마다 현재 상태를 저장해두었다가, 실행 취소를 누르면 이전 상태로 돌아갈 수 있어야 합니다. 문서 객체의 내부 구조를 외부에 노출하지 않으면서도 상태를 저장하고 복원할 수 있어야 합니다.
구현 가이드:
Originator (CreateMemento(), RestoreMemento(Memento*)), Memento (상태 저장), Caretaker (Memento 보관)
Originator의 상태를 저장하고 복원하는 기능을 구현하세요.
학습 목표:
•
Memento Pattern: 객체의 상태를 외부에 저장하고 복원하는 패턴을 배웁니다
•
캡슐화를 유지하면서 스냅샷을 만드는 방법을 익힙니다
•
Undo/Redo 기능을 구현하는 실무적인 기법을 이해합니다
핵심 포인트:
•
Originator는 상태를 가진 원본 객체입니다
•
Memento는 Originator의 상태를 저장하는 스냅샷 객체입니다
•
CreateMemento()는 현재 상태의 Memento를 생성합니다
•
RestoreMemento()는 Memento로부터 상태를 복원합니다
•
Caretaker는 Memento를 보관하지만 내용을 보거나 수정할 수 없습니다
디자인 패턴: Memento Pattern
이 패턴은 "타임머신"과 같습니다. 특정 시점의 상태를 저장했다가 나중에 그 시점으로 돌아갈 수 있습니다.
Command Pattern과의 조합:
Command 패턴과 함께 사용하면 강력한 Undo/Redo를 만들 수 있습니다:
•
Command 실행 전: Memento 생성
•
Undo: Memento로부터 복원
실무 응용:
•
텍스트 에디터 Undo/Redo
•
게임 세이브/로드
•
트랜잭션 롤백
•
버전 관리 시스템
캡슐화 보호:
Memento는 Originator의 내부 상태를 저장하지만, 외부에서는 Memento의 내용을 볼 수 없어야 합니다. C++에서는 friend 키워드로 구현할 수 있습니다.
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() 추가.
학습 목표:
•
여러 디자인 패턴을 조합하여 복잡한 시스템을 설계하는 방법을 배웁니다
•
다중 상속을 통해 하나의 객체가 여러 역할을 수행하도록 만드는 기법을 익힙니다
•
실제 게임 엔진에서 사용되는 엔티티 관리 시스템의 구조를 이해합니다
핵심 포인트:
•
GameObject는 모든 게임 오브젝트의 공통 기능(Update, Render)을 정의합니다
•
IControllable, ICollectable 등은 선택적 기능을 나타내는 인터페이스입니다
•
Character는 GameObject와 IControllable을 모두 구현합니다 (다중 상속)
•
dynamic_cast로 특정 인터페이스를 구현하는지 확인하고 해당 기능을 호출합니다
디자인 패턴: 종합 (다중 패턴 조합)
이 문제는 여러 패턴을 결합합니다:
•
Component Pattern: GameObject가 공통 인터페이스
•
Interface Segregation: 기능별로 인터페이스 분리 (IControllable, ICollectable)
•
다형성: GameObject 배열로 모든 객체 관리
다중 상속의 활용:
게임 엔진 설계:
실무 응용:
Unity의 Component 시스템, Unreal의 Actor 시스템이 이와 유사한 구조를 사용합니다.
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() 호출
if (IControllable* ctrl = dynamic_cast<IControllable*>(&player)) {
ctrl->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문제 |
디자인 패턴 전체 요약
GoF 디자인 패턴 분류
생성 패턴 (Creational Patterns) - 객체 생성 메커니즘
•
Factory Method (문제 42): 객체 생성을 서브클래스에 위임
•
Builder (문제 39): 복잡한 객체를 단계별로 생성
구조 패턴 (Structural Patterns) - 객체와 클래스를 조합하여 더 큰 구조 구성
•
Composite (문제 32, 33): 개별 객체와 복합 객체를 동일하게 다룸
•
Decorator (문제 35, 41): 객체에 동적으로 기능 추가
•
Proxy (문제 46): 객체에 대한 대리자 제공
•
Adapter (문제 47): 호환되지 않는 인터페이스 연결
행위 패턴 (Behavioral Patterns) - 객체 간의 책임 분배와 알고리즘
•
Strategy (문제 27, 28, 30, 34, 36): 알고리즘을 캡슐화하고 교체 가능하게
•
State (문제 37): 상태에 따라 객체의 행동 변경
•
Command (문제 38): 요청을 객체로 캡슐화
•
Observer (문제 43, 44): 일대다 의존 관계, 상태 변화 자동 전파
•
Chain of Responsibility (문제 45): 요청을 체인으로 전달
•
Template Method (문제 40): 알고리즘 골격 정의, 일부 단계를 서브클래스에 위임
•
Visitor (문제 48): 데이터 구조와 알고리즘 분리
•
Memento (문제 49): 객체의 상태를 저장하고 복원
패턴 선택 가이드
언제 어떤 패턴을 사용할까?
•
알고리즘을 교체하고 싶다 → Strategy
•
상태에 따라 동작이 달라진다 → State
•
객체 생성을 유연하게 → Factory Method, Builder
•
기능을 동적으로 추가 → Decorator
•
계층 구조를 동일하게 다루고 싶다 → Composite
•
실행 취소가 필요하다 → Command, Memento
•
여러 객체에게 변화를 알리고 싶다 → Observer
•
요청을 순차적으로 처리 → Chain of Responsibility
•
호환되지 않는 인터페이스 연결 → Adapter
•
접근을 제어하고 싶다 → Proxy
•
알고리즘 골격은 고정, 세부만 변경 → Template Method
•
새로운 작업 추가가 많다 → Visitor
학습 팁
1.
먼저 문제를 읽고 직접 구조를 그려보세요 - 클래스 다이어그램이나 간단한 상속 구조도
2.
코드를 작성하기 전에 어떤 패턴인지 파악하세요 - 패턴의 의도를 이해하면 구현이 쉬워집니다
3.
실행해보고 동작을 확인하세요 - 다형성이 실제로 어떻게 작동하는지 눈으로 보는 것이 중요합니다
4.
패턴을 외우지 말고 이해하세요 - "왜 이렇게 설계했을까?"를 생각하면 응용력이 생깁니다
5.
실무 예시와 연결하세요 - 실제 게임이나 프로그램에서 어떻게 사용되는지 상상해보세요
