Unreal 개발 본 캠프 14일차

HappyCircle·2025년 12월 16일

Unreal 개발

목록 보기
31/163

오늘 학습 진행 내용

[2번 과제] 전직 시스템과 전투 시스템

필수 기능 가이드

  • Player라는 기본 클래스를 정의 합니다.
    • Player 클래스에는 attack()이라는 순수 가상 함수를 포함합니다.
      • attack() 함수는 무기를 휘두르기만 하는 기본적인 공격 함수입니다.
  • Player 클래스를 상속받아 다양한 직업 클래스를 생성합니다.
    • 예) Warrior, Magician, Thief, Archer
      • 각 직업 클래스에서 attack()함수를 재정의(오버라이딩)하여 해당 직업의 공격을 출력하면 됩니다!
  • 메인 함수에서 Player 타입의 포인터를 선언하고, 해당하는 번호를 입력하면 (1. 전사 2. 마법사 3. 도적 4. 궁수) Warrior, Magician, Thief, Archer 중 해당하는 직업을 할당해줍니다. → 이후 Player는 attack() 함수를 호출하여 공격합니다.
  • 전체적인 구조는 아래와 같습니다.

📁JobChange/

├── main.cpp // 메인 실행 파일
├── Player.h // Player 클래스 정의
├── Player.cpp // Player 클래스 구현
├── Warrior.h // Warrior 클래스 정의
├── Warrior.cpp // Warrior 클래스 구현
├── Magician.h // Magician 클래스 정의
├── Magician.cpp // Magician 클래스 구현
├── Thief.h // Thief 클래스 정의
├── Thief.cpp // Thief 클래스 구현
├── Archer.h // Archer 클래스 정의
├── Archer.cpp // Archer 클래스 구현

코드 스니펫 Player 클래스

  • Player.h
#pragma once
#include <string>
using namespace std;

class Player {
public:
    Player(string nickname);

    virtual void attack() = 0;
    void printPlayerStatus();
		
		// getter 함수
    string getJobName();
    string getNickname();
    int getLevel();
    int getHP();
    int getMP();
    int getPower();
    int getDefence();
    int getAccuracy();
    int getSpeed();
		
		// setter 함수
    void setNickname(string nickname);
    void setHP(int HP);
    void setMP(int MP);
    void setPower(int power);
    void setDefence(int defence);
    void setAccuracy(int accuracy);
    void setSpeed(int speed);

protected:
    string job_name;
    string nickname;
    int level;
    int HP;
    int MP;
    int power;
    int defence;
    int accuracy;
    int speed;
};
  • void Player::printPlayerStatus()
void Player::printPlayerStatus() {
    cout << "------------------------------------" << endl;
    cout << "* 현재 능력치" << endl;
    cout << "닉네임: " << nickname << endl;
    cout << "Lv. " << level << endl;
    cout << "HP: " << HP << endl;
    cout << "MP: " << MP << endl;
    cout << "공격력: " << power << endl;
    cout << "방어력: " << defence << endl;
    cout << "정확도: " << accuracy << endl;
    cout << "속도: " << speed << endl;
    cout << "------------------------------------" << endl;
}

코드 스니펫 main 함수

  • main.cpp
#include <iostream>
#include "player.h"
#include "warrior.h"
#include "magician.h"
#include "thief.h"
#include "archer.h"
using namespace std;

// 메인 함수
int main() {
    string jobs[] = { "전사", "마법사", "도적", "궁수" };
    int job_choice = 0;
    string nickname;

    Player* player = nullptr;

    cout << "* 닉네임을 입력해주세요: ";
    cin >> nickname;

    cout << "<전직 시스템>" << endl;
    cout << nickname << "님, 환영합니다!" << endl;
    cout << "* 원하시는 직업을 선택해주세요." << endl;

    for (int i = 0; i < 4; i++) {
        cout << (i + 1) << ". " << jobs[i] << endl;
    }

    cout << "선택: ";
    cin >> job_choice;

    switch (job_choice) {
    case 1: 
	    player = new Warrior(nickname);
	    break;
    case 2: 
	    player = new Magician(nickname);
	    break;
    case 3: 
	    player = new Thief(nickname);
	    break;
    case 4: 
	    player = new Archer(nickname); 
	    break;
    default:
        cout << "잘못된 입력입니다." << endl;
        return 1;
    }
		
		player->attack();
    player->printPlayerStatus();
    
    if(player != nullptr)
      delete player;

    return 0;
}
  • void Player::printPlayerStatus()
void Player::printPlayerStatus() {
    cout << "------------------------------------" << endl;
    cout << "* 현재 능력치" << endl;
    cout << "닉네임: " << nickname << endl;
    cout << "직업: " << job_name << endl;
    cout << "Lv. " << level << endl;
    cout << "HP: " << HP << endl;
    cout << "MP: " << MP << endl;
    cout << "공격력: " << power << endl;
    cout << "방어력: " << defence << endl;
    cout << "정확도: " << accuracy << endl;
    cout << "속도: " << speed << endl;
    cout << "------------------------------------" << endl;
}

도전 기능 가이드
아래 코드 스니펫을 보고 Monster 클래스를 작성해주세요!
코드 스니펫 Monster 클래스 요구사항

class Monster {
public:
		// Monster 생성자
		// - 몬스터의 이름을 매개변수로 입력 받습니다.
		// - 모든 몬스터는 HP 10, 공격력 30, 방어력 10, 스피드 10의 능력치를 가집니다.
    Monster(string name);
    
