Rust #5: 5장 구조체를 사용하여 관련 데이터 구조화
개요
의미있는 값을 그룹으로 묶어 관리하면 좀 더 효율적으로 프로그래밍 할 수 있습니다. Rust에도 마찬가지로 여러 관련된 값을 하나의 그룹으로 패키징 하여 사용자 정의 데이터 유형을 만들 수 있으며 이를 구조체(struct)라고 합니다.
구조체 정의 및 인스턴스화
구조체는 튜플과 유사한 값의 집합입니다. 차이점은 값의 의미를 명확하게 전달할 수 있도록 필드 이름을 지정할 수 있습니다. 또한 변경 가능한 값을 가질 수도 있는데요, 지금 부터 살펴봅시다.
다음은 일반적인 Rust의 구조체를 보여줍니다.
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
User라는 구조체 형을 정의하며, User는 각각 username과 email, sign_in_count와 active라는 필드명을 가진 형을 담게 됩니다. 이 필드들의 공통점은 사용자(User)의 속성이라는 것인데요, 이런 식으로 관련된 값을 하나의 구조체 형태로 만들어 사용할 수 있습니다.
다음은 구조체로 만든 사용자 데이터 형을 다음과 같이 선언하여 인스턴스화 하는 것을 보여줍니다.
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
위와 같이 구조체를 인스턴스화 하여 user1에 할당할 수 있습니다. 구조체는 Rust의 변수의 선언과 마찬가지로 불변하므로 생성한 후 필드를 수정할 수 없는데요, 다음과 같이 mut
키워드를 더해 수정 가능한 구조체를 선언할 수 있습니다.
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
mut
키워드를 주면 특정 필드만 변경 가능한 것으로 표시하는 것은 허용하지 않습니다. 다른 표현식과 마찬가지로 구조체 또한 함수 본문의 마지막 표현식으로 구성하여 새 인스턴스를 암시적으로 반환할 수 있습니다.
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
변수와 필드의 이름이 같을 경우 필드명 생략 가능
구조체 필드와 매개변수 이름이 동일할 경우 필드명을 생략할 수 있습니다.
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
구조체 업데이트 구문을 이용하여 다른 인스턴에서 인스턴스 생성
다른 인스턴스의 필드 값을 그대로 가져야 사용하는 경우가 자주 있습니다. Rust는 그럴 경우를 다음처럼 축약하여 사용할 수 있습니다.
let user2 = User {
email: String::from("another@example.com"),
username: String:: from("anotherusername567"),
active: user1.active,
sign_in_count: user1.sign_in_count,
};
이 코드를
let user2 = User {
email: String::from("another@example.com"),
username: String:: from("anotherusername567"),
..user1
};
로 active
와 sign_in_count
필드 값을 그대로 가져와 사용할 수 있습니다.
극단적으로 다음과 같이 쓸수도 있습니다.
let user2 = User { ..user1 };
명명된 필드가 없는 튜플 구조체를 사용하여 다른 유형 만들기
필드명이 주어지지 않아도 그 의미가 유추가 되는 구조체의 경우 필드를 생략하고 정의를 단순화 할 수 있습니다.
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
Color 구조체는 R, G, B로 이루어졌다는 것을 유추할 수 있으며 그러한 맥락으로 필드명을 생략할 수 있습니다. Point도 마찬가지로 X, Y, Z로 이루어졌다는 것을 유추할 수 있으며 마찬가지로 필드명을 생략할 수 있는 대상이 됩니다.
필드가 없는 단위 유사 구조체
구조체를 필드 없이 정의할 수 도 있습니다. 이에 대해서는 10장에서 논의될 예정입니다.
구조체의 소유권
구조체의 소유권도 변수의 소유권과 동일합니다.
let user3 = user2;
// 소유권이 user3으로 넘어갔으므로 `user2.email`을 쓸 수 없음
println!("{}", user2.email);
필드의 소유권도 마찬가지입니다.
let user2 = User { ..user1 };
println!("{}", user2.email);
// email이 String 형이고 `user2.email`로 소유권이 넘어갔으므로 `user1.email`을 쓸 수 없음
println!("{}", user1.email);
구조체를 사용한 예제 프로그램
구조체를 이해하기 위해 직사각형의 면적을 계산하는 프로그래을 작성해 봅시다.
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
잘 작동합니다. 하지만 서로 관련이 있는 width
와 height
가 별개의 변수로 사용되고 함수의 인자로 전달되었기 때문에 코드 가독성이 떨어지고 수정시 어렵습니다.
튜플을 사용한 리팩토링
width
와 height
는 서로 관련이 있으므로 이를 튜플로 표현해 봅시다.
fn main() {
let rect1 = (30, 50);
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
fn area(dimensitions: (u32, u32)) -> u32 {
dimensitions.0 * dimensitions.1
}
잘 작동합니다. 두 값이 튜플로 서로 묶였기 때문에 좀 더 의미가 명확해집니다. 하지만 필드명이 없기 때문에 그 의미가 명확하지 않다는 단점이 있습니다.
구조체를 사용한 리팩토링: 더 많은 의미 추가
이제 위의 코드를 구조체로 리팩토링 해 봅시다.
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
구조체를 이용해서 직사각형의 속성이 더 명확해졌습니다. 필드명을 이용하기 때문에 값 계산 시 혼돈이 없습니다.
파생된 특성으로 유영한 기능 추가하기
어노테이션을 이용하면 다양한 속성을 구조체에 부여할 수 있습니다.
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
// Rectangle은 `std::fmt::Display`를 구현하지 않았으므로 오류가 발생함
println!("rect1 is {}", rect1);
}
구조체에 #[derive(Debug)]
어노테이션을 부여하고 포맷에 {}
대신 {:?}
으로 변경하면 오류가 제거됩니다. Debug의 구현을 이용해 정상적으로 출력할 수 있게 됩니다.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
// 오류 발생하지 않음
println!("rect1 is {:?}", rect1);
}
Rectange 구조체는 사각형의 너비와 높이를 갖는 훌륭한 사용자 유형입니다. 하지만 사각형의 면적을 구하는 함수가 Rectangle 구조체 외부에 있습니다. 사각형의 면적은 사각형과 밀접한 관련이 있으므로 내부의 함수로 제공하면 좋을 것 같습니다.
메서드 구문
메소드는 함수와 유사하게 fn 키워드와 이름으로 선언하고 매개변수화 반환값을 가집니다. 그러나 메소드는 구조체 내에 정의되고 첫번째 매개변수가 항상 self라는 점에서 다릅니다.
메소드 정의
면적을 구하는 함수를 Rectangle
의 메소드로 리펙토링 해봅시다.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
)
}
Rectangle의 메소드를 정의하기 위해 impl
구현 블록을 이용하여 메소드를 정의할 수 있습니다.
더 많은 매개변수가 있는 메서드
메소드의 첫번째 인자가 &self
라는 것만 제외하고 함수와 동일 합니다.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
}
관련 기능
&self
를 인자로 사용하지 않아도 됩니다. self를 사용하지는 않지만 구조체에 포함되어 있기 때문에 이를 이런 유형을 연결된 함수
라고 합니다. 구조체의 필드나 메소드에 접근할 필요가 없는 경우 이렇게도 사용되는데요, 대표적인 사용이 새 인스턴스를 반환하는 생성자에 자주 사용됩니다.
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
다중 impl 블록
다중 impl 블록을 통해 메소드를 정의할 수도 있습니다. 굳이 그럴 필요는 없지만 유용한 경우가 있습니다. 10장에서 다중 블록이 유용한 경우를 다룰 것입니다.
요약
구조체를 사용하면 의미있는 사용자 지정 유형을 만들 수 있습니다. 구조체를 사용하면 연결된 데이터 조각을 구조체라는 단위로 유지하고 코드를 명확하게 하기 위해 각 조각에 이름을 지정할 수 있습니다. 메소드를 이용하면 구조체의 인스턴스가 가지는 값에 따라 동작을 지정할 수 있고 연결된 함수를 사용하면 인스턴스를 사용할 수 없는 상태에서 구조체에 특정한 기능을 제공할 수 있습니다.
Rust에서는 구조체와 더불어 다른 언어와 다르게 강력한 열거형 기능을 제공합니다.