9. 구조체, 공용체, 열거형으로 데이터 모델링하기
지금까지는 주로 숫자 하나, 문자 하나, 배열 하나처럼 "낱개 데이터"를 다뤘습니다. 그런데 실제 프로그램은 그렇게 단순하지 않습니다. 학생 한 명을 표현하려면 이름, 학년, 점수, 연락처가 함께 필요하고, 상품 하나를 표현하려면 이름, 가격, 재고, 상태가 같이 움직여야 합니다.
이럴 때 int, double, char[]를 제각각 따로 들고 다니면 코드가 금방 복잡해집니다. 누가 누구의 정보인지 헷갈리고, 함수에 넘길 값도 많아지고, 실수도 늘어납니다. C 언어는 이런 문제를 해결하려고 struct, union, enum을 제공합니다.12
이 챕터의 핵심은 간단합니다. 현실의 데이터를 코드 속에서 그럴듯하게 표현하는 방법을 배우는 것입니다. 이 감각이 생기면 프로그램이 갑자기 훨씬 실용적으로 보이기 시작합니다.

왜 데이터 모델링이 중요할까
예를 들어 학생 정보를 저장한다고 해 봅시다.
이렇게 따로따로 만들 수도 있습니다.
char name[20];
int age;
double score;학생이 한 명이면 그럭저럭 버틸 수 있습니다. 하지만 학생이 30명이 되면 어떨까요? 이름 배열 하나, 나이 배열 하나, 점수 배열 하나를 따로 관리해야 합니다. 이때 name[3]의 학생과 score[3]의 학생이 정말 같은 사람인지, 코드만 봐서는 쉽게 놓칠 수 있습니다.
그래서 관련 있는 데이터를 하나로 묶는 것이 중요합니다. 학생은 학생답게, 상품은 상품답게, 좌표는 좌표답게 표현해야 합니다. 이때 가장 먼저 등장하는 도구가 구조체입니다.
구조체: 관련 있는 데이터를 하나로 묶기
구조체(struct)는 서로 다른 자료형의 데이터를 하나로 묶어 새로운 형태처럼 다루게 해 주는 도구입니다.13
학생을 표현하는 구조체를 만들어 보겠습니다.
#include <stdio.h>
struct Student {
char name[20];
int age;
double score;
};
int main(void) {
struct Student s1;
return 0;
}struct Student는 이제 "학생 정보 하나"를 담는 틀입니다. s1은 그 틀로 만든 실제 변수입니다.
구조체의 각 내부 변수는 멤버(member)라고 부릅니다. name, age, score가 바로 멤버입니다.
구조체 멤버에 값 넣고 꺼내기
구조체 변수의 멤버에 접근할 때는 점(.) 연산자를 사용합니다.
#include <stdio.h>
#include <string.h>
struct Student {
char name[20];
int age;
double score;
};
int main(void) {
struct Student s1;
strcpy(s1.name, "Kim");
s1.age = 20;
s1.score = 95.5;
printf("이름: %sn", s1.name);
printf("나이: %dn", s1.age);
printf("점수: %.1fn", s1.score);
return 0;
}여기서 눈여겨볼 점이 하나 있습니다. age와 score는 그냥 =로 값을 넣었지만, name은 strcpy()를 사용했습니다. 이유는 name이 문자열처럼 보이지만 실제로는 char 배열이기 때문입니다. 배열은 선언 뒤에 통째로 = 대입할 수 없으므로 복사 함수로 값을 넣어야 합니다.4
초보자가 아주 자주 하는 실수는 아래와 같습니다.
s1.name = "Kim"; // 오류문자열 멤버가 배열이라면 이렇게 직접 대입할 수 없습니다. 이 부분은 문자열 챕터의 내용과 연결됩니다.
구조체 변수는 선언하면서 초기화할 수 있다
구조체 변수는 선언할 때 바로 초기화할 수 있습니다.
struct Student s1 = {"Kim", 20, 95.5};이 방법은 간단하지만, 값의 순서를 구조체 선언 순서와 정확히 맞춰야 합니다.
그래서 C99부터는 지정 초기화(designated initializer)도 자주 씁니다.5
struct Student s2 = {
.age = 21,
.name = "Lee",
.score = 88.0
};이 방식은 순서를 바꿔도 되고, 어떤 값이 어떤 멤버에 들어가는지 눈에 잘 들어와서 읽기 편합니다.
다만 주의할 점도 있습니다. 구조체 정의 안에서는 멤버를 직접 초기화할 수 없습니다.
struct Student {
char name[20];
int age = 20; // 오류
double score;
};초기화는 구조체를 정의하는 곳이 아니라, 구조체 변수를 만들 때 해야 합니다.35
typedef로 구조체 이름을 더 편하게 쓰기
구조체를 많이 쓰다 보면 struct Student를 계속 쓰는 것이 조금 길게 느껴집니다. 이럴 때 typedef를 함께 사용하면 더 간단하게 쓸 수 있습니다.26
#include <stdio.h>
typedef struct {
char title[30];
int price;
} Book;
int main(void) {
Book b1 = {"C Programming", 25000};
printf("책 제목: %sn", b1.title);
printf("가격: %dn", b1.price);
return 0;
}이제 struct Book 대신 그냥 Book이라고 쓸 수 있습니다.
엄밀히 말하면 typedef는 완전히 새로운 타입을 만드는 것이라기보다, 기존 타입에 별칭(alias)을 붙이는 도구에 가깝습니다.7 하지만 초보자 입장에서는 "이제 Book이 하나의 타입처럼 보이는구나" 정도로 이해해도 충분합니다.
구조체는 배열로도 만들 수 있다
학생이 여러 명이면 구조체 배열을 만들면 됩니다.
#include <stdio.h>
typedef struct {
char name[20];
int age;
double score;
} Student;
int main(void) {
Student students[3] = {
{"Kim", 20, 95.5},
{"Lee", 21, 88.0},
{"Park", 19, 91.2}
};
for (int i = 0; i < 3; i++) {
printf("%s %d %.1fn",
students[i].name,
students[i].age,
students[i].score);
}
return 0;
}이제 학생 3명의 정보가 각각 하나의 덩어리로 저장됩니다. students[i] 하나가 학생 한 명입니다. 코드를 읽는 사람 입장에서는 훨씬 자연스럽습니다.
배열 챕터에서 우리는 같은 타입의 값을 여러 개 저장하는 법을 배웠습니다. 구조체 배열은 그 아이디어를 한 단계 확장한 것입니다. "복잡한 데이터 한 개"를 여러 개 저장하는 방식이라고 생각하면 됩니다.
구조체 안에 구조체 넣기
현실 데이터는 한 겹으로 끝나지 않는 경우가 많습니다. 사람은 생년월일을 가지고 있고, 생년월일은 다시 연도, 월, 일로 나뉩니다. 이런 계층 구조를 표현할 때는 구조체 안에 다른 구조체를 넣으면 됩니다.2
#include <stdio.h>
typedef struct {
int year;
int month;
int day;
} Date;
typedef struct {
char name[20];
Date birth;
} Person;
int main(void) {
Person p = {"Kim", {1995, 5, 5}};
printf("이름: %sn", p.name);
printf("생일: %d-%d-%dn",
p.birth.year, p.birth.month, p.birth.day);
return 0;
}p.birth.year처럼 점을 여러 번 이어서 접근할 수 있습니다.
이 방법은 프로그램 구조를 훨씬 깔끔하게 만듭니다. 날짜 관련 멤버를 여기저기 흩뿌리는 대신, Date라는 의미 있는 덩어리로 묶어 표현할 수 있기 때문입니다.
구조체끼리는 대입이 가능하다
배열은 배열끼리 통째로 = 대입할 수 없지만, 구조체는 같은 타입끼리 대입할 수 있습니다.5
#include <stdio.h>
typedef struct {
char name[20];
int age;
} Person;
int main(void) {
Person p1 = {"Kim", 20};
Person p2;
p2 = p1;
printf("p2 이름: %sn", p2.name);
printf("p2 나이: %dn", p2.age);
return 0;
}이 코드는 p1의 멤버 값을 p2로 복사합니다. 초보자에게는 꽤 신기한 장면입니다. 문자열 배열이 들어 있어도 구조체 전체 대입은 가능합니다.
다만 나중에 동적 메모리를 배우고 구조체 안에 포인터가 들어가면 이야기가 조금 복잡해집니다. 그때는 겉껍데기만 복사되는 얕은 복사(shallow copy) 문제가 생길 수 있습니다.5 지금은 "구조체 대입이 가능하다" 정도만 정확히 기억하면 충분합니다.
구조체와 함수: 값을 넘길까, 주소를 넘길까
구조체도 함수의 인자로 전달할 수 있고, 함수의 반환값으로도 사용할 수 있습니다.3
먼저 값을 복사해서 넘기는 예를 보겠습니다.
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
void printPoint(Point p) {
printf("(%d, %d)n", p.x, p.y);
}
int main(void) {
Point pt = {3, 5};
printPoint(pt);
return 0;
}이 방식은 쉽지만, 큰 구조체를 계속 복사하면 비효율적일 수 있습니다. 또 함수 안에서 값을 바꿔도 원본은 바뀌지 않습니다.
원본을 바꾸고 싶다면 구조체의 주소를 넘겨야 합니다.
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
void movePoint(Point *p, int dx, int dy) {
p->x += dx;
p->y += dy;
}
int main(void) {
Point pt = {3, 5};
movePoint(&pt, 2, 4);
printf("(%d, %d)n", pt.x, pt.y);
return 0;
}여기서 p->x는 (*p).x를 줄여 쓴 형태입니다.8 포인터 챕터에서 배운 내용을 떠올리면 됩니다. 구조체 포인터로 멤버에 접근할 때는 점(.)이 아니라 화살표(->)를 씁니다.
열거형: 숫자에 의미 있는 이름 붙이기
이번에는 enum을 볼 차례입니다. 열거형(enum)은 관련 있는 정수 상수 집합에 이름을 붙이는 도구입니다.29
예를 들어 주문 상태를 숫자로 표현한다고 해 봅시다.
int status = 2;이 코드만 보면 2가 무슨 뜻인지 알 수 없습니다. 결제 완료인지, 배송 중인지, 취소인지 아무도 모릅니다. 이런 숫자를 매직 넘버(magic number)라고 부릅니다. 뜻이 숨어 있어서 읽기 어렵고, 실수도 잘 납니다.
열거형을 쓰면 훨씬 명확해집니다.
#include <stdio.h>
enum OrderStatus {
ORDERED,
PAID,
SHIPPED,
DELIVERED
};
int main(void) {
enum OrderStatus status = SHIPPED;
if (status == SHIPPED) {
printf("상품이 배송 중입니다.n");
}
return 0;
}이제 status == SHIPPED는 읽는 순간 의미가 보입니다. 코드가 설명을 시작한 셈입니다.
열거형의 값은 기본적으로 0부터 시작한다
열거형 멤버는 기본적으로 0부터 시작해서 1씩 증가합니다.9
enum Weekday { MON, TUE, WED, THU, FRI, SAT, SUN };이때 값은 다음과 같습니다.
MON은 0TUE는 1WED는 2...
SUN은 6
원하면 직접 값을 지정할 수도 있습니다.
enum Status {
OK = 200,
NOT_FOUND = 404,
SERVER_ERROR = 500
};또는 중간부터 시작할 수도 있습니다.
enum Level {
LOW = 1,
MEDIUM,
HIGH
};이 경우 LOW는 1, MEDIUM은 2, HIGH는 3이 됩니다.
열거형은 가독성을 높여 주지만, 완전한 안전장치는 아니다
여기서 한 가지는 꼭 알아야 합니다. C의 enum은 내부적으로 정수처럼 다뤄집니다. 그래서 범위를 벗어난 값을 넣는 일이 완전히 막히지는 않습니다.7
예를 들어 이런 코드는 문법상 가능할 수 있습니다.
enum Weekday today = 100;이렇게 하면 today는 선언상 요일 타입이지만, 실제로는 이상한 값이 들어갈 수 있습니다. 따라서 switch 문을 쓸 때는 default를 두는 습관이 좋습니다.
switch (today) {
case MON:
printf("월요일n");
break;
case TUE:
printf("화요일n");
break;
default:
printf("알 수 없는 요일 값n");
break;
}초보자 관점에서는 enum을 "값의 의미를 분명하게 해 주는 이름표" 정도로 이해하면 좋습니다.
공용체: 여러 멤버가 같은 메모리를 함께 쓰기
이제 가장 낯선 도구인 union을 보겠습니다. 공용체(union)는 문법이 구조체와 비슷하지만, 아주 중요한 차이가 있습니다. 모든 멤버가 같은 메모리 공간을 공유한다는 점입니다.1011
#include <stdio.h>
union Data {
int i;
float f;
char str[20];
};
int main(void) {
union Data data;
data.i = 10;
printf("정수: %dn", data.i);
data.f = 3.14f;
printf("실수: %.2fn", data.f);
return 0;
}이 코드는 얼핏 구조체처럼 보이지만 동작 방식은 다릅니다. data.i와 data.f와 data.str은 각각 따로 저장되는 것이 아니라, 같은 한 칸을 번갈아 사용합니다.
그래서 공용체에서는 마지막으로 저장한 값만 유효하다고 생각하는 편이 안전합니다.210
구조체와 공용체의 차이
두 도구를 비유로 정리해 보겠습니다.
구조체는 서랍장과 비슷합니다. 첫 번째 서랍에는 이름, 두 번째 서랍에는 나이, 세 번째 서랍에는 점수가 각각 따로 들어갑니다.
공용체는 서랍장이 아니라 한 개의 상자에 가깝습니다. 지금은 정수를 넣을 수도 있고, 나중에는 실수를 넣을 수도 있지만, 동시에 둘 다 안전하게 보관할 수는 없습니다.
즉,
구조체는 멤버마다 자기 공간이 있다.
공용체는 멤버들이 같은 공간을 나눠 쓴다.
구조체는 여러 멤버를 동시에 의미 있게 사용할 수 있다.
공용체는 한 시점에 한 멤버만 사용한다고 생각해야 한다.1011
공용체의 크기는 왜 가장 큰 멤버 기준일까
공용체의 멤버는 같은 메모리를 공유하므로, 전체 크기는 보통 가장 큰 멤버의 크기를 기준으로 결정됩니다.10
#include <stdio.h>
union Sample {
char c;
int i;
double d;
};
int main(void) {
printf("크기: %zun", sizeof(union Sample));
return 0;
}이때 char보다 int가 크고, double이 가장 크다면 공용체 크기는 대체로 double 크기에 맞춰집니다.
왜냐하면 그 안에 들어갈 수 있는 후보 중 가장 큰 값을 담을 수 있어야 하기 때문입니다. 작은 컵, 중간 컵, 큰 컵 중 하나만 골라 넣는 상자라면, 상자는 적어도 큰 컵이 들어갈 만큼 커야 하는 것과 같습니다.
공용체는 언제 쓸까
초보자가 공용체를 처음 보면 대개 이런 생각이 듭니다.
"그냥 어려워 보이기만 하는데, 왜 쓰지?"
좋은 질문입니다. 공용체는 일상적인 초급 코드에서는 구조체만큼 자주 쓰이지 않습니다. 하지만 다음과 같은 상황에서는 꽤 유용합니다.10
한 시점에 여러 타입 중 하나만 저장하면 될 때
메모리를 아껴야 할 때
같은 메모리를 다른 방식으로 해석해야 할 때
예를 들어 메시지 데이터가 "정수일 수도 있고, 실수일 수도 있고, 문자일 수도 있다"면 공용체가 후보가 됩니다. 다만 이때는 "지금 무엇이 들어 있는가"를 함께 기록해야 안전합니다.
enum + union 조합: 어떤 값이 들어 있는지 함께 기록하기
공용체는 혼자 쓰면 헷갈리기 쉽습니다. 그래서 실전에서는 보통 enum과 함께 사용합니다.7
생각해 봅시다. union은 여러 타입 중 하나를 담을 수 있지만, 현재 어떤 타입이 들어 있는지 스스로 말해 주지는 않습니다. 그래서 옆에 enum으로 꼬리표를 붙여 주는 것입니다.
#include <stdio.h>
typedef enum {
TYPE_INT,
TYPE_DOUBLE,
TYPE_CHAR
} ValueType;
typedef struct {
ValueType type;
union {
int i;
double d;
char c;
} data;
} Value;
void printValue(Value v) {
switch (v.type) {
case TYPE_INT:
printf("int: %dn", v.data.i);
break;
case TYPE_DOUBLE:
printf("double: %.2fn", v.data.d);
break;
case TYPE_CHAR:
printf("char: %cn", v.data.c);
break;
default:
printf("알 수 없는 타입n");
break;
}
}
int main(void) {
Value a = {TYPE_INT, .data.i = 42};
Value b = {TYPE_DOUBLE, .data.d = 3.14};
Value c = {TYPE_CHAR, .data.c = 'Z'};
printValue(a);
printValue(b);
printValue(c);
return 0;
}이 예제는 꽤 중요합니다. 왜냐하면 struct, enum, union이 각각 따로 노는 것이 아니라, 서로 협력해서 의미 있는 데이터 모델을 만드는 모습이기 때문입니다.
struct는 데이터 전체를 묶고enum은 현재 값의 종류를 설명하고union은 실제 값 저장 공간을 아껴 줍니다
이 조합은 나중에 파일 포맷 해석, 간단한 인터프리터, 메시지 처리 같은 곳에서 자주 등장합니다.
구조체의 메모리 크기는 단순 합과 다를 수 있다
구조체는 겉으로 보기에는 멤버 크기를 단순히 더하면 될 것 같지만, 실제 sizeof 결과는 다를 수 있습니다. 이유는 패딩(padding) 때문입니다.5
패딩은 컴퓨터가 데이터를 더 효율적으로 읽도록 중간중간 빈 공간을 넣는 현상입니다.
#include <stdio.h>
struct Example {
char c;
int i;
};
int main(void) {
printf("크기: %zun", sizeof(struct Example));
return 0;
}char가 1바이트, int가 4바이트니까 합이 5바이트라고 단순 계산하고 싶지만, 실제 결과는 8바이트가 나올 수도 있습니다. 컴파일러와 환경에 따라 달라질 수 있기 때문입니다.5
초보 단계에서는 외우기보다 다음 한 문장을 기억하면 충분합니다.
구조체의 크기는 멤버 크기의 합과 항상 같지 않다.
이 사실은 특히 메모리 사용량을 계산하거나, 바이너리 파일과 직접 맞물리는 코드를 쓸 때 중요해집니다.
실전 예제: 학생 데이터를 더 사람답게 표현하기
이제 구조체와 열거형을 함께 써서 현실적인 데이터를 모델링해 보겠습니다.
#include <stdio.h>
typedef enum {
FRESHMAN = 1,
SOPHOMORE,
JUNIOR,
SENIOR
} GradeLevel;
typedef struct {
char name[20];
int age;
GradeLevel grade;
double score;
} Student;
void printStudent(Student s) {
printf("이름: %sn", s.name);
printf("나이: %dn", s.age);
switch (s.grade) {
case FRESHMAN:
printf("학년: 1학년n");
break;
case SOPHOMORE:
printf("학년: 2학년n");
break;
case JUNIOR:
printf("학년: 3학년n");
break;
case SENIOR:
printf("학년: 4학년n");
break;
default:
printf("학년: 알 수 없음n");
break;
}
printf("점수: %.1fn", s.score);
}
int main(void) {
Student s = {"Kim", 20, SOPHOMORE, 91.5};
printStudent(s);
return 0;
}여기서 grade를 그냥 int로 두지 않고 GradeLevel로 표현한 것이 포인트입니다. 2라는 숫자보다 SOPHOMORE가 훨씬 읽기 쉽고, 코드의 의도도 잘 드러납니다.
이것이 바로 데이터 모델링입니다. 프로그램이 현실 세계의 개념을 코드 속에서 닮아 가는 순간입니다.
초보자가 자주 하는 실수
처음 배우는 단계에서 특히 많이 헷갈리는 부분을 정리해 보겠습니다.
첫째, 구조체 정의 끝의 세미콜론을 빼먹는 경우가 많습니다.
struct Student {
char name[20];
int age;
}; // 세미콜론 필요둘째, 구조체 멤버를 구조체 정의 안에서 직접 초기화하려고 하는 실수가 있습니다. 초기화는 변수 선언 시점에 해야 합니다.
셋째, 문자열 멤버를 =로 대입하려고 하는 경우가 많습니다. 배열 멤버라면 strcpy()가 필요할 수 있습니다.
넷째, 구조체 포인터인데 .로 접근하는 실수가 자주 나옵니다.
ptr.name // 오류 가능
ptr->name // 올바른 방식다섯째, 공용체에서 여러 멤버가 동시에 모두 유효하다고 착각하기 쉽습니다. 공용체는 마지막에 저장한 값 기준으로 생각하는 습관이 필요합니다.
여섯째, enum을 썼다고 해서 이상한 값이 절대 들어오지 않는다고 믿으면 곤란합니다. 필요하면 default 처리도 해 주어야 합니다.
정리: 이제 데이터가 코드 속에서 모양을 갖기 시작한다
이번 챕터에서 배운 세 도구는 문법보다 표현력이 중요합니다.
구조체는 관련 있는 데이터를 하나로 묶어 현실의 대상을 자연스럽게 표현하게 해 줍니다. 열거형은 숫자에 이름을 붙여 코드의 의미를 선명하게 만듭니다. 공용체는 여러 타입이 하나의 메모리 공간을 공유하도록 해서, 상황에 따라 메모리를 아끼거나 한 데이터를 여러 방식으로 다루게 해 줍니다.110
무엇보다 중요한 것은, 이제부터는 단순히 "변수를 만든다"를 넘어서 "현실의 개념을 설계한다"는 생각으로 코드를 보기 시작하는 것입니다.
학생, 상품, 날짜, 좌표, 메시지, 주문 상태 같은 것들이 더 이상 흩어진 변수 조각이 아니라, 의미 있는 데이터 단위로 보이기 시작하면 아주 잘 따라오고 있는 것입니다.
다음 챕터에서는 여기서 한 걸음 더 나아가, 필요한 만큼 메모리를 직접 확보하는 동적 메모리 할당을 배우게 됩니다. 그때 구조체는 더 큰 힘을 발휘하게 됩니다. 진짜 살아 움직이는 데이터가 되기 시작하니까요.
연습 문제
다음 문제를 직접 풀어 보세요. 손으로만 읽는 것보다, 한 번이라도 직접 작성해 보는 쪽이 훨씬 오래 남습니다.
이름, 나이, 평균 점수를 저장하는
Student구조체를 만들고, 학생 3명의 정보를 배열로 저장한 뒤 출력해 보세요.Date구조체와Person구조체를 만들고,Person안에 생년월일 정보를 포함시켜 보세요.요일을 나타내는
enum Weekday를 만들고, 오늘이 평일인지 주말인지 출력해 보세요.enum과union을 함께 사용해 정수 또는 실수를 담을 수 있는Value구조체를 만들어 보세요.
참고
214) C언어 구조체와 사용자 정의 자료형: typedef, 중첩 구조체, union, enum 정리
3[C] 12. 구조체(Struct) / 열거형(Enum) / 공용체(Union)
4C Structures (structs) - W3Schools
6[C 언어 기초 정리 08] 구조체(Struct), union, enum :: 곰곰의 일지](https://secundo.tistory.com/65)
7[C언어 50강] 36강. typedef / enum / union / bit-field: 타입 설계 도구
8Introduction to C / C++ Programming Structures, Unions, and Enumerated Types
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.