메서드 문법

메서드 문법 (Method Syntax)

메서드(method)는 함수와 비슷해요. fn 키워드와 이름으로 선언하고, 매개변수와 반환 값을 가질 수 있으며, 다른 곳에서 호출했을 때 실행되는 코드를 담고 있죠. 하지만 함수와 달리 메서드는 구조체(또는 각각 6장과 18장에서 다룰 열거형·트레이트 객체)의 문맥 안에서 정의돼요. 그리고 메서드의 첫 번째 매개변수는 항상 self인데, 이는 메서드가 호출되는 구조체의 인스턴스를 나타내요.

출처: The Rust Book

메서드 문법

Rectangle 인스턴스를 매개변수로 받는 area 함수를, Rectangle 구조체에 정의된 area 메서드로 바꿔 볼게요. Listing 5-13을 보세요.

Filename: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

Listing 5-13: Rectangle 구조체에 area 메서드 정의하기

함수를 Rectangle의 문맥 안에 정의하려면 Rectangle에 대한 impl(구현) 블록을 시작해요. 이 impl 블록 안의 모든 것은 Rectangle 타입과 연관돼요. 그런 다음 area 함수를 impl 중괄호 안으로 옮기고, 시그니처와 본문 전체에서 첫 번째(그리고 이 경우 유일한) 매개변수를 self로 바꿔요. main에서 area 함수를 호출하며 rect1을 인자로 넘겼던 자리에는, 이제 *메서드 문법(method syntax)*을 사용해 Rectangle 인스턴스에 area 메서드를 호출할 수 있어요. 메서드 문법은 인스턴스 뒤에 옵니다. 점을 붙이고 메서드 이름, 괄호, 그리고 인자들을 차례로 쓰면 돼요.

area의 시그니처에서는 rectangle: &Rectangle 대신 &self를 사용해요. &self는 사실 self: &Self의 축약형이에요. impl 블록 안에서 Self 타입은 그 impl 블록이 대상으로 하는 타입의 별칭이에요. 메서드는 첫 번째 매개변수로 타입 Self에 해당하는 self라는 이름의 매개변수를 가져야 해서, Rust는 첫 번째 매개변수 자리에 self라는 이름만 쓰는 축약을 허용해요. rectangle: &Rectangle에서 그랬던 것처럼 이 축약형 self 앞에 &를 붙여서 메서드가 Self 인스턴스를 대여한다는 것을 나타낼 필요가 여전히 있다는 점을 기억하세요. 메서드는 다른 매개변수에서처럼 self의 소유권을 가져가거나, 여기서 한 것처럼 self를 불변으로 대여하거나, self를 가변으로 대여할 수 있어요.

함수 버전에서 &Rectangle을 사용한 것과 같은 이유로 여기서 &self를 선택했어요. 소유권을 가져가고 싶지 않고, 구조체의 데이터를 쓰기보다는 읽기만 하면 되니까요. 메서드의 일부로서 메서드를 호출한 인스턴스를 바꾸고 싶다면 첫 번째 매개변수로 &mut self를 쓰면 돼요. 첫 번째 매개변수로 그냥 self만 써서 인스턴스의 소유권을 가져가는 메서드는 드물어요. 이 기법은 보통 메서드가 self를 다른 무언가로 변환하고, 변환 후에는 호출자가 원래 인스턴스를 사용하지 못하게 하려고 할 때 쓰여요.

메서드 문법을 제공하고 모든 메서드 시그니처에서 self의 타입을 반복하지 않아도 된다는 점 외에, 함수 대신 메서드를 사용하는 주된 이유는 정리(organization)에 있어요. 타입의 인스턴스로 할 수 있는 모든 것을 하나의 impl 블록에 모아 두면, 앞으로 우리 코드를 쓰는 사람들이 우리가 제공하는 라이브러리의 여러 곳에서 Rectangle의 기능을 찾아 헤맬 필요가 없어져요.

메서드에 구조체 필드 중 하나와 같은 이름을 줄 수도 있다는 점에 주목하세요. 예를 들어 Rectanglewidth라는 이름의 메서드를 정의할 수 있어요.

Filename: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    if rect1.width() {
        println!("The rectangle has a nonzero width; it is {}", rect1.width);
    }
}