    // 몬스터의 공격 함수
    // - 플레이어 객체 포인터를 매개변수로 입력 받습니다.
    // - 몬스터의 공격력-플레이어의 방어력을 데미지로 정의합니다.
    // - 만약 위에서 계산한 데미지가 0 이하라면, 데미지를 1로 정의합니다.
    // - 플레이어에게 얼마나 데미지를 입혔는지 출력합니다.
    // - 플레이어 객체의 getHP 함수를 실행하여 플레이어HP-데미지 계산 결과를
    // - 플레이어 객체의 setHP 매개변수로 전달합니다.
    // - 플레이어가 생존했을 경우, 플레이어의 남은 HP를 출력합니다.
    void attack(Player* player);
    
    // 몬스터의 속성값을 리턴하는 get 함수
    string getName();
    int getHP();
    int getPower();
    int getDefence();
    int getSpeed();
		
		// 몬스터의 속성값을 정의하는 set 함수
    void setName(string name);
    void setHP(int HP);
    void setPower(int power);
    void setDefence(int defence);
    void setSpeed(int speed);

protected:
    string name; // 몬스터의 이름
    int HP; // 몬스터의 HP
    int power; // 몬스터의 공격력
    int defence; // 몬스터의 방어력
    int speed; // 몬스터의 스피드
};

Player의 attack() 함수를 오버로딩하여 attack(Monster* monster)함수를 구현하세요. 자세한 사항은 아래 코드 스니펫을 참조하시면 됩니다!

*코드 스니펫 attack(Monster monster) 함수 요구사항

  • player.h
class Monster;

class Player {
public:
    Player(string nickname);

    virtual void attack() = 0;
    virtual void attack(Monster* monster) = 0;
    void printPlayerStatus();

    // getter
    // ...
    
    // setter
    // ...

protected:
    // attributes
};
  • warrior.cpp, magician.cpp, thief.cpp, archer.cpp
void JobName::attack(Monster* monster) {
    // 플레이어의 공격력-몬스터의 방어력을 계산하여 데미지로 정의합니다.
		// 계산된 데미지가 0 이하일 경우, 데미지를 1로 정의합니다.
		
		// 공격 문장을 출력합니다.
		// - 전사, 마법사: 계산된 데미지로 1회 공격
		// - 궁수: 계산된 데미지/3으로 3회 공격 (소수점 생략)
		// - 도적: 계산된 데미지/5으로 5회 공격 (소수점 생략)
		// - 궁수와 도적의 경우, 3과 5로 나눈 결과가 0이라면 1로 정의합니다.
		// ex) * 슬라임에게 화살로 3의 피해를 입혔다! 3번 출력
		
		// 몬스터의 setHP를 호출하여 몬스터의 HP-데미지를 계산한 값을 매개변수로 전달합니다.
		// 몬스터의 getHP를 호출 분기문이 실행됩니다.
		// 몬스터가 생존했을 경우 몬스터의 남은 HP 출력
		// 몬스터가 생존하지 못했을 경우 플레이어의 승리 문구 출력
}

과제 코드

main.cpp

#include <iostream>
#include "Player.h"
#include "Warrior.h"
#include "Magician.h"
#include "Thief.h"
#include "Archer.h"
#include "Monster.h"
using namespace std;

// 메인 함수
int main() {
    string jobs[] = { "전사", "마법사", "도적", "궁수" };
    int job_choice = 0;
    string nickname;

    Player* player = nullptr;

    cout << "* 닉네임을 입력해주세요: ";
    cin >> nickname;

    cout << "<전직 시스템>" << endl;
    cout << nickname << "님, 환영합니다!" << endl;
    cout << "* 원하시는 직업을 선택해주세요." << endl;

    for (int i = 0; i < 4; i++) {
        cout << (i + 1) << ". " << jobs[i] << endl;
    }

    cout << "선택: ";
    cin >> job_choice;

    switch (job_choice) {
    case 1:
        player = new Warrior(nickname);
        break;
    case 2:
        player = new Magician(nickname);
        break;
    case 3:
        player = new Thief(nickname);
        break;
    case 4:
        player = new Archer(nickname);
        break;
    default:
        cout << "잘못된 입력입니다." << endl;
        return 1;
    }
    player->printPlayerStatus();
    Monster* monster = nullptr;
    string monster_name = "슬라임";
    monster = new Monster(monster_name);
    cout << "플레이어가 " << monster_name << " 몬스터와 조우하였습니다." << endl;
    while (player->getHP() > 0 && monster->getHP() > 0) {
        if (player->getSpeed() >= monster->getSpeed()) {
            player->attack(monster);
            if (monster->getHP() > 0) {
                monster->attack(player);
            }
        }
        else {
            monster->attack(player);
            if (player->getHP() > 0) {
                player->attack(monster);
            }
        }
    }
    //player->attack();


    delete player;
    delete monster;
    return 0;
}

Player.h

#pragma once
#ifndef PLAYER_H_
#define PLAYER_H_
#include <string>
#include "Monster.h"
using namespace std;

class Player {
public:
    Player(string nickname);

    virtual void attack() = 0;
    virtual void attack(Monster* monster) = 0;
    void printPlayerStatus();


    string getJobName();
    string getNickname();
    int getLevel();
    int getHP();
    int getMP();
    int getPower();
    int getDefence();
    int getAccuracy();
    int getSpeed();

    void setNickname(string nickname);
    bool setHP(int HP);
    bool setMP(int MP);
    void setPower(int power);
    void setDefence(int defence);
    void setAccuracy(int accuracy);
    void setSpeed(int speed);
    void setLevel(int level);
protected:
    string job_name;
    string nickname;
    int level;
    int HP;
    int MP;
    int power;
    int defence;
    int accuracy;
    int speed;
};
#endif

Player.cpp

#include "Player.h"
#include <iostream>

using namespace std;

