#9 프로젝트 관리

Pt J·2020년 8월 19일
1

[完] Rust Programming

목록 보기
11/41
post-thumbnail

이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.

지금까지 우리는 하나의 파일에 하나의 모듈로 작성했다.
모듈이 뭐지, 싶을 수도 있지만 그것을 배우지 않았다는 점에서 우린 모듈을 나눈 적 없다는 건 알 것이다.
아무튼 이렇게 하나로 모든 것을 구현하는 방식은 지금처럼 작은 프로젝트에서는 문제 없지만
프로젝트가 크고 복잡해질수록 이해하기 어렵고 추적하기 힘들어 문제를 야기할 수도 있다.

프로젝트를 여러 개의 파일과 모듈로 나누어 관리하면 보다 효율적으로 관리할 수 있다.
하나의 패키지는 여러 개의 바이너리 크레이트를 포함할 수 있으며
필요에 따라 하나의 라이브러리 크레이트를 포함할 수 있다.
그리고 어떤 부분은 패키지 내에 구현하는 게 아니라 외부의 의존 패키지로 추가할 수도 있다.

이와 같은 패키지 관리에 필요한 개념들을 알아보도록 하겠다.
우리가 앞으로 배울 모듈 시스템에는 다음과 같은 개념들이 포함된다.

  • 패키지 Packages
    크레이트를 빌드, 테스트, 공유할 수 있는 Cargo의 기능
    A Cargo feature that lets you build, test, and share crates
  • 크레이트 Crates
    라이브러리나 실행파일을 생성하는 모듈의 트리
    A tree of modules that produces a library or executable
  • 모듈 Modulesuse
    구조, 범위, 경로의 접근성을 제어하는 것
    Let you control the organization, scope, and privacy of paths
  • 경로 Paths
    구조체, 함수, 모듈 등의 이름을 결정하는 방식
    A way of naming an item, such as a struct, function, or module

패키지Package와 크레이트Crate

패키지는 하나 이상의 크레이트와 그것을 빌드하는 방법을 서술하는 Cargo.toml 파일로 이루어져 있다.
크레이트는 하나의 바이너리 혹은 라이브러리로, 모듈로 이루어져 있다.
Rust 컴파일러가 컴파일을 시작하는 소스 파일을 크레이트 루트라고 부른다.
그 구성을 알아보기 위해 cargo new 명령어를 통해 새 프로젝트,
그러니까 새 패키지를 생성해보자.

peter@hp-laptop:~/rust-practice$ mkdir chapter07
peter@hp-laptop:~/rust-practice$ cd chapter07
peter@hp-laptop:~/rust-practice/chapter07$ cargo new my-project
     Created binary (application) `my-project` package
peter@hp-laptop:~/rust-practice/chapter07$ tree my-project/
my-project/
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files
peter@hp-laptop:~/rust-practice/chapter07$ 

src/main.rs이 패키지와 같은 이름을 갖는 바이너리 크레이트의 크레이트 루트라는 것이 약속되어 있어
따로 명시해주지 않고 src/main.rs가 크레이트 루트로 사용된다.
마찬가지로 라이브러리 크레이트의 경우 src/lib.rs가 존재한다면
그것이 패키지와 같은 이름을 갖는 라이브러리 크레이트의 크레이트 루트가 된다.
여러 개의 바이너리 크레이트를 포함하고 싶다면 src/bin 디렉토리를 만들어
그 디렉토리 안에 작성하면 각 파일은 별개의 바이너리 크레이트가 된다.

기능을 크레이트 단위로 관리하면 필요한 기능 및 그것과 연관된 기능을 원활하게 가져다 사용할 수 있다.
그리고 이름이 겹치는 구조체, 함수 등이 존재하더라도 크레이트를 통해 구분할 수 있다.

모듈Module과 범위Scope

크레이트의 코드는 모듈 단위로 그룹화된다.
이로서 가독성과 재사용성을 높일 수 있고, 접근성을 제어할 수 있다.

