Go to Rust: #4. 구조체, 열거형, 패턴 매칭
이 글은 Claude Opus 4.5 을 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.
이 섹션에서는 Rust에서 데이터를 구조화하는 방법을 학습합니다. Go 개발자에게 구조체는 익숙하겠지만, Rust의 열거형(enum)은 Go의 const/iota보다 훨씬 강력하며, 패턴 매칭은 switch문을 완전히 새로운 수준으로 끌어올립니다.
4.1 구조체 (Struct)#
구조체 정의와 인스턴스 생성#
// 구조체 정의
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
// 인스턴스 생성
fn main() {
let user = User {
username: String::from("alice"),
email: String::from("alice@example.com"),
sign_in_count: 1,
active: true,
};
// 필드 접근
println!("Username: {}", user.username);
}
Go struct와의 문법 비교:
// Go
type User struct {
Username string
Email string
SignInCount int64
Active bool
}
func main() {
user := User{
Username: "alice",
Email: "alice@example.com",
SignInCount: 1,
Active: true,
}
}
// Rust
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user = User {
username: String::from("alice"),
email: String::from("alice@example.com"),
sign_in_count: 1,
active: true,
};
}
주요 차이점:
- Go: 대문자로 시작하면 public
- Rust:
pub키워드로 명시 - Go:
,로 필드 구분 (마지막 필드 후 선택) - Rust:
,로 필드 구분 (마지막 필드 후 권장)
필드 초기화 축약 문법#
변수 이름이 필드 이름과 같으면 축약 가능:
fn build_user(email: String, username: String) -> User {
User {
email, // email: email 대신
username, // username: username 대신
active: true,
sign_in_count: 1,
}
}
Go에서도 비슷한 축약이 가능합니다 (embedding):
// Go: 임베딩과는 다른 개념
user := User{
Username: username, // 축약 없음
Email: email,
}
구조체 업데이트 문법: .. 스프레드#
다른 인스턴스의 값을 기반으로 새 인스턴스 생성:
let user1 = User {
username: String::from("alice"),
email: String::from("alice@example.com"),
sign_in_count: 1,
active: true,
};
// user1의 나머지 필드 사용
let user2 = User {
email: String::from("bob@example.com"),
..user1 // 나머지 필드는 user1에서
};
// 주의: user1.username은 이동됨!
// println!("{}", user1.username); // 에러!
println!("{}", user1.sign_in_count); // OK, Copy 타입
Go에서는 명시적 복사가 필요합니다:
// Go
user2 := User{
Email: "bob@example.com",
Username: user1.Username, // 명시적
Active: user1.Active,
SignInCount: user1.SignInCount,
}
튜플 구조체#
이름 없는 필드를 가진 구조체:
struct Point(i32, i32, i32);
struct Color(u8, u8, u8);
fn main() {
let origin = Point(0, 0, 0);
let black = Color(0, 0, 0);
// 인덱스로 접근
println!("x: {}", origin.0);
// 구조 분해
let Point(x, y, z) = origin;
// 타입이 다르므로 혼용 불가
// let p: Point = black; // 에러!
}
Go에는 직접적인 대응이 없습니다. 타입 별칭과 비슷하지만 더 강력합니다:
// Go: 타입 별칭은 같은 타입
type Point [3]int
type Color [3]int
// 구분되지 않음...
var p Point = Color{0, 0, 0} // 가능
유닛 구조체#
필드가 없는 구조체:
struct Marker;
fn main() {
let m = Marker;
// 트레이트 구현에 유용
}
가시성과 pub 키워드#
// lib.rs
pub struct User {
pub username: String, // public 필드
email: String, // private 필드
pub(crate) internal: String, // 크레이트 내부에서만
}
// 생성자 필요 (private 필드 때문)
impl User {
pub fn new(username: String, email: String) -> Self {
User {
username,
email,
internal: String::new(),
}
}
}
Go와 비교:
// Go: 대문자 = public, 소문자 = private
type User struct {
Username string // public
email string // private
}
4.2 메서드와 연관 함수#
impl 블록#
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// 메서드 (self 받음)
fn area(&self) -> u32 {
self.width * self.height
}
// 가변 메서드
fn double(&mut self) {
self.width *= 2;
self.height *= 2;
}
// 소유권 가져가는 메서드
fn destroy(self) -> (u32, u32) {
(self.width, self.height)
}
}
fn main() {
let mut rect = Rectangle { width: 30, height: 50 };
println!("Area: {}", rect.area());
rect.double();
println!("Area after double: {}", rect.area());
let (w, h) = rect.destroy();
// rect은 더 이상 사용 불가
}
self, &self, &mut self#
| 시그니처 | 의미 | 사용 시점 |
|---|---|---|
self |
소유권 가져감 | 인스턴스 변환/소비 |
&self |
불변 빌림 | 읽기 전용 |
&mut self |
가변 빌림 | 수정 필요 |
Go 리시버와 비교:
// Go: 값 리시버 vs 포인터 리시버
func (r Rectangle) Area() int {
return r.Width * r.Height
}
func (r *Rectangle) Double() {
r.Width *= 2
r.Height *= 2
}
// Rust
impl Rectangle {
fn area(&self) -> u32 { // Go의 값 리시버와 유사
self.width * self.height
}
fn double(&mut self) { // Go의 포인터 리시버와 유사
self.width *= 2;
self.height *= 2;
}
}
연관 함수 (Associated Functions)#
self를 받지 않는 함수. Go의 생성자 패턴과 유사:
impl Rectangle {
// 연관 함수 (생성자)
fn new(width: u32, height: u32) -> Self {
Rectangle { width, height }
}
fn square(size: u32) -> Self {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
// :: 로 호출
let rect = Rectangle::new(30, 50);
let square = Rectangle::square(10);
}
Go에서의 생성자:
// Go: 관례적인 New 함수
func NewRectangle(width, height int) *Rectangle {
return &Rectangle{Width: width, Height: height}
}
여러 impl 블록#
같은 타입에 여러 impl 블록 가능:
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn perimeter(&self) -> u32 {
2 * (self.width + self.height)
}
}
// 트레이트 구현도 별도 블록
impl std::fmt::Display for Rectangle {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}x{}", self.width, self.height)
}
}
4.3 열거형 (Enum)#
기본 열거형#
enum Direction {
North,
South,
East,
West,
}
fn main() {
let dir = Direction::North;
match dir {
Direction::North => println!("Going north"),
Direction::South => println!("Going south"),
Direction::East => println!("Going east"),
Direction::West => println!("Going west"),
}
}
Go의 const + iota vs Rust enum#
// Go: const와 iota
type Direction int
const (
North Direction = iota
South
East
West
)
// 타입 안전성 약함
var d Direction = 100 // 유효하지 않은 값이지만 컴파일됨
// Rust: 열거형
enum Direction {
North,
South,
East,
West,
}
// 타입 안전
// let d: Direction = 100; // 컴파일 에러!
데이터를 담는 열거형#
Go에 없는 매우 강력한 기능!
enum Message {
Quit, // 데이터 없음
Move { x: i32, y: i32 }, // 익명 구조체
Write(String), // 단일 값
ChangeColor(u8, u8, u8), // 튜플
}
fn main() {
let msg1 = Message::Quit;
let msg2 = Message::Move { x: 10, y: 20 };
let msg3 = Message::Write(String::from("hello"));
let msg4 = Message::ChangeColor(255, 0, 0);
process_message(msg2);
}
fn process_message(msg: Message) {
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Write: {}", text),
Message::ChangeColor(r, g, b) => {
println!("Color: rgb({}, {}, {})", r, g, b)
}
}
}
Go에서 비슷한 패턴을 구현하려면:
// Go: 인터페이스와 구조체 조합 필요
type Message interface {
isMessage()
}
type Quit struct{}
func (Quit) isMessage() {}
type Move struct {
X, Y int
}
func (Move) isMessage() {}
type Write struct {
Text string
}
func (Write) isMessage() {}
// 타입 스위치로 처리
func processMessage(msg Message) {
switch m := msg.(type) {
case Quit:
fmt.Println("Quit")
case Move:
fmt.Printf("Move to (%d, %d)\n", m.X, m.Y)
case Write:
fmt.Println("Write:", m.Text)
}
}
Option: null 없는 세상#
Rust에는 null이 없습니다. 대신 Option<T>를 사용합니다:
enum Option<T> {
Some(T), // 값이 있음
None, // 값이 없음
}
fn find_user(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("Alice"))
} else {
None
}
}
fn main() {
let user = find_user(1);
// Option은 반드시 처리해야 함
match user {
Some(name) => println!("Found: {}", name),
None => println!("Not found"),
}
// 또는 if let
if let Some(name) = find_user(2) {
println!("Found: {}", name);
} else {
println!("Not found");
}
// 메서드 활용
let name = find_user(1).unwrap_or(String::from("Unknown"));
}
Go의 nil과 비교:
// Go: nil 검사 필요
func findUser(id int) *User {
if id == 1 {
return &User{Name: "Alice"}
}
return nil
}
func main() {
user := findUser(1)
if user != nil { // nil 검사
fmt.Println(user.Name)
}
// nil 검사 없이 접근하면 패닉
// user := findUser(2)
// fmt.Println(user.Name) // 런타임 패닉!
}
// Rust: 컴파일러가 강제
fn find_user(id: u32) -> Option<User> {
if id == 1 {
Some(User { name: String::from("Alice") })
} else {
None
}
}
fn main() {
let user = find_user(1);
// 컴파일 에러! Option에 직접 접근 불가
// println!("{}", user.name);
// 반드시 처리
if let Some(u) = user {
println!("{}", u.name);
}
}
Result<T, E>: 에러 처리의 새로운 패러다임#
enum Result<T, E> {
Ok(T), // 성공
Err(E), // 실패
}
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("division by zero"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10, 2) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
// ? 연산자로 전파
fn calculate() -> Result<i32, String> {
let a = divide(10, 2)?; // 에러면 조기 반환
let b = divide(a, 3)?;
Ok(b)
}
}
Go의 (value, error) 반환과 비교:
// Go
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}
| 측면 | Go | Rust |
|---|---|---|
| 에러 무시 | 가능 (result, _ := ...) |
컴파일 경고/에러 |
| 타입 | (T, error) 관례 |
Result<T, E> 타입 |
| 전파 | if err != nil { return } |
? 연산자 |
| 강제성 | 관례에 의존 | 컴파일러가 강제 |
4.4 패턴 매칭#
match 표현식#
fn main() {
let number = 13;
match number {
1 => println!("One"),
2 | 3 | 5 | 7 | 11 | 13 => println!("Prime"),
13..=19 => println!("Teen"), // 범위
_ => println!("Other"), // 기본
}
}
Go switch와의 비교#
Exhaustive 검사 (모든 경우 처리 필수):
// Go: 기본값 없어도 컴파일됨
type Direction int
const (
North Direction = iota
South
East
West
)
func describe(d Direction) string {
switch d {
case North:
return "north"
case South:
return "south"
// East, West 누락해도 컴파일됨
default:
return "unknown"
}
}
// Rust: 모든 경우 처리 필수
enum Direction { North, South, East, West }
fn describe(d: Direction) -> &'static str {
match d {
Direction::North => "north",
Direction::South => "south",
// 컴파일 에러! East, West 미처리
}
}
// 올바른 버전
fn describe(d: Direction) -> &'static str {
match d {
Direction::North => "north",
Direction::South => "south",
Direction::East => "east",
Direction::West => "west",
}
}
표현식으로 사용:
let description = match direction {
Direction::North => "going up",
Direction::South => "going down",
_ => "going sideways",
};
if let: 단일 패턴 매칭#
let some_value = Some(42);
// match
match some_value {
Some(x) => println!("Got: {}", x),
None => (),
}
// if let (더 간결)
if let Some(x) = some_value {
println!("Got: {}", x);
}
// else와 결합
if let Some(x) = some_value {
println!("Got: {}", x);
} else {
println!("Got nothing");
}
while let: 반복 패턴 매칭#
let mut stack = vec![1, 2, 3];
// pop()이 Some을 반환하는 동안 반복
while let Some(top) = stack.pop() {
println!("{}", top); // 3, 2, 1
}
패턴 문법 총정리#
리터럴 매칭:
match x {
1 => println!("one"),
2 => println!("two"),
_ => println!("other"),
}
변수 바인딩:
match x {
n => println!("got {}", n), // n에 바인딩
}
다중 패턴:
match x {
1 | 2 | 3 => println!("one, two, or three"),
_ => println!("other"),
}
범위:
match x {
1..=5 => println!("1 to 5"),
_ => println!("other"),
}
match c {
'a'..='z' => println!("lowercase"),
'A'..='Z' => println!("uppercase"),
_ => println!("other"),
}
구조 분해:
struct Point { x: i32, y: i32 }
let p = Point { x: 0, y: 7 };
match p {
Point { x: 0, y } => println!("on y-axis at {}", y),
Point { x, y: 0 } => println!("on x-axis at {}", x),
Point { x, y } => println!("at ({}, {})", x, y),
}
// 열거형 구조 분해
enum Message {
Move { x: i32, y: i32 },
Write(String),
}
match msg {
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Write: {}", text),
}
가드 조건:
match num {
x if x < 0 => println!("negative"),
x if x > 0 => println!("positive"),
_ => println!("zero"),
}
@ 바인딩:
match age {
n @ 0..=12 => println!("child aged {}", n),
n @ 13..=19 => println!("teen aged {}", n),
n => println!("adult aged {}", n),
}
_ 와일드카드:
match tuple {
(_, y, _) => println!("y = {}", y), // x, z 무시
}
.. 나머지 무시:
struct Point3D { x: i32, y: i32, z: i32 }
match point {
Point3D { y, .. } => println!("y = {}", y), // x, z 무시
}
match numbers {
[first, .., last] => println!("{} to {}", first, last),
_ => (),
}
4.5 실전 패턴: 타입으로 상태 표현하기#
상태 머신 패턴#
// 상태를 타입으로 표현
enum ConnectionState {
Disconnected,
Connecting { address: String },
Connected { socket: TcpSocket },
Error { message: String },
}
struct Connection {
state: ConnectionState,
}
impl Connection {
fn new() -> Self {
Connection {
state: ConnectionState::Disconnected,
}
}
fn connect(&mut self, address: &str) {
match &self.state {
ConnectionState::Disconnected => {
self.state = ConnectionState::Connecting {
address: address.to_string(),
};
}
_ => println!("Already connecting or connected"),
}
}
fn handle_event(&mut self, event: Event) {
self.state = match std::mem::take(&mut self.state) {
ConnectionState::Connecting { address } => {
match event {
Event::Connected(socket) => {
ConnectionState::Connected { socket }
}
Event::Error(msg) => {
ConnectionState::Error { message: msg }
}
_ => ConnectionState::Connecting { address },
}
}
other => other,
};
}
}
Go interface vs Rust enum: 선택 기준#
Rust enum을 선택해야 할 때:
- 가능한 변형이 고정되어 있고 알려져 있음
- 모든 변형을 한 곳에서 처리해야 함
- 데이터 타입이 변형마다 다름
Go interface (Rust trait 객체)를 선택해야 할 때:
- 확장 가능해야 함 (새 타입 추가)
- 다른 패키지/크레이트에서 구현 가능해야 함
- 동적 디스패치가 필요함
// enum: 닫힌 타입, 모든 변형 알려짐
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle { base, height } => base * height / 2.0,
}
}
// trait: 열린 타입, 확장 가능
trait Shape {
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
// 새 타입 추가 가능
struct Hexagon { side: f64 }
impl Shape for Hexagon {
fn area(&self) -> f64 {
// ...
}
}
newtype 패턴: 타입 안전성 강화#
// 단순 타입 별칭 (약한 타입 안전성)
type Meters = f64;
type Seconds = f64;
fn speed(distance: Meters, time: Seconds) -> f64 {
distance / time
}
let d: Meters = 100.0;
let t: Seconds = 10.0;
speed(t, d); // 컴파일됨! 잘못된 순서
// newtype 패턴 (강한 타입 안전성)
struct Meters(f64);
struct Seconds(f64);
fn speed(distance: Meters, time: Seconds) -> f64 {
distance.0 / time.0
}
let d = Meters(100.0);
let t = Seconds(10.0);
// speed(t, d); // 컴파일 에러!
speed(d, t); // OK
// 실전 예: ID 타입
struct UserId(u64);
struct OrderId(u64);
fn get_user(id: UserId) -> User { /* ... */ }
fn get_order(id: OrderId) -> Order { /* ... */ }
let user_id = UserId(42);
let order_id = OrderId(42);
get_user(user_id); // OK
// get_user(order_id); // 컴파일 에러! 타입 불일치
4.6 요약#
구조체#
| 측면 | Go | Rust |
|---|---|---|
| 정의 | type Name struct {} |
struct Name {} |
| 가시성 | 대문자 = public | pub 키워드 |
| 메서드 | func (r *T) Method() |
impl T { fn method(&self) } |
| 생성자 | NewT() 관례 |
T::new() 연관 함수 |
| 업데이트 | 명시적 복사 | ..other 스프레드 |
열거형#
| 측면 | Go | Rust |
|---|---|---|
| 정의 | const + iota |
enum Name {} |
| 데이터 포함 | 불가 | 가능 |
| 타입 안전성 | 약함 | 강함 |
| null 처리 | nil |
Option<T> |
| 에러 처리 | (T, error) |
Result<T, E> |
패턴 매칭#
| 측면 | Go switch | Rust match |
|---|---|---|
| 완전성 검사 | 없음 | 있음 (필수) |
| 표현식 | 아니오 | 예 |
| 구조 분해 | 제한적 | 풍부함 |
| 가드 조건 | 없음 | 있음 |
연습 문제#
-
구조체:
Book구조체를 정의하고new,description메서드를 구현하세요. -
열거형: 웹 요청 결과를 나타내는
Response열거형을 정의하세요:Success { data: String, status: u16 }Error { message: String, status: u16 }Loading
-
패턴 매칭: 위
Response열거형을 처리하는 함수를 작성하세요. -
Option 활용: 정수 벡터에서 첫 번째 짝수를 찾는 함수를 작성하세요.
fn first_even(numbers: &[i32]) -> Option<i32> -
newtype 패턴:
Email타입을 정의하고, 유효성 검사를 포함한 생성자를 만드세요. -
상태 머신: 주문 상태(
Pending,Paid,Shipped,Delivered,Cancelled)를 열거형으로 모델링하고, 상태 전이 메서드를 구현하세요.