모듈 (Module)

모듈 (Module)

모듈 (mod)

모듈은 코드를 하나의 논리적인 단위로 묶은 것으로, 모듈 안에는 함수, enum, struct, 상수, 변수 등의 Item(Rust에서 이를 아이템이라고 함)들이 들어 갈 수 있다. 모듈은 mod 라는 키워드 뒤에 모듈명을 적어 표시한다.

예를 들어, 아래는 circle 이라는 모듈을 예시한 것인데, 이 모듈은 상수 PI와 perimeter()와 area()라는 함수를 가지고 있다. 모듈 내의 아이템은 기본적으로 private 이기 때문에 외부에서 사용할 수 없다. 예를 들어, 아래에서 상수 PI는 private으로 외부에서 사용할 수 없다. 외부에서 사용하기 위해서는 pub 이라는 키워드는 적어 해당 아이템이 public 임을 표시해야 한다. 아래 예제에서 perimeter()와 area() 함수는 public 아이템으로 외부 main() 함수에서 호출할 수 있다.

// main.rs 파일

mod circle {
    const PI: f64 = 3.14;

    pub fn perimeter(r: f64) -> f64 {
        2.0 * PI * r
    }

    pub fn area(r: f64) -> f64 {
        PI * r * r
    }
}

fn main() {
    let a = circle::area(5.0);
    let p = circle::perimeter(5.0);
    println!("{}, {}", a, p);
}

참고로, 여기서의 circle 모듈은 main.rs 라는 한 파일에 있어서 main에서 모듈을 엑세스할 수 있게 된다. 만약 모듈이 다른 파일에 있다면 "mod circle" 앞에 pub을 붙여 주어야 한다.

Module 파일

여러 개의 모듈들을 한 파일에 모두 담으면 코드 관리가 힘들어 지기 때문에, 일반적으로 다른 파일에 각각의 모듈을 나누어 저장한다. 예를 들어, 위 예제의 circle 모듈을 아래와 같이 circle.rs 라는 파일에 옮겨 저장할 수 있다.

// circle.rs 파일
const PI: f64 = 3.14;

pub fn perimeter(r: f64) -> f64 {
    2.0 * PI * r
}

pub fn area(r: f64) -> f64 {
    PI * r * r
}

이렇게 (동일한 폴더의) 다른 파일에 저장된 모듈을 불러 사용하기 위해서는 아래와 같이 "mod circle;" 을 상단에 선언하고, circle::area() 와 같이 모듈명::함수명의 형식으로 불러 사용하면 된다.

circle 모듈을 (동일한 폴더의) 다른 파일에 저장할 때, 모듈명과 동일한 파일명(즉, circle.rs)을 사용하고, circle.rs 에서 mod circle { } 을 생략하고 함수 등과 같은 모듈 아이템만을 직접 작성한다. 즉, circle.rs 파일명에서 어떤 모듈인지 알아내기 때문에 이를 생략한다. 만약 circle.rs 파일 안에 다시 mod circle { } 와 같이 mod 키워드를 사용하면, 이는 circle::circle::area() 처럼 circle 안에 다른 circle 모듈이 있는 것으로 여겨진다.

// main.rs 파일

mod circle;

fn main() {
    let a = circle::area(5.0);
    let p = circle::perimeter(5.0);
    println!("{}, {}", a, p);
}

Nested Module

하나의 모듈 안에 다른 내포 모듈(nested module)을 정의할 수 있다.

예를 들어, 아래 예제에서 shape 모듈 안에 rect 모듈과 circle 모듈을 정의하였고, rect 모듈 안에는 다시 square 라는 모듈을 정의하였다. 모듈 아이템은 디폴트로 private 이므로 모듈 안에 정의된 내포 모듈도 기본적으로 private이다. 따라서 모듈 외부에서 사용하기 위해서는 pub 키워드롤 지정하여 public으로 만들어 준다.

// main.rs 파일
mod shape {
    pub mod rect {
        pub fn draw() {}

        mod sqaure {
        }
    }

    mod circle {
        fn draw() {}
    }
}

fn main() {
    shape::rect::draw();
}

