모듈 시스템 개요
Rust에서 모듈(module)은 코드의 가시성과 접근 범위를 체계적으로 관리하고 재사용성을 높이기 위한 핵심적인 구조적 단위다. 프로젝트 규모가 커질수록 많은 소스 파일이 생기고 각각의 구현이 복잡해지므로, 이를 적절히 계층화하여 관리하지 않으면 유지보수와 협업이 어려워진다. 모듈 시스템은 이러한 문제를 해결하기 위해 철저히 설계되었으며, 최상위 단위인 크레이트(crate)부터 내부의 하위 모듈들까지 명확하게 구분할 수 있도록 한다.
크레이트는 Rust 컴파일러가 독립적으로 컴파일할 수 있는 최소한의 단위다. 프로젝트에서 단일 크레이트가 빌드 대상이 되거나, 여러 개의 크레이트를 묶어 하나의 패키지(package)를 구성할 수도 있다. 예를 들어 바이너리 크레이트는 실행 가능한 프로그램을 만들어내고, 라이브러리 크레이트는 다른 프로젝트에서 재사용할 수 있는 공용 API나 기능을 제공한다. Cargo를 통해 프로젝트를 생성하면 보통 하나의 패키지 안에 하나의 바이너리 크레이트가 만들어지는데, 여기서 필요하다면 추가로 라이브러리 크레이트를 포함하거나 서브 크레이트들을 관리할 수도 있다.
각 크레이트 내부에서는 여러 개의 모듈을 정의할 수 있다. 모듈을 선언하려면 mod 키워드를 사용한다. 프로젝트가 확장됨에 따라 모듈도 여러 파일로 분할하여 관리할 수 있다. Rust의 모듈 트리는 트리 구조를 띠므로, 상위 모듈과 하위 모듈 간의 경로를 식별하는 방식이 중요하다. 모듈 내부에 또 다른 모듈을 만들면, 해당 모듈은 상위 모듈의 하위 계층에 위치하게 된다.
모듈에서 정의된 아이템(함수, 구조체, 열거형, 상수 등)의 접근 범위는 pub 키워드로 제어한다. Rust는 기본적으로 외부에서 모듈 내부의 심볼을 사용할 수 없게 막아둔다. 어떤 심볼을 외부 모듈에서 사용하고자 한다면, 해당 심볼이 선언된 곳에서 pub으로 공개 범위를 지정해야 한다. 또한 특정 모듈 전체를 공개 범위로 설정하고 싶다면 모듈 선언 시에도 pub mod 형태를 사용할 수 있다.
모듈로 나뉜 각 심볼은 상위 혹은 동등 계층 모듈에서 use 키워드를 통해 가져와 사용할 수 있다. 이때 경로 표기법이 중요한데, 전역 범위는 crate:: 키워드를 통해 명시할 수도 있고 모듈 간의 상대 경로 표기법도 지원한다. 예를 들어 현재 모듈에서 같은 계층에 위치한 다른 모듈의 함수를 가져오려면 use super::다른_모듈::함수명; 형태로 작성할 수 있다. 전역 범위나 루트 모듈에서부터 시작할 때는 crate:: 경로를 사용하거나, 라이브러리 크레이트를 참조할 때는 패키지의 라이브러리 이름을 사용하기도 한다.
Rust에서 모듈은 파일과 디렉터리 구조와도 밀접하게 연관된다. 만약 mod example;이라는 선언을 소스 코드에 명시하면, 컴파일러는 우선 현재 파일과 같은 디렉터리에 있는 example.rs 파일을 찾아 그 안의 내용을 example 모듈로 인식한다. 혹은 example이라는 디렉터리를 찾고, 그 내부의 mod.rs 파일을 확인하여 해당 디렉터리를 example 모듈의 소스 트리로 간주할 수도 있다. 이렇게 이름에 대응되는 파일이나 디렉터리를 찾는 방식은 Rust 컴파일러의 규칙에 따라 자동으로 이루어진다.
코드를 실제로 어떻게 분할하고, 어떤 범위로 공개할지 결정하는 과정은 프로젝트와 API의 요구 사항에 따라 달라진다. 라이브러리 크레이트를 개발한다면, 외부 사용자가 필요한 기능만 노출하도록 pub 키워드를 엄격히 관리해야 한다. 내부적으로 사용되는 구현 세부 사항이라면 공개하지 않는 편이 낫다. 반면 바이너리 크레이트라면 프로젝트가 자체적으로 사용하는 코드가 대부분일 것이므로, 모듈의 공개 여부도 프로젝트 내부 관리를 용이하게 하는 방향으로 결정하게 된다.
예를 들어 작은 프로젝트에서 모듈이 하나만 필요할 수도 있다. 그러나 대형 프로젝트라면 하위 기능별로 여러 개의 모듈을 갖추고, 그 하위에서 다시 서브 모듈을 여러 계층으로 구성한다. 전역적으로 사용될 도구 모음 모듈(utils), 입출력 모듈(io), 비즈니스 로직 모듈(core) 등을 따로 두고 서로 간에 종속성을 명확히 표현함으로써 코드 품질과 유지보수성이 향상된다.
Rust가 엄격한 스코프 및 경로 규칙을 제공하는 이유는 컴파일 시점에 구조적 오류를 쉽고 빠르게 감지하기 위함이다. 만약 잘못된 경로나 심볼로 접근을 시도한다면, 컴파일러가 경고나 에러 메시지를 명확히 제공하여 개발자가 수정할 수 있도록 유도한다. 이는 모듈 구조가 복잡해지더라도 컴파일 수준에서 오류를 빠르게 잡아낼 수 있게 해주는 강력한 장점이다.
다음은 간단한 모듈 선언 예시다.
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
fn multiply(a: i32, b: i32) -> i32 {
a * b
}
}
fn main() {
let sum = math::add(2, 3);
println!("2 + 3 = {}", sum);
}여기서 add 함수는 pub 키워드를 통해 공개되므로, main 함수에서 math::add 형태로 접근할 수 있다. 반면 multiply 함수는 공개되지 않았으므로 외부에서 호출할 수 없다. multiply 함수를 외부에서도 사용하고 싶다면 pub fn multiply(...) 형태로 명시하면 된다.
좀 더 복잡한 구조로 구성하고 싶다면 여러 파일을 사용해 모듈을 정의할 수 있다. 예를 들어 프로젝트의 src 디렉터리에 math.rs 파일을 만들고, 그 안에 모듈 구현을 배치한 뒤 상위 파일에서 mod math;로 선언할 수 있다. 그러면 main 함수에서는 마찬가지로 math::add와 같은 경로를 통해 접근할 수 있다. 혹은 math라는 디렉터리를 만들고 그 안에 mod.rs 파일을 두어 math 모듈을 구성하고, 하위 파일을 통해 더 세분화된 모듈을 관리하는 방법도 가능하다.
이처럼 Rust의 모듈 시스템은 단일 크레이트에서부터 시작해 모듈, 하위 모듈, 그리고 내부 심볼에 이르는 계층적 구조를 이루어 코드의 가독성과 재사용성을 높인다. 외부에 공개할 심볼과 내부 전용 심볼을 분리하여 관리하고, use 구문을 통해 필요한 부분만 가져다 쓰는 방식을 잘 설계하면, 규모가 커져도 유지보수가 쉬운 코드를 작성할 수 있다. 파일과 디렉터리 배치는 모듈 구조를 반영하게 되므로, 프로젝트가 성장할수록 파일 구조와 모듈 트리를 함께 정비하는 것이 권장된다.
모듈 시스템을 완전히 이해하기 위해서는 패키지, 크레이트, 모듈, 심볼에 대한 상호 관계와 함께 pub, use, mod 키워드가 어떤 식으로 작동하는지 충분히 숙지해야 한다. 이를 통해 Rust 코드베이스를 직관적으로 구성할 수 있고, 외부에 안전하고 간결한 API를 노출하는 라이브러리를 만들 수 있다. Rust가 제공하는 강력한 컴파일 검사 또한 모듈 경로와 심볼 접근 권한에 대한 실수를 조기에 발견할 수 있도록 돕는다.
Last updated