Player::Player(string nickname) {
    this->nickname = nickname;
}
string Player::getNickname() {
    return nickname;
}
string Player::getJobName() {
    return job_name;
}

int Player::getLevel() {
    return level;
}
int Player::getHP() {
    return HP;
}
int Player::getMP() {
    return MP;
}
int Player::getPower() {
    return power;
}
int Player::getDefence() {
    return defence;
}
int Player::getAccuracy() {
    return accuracy;
}
int Player::getSpeed() {
    return speed;
}
void Player::setNickname(string nickname) {
    this->nickname = nickname;
}
bool Player::setHP(int HP) {

    if (HP <= 0) {
        this->HP = 0;
        return false;
    }
    else {
        this->HP = HP;
        return true;
    }
};
bool Player::setMP(int MP) {
    this->MP = MP;
    if (MP <= 0) {
        return false;
    }
    else {
        return true;
    }
};
void Player::setPower(int power) {
    this->power = power;
};
void Player::setDefence(int defence) {
    this->defence = defence;    
};
void Player::setAccuracy(int accuracy) {
    this->accuracy = accuracy;
};
void Player::setSpeed(int speed) {
    this->speed = speed;
};
void Player:: setLevel(int level) {
    this->level = level;
}

void Player::printPlayerStatus() {
    cout << "------------------------------------" << endl;
    cout << "* 현재 능력치" << endl;
    cout << "닉네임: " << nickname << endl;
    cout << "Lv. " << level << endl;
    cout << "HP: " << HP << endl;
    cout << "MP: " << MP << endl;
    cout << "공격력: " << power << endl;
    cout << "방어력: " << defence << endl;
    cout << "정확도: " << accuracy << endl;
    cout << "속도: " << speed << endl;
    cout << "------------------------------------" << endl;
}

Warrior.h

#pragma once
#ifndef WARRIOR_H_
#define WARRIOR_H_
#include <string>
#include "Player.h"
using namespace std;
class Warrior : public Player {
public:
    Warrior(string nickname);
    void attack() override;
    void attack(Monster* monster) override;

};
#endif

Warrior.cpp

#include <string>
#include <iostream>
using namespace std;
#include "Warrior.h"

Warrior::Warrior(string nickname) : Player(nickname) {
    level = 10;
    accuracy = 8;
    power = 20;
    defence = 25;
    HP = 100;
    MP = 10;
    speed = 10;
}
void Warrior::attack() {
    int Damage = power * accuracy/10;
    int Count = speed / 10;
    for (int i = 1; i <= Count; i++) {
        cout << "가로 베기로" << Damage << " 데미지의 공격을 합니다!" << endl;
    }
}

void Warrior::attack(Monster* monster) {
    int MonsterDefence = monster->getDefence();
    int raw = power - MonsterDefence;
    int Damage = raw <= 0 ? 1 : raw;
    cout << "몬스터에게" << Damage << "의 피해를 입혔습니다." << endl;
    int MonsterHP = monster->getHP();
    bool MonsterSurvive = monster->setHP(MonsterHP - Damage);
    cout << "몬스터의 남은 체력 : " << monster->getHP() << endl;
    if (!MonsterSurvive) {
        cout << "플레이어의 승리!!" << endl;
    }
}

Magician.h

#pragma once
#ifndef MAGICIAN_H_
#define MAGICIAN_H_
#include <string>
#include "Player.h"
using namespace std;
class Magician : public Player {
public:
    Magician(string nickname);
    void attack() override;
    void attack(Monster* monster) override;
};
#endif

Magician.cpp

#include "Windows.h"
#include <string>
#include <iostream>
#include "Magician.h"

using namespace std;
Magician::Magician(string nickname) : Player(nickname) {
    level = 12;
    accuracy = 10;
    power = 1;
    defence = 15;
    HP = 50;
    MP = 60;
    speed = 8;
}
void Magician::attack() {
    int Damage = MP * accuracy / 10;
    int Count = speed / 100;
    if (Count < 1) {
        Sleep(500);
    }
    cout << Damage<<" 데미지의 에너지 볼을 발사합니다!" << endl;
}

void Magician::attack(Monster* monster) {
    int MonsterDefence = monster->getDefence();
    int raw = power - MonsterDefence;
    int Damage = raw <= 0 ? 1 : raw;
    cout << "몬스터에게" << Damage << "의 피해를 입혔습니다." << endl;
    int MonsterHP = monster->getHP();
    bool MonsterSurvive = monster->setHP(MonsterHP - Damage);
    cout << "몬스터의 남은 체력 : " << monster->getHP() << endl;
    if (!MonsterSurvive) {
        cout << "플레이어의 승리!!" << endl;
    }
}

Thief.h

#pragma once
#ifndef THIEF_H_
#define THIEF_H_
#include <string>
#include "Player.h"
using namespace std;
class Thief : public Player {
public:
    Thief(string nickname);
    void attack()  override;;
    void attack(Monster* monster) override;
};

#endif

Thief.cpp

#include <string>
#include <iostream>
using namespace std;
#include "Thief.h"

Thief::Thief(string nickname) : Player(nickname) {
    level = 10;
    accuracy = 10;
    power = 7;
    defence = 18;
    HP = 80;
    MP = 30;
    speed = 30;
}
void Thief::attack() {
    int Damage = power * accuracy / 10;
    int Count = speed / 10;
    for (int i = 1; i <= Count; i++) {
        cout<<Damage << "데미지의 단검을 투척합니다!" << endl;
    }

}

void Thief::attack(Monster* monster) {
    int MonsterDefence = monster->getDefence();
    int raw = power - MonsterDefence;
    int Damage = raw <= 0 ? 1 : raw;
    cout << "몬스터에게" << Damage << "의 피해를 입혔습니다." << endl;
    int MonsterHP = monster->getHP();
    bool MonsterSurvive = monster->setHP(MonsterHP - Damage);
    cout << "몬스터의 남은 체력 : " << monster->getHP() << endl;
    if (!MonsterSurvive) {
        cout << "플레이어의 승리!!" << endl;
    }
}