내포 모듈을 엑세스하기 위해서는 namespace 연산자인 :: 을 사용하여 외부 모듈에서부터 내부 모듈까지 연결하고 모듈 아이템까지 호출할 수 있다. 예를 들어, main() 함수에서 rect 모듈의 draw() 함수를 호출하기 위해서는 shape::rect:draw() 와 같이 함수를 지정한다.

Nested Module 파일 구조

Nested Module들을 각각의 파일로 옮기 위해서는 다음과 같은 관례(convention)을 따른다.

  • 해당 모듈이 그 안에 내포 모듈(nested module)을 갖지 않은 경우는 그 모듈명으로 파일명을 정한다. 예를 들어, 위 예제의 square 모듈은 내포 모듈을 갖지 않으므로 square.rs 파일명을 사용한다.
  • 만약 해당 모듈이 그 안에 내포 모듈을 갖는 경우, 해당 모듈명으로 디렉토리를 만들고, 그 디렉토리 안에 mod.rs 라는 파일을 만들고 그 안에 모듈 아이템을 넣는다. 그리고 내포 모듈은 (그 안에 다시 또 다른 내포 모듈이 없는 경우) 그 내포 모듈명으로 파일을 만든다. 예를 들어, 위 예제의 rect 모듈은 square 라는 내포 모듈을 가지므로, rect 폴더를 만들고 그 안에 mod.rs 파일을 만들어 rect 모듈의 아이템을 넣는다. 그리고 square.rs 파일을 만들어 square 내포 모듈의 아이템을 넣는다.

위와 같은 관례를 따라 위 예제의 shape, circle, rect, square 모듈들을 폴더와 파일들로 나누어 구성하면 아래와 같은 파일 구조가 나올 것이다.

모듈 파일 구조

참고로, 모듈 파일들은 파일별로 아래와 같은 내용으로 나뉘어 질 것이다.

// main.rs 파일
mod shape;

fn main() {
    shape::rect::draw();
}
// shape/mod.rs 파일
mod circle;
pub mod rect;

// shape/circle.rs 파일
// ... 
// shape/rect/mod.rs 파일
mod square;

pub fn draw() {
    println!("rect draw");
}

// shape/rect/square.rs 파일
// ... 

라이브러리 모듈

Rust에서 (실행파일이 아닌) 라이브러리를 만들기 위해서 "cargo new {프로젝트명} --lib"와 같이 --lib 를 사용한다. 이렇게 생성된 라이브러리 프로젝트는 (src/main.rs 대신) src/lib.rs 라는 라이브러리 Crate 루트 파일을 생성한다.

src/lib.rs 에서는 아래와 같이 기본적으로 간단한 Boilerplate 테스트 모듈이 들어 있는데, 이는 나중에 라이브러리를 테스트할 때 사용될 수 있다.

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

라이브러리에서 여러 개의 모듈들을 사용할 수 있는데, 기본적으로 위에 설명한 방식과 관례를 따라 작성하면 된다. 즉, lib.rs 안에 (main.rs 에서와 같이) 함수 등과 같이 모듈 아이템을 바로 작성할 수 있고, 또는 mod 를 사용하여 모듈을 선언하거나 사용할 수 있다.

예를 들어, 아래는 lib.rs 파일 안에 calc 라는 모듈을 선언하고, tests 모듈에서 이를 테스트해 본 예이다. tests 모듈의 it_works() 함수에서 calc 의 add() 함수를 호출할 때 super 키워드를 사용하였는데, 이는 tests 모듈의 상위 모듈을 가리키는 것으로 이 상위 모듈 내에 있는 calc 모듈을 엑세스하기 위한 것이다.

// src/lib.rs 파일

mod calc {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b 
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = super::calc::add(2,2);
        assert_eq!(result, 4);
    }
}

라이브러리 Crate는 "cargo build"를 사용하여 라이브러리를 빌드하게 되며, 테스트코드가 있는 경우 "cargo test" 명령을 통해 테스트 코드를 실행하게 된다.

    $ cargo build        (빌드)
    $ cargo test         (테스트 실행)

라이브러리는 여러 개의 모듈들로 이루어질 수 있으며, 모듈 간 구조는 위의 "Nested Module 파일 구조" 섹션에서 설명한 방식과 동일하게 작성하면 된다.

This site is not affiliated with or endorsed by the Rust Foundation or Rust Project.