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 프로젝트에서 데이터 모델링이 훨씬 쉬워질 것입니다.
This work is licensed under
CC BY 4.0