여기서는 인스턴스의 width 필드 값이 0보다 크면 width 메서드가 true를, 값이 0이면 false를 반환하도록 정했어요. 같은 이름의 메서드 안에서 필드를 어떤 목적으로든 사용할 수 있죠. main에서 rect1.width 뒤에 괄호를 붙이면 Rust는 width 메서드를 뜻하는 것으로 알고, 괄호를 붙이지 않으면 width 필드를 뜻하는 것으로 알아요.

필드와 같은 이름의 메서드를 만들 때 흔히, 그리고 항상 그런 건 아니지만, 필드의 값을 반환할 뿐 다른 일은 하지 않기를 바라요. 이런 메서드를 *게터(getter)*라고 하는데, Rust는 다른 일부 언어처럼 구조체 필드에 대해 게터를 자동으로 구현하지 않아요. 게터는 필드를 private으로 만들고 메서드를 public으로 만들어, 타입의 공개 API 일부로서 그 필드에 대한 읽기 전용 접근을 허용한다는 점에서 유용해요. public과 private이 무엇이고 필드나 메서드를 어떻게 public 또는 private으로 지정하는지는 7장에서 다룰게요.

-> 연산자는 어디에 있나요?

C와 C++에서는 메서드를 호출할 때 두 개의 서로 다른 연산자를 사용해요. 객체에 직접 메서드를 호출할 때는 .을, 객체에 대한 포인터로 메서드를 호출해서 먼저 포인터를 역참조해야 할 때는 ->를 사용하죠. 다시 말해 object가 포인터라면 object->something()(*object).something()과 비슷해요.

Rust에는 -> 연산자에 해당하는 것이 없어요. 대신 Rust에는 *자동 참조 및 역참조(automatic referencing and dereferencing)*라는 기능이 있어요. 메서드 호출은 Rust에서 이런 동작이 일어나는 몇 안 되는 곳 중 하나예요.

작동 방식은 이래요. object.something()으로 메서드를 호출하면, Rust가 object가 메서드의 시그니처와 일치하도록 &, &mut, 또는 *을 자동으로 추가해요. 다시 말해 다음 두 코드는 같아요.

#![allow(unused)]
fn main() {
#[derive(Debug,Copy,Clone)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
   fn distance(&self, other: &Point) -> f64 {
       let x_squared = f64::powi(other.x - self.x, 2);
       let y_squared = f64::powi(other.y - self.y, 2);

       f64::sqrt(x_squared + y_squared)
   }
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
}

첫 번째가 훨씬 깔끔해 보이죠. 이런 자동 참조 동작은 메서드에 명확한 수신자(receiver), 즉 self의 타입이 있기 때문에 가능해요. 수신자와 메서드 이름이 주어지면 Rust는 메서드가 읽기(&self), 변경(&mut self), 소비(self) 중 어느 것인지를 확정적으로 알아낼 수 있어요. Rust가 메서드 수신자에 대해 대여를 암시적으로 만든다는 사실은 실제로 소유권을 사용하기 편하게 만드는 큰 부분이에요.

매개변수가 더 많은 메서드

Rectangle 구조체에 두 번째 메서드를 구현하면서 메서드 사용을 연습해 볼게요. 이번에는 Rectangle의 인스턴스가 다른 Rectangle 인스턴스를 받아, 그 두 번째 Rectangleself(첫 번째 Rectangle) 안에 완전히 들어갈 수 있으면 true를, 그렇지 않으면 false를 반환하게 만들 거예요. 즉 can_hold 메서드를 정의하고 나면, Listing 5-14에 나온 프로그램을 쓸 수 있게 되기를 원해요.

Filename: src/main.rs

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Listing 5-14: 아직 작성하지 않은 can_hold 메서드 사용하기

rect2의 두 변이 모두 rect1의 변보다 작지만 rect3rect1보다 더 넓으므로, 예상 출력은 다음과 같아요.

Can rect1 hold rect2? true
Can rect1 hold rect3? false

