All posts

Rust 초급 10편: 모듈과 외부 크레이트로 코드 정리하기

main.rs에 데이터 정의와 출력 로직을 전부 적다 보면 파일이 길어집니다. 이번에는 학습 기록 코드를 별도 모듈로 옮기고, 외부 크레이트인 serde_json으로 JSON을 출력합니다. 중간에는 여러 타입이 같은 동작을 제공하도록 정하는 trait도 맛봅니다.

작은 작업실 모듈들을 연결하는 게로 모듈과 크레이트를 표현한 일러스트

main.rs에 데이터 정의와 출력 로직을 전부 적다 보면 파일이 길어집니다. 이번에는 학습 기록 코드를 별도 모듈로 옮기고, 외부 크레이트인 serde_json으로 JSON을 출력합니다. 중간에는 여러 타입이 같은 동작을 제공하도록 정하는 trait도 맛봅니다.

준비와 목표

9편까지의 구조체·String·메서드를 알고 있으면 됩니다. 이번 편은 파일 두 개를 작성합니다. Cargo가 사용하는 패키지, 크레이트, 모듈의 관계를 구분하는 것이 목표입니다.

cargo new study_modules
cd study_modules
cargo add serde_json

마지막 명령은 외부 의존성을 Cargo.toml에 등록합니다. 네트워크 접근이 필요할 수 있습니다. 이후 실제 빌드에서 선택된 정확한 버전은 Cargo.lock에 기록되므로 이 파일을 임의로 편집하지 마세요. cargo add 공식 안내

모듈 파일 만들기

src/records.rs를 새로 만들고 아래 내용을 저장합니다. 이 블록은 main.rs에 붙여 넣는 코드가 아닙니다.

pub struct Record {
    pub title: String,
    pub minutes: u32,
}

pub trait Summary {
    fn summary(&self) -> String;
}

impl Record {
    pub fn new(title: String, minutes: u32) -> Self {
        Self { title, minutes }
    }
}

impl Summary for Record {
    fn summary(&self) -> String {
        format!("{}: {}분", self.title, self.minutes)
    }
}

pub는 외부에서 접근할 수 있게 공개한다는 뜻입니다. 모듈을 나누면 선언된 항목의 가시성을 생각해야 합니다. 여기서는 예제를 읽기 쉽게 하려고 타입, 필드, 생성 함수를 공개했습니다. 실제 프로젝트에서는 필요 없는 필드까지 공개할 이유는 없습니다.

main에서 사용하기

src/main.rs는 다음 전체 코드로 바꿉니다.

mod records;

use records::{Record, Summary};

fn main() {
    let record = Record::new(String::from("모듈 학습"), 45);
    println!("{}", record.summary());

    let json = serde_json::json!({
        "title": record.title,
        "minutes": record.minutes
    });
    println!("제목: {}", json["title"].as_str().unwrap_or("제목 없음"));
    println!("시간: {}", json["minutes"]);
}
cargo run
모듈 학습: 45분
제목: 모듈 학습
시간: 45

mod records;는 모듈을 선언합니다. 이 위치에서는 Cargo와 Rust 컴파일러가 src/records.rs를 찾아 연결합니다. use는 긴 경로를 매번 쓰지 않도록 이름을 현재 범위에 가져옵니다. 파일을 만드는 것만으로 모듈이 자동 연결되는 것은 아닙니다. 모듈 공식 설명

trait은 공통 동작의 약속입니다

trait Summary에는 summary(&self) -> String이라는 메서드 형태를 적었습니다. impl Summary for Record는 Record가 그 동작을 어떻게 제공하는지 구현합니다. main의 use에 Summary도 포함한 이유는 trait의 메서드를 사용하기 위해서입니다.

이런 의미에서 trait을 인터페이스나 동작의 계약에 비유할 수 있습니다. 하지만 summary라는 이름이 자동으로 "정확한 요약"을 보장하지는 않습니다. 컴파일러가 확인하는 것은 주로 타입과 요구한 구현 형태입니다. 업무 의미는 코드와 테스트로 검증해야 합니다. 트레이트 공식 설명

이번 예제는 Summary 구현이 한 개뿐입니다. 이후 다른 종류의 기록에도 같은 trait을 구현하면 summary를 제공한다는 공통점을 표현할 수 있습니다. 제네릭과 trait bound를 깊게 다루는 것은 초급 연재 이후의 주제로 남깁니다.

외부 크레이트 사용하기

serdejson::json!은 JSON 값을 만드는 매크로입니다. asstr()은 그 값이 문자열이면 Some(&str), 아니면 None을 반환하므로 기본 문구를 지정했습니다. 8편의 Option 처리와 이어지는 부분입니다. serde_json 공식 API 문서

패키지는 Cargo.toml로 관리하는 프로젝트 단위입니다. 크레이트는 Rust 컴파일 단위이고, 모듈은 크레이트 내부의 이름과 코드를 조직합니다. 이번 패키지에는 main.rs를 루트로 하는 실행 크레이트가 있고 그 안에 records 모듈이 있습니다. serde_json은 의존성으로 가져온 외부 크레이트입니다.

자주 만나는 오류

file not found for module 오류라면 records.rs가 프로젝트 루트가 아닌 src 안에 있는지 확인하세요. records.rs를 record.rs로 저장했다면 mod records와 이름이 맞지 않습니다.

비공개 항목 접근 오류가 나면 외부에서 실제로 필요한 타입·필드·메서드에 pub가 있는지 확인합니다. serde_json을 찾을 수 없다면 cargo add를 해당 프로젝트 폴더에서 실행했는지 Cargo.toml의 dependencies를 확인하세요.

메서드를 찾지 못한다는 오류라면 main의 use records::{Record, Summary};에서 Summary를 빠뜨렸는지도 살펴보세요. trait 구현과 trait을 범위에 가져오는 일은 구분됩니다.

연습과 해설

JSON에 completed: true 항목을 추가하고 json["completed"]를 출력해 보세요. 출력 마지막에 true가 추가되면 됩니다.

해설: json! 객체에 "completed": true를 넣고 println!("완료: {}", json["completed"]);를 추가합니다. Rust 코드에서 true와 "true"는 각각 불리언과 문자열이며 JSON에서도 다른 타입입니다.

다음 11편에서는 반환값을 테스트로 확인하고, 경계값 오류를 고치는 과정을 실습합니다.

실행 확인

이 글의 핵심 실습은 Rust 1.99.1, 2024 Edition의 Linux 환경에서 확인했습니다. 의도적으로 실패하는 예제는 설명한 오류가 발생하는지 구분해 검사했습니다. 운영체제별 오류 문구는 다를 수 있습니다.

Rust 초급 시리즈 전체 목차

  1. 설치와 Cargo로 첫 프로그램 만들기
  2. 변수·자료형·문자열로 학습 기록 만들기
  3. 함수·조건문·반복문으로 학습 기록 계산하기
  4. String으로 이해하는 소유권과 이동
  5. 참조와 빌림으로 문자열 읽고 수정하기
  6. 구조체와 메서드로 학습 할 일 만들기
  7. 열거형과 패턴 매칭으로 학습 상태 표현하기
  8. Option·Result로 빈 값과 오류 처리하기
  9. 벡터·해시맵·문자열로 학습 기록 모으기
  10. 모듈과 외부 크레이트로 코드 정리하기
  11. 테스트와 디버깅으로 목표 계산 검증하기
  12. 파일을 검색하는 CLI 프로젝트 완성하기

시리즈 모아보기

Discussion

No comments yet