모듈에 대한 이해를 돕기 위해 레스토랑 패키지를 생성하여 예제를 작성해보자.
이것은 기능적인 것보다는 구조적인 부분에 집중할 것이다.
cargo new 명령어는 기본적으로 바이너리 크레이트를 하나 가진 패키지를 생성하는데
라이브러리 크레이트를 하나 가진 패키지를 생성하려면 --lib 옵션을 추가하면 된다.
자동으로 생성되는 녀석에 대해서는 나중에 다루게 되겠지만 일단 지우고 예제를 작성하자.

peter@hp-laptop:~/rust-practice/chapter07$ cargo new --lib restaurant
     Created library `restaurant` package
peter@hp-laptop:~/rust-practice/chapter07$ cd restaurant/
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/lib.rs

src/lib.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

// 이대로 컴파일하면 사용하지 않는 함수가 포함되어 있다는 경고가 뜨는데 당연한 일이다.

mod 키워드로 시작되는 부분이 모듈이며 모듈 안에 다른 모듈이 정의될 수 있다.
모듈 안에는 구조체, 열거형, 상수, 트레이트 함수 등이 들어갈 수 있다.
이것을 통해 코드를 그룹화하면 그 이름으로 그것이 무엇에 쓰이는 녀석인지 알 수 있어
이해하기도 쉽고 필요한 코드를 찾기도 쉬우며
새로운 코드를 추가할 때도 적절한 위치를 파악할 수 있다.

크레이트는 모듈로 구성되어 있다고 했는데, 그 구조를 모듈 트리라고 한다.
모듈 트리의 루트는 crate라는 이름의 모듈이며
크레이트 루트인 src/main.rs 또는 src/lib.rs가 이에 해당한다.
우리가 작성한 restaurant 크레이트의 모듈 트리는 개념적으로 다음과 같이 구성된다.

restaurant 크레이트의 모듈 트리

crate
└── front_of_house
    ├── hosting
    │   ├── add_to_waitlist
    │   └── seat_at_table
    └── serving
        ├── table_order
        ├── serve_order
        └── take_payment

그리고 이러한 모듈 트리의 구조는 모듈의 경로를 찾을 때도 이용된다.

경로 Path

경로는 절대 경로와 상대 경로로 구분할 수 있다.

  • 절대 경로
    크레이트 루트로부터 시작하여 크레이트 이름이나 crate를 이용하여 나타낸 경로
    An absolute path starts from a crate root by using a crate name or a literal crate.
  • 상대 경로
    현재 모델로부터 시작하여 self, super, 또는 현재 모듈의 식별자를 이용하여 나타낸 경로
    A relative path starts from the current module and uses self, super, or an identifier in the current module.

우리가 작성한 모듈 아래에 새 함수를 만들고
모듈 내 함수를 절대 경로 및 상대 경로를 통해 호출하는 코드를 작성해보자.

peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/lib.rs

src/lib.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

pub fn eat_at_restaurant() {
    crate::front_of_house::hosting::add_to_waitlist();

    front_of_house::hosting::add_to_waitlist();
}

eat_at_restaurant 함수의 두 함수 호출은 동일한 함수를 호출하는 것이며
첫번째 것이 절대 경로, 두번째 것이 상대 경로로 나타낸 것이다.
둘 다 add_to_waitlist에 도달하기까지의 경로를 ::를 통해 작성했다.
절대 경로를 사용할지 상대 경로를 사용할지는 프로젝트를 관리하기 나름이다.
공식 문서를 작성한 필자들은 절대 경로를 선호한다는 건 여담.

"Our preference is to specify absolute paths because it’s more likely to move code definitions and item calls independently of each other."

pub 키워드

그런데 방금 작성한 코드를 cargo build로 컴파일하면 오류를 만나게 된다.

peter@hp-laptop:~/rust-practice/chapter07/restaurant$ cargo build
   Compiling restaurant v0.1.0 (/home/peter/rust-practice/chapter07/restaurant)
error[E0603]: module `hosting` is private
  --> src/lib.rs:18:28
   |
18 |     crate::front_of_house::hosting::add_to_waitlist();
   |                            ^^^^^^^ private module
   |
note: the module `hosting` is defined here
  --> src/lib.rs:2:5
   |
