모듈 파일 구조

러스트에서 모듈은 코드를 논리적으로 분할하고 관리하기 위한 중요한 개념이다. 모듈 시스템을 제대로 활용하면 관련된 함수와 자료구조를 그룹화하여 가독성을 높이고 코드 재사용성을 극대화할 수 있다. 모듈은 단순히 main.rs나 lib.rs 안에만 정의될 수도 있지만, 실제 현업 프로젝트에서는 일반적으로 여러 파일로 모듈을 분산해 관리한다. 이때 모듈 파일 구조를 명확히 이해해야 빌드나 컴파일 오류 없이 효율적으로 프로젝트를 구성할 수 있다. 러스트 컴파일러는 mod 키워드와 파일 이름의 규칙을 기반으로 소스 트리를 탐색하면서 필요한 모듈을 로드해 빌드한다. 이러한 특성을 올바르게 이해하지 못하면 제대로 된 모듈 구조를 설계하기 어려워진다.

러스트의 모듈 파일 구조를 구성하는 방식은 크게 두 가지가 있다. 첫 번째로 하나의 파일에 모든 모듈을 선언하고 내부에 서브모듈을 포함시킬 수 있는 방법이 있다. 이 경우에는 내부에 선언되는 서브모듈이 사용하는 함수나 자료구조가 어디까지 공개되어야 하는지를 pub 사용을 통해 세밀하게 제어한다. 두 번째는 모듈 단위를 개별 파일로 분리해 관리하는 방법이다. 예컨대 src 디렉터리 안에 my_module.rs라는 파일을 만들고, 상위 파일에서 mod my_module; 구문을 통해 이를 로드하면 my_module.rs에 정의된 모듈이 상위 모듈에 포함된다. 이 방법이 보다 일반적이며, 규모가 큰 프로젝트에서 모듈을 체계적으로 관리하기에 적합하다.

상위 모듈 파일에서 선언한 서브모듈이 또 다른 하위 모듈을 가질 수 있다는 점에도 주의해야 한다. 예컨대 my_module 내부에 sub_module이라는 하위 모듈이 있으면, 보통은 my_module 디렉터리를 만들고 그 안에 sub_module.rs 파일을 배치하거나 mod.rs라는 특별한 파일을 사용한다. 러스트 2018 에디션을 포함한 최신 에디션에서는 모듈 디렉터리 내부에 mod.rs 파일을 두기보다는, 디렉터리 이름과 동일한 rs 파일을 활용하는 방식이 더 권장된다. 예를 들어 다음과 같은 디렉터리 구조를 가진 프로젝트가 있을 수 있다.

프로젝트 루트
 ├── src
 │    ├── main.rs
 │    ├── my_module.rs
 │    └── my_module
 │         ├── sub_module.rs
 │         └── helper.rs
 └── Cargo.toml

main.rs 파일 안에서 mod 키워드를 사용해 my_module을 로드할 수 있다. my_module.rs 내부에서 다시 서브모듈 sub_module을 로드하려면 mod sub_module; 구문을 사용한다. 또 다른 파일 helper.rs를 같은 my_module 디렉터리에 두었으면, sub_module에서 pub(super) 등을 통해 helper.rs의 함수에 접근하게 만들 수도 있다. 이처럼 모듈의 가시성(pub, pub(crate), pub(super) 등)을 적절히 설정하여 내부 구현을 감추고 필요한 인터페이스만 공개함으로써 모듈 간 의존성을 줄이고 코드를 안전하게 재활용할 수 있다.

여러 파일로 모듈을 구성할 때 가장 중요한 핵심은 “러스트 컴파일러가 어떤 규칙으로 모듈 소스를 찾아가는가”를 이해하는 것이다. 상위 파일에서 mod my_module;를 선언했다면 컴파일러는 src/my_module.rs 또는 src/my_module/mod.rs 파일이 있는지 찾는다. 만약 my_module 디렉터리가 있고 그 안에 서브모듈이 있다면, my_module.rs 내부에서 추가로 mod sub_module;를 선언해야 하며, 그것은 다시 my_module 디렉터리 아래에 있는 sub_module.rs 또는 sub_module/mod.rs 파일로 매핑된다. 이러한 구조가 어긋나면 컴파일 에러가 발생하므로 파일과 디렉터리 배치, 그리고 모듈 선언 구문 mod 키워드의 사용 위치를 정확히 맞춰줘야 한다.

