본문 바로가기

러스트의 구조체 살펴보기

기술적인 이야기/기타 개발 2020. 8. 19.
반응형

러스트(Rust)는 다양한 타입을 제공합니다. 원시 타입의 종류도 많지요. 그리고 튜플과 같이 다양한 타입을 묶을 수 있는 그룹형 타입도 있습니다. 그렇다면 러스트의 복잡한 자료구조는 튜플만 쓰면 되는 것일까요? 물론 아니니까 이런 글을 쓰고 있는 것이겠지요.

C의 구조체(Structs)와 비슷하게 러스트도 구조체라 불리는 특수한 복합 타입을 정의하는 기능을 가지고 있습니다. 현대적인 OOP 언어의 클래스(class)와도 비슷하다고도 볼 수 있겠지만 물론 다른 겁니다.

러스트에서 구조체는 세 가지 의미가 있습니다. 하나는 C의 구조체와 비슷한 것, 다른 하나는 튜플 구조체, 다른 하나는 단위로만 쓰는 구조체의 세 가지입니다. 여기서 아무래도 제일 중요한 것은 처음의 C의 구조체와 비슷한 것이겠지요.

일반적인 구조체(Structs)

구조체는 하나 이상의 필드(fields)를 가질 수 있는 복합 타입입니다. 각 필드는 이름과 타입을 가지고 있습니다. 이런 구조체를 선언하는 간단한 예는 아래와 같습니다.

struct Human {
    name: String,
    family_name: String,
    age: u32
}

위 예는 Human이라는 구조체를 선언하고 있습니다. Human 구조체는 name이라는 문자열 필드와 family_name이라는 문자열 필드, 그리고 age라고 부르는 32비트 정수형 필드를 가진다고 선언하고 있습니다.

구조체는 이렇게 떨어뜨려 놓으면 뭔가 어색한 데이터들을 하나로 끌어 모아서 좋은 디자인을 만들 수 있게 해 줍니다.

구조체 인스턴스 생성 및 초기화

앞서 작성한 Human 구조체의 인스턴스는 아래와 같이 생성하고 사용할 수 있습니다.

let james = Human {
    name: String::from("James"),
    family_name: String::from("Lee"),
    age: 12
};

println!("{} {}, {} years old", james.name, james.family_name, james.age);

위 예제는 james라는 변수에 Human 구조체 인스턴스를 생성하고 초기값을 생성하고 마지막으로 이 구조체의 각 필드의 값을 출력하는 예제입니다. 인스턴스를 생성할 때의 초기화 방법이 좀 특이하긴 합니다만, 그 외에 사용 방법은 친숙한 편입니다.

러스트의 특수 문법으로 인스턴스 생성 시에는 동일한 타입의 다른 구조체의 필드 값들을 초기값으로 이용하는 방법이 있습니다.

let bruce = Human {
    name: String::from("Bruce"),
    ..james
};

위 예제는 bruce라는 변수에 Human 인스턴스를 생성하면서 이름을 제외한 나머지 필드들은 james의 것을 그대로 이용하여 생성하는 예제입니다. 참고로 위 코드에서는 소유권 분쟁이 발생하지는 않는 것으로 보아 각 필드는 복사되는 것으로 유추되네요.

구조체 해체하기(Destructure)

러스트의 구조체는 각 필드의 값을 일반 변수로 한 번에 풀어주는 문법이 있습니다.

let Human {name: n, family_name: f, age: a} = james;
println!("{}, {}, {} years old", n, f, a);

이 문법은 구조체 해체하기(destructure)라고 부릅니다. 위의 예처럼 구조체 필드를 일반 변수로 한 번에 풀어주는 방식은 구조체 필드를 자주 액세스해야 하는 상황에선 유용할 것으로 생각됩니다.

이렇게 구조체를 해체해버리면 소유권 이전이 발생합니다. 즉 위의 경우 james 변수는 이후 사용할 수 없게 됩니다. 해체하다는 의미가 딱 와 닿네요.

메서드(Methods)

사실 OOP 클래스도 아닌데 메서드라는 용어가 적당한지는 잘 모르겠습니다만, 이렇게 이해하는 게 편할 것 같아 그냥 메서드라고 부르겠습니다. 즉 러스트의 구조체는 필드 말고도 멤버 함수를 구현할 수도 있습니다. 다만 struct 블록 내에서 구현하는 것이 아닌 impl이라 불리는 추가 기능으로 메서드를 붙여줄 수 있습니다.

impl Human {
    fn birthday_party(&mut self) {
        self.age += 1;
    }

    fn marry(&mut self, mate_family_name: String) {
        self.family_name = mate_family_name;
    }

    fn name_card(self) -> String {
        String::from(format!("{} {}, {} ages old", self.name, self.family_name, self.age))
    }
}

위의 예제는 앞서 구현한 Human 구조체에 추가로 세 가지 메서드를 구현하고 있는 예제입니다.

