Rust 핵심: 구조체 정의와 활용
안녕하세요! 여러분의 코딩 구원투수, 재준봇입니다!
자, 여러분. 지금까지 우리는 변수니, 함수니 하는 아주 기초적인 재료들을 배웠습니다. 그런데 공부를 하다 보면 이런 생각이 들 거예요. “아니, 데이터가 너무 많은데 이걸 하나하나 변수로 만들고 있자니 내 손가락이 남아나지 않겠어!” 맞습니다. 우리가 현실 세계에서 사람 한 명을 정의할 때 이름, 나이, 키, 주소, 혈액형을 각각 따로 관리하지 않죠? 그냥 ‘사람’이라는 하나의 덩어리로 생각합니다.
오늘 배울 ‘구조체(Struct)’가 바로 그 덩어리를 만드는 마법입니다. 오늘 이 강의만 제대로 완벽하게 소화하시면, 여러분은 Rust라는 거대한 성의 설계도를 그릴 수 있게 될 겁니다. 이거 모르면 나중에 복잡한 프로그램 짤 때 진짜 큰일 납니다! 집중해서 따라오세요!
14강: Rust 핵심: 구조체 정의와 활용
1. 구조체가 대체 왜 필요한가요? (비유로 이해하기)
여러분, 편의점에서 도시락을 산다고 생각해 보세요. 도시락 안에는 밥도 있고, 반찬도 있고, 메인 메뉴인 제육볶음도 들어있죠? 만약 도시락이라는 ‘그릇’이 없다면 어떻게 될까요? 밥은 손바닥에 얹고, 제육볶음은 컵에 담고, 김치는 비닐봉지에 담아서 들고 다녀야 합니다. 정말 끔찍하죠?
코딩에서도 마찬가지입니다. 사용자 정보를 저장하는데 user_name, user_age, user_email 이렇게 변수를 따로 만들면, 나중에 사용자 100명을 관리할 때 변수를 300개 만들어야 합니다. 이건 그냥 코딩 자살행위나 다름없어요.
그래서 우리는 “관련 있는 데이터들을 하나로 묶어주는 전용 그릇”을 만드는데, 그것이 바로 구조체(Struct)입니다.
2. 구조체의 3가지 얼굴 (구현 방법 3가지)
Rust는 아주 친절하게도 상황에 따라 골라 쓸 수 있도록 세 가지 형태의 구조체를 제공합니다. 이걸 구분해서 쓸 줄 알아야 “오, 이 사람 좀 치는데?”라는 소리를 듣습니다.
(1) 클래식 구조체 (Classic Struct): 이름표가 붙은 정석 스타일
가장 많이 쓰이는 방식입니다. 각 데이터에 이름(필드명)이 붙어 있어서 가독성이 끝내줍니다.
// 1. 구조체 정의: 'User'라는 이름의 그릇을 만듭니다.
struct User {
username: String, // 사용자의 이름 (문자열)
email: String, // 사용자의 이메일 (문자열)
sign_in_count: u64, // 로그인 횟수 (부호 없는 64비트 정수)
active: bool, // 현재 활동 중인지 여부 (불리언)
}
fn main() {
// 2. 구조체 인스턴스 생성: 정의한 그릇에 실제 데이터를 채워 넣습니다.
let user1 = User {
email: String::from("rust_love@example.com"),
username: String::from("재준봇_팬1호"),
active: true,
sign_in_count: 1,
};
// 3. 데이터 접근: 점(.) 연산자를 이용해 원하는 필드만 쏙 뽑아옵니다.
println!("사용자 이름: {}", user1.username);
}
[코드 뜯어보기]
struct User { ... }: “이제부터User라는 타입은 이런 데이터들을 가지고 있을 거야!”라고 선언하는 설계도입니다.username: String: 필드명과 타입을 지정합니다. 마치 도시락 칸에 ‘밥 자리’, ‘반찬 자리’를 정해두는 것과 같습니다.let user1 = User { ... }: 설계도를 바탕으로 실제 객체(인스턴스)를 만드는 과정입니다. 이때 정의된 모든 필드에 값을 넣어줘야 합니다. 하나라도 빼먹으면 Rust 컴파일러가 아주 무섭게 화를 냅니다.user1.username: 마침표(.)를 사용해 구조체 내부의 특정 값에 접근합니다.
(2) 튜플 구조체 (Tuple Struct): 이름은 필요 없고 타입만 중요할 때
가끔은 필드 이름까지 붙이는 게 오히려 오버스러울 때가 있습니다. 예를 들어 좌표(x, y)나 RGB 색상 값 같은 경우죠. 이때는 이름표를 떼고 타입만 정의합니다.
// 1. 튜플 구조체 정의: 필드 이름 없이 타입만 나열합니다.
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
// 2. 인스턴스 생성: 괄호를 이용해 값을 넣습니다.
let black = Color(0, 0, 0);
let white = Color(255, 255, 255);
let origin = Point(0, 0, 0);
// 3. 데이터 접근: 이름이 없으니 인덱스(0, 1, 2...)로 접근합니다.
println!("빨간색 값: {}", black.0);
println!("X 좌표 값: {}", origin.0);
}
[코드 뜯어보기]
struct Color(i32, i32, i32):Color라는 타입은 정수 3개가 묶여 있다는 뜻입니다.let black = Color(0, 0, 0): 튜플처럼 아주 빠르게 생성할 수 있습니다.black.0: 이름이 없기 때문에 0번, 1번, 2번 순서로 데이터를 가져옵니다.- 꿀팁:
Color와Point는 둘 다(i32, i32, i32)형태지만, Rust는 이 둘을 완전히 다른 타입으로 인식합니다. 그래서 실수로 색상 값에 좌표 값을 넣는 대참사를 막을 수 있습니다.
(3) 유닛 구조체 (Unit-Like Struct): 데이터는 없는데 타입은 필요할 때
“아니, 데이터가 하나도 없는데 왜 구조체를 만들어요?”라고 물으실 수 있습니다. 보통은 나중에 배울 ‘트레이트(Trait)’라는 기능을 구현할 때, 특정 상태나 마커(Marker) 용도로 사용합니다. 지금은 “아, 이런 게 있구나” 정도로만 이해하셔도 충분합니다.
// 1. 유닛 구조체 정의: 아무런 필드가 없습니다.
struct AlwaysHappy;
fn main() {
// 2. 인스턴스 생성: 그냥 이름만 적으면 됩니다.
let me = AlwaysHappy;
println!("나는 항상 행복해!");
}
[코드 뜯어보기]
struct AlwaysHappy;: 세미콜론 하나로 정의가 끝납니다. 내부 데이터가 전혀 없습니다.- 주로 특정 기능을 구현하기 위한 ‘표식’으로 사용하며, 메모리를 거의 차지하지 않는 아주 가벼운 녀석입니다.
3. 구조체에 ‘지능’ 부여하기: impl 블록
지금까지의 구조체는 그냥 ‘데이터 저장소’였습니다. 하지만 여기에 함수를 추가하면 스스로 행동하는 ‘객체’가 됩니다. Rust에서는 impl (implementation) 키워드를 사용해 구조체의 메서드를 정의합니다.
여기서 연관 함수(Associated Function)와 메서드(Method)의 차이를 아는 것이 핵심입니다!
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// [1] 연관 함수: 인스턴스 없이 호출 (보통 생성자로 사용)
// self가 없습니다!
fn new(w: u32, h: u32) -> Rectangle {
Rectangle {
width: w,
height: h,
}
}
// [2] 메서드: 인스턴스를 통해 호출
// 첫 번째 인자로 &self가 반드시 들어가야 합니다!
fn area(&self) -> u32 {
self.width * self.height
}
// [3] 값을 변경하는 메서드: &mut self를 사용
fn scale(&mut self, factor: u32) {
self.width *= factor;
self.height *= factor;
}
}
fn main() {
// 연관 함수를 이용해 객체 생성 (Rectangle::new() 스타일)
let mut rect = Rectangle::new(10, 20);
// 메서드를 이용해 넓이 계산
println!("처음 넓이: {}", rect.area());
// 값을 변경하는 메서드 호출
rect.scale(2);
println!("확대 후 넓이: {}", rect.area());
}
[코드 뜯어보기]
impl Rectangle { ... }: “이제부터Rectangle구조체가 할 수 있는 행동들을 정의하겠다!”라는 선언입니다.fn new(...) -> Rectangle:self가 없죠? 이건 객체가 생성되기 전에도 호출할 수 있는 함수입니다. 보통Rectangle::new()처럼 호출하며, 객체를 생성해서 반환하는 ‘생성자’ 역할을 합니다.fn area(&self):&self가 들어갔습니다. 이것은 “이 함수를 쓰려면Rectangle객체가 먼저 있어야 해!”라는 뜻입니다. 객체의 데이터를 읽기만 하므로 참조자(&)를 씁니다.fn scale(&mut self, ...): 데이터를 수정해야 하므로 가변 참조자(&mut self)를 사용합니다. 이 메서드를 쓰려면 변수가let mut으로 선언되어 있어야 합니다.
🚀 초보자 폭풍 질문!
Q: 재준봇님! 자바나 파이썬의 ‘클래스(Class)’랑 구조체가 똑같은 건가요?
A: 오, 아주 날카로운 질문입니다! 비슷해 보이지만 결정적인 차이가 있습니다. Rust의 구조체는 데이터와 행동(impl)이 엄격하게 분리되어 있습니다. 클래스는 데이터와 함수가 한데 묶여 있는 거대한 덩어리라면, Rust는 “데이터는 구조체에 담고, 기능은 impl 블록에 따로 정의해!”라고 강제합니다. 덕분에 메모리 구조가 훨씬 단순해지고 성능이 올라갑니다. Rust는 겉멋보다는 실속을 챙기는 언어거든요!
⚠️ 실무주의보
주의: 구조체 필드 접근 제어(Visibility)
실무에서 코드를 짤 때, 구조체의 모든 필드를 아무나 수정하게 두면 프로그램이 금방 엉망이 됩니다. 기본적으로 Rust의 모든 것은 private(비공개)입니다. 다른 모듈에서도 이 구조체를 사용하게 하려면 pub 키워드를 붙여줘야 합니다.
pub struct User {
pub username: String, // 외부에서 접근 가능
email: String, // 외부에서 접근 불가 (내부 전용)
}
만약 pub을 안 붙였는데 외부에서 user.email에 접근하려고 하면, 컴파일러가 “어딜 감히! 이건 비밀이야!”라며 빨간 줄을 그을 겁니다. 캡슐화라는 개념인데, 중요하니까 꼭 기억하세요!
마무리하며
오늘 우리는 Rust의 핵심 중의 핵심, 구조체에 대해 알아봤습니다.
- 클래식 구조체: 이름표 붙은 정석 그릇.
- 튜플 구조체: 타입만 중요한 간편 그릇.
- 유닛 구조체: 데이터 없는 마커 그릇.
- impl 블록: 구조체에 생명력(함수)을 불어넣는 마법.
이 개념들이 처음에는 낯설겠지만, 계속 코드를 치다 보면 어느새 “아, 여기선 튜플 구조체가 낫겠는데?”라고 생각하는 본인의 모습에 놀라실 겁니다.
오늘 강의는 여기까지입니다! 다음 시간에는 더 강력하고 다이나믹한 기능을 들고 오겠습니다. 포기하지 말고 끝까지 가봅시다. 여러분은 할 수 있습니다! 지금까지 재준봇이었습니다!
<hr>