본문 바로가기

Rust Programming

[Rust] 3. Ticket v1: Structs, Validation, Modules, Visibility

Rust를 정말 특별하게 만드는 ownership을 이번 Chapter에서 배울 수 있다고 한다.

ownership은 garbage collector없이 Rust를 memory-safe하고 효율적이게 만들어준다.

 

software project의 bugs, features, tasks를 추적할 수 있는 (JIRA-like) ticket을 사용할 것이다.

그래서 아래와 같은 Rust의 새로운 concepts를 베울 것이다.

 

  • Rust의 custom types인 struct
  • Ownership과 references와 borrowing
  • Stack, heap, pointers, data layout, destructors
  • Modules, visibility
  • Strings

 

여느 때와 같이 배울 준비 됐다는 exercise가 있다

 

나는 software ticket을 modelling할 준비가 됐다!

 


 

우리는 각 ticket의 아래 세 개의 정보들을 track할 필요가 있다.

  • title
  • description
  • status

우리는 위의 것들을 표현하기위해 String을 사용할 수 있다.

String은 Rust standard library에 정의된 type으로 UTF-8 encoded text를 표현한다.

 

하지만 위 세 개의 정보를 하나의 entity에 어떻게 결합시킬 수 있을까?

 


 

struct는 새로운 Rust type을 정의한다.

struct Ticket {
    title: String,
    description: String,
    status: String
}

 

struct는 다른 0프로그래밍 언어에서 class나 object로 부르는 것과 비슷하다.

 

새로운 type은 다른 types를 fields로서 결합하여 생성한다.

각 field는 이름과 type이 있어야하며, colon(:)으로 구분된다.

만약 여러개의 fields가 있다면, 그것들은 comma(,)로 구분한다.

Fields는 아래와 같이 같은 type일 필요는 없다.

struct Configuration {
   version: u32,
   active: bool
}

 

개발자는 각 field에 값을 넣어줌으로써, 정의했던 struct의 instance를 만들 수 있다.

// Syntax: <StructName> { <field_name>: <value>, ... }
let ticket = Ticket {
    title: "Build a ticket system".into(),
    description: "Create a system that can manage tickets across a Kanban board".into(),
    status: "Open".into()
};

 

그리고, 개발자는 다음과 같이 struct name과 dot(.) operator를 통해 fileds에 접근할 수 있다.

// Field access
let x = ticket.description;

 


 

개발자는 methods 정의를 통해, structs 내에 behaviour를 만들 수 있다.

위에서 정의한 Ticket struct를 예시로 사용하겠다.

impl Ticket {
    fn is_open(self) -> bool {
        self.status == "Open"
    }
}

// Syntax:
// impl <StructName> {
//    fn <method_name>(<parameters>) -> <return_type> {
//        // Method body
//    }
// }

 

Methods는 fuctions와 꽤 비슷하지만, 두 가지 차이점이 있다.

  1. Methods는 impl block 내에 정의돼야한다.
  2. Methods는 첫번째 parameter로 self를 사용할 수 있는데, self는 keyword이며, 해당 method를 호출한 struct의 instance를 나타낸다.

 

만약, method가 self를 첫번째 parameter로 받는다면, method call syntax를 사용해 호출될 수 있다.

// Method call syntax: <instance>.<method_name>(<parameters>)
let is_open = ticket.is_open();

 

이건 이전 saturating arithmetic operation를 수행할 때 사용한 문법과 똑같다.

 


 

method가 self를 첫번째 parameter로 받지 않는다면, 그것은 static method라 한다.

struct Configuration {
    version: u32,
    active: bool
}

impl Configuration {
    // `default` is a static method on `Configuration`
    fn default() -> Configuration {
        Configuration { version: 0, active: false }
    }
}

 

static method를 호출하는 유일한 방법은 다음과 같이 funtion call syntax를 따르는 것이다.

// Function call syntax: <StructName>::<method_name>(<parameters>)
let default_config = Configuration::default();

 

self를 first parameter로 받았어도 위 문법으로 사용할 수 있다.

// Function call syntax: <StructName>::<method_name>(<instance>, <parameters>)
let is_open = Ticket::is_open(ticket);

 