2  |     mod hosting {
   |     ^^^^^^^^^^^

error[E0603]: module `hosting` is private
  --> src/lib.rs:20:21
   |
20 |     front_of_house::hosting::add_to_waitlist();
   |                     ^^^^^^^ private module
   |
note: the module `hosting` is defined here
  --> src/lib.rs:2:5
   |
2  |     mod hosting {
   |     ^^^^^^^^^^^

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant`.

To learn more, run the command again with --verbose.
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ 

hosting 모듈이 private인데 우리가 외부에서 접근하려고 했다는 것이다.
모듈은 코드의 효율적인 관리만을 위한 게 아니고 캡슐화의 기능도 있다.
그리고 Rust는 변수를 기본적으로 불변성을 띄게 만드는 것과 같이
모듈도 따로 명시하지 않는다면 private으로 정의된다.
모듈 내부의 구조체, 함수 등 모든 것이 그렇다.
위 예제에서 front_of_houseeat_at_restaurant와 같은 범위에 있어 접근 가능하지만
front_of_house 내부에 있는 hosting 같은 경우에는 그렇지 않아 접근할 수 없다.
만약 외부에서 접근하고 싶다면 따로 public을 명시해주어야 하는데
pub 키워드를 사용하면 된다.
이 때, 경로 상의 모든 모듈, 함수 등이 public이어야 한다는 점을 유의하자.

즉, 다음과 같이 수정해야 컴파일을 할 수 있다.

peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/lib.rs

src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

pub fn eat_at_restaurant() {
    crate::front_of_house::hosting::add_to_waitlist();

    front_of_house::hosting::add_to_waitlist();
}

상대 경로 중에는 부모 모듈을 나타내는 super로부터 시작하는 경우도 있다.
그 예시로, 잘못된 주문을 받은 경우 요리사가 요리 후 직접 음식을 가져다준다고 하자.
이 때, 음식을 가져다주는 것은 front_of_house 모듈에 정의되어 있고
요리사의 작업은 back_of_house 모듈에 정의된다고 하면
이는 다음과 같이 작성될 수 있다.

peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/lib.rs

src/lib.rs

mod front_of_house {
    // snip

    pub mod serving {
        fn take_order() {}

        pub fn serve_order() {}

        fn take_payment() {}
    }
}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::front_of_house::serving::serve_order();
    }

    fn cook_order() {}
}

pub fn eat_at_restaurant() {
    crate::front_of_house::hosting::add_to_waitlist();

    front_of_house::hosting::add_to_waitlist();
}

back_of_house 모듈의 fix_incorrect_order 함수에서
그것이 정의된 모듈 바깥의 함수에 접근하고자 하여 super를 사용한 것을 확인할 수 있다.
이렇게 super를 사용하면 이후에 모듈 트리를 재구성하게 되더라도
이들끼리의 상대적인 경로가 달라지지 않는다면 코드 수정을 최소화 할 수 있다.

함수와 모듈뿐만 아니라 구조체나 열거형도
pub 키워드를 통해 외부에서 접근 가능하도록 공개할 수 있는데
이와 관련하여 몇 가지 알아두어야 하는 것이 있다.

구조체는 가변성이 필드가 아닌 구조체 전체를 대상으로 설정되었던 것과 달리
pub 키워드는 각각의 필드 단위로 적용된다.
따라서 공개하고자 하는 필드만 공개하고
나머지는 비공개로 놔둔 채 메서드를 통해서만 접근할 수 있도록 한다.
그리고 열거형의 경우 열거형만 공개하면 그 안의 열것값들은 자동으로 공개된다.
생각해보면 열것값은 공개 안하고 열거형만 공개한다거나 열것값 중 일부만 공개하는 건
그닥 의미 있는 일은 아니긴 하다.

restaurant 예제에 구조체와 열거형을 사용하는 코드를 추가해보자.

peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/lib.rs

src/lib.rs

mod front_of_house {
    // snip
}

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }
  
    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }

    pub enum Appetizer {
        Soup,
        Salad,
    }

    fn fix_incorrect_order() {
        cook_order();
        super::front_of_house::serving::serve_order();
    }

    fn cook_order() {}
}

pub fn eat_at_restaurant() {
    front_of_house::hosting::add_to_waitlist();
  
    let appetizer = back_of_house::Appetizer::Salad;
    let mut meal = back_of_house::Breakfast::summer("Rye");

    meal.toast = String::from("Wheet");
    println!("I'd like {} toast please", meal.toast);
}

use 키워드

우리가 작성한 코드를 보면 모듈이 중첩될수록 그 길이가 길어지는 것을 볼 수 있다.
이것이 길어질수록 우리 코드는 복잡해보인다.

따라서 우리는 필요에 따라 다른 범위의 코드를 현재 범위로 가져와 사용할 수 있는데
이 때 사용하는 것이 use 키워드다.
use 키워드를 통해 어떤 구문을 현재 범위로 가져오면
그것을 마치 현재 범위에서 정의한 것처럼 사용할 수 있다.

예를 들어, 다음과 같이 crate::front_of_house::hosting을 루트 범위로 가져오면
크레이트 루트에 정의된 eat_at_restaurant 함수에서 그것을 hosting으로 접근할 수 있다.

peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/lib.rs

src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    // snip
}

mod back_of_house {
    // snip
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
   
    let appetizer = back_of_house::Appetizer::Salad;
    let mut meal = back_of_house::Breakfast::summer("Rye");

    meal.toast = String::from("Wheet");
    println!("I'd like {} toast please", meal.toast);
}

상대경로를 사용할 경우 현재 범위의 이름 대신 self를 넣어
use self::front_of_house::hosting;과 같이 작성한다.

물론 use crate::front_of_house::hosting::add_to_waitlist();처럼 가져와서
add_to_waitlist();만으로 호출하는 것도 가능하지만
그럴 경우 이게 현재 범위에서 정의된 것인지 누구의 것인지 알기 어려워
구조체, 열거형 등을 가져올 땐 그 메서드가 아닌 그 아이템 단위로 가져오는 것이 관용적이다.

특히 서로 다른 경로에서부터 같은 이름의 아이템을 가져올 경우
Rust 컴파일러는 그 이름만으로 그것을 구분할 수 없어 이를 허용하지 않는다.
굳이 같은 이름의 아이템을 가져오고 싶다면 as 명령어를 사용할 수 있는데
use A as B;와 같은 형식으로 작성하면 A 대신 B라는 이름으로 사용할 수 있다.
이와 같이 명시적으로 구분지으면 원래 이름이 같아도 새 이름으로 구분해서 사용할 수 있다.

pub use 키워드

pub use 키워드는 현재 범위로 가져온 것을 다시 내보내기까지 한다.
즉, 현재 범위 밖에서도 그것을 pub use가 사용된 위치에 정의된 것처럼 사용할 수 있게 된다.
이러한 기법은 크레이트의 구조는 관리하기 좋은 상태로 둔 채
pub use를 통해 외부에서 사용하기 좋은 형태로 접근 구조를 바꿔준다.

외부 크레이트

외부 크레이트 또한 use 키워드를 통해 가져올 수 있는데,
표준 라이브러리처럼 Prelude에 포함된 크레이트라면 그냥 use만으로 사용 가능하지만
일반적으로는 Cargo.toml에 패키지와 버전을 명시해주어야 한다.
숫자 맞추기 게임을 만들었을 때 했던 것처러 말이다.
Cargo.toml의 의존성 목록에 추가하고 나면 그것의 하위 모듈 및 아이템은
use를 통해 적절한 범위에 가져올 수 있다.

중첩 경로 Nested Path

만약 둘 이상의 아이템을 use로 가져오는데 그들의 경로에 겹치는 부분이 있다면
우리는 중첩 경로를 통해 코드의 양을 조금 줄일 수 있다.

예를 들어 다음과 같이 작성했던 건

use std::io;
use std::cmp::Ordering;

다음과 같이 작성할 수 있다.

use std::{io, Ordering};

만약 그 중 중첩된 부분과 완전히 일치하는 경로가 있다면 그것은 self를 통해 나타낼 수 있다.

예를 들어 다음과 같이 작성했던 건

use std::io;
use std::io::Write;

다음과 같이 작성할 수 있다.

use std::io::{self, Write};

그리고 해당 범위의 모든 것을 가져오고자 한다면 다음과 같은 것도 가능한데

use std::collections::*;

어떤 것들이 가져와졌는지 알기 어려워 주의해서 사용해야 한다.

모듈 분할

지금까지의 예제는 하나의 src/main.rs 파일, 또는 하나의 src/lib.rs 파일로 작성되었다.
하지만 전체 패키지가 커질수록 이것은 모듈 단위로 분할하는 쪽이 관리하기 좋다.
src/ 아래에 front_of_house.rs, back_of_house.rs 파일을 생성하고
해당 이름의 모듈의 본문을 각각 파일로 옮겨 보도록 하자.
그리고 src/lib.rs에서는 코드블록을 지우고 세미콜론을 찍어
그런 이름을 가진 모듈이 존재한다는 사실만을 남긴다.

peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/front_of_house.rs

src/front_of_house.rs

pub mod hosting {
    pub fn add_to_waitlist() {}

    fn seat_at_table() {}
}

pub mod serving {
    fn take_order() {}

    pub fn serve_order() {}

    fn take_payment() {}
}
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/back_of_house.rs

src/back_of_house.rs

pub struct Breakfast {
    pub toast: String,
    seasonal_fruit: String,
}

impl Breakfast {
    pub fn summer(toast: &str) -> Breakfast {
        Breakfast {
            toast: String::from(toast),
            seasonal_fruit: String::from("peaches"),
        }
    }
}

pub enum Appetizer {
    Soup,
    Salad,
}

fn fix_incorrect_order() {
    cook_order();
    super::front_of_house::serving::serve_order();
}

fn cook_order() {}
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/lib.rs

src/lib.rs

mod front_of_house;
mod back_of_house;

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
  
    let appetizer = back_of_house::Appetizer::Salad;
    let mut meal = back_of_house::Breakfast::summer("Rye");

    meal.toast = String::from("Wheet");
    println!("I'd like {} toast please", meal.toast);
}

이렇게 하면 Rust 컴파일러가 src/lib.rs를 컴파일하다가
mod front_of_house;를 만나면 같은 이름을 가진 .rs 파일을 찾아
그곳에서 front_of_house 모듈의 정의를 확인한다.
back_of_house도 마찬가지다.

그리고 하위 모듈들도 같은 방식으로 분할할 수 있는데
하위 모듈은 상위 모듈과 같은 이름을 가진 디렉토리를 생성하여 그 안에 작성한다.

peter@hp-laptop:~/rust-practice/chapter07/restaurant$ mkdir src/front_of_house
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/front_of_house/hosting.rs

src/front_of_house/hosting.rs

pub fn add_to_waitlist() {}

fn seat_at_table() {}
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/front_of_house/serving.rs

src/front_of_house/serving.rs

fn take_order() {}

pub fn serve_order() {}

fn take_payment() {}
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/front_of_house.rs

src/front_of_house.rs

pub mod hosting;
pub mod serving;

그리고 이렇게 하면 front_of_table이 파일 이름이기도 하고 폴더 이름이기도 한데
src/front_of_table.rsscr/front_of_table/mod.rs로 옮길 수도 있다.

peter@hp-laptop:~/rust-practice/chapter07/restaurant$ mv src/front_of_house.rs src/front_of_house/mod.rs

자, 이로서 모듈 트리는 유지한 채 관리하기 수월한 파일 단위로 분할하였다.

개념 적인 모듈 트리는 다음과 같고

crate
├── front_of_house
│   ├── hosting
│   │   ├── add_to_waitlist
│   │   └── seat_at_table
│   └── serving
│       ├── table_order
│       ├── serve_order
│       └── take_payment
└── back_of_house
    ├── fix_incorrect_order
    └── cook_order

실제 파일 구조는 다음과 같다.

peter@hp-laptop:~/rust-practice/chapter07/restaurant$ tree src
src
├── back_of_house.rs
├── front_of_house
│   ├── hosting.rs
│   ├── mod.rs
│   └── serving.rs
└── lib.rs

1 directory, 5 files
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ 

모듈의 경로를 알면 그것이 실제 경로도 쉽게 찾을 수 있을 것이다.

이 포스트의 내용은 공식문서의 7장 Managing Growing Projects with Packages, Crates, and Modules에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글