Logo

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 프로젝트에서 데이터 모델링이 훨씬 쉬워질 것입니다.