메서드를 정의하고 싶으므로 impl Rectangle 블록 안에 들어갈 거예요. 메서드 이름은 can_hold이고, 매개변수로 다른 Rectangle의 불변 대여를 받을 거예요. 매개변수의 타입이 무엇인지는 메서드를 호출하는 코드를 보면 알 수 있어요. rect1.can_hold(&rect2)&rect2를 넘겨주는데, 이는 Rectangle 인스턴스인 rect2에 대한 불변 대여예요. rect2는 읽기만 하면 되고(쓰려면 가변 대여가 필요하겠죠), can_hold 메서드를 호출한 뒤에도 rect2를 다시 사용할 수 있도록 mainrect2의 소유권을 유지하기를 원하니 이는 합리적이에요. can_hold의 반환 값은 Boolean이고, 구현은 self의 가로·세로가 각각 다른 Rectangle의 가로·세로보다 큰지 검사할 거예요. Listing 5-13의 impl 블록에 새 can_hold 메서드를 추가해 볼게요. Listing 5-15를 보세요.

Filename: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Listing 5-15: 다른 Rectangle 인스턴스를 매개변수로 받는 can_hold 메서드를 Rectangle에 구현하기

Listing 5-14의 main 함수와 함께 이 코드를 실행하면 원하는 출력을 얻을 수 있어요. 메서드는 self 매개변수 뒤의 시그니처에 추가할 매개변수를 여러 개 가질 수 있고, 그 매개변수들은 함수의 매개변수와 똑같이 동작해요.

연관 함수(Associated Functions)

impl 블록 안에 정의된 모든 함수는 *연관 함수(associated function)*라고 불러요. impl 뒤에 나오는 타입과 연관되어 있기 때문이죠. 첫 번째 매개변수로 self를 갖지 않는 연관 함수도 정의할 수 있는데, 그런 함수들은 동작에 타입의 인스턴스가 필요 없어서 메서드가 아니에요. 우리는 이미 이런 함수를 하나 사용했어요. String 타입에 정의된 String::from 함수가 바로 그것이에요.

메서드가 아닌 연관 함수는 구조체의 새 인스턴스를 반환하는 생성자(constructor)로 자주 쓰여요. 이런 함수는 흔히 new라고 부르는데, new는 특별한 이름도 아니고 언어에 내장되어 있지도 않아요. 예를 들어 square라는 연관 함수를 제공할 수도 있어요. 이 함수는 한 변의 길이인 하나의 차원 매개변수를 받아 그 값을 가로와 세로 양쪽에 사용해서, 같은 값을 두 번 지정하지 않아도 정사각형 Rectangle을 쉽게 만들게 해줘요.

Filename: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let sq = Rectangle::square(3);
}

반환 타입과 함수 본문에 있는 Self 키워드는 impl 키워드 뒤에 나오는 타입의 별칭이에요. 이 경우엔 Rectangle이죠.

이 연관 함수를 호출하려면 구조체 이름과 함께 :: 문법을 사용해요. let sq = Rectangle::square(3);이 그 예시예요. 이 함수는 구조체에 의해 네임스페이스가 나뉘어요. :: 문법은 연관 함수와 모듈이 만드는 네임스페이스 양쪽에 사용돼요. 모듈은 7장에서 다룰게요.

여러 개의 impl 블록

각 구조체는 여러 개의 impl 블록을 가질 수 있어요. 예를 들어 Listing 5-15는 각 메서드가 자기만의 impl 블록에 있는 Listing 5-16의 코드와 동등해요.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Listing 5-16: 여러 impl 블록을 사용해 Listing 5-15 다시 작성하기

여기서 이 메서드들을 여러 impl 블록으로 나눌 이유는 없지만, 유효한 문법이에요. 여러 impl 블록이 유용한 경우는 제네릭 타입과 트레이트를 다루는 10장에서 볼 수 있을 거예요.

요약

구조체는 여러분의 도메인에 의미 있는 커스텀 타입을 만들 수 있게 해줘요. 구조체를 사용하면 관련된 데이터 조각들을 서로 연결해 두고 각 조각에 이름을 붙여 코드를 명확하게 만들 수 있어요. impl 블록 안에서는 타입과 연관된 함수를 정의할 수 있고, 메서드는 구조체의 인스턴스가 갖는 동작을 지정할 수 있게 해주는 일종의 연관 함수예요.

하지만 구조체가 커스텀 타입을 만드는 유일한 방법은 아니에요. Rust의 열거형(enum) 기능으로 넘어가서 여러분의 도구 상자에 도구를 하나 더 추가해 볼게요.

더 알아보기 (Learn more)