보통 단순한 프로젝트라면 main.rs 하나에 모든 모듈을 선언하고 함수 구현을 몰아넣어도 된다. 그러나 프로젝트가 커질수록 파일을 여러 개로 나누어 체계적으로 관리하는 것이 훨씬 읽기 쉽고 유지 보수에 유리하다. 특히 라이브러리 크레이트를 작성할 때는 lib.rs에 기본 모듈들을 배치하고, 세부 구현들은 별도의 파일들에 나누는 방식이 일반적이다. main.rs를 사용해 실행 바이너리 크레이트를 작성한다면, main.rs에서 필요한 라이브러리를 lib.rs로부터 가져와 사용하는 것이 권장된다. 이러한 패턴은 많은 러스트 오픈소스 프로젝트에서 표준처럼 활용되고 있으며, 개발자가 프로젝트 규모를 확장할 때 발생하는 복잡도를 크게 줄여준다.

러스트의 모듈 시스템에서는 외부 공개 여부를 결정하는 키워드(pub)가 매우 중요하므로, 모듈을 파일로 분리할 때도 공개 범위를 엄밀히 정의해야 한다. 파일을 분할해놓고 모든 기호를 전부 pub로 공개해버리면 모듈링의 이점이 크게 줄어든다. 필요한 함수나 자료구조만 최종적으로 외부에 노출하는 설계를 잘 해놓으면, 모듈 내부 구현이 바뀌더라도 외부 모듈과의 인터페이스만 유지하면 되므로 코드 변경으로 인한 파급이 최소화된다.

러스트에서는 use 키워드를 이용해 다른 모듈에 있는 타입이나 함수를 현재 스코프로 가져올 수 있다. 파일 구조를 활용해 모듈을 분산해두었다면, 모듈 간에 필요한 기능을 use 구문을 통해 불러와야 한다. 이때 상위 모듈과 하위 모듈 간의 위치 관계를 정확히 고려해야 한다. 예컨대 하위 모듈에서는 use super:: 타입 형태로 상위 모듈에 정의된 항목을 가져오기도 하며, 최상위 crate 루트부터 경로를 작성할 수도 있다. 경로 문제를 잘못 처리하면 컴파일 에러가 자주 발생하므로, 프로젝트 구조가 복잡해질수록 명시적 경로나 alias를 잘 설정해야 한다.

아래는 가장 기본적인 분리 예시로, main.rs와 별도 파일 my_module.rs 두 개의 파일을 이용해 하나의 바이너리 크레이트를 구성한 형태다.

.
├── Cargo.toml
└── src
    ├── main.rs
    └── my_module.rs

다음은 main.rs의 내용이다.

my_module.rs에는 간단한 함수를 정의할 수 있다.

위 예시에서 main.rs 마지막 부분에서 mod my_module;를 선언했기 때문에, 컴파일러는 동일한 디렉터리(src)에 my_module.rs가 존재하는지 확인한다. 파일을 찾으면 해당 파일을 상위 모듈의 일부분으로 취급하여 빌드한다. my_module.rs 내부에서 pub fn으로 함수를 선언했으므로 main.rs(상위 모듈)에서 my_module::say_hello() 형태로 접근 가능하다. 만약 my_module 내부에 또 다른 파일을 만들어 하위 모듈을 관리하고 싶다면 my_module.rs 파일 안에 mod sub_module;라는 구문을 추가하고, my_module 디렉터리 아래에 sub_module.rs를 배치하면 된다.

파일 이름과 디렉터리 구조는 프로젝트의 크기와 복잡도에 따라 달라지며, 필요한 경우 Rust Edition 규칙에 맞게 자유롭게 구성할 수 있다. 다만, 모듈 선언(mod)과 파일 이름, 디렉터리 구조가 일관성을 가져야 한다는 점이 가장 중요하다. 이는 러스트 컴파일러가 모듈을 찾는 유일한 기준이기 때문이다. 불필요하게 pub로 전부 공개하는 대신, 모듈 파일 구조를 체계적으로 설정하고 함수와 타입의 공개 범위를 정확히 제어하면, 긴 시간에 걸쳐 유지보수하는 대규모 프로젝트에서 큰 이점을 얻게 된다.

러스트는 모듈을 다루는 구문 자체는 매우 간단하지만, 프로젝트가 커질 때 발생하는 모듈 간 참조 관계와 재귀적 의존성, 가시성(public/private) 제어 이슈가 현실적 과제로 부각된다. 따라서 시작 단계부터 모듈 파일 구조를 명확히 이해하고, 기능 단위로 깔끔하게 나누어 배치하는 습관을 들이는 것이 좋다. 이렇게 하면 추후 코드가 확장되더라도 구조가 자연스럽게 확립되어 유지보수가 쉬워진다.

Last updated