여기서 상단 2개의 메서드, 즉 birthday_party()marry() 메서드는 일반적인 클래스의 동적 메서드로 볼 수 있습니다. 마치 Python처럼 self를 매개변수에 명시하고 있습니다. 특히 self&mut로 표기해서 자신의 필드를 수정할 수 있도록 정의하고 있습니다.

세 번째 메서드의 경우는 self 앞에 &mut 표기가 없습니다. 이는 즉 자신의 필드를 변경하지 않는 순수 함수라는 것으로 유추할 수 있습니다.

이런 메서드를 사용하는 것도 제법 친숙한 모습입니다.

let mut james = Human {
    name: String::from("James"),
    family_name: String::from("Lee"),
    age: 12
};

james.birthday_party();
// james.age == 13

james.marry(String::from("Kim"));
// james.family_name == "Kim"

대부분의 OOP 언어에서 사용하는 것처럼 .을 이용해 특정 구조체의 메서드를 호출하는 모습을 볼 수 있습니다.

개인적으로 러스트의 메서드 구현 방식은 Swift에서 extension을 이용해 구조체의 메서드를 확장해서 구현하는 것과 참 닮았다고 느껴졌습니다.

정적 메서드(Static Methods)

구조체 메서드의 경우 한 가지 형태가 더 있습니다. 다른 언어식으로 불러보자면 정적 메서드와 비슷한 형태입니다.

impl Human {
    fn birth(name: String, family_name: String) -> Human {
        Human {
            name: name,
            family_name: family_name,
            age: 0
        }
    }
}

앞서 살펴본 다른 메서드와는 다르게 self를 매개변수에 명시하고 있지 않다는 점을 볼 수 있습니다. 이 경우 이 메서드는 인스턴스화 되지 않았을 때 사용할 수 있는 정적 메서드가 됩니다. 따라서 아래와 같이 바로 사용할 수 있습니다.

let john = Human::birth(String::from("John"), String::from("Lee"));

위 예제에서도 볼 수 있지만 이런 정적 메서드의 유명한 형태는 String::from() 같습니다. 확인은 해보지 않았지만 아마도 이 메서드도 이렇게 구현되어 있으리라 생각되네요.

 

튜플 구조체(Tuple Structs)

튜플 구조체는 튜플을 이용해 필드 선언을 하는 형태의 구조체입니다.

struct Materials(String, i32);

위 코드는 두 개의 필드를 가지는 Materials 구조체를 선언하는 예입니다. 튜플 형식으로 구현하면 특징적으로 필드의 이름이 없다는 점이 있겠네요.

이 형태의 구조체는 구조체라는 이름이 붙어있을 뿐이지 사실상 튜플과 비슷하게 사용할 수 있습니다.

let boxes = Materials(String::from("Color Box"), 10);
println!("{} x {}", boxes.0, boxes.1);

튜플에서 각 원소를 .을 이용해 액세스 하는 것과 비슷한 방식으로 그대로 사용할 수 있습니다.

튜플 구조체도 구조체이기 때문에 앞서 살펴본 구조체 해체 기능도 사용이 가능합니다.

let Materials(name, count) = boxes;
println!("{} x {}", name, count);

이 경우도 아마 소유권이 넘어가리라 생각됩니다.

 

단위 구조체(Unit-like Structs)

단위 구조체는 단위(unit)를 정의하는 용도로 사용되는 듯한(?) 구조체입니다. 타입 별칭을 만드는 것처럼 그냥 구조체를 이름만 지어주는 형태로 사용합니다.

struct Centimeters;

위의 코드는 Centimeters라는 구조체를 구현합니다.

이렇게 구현된 구조체는 아래와 같이 사용합니다.

let unit = Centimeters;

필드가 없기 때문에 초기화할 데이터도 없는 정말 타입 별칭 같은 느낌입니다.

안타깝게도 이 유형의 유용한 예제는 현재 시점에선 아직은 잘 모르겠습니다. 일단 impl이나 trait과 관련되어서 사용되는 경우가 있다 정도로만 알고 넘어가야 할 것 같습니다.

 

맺음말

처음에는 OOP의 클래스와 비슷한 존재로써 다뤄볼까 생각은 했지만 구조체에는 중요한 것이 하나 빠져있죠. 바로 상속입니다. OOP의 클래스는 상속 없이는 설명이 안 되는 개념이지요. 그래도 이것만 빼면 상당히 닮기도 한 것 같기도 합니다. 뭐... 참고는 가능하겠지요.

대신 구조체 개념은 Swift의 구조체와 비슷하고 거기다 protocol과 비슷한 trait까지 있는 것으로 봐서 나중에 Swift의 POP(Protocol Oriented Programming)와 비슷한 형태의 패러다임을 언급할 일이 있을지도 모르겠다는 생각이 드네요.

어쨌든 구조체에 관한 것은 이것으로 마무리합니다. 다음 글은 러스트의 가변성(Mutability)이라는 주제로 이어집니다.

728x90
반응형

댓글