Archer.h

#pragma once
#ifndef ARCHER_H_
#define ARCHER_H_
#include <string>
#include "Player.h"
using namespace std;
class Archer : public Player {
public:
    Archer(string nickname);
    void attack() override;
    void attack(Monster* monster) override;
};
#endif

Archer.cpp

#include <string>
#include <iostream>
using namespace std;
#include "Archer.h"


Archer::Archer(string nickname) : Player(nickname) {
    level = 10;
    accuracy = 10;
    power = 5;
    defence = 20;
    HP = 60;
    MP = 40;
    speed = 20;
}
void Archer::attack() {
    int Damage = power * accuracy / 100;
    int Count = speed / 100;
    for (int i = 1; i <=Count; i++) {
        cout << Damage<<"데미지의 1발 화살을 발사합니다!" << endl;
    }
    
}

void Archer::attack(Monster* monster) {
    int MonsterDefence = monster->getDefence();
    int raw = power - MonsterDefence;
    int Damage = raw <= 0 ? 1 : raw;
    cout << "몬스터에게" << Damage << "의 피해를 입혔습니다." << endl;
    int MonsterHP = monster->getHP();
    bool MonsterSurvive = monster->setHP(MonsterHP - Damage);
    cout << "몬스터의 남은 체력 : " << monster->getHP() << endl;
    if (!MonsterSurvive) {
        cout << "플레이어의 승리!!" << endl;
    }
}

Monster.h

#pragma once

#include <string>
using namespace std;
class Player;

class Monster {
public:
    // Monster 생성자
    // - 몬스터의 이름을 매개변수로 입력 받습니다.
    // - 모든 몬스터는 HP 10, 공격력 30, 방어력 10, 스피드 10의 능력치를 가집니다.
    Monster(string name);

    // 몬스터의 공격 함수
    // - 플레이어 객체의 포인터를 매개변수로 입력 받습니다.
    // - 몬스터의 공격력-플레이어의 방어력을 데미지로 정의합니다.
    // - 만약 위에서 계산한 데미지가 0 이하라면, 데미지를 1로 정의합니다.
    // - 플레이어에게 얼마나 데미지를 입혔는지 출력합니다.
    // - setHP 함수를 실행하여 HP-데미지 계산 결과를 매개변수로 전달합니다.
    // - setHP에서 리턴 받은 생존 여부를 기준으로 분기문이 실행됩니다.
    // - 생존했을 경우, 플레이어의 남은 HP만 출력합니다.
    // - 생존하지 못했을 경우, 플레이어의 남은 HP와 몬스터의 승리 문구를 출력합니다.
    void attack(Player* player);

    // 몬스터의 속성값을 리턴하는 get 함수
    string getName();
    int getHP();
    int getPower();
    int getDefence();
    int getSpeed();

    // 몬스터의 속성값을 정의하는 set 함수
    // setHP의 경우 배틀 시스템에 직결되는 함수이므로, 생존 여부를 리턴합니다.
    // HP가 1 이상일 때 true, HP가 0 이하가 되었을 때 false를 리턴합니다.
    // HP가 1 이상일 때만 새로운 HP값 정의를 진행합니다.
    // 0 이하일 경우 HP를 0으로 정의합니다.
    void setName(string name);
    bool setHP(int HP);
    void setPower(int power);
    void setDefence(int defence);
    void setSpeed(int speed);

protected:
    string name; // 몬스터의 이름
    int HP; // 몬스터의 HP
    int power; // 몬스터의 공격력
    int defence; // 몬스터의 방어력
    int speed; // 몬스터의 스피드
};

Monster.cpp

#include "Monster.h"
#include "Player.h"
#include <iostream>

using namespace std;

Monster::Monster(string name) {
	this->name = name;
	HP = 10;
	power = 30;
	defence = 10;
	speed = 10;
}

void Monster::attack(Player* player) {
	if (!player) {
		cout << "공격할 플레이어가 없습니다." << endl;
		return;
	}
	int PlayerDefence = player->getDefence();
	int raw = power - PlayerDefence;
	int Damage = raw <= 0 ? 1 : raw;
	cout << "플레이어에게" << Damage << "의 피해를 입혔습니다." << endl;
	int PlayerHP = player->getHP();
	bool PlayerSurvive = player->setHP(PlayerHP - Damage);
	cout << "플레이의 남은 체력 : " << player->getHP() << endl;
	if (!PlayerSurvive) {
		cout << "몬스터의 승리!!" << endl;
	}
}

string Monster::getName() {
	return name;
}

int Monster::getHP() {
	return HP;
}

int Monster::getPower() {
	return power;
}

int Monster::getDefence() {
	return defence;
}

int Monster::getSpeed() {
	return speed;
}

bool Monster::setHP(int HP) {
	if (HP <= 0) {
		this->HP = 0;
		return false;
	}
	else {
		this->HP = HP;
		return true;
	}
};
void Monster::setPower(int power) {
	this->power = power;
};
void Monster::setDefence(int defence) {
	this->defence = defence;
};
void Monster::setSpeed(int speed) {
	this->speed = speed;
};

구현 결과


게임 개발자를 위한 C++문법

챕터2-4: 객체지향적 설계 2.

SOLID 원칙

객체 지향 프로그래밍을 위해서 많은 개발자들이 검증한 설계 원칙이 존재 바로 SOLID 원칙
SOLID 원칙의 주요 목적

  • 유지보수성 및 확장성 향상
  • 변경에 유연한 설계 제공

