Rust 기초: 구조체 (Structure) 사용법
Rust에 내장된 원시 자료형만으로는 실제 비지니스에서 필요한 복잡한 구조의 데이터를 표현하는데는 한계가 있습니다. 그래서 구조체(Structure)를 통해서 여러 개의 관련된 데이터를 한 곳에 묶어서 추상화하게 되죠.
이 글에서는 Rust에서 구조체가 무엇이고 어떻게 사용하는지 예제와 함께 살펴보겠습니다.
구조체란?
구조체(Structure)는 여러 필드를 가진 사용자 정의(custom) 자료형(type)입니다.
struct
키워드로 구조체의 이름을 붙이고, 중괄호 안에 각 필드의 이름과 자료형을 나열하면 됩니다.
예를 들어, 번호, 이메일, 활성화 여부로 이루어진 사용자 정보를 나타내는 간단한 구조체를 다음과 같이 정의할 수 있습니다.
struct User {
no: u16,
email: String,
active bool,
}
Java와 같은 객체 지향 프로그래밍 언어를 써보셨다면 클래스(class)와 개념적으로 유사하다고 느끼실 것 같습니다.
인스턴스 생성
구조체를 사용하려면 각 필드의 값을 지정하여 인스턴스를 생성해야 합니다.
Rust에서 인스턴스를 생성할 때는 new
키워드는 필요없으며, 구조체 이름 뒤에 중괄호로 각 필드의 이름과 값을 명시해주면 됩니다.
구조체 인스턴스를 생성하려면 필드 값을 지정해야 합니다.
let user = User {
no: 1,
email: String::from("test@user.com"),
active: true,
};
변수 이름과 필드 이름이 동일한 경우에는 인스턴스를 생성할 때 굳이 같은 이름을 두 번 반복할 필요없이 생략가능합니다.
let no = 1;let email = String::from("test@user.com");
let user = User {
no, // `no: no,`와 동일 email, // `email: email,`와 동일 active: true,
};
기존 구조체 인스턴를 기반으로 새로운 구조체 인스턴스를 만드는 것도 가능합니다.
let otherUser = User {
no: 2,
..user
};
필드 접근
구조체의 필드 값은 .
연산자를 통해 인스턴스명.필드명
의 형식으로 접근할 수 있습니다.
fn main() {
let user = User {
no: 1,
email: String::from("test@user.com"),
active: true,
};
println!("번호: {}", user.no); println!("이메일: {}", user.email); println!("활성화 여부: {}", user.active);}
번호: 1
이메일: test@uesr.com
활성화 여부: true
구조 분해 할당(Destructuring)을 통해서 동시에 필드 값을 여러 변수에 저장한 후에 접근할 수도 있습니다.
fn main() {
let user = User {
no: 1,
email: String::from("test@user.com"),
active: true,
};
let User { no, email, active } = user;
println!("번호: {no}"); println!("이메일: {email}"); println!("활성화 여부: {active}");}
번호: 1
이메일: test@uesr.com
활성화 여부: true
메서드 추가
구조체를 이루고 있는 데이터를 상대로 빈번하게 수행해야 하는 작업은 구조체에 메서드로 추가할 수 있습니다. 이렇게 특정 구조체를 위해 작성하는 함수를 메서드라고 합니다.
impl
키워드로 대상 구조체의 이름을 명시하고, 중괄호 안에 메서드를 일반 함수 정의하듯이 나열해주면 됩니다.
메서드 안에서 인스턴스의 필드에 접근할 수 있도록, 첫 번째 매개변수인 self
를 통해서 인스턴스 자신이 넘어오게 되어있습니다.
예를 들어, 위에서 정의한 User
구조체에 활성화 여부에 따라 다른 메시지를 출력해주는 display()
메서드를 추가해보겠습니다.
struct User {
no: u16,
email: String,
active bool,
}
impl User { fn display(&self) { if self.active { println!("사용자 {}: {}", self.no, self.email); } else { println!("비활성화된 사용자입니다."); } }}
그러면 이렇게 구조체의 인스턴스를 상대로 메서드를 호출할 수 있습니다.
fn main() {
let user = User {
no: 1,
email: String::from("test@user.com"),
active: true,
};
user.display();}
사용자 1: test@user.com
튜플 구조체
구조체를 정의할 때 필드 이름없이 필드 자료형만 나열할 수도 있는데 이를 튜플 구조체(Tuple Structure)라고 합니다.
이름이 있는 튜플처럼 사용할 수 있는 구조체입니다:
struct Point(i32, i32);
fn main() {
let p = Point(3, 4);
println!("({}, {})", p.0, p.1);
}
튜플 구조체는 필드의 이름이 없고 대신 필드의 순서가 있기 때문에 인덱스를 통해서 필드 값에 접근합니다.
파이썬을 사용해보셨다면 Namedtuple과 비슷한 개념이라고 볼 수 있겠습니다.
유닛 구조체
유닛 구조체(Unit Structure)라는 아예 필드가 없는 특수한 형태의 구조체도 있습니다. 어떤 타입의 태그나 마커처럼 활용됩니다.
struct Marker;
fn main() {
let _m = Marker;
}
주로 Trait 구현 시 타입 구분자로 많이 볼 수 있습니다.
디버깅 요령
기본적으로 구조체의 인스턴스는 println!
과 같은 출력 메크로를 통해서 출력이 불가능합니다.
println!("{user}");
^^^^^^ `User` cannot be formatted with the default formatter
이 문제는 Display
트레잇을 직접 구현하거나 Debug
트레잇을 자동 파생시켜서 해결할 수 있습니다.
예를 들어, 우리가 작성한 User
구조체에 Debug
트레잇을 파생시킨 후 인스턴스를 출력해보겠습니다.
#[derive(Debug)]struct User {
username: String,
email: String,
active: bool,
}
fn main() {
let user = User {
no: 1,
email: String::from("test@user.com"),
active: true,
};
println!("{user:?}"); // 한 줄 출력 println!("{user:#?}"); // 예쁘게 출력}
User { no: 1, email: "test@user.com", active: true }
User {
no: 1,
email: "test@user.com",
active: true,
}
출력 메크로와 Display, Debug 트레잇에 대해서는 별도 포스팅에서 자세히 다루고 있으니 참고바랍니다.
전체 코드
본 포스팅에서 작성한 실습 코드는 Rust Playgrond 확인하시고 직접 실행해보실 수 있습니다.
마무리
Rust의 구조체는 단순한 데이터 추상화 도구를 넘어서 메서드까지 추가하면 강력한 캡슐화 도구로 발전합니다. 다양한 구조체 표현법과 활용 방법을 익혀두면, 앞으로 Rust 프로젝트에서 데이터 모델링이 훨씬 쉬워질 것입니다.