Rust에서 구조체를 정의한다는 것은 여러 개의 관련된 데이터를 하나의 사용자 정의 타입으로 묶는다는 의미이다. 구조체는 필드(Field)와 그 필드의 타입을 명시하며, 이를 통해 코드의 가독성과 안정성을 높일 수 있다. 구조체는 크게 세 가지 형태가 있으며, 여기서는 일반적으로 많이 사용하는 명명된 필드 구조체(Named-field Struct)를 중심으로 살펴본다.
명명된 필드 구조체를 정의하기 위해서는 struct 키워드를 사용한다. 구조체 이름을 명시한 뒤 중괄호 안에 필드 이름과 타입을 차례로 작성한다. 필드 타입은 어떠한 Rust 타입이든 올 수 있다.
이 구조체는 User라는 이름을 갖는다. username, email, active, sign_in_count라는 네 개의 필드가 정의되어 있으며 각각 String, String, bool, u64 타입을 갖는다. 필드를 정의하는 순서는 중요하지 않지만, 가독성과 유지보수를 위해 의도를 드러내는 순서대로 작성하는 경우가 많다.
명명된 필드 구조체를 인스턴스화하려면 정의된 구조체의 이름과 함께 중괄호 안에 필드 이름과 값을 대응시킨다. 구조체의 각 필드는 타입에 맞는 값을 가져야 하며, 필드의 순서와 이름을 정확히 맞추어야 한다.
let user1 = User {
username: String::from("example_user"),
email: String::from("user@example.com"),
active: true,
sign_in_count: 1,
};
변수 user1은 User 구조체의 인스턴스로 생성되었다. user1.username으로 사용자 이름을, user1.email로 이메일을, user1.active로 활동 여부를 확인할 수 있다. 구조체 인스턴스는 불변(immutable)으로 생성되므로 필드 값을 변경하려면 let 키워드 대신 let mut를 사용해야 한다.
구조체의 일부 필드만 새로운 값으로 바꾸고 나머지 필드는 기존 인스턴스에서 복사해오는 문법도 지원한다. 이를 갱신 문법(Update Syntax)이라 부르며, 두 구조체의 타입이 같아야 한다는 제약이 있다. 갱신 문법을 사용하면 아래와 같은 코드를 작성할 수 있다.
let user2 = User {
email: String::from("another@example.com"),
..user1
};
username, active, sign_in_count 필드는 user1에서 복사하고 email 필드만 새로 지정하는 방식이다. 이때 갱신 문법을 적용하기 위해서는 user1이 더 이상 유효하지 않거나(소유권을 완전히 넘기거나) Clone 트레이트를 구현해야 한다.
명명된 필드 구조체와 달리 튜플 구조체(Tuple Struct)는 필드에 이름이 없고, 튜플처럼 필드 위치로만 식별된다. 정의 방법은 struct 키워드 뒤에 구조체 이름, 그리고 소괄호 안에 필드 타입을 차례대로 명시한다.
red.0, red.1, red.2와 같이 점(.) 연산자 뒤에 필드의 인덱스를 사용해 값을 추출한다. 이름이 없기 때문에 필드 의미를 추측하기가 어려울 수 있으므로, 데이터가 명확한 ‘튜플’ 특성을 가지는 경우에만 사용하거나, 코드에 주석을 달아 어떤 용도인지 분명히 하는 것이 좋다.
필드가 전혀 없는 구조체를 정의할 수도 있다. 이를 유닛 구조체(Unit Struct)라고 부른다. 유닛 구조체는 주로 트레이트 구현의 대상이 되거나 특정 타입으로만 구분되어야 할 때 사용된다.
이렇게 유닛 구조체를 인스턴스화하면 별도의 필드 없이도 특정 타입을 나타낼 수 있다.
정리하자면, 명명된 필드 구조체는 필드 이름과 타입을 명시적으로 표현할 수 있어 가장 직관적이며 흔히 사용하는 방식이다. 구조체를 정의할 때는 구조체가 표현하고자 하는 개념과 필드들의 의미를 명확히 드러내는 것이 중요하다. 인스턴스화할 때는 필드 이름을 일치시켜야 하며, 소유권과 가변성에 유의해야 한다. Rust의 구조체는 객체지향 언어의 클래스와는 다르지만, 데이터를 구조화하여 프로그램을 안정적으로 유지보수할 수 있도록 설계된 강력한 도구이다.