SOLID 원칙은 객체지향 설계에서 유지 보수성과 확장성을 높이기 위한 5가지 원칙

1. 단일 책임 원칙 (SRP)

각 클래스는 하나의 책임만 가져야 한다는 원칙
즉, 크랠스는 하나의 변경 이유만 가져야 됨, 특정 기능 역할 수행하는데 집중
구현 예시
아래 상황을 구현
1. 학생의 이름을 받을 수 있어야 함
2. 학생의 이름을 출력할 수 있어야 함
3. 학생의 점수를 받고 성적을 계산할 수 있어야 함

잘못된 구현과 제대로 된 코드 비교

    #include <iostream>
    #include <string>

    using namespace std;

    // =======================================================
    // 🔴 단일 책임 원칙이 제대로 적용되지 않은 사례
    // Student 클래스 내 모든 기능 구현
    // =======================================================
    class Student{
        public:
            void setName(const string& name){
                this->name = name;
            }

            void displayDetails(){
                cout<<"Student Name: "<<name<<endl;
            }

            void calculateGrade(int score){
                if(score>=90){
                    cout<<"Grade: A"<<endl;
                }else if(score >=80){
                    cout<<"Grade: B"<<endl;
                }else{
                    cout<<"Grade: C"<<endl;
                }
            }
        private:
            string name;
    };

    // =======================================================
    // 🟢 단일 책임 원칙이 적용된 코드
    // Student는 학생 정보만 관리
    // GradeCalculator는 성적 계산
    // StudentPrinter는 출력 담당
    // =======================================================

    // 학생 정보 관리 클래스
    class Student{
        public:
            void setName(const string& name){
                this->name = name;
            }
            string getName() const{
                return name;
            }
        private:
            string name;
    };

    // 성적 계산 클래스
    class GradeCalculator{
        public:
            void calculateGrade(int score){
                if(score>=90){
                    cout<<"Grade: A"<<endl;
                }else if(score >=80){
                    cout<<"Grade: B"<<endl;
                }else{
                    cout<<"Grade: C"<<endl;
                }
            }
    };

    // 출력 클래스
    class StudentPrinter{
        public:
            void displayDetails(const Student& student){
                cout<<"Student Name: "<<student.getName()<<endl;
            }
    };

2. 개방 폐쇄 원칙(OCP)

확장에서는 열려 있어야 하고, 수정에는 닫혀있어야한다는 개념
기존 코드를 최소한으로 변경하면서 새로운 기능을 추가할 수 있도록 설계
구현 예시
도형에 해당하는 번호를 받고, 해당 도형을 그려주는 클래스 제작
잘못된 구현과 제대로 된 코드 비교

    #include <iostream>
    using namespace std;

    // =======================================================
    // 🔴 개방-폐쇄 원칙이 제대로 적용되지 않은 사례
    // ShapeManager 클래스 하나로 모든 도형을 관리
    // =======================================================
    class ShapeManager {
        public:
            void drawShape(int shapeType){
                if(shapeType == 1){
                    // 원 그리기
                } else if(shapeType == 2){
                    // 사각형 그리기
                }
            }
    };

    // =======================================================
    // 🟢 개방-폐쇄 원칙이 적용된 사례
    // ShapeManager는 Shape 인터페이스를 인자로 받아
    // 새로운 도형 추가 시에도 기존 코드 영향 없음
    // =======================================================

    // 도형 인터페이스
    class Shape {
        public:
            virtual void draw() = 0; // 순수 가상 함수
    };

    // 원 클래스
    class Circle : public Shape {
        public:
            void draw() {
                // 원 그리기
            }
    };

    // 사각형 클래스
    class Square : public Shape {
        public:
            void draw() {
                // 사각형 그리기
            }
    };

    // ShapeManager 클래스 (다형성 활용)
    class ShapeManager {
        public:
            void drawShape(Shape& shape){
                shape.draw(); // 다형성 적용
            }
    };

3. 리스 코프 치환 원칙(LSP)

자식 클래스는 부모 클래스에서 기대되는 행동을 보장
부모 클래스가 사용하는 코드가 자식 클래스로 대체되더라도 정상적으로 동작해야 함
자식 클래스는 부모 클래스의 동작을 일관되게 유지
구현 예시
모든 도형은 넓이를 계산할 수 있음
Rectangle 클래스와 이를 상속받는 Square 클래스를 설계
잘못된 구현과 제대로 된 코드 비교

    #include <iostream>
    using namespace std;

    // =======================================================
    // 🔴 리스코프 치환 원칙이 제대로 적용되지 않은 코드
    // Rectangle과 Square 상속 문제 발생
    // =======================================================

    class Rectangle {
        public:
            virtual void setWidth(int w) { width = w; }
            virtual void setHeight(int h) { height = h; }
            int getWidth() const { return width; }
            int getHeight() const { return height; }
            int getArea() const { return width * height; }

        private:
            int width = 0;
            int height = 0;
    };

    class Square : public Rectangle {
        public:
            void setWidth(int w) override {
                Rectangle::setWidth(w);
                Rectangle::setHeight(w); // ⚠️ 정사각형: 너비와 높이가 같아야 함
            }
            void setHeight(int h) override {
                Rectangle::setHeight(h); // ⚠️ 문제 발생 가능
            }
    };

    // Rectangle 객체 테스트 함수
    void testRectangle(Rectangle& rect){
        rect.setWidth(5);
        rect.setHeight(10); // ⚠️ Square 객체면 기대 결과와 다름
        cout << "Expected area: 50, Actual area: " << rect.getArea() << endl;            
    }

    int main(){
        Rectangle rect;
        testRectangle(rect); // ✅ Expected area: 50

        Square square;
        testRectangle(square); // ⚠️ Expected 50, Actual 100 (문제 발생)
        return 0;
    }

    // =======================================================
    // 🟢 리스코프 치환 원칙이 적용된 코드
    // Shape 인터페이스로 넓이 계산 통일
    // =======================================================

    // 도형 인터페이스
    class Shape {
        public:
            virtual int getArea() const = 0; // 순수 가상 함수
    };

    // Rectangle 클래스 (Shape 상속 아님, 기존 기능 유지)
    class RectangleLSP {
        public:
            void setWidth(int w) { width = w; }
            void setHeight(int h) { height = h; }
            int getWidth() const { return width; }
            int getHeight() const { return height; }
            int getArea() const { return width * height; }

        private:
            int width = 0;
            int height = 0;
    };

    // Square를 Shape로 추상화
    class SquareLSP : public Shape {
        public:
            void setSide(int s) { side = s; }
            int getSide() const { return side; }
            int getArea() const override { return side * side; }

        private:
            int side = 0;
    };

    // Shape 테스트 함수
    void testShape(Shape& shape){
        cout << "Area: " << shape.getArea() << endl;
    }

    int mainLSP(){
        RectangleLSP rect;
        rect.setWidth(5);
        rect.setHeight(10);
        cout << "Rectangle area: " << rect.getArea() << endl; // ✅ Area: 50

        SquareLSP square;
        square.setSide(7);
        testShape(square); // ✅ Area: 49

        return 0;
    }

