icon

안동민 개발노트

6장 : 커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기

패키지와 크레이트


거대한 프로그램을 작성할 때는 코드의 구조화가 무척 중요해집니다. 코드에서 연관된 기능을 묶고 서로 다른 기능을 분리해 두면 이후 특정 기능을 구현하는 코드를 찾거나 변경할 때 헤매지 않게 됩니다.

앞서 작성한 프로그램들은 하나의 모듈, 하나의 파일로 이루어져 있었지만, 프로젝트 규모가 커지면 코드를 여러 모듈, 여러 파일로 나누어 관리해야 합니다. 한 패키지에는 여러 개의 바이너리 크레이트와(원할 경우) 라이브러리 크레이트가 포함될 수 있으므로, 커진 프로젝트의 각 부분을 크레이트로 나눠서 외부 라이브러리처럼 쓸 수 있습니다. 이번 장에서 배워 볼 것은 이러한 기법들입니다. 상호연관된 패키지들로 이루어진 대규모 프로젝트의 경우에는 13장 ‘Cargo 작업공간’ 절에서 다룰 예정인, Cargo에서 제공하는 작업공간(workspace) 기능을 이용합니다.

또한 세부 구현을 캡슐화하면 더 고수준에서 코드를 재사용할 수 있는 방법에 대해서도 설명합니다. 일단 어떤 연산을 구현하면 그 구현체의 작동 방식을 몰라도 다른 코드에서 공개 인터페이스를 통해 해당 코드를 호출할 수 있습니다. 코드를 작성하는 방식에 따라 다른 코드가 사용할 수 있는 공개 부분과 변경 권한을 작성자에게 남겨두는 비공개 구현 세부 사항이 정의됩니다. 이는 머릿속에 기억해 둬야 하는 세부 사항의 양을 제한하는 또 다른 방법입니다.

스코프 개념도 관련되어 있습니다. 중첩된 컨텍스트에 작성한 코드는 ‘스코프 내에’ 정의된 다양한 이름들이 사용됩니다. 프로그래머나 컴파일러가 코드를 읽고, 쓰고, 컴파일할 때는 특정 위치의 특정 이름이 무엇을 의미하는지 알아야 합니다. 해당 이름이 변수인지, 함수인지, 열거형인지, 모듈인지, 상수인지, 그 외 아이템인지 말이죠. 스코프를 생성하고 스코프 안 혹은 바깥에 있는 이름을 변경할 수 있습니다. 동일한 스코프 내에는 같은 이름을 가진 아이템이 둘 이상 있을 수 없으며, 이름 충돌을 해결하는 도구를 사용할 수 있습니다.

러스트에는 코드 조직화에 필요한 기능이 여럿 있습니다. 어떤 세부 정보를 외부에 노출할지, 비공개로 둘지, 프로그램의 스코프 내 어떤 이름이 있는지 등 다양합니다. 이를 통틀어 모듈 시스템이라 하며, 다음 기능들이 포함됩니다.

  • 패키지: 크레이트를 빌드하고, 테스트하고, 공유하는 데 사용하는 Cargo 기능입니다.
  • 크레이트: 라이브러리나 실행 가능한 모듈로 구성된 트리 구조입니다.
  • 모듈use: 구조, 스코프를 제어하고, 조직 세부 경로를 감추는 데 사용합니다.
  • 경로: 구조체, 함수, 모듈 등의 이름을 지정합니다.

이번 장에서는 이 기능들을 모두 다뤄보면서 이 기능들이 상호작용하는 방식을 논의하고 이를 사용하여 스코프를 관리하는 방법을 설명하겠습니다. 이번 장을 마치고 나면, 모듈 시스템을 확실히 이해하고 프로처럼 스코프를 다룰 수 있을 거랍니다!

이제 모듈과 크레이트 구조를 실제 코드로 정리해 보겠습니다.

여기서 다룰 모듈 시스템의 첫 부분은 패키지와 크레이트입니다.

크레이트(crate) 는 러스트가 컴파일 한 차례에 고려하는 가장 작은 코드 단위입니다. (1장의 ‘러스트 프로그램 작성하고 실행하기’절에서 했던 것처럼) cargo 대신 rustc를 실행하여 단일 소스 코드 파일을 넘겨주더라도, 컴파일러는 그 파일이 크레이트라고 생각합니다. 크레이트는 여러 모듈을 담을 수 있고, 다음 절에서 곧 알게 되겠지만 모듈은 이 크레이트와 함께 컴파일되는 다른 파일들에 정의되어 있을 수도 있습니다.

