11. 구조체, 열거형, 클래스: 사용자 정의 타입 만들기
구조체, 열거형, 클래스: 사용자 정의 타입 만들기

기본 자료형만으로는 세상이 너무 넓다
지금까지 우리는 int, double, char, bool, std::string 같은 기본 도구로 많은 것을 만들었습니다. 하지만 현실의 문제를 표현하려고 하면 금방 답답해집니다.
학생을 표현하고 싶다면 학번, 이름, 나이, 평균 점수가 함께 움직여야 합니다. 게임 캐릭터를 표현하고 싶다면 이름, 체력, 직업, 위치가 따로따로 흩어져 있으면 관리가 어렵습니다. 은행 계좌를 표현하려면 잔액뿐 아니라 입금과 출금 규칙도 같이 있어야 자연스럽습니다.
C++은 이런 문제를 해결하기 위해 사용자 정의 타입을 만들 수 있게 해 줍니다. 즉, 언어가 기본으로 제공하는 타입만 쓰는 것이 아니라, 우리가 프로그램에 맞는 새로운 타입을 직접 정의할 수 있습니다. 그리고 이런 타입은 사용하기 전에 먼저 정의되어 있어야 합니다.1
이번 장에서 다룰 세 가지 도구는 다음과 같습니다.
struct: 관련 있는 데이터를 한데 묶을 때 좋습니다.enum/enum class: 정해진 선택지 중 하나를 표현할 때 좋습니다.class: 데이터와 동작을 함께 묶고, 사용 규칙까지 관리할 때 좋습니다.
한마디로 말하면, 이제부터는 숫자와 문자만 다루는 단계에서 의미 있는 개념을 직접 설계하는 단계로 넘어갑니다.
struct: 관련 있는 데이터를 한 상자에 담기
가장 먼저 익히기 좋은 것은 struct입니다. struct는 여러 데이터를 하나의 이름 아래 묶어 주는 도구입니다.
예를 들어 학생 정보를 따로따로 저장하면 이런 식이 됩니다.
int studentId = 1001;
int age = 20;
double gpa = 3.8;
std::string name = "Kim";이 코드는 당장 동작은 하지만, 학생이 여러 명 생기면 금방 복잡해집니다. student1Age, student2Age, student3Age 같은 이름이 끝도 없이 등장할 것입니다.
이럴 때 struct를 쓰면 훨씬 자연스럽습니다.
#include <iostream>
#include <string>
struct Student {
int id;
int age;
double gpa;
std::string name;
};
int main() {
Student s1{1001, 20, 3.8, "Kim"};
std::cout << s1.name << 'n';
std::cout << s1.age << 'n';
}이제 Student라는 새로운 타입이 생겼습니다. s1은 그 타입의 변수이고, 점(.) 연산자로 내부 멤버에 접근합니다.
struct는 "학생", "좌표", "상품 정보", "날짜"처럼 서로 관련 있는 값들을 묶는 용도로 아주 잘 어울립니다.
struct는 왜 편할까
struct의 장점은 단순히 예쁘다는 데 있지 않습니다. 의미가 생긴다는 것이 핵심입니다.
예를 들어 2차원 좌표를 배열로 표현할 수도 있습니다.
int point[2]{3, 5};하지만 point[0]이 x인지 y인지 매번 기억해야 합니다. 반면 struct를 쓰면 코드가 바로 읽힙니다.
struct Point {
int x;
int y;
};
Point p{3, 5};
std::cout << p.x << ", " << p.y << 'n';p.x와 p.y는 읽는 순간 뜻이 보입니다. 초보자일수록 이런 "뜻이 보이는 코드"가 큰 도움이 됩니다.
struct의 기본 성격
C++에서 struct와 class는 생각보다 매우 비슷합니다. 가장 중요한 차이는 기본 접근 지정자입니다. struct의 멤버는 기본적으로 public이고, class의 멤버는 기본적으로 private입니다.2
이 말은 곧, struct는 기본적으로 바깥에서 바로 꺼내 쓰기 쉬운 상자에 가깝고, class는 기본적으로 내부를 감추는 상자에 가깝다는 뜻입니다.
그래서 보통은 다음처럼 감각적으로 구분해서 씁니다.
값 몇 개를 단순히 묶을 때는
struct데이터와 규칙을 함께 관리할 때는
class외부에서 마음대로 바꾸면 안 되는 상태가 있을 때는
class
물론 절대 규칙은 아닙니다. 하지만 처음 배울 때는 이 기준이 꽤 도움이 됩니다.
struct에도 함수가 들어갈 수 있다
많은 초보자가 struct는 데이터만 넣는 통이라고 생각합니다. 하지만 C++의 struct는 함수도 가질 수 있습니다.
#include <iostream>
struct Point {
int x{};
int y{};
void move(int dx, int dy) {
x += dx;
y += dy;
}
void print() const {
std::cout << "(" << x << ", " << y << ")n";
}
};
int main() {
Point p{1, 2};
p.move(3, -1);
p.print();
}이 코드를 보면 Point는 단순한 데이터 묶음이면서도, 자기 자신을 움직이는 동작까지 가지고 있습니다. 아주 작은 객체 느낌이 나기 시작합니다.
enum: 정해진 선택지를 이름으로 표현하기
이제 enum을 봅시다. 어떤 변수는 아무 값이나 가질 수 있는 것이 아니라, 정해진 후보 중 하나만 가져야 합니다.
예를 들어 신호등 색은 빨강, 노랑, 초록 중 하나면 됩니다. 학생의 학년은 1학년, 2학년, 3학년, 4학년 정도로 정해질 수 있습니다. 게임 캐릭터의 상태도 대기, 이동, 공격, 사망처럼 제한된 목록으로 표현할 수 있습니다.
이럴 때 숫자로만 처리하면 코드가 금방 수수께끼가 됩니다.
int state = 2; // 2가 뭘 의미하지?이런 코드는 작성한 본인도 며칠 뒤에 보면 멍해집니다. 그래서 이름 붙은 상수 집합인 enum을 씁니다.
enum Direction {
North,
East,
South,
West
};이제 Direction 타입의 변수는 방향을 표현한다는 뜻이 분명해집니다.
Direction dir = East;enum class를 더 자주 쓰는 이유
C++에서는 enum보다 enum class를 더 선호하는 편입니다. 이유는 두 가지입니다.
첫째, 이름 충돌을 줄여 줍니다. 둘째, 타입 안전성이 더 좋습니다. enum class의 열거자들은 자기 범위 안에 있고, 정수로 암시적으로 바뀌지 않습니다.3
예를 들어 다음처럼 쓰면 의미가 분명합니다.
enum class Job {
Warrior,
Archer,
Mage
};
Job playerJob = Job::Mage;여기서 Job::Mage라고 적는 순간, 이 값이 Job 타입에 속한 값이라는 사실이 눈에 잘 들어옵니다.
또한 enum class는 그냥 정수처럼 마구 섞어 쓰기 어렵습니다. 초보자에게는 이 불편함이 오히려 안전장치입니다.
enum 값은 내부적으로 숫자와 연결된다
열거형의 값은 내부적으로 정수값과 연결됩니다. 값을 따로 지정하지 않으면 첫 번째가 0, 그다음이 1, 2, 3처럼 증가합니다. 필요하면 직접 숫자를 지정할 수도 있습니다.4
enum class HttpStatus {
OK = 200,
NotFound = 404,
ServerError = 500
};이런 식으로 쓰면 코드의 의미가 더 또렷해집니다.
다만 초보자라면 한 가지를 기억합시다. 열거형의 진짜 목적은 숫자 계산이 아니라 의미 표현입니다. 숫자가 보이더라도, 핵심은 "이 값이 어떤 상태를 뜻하는가"에 있습니다.
class: 데이터와 동작을 하나로 묶기
이제 class를 봅시다. class는 struct와 비슷하지만, 더 적극적으로 데이터와 동작, 그리고 사용 규칙을 묶는 데 쓰입니다.
예를 들어 게임 캐릭터를 생각해 봅시다. 캐릭터에게는 이름과 체력이 있고, 데미지를 입는 동작이 있습니다. 여기까지는 struct로도 만들 수 있습니다. 하지만 체력이 음수가 되면 안 된다는 규칙까지 지키고 싶다면 class가 더 잘 어울립니다.
#include <iostream>
#include <string>
class Character {
private:
std::string name;
int hp;
public:
void setName(const std::string& newName) {
name = newName;
}
void setHp(int newHp) {
if (newHp < 0) {
hp = 0;
} else {
hp = newHp;
}
}
void takeDamage(int damage) {
hp -= damage;
if (hp < 0) {
hp = 0;
}
}
void printStatus() const {
std::cout << name << "의 체력: " << hp << 'n';
}
};
int main() {
Character hero;
hero.setName("Aria");
hero.setHp(100);
hero.takeDamage(30);
hero.printStatus();
}여기서 중요한 점은 hp를 아무 데서나 직접 바꾸지 않게 했다는 것입니다. 대신 setHp()나 takeDamage() 같은 함수를 통해서만 상태를 변경합니다.
이 방식의 장점은 분명합니다. 캐릭터 체력이 음수가 되지 않는다는 규칙을 타입 스스로 지킬 수 있다는 점입니다.
객체란 무엇일까
class를 배우면 꼭 따라오는 단어가 있습니다. 바로 객체(object) 입니다.
어렵게 들리지만 뜻은 단순합니다.
클래스는 설계도
객체는 설계도로 만든 실제 대상
예를 들어 Character는 캐릭터의 설계도입니다. hero, boss, npc1 같은 실제 변수들은 그 설계도로 만들어진 객체입니다.
Character hero;
Character boss;둘 다 같은 Character 타입이지만, 각자 자기 이름과 체력을 따로 가집니다. 마치 같은 붕어빵 틀로 만들었지만, 서로 다른 붕어빵 두 개가 생긴 것과 비슷합니다. 하나를 한입 베어 문다고 다른 하나까지 줄어들지는 않습니다.
public, private, protected는 문지기다
클래스 안에는 접근 범위를 정하는 문지기가 있습니다. 바로 접근 지정자입니다.
public: 바깥에서도 사용할 수 있습니다.private: 클래스 내부에서만 사용할 수 있습니다.protected: 클래스 내부와 특별한 관계의 클래스에서 사용할 수 있습니다.
초보자에게 가장 중요한 것은 public과 private입니다. class의 멤버는 기본적으로 private입니다.2
즉, 아무 표시가 없으면 바깥에서 직접 만질 수 없다고 생각하면 됩니다.
class Account {
int balance; // 기본적으로 private
};이 특징 때문에 class는 "마구 손대면 안 되는 데이터"를 다루기에 좋습니다.
class는 규칙을 지키게 만든다
다음 예제를 봅시다.
class BankAccount {
private:
int balance{0};
public:
void deposit(int amount) {
if (amount > 0) {
balance += amount;
}
}
bool withdraw(int amount) {
if (amount <= 0 || amount > balance) {
return false;
}
balance -= amount;
return true;
}
int getBalance() const {
return balance;
}
};이 클래스는 은행 계좌라는 개념을 꽤 잘 표현합니다.
잔액은 밖에서 마음대로 -1000000으로 바꿀 수 없습니다. 반드시 입금과 출금 규칙을 따라야 합니다. 바로 이런 점 때문에 클래스를 쓰는 것입니다. 데이터를 저장하기 위해서만이 아니라, 올바르게 사용하게 만들기 위해서입니다.
struct와 class는 어떻게 구분해서 쓸까
둘 다 새로운 타입을 만드는 도구이기 때문에 처음엔 헷갈립니다. 가장 쉬운 기준은 "규칙의 강도"입니다.
단순한 좌표나 색상, 날짜처럼 값을 담아두는 성격이 강하면 struct가 잘 어울립니다.
struct Date {
int year;
int month;
int day;
};반면 상태를 보호해야 하거나, 특정 방식으로만 다뤄야 하는 데이터는 class가 잘 어울립니다.
class Timer {
private:
int seconds;
public:
void setSeconds(int value) {
if (value >= 0) {
seconds = value;
}
}
};딱 잘라 말하면 이렇습니다.
struct는 묶기에 강하고, class는 관리하기에 강합니다.
enum, struct, class를 함께 써 보기
세 가지를 따로 배우는 것도 중요하지만, 진짜 재미는 같이 쓸 때 나옵니다.
아래 예제는 직업은 enum class, 위치는 struct, 플레이어는 class로 표현한 코드입니다.
#include <iostream>
#include <string>
struct Position {
int x{};
int y{};
};
enum class Job {
Warrior,
Archer,
Mage
};
class Player {
private:
std::string name;
Job job;
int hp;
Position pos;
public:
void init(const std::string& newName, Job newJob, int newHp, Position newPos) {
name = newName;
job = newJob;
hp = (newHp < 0) ? 0 : newHp;
pos = newPos;
}
void move(int dx, int dy) {
pos.x += dx;
pos.y += dy;
}
void print() const {
std::cout << "이름: " << name << 'n';
std::cout << "체력: " << hp << 'n';
std::cout << "위치: (" << pos.x << ", " << pos.y << ")n";
}
};
int main() {
Player p;
p.init("Luna", Job::Mage, 120, {0, 0});
p.move(3, 2);
p.print();
}이 예제는 꽤 많은 것을 보여 줍니다.
Job은 아무 숫자나 받지 않고 정해진 직업만 가집니다. Position은 x와 y를 함께 묶습니다. Player는 이름, 직업, 체력, 위치를 하나로 묶고 움직이기라는 동작도 가집니다.
이제 프로그램은 단순히 숫자를 굴리는 것이 아니라, 플레이어라는 개념을 직접 다루기 시작합니다.
생성자라는 것도 있다
객체를 만들 때 자동으로 호출되는 특별한 함수도 있습니다. 이것을 생성자라고 합니다.2
초보 단계에서는 일단 "객체가 태어날 때 초기값을 정해 주는 도구"라고 이해하면 충분합니다.
class Book {
private:
std::string title;
int price;
public:
Book(const std::string& t, int p)
: title(t), price(p) {
}
void print() const {
std::cout << title << ", " << price << "원n";
}
};이제 객체를 만들면서 바로 값을 넣을 수 있습니다.
Book b{"C++ 입문", 25000};이 문법은 처음엔 조금 낯설지만, "만들면서 초기화한다"는 의미로 보면 이해가 됩니다.
초보자가 자주 하는 실수
1. 타입 정의 끝의 세미콜론을 빼먹는다
struct, class, enum 정의 뒤에는 세미콜론이 필요합니다.
struct Point {
int x;
int y;
}; // 꼭 필요이 세미콜론을 빼먹으면 에러 메시지가 이상한 곳에서 터져서 사람을 당황하게 만듭니다.
2. class 멤버가 왜 안 보이는지 모른다
class는 기본이 private라서 바깥에서 바로 접근되지 않습니다.2
class Test {
int x;
};
int main() {
Test t;
// t.x = 10; // 오류
}안 보이는 것이 정상입니다. 보이게 하려면 public: 아래에 두거나, 함수를 통해 다루어야 합니다.
3. 상태를 숫자로 대충 표현한다
직업, 방향, 상태 같은 값을 그냥 int로 처리하면 나중에 코드를 읽기 매우 힘들어집니다.
int mode = 3; // 3이 뭐였더라?이럴 때는 enum class를 떠올립시다.
enum class Mode {
Easy,
Normal,
Hard
};코드가 훨씬 또렷해집니다.
4. 관련 없는 데이터를 따로따로 흩어 놓는다
이름, 체력, 위치처럼 하나의 대상을 이루는 정보는 함께 묶는 습관이 중요합니다. 묶지 않으면 함수 인자가 길어지고, 실수도 늘고, 코드의 의미도 흐려집니다.
좋은 사용자 정의 타입은 어떤 모습일까
좋은 타입은 거창하지 않습니다. 읽는 사람이 "아, 이건 학생이구나", "이건 좌표구나", "이건 계좌구나" 하고 바로 이해할 수 있으면 좋습니다.
또 좋은 타입은 자기 규칙을 스스로 지키려 합니다. 예를 들어 체력이 음수가 되지 않게 하거나, 잔액보다 많이 출금하지 못하게 하거나, 방향이 엉뚱한 숫자가 되지 않게 만듭니다.
즉, 좋은 사용자 정의 타입은 다음 질문에 잘 답합니다.
이 타입은 무엇을 표현하는가?
어떤 값이 들어가야 자연스러운가?
어떤 행동을 할 수 있는가?
어떤 잘못된 상태를 막아야 하는가?
이 질문을 떠올리며 타입을 만들면 코드가 훨씬 단단해집니다.
정리
기본 자료형은 벽돌과 같습니다. 벽돌만으로도 뭔가 만들 수는 있지만, 집의 의미는 자동으로 생기지 않습니다. struct, enum, class는 벽돌을 가지고 방을 만들고, 문을 만들고, 집이라는 형태를 만드는 도구입니다.
struct는 관련 있는 데이터를 자연스럽게 묶어 줍니다. enum class는 정해진 선택지에 이름을 붙여 코드를 또렷하게 만듭니다. class는 데이터와 동작, 그리고 사용 규칙을 한곳에 모아 객체를 더 안전하고 의미 있게 다루게 해 줍니다.
C++를 잘한다는 것은 문법을 많이 외우는 것만이 아닙니다. 문제를 보고, 거기에 맞는 타입을 설계하는 능력을 키우는 것입니다. 이제부터는 int와 double만 쳐다보지 말고, "내 프로그램에는 어떤 타입이 필요하지?"라는 질문도 함께 던져 봅시다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.