4. 인터페이스 분리원칙(ISP)

클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙
하나의 거대한 인터페이스보다는 역할별로 세분화된 인터페이스로 구성
필요한 기능만 구현하도록 설계
구현 예시
프린트,스캔 기능을 포함한 클래스를 구현
잘못된 구현과 제대로 된 코드 비교

//인터페이스 분리 원칙이 제대로 적용되지 않은 코드
//프린터 스캔을 하나의 클래스에서 모두 구현, 이러면 상속 받는 클래스에서 필요 없는 경우에도 모두 구현
class Machine {
    private:

    public:
        Machine(){}

        void priint() {
            //세부 기능 구현
        }
        void scan(){
            //세부 기능 구현
        }
}

//인터페이스 분리 원칙이 제대로 적용된 코드
//프린터, 스캔 클래스 따로 구현해서 각각 필요한 클래스만 가져다가 사용
class Printer{
    public:
        virtual void print() =0;
}

class Scanner {
    public:
        virtual void scan() = 0;
}

class BasePrinter : public Printer {
    public:
        void print() override {
            //문서 출력
        }
}

class MultiFunctionDevice{
    private:
        Printer* printer;
        Scanner* scanner;
    public:
        MultiFunctionDevice(Printer* p, Scanner* s) : printer(p), scanner(s) {}

        void print(){
            if(printer) printer-> print();
        }
        void scan(){
            if(scanner) scanner->scan();
        }

}

5. 의존 역전 원칙(DIP)

고수준 모듈은 저수준 모듈에 직접 의존하는 것이 아니라 두 모듈 모두 추상화에 의존
구체적인 구현(저수준 모듈)에 의존하는 것이 아니라 인터페이스나 추상 클래스 같은 추상화 계층을 두어 결합도 낮추는 것이 좋은 설계
구현 예시
1. 컴퓨터는 키보드와 모니터가 있습니다.
2. 키보드는 입력받을 수 있고 모니터는 출력할 수있습니다.
잘못된 구현과 제대로 된 코드 비교

#include <iostream>
using namespace std;
//의존 역전 원칙이 제대로 적용되지 않은 코드
//Computer 클래스는 Keyboard 클래스와 Monitor 클래스 강하게 결합된 상태
//Keyboard 종류 또는 Montior의 종류가 여러 개가 되면 변경량이 많아짐
class Keyboard{
    public:
        stirng getInput(){
            return "입력 데이터";
        }
};
class Monitor{
    public:
        void dispaly(const string& date){
            //출력
        }
};
class Computer{
    Keyboard keyboard;
    Monitor monitor;

    public:
        void operate(){
            string input = keyboard.getInput();
            monitor.display(input);
        }
};

//의존 역전 원칙이 제대로 적용된 코드
//키보드와 모니터를 인터페이스화해서 강한 결합이 아닌 약한 결합으로 설정
//이런 설계는 다른 입/출력 장치 쉽게 교체 가능
class InputDevice{
    public:
        virtual string getInput()=0;
};

class OutputDevice{
    public:
        virtual void display(const string& data)=0;
};

class Keyboard : public InputDevice {
    public:
        string getInput() override {
            return "키보드 입력 데이터";
        }
};

class Monitor : public OutputDevice{
    public:
        string display(const stirng& data)override{
            //화면에 출력
        }
}

class Computer{
    private:
        InputDevice* inputDevice;
        OutputDevice* outputDevice;
    public:
        Computer(InputDevice* input, OutputDevice* output)
            : inputDevice(input),outputDevice(output){}
        
            void operate(){
                string data= inputDevice->getInput();
                outputDevice->display(data);
            }
}

객체 지향적 설계 실습 1.

SOLID원칙을 적용한 Student 클래스 구현
단일 책임 원칙은 클래스가 단 하나의 책임만, 가지며 하나의 역할만 수행해야 한다는 원칙입니다.
이 원칙이 잘 적용된 간단한 “학생 정보를 관리하는 프로그램”을 구현해 봅시다.

구현 요구사항은 아래와 같습니다.

📌Student 클래스
  • 학생의 이름과 나이를 저장하는 역할을 가집니다.
  • 학생 정보를 문자열로 반환하는 메서드가 있습니다.
