[Rust 시리즈 8편] 구조체와 메서드로 데이터 묶기

English version

문자열과 슬라이스로 데이터를 안전하게 빌릴 수 있게 되었으니, 이제 여러 값을 하나의 타입으로 묶을 차례입니다. Rust의 struct는 연관된 데이터를 의미 있게 이름 붙여 묶는 도구이고, [[impl|impl 블록]]은 그 데이터에 대한 행동을 정의합니다. 이번 글에서는 학습 진행도를 추적하는 작은 구조체를 만들고, 불변·가변 참조 규칙이 메서드에도 똑같이 적용된다는 것을 확인합니다.

구조체 정의하기

struct StudyLog {
    topic: String,
    completed: bool,
    notes: Vec<String>,
}

fn main() {
    let mut log = StudyLog {
        topic: String::from("borrowing"),
        completed: false,
        notes: vec![String::from("불변 참조 복습")],
    };

    println!("{} 진행 중", log.topic);
    log.notes.push(String::from("가변 참조 예제 작성"));
    println!("노트 개수: {}", log.notes.len());
}

구조체 필드는 기본적으로 공개되지 않습니다. 이 예제는 같은 모듈 안에서만 사용하므로 그대로 접근할 수 있지만, 보통은 메서드를 통해 값을 읽고 수정하도록 설계합니다.

impl 블록과 메서드

메서드를 정의할 때는 첫 번째 인자로 self, &self, &mut self 중 하나를 선택합니다. 각 선택은 ownership과 borrowing 규칙을 그대로 반영합니다.

  • &self: 빌려서 읽기만 함
  • &mut self: 빌려서 수정함
  • self: 값을 완전히 가져감
impl StudyLog {
    fn new(topic: &str) -> Self {
        Self {
            topic: topic.to_string(),
            completed: false,
            notes: Vec::new(),
        }
    }

    fn add_note(&mut self, memo: &str) {
        self.notes.push(memo.to_string());
    }

    fn summary(&self) -> String {
        format!("{} / 완료 여부: {} / 메모 {}건",
            self.topic,
            self.completed,
            self.notes.len(),
        )
    }

    fn finish(self) -> StudyLog {
        StudyLog {
            completed: true,
            ..self
        }
    }
}

fn main() {
    let mut log = StudyLog::new("strings");
    log.add_note("String vs &str 구분");
    log.add_note("슬라이스 안전 범위");
    println!("{}", log.summary());

    let finished = log.finish();
    // println!("{}", log.summary()); // 컴파일 오류: log는 finish로 이동됨
    println!("완료 상태: {}", finished.summary());
}
  • new는 소유권을 가진 StudyLog를 반환합니다.
  • add_note는 가변 참조로 호출되므로, 동시에 다른 곳에서 불변 참조를 사용할 수 없습니다.
  • summary는 불변 참조만 필요하므로 &self를 사용합니다.
  • finishself를 값으로 받기 때문에 ownership을 가져오고, 메서드가 끝나면 기존 인스턴스를 더 이상 쓸 수 없습니다.

메서드 안에서 슬라이스와 참조 재사용하기

문자열과 슬라이스 개념을 구조체 메서드 안으로 옮겨오면 다음과 같이 동작합니다.

impl StudyLog {
    fn latest_note(&self) -> Option<&str> {
        self.notes.last().map(|note| note.as_str())
    }
}

fn main() {
    let mut log = StudyLog::new("structs");
    log.add_note("필드 초기화 연습");
    log.add_note("impl 블록 만들기");

    if let Some(text) = log.latest_note() {
        println!("최근 메모: {}", text);
    }
}

Option<&str>는 구조체가 문자열을 소유한 채, 외부에는 슬라이스 참조만 빌려줍니다. 즉, 메모를 복사해서 새 문자열을 만드는 대신 "있으면 빌려주고, 없으면 None"으로 표현하는 방식입니다. 이 패턴은 소유권과 참조 규칙을 동시에 만족시켜, 불필요한 복사를 줄입니다.

CodeSandbox로 이어서 실습하기

아래 샌드박스는 CodeSandbox의 Rust starter입니다. 이번 글의 핵심 코드를 src/main.rs에 옮기고, cargo checkcargo run 결과를 나란히 보면서 컴파일 메시지와 실행 출력을 비교해 보세요.

Live Practice

Rust Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Rust startercargoterminal
  1. starter를 fork한 뒤 src/main.rs를 연다
  2. 본문 예제를 붙여 넣고 cargo check와 cargo run을 차례로 실행한다
  3. 타입, 값, 참조 흐름을 바꿔 컴파일 피드백과 출력 차이를 비교한다

Rust 실습은 브라우저 미리보기보다 터미널 피드백이 더 중요합니다. 여러 파일 구조나 추가 crate가 필요한 예제는 파일 배치를 조금 더 손봐야 할 수 있습니다.

손으로 따라하기

  • completed 상태를 토글하는 mark_done(&mut self) 메서드를 구현하고, 불변 참조로 상태를 읽는 함수와 동시에 사용하면 어떤 경고가 나오는지 확인해 보세요.
  • 학습 시간을 분 단위로 저장하는 minutes 필드를 추가하고, impl 블록에서 평균 학습 시간을 계산하는 메서드를 작성해 보세요.
  • 구조체를 다른 함수에 넘길 때 self, &self, &mut self 중 어떤 선택이 가장 안전하고 명확한지 직접 비교해 보세요.

마무리

구조체와 메서드는 Rust 코드에 맥락을 부여하는 기본 단위입니다. 필드마다 ownership을 명확히 선언하고, 메서드는 참조 규칙을 그대로 따르기 때문에 코드를 읽을 때 "어디서 데이터를 빌렸는가"가 한눈에 보입니다. 다음 글에서는 enum과 패턴 매칭으로 상태를 더 풍부하게 표현하며, Option과 같은 표준 enum을 구조체 안에서 어떻게 활용하는지 살펴보겠습니다.

💬 댓글

이 글에 대한 의견을 남겨주세요