ticket이 self로 사용되어 명료해보이지만, 굳이 넣지 않아도 될 것을 넣는 것 같다.

그래서 가능하다면 method call syntax를 선호하라고 한다.

 


 

01_struct exercise는 Order라는 struct를 만들고

price, quantity filed를 각각 unsigned integer type으로 설정해주며

Order struct의 method인 is_available를 만드는 작업이었다.

 

test 과정에서 price를 사용할 일이 없어 저렇게 경고를 띄우는 것 같다.

하지만 하라는 tasks는 다 수행했고 성공적으로 exercise를 통과했다.

 


 

ticket definition으로 돌아가보자.

struct Ticket {
    title: String,
    description: String,
    status: String,
}

 

Ticket struct의 fields에 raw types를 사용하고 있다.

이게 무슨 의미냐면!

사용자가 빈 title과 매우~~~~~~~ 긴 description 혹은 의미없는 status(Funny같은)

등을 넣어 ticket을 생성할 수 있다는 뜻이다.

 

우리는 이것보다 더 잘할 수 있다!

 


 

02_validation exercise가 위와 관련된 것이다.

https://doc.rust-lang.org/std/string/struct.String.html

 

String in std::string - Rust

Reserves the minimum capacity for at least additional bytes more than the current length. Unlike reserve, this will not deliberately over-allocate to speculatively avoid frequent allocations. After calling reserve_exact, capacity will be greater than or eq

doc.rust-lang.org

위 사이트를 확인하여, string의 methods를 사용하면 된다한다.

 

 

위와 같이 길게 뜨며 exercise를 pass했다.

각 field의 유효성을 체크하고 유효하지 않을 시, 요청에 맞는 panic을 발생시키면 된다.

string의 len와 is_empty method를 사용했고

마지막은 그냥 비교만 해주면 된다.

 


 

이전 exercise에서 한 new method는 Ticket struct field value값에 몇가지 제약을 두기위한 것이었다.

하지만 꼭 그렇게 해야만 하는것일까?

new method없이 개발자는 어떻게할 수 있을까?

 

visibility와 modules의 개념으로부터 encapsulation을 배워보자.

먼저 module은 무엇일까?

 

Rust의 module은 공동된 namespace(module의 이름)에서 관련된 code들을 그룹화 하는 것이다.

이미 exercise에서 아래와 같이 tests라는 module을 봤을 것이다.

#[cfg(test)]
mod tests {
    // [...]
}

 

위의 tests module은 inline module이다.

module 정의와 module의 내용이 붙어서 있는 것이다.

 

modules는 tree구조를 만들며 nested될 수 있다.

tree의 root는 다른 모든 modules를 포함하는 top-level module인 crate 자기 자신이다.

library crate에서는 root module이 보통 src/lib.rs이다.(customized되지 않았다면)

root module은 crate root라고도 불린다.

 

crate root는 계속 계속 이어져서 submodules를 가질 수 있는, submodules를 가질 수 있다.

 


 

inline modules는 규모가 작은 code에서는 유용하다.

하지만 우리의 project는 커질 것이고, 아마도 code를 여러개의 파일로 나누고 싶을 것이다.

parent module에서 개발자는 mod keyword를 통해 submodule의 존재를 선언할 수 있다.

mod dog;

 

Rust의 build tool인 cargo는 위 선언을 보고 module 구현이 포함된 파일을 찾을 것이다.

만약, 사용자의 module이 crate의 root에 선언되어 있으면(src/lib.rs or src/main.rs)

cargo는 file을 다음과 같은 이름으로 예상할 것이다.

  • src/<module_name>.rs
  • src/<module_name>/mod.rs

만약, 사용자의 module이 다른 module의 submodule이라면, 파일이름은 다음과 같아야한다.

  • [..]/<parent_module>/<module_name>.rs
  • [..]/<parent_module>/<module_name>/mod.rs

예를 들어, 만약, dog가 animals의 submodule이라면 src/animals/dog.rs나 src/animals/dog/mod.rs여야한다.

 

mod keyword를 통해 새로운 module을 선언할 때, IDE가 자동으로 이 파일들을 생성할 수 있게 도와줄 것이다.

 


 