📌StudentPrinter 클래스
  • 학생 정보를 출력하는 역할을 가집니다.
  • Stuent 클래스와는 분리되어 있어야 합니다.
📌사용자가 `Student` 객체를 생성하고 이를 `StudentPrinter`로 출력할 수 있도록 구현합니다. 완성된 모습


[출력 예시]

학생 이름: John Doe
학생 나이: 20

실습 코드

#include<string>
#include<iostream>
using namespace std;
class Student{
    private:
        string name;
        int age;
    public:
        //생성자
        Student(string StudentName,int studentAge):name(StudentName),age(studentAge){}
        //학생 이름 가져오기
        string getName(){ 
            return name;
        }
         //학생 나이 가져오기
        int getAge(){
            return age;
        }
         //학생 정보를 문자열로 반환
        string getInfo(){
            string info = "학생 이름 :" + name + "\n" + "학생 나이 : " + to_string(age);
            return info;
        }
};

//학생 정보 출력하는 클래스
class StudentPrinter{
    public:
        void print(Student& student){ //참조 처리로 불필요한 객체 복사 방지
            cout<<student.getInfo()<<endl;
        }
};

int main(){
    Student student("John doe",20);
    StudentPrinter studentPrinter;
    studentPrinter.print(student);
}

객체 지향적 설계 실습 2.

TO-DO LIST 구현하기
필수 요구사항

  • 사용자가 할 일을 추가할 수 있어야 합니다.
  • 사용자가 현재 할 일 목록을 확인할 수 있어야 합니다.
  • 사용자가 특정할 일을 완료 처리할 수 있어야 합니다.
📌

클래스 설계

  • Task 클래스
    • 할 일의 데이터를 관리합니다. 할 일의 내용과 완료 여부를 포함합니다.
  • IStorage 클래스
    • 할 일을 저장하고 불러오는 동작을 추상화합니다.
    • 현재 어떤 Storage를 사용하고 있는지 반환합니다.
  • MemoryStorage 클래스
    • 할 일을 메모리에 저장합니다.
    • IStorage 인터페이스를 구현한 간단한 버전입니다.
  • DBStorage
    • 할 일을 DB에 저장합니다.
    • 실제 DB를 구축하는게 아니라 DB를 사용하고 있다고 반환만 하면 됩니다.
      (개념상 DB이지 실제 DB가 아닙니다.)
  • TasKManager 클래스
    - Task 객체를 추가, 조회, 완료 처리하는 기능을 제공합니다.
**제공되는 코드**
#include <iostream>
#include <vector>
#include <string>

using namespace std;


class TaskManager {
    IStorage* storage;

public:
    TaskManager(IStorage* s) : storage(s) {}

    // 새로운 할 일 추가
    void addTask(const string& description) {
        storage->addTask(Task(description));
    }

    // 현재 할 일 목록 출력
    void showTasks() {
        cout << "저장 방식: " << storage->getStorageType() << "\n";
        vector<Task> tasks = storage->getTasks();
        for (size_t i = 0; i < tasks.size(); ++i) {
            cout << i + 1 << ". " << tasks[i].getDescription();
            if (tasks[i].isCompleted()) cout << " [완료]";
            cout << "\n";
        }
    }

    // 특정 할 일을 완료 처리
    void completeTask(size_t index) {
        vector<Task> tasks = storage->getTasks();
        if (index > 0 && index <= tasks.size()) {
            tasks[index - 1].complete();
            storage->addTask(tasks[index - 1]); // 변경된 할 일 저장
        }
        else {
            cout << "잘못된 번호입니다.\n";
        }
    }
};



// 메인 함수
int main() {
    cout << "=== MemoryStorage로 작업 ===\n";
    MemoryStorage memoryStorage;
    TaskManager manager1(&memoryStorage);

    manager1.addTask("C++ 과제 작성하기");
    manager1.addTask("SOLID 원칙 공부하기");

    cout << "\n현재 할 일 목록:\n";
    manager1.showTasks();

    manager1.completeTask(1);

    cout << "\n업데이트된 할 일 목록:\n";
    manager1.showTasks();

    cout << "\n=== DBStorage로 작업 ===\n";
    DBStorage dbStorage;
    TaskManager manager2(&dbStorage);

    manager2.addTask("DB 작업 테스트");
    manager2.addTask("To-Do 목록 추가");

    cout << "\n현재 할 일 목록:\n";
    manager2.showTasks();

    manager2.completeTask(2);

    cout << "\n업데이트된 할 일 목록:\n";
    manager2.showTasks();

    return 0;
}

완성된 모습

[실제 제공된 메인함수의 출력 예시 입니다]

=== MemoryStorage로 작업 ===

현재 할 일 목록:
저장 방식: 메모리 저장소
1. C++ 과제 작성하기
2. SOLID 원칙 공부하기

업데이트된 할 일 목록:
저장 방식: 메모리 저장소
1. C++ 과제 작성하기
2. SOLID 원칙 공부하기
3. C++ 과제 작성하기 [완료]

=== DBStorage로 작업 ===
DB에 할 일 추가: DB 작업 테스트
DB에 할 일 추가: To-Do 목록 추가

현재 할 일 목록:
저장 방식: DB 저장소 (시뮬레이션)
DB에서 할 일 가져오기
1. DB 작업 테스트
2. To-Do 목록 추가
DB에서 할 일 가져오기
DB에 할 일 추가: To-Do 목록 추가

업데이트된 할 일 목록:
저장 방식: DB 저장소 (시뮬레이션)
DB에서 할 일 가져오기
1. DB 작업 테스트
2. To-Do 목록 추가
3. To-Do 목록 추가 [완료]

실습 코드

#include <iostream>
#include <vector>
#include <string>

using namespace std;

