프로그래밍/RUST 공부

2. 러스트 서버, 서비스, 앱 만들기: Trait 이해하기

리블스톤 2025. 5. 2. 19:00

 

Intro

러스트에서의 트레이트(Trait)를 공부해보자.

 

조사를 먼저 해 봤다. 러스트 공식 문서에서나 다른 검색결과에서나 러스트의 트레이트는 다음과 닽이 정의됐다.

 

 

정의

 

트레이트(trait)는 특정한 타입이 가지고 있으면서 다른 타입과 공유할 수 있는 기능을 정의합니다. 트레이틀르 사용하면 공통된 기능을 추상적으로 정의할 수 있습니다. 트레이트 바운드(trait bound)를 이용하면 어떤 제네릭 타입 자리에 특정한 동작을 갖춘 타입이 올 수 있음을 명시할 수 있습니다.

Note: 약간의 차이는 있으나, 트레이트는 다른 언어에서 흔이 인터페이스(interface)라고 부르는 기능과 유사합니다.

 

설명만 보면, 파이썬(python)의 클래스 상속과 비슷해 보인다.

 

비단 파이썬 뿐만 아니라, OOP(Object Oriented Programming)을 지향하는 코드에서는, 어떤 추상적인 객체를 만든 뒤 이를 다른 객체가 상속하는 방식으로 함수를 공유한다.

 

예시를 하나 들어보자면,

class  Animal:
    name = "animal"
    
    def __init__(self, food):
        self.food = food

    def eat(self):
        print(f"{self.name} ate {self.food}")    
        self.food = None

 

이라는 파이썬 클래스를 하나 만들어 보았다. 이 클래스의 eat() 이라는 메소드를 다른 클래스에서 쓰고 싶다면? 단순하게 생각하면 다른 클래스에서도 eat이라는 메소드를 만들면 되겠지만, 그건 너무 비효율적이다. 그래서, 메소드를 상속(inheritance) 받는다.

 

Cat이라는 클래스와 Dog라는 클래스를 만들자.

 

class Cat(Animal):
    name = "Cat"

    def __init__(self, food):
        self.food = food

class Dog(Animal):
    name = "Dog"
    
    def __init__(self, food):
        self.food = food

 

또는 super() 메소드를 이용해서,

class  Animal:
    name = "animal"
    
    def __init__(self, food):
        self.food = food

    def eat(self):
        print(f"{self.name} ate {self.food}")    
        self.food = None
        
class Cat(Animal):
    name = "Cat"
    
    def __init__(self, food):
        super().__init__(food)

class Dog(Animal):
    name = "Dog"
    
    def __init__(self, food):
        super().__init__(food)

 

이제 Dog 클래스를 호출해 뼈다귀를 쥐어주자. 다음과 같은 결과를 볼 수 있다.

dog = Dog("bone")
print(dog.food)

'bone'

 

또한, Cat 클래스와 Dog 클래스에서 정의하지 않았던 eat메서드도 정상 작동하는 걸 볼 수 있다.

 

dog.eat()
print(dog.food)

result:

Dog ate bone
None

 

의문

 

그러면, 다시 본론으로 돌아가서, 러스트의 트레이트 역시 이런 상속 가능한 객체라고 이해해도 되는 걸까?

 

책의 러스트 코드를 일부 발췌해 보자.

#[derive(Debug, PartialEq)]
pub enum Method {
    Get,
    Post,
    Uninitialized,
}

 

impl From<&str> for Method {
    fn from(s: str) -> Method {
    	match s {
            "GET" => Method::Get,
            "POST" => Method::Post,
            _ 	=> Method::Uninitialized,
        }
    }
}

 

상단은 Method라는 열거형을 정의한다. 하단의 impl 구문이 내가 이해를 하기 위해 작성중인 부분이다.

 

아직 러스트 생초보인 나는 impl 이라는 구문이 구조체의 메소드를 정의하기 위한 문법이라는 것 까지만 알고 있다.

그런데, 문법을 확인해 보면, 두 번째 코드는 Method에 대한 impl 구문이 아니라, From이라는 새로운 구조체를 이야기하는 것 같다.

 

본래 기초적인 러스트 struct - impl 문법은 이렇다.

 

struct Animal {
    name: String;
    type: String;
}

impl Animal {
    fn new(name: String, type: String) -> Animal {
    	Animal{	name: name, type: type}
    }
}

 

저렇게 된다면, 우리는 다음과 같은 함수를 호출할 수 있게 된다.

 

fn main() {
    let p = Animal::new(String::from("Dog"), String::from("Spinal"));
    println!("{}, {}", p.name, p,type);
}

 

그럼, 다시 돌아가서, 저 문법은 뭘까? 명백하게 Method 안에서 정의되는 메소드는 아니다. From이라는 트레이트의 메서드를 구현한 부분이다.

 

이래저래 삽질하며 찾아본 결과, 사실 이 부분은 퍼블릭 트레이트라고, 해당 링크에 자세한 설명과 문법이 나와 있더라...

 

일반적인 방식인지는 모르겠지만, 이 부분은 UFCS(Uniform Function Call Syntax)라는 개념을 참조했다나 뭐라나...

UFCS에 대해서는 다음에 다시 공부해 보도록 하고, 해당 구문의 의미를 다시 살펴보자.

 

실제 러스트 문법에서는 저렇게 작동하진 않는다. 다만 컴파일 단계에서 컴파일러가 이 문법을 인식하고 다음과 같이 변환한다고 한다.

 

// 우리가 쓰는 형태
let m = Method::from("GET");

// 컴파일러가 보는 진짜 호출
let m = <Method as From<&str>>::from("GET");

 

...어째 점점 모르는 게 많아지는 기분이다.

 

결론

 

어쨌든 결론은 이렇다. From이라는 퍼블릭 트레이트를 "From<&str> for Method" 라는 방식으로 작성해, Method 타입을 변경할 수 있는 구조로 작성했다.

 

따라서, 우리는 Method 트레잇과, "From <&str> for Method"라는 퍼블릭 트레잇을 사용해, From 트레잇의 타입 변환 함수를 Method::from이라는 Method의 내부 메서드처럼 사용할 수가 있다는 점이다.

 

사실, 이 과정에서 From이라는 트레잇이 하는 역할을 정확히 이해하진 못했다. 코드의 구성으로만 보아선, From이란 퍼블릭 트레잇은 위 파이썬 구조의 추상화 객체(Animal)을 의미하는 것 같다. 내부 인스턴스로 from 이라는 메소드가 구현되어 있는데, Method가 이를 상속해서 구현할 수 있도록 도와주는 게 아닐까 정도로 이해하고 넘어가려 한다.