[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"으로 표현하는 방식입니다. 이 패턴은 소유권과 참조 규칙을 동시에 만족시켜, 불필요한 복사를 줄입니다.

손으로 따라하기

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

마무리

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

💬 댓글

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