class Task{
    public:
        Task(string desc) : description(desc),completed(false){}
        void Complete(){
            completed =true;
        }
        string getDescription(){
            return description;
        }
        bool isCompleted(){
            return completed;
        }
    private:
        string description;
        bool completed;
};

class IStorage{
    public:
        virtual void addTask(Task task) =0;
        virtual vector<Task> getTasks()=0;
        virtual string getStorageType()=0;
};

class MemoryStorage : public IStorage{
    private:
        vector<Task> tasks;
    public:
        vector<Task> getTasks(){
            return tasks;
        }
        void addTask(Task task){
            tasks.push_back(task);
        }
        string getStorageType(){
            return "메모리 저장소";
        }
};

class DBStorage : public IStorage{
    private:
        vector<Task> tasks;
    public:
        vector<Task> getTasks(){
            return tasks;
        }
        void addTask(Task task){
            tasks.push_back(task);
        }
        string getStorageType(){
            return "DB 저장소 (시뮬레이션)";
        }
};

class TaskManager {
    IStorage* storage;

public:
    TaskManager(IStorage* s) : storage(s) {}

    // 새로운 할 일 추가
    void addTask(const string& description) {
        storage->addTask(Task(description));
    }

    // 현재 할 일 목록 출력
    void showTasks() {
        cout << "저장 방식: " << storage->getStorageType() << "\n";
        vector<Task> tasks = storage->getTasks();
        for (size_t i = 0; i < tasks.size(); ++i) {
            cout << i + 1 << ". " << tasks[i].getDescription();
            if (tasks[i].isCompleted()) cout << " [완료]";
            cout << "\n";
        }
    }

    // 특정 할 일을 완료 처리
    void completeTask(size_t index) {
        vector<Task> tasks = storage->getTasks();
        if (index > 0 && index <= tasks.size()) {
            tasks[index - 1].Complete();
            storage->addTask(tasks[index - 1]); // 변경된 할 일 저장
        }
        else {
            cout << "잘못된 번호입니다.\n";
        }
    }
};



// 메인 함수
int main() {
    cout << "=== MemoryStorage로 작업 ===\n";
    MemoryStorage memoryStorage;
    TaskManager manager1(&memoryStorage);

    manager1.addTask("C++ 과제 작성하기");
    manager1.addTask("SOLID 원칙 공부하기");

    cout << "\n현재 할 일 목록:\n";
    manager1.showTasks();

    manager1.completeTask(1);

    cout << "\n업데이트된 할 일 목록:\n";
    manager1.showTasks();

    cout << "\n=== DBStorage로 작업 ===\n";
    DBStorage dbStorage;
    TaskManager manager2(&dbStorage);

    manager2.addTask("DB 작업 테스트");
    manager2.addTask("To-Do 목록 추가");

    cout << "\n현재 할 일 목록:\n";
    manager2.showTasks();

    manager2.completeTask(2);

    cout << "\n업데이트된 할 일 목록:\n";
    manager2.showTasks();

    return 0;
}

라이브 세션

C++ 개념 강의

객체

함수,변수만으로 구현하기에는 프로그램 사이즈가 많게는 3명~20명, 3개월~2년 이런식으로 사이즈가 커지게 되어서 다 담아내기에는 한계 발생
해결하기 위해서
1. 코드 분리
2. 코드를 마치 현실의 사물처럼 생각(클래스 형태로 분리)
객체지향적 방법론
객체지향적 사고->기계적인 코드 형태에서 벗어나게 됨
객체지향 -> 클래스는 사물이다.
사물(클래스)과 사물(클래스)끼리 상호작용할 수 있게 하는 것

캡슐화
비유 예시)
회사에서 업무를 분배
A는 캐릭터의 데이터에 대해서 맡음
A는 Character Manager라는 클래스를 만듬
B는 캐릭터의 액션에 대해서 맡음
B는 Character Action이라는 클래스를 만듬
A와 B가 서로의 클래스 변수나 함수를 임의 수정하게 되면
혼잡한 상황 발생

보통 클래스 1개는 용도가 1개
담당 프로그래머가 하나의 클래스만 보고 작업하는게 편함
용도 책임에 따라서 클래스로 분리

클래스와 객체의 차이
클래스는 이 객체는 이렇게 생겼다는 설계도(사용자 정의 타입)
클래스는 데이터와 행동으로 구성

지갑을 클래스로 만들 경우
class Wallet 안에는
데이터는 카드갯수, 현금, 라이센스
행동은 카드 넣기, 빼기, 현금 사용, 추가

캡슐화는 외부에서 내 소중한 정보에 접근(읽기/쓰기)하지 못하게 막는 것
필요한 정보만 가공해서 제공하게끔 하는 것
객체 입장에서 외부 접근 컨트롤
Getter는 변수값을 보내는 함수
Setter는 변수값을 설정하는 함수

생성자-소멸자
생성자는 객체 생성 시 호출됨
매개변수로 처리해도 되지 않냐?
객체 생성 시작과 함께 세팅하게 하려고
클래스는 생성자 작성 안해도 기본 생성자가 있음
소멸자는 객체 삭제 시 호출됨
소멸자는 파라미터가 존재하지 않음
객체가 언제 어떻게 제거될지 모르기 때문에

  • 지역 변수로 선언된 객체는 함수 끝날 때 소멸
  • 동적 할당하고 delete로 제거하면 소멸
    객체 소멸은 내가 명시적으로 제거하는 경우가 아닌 케이스들 존재해서 소멸할 때 파라미터 못넣는다.

상속-가상 함수
상속
부모->자식 클래스
가상 함수
부모 포인터로 자식을 가리킴
자식에서 구현

profile
개발합시다!

0개의 댓글