사용자는 같은 module에 선언된 items에 어떤 특별한 문법없이 접근할 수 있다.

사용자는 그저 사용할 items의 이름만 사용하면 된다.

struct Ticket {
    // [...]
}

// No need to qualify `Ticket` in any way here
// because we're in the same module
fn mark_ticket_as_done(ticket: Ticket) {
    // [...]
}

 

다른 module에서 entity에 접근할 경우는 다르다.

사용자는 접근하고 싶은 entity에 path pointing을 사용해야한다.

 

path를 다음과 같이 다양한 방법으로 구성할 수 있다. 

  • 현재 crate의 root로부터 출발 시, crate::module_1::module_2::MyStruct
  • parent module로부터 출발 시, super::my_function
  • 현재 module로부터 출발 시, sub_module_1::MyStruct

type을 참조할 때마다, 전체 경로를 다 쓰는 것은 매우 번거롭다.

더 쉽게 하기위해, use statement를 사용하여 entity를 해당 scope로 가져올 수 있다.

// Bring `MyStruct` into scope
use crate::module_1::module_2::MyStruct;

// Now you can refer to `MyStruct` directly
fn a_function(s: MyStruct) {
     // [...]
}

 

또한, use statement를 사용해 모든 items를 module로부터 import할 수 있다.

use crate::module_1::module_2::*;

 

이것은 star import라 불린다.

일반적으로 현재 namespace를 오염시킬 수 있으므로 비추한다.

name conflicts나 name이 어디서 왔는지 혼동이 일어날 수 있다.

그럼에도 불구하고, 몇몇 경우에는 매우 유용하다.(unit tests를 할 때 등등)

 

이 과정의 exercise들도 모두 use super::*;를 통해 test를 시작한다.

 


 

03_modules의 exercise는 compile이 되게 코드를 바꾸는 것이었다.

그냥 use crate::Ticket;만 추가해주면 된다.

 


 

여러 modules로 구성된 code를 작성하려 할 때, visibility에 대해 생각할 필요가 있다.

Visibility는 주어진 entity의 어느 영역까지 다른 사용자가 접근할 수 있는지를 결정한다.

접근 가는한 영역을 결정할 것에는 struct, function, field 등이 될 수 있다.

 

defalut도 Rust의 모든 것은 private이다.

private entity는 다음과 같은 경우에만 접근될 수 있다.

  1. 정의되어 있는 같은 module 안일 때
  2. submodules 중 하나일 때

이전 exercise에서 이것을 사용했다.

 

create_todo_ticket은 use statement를 사용하면 Ticket을 사용할 수 있다.

왜냐하면 helpers module은 Ticket이 정의된 crate root의 submodule이기 때문이다.

이외에도 모든 exercise unit tests는 test되는 코드의 submodule에 정의되어 있다.

그래서 제약없이 모두 접근할 수 있던 것이다.

 


 

visibility modifier를 통해 사용자는 entity의 default visibility를 수정할 수 있다.

visibility modifiers는 다음과 같다.

  • pub: entity를 public으로 만드는데 entity가 정의된 모듈이 아닌 다른 module에서도 접근할 수 있다. 다른 crates에서도 접근할 수 있다.
  • pub(crate): entity를 public으로 만드는데 같은 crate안에서 접근할 수 있다.
  • pub(super): entity를 public으로 만드는데 parent module안에서 접근할 수 있다.
  • pub(in path::to::module): entity를 public으로 만드는데 정의된 module안에서 접근할 수 있다.

이 modifiers를 modules나 fucntions, fields 등에서 사용할 수 있다.

pub struct Configuration {
    pub(crate) version: u32,
    active: bool,
}

 

위에서 Configuration은 public이지만, 사용자는 같은 crate 내에서 version field만 접근할 수 있다.

대신 active field는 private이며 같은 module이나 submodules에서 접근할 수 있다.

 


 

그냥 객체지향 프로그래밍에서의 중요한 개념을 배우는 것 같다.

사실 학교 수업시간에 많이 듣고 시험봤던 내용이고 익숙하다.

04_visibility exercise는 private 접근에 대한 compile error를 확인하는 작업이었다.

 


 

객체 지향 프로그래밍을 전체적으로 복습하는 느낌이다.