크레이트는 바이너리일 수도 있고, 라이브러리일 수도 있습니다. 바이너리 크레이트(binary crate) 는 커맨드 라인 프로그램이나 서버처럼 실행 가능한 실행파일로 컴파일할 수 있는 프로그램입니다. 바이너리 크레이트는 실행파일이 실행되면 무슨 일이 일어나는지를 정의한 main 함수를 포함하고 있어야 합니다. 여태껏 만들어 본 모든 크레이트는 바이너리 크레이트였습니다.

라이브러리 크레이트(library crate)main 함수를 가지고 있지 않고 실행파일 형태로 컴파일되지 않습니다. 그 대신, 여러 프로젝트에서 공용될 의도로 만들어진 기능들이 정의되어 있습니다. 예를 들어, 1장에서 사용한 rand 크레이트는 난수를 생성하는 기능을 제공합니다. 러스타시안들이 ‘크레이트’라고 말하면 대부분은 이 라이브러리 크레이트를 의미하는 것이고, ‘크레이트’라는 단어는 일반적인 프로그래밍 개념에서의 ‘라이브러리’와 혼용됩니다.

크레이트 루트(crate root) 는 러스트 컴파일러가 컴파일을 시작하는 소스 파일이고, 크레이트의 루트 모듈을 구성합니다. (모듈은 ‘모듈을 정의하여 스코프 및 공개 여부 제어하기’에서 알아볼 예정입니다.)

패키지(package) 는 일련의 기능을 제공하는 하나 이상의 크레이트로 구성된 번들입니다. 패키지에는 이 크레이트들을 빌드하는 법이 설명된 Cargo.toml 파일이 포함되어 있습니다. Cargo는 실제로 코드를 빌드하는 데 사용하는 커맨드 라인 도구의 바이너리 크레이트가 포함된 패키지입니다. Cargo 패키지에는 또한 이 바이너리 크레이트가 의존하고 있는 라이브러리 패키지도 포함되어 있습니다. 다른 프로젝트도 Cargo의 라이브러리 크레이트에 의존하여 Cargo의 커맨드 라인 도구가 사용하는 것과 동일한 로직을 사용할 수 있습니다.

패키지에는 여러 개의 바이너리 크레이트가 원하는 만큼 포함될 수 있지만, 라이브러리 크레이트는 하나만 넣을 수 있습니다. 패키지에는 적어도 하나 이상의 크레이트가 포함되어야 하며, 이는 라이브러리든 바이너리든 상관없습니다.

패키지를 생성할 때 어떤 일이 일어나는지 살펴봅시다. 먼저 cargo new 명령어를 입력합니다.

$ cargo new my-project
     Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs

cargo new를 실행한 후 ls 명령을 사용하여 Cargo가 만든 것들을 살펴봅니다. 프로젝트 디렉터리에는 Cargo.toml 파일이 있는데, 이것이 패키지를 만들어 줍니다. main.rs 파일을 가지고 있는 src라는 디렉터리도 있습니다. Cargo.toml을 텍스트 편집기로 열어보면 src/main.rs 가 따로 적시되진 않음을 알 수 있습니다. Cargo는 패키지명과 같은 이름의 바이너리 크레이트는 src/main.rs가 크레이트 루트라는 관례를 준수합니다. 마찬가지로, 패키지 디렉터리에 src/lib.rs 파일이 존재할 경우, Cargo는 해당 패키지가 패키지명과 같은 이름의 라이브러리 크레이트를 포함하고 있다고 판단합니다. 그리고 그 라이브러리 크레이트의 크레이트 루트는 src/lib.rs고요. Cargo는 라이브러리 혹은 바이너리를 빌드할 때 이 크레이트 루트 파일을 rustc에게 전달합니다.

현재 패키지는 src/main.rs 만 포함하고 있으므로 이 패키지는 my-project라는 이름의 바이너리 크레이트만으로 구성되어 있습니다. 만약 어떤 패키지가 src/main.rssrc/lib.rs를 가지고 있다면 해당 패키지는 패키지와 같은 이름의 바이너리, 라이브러리 크레이트를 포함하게 됩니다. src/bin 디렉터리 내에 파일을 배치하면 각각의 파일이 바이너리 크레이트가 되어, 여러 바이너리 크레이트를 패키지에 포함할 수 있습니다.