Go to Rust: #5. 트레이트와 제네릭
이 글은 Claude Opus 4.5 을 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.
트레이트(Trait)는 Rust의 다형성 메커니즘입니다. Go의 인터페이스와 비슷한 역할을 하지만, 구현 방식과 기능에서 큰 차이가 있습니다. 이 섹션에서는 트레이트와 제네릭을 Go와 비교하며 깊이 있게 학습합니다.
5.1 트레이트 기초#
트레이트란?#
트레이트는 타입이 구현해야 하는 메서드 집합을 정의합니다:
// 트레이트 정의
trait Summary {
fn summarize(&self) -> String;
}
// 타입 정의
struct Article {
title: String,
author: String,
content: String,
}
// 트레이트 구현
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} by {}", self.title, self.author)
}
}
Go interface vs Rust trait#
가장 큰 차이: 암묵적 vs 명시적
// Go: 암묵적 구현 (덕 타이핑)
type Summary interface {
Summarize() string
}
type Article struct {
Title string
Author string
Content string
}
// interface를 언급하지 않아도 자동으로 구현됨
func (a Article) Summarize() string {
return a.Title + " by " + a.Author
}
// Article은 자동으로 Summary를 구현
var s Summary = Article{} // OK
// Rust: 명시적 구현
trait Summary {
fn summarize(&self) -> String;
}
struct Article {
title: String,
author: String,
content: String,
}
// 반드시 impl Trait for Type으로 구현 명시
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} by {}", self.title, self.author)
}
}
// Summary 없이는 사용 불가
fn print_summary(item: &impl Summary) {
println!("{}", item.summarize());
}
비교:
| 측면 | Go interface | Rust trait |
|---|---|---|
| 구현 방식 | 암묵적 | 명시적 |
| 발견 시점 | 런타임 | 컴파일타임 |
| 확장성 | 어디서든 구현 | 고아 규칙 적용 |
| 디스패치 | 항상 동적 | 정적/동적 선택 |
| 메서드 기본 구현 | 불가 | 가능 |
기본 구현 (Default Implementation)#
Go에는 없는 기능:
trait Summary {
fn summarize_author(&self) -> String;
// 기본 구현 제공
fn summarize(&self) -> String {
format!("Read more from {}...", self.summarize_author())
}
}
struct Article {
author: String,
content: String,
}
impl Summary for Article {
// summarize_author만 구현하면 됨
fn summarize_author(&self) -> String {
self.author.clone()
}
// summarize()는 기본 구현 사용
}
let article = Article {
author: String::from("Alice"),
content: String::from("..."),
};
println!("{}", article.summarize()); // "Read more from Alice..."
고아 규칙 (Orphan Rule)#
외부 타입에 외부 트레이트를 구현할 수 없습니다:
// 우리 크레이트에서...
// OK: 우리 트레이트를 외부 타입에 구현
trait MyTrait {}
impl MyTrait for Vec<i32> {}
// OK: 외부 트레이트를 우리 타입에 구현
struct MyType;
impl std::fmt::Display for MyType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "MyType")
}
}
// 에러: 외부 트레이트를 외부 타입에 구현
// impl std::fmt::Display for Vec<i32> {} // 불가!
해결책: newtype 패턴
// wrapper 타입 생성
struct Wrapper(Vec<String>);
impl std::fmt::Display for Wrapper {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
5.2 표준 라이브러리의 주요 트레이트#
Debug와 Display#
use std::fmt;
struct Point {
x: i32,
y: i32,
}
// Debug: {:?} 포맷, 개발자용
impl fmt::Debug for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y)
}
}
// Display: {} 포맷, 사용자용
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("{:?}", p); // Point { x: 1, y: 2 }
println!("{}", p); // (1, 2)
}
// derive로 자동 구현 (Debug만)
#[derive(Debug)]
struct Point2 { x: i32, y: i32 }
Go 비교:
// Go: String() 메서드가 fmt.Stringer 구현
type Point struct { X, Y int }
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
Clone과 Copy#
// Clone: 명시적 깊은 복사
#[derive(Clone)]
struct Article {
title: String,
content: String,
}
let a1 = Article { title: String::from("Hi"), content: String::from("...") };
let a2 = a1.clone(); // 깊은 복사
// Copy: 암묵적 복사 (스택 데이터만)
#[derive(Copy, Clone)] // Copy는 Clone 필요
struct Point {
x: i32,
y: i32,
}
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // 복사 (p1 여전히 유효)
Copy가 될 수 있는 조건:
- 모든 필드가 Copy 타입
- Drop 구현이 없음
PartialEq, Eq, PartialOrd, Ord#
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Person {
age: u32,
name: String,
}
let alice = Person { age: 30, name: String::from("Alice") };
let bob = Person { age: 25, name: String::from("Bob") };
// PartialEq: == 연산자
println!("{}", alice == bob); // false
// Ord: 비교 연산자와 정렬
println!("{}", alice > bob); // true (age로 먼저 비교)
let mut people = vec![alice, bob];
people.sort(); // Ord 필요
Partial vs Full:
PartialEq: 모든 값이 비교 가능하지 않을 수 있음 (예:f64::NAN)Eq: 모든 값이 자기 자신과 같음PartialOrd: 일부 값은 비교 불가 (예:f64::NAN)Ord: 모든 값이 비교 가능 (전순서)
Default#
#[derive(Default)]
struct Config {
debug: bool, // false
timeout: u32, // 0
name: String, // ""
}
let config = Config::default();
// 커스텀 기본값
struct ServerConfig {
port: u16,
max_connections: u32,
}
impl Default for ServerConfig {
fn default() -> Self {
ServerConfig {
port: 8080,
max_connections: 100,
}
}
}
// 일부만 지정하고 나머지는 기본값
let config = ServerConfig {
port: 3000,
..Default::default()
};
From과 Into#
// From 구현
struct Wrapper(String);
impl From<String> for Wrapper {
fn from(s: String) -> Self {
Wrapper(s)
}
}
impl From<&str> for Wrapper {
fn from(s: &str) -> Self {
Wrapper(s.to_string())
}
}
// From을 구현하면 Into는 자동
let w1: Wrapper = String::from("hello").into();
let w2: Wrapper = "hello".into();
let w3 = Wrapper::from("hello");
Iterator#
struct Counter {
count: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Self {
Counter { count: 0, max }
}
}
impl Iterator for Counter {
type Item = u32; // 연관 타입
fn next(&mut self) -> Option<Self::Item> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
fn main() {
let counter = Counter::new(5);
for n in counter {
println!("{}", n); // 1, 2, 3, 4, 5
}
}
Drop#
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping: {}", self.data);
}
}
fn main() {
let a = CustomSmartPointer { data: String::from("A") };
let b = CustomSmartPointer { data: String::from("B") };
println!("Created pointers");
}
// 출력:
// Created pointers
// Dropping: B
// Dropping: A
5.3 제네릭#
제네릭 함수#
// 제네릭 함수
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
let numbers = vec![34, 50, 25, 100, 65];
let result = largest(&numbers);
let chars = vec!['y', 'm', 'a', 'q'];
let result = largest(&chars);
제네릭 구조체#
struct Point<T> {
x: T,
y: T,
}
// 다른 타입의 x, y
struct Point2<T, U> {
x: T,
y: U,
}
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
let mixed_point = Point2 { x: 5, y: 4.0 };
제네릭 메서드#
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
// 특정 타입에만 구현
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
let p = Point { x: 3.0, y: 4.0 };
println!("Distance: {}", p.distance_from_origin()); // 5.0
let p2 = Point { x: 3, y: 4 };
// p2.distance_from_origin(); // 에러! Point<i32>에는 없음
Go 1.18+ 제네릭과의 비교#
// Go 제네릭 (1.18+)
func Largest[T constraints.Ordered](list []T) T {
largest := list[0]
for _, item := range list {
if item > largest {
largest = item
}
}
return largest
}
type Point[T any] struct {
X, Y T
}
func (p Point[T]) GetX() T {
return p.X
}
// Rust 제네릭
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
차이점:
| 측면 | Go | Rust |
|---|---|---|
| 문법 | [T constraint] |
<T: Bound> |
| 제약 | constraints 패키지 |
트레이트 |
| 복잡한 제약 | 제한적 | where 절로 풍부 |
| 연관 타입 | 없음 | 있음 |
| 구현 | 항상 단형화 | 단형화 또는 동적 |
단형화 (Monomorphization)#
Rust는 제네릭 코드를 컴파일할 때 단형화합니다:
// 소스 코드
fn double<T: std::ops::Mul<Output = T> + Copy>(x: T) -> T {
x * x
}
let a = double(5i32);
let b = double(3.14f64);
컴파일 후:
// 컴파일러가 생성한 코드 (개념적)
fn double_i32(x: i32) -> i32 {
x * x
}
fn double_f64(x: f64) -> f64 {
x * x
}
let a = double_i32(5);
let b = double_f64(3.14);
장점: 런타임 비용 없음 (정적 디스패치) 단점: 바이너리 크기 증가 가능
5.4 트레이트 바운드#
기본 문법#
// 방법 1: 함수 시그니처에 직접
fn print_info<T: Summary>(item: &T) {
println!("{}", item.summarize());
}
// 방법 2: impl Trait (간편 문법)
fn print_info(item: &impl Summary) {
println!("{}", item.summarize());
}
// 방법 3: where 절
fn print_info<T>(item: &T)
where
T: Summary,
{
println!("{}", item.summarize());
}
다중 바운드#
// + 로 여러 트레이트 요구
fn print_and_clone<T: Summary + Clone>(item: &T) {
println!("{}", item.summarize());
let cloned = item.clone();
}
// where 절이 더 읽기 쉬움
fn complex_function<T, U>(t: &T, u: &U) -> i32
where
T: Summary + Clone,
U: Debug + PartialEq,
{
// ...
}
Go의 타입 제약과 비교#
// Go: 인터페이스로 제약
type Number interface {
int | int64 | float64
}
func Sum[T Number](numbers []T) T {
var sum T
for _, n := range numbers {
sum += n
}
return sum
}
// Rust: 트레이트로 제약
use std::ops::Add;
use std::iter::Sum;
fn sum<T>(numbers: &[T]) -> T
where
T: Add<Output = T> + Sum + Copy,
{
numbers.iter().copied().sum()
}
조건부 구현#
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
// 모든 T에 대해 구현
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
// Display + PartialOrd를 구현하는 T에 대해서만
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("Largest: {}", self.x);
} else {
println!("Largest: {}", self.y);
}
}
}
블랭킷 구현:
// 표준 라이브러리의 예
// Display를 구현하는 모든 타입에 ToString 자동 구현
impl<T: Display> ToString for T {
fn to_string(&self) -> String {
format!("{}", self)
}
}
5.5 고급 트레이트 기능#
연관 타입 (Associated Types)#
제네릭 파라미터 대신 트레이트 내부에 타입 정의:
// 연관 타입 사용
trait Iterator {
type Item; // 연관 타입
fn next(&mut self) -> Option<Self::Item>;
}
impl Iterator for Counter {
type Item = u32; // 구현 시 타입 지정
fn next(&mut self) -> Option<Self::Item> {
// ...
}
}
// 제네릭으로 했다면...
trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
// 같은 타입에 여러 구현 가능 (혼란)
impl Iterator<u32> for Counter { /* ... */ }
impl Iterator<i32> for Counter { /* ... */ }
연관 타입 vs 제네릭:
- 연관 타입: 타입당 하나의 구현만 (명확)
- 제네릭: 타입당 여러 구현 가능 (유연하지만 복잡)
트레이트 객체: dyn Trait#
동적 디스패치:
trait Animal {
fn speak(&self);
}
struct Dog;
impl Animal for Dog {
fn speak(&self) { println!("Woof!"); }
}
struct Cat;
impl Animal for Cat {
fn speak(&self) { println!("Meow!"); }
}
fn main() {
// 정적 디스패치 (컴파일 타임에 결정)
fn make_speak<T: Animal>(animal: &T) {
animal.speak();
}
// 동적 디스패치 (런타임에 결정)
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for animal in &animals {
animal.speak(); // vtable을 통한 호출
}
}
Go interface와의 유사성:
// Go: 항상 동적 디스패치
type Animal interface {
Speak()
}
type Dog struct{}
func (Dog) Speak() { fmt.Println("Woof!") }
type Cat struct{}
func (Cat) Speak() { fmt.Println("Meow!") }
func main() {
animals := []Animal{Dog{}, Cat{}}
for _, a := range animals {
a.Speak() // 동적 디스패치
}
}
객체 안전성 (Object Safety)#
모든 트레이트가 dyn Trait로 사용될 수 있는 것은 아닙니다:
// 객체 안전 (dyn 사용 가능)
trait Animal {
fn speak(&self);
}
// 객체 불안전 (dyn 사용 불가)
trait Clone {
fn clone(&self) -> Self; // Self 반환 = 객체 불안전
}
trait Sized {} // Sized 바운드 = 객체 불안전
// 이유: 동적 디스패치 시 반환 타입 크기를 알 수 없음
객체 안전 조건:
- 반환 타입이
Self가 아님 - 제네릭 타입 파라미터가 없음
Self: Sized바운드가 없음
impl Trait#
반환 타입에서:
// 구체적 타입을 숨김
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
// 복잡한 이터레이터 타입 숨김
fn even_numbers() -> impl Iterator<Item = i32> {
(0..).filter(|n| n % 2 == 0)
}
매개변수에서 (impl Trait vs 제네릭):
// 이 둘은 비슷하지만...
fn process(item: &impl Summary) { /* ... */ }
fn process<T: Summary>(item: &T) { /* ... */ }
// 제네릭은 터보피시 가능
process::<Article>(&article);
// impl Trait은 불가
// process::<Article>(&article); // 에러
정적 vs 동적 디스패치 비교#
// 정적 디스패치 (impl Trait / 제네릭)
fn speak_static(animal: &impl Animal) {
animal.speak();
}
// 장점: 인라인 가능, 최적화, 성능
// 단점: 바이너리 크기 증가 (단형화)
// 동적 디스패치 (dyn Trait)
fn speak_dynamic(animal: &dyn Animal) {
animal.speak();
}
// 장점: 바이너리 크기 작음, 유연성
// 단점: vtable 오버헤드, 인라인 불가
5.6 Derive 매크로#
#[derive(…)] 속성#
// 여러 트레이트 자동 구현
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
struct User {
name: String,
age: u32,
}
// 이것은 이것과 동등:
impl Debug for User { /* ... */ }
impl Clone for User { /* ... */ }
impl PartialEq for User { /* ... */ }
// ...
자동 구현 가능한 트레이트들#
| 트레이트 | 설명 | 요구사항 |
|---|---|---|
Debug |
{:?} 포맷 |
모든 필드가 Debug |
Clone |
.clone() |
모든 필드가 Clone |
Copy |
암묵적 복사 | 모든 필드가 Copy, Clone 필요 |
PartialEq |
== 비교 |
모든 필드가 PartialEq |
Eq |
반사적 동등성 | PartialEq 필요 |
PartialOrd |
<, > 비교 |
PartialEq 필요 |
Ord |
전순서 | PartialOrd + Eq 필요 |
Hash |
해시 가능 | 모든 필드가 Hash |
Default |
기본값 | 모든 필드가 Default |
serde와 직렬화/역직렬화#
# Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
name: String,
age: u32,
#[serde(rename = "emailAddress")]
email: String,
#[serde(skip_serializing_if = "Option::is_none")]
nickname: Option<String>,
}
fn main() {
let user = User {
name: String::from("Alice"),
age: 30,
email: String::from("alice@example.com"),
nickname: None,
};
// 직렬화
let json = serde_json::to_string(&user).unwrap();
println!("{}", json);
// {"name":"Alice","age":30,"emailAddress":"alice@example.com"}
// 역직렬화
let json = r#"{"name":"Bob","age":25,"emailAddress":"bob@example.com"}"#;
let user: User = serde_json::from_str(json).unwrap();
println!("{:?}", user);
}
Go의 encoding/json과 비교:
// Go
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"emailAddress"`
Nickname string `json:"nickname,omitempty"`
}
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
json, _ := json.Marshal(user)
5.7 요약#
Go interface vs Rust trait#
| 측면 | Go interface | Rust trait |
|---|---|---|
| 구현 | 암묵적 | 명시적 (impl Trait for Type) |
| 디스패치 | 항상 동적 | 정적 또는 동적 선택 |
| 기본 구현 | 불가 | 가능 |
| 연관 타입 | 불가 | 가능 |
| 제네릭 | 별도 문법 | 트레이트 바운드로 통합 |
| 조건부 구현 | 불가 | 가능 |
언제 무엇을 사용할까#
| 상황 | 선택 |
|---|---|
| 성능이 중요 | 제네릭 + 트레이트 바운드 (정적) |
| 다양한 타입을 컬렉션에 저장 | dyn Trait (동적) |
| 반환 타입 숨기기 | impl Trait |
| 외부 타입에 기능 추가 | newtype + 트레이트 구현 |
연습 문제#
-
트레이트 정의:
Drawable트레이트를 정의하고Circle,Rectangle에 구현하세요. -
제네릭 함수: 두 값 중 큰 값을 반환하는 제네릭 함수
max를 작성하세요. -
연관 타입: 키-값 저장소를 나타내는
Storage트레이트를 연관 타입으로 정의하세요. -
트레이트 객체:
Vec<Box<dyn Drawable>>를 만들고 모든 요소를 그리는 코드를 작성하세요. -
serde 활용: JSON 파일을 읽어 구조체로 역직렬화하고, 수정 후 다시 저장하는 프로그램을 작성하세요.
-
조건부 구현:
Display를 구현하는 타입에 대해서만println!메서드를 제공하는 wrapper 타입을 만드세요.