2026년 2월 2일 월요일

How.about.learning().rust().programming().language()?

이번 시간에는 C/C++를 대체할 목적으로 만들어진, Rust programming language를 소개하는 시간(첫번째 시간)을 가져 보고자 한다. 😎

AI 시대, Python도 좋지만 Rust를 배워 보는 것은 어떨까 ?

목차
let n = vec!["Chapter1", "Chapter2", "Chapter3", "Chapter4", "Chapter5", "Chapter6", "Epilogue", "References"];
let mut ch = n.iter();
ch.next().unwrap() How about learning Rust ?
ch.next().unwrap() 소유권과 Borrowing & Reference에 대한 이해
ch.next().unwrap() Option/Result 사용법
ch.next().unwrap() 다양한 표준 Trait 사용법
ch.next().unwrap() Smart Pointer 사용법
ch.next().unwrap() Threads와 비동기 프로그래밍
ch.next().unwrap() Epilogue
ch.next().unwrap() References


요즘에는 AI에 의존하는 Vibe Coding이라는 용어까지 생기면서 programming language를 전혀 배우지 않은 일반인들도 coding을 할 수 있다고들 말한다. 이는 AI의 발전으로 그만큼 computer programming이 쉬워졌다는 것을 뜻하지만, AI가 아무리 좋은 code를 자동으로 만들어 낸다고 해도, code에 하자가 있는지 없는지를 판단하는 것은 어디까지나 사람(개발자)의 몫이다. 그러니, AI(유발 하라리가 표현한 Alien Intelligence)시대를 맞이하여 "Copy & Paste를 반복하는 단순 coder가 아니라, 생각하고 고민하는 현명한 programmer가 되어야 한다" 라는 말을하고 싶다. 생각하지 않으면, 언젠가 AI에게 통제당하는 세상에서 살게 될런지도 모를 일이다. 💢 


Chapter1. How about learning Rust ?
1.1 Rust 왜 배워야 하나 ?
Rust는 C/C++를 대체한다는 명확한 목적을 가지고 만들어졌다. 그렇다면 Rust 진영에서 의도한 대로, Rust의 출현으로 C/C++ 언어는 정말로 역사속으로 사라지게 되는 것일까 ?

정답은 "결코 그렇지 않을 것이다" 라는 것이다(최소한 당분간은 그럴 것이다). 😂

C는 1972년에서 세상에 나와서 (2026년 현재) 55년 간이나 당당하게 버텨왔고, C++도 1979년(1983에 C++로 공식 명칭 변경)에 세상에 나와서 48년이라는 시간을 견뎌왔다. 특히 C++의 경우는 C++11, C++14, C++17, C++20, C++23 등으로 (3년 단위로) 버젼을 up하면서 현대적인 programming language style로 진화하기까지 하였다. 거의 50년 가까운 시간 동안  C/C++로 구축된 무수히 많은 코드(대표적인 예: OS kernel)를 Rust로 전부 교체하는 것은 사실상 불가능에 가깝다.

물론 최근에 M$가 C/C++로 개발된 자신들의 code(windows kernel, Azure infrastructure 등) 중 일부를 Rust로 전환하여 효과를 보았다는 얘기가 들리며, Google 역시 Android를 구성하는 C/C++ 코드를 Rust로 전환하여 상당한 효과(C/C++ 대비 메모리 안전 문제를 1,000배 이상 줄였다고 함)를 보고 있는 것도 사실인 것 같다. 또한 Linux kernel에는 Rust로 구현한 코드가 6.1 version(2022년 10월)부터 보이기 시작하더니, 급기야는 작년 말(2025년 12월)에 C, Assembly에 이어 Rust를 공식 언어로 사용하기로 결정했다고 하는 걸 보니, 앞서 불가능하다고 했던 말이 다소 무색해지는 느낌도 든다. 😓

그럼, 왜 Rust를 배워야 하는 것일까 ?

1) Rust는 현대적인 programming language 답게, (절차지향 programming은 물론이고) 요즘 유행하는 functional programming 적인 요소를 기본으로 탑재하고 있다(따라서 Python이나 Golang에서 보았던 요소들이 많이 눈에 띈다). 뿐만 아니라 Class는 없으나 Struct와 Trait라는 개념을 통해 Object oriented programming도 흉내(class 상속을 제외한 전 기능 구현 가능)낼 수가 있다.

#1 functional programming: Iterator(반복자)와 Closure(익명함수 혹은 람다식)를 이용하여 a().b().c().d().e() 형태의 programming이 가능하다.
#2 object oriented programming: Struct/Enum, Trait(= Interface 개념), Generic, Module 개념을 잘 접목하여 OOP가 가능하게 하고 있다.

2) 변수 사용 기본 원칙#1 - Rust는 기본적으로 변수를 수정할 수 없는 상태(immutable)에서 출발한다. 만일 수정하고 싶은(mutable) 변수가 있다면 mut라는 속성을 지정하여 선언해 주어야 한다. (살짝 지나친 비유로 생각되지만) 자동차로 비유하자면, 자동차는 시동을 건 후 break 페달을 밟지 않으면, 기본적으로 움직이도록 설계되어져 있다. 이를 C/C++ 등 기존 programming language의 설계 방식으로 비유(변수 선언 즉시 사용/수정 가능)한다면, Rust의 경우는 반대로 설명할 수 있을 듯하다. 즉, 자동차의 시동을 건 상태에서 break 페달을 밟지 않았지만, 자동차는 움직이지 않고 멈춰 서 있는다. 자동차를 움직이게 하려면 명확히 악셀 페달(마치 mut keyword를 지정해 줘야 하는 것 처럼)을 밟아 주어야만 한다. 🚗 Rust에서 이러한 방식을 채택한 이유는, 불필요한 변수 수정(혹은 실수로 수정)으로 인하여 발생할 수 있는 문제를 원천적으로 차단하기 위해서이다.

3) 변수 사용 기본 원칙#2 - Rust는 C/C++처럼 빠른 속도를 제공하면서도, C/C++에서는 제공하지 못하는 메모리 안정성을 보장한다는 커다란 장점이 있다. 소위 말해, Rust는 소유권(Ownership) 개념(이동, 빌림/참조, 수명)이 있기 때문에 수동으로 메모리를 할당/해제하는 C/C++에서 자주 발생하는 메모리 관련 오류(null pointer dereferences, buffer overflows, double-free memory issues, dangling references, data races 등)가 발생하지 않아 메모리 안정성이 확보된다고 말할 수 있다(Rust는 garbage collector를 사용하지 않음). 

소유권 개념: 변수에 대한 소유자는 (어느 시점에서) 오직 하나뿐이며, 해당 변수를 제 3자가 접근해서 사용(수정)하려고 하면, 소유권을 이동시키거나, 소유권을 대여(빌림)해 주어야만 한다.

4) Rust가 Safety(코드의 안전성)를 위해 소유권 개념만 제공하는 것은 아니다. 특정 변수가 가리키는 값의 유무를 표시하기 위해 Option<T>라는 껍데기(Some or None를 담고 있음)를 제공하므로써, NULL pointer 사용 시에 발생하는 문제를 해결하고 있다(Rust에는 NULL pointer 개념이 없다). 또한 거의 대부분의 함수는 (지나치다 싶을 정도로) 함수 수행 후 결과(성공 유무 - Ok or Error)를 Result<T, E> 라는 껍데기(wrapper)에 담아 함수 호출자에게 전달하고, 함수 호출자는 이를 분해하여 성공 및 에러에 대한 처리를 안전하게 진행하므로써, code의 안정성을 높이고 있다. 이런 관점에서 볼 때 (비유적으로) Rust는 단단한 게 껍질로 감싸져 있는 아주 딱딱한 언어라고 말할 수 있을 듯하다. 😋

5) 뿐만 아니라, Smart Pointer라는 (다소 복잡한) 개념을 도입하여, single or multi-thread 환경에서 안전하게 data 공유 및 수정이 가능하도록 유도하고 있다. (물론, unsafe block을 통해 원시 pointer를 사용하는 예외가 있기는 하지만) Rust는 기본적으로 원시 pointer 대신 smart pointer만을 사용하도록 강제하고 있다. 아이러니 하게도, smart pointer의 개념은 C++에서 처음 등장했는데,  Rust는 이를 채용한 후, 앞서 설명한 소유권 개념과 접목시켜 훨씬 더 안전하면서도 강력한 형태로 발전시켰다. 따라서 필자의 개인적인 생각으로는 Smart Pointer라는 용어보다는 Safe Pointer 혹은 Strong Pointer라는 말이 더 맞는 것 같다는 생각이 들기도 한다. 😋

Smart Pointer = Safe Pointer + Strong Pointer

6) Rust는 소유권 개념과 Smart Pointer를 사용하므로써 (어느 programming language 보다도) concurrent programming에 적합한 code를 만들 수 있게 되었다. "fearless concurrency"라는 말이 단순히 마케팅 목적으로 등장한 문구가 아니라, 실제로도 신뢰할만한 수준의 concurrent programming이 가능함을 입증해 주고 있다.

7) Python, Golang 처럼 이해하기 쉽고 programming하기 편한 language가 있는 반면, C++ 처럼 이해하기 매우 어려운 language도 있다. Rust 역시 매우 어려운 language로 분류되는데, 빠르고 높은 안전성을 얻기 위해 이런 저런 제약을 가하다 보니, 다소 난해한 programming language 형태가 될 수 밖에 없었던 것 같다. 하지만, 단순히 어렵게만 느껴진다기 보다는, 알면 알수록 programmer에게 흥미를 유발시키는 매력적인 언어이기도 한 것 같다. 😍

[그림 1.1] Rust vs Python and C/C++ 비교 [출처 - 참고문헌 9]

Rust를 배워야 하는 이유는, C/C++가 50여년간 쌓아온 견고한 성을, Rust가 당장에 무너뜨릴 수는 없지만, 앞으로의 50년간은 (점차적으로) Rust가 그 역할을 대신할 가능성이 매우 높기 때문이다.

1.2 Rust 왜 어렵게 느켜지는 것일까 ?
하지만, Rust는 언어 자체의 난이도가 높은 탓에 정복하기가 쉽지 않은 언어(learning curve가 가파르다고 표현)로 인식되고 있다고 하였다. 그렇다면, Rust는 왜 그렇게 어렵게 느껴지는 것일까 ? 이는 Rust가 memory 안정성을 보장하기 위해, 여타의 programming language하고는 조금은 다른 접근 방법(많은 제약 사항 탑재)을 채택하고 있기 때문이다.

<Rust의 난해한 요인 및 주요 특징>
let i = RefCell::new(0); let mut i = i.borrow_mut();
*i + 1) Trait + Generic + Struct가 함께 표현되어 있을 경우, 코드가 매우 복잡하고 난해함.
   - Class & Interface는 없으나, Struct + Trait가 그 이상을 해 낼 수가 있다.
   - self, Self가 엄청 자주 나온다.
   - 코드를 보다 보면, 이게 programming code인지 암호 코드인지 분간하기가 힘들다. 😂

*i + 2) Rust에서 제공하는 다양한 표준 Trait에 대한 의미 및 사용 방식에 대한 이해 어려움.
   - Drop, Sized, Clone, Copy, PartialEq, Deref, Default, AsRef, Borrow, BorrowMut, From, Into, ToOwned, Cow, Iterator 등..
   - 뭐하러 이렇게 만들었나 싶은 생각이 들 정도로, 초반에 이해하기 쉽지가 않다. 

*i + 3) 소유권(Ownership - 이동/빌림 & 참조), Liefetime 개념
   - 기존 programming language랑은 접근 방식이 좀 남 다르다.
   - 분명 별거 아닌 내용인 듯 하다가도, 복잡한 코드 속에서 헤메게 만든다. 😂

*i + 4) 다양한 Smart Pointer 사용 방법
   - 종류도 많고, Generic 표현 기법이 잘 와닫질 않는다.
   - Box<T>, Rc<T>, Weak<T>, Cell<T>, RefCell<T>, Ref<T>, RefMut<T>, Cow<T>,
     Arc<T>, Mutex<T>, MutexGuard<T> 등..

*i + 5) Option, Result를 제공하는 enum type 
   - 별거 아닌듯 한데, 사용하기에 살짝 불편하다(어렵다는 표현이 맞으려나). 이것 때문에 코드가 장황해 진다는 얘기도 있다. 😂
   - 안에 담겨진 내용을 꺼내서 사용해야 한다.
   - 어떤 함수가 Option이나 Result를 결과로 반환하는지 알아야 programming이 가능하다.

*i + 6) 현란한 Functional programming 기법 
   - a().b().c().d().e() ....
   - iterator & closure => (대부분의 언어에도 있는 개념이지만) 쉬운 듯 어렵다.
   - 적절한 function을 연이어 붙이는 것이 쉽지가 않다. 뿐만아니라, unwrap, unwrap_or_else, ?, expect, as_ref, as_mut 등 함께할 때 꽤나 복잡해진다.
   - reference 사용 방법에도 신경을 써야 한다.

*i + 7) 여타 programming lanaguage에서 제공하는 concept
   - Generic, Collections(Vector, HashMap, LinkedList...), Iterator, Closure, Thread(spawn, channel) ...
   - if/else, loop, while, for, match 문

*i + 8) module(crate라고 함) 개념 제공
   - use x::y::z 형태로 module을 import하여 사용한다. C++ 느낌도 살짝 난다.
   - 근데, crate랑 trait는 전혀 다른 개념이다.

*i + 9) 강력한 concurrency(동시성) programming 기법 제공
   - Golang의 Go routine 보다 안전하단다.
   - memory safe를 위해 접근한 개념이 concurreny programming에도 지대한 영향을 주었다.
   -  tokio crate를 이용한 비동기 프로그래밍(async/await)에도 익숙해져야 한다.

*i + 10) 강력한 macro
   - C/C++의 macro 보다 강력하다.
   - 다양한 attributes(얘도 종류가 많아서 코드 이해를 어렵게 만든다).

*i + 11) 다른 language code 사용 가능(가령 C code 호출)
   - Unsafe { } 사용

*i + 12) Compiler(rustc)와 package manager(cargo)가 강력하다.
   - compile 단계에서 많은 error를 잡아준다. 뿐만아니라 package 관리까지 해준다.
   - 코드량이 많아질 경우, 좀 느릴 수 있다.

*i + 13) test code를 작성하기 편리하다.
__________________________________________________________________

어떤 면에서 볼 때, Rust는 코드 가독성이 그리 좋은 편은 못되는 것 같다하지만, 전반적인 개념을 파악하고 나면, 아주 흥미로운 language라는 점을 깨닫게 된다(정복하고 싶다는 강력한 욕구가 샘솟는다).

1.3 Rust를 master하기 위한 노력
필자는 (10여년 전에) Go언어를 배우기 위해 internet site에서 소개하는 내용을 토대로 1주일만에 Go언어의 대략적인 면모를 파악할 수 있었다. 물론 1주일 만에 Go언어를 이해했다고 하는 것이, 1주일 만에 Go언어 programming을 자유자재로 할 수 있었다는 뜻은 아니다. 다만, 그 만큼 Go 언어(Golang)는 쉬우면서도 현대적인 programming language라는 것이다. 😍

한편, Rust를 이해하기 위해서 러스트 공식 가이드 북(한글판)을 읽다가 1주일만에 포기하고 말았다. 😓 아무래도 Rust 자체의 난이도가 높다는 점이 가장 큰 이유였겠지만, (그때 당시) master해 보려는 의지가 좀 부족했던 것도 같고, 또한 (조금은 난해하게 해석된) 번역서를 통해 Rust를 이해하려고 했던 부분도 한가지 이유(흥미 저하)였던 것 같다(그렇다고, 오해는 말자. 해당 번역서가 문제가 있다는 뜻은 아니다).

필자가 생각하기에는 programming language를 제대로 이해하려면, 먼저 본인의 상태(?)를 정확히 파악하고 그에 맞는 처방(?)을 내려야 한다고 본다. 남들이 일반적으로 말하는 훌륭한 방법(or 인터넷 글 or 너튜브 동영상)이 자신에게는 별로 도움이 되지 않을 수도 있으며, 반대로 남들이 불평을 늘어 놓는 방법(or 인터넷 글 or 너튜브 동영상)이 오히려 자신에게는 맞는 경우도 있기 때문이다. (다시 말하지만) 자신만의 방법을 찾을 필요가 있다는 말이다. 👀

howto_master_rust.iter().find(|&&x| x is mystyle == true)

____________________
Rust 공식 home page에는 rust 관련 book & examples page가 link되어 있다. 따라서 (이게 맞는 분들은) 이 내용을 토대로 rust를 연마할 수 있다.





하지만, 위의 내용이 좀 어려운 분들을 위해, 여기에 초심자들이 읽기에 좋은 몇가지 rust 책을 열거해 본다(이는 어디까지나 필자의 개인적인 의견일 뿐 책 저자나 출판사랑은 전혀 무관하다. 😋).


<기본 편>
Rc::new(1) 이지하지 않은 rust를 easy하게 설명한 책이다. 러스트 공식 가이드 북을 읽다가 포기한 분들께 강추한다. 오른쪽이 원서다(저자는 한국에 사는 캐나다인이다). 👍👍👍



[그림 1.2] Rust 책 추천 #1 - 초급(1)


Rc::new(2) Python과 Rust를 비교하면서 아주 쉽게 Rust를 설명하는게 특징이다. 어렵기만 하다고 다 좋은게 결코 아니다. 분량이 많지 않고, 번역서가 아니니 빠르게 읽어 볼만하다. 👍👍



[그림 1.3] Rust 책 추천 #2 - 초급(2)


Rc::new(3) 역시 Python과 Rust를 비교하면서 아주 쉽게 Rust를 설명하는게 특징이다(일본인 저자가 쓴 책에 대한 번역판). 따라해 보기 좋은 example 들이 아주 많다(초심자에게 자신감을 줄 수 있다). 👍👍 물론 (책을 다 읽고 나면) Rust에 대한 보다 심도있는 설명이 아쉽다고 느낄 수도 있는데, 이 시점에 다른 책을 참조하면 좋을 듯하다.



[그림 1.4] Rust 책 추천 #3 - 초급(3)

Rc::new(4) Rust community에서 만든 공식 가이드 북이다. 공식 가이드 북이라는 이유로, 보통은 이 책부터 보게 되는 것 같다. 기본 개념이 아주 세세하게 설명되어 있긴 하지만, (필자의 생각으로는) 앞서 제시한 책들과 병행해서 보면 좋을 듯하다(일부 내용은 처음 접할 때는 좀 어렵게 느껴질 수도 있다). 한글판도 있고, 번역된 site도 있다. 👍👍👍

[그림 1.5] Rust 책 추천 #4 - 초급(4)


Rc::new(5) Rust의 기본 원리만 깊게 설명하는 책들에 비해 많은 예제를 설명하는 좋은 책이다. 원서이지만, 아주 쉽게 설명되어 있어, 막힘없이 내용이 술술 이해가 된다. 나중에 읽어서 그런지, 현재로써는 이책이 제일 좋은 것 같기도 하다(한가지 아쉬운 부분은 Trait에 대한 깊은 내용이 빠져 있다). 👍👍👍👍

[그림 1.6] Rust 책 추천 #6 - 초급(6)


Rc::new(6) 책을 별도로 구매하는 것이 부담스러운 분들은 아래 site 내용(Just Do Rust - 러스트 기초부터 고급까지)을 참조하는 것도 추천한다. 종이 책보다는 읽기에 편한 측면도 있고, (정식 book으로 출간된 것이 아니라, 오탈자 등이 많이 보이기는 하지만) 꼭 필요한 내용을 이해하기 쉽게 제대로 설명하고 있다. 👍👍


[그림 1.7] Rust 책 추천 #5 - 초급(5)

아래 blog도 추천한다(내용일 아주 실하다).


Rc::new(7) 이 책은 중급 개발자를 위한 내용을 담고 있다. 👍👍👍 처음부터 끝까지 완독하기에는 분량이 상당하고 난해한 내용(원문이 어려운 것인지 이상하게 어렵게 느껴진다)이 많이 있으니, 필요한 부분을 중심으로 그때 그때 읽어 볼 것을 권한다. "나는 수준높은 개발자이므로 기초책은 필요 없고, 이 책부터 읽어야지" 했다가는 낭패를 볼 수도 있다. 😋

 



Rc::new(8) 이 책은 다소 난해하지만 트레이트와 제네릭 등을 이용한 rust의 다양한 design pattern에 대해 소개하고 있다. 👍👍

 
[그림 1.9] Rust 책 추천 #7 - 중급(2)
___________________________
<응용 편>

Arc::new(9) 이 두권의 책은 web backend programming 관점에서 rust를 소개하고 있다. Backend programming에 관심있는 독자에게 추천한다. 첫번째 책(Actix web framework 기반)은 번역서가 존재하며, 두번째 책(Axum web framework 기반)은 한국인 저자가 쓴 책이다. 👍👍


Arc::new(10) (내용 중 절반은 rust 기초를 설명하고 있어 다소 실망했다는 의견도 있으나) linux kernel module을 rust로 작성하고 싶은 kernel 개발자에게 추천할만한 유일한 책이다(원서 중에도 이런 책은 아직 보질 못했다). 👍👍

[그림 1.11] Rust 책 추천 #9 - linux kernel programming 관련


이 밖에도 Rust Asynchronous programming, Effective Rust 등 추천하고 싶은 책이 많이 있지만, 요 정도 선에서 break 하기로 하자. 😋
_______________
정답이 어디 있겠는가 ? 본인이 느끼기에 가장 쉬운 방법(서적 혹은 internet site)을 선택하여, 인내심을 가지고 study해 보는 것이 답일 것이다(위의 책은 어디까지나 필자가 제시한 예시에 불과하다 💣).


<Rust study 방법 - 너무 뻔한 얘긴가 ? 😂>
단계 'a) 책을 한두권 선정하여 빠르게 읽어본다. Rust언어에서 말하고자 하는 전반적인 내용과 scope를 파악하는 것이 중요하다.
-> 이게 생각보다 쉽지 않을 수 있다. 한번으로 어려우면 2번 정도 읽어 보기 바란다.
-> 대개의 경우 이 단계에서 포기해 버리는 경우가 많다. 😓
-> "C/C++로도 살아가는데에는 지장이 없다."고 여겨지면 여기서 break; 😋

단계 'b) 책에서 소개하는 예제를 직접 typing 해보고, 발생하는 에러를 잡아본다.
-> Rust에만 존재하는 독특한 개념도 중요하지만, rust의 일반적인 programming 기법에 익숙해질 필요가 있다.
-> 즉, 변수 처리, String, &str, Vector, for loop, iterator, function, closure, struct, trait, generic, thread, module, file opertations ...
-> 어려운 개념을 많이 알아야 programming을 할 수 있는 것은 아니다. 기본에 충실할 필요가 있다. 💪

단계 'c) 'b 단계까지 진행한 후에는 Rust에서 말하는 독특한 기능(개념)에 좀 더 익숙해지려고 노력해 본다.
-> 소유권(이동, 빌림 & 참조), 수명(lifetime), Option/Result, Smart Pointer, 다양한 Trait, 비동기 programming ...
-> Rust! 보면 볼수록 어렵다. 그래도 Go go~ 😂

단계 'd) 작은 규모라도 좋으니, 의미있는 program을 직접 만들어 본다.
-> 이건 뭐, loop { if you().are().professional().unwrap() == true {break} } 이다. ㅋ 💢

Language에서 제공하는 모든 기능을 다 이해하고 있어야 programming이 가능한 것은 아니다. 중요한 것은 전체적인 개념을 파악(rust의 경우는 인내심을 요구한다)한 후, 직접 coding해 보는 것이다. 그리고, 언어를 master하기 위해서는 누구에게나 충분한 시간이 필요한 법이다. ⏳

(*) Rust가 master하기에 어려운 언어로 정평이 나 있긴 하지만, (마음먹고 덤벼든다면) 2개월 정도면 충분히 정복 가능하다. Modern C++도 master 하려면 비슷한 시간이 걸린다(더 걸릴 수도 있다). 그러니, 안할 이유가 없다. 😎

1.4 Rust programming에 AI 활용하기
처음에는 Rust를 study하면서 자주 등장하는 주제들을 엄선하여 차례로 소개해 볼 생각이었다. 하지만, (이는 앞서 제시한 서적을 참조하는게 맞을 듯하여) 그것보다는 초심자들이 느끼기에 조금은 난해한 몇가지 주제를 정한 후, (좀 아이러니 하지만) AI의 도움을 받아 간단한 예제 code를 만들어 보고, 이를 분석해 보는 것이 좋겠다는 결론에 이르게 되었다. 🚀

<이번 posting에서 중점적으로 소개할 주제>
______________________________________________________________
1) 소유권(이동, 빌림 & Reference), 수명 문제
- 개념은 쉬운 듯 하나, 복잡한 코드와 함께 사용 시 혼란스러운 경우가 많다.
- Lifetime은 아직도 어렵다.

2) 사실 1번 보다 더 어렵게 느끼는 부분은, 다양한 표준 Trait의 정확한 용도이다.
- Drop, Sized, Clone, Copy, Dreref/DerefMut, Default, AsRef/AsMut, Borrow/BorrowMut, From/Into, TryFrom/TryInto, ToOwned
- 이 외에도 다양한 Trait 개념 및 종류: Trait bound, Trait object, Marker trait, Extension trait, Blanket trait 등

3) 사용하기 복잡하고 난해한 Smart Pointer
- Box, Arc, Rc/Weak/RefCell, Ref/RefMut, Cow, Mutex/MutexGuard, RwLock
- 동일한 내용을 C/C++로 작성할 경우와 비교해 봐도 사용법이 너무 복잡하고 까다롭다.

4) Iterator & functional programming에 입각한 함수 사용
- 다른 언어에도 있는 어찌보면 매우 일반적인 내용이긴 하지만, a().b().c() 식으로 c() 다음에 어떤 함수가 나오는지를 예측하기 어렵다(익숙해 지는데 시간이 필요하다).
- 또한, 아래 5번 항목과 결부하여, unwrap(), ?,  expect() 등을 이어서 호출하는 부분이 귀찮기도 하고, 잊기 쉽기도 하다.

5) Option, Result 사용법
- 지금은 많이 익숙해 지긴 했으나, Some(x), Ok(y), Err(z) 에서 x, y, z 등을 빼내어 사용하는 부분이 불편하기도 하고, 때로는 어렵게 느껴지기도 한다.

6) 비동기 programming
- (다른 언어에도 있는 내용이라) concept 자체는 이해하기 어렵지 않으나, 실제  coding시 헷갈리는 부분이 보인다.
__________________________________________________

따라서, 이어지는 장에서는 이상의 5-6가지 주제를 아우르는 예제  program을 만들어 보고, 이를 통해  Rust에 한발 더 다가갈 수 있는 시간을 가져보고자 한다. 이어지는 장들에서 소개할 모든 예제 코드는 Gemini 3 pro를 통해 생성하였음을 밝힌다. 🐾



Chapter2. 소유권과 Borrowing & Reference에 대한 이해
이번 장에서는 Rust의 소유권(Ownership)Borrowing(빌림)Reference(참조)의 개념을 소개하고, 후반부에서는 Reference 사용법과 관련하여 좀 헷갈리는 내용(특히 iterator 사용 시)을 예제를 통해 파악해 보고자 한다.

2.1 소유권(Ownership)의 이해
먼저, Rust는 소유권(Ownership) 개념이 있기 때문에 수동으로 메모리를 해제하는 C/C++에서 자주 발생하는 메모리 관련 오류(null pointer dereferences, buffer overflows, double-free memory issues, dangling references, data races )가 발생하지 않는다. 즉, 소유권 시스템으로 인해 메모리가 원인이 되는 문제는 원천적으로 방지되며, 메모리 안정성이 확보된다고 말할 수 있다.

<Rust 소유권의 3대 원칙>
1) 값(value) 혹은 resource에는 소유권이 있으며, 변수(variable)는 값(value) 혹은 resource의 소유자(Owner)가 된다.

2) 소유권(Ownership)은 이동(move)할 수 있으며, 특정 시점에서의 소유자(Owner)는 오직 1개(1개의 변수) 뿐이다.
  2.1) 소유권 이동은 다른 변수에 대입하거나, 함수의 파라미터로 전달할 경우에 발생한다.
  2.2) 소유권 이동은 String, Vec<T>, Box<T> 등 힙(Heap) 메모리에 resource가 할당되어 있는 type에 대해서만 적용되며, 정수, 실수, Boolean 등 stack에만 존재하는 기본 type 들에는 적용되지 않는다(이 경우는 소유권이 이동하지 않고 복사된다).
  2.3) 소유권 이동 시에는 heap data는 복사되지 않으며, stack data 즉, pointer, length, capacity만 복사된다. 따라서 이동 과정은 빠르게 진행된다.

[그림 2.1] Rust의 소유권 이동

[코드 2.1] 함수로 String type data에 대한 소유권이 이동하는 예

3) 소유자가 유효한 범위(scope)를 벗어나면, 소유자가 소유한 값은 자동으로 파기된다. 이는 memory leak을 방지해주는 효과가 있다.

[코드 2.2] String type data가 scope를 벗어날 때 소유한 값이 파기되는 예

📌 참고로, println!() macro를 반복하여 호출하여도 소유권 문제는 발생하지 않는다(않도록 되어 있다).

아래 코드는 함수에서 값을 return할 때 발생할 수 있는 소유권 이동에 관한 예제이다.

<예제#1 - 소유권 이동>
_____________________________________________________

<함수 return 값에 대한 소유권 문제 - 수정 전>
fn gen_message() -> &str {
let msg = String::from("실수할 줄 아는 사람이 아름답다.");
return &msg;
} //이 시점에 msg가 가리키는 String buffer(heap에 존재) 내용이 자동 해제되므로,
//이를 가리키는 reference는 문제가 된다(compile error 발생).

fn main() {
let m = gen_message();
println!("{}", m);
}

<함수 return 값에 대한 소유권 문제 - 수정 후>
fn gen_message() -> String {
let msg = String::from("실수할 줄 아는 사람이 아름답다.");
return msg; //이렇게 해 주어야 String에 대한 소유권이 main 함수로 이동하여 정상 동작한다.
}

fn main() {
let m = gen_message();
println!("{}", m);
}
_____________________________________________________

2.2 Borrowing & Reference의 이해
Rust의 소유권 model에서는 특정 순간에 값(value)을 소유하는 소유자(변수)는 오직 1개 뿐이라고 하였다. 하지만, 코드를 작성하다 보면, 소유권을 가져올 필요는 없지만 해당 값을 잠시동안 사용하고 반납하고 싶은 경우가 많이 있다. 이런 경우에 소유권을 이동시키거나, 해당 값을 통째로 복사(clone)하지 않으면서도 다른 변수가 사용할 수 있도록 할 수 있는 방법이 필요한데, 이것이 빌림(borrowing)이다. 빌림은 Reference(예: &symbo)를 이용하여 구현되는데, (다들 아는 것처럼) reference는 일종의 pointer로 볼 수 있다. 다만, Rust의 reference는 compiler가 제공하는 borrow-checker라는 것을 통해서 엄격하게 통제된다는 점이 다른 언어의 reference와는 다르다(위배 시 compile error 발생).

[그림 2.2] Rust의 소유권 빌림

<빌림 규칙(Borrowing rules) - Rust borrow checker의 핵심 구성 요소>
1) 특정 시점에 1개의 mutable reference(&mut T)와 여러 개의 immutable reference(&T)를 동시에 사용해서는 안된다(한 놈이 수정하고 있는데, 다른 녀석들이 그 값을 읽어가는 상황이 벌어져서는 안된다). 이는 data races, dangling pointers, unsafe memory access 등을 (compile 시점에) 찾아내기 위해 반드시 필요하다.

[코드 2.3] 1 mutable & many immutable reference 예제

2) 모든 reference는 유효(valid)해야 한다. 즉, 모든 reference는 할당 해제된 resource나 scope를 벗어난 resource를 참조하면 안된다. 이는 dangling pointer or reference issue를 막기 위해 반드시 필요하다.

[코드 2.4] Dangling reference 예제
 
______________________________________________________

(특히) 함수에 값을 전달하고자 할 경우, Reference를 자주 사용하게 되는데, 상황에 따라서 reference를 사용하는 부분이 다소 어렵게 느껴질 수가 있다. 따라서, 예제를 통해서 상황에 맞는 reference 사용 방법을 살펴 보기로 하자.

Keywords: Reference, DeReference, Iterator, Iterator adapter, filter


numbers.iter().filter(|&&x| { })
 case #1) &&x = &&i32 => x 

numbers.iter().filter(|x| { })
case #2) x = &&i32 => **x


<예제#2 - 다양한 Reference 사용 방법>
_______________________________________________________________________

fn main() {
// ==========================================================
// 1. 기본 개념: 참조(Reference)와 이중 참조(Double Reference)
// ==========================================================
println!("--- 1. 기본 개념 ---");

let val: i32 = 10;
let ref1: &i32 = &val; // 싱글 참조
let ref2: &&i32 = &ref1; // 이중 참조 (참조에 대한 참조)

// 값을 꺼내기 위해서는 참조된 횟수만큼 역참조(*)해야 합니다.
println!("val: {}", val);
println!("*ref1: {}", *ref1);
println!("**ref2: {}", **ref2); // 두 번 벗겨야 원본 i32 도달

// ==========================================================
// 2. Closure와 이중 참조 (가장 헷갈리는 부분)
// ==========================================================
println!("\n--- 2. Closure와 이중 참조 ---");

let numbers = vec![1, 2, 3, 4, 5];

// 시나리오: vector의 iter()는 요소를 빌려줍니다(&i32).
// filter()는 그 빌려온 요소에 대한 참조를 또 만듭니다(&(&i32)).
// 즉, 클로저 인자 x의 타입은 &&i32가 됩니다.

// [Case A] 정석적인 완전 명시적 방법
// x는 &&i32이므로, 값 비교를 위해 **를 사용해 i32로 만듦
let count_explicit = numbers.iter().filter(|x| {
// x의 타입: &&i32
**x > 3
}).count();
println!("Case A (Explicit **): {}", count_explicit);

// [Case B] 구조 분해 (Destructuring) 사용 - 추천 방식
// 인자 패턴 매칭에서 &&를 벗겨버림. 이러면 내부에서 x는 i32가 됨.
let count_destructure = numbers.iter().filter(|&&x| {
// x의 타입: i32 (이미 껍질을 벗기고 받음)
x > 3
}).count();
println!("Case B (Destructuring &&): {}", count_destructure);

// [Case C] 암묵적 처리 (Auto-deref & Method Call)
let count_implicit = numbers.iter().filter(|x| {
// x의 타입: &&i32
// 1. Dot Operator (.)의 마법
// Rust에서 메서드를 호출할 때(.), 컴파일러는 필요한 만큼 자동으로 역참조를 수행합니다.
// x가 &&i32라도, i32의 메서드인 abs() 등을 찾기 위해 자동으로 Deref를 수행합니다.
//let _test_method = x.abs(); // (*(*x)).abs() 와 동일하게 처리됨

// 2. println! 등의 매크로
// 매크로는 참조를 자동으로 따라가서 값을 출력해줍니다.
// println!("{}", x); // 가능함

// 3. 비교 연산자 (PartialEq)
// Rust는 &T와 &T, 혹은 T와 T의 비교를 엄격히 따집니다.
// 하지만 **x > 3 처럼 하지 않고, 참조 레벨을 맞춰주면 비교가 가능합니다.
// x는 &&i32, &3은 &i32. 서로 타입이 안 맞아서 아래는 원래 에러가 날 수 있지만,
// 보통은 **x > 3으로 값을 비교하거나, 구조 분해를 씁니다.
// 여기서는 명시적 역참조가 가장 안전하고 확실한 방법입니다.
**x > 3
}).count();
println!("Case C (Implicit/Method): {}", count_implicit);


// ==========================================================
// 3. 헷갈리는 상황 정리 (x vs &x vs &&x)
// ==========================================================
println!("\n--- 3. 인자 패턴에 따른 x의 타입 변화 ---");
// 상황: iter() -> &i32 yield
// filter() -> 인자로 &(&i32) 전달

numbers.iter().filter(|val| {
// val: &&i32
// 값을 쓰려면 **val 필요
**val % 2 == 0
});

numbers.iter().filter(|&val| {
// &val 패턴이 &&i32와 매칭됨 -> 껍질 하나 벗겨짐
// val: &i32
// 값을 쓰려면 *val 필요
*val % 2 == 0
});

numbers.iter().filter(|&&val| {
// &&val 패턴이 &&i32와 매칭됨 -> 껍질 두 개 벗겨짐
// val: i32
// 그냥 val 사용 가능
val % 2 == 0
});
println!("예제 실행 완료");
}
_______________________________________________________________________


Chapter3. Option/Result 사용법
Rust에는 NULL pointer의 개념이 아예 없다. 또한 error handling 처리 방식도 기존 programming language와는 사뭇 다르다. 기본적으로 Rust에서는 문제가 될만한 상황(Null pointer 상황 혹은 error 발생 상황)을 안전하게 처리하기 위해, 이들을 뭔가로 감싸서 전달하는 방식(Option<T>Result<T, E>)을 사용한다. 따라서, 실제(최종) 결과를 처리하기 위해서는 감싸져 있는 값을 꺼내는 작업이 항상 동반되게 되는데, 때로는 이 과정이 매우 번거롭게 느껴지기도 하지만, 이를 통해 안정성을 확보하겠다는 Rust의 강력한 의지가 느껴지기도 한다.

(이해를 돕기 위해) Option<T>와 Result<T, E>는 아래 그림과 같은 상자 속에 값을 넣어두는 개념에 비유할 수 있다.
[그림 3.1] Option & Result의 개요 [출처 - 참고문헌 11]

Option<T> : 값이 있을 수도(Some) 혹은 없을 수도(None) 있음을 표현하는 상자
1) 값이 있는 경우는 다시 Some이라는 작은 상자 안에 T 값을 포장하여 넣어 둠. 따라서 이 경우, Option 상자에서 꺼낸 내용은 실제 값 T가 아니라 Some(T)가 됨.
2) 값이 없는 경우는 별도의 추가 상자 없이 None을 그대로 넣어 둠.

Result<T, E> : 작업이 성공(Ok)했거나 실패(Err)했음을 표현하는 상자
1) 함수 실행 작업이 성공한 경우는 다시 Ok라는 작은 상자에 그 결과 T를 포장하여 넣어둠. 따라서 Result 상자에서 꺼낸 내용은 Ok(T)가 됨.
2) 함수 작업이 실패한 경우는 Err이라는 작은 상자에 그 결과(에러 E)를 포장하여 넣어둠. 따라서 Result 상자에서 꺼낸 내용은 Err(E)가 됨.

중요한 사실은, code에서 Option 혹은 Result을 받은 경우, 실제 값 T를 얻어오기 위해서는 (None을 제외한) Some(T), Ok(T), Err(E) 등에서 각각의 해당 상자(Some, Ok, Err)를 제거해야만 한다는 것이다. 이를 위해서 rust에서는 match 문을 이용하거나, unwrap() 등의 간편 함수(method)를 이용할 수 있다.

<Option<T>와 Result<T, E>에서 값을 추출하는 방법에 대한 보충 설명>
1) match 문: Option<T> 혹은 Result<T, E>에서 결과 값을 추출하는 가장 확실한 방법은 match문을 사용하는 것이다. 아래 예제는 Result<f64, String>을 return하는 divide( ) 함수를 호출한 후, 상위 함수에서 match result1 { Ok(value) => ....value..., Err(message) => ...message..., } 문을 사용하여 값을 추출하는 모습을 보여준다.

[코드 3.1] Result<f64, String>에 대한 match 문 수행 예제

match 문의 단점은 코드를 장황하게(verbose) 만든다는 것이다. 😂

2) unwrap( ) 메서드: Rust에는 복잡한 match 문을 간소화시켜 주는 여러가지 방법이 존재하는데, 그 중 대표적인 method가 unwrap( )이다. unwrap( )은 Result가 Ok(value) 이거나 Option이 Some(value)인 경우에는 value를 return하고, Err이거나 None인 경우에는 panic을 발생시킨다. 💣 따라서 이 method는 결과 값을 확신할 수 있을 경우, 아주 간편하게 사용할 수 있으나, 실패할 경우에는 panic이 발생하는 만큼 사용에 신중을 기해야 한다(특히 상용 제품에서는 가급적 사용을 자제해야 함).

let value = ok_result.unwrap();

3) expect("error message") 메세드: unwrap( )과 동일하게 동작하는 method이다. 다만, panic 발생시 파라미터로 넘긴 "error message"가 panic message와 함께 출력된다는 점이 다를 뿐이다(debugging 용으로 사용함).

non_existent_file.expect("Expected the file to definitelyexist!");

4) unwrap_or(default_value) 메서드: unwrap( )과 유사하게 동작한다. 다만, Err/None 시, 함수 결과로 default_value를 return한다(panic은 발생하지 않음).

let value_or_default = err_result.unwrap_or(0.0);

5) unwrap_or_else(closure) 메서드: unwrap_or( )와 유사하게 동작하는데, Err/None 시, 파라미터로 넘긴 closure를 실행하는 점이 다르다.

let err_result = divide(10.0, 0.0);
let value_or_computed = err_result.unwrap_or_else(|err_msg| {
      println!("Error during division: {}. Using fallback value.", err_msg);
      -1.0 // Value computed/returned by the closure
});
println!("Value or computed: {}", value_or_computed);

6) ? 기호 : ? (question mark) operator를 사용하면, Result나 Option을 아주 간편하게 처리할 수가 있다. 예를 들어, 함수 A() -> B() -> C()를 호출한 상태에서, C() 함수를 수행하다가 error가 발생하여 그 결과를 B() 함수로 return하고자 할 경우, 문제가 되는 함수 끝에  ? 기호를 붙여 주면 match 문을 사용할 때 생기는 번거로움을 줄일 수 있다.
📌 ? 처럼, 문법적으로 간단하게 사용할 수 있는 방법을 syntactic sugar(이를 굳이, 문법 설탕이라는 어색한 표현으로 해석하지는 말자 😂)라고 한다. 🍬

let content = fs::read_to_string(file_path)?;

? 기호의 동작 방식
a) 함수 수행 결과, Ok(value) or Some(value)인 경우에는 value를 함수의 결과로 return한다(이후 코드는 계속 실행된다).
b) Err(error) or None인 경우에는, 현재 상태에서 해당 함수를 멈추고, 즉시 Err(error) or None을 상위 함수로 return한다.

? 기호 사용시 주의할 점
한가지 중요한 사실은 A() -> B() -> C() ... 호출 시 C()에서 return한 결과가 B()의 return 결과 type과 일치하지 않을 경우, compile error가 발생하게되므로 이를 일치(혹은 변환)시켜 주는 노력이 필요하다는 것이다. 이는 B() -> A()로 return되는 경우에도 동일하게 적용되어야 하는 문제이기도 하다. 따라서 이를 위해서는 함수 간에 return되는 error type이 문제없이 잘 전달(전파)되도록 하기 위하여, (번거롭더라도) 사전에 From trait를 이용하여 type conversion하는 코드를 준비해 두어야만 한다(이 부분은 아래 예제 코드에서 확인 가능하다).

📌 이 밖에도 map( ), and_then( ), or_else( ) 등의 조합(combinator) method를 쓰면 편리하다.
____________________________________________

Keywords: enum, Option<T>, Some(), None, Result<T, E>, Ok(), Err(), match, ?, From trait, thiserror crate, anyhow crate

<예제#1 - Option과 Result 사용 방법>
_______________________________________________________________________

use std::fmt;
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;

// 1. 모든 에러를 통합할 사용자 정의 Enum 정의
// 여기서는 IO 에러, 숫자 파싱 에러, 그리고 데이터가 없는 경우(Option 처리)를 포함합니다.
#[derive(Debug)]
enum AppError {
Io(io::Error), // 파일 읽기 실패 등
Parse(ParseIntError), // 문자열 -> 숫자 변환 실패
MissingData(String), // Option::None 처리를 위한 커스텀 에러
}

// 에러 출력을 위해 Display 트레잇 구현 (사용자 친화적 메시지)
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::Io(err) => write!(f, "IO 오류 발생: {}", err),
AppError::Parse(err) => write!(f, "파싱 오류 발생: {}", err),
AppError::MissingData(msg) => write!(f, "데이터 누락: {}", msg),
}
}
}

// std::error::Error 트레잇 구현 (선택 사항이지만 모범 사례임)
impl std::error::Error for AppError {}

// 2. 'From' 트레잇 구현: 하위 에러들을 AppError로 자동 변환하기 위함
// 이를 구현하면 '?' 연산자 사용 시 자동으로 변환됩니다.

// io::Error -> AppError 변환
impl From<io::Error> for AppError {
fn from(err: io::Error) -> AppError {
AppError::Io(err)
}
}

// ParseIntError -> AppError 변환
impl From<ParseIntError> for AppError {
fn from(err: ParseIntError) -> AppError {
AppError::Parse(err)
}
}

// --- 시뮬레이션 함수들 ---

// (A) Result를 반환하는 함수: 파일 읽기 시뮬레이션 (IO Error 발생 가능)
fn read_config_file(path: &str) -> Result<String, io::Error> {
// 실제 파일이 없으므로 에러를 낼 수도 있고 성공을 흉내낼 수도 있습니다.
// 여기서는 데모를 위해 성공한 척 하거나, 경로가 "bad"면 에러를 냅니다.
if path == "bad_path" {
return Err(io::Error::new(io::ErrorKind::NotFound, "파일을 찾을 수 없습니다"));
}
// 정상 케이스: 파일 내용에 숫자가 들어있다고 가정
Ok(String::from("42"))
}

// (B) Result를 반환하는 함수: 파싱 시뮬레이션 (ParseIntError 발생 가능)
fn parse_config_value(text: &str) -> Result<i32, ParseIntError> {
let val: i32 = text.trim().parse()?; // 여기서 에러나면 ParseIntError 리턴
Ok(val)
}

// (C) Option을 반환하는 함수: 환경변수 가져오기 시뮬레이션
fn get_env_var(key: &str) -> Option<String> {
if key == "MODE" {
Some("PROD".to_string())
} else {
None
}
}

// 3. 메인 로직: 여러 에러를 하나의 AppError로 처리
fn run_application() -> Result<(), AppError> {
println!("1. 설정 파일 읽기 시도...");
// [중요] ? 연산자 사용
// read_config_file은 io::Error를 뱉지만, impl From 덕분에 자동으로 AppError::Io로 변환되어 반환됨
let content = read_config_file("config.txt")?;
println!(" -> 파일 내용: {}", content);

println!("2. 설정 값 파싱 시도...");
// parse_config_value는 ParseIntError를 뱉지만, 자동으로 AppError::Parse로 변환됨
let number = parse_config_value(&content)?;
println!(" -> 파싱된 숫자: {}", number);

println!("3. Option 처리 시도...");
// get_env_var는 Option을 반환합니다.
// Option을 Result로 변환하려면 .ok_or()를 사용합니다.
// None일 경우 AppError::MissingData로 변환하여 에러 전파
let mode = get_env_var("USER_KEY")
.ok_or(AppError::MissingData("USER_KEY 환경변수가 없습니다".to_string()))?;
println!(" -> 모드: {}", mode);

println!("모든 작업 성공!");
Ok(())
}

fn main() {
// 최종적으로 main에서 단일한 AppError 타입으로 결과를 받습니다.
match run_application() {
Ok(_) => println!("\n[Main] 프로그램 정상 종료"),
Err(e) => {
// e는 AppError 타입입니다.
println!("\n[Main] 프로그램 에러 종료!");
println!("에러 상세: {}", e); // Display 트레잇 덕분에 깔끔하게 출력
// 필요하다면 에러 종류에 따라 다른 처리를 할 수도 있습니다.
match e {
AppError::Io(_) => println!("(파일 시스템을 확인하세요)"),
AppError::MissingData(_) => println!("(설정을 확인하세요)"),
_ => {}
}
}
}
}
_______________________________________________________________________

cargo run

1. 설정 파일 읽기 시도...
   -> 파일 내용: 42
2. 설정 값 파싱 시도...
   -> 파싱된 숫자: 42
3. Option 처리 시도...

[Main] 프로그램 에러 종료!
에러 상세: 데이터 누락: USER_KEY 환경변수가 없습니다
(설정을 확인하세요)
_______________________________________________________________________

앞서 제시한 예제 코드는 custom error type(예: AppError)에 대해 Display trait와 Error trait 및 From trait를 일일히 구현해 주어야만 했다. Rust crate 중에는 이러한 절차를 간소화 시켜 주는 2개의 crate가 있는데, 이것이 바로 thiserror crateanyhow crate이다.

아래 2개의 예제 코드는 각각 thiserror crate와 anyhow crate를 이용하여 앞서 제시한 코드를 재 구현한 것이다. 어떤 코드 부분이 간소화되었는지 확인해 보기 바란다. 😋

<예제#2 - thiserror crate로 Option과 Result 사용하기>
_______________________________________________________________________
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;
use thiserror::Error; // cargo.toml에 thiserror 추가 필요

// 1. thiserror를 사용하여 매크로로 Display와 From 구현 자동화
#[derive(Debug, Error)]
enum AppError {
// #[error("...")]는 Display 트레잇을 자동 구현
// #[from]은 From 트레잇을 자동 구현하여 자동 형변환 지원
#[error("IO 오류 발생: {0}")]
Io(#[from] io::Error),

#[error("파싱 오류 발생: {0}")]
Parse(#[from] ParseIntError),

// From 트레잇이 필요 없는 경우(직접 생성하는 에러)는 #[from] 생략
#[error("데이터 누락: {0}")]
MissingData(String),
}

// --- 시뮬레이션 함수들 (기존과 동일) ---

fn read_config_file(path: &str) -> Result<String, io::Error> {
if path == "bad_path" {
return Err(io::Error::new(io::ErrorKind::NotFound, "파일을 찾을 수 없습니다"));
}
Ok(String::from("42"))
}

fn parse_config_value(text: &str) -> Result<i32, ParseIntError> {
let val: i32 = text.trim().parse()?;
Ok(val)
}

fn get_env_var(key: &str) -> Option<String> {
if key == "MODE" {
Some("PROD".to_string())
} else {
None
}
}

// 2. 메인 로직
fn run_application() -> Result<(), AppError> {
println!("1. 설정 파일 읽기 시도...");
// #[from] 덕분에 io::Error -> AppError::Io 자동 변환
let content = read_config_file("config.txt")?;
println!(" -> 파일 내용: {}", content);

println!("2. 설정 값 파싱 시도...");
// #[from] 덕분에 ParseIntError -> AppError::Parse 자동 변환
let number = parse_config_value(&content)?;
println!(" -> 파싱된 숫자: {}", number);

println!("3. Option 처리 시도...");
// Option은 여전히 수동으로 에러 변환 필요
let mode = get_env_var("USER_KEY")
.ok_or_else(|| AppError::MissingData("USER_KEY 환경변수가 없습니다".to_string()))?;
println!(" -> 모드: {}", mode);

Ok(())
}

fn main() {
match run_application() {
Ok(_) => println!("\n[Main] 프로그램 정상 종료"),
Err(e) => {
println!("\n[Main] 프로그램 에러 종료!");
println!("에러 상세: {}", e); // thiserror의 #[error] 메시지 출력
// Enum 구조가 살아있으므로 패턴 매칭 가능
match e {
AppError::Io(_) => println!("(파일 시스템을 확인하세요)"),
AppError::MissingData(_) => println!("(설정을 확인하세요)"),
_ => {}
}
}
}
}
_______________________________________________________________________

$ vi Cargo.toml
[package]
name = "option_result_with_thiserror"
version = "0.1.0"
edition = "2024"

[dependencies]
thiserror = "2.0"

$ cargo run

1. 설정 파일 읽기 시도...
   -> 파일 내용: 42
2. 설정 값 파싱 시도...
   -> 파싱된 숫자: 42
3. Option 처리 시도...

[Main] 프로그램 에러 종료!
에러 상세: 데이터 누락: USER_KEY 환경변수가 없습니다
(설정을 확인하세요)


<예제#3 - anyhow crate로 Option과 Result 사용하기>
_______________________________________________________________________
use std::io::{self};
use std::num::ParseIntError;
use anyhow::{Context, Result, anyhow}; // cargo.toml에 anyhow 추가 필요

// --- 시뮬레이션 함수들 ---
// 개별 함수들은 여전히 구체적인 에러 타입(io::Error 등)을 반환해도 됩니다.
// anyhow가 알아서 변환해줍니다.

fn read_config_file(path: &str) -> Result<String, io::Error> {
if path == "bad_path" {
return Err(io::Error::new(io::ErrorKind::NotFound, "파일을 찾을 수 없습니다"));
}
Ok(String::from("42"))
}

fn parse_config_value(text: &str) -> Result<i32, ParseIntError> {
let val: i32 = text.trim().parse()?;
Ok(val)
}

fn get_env_var(key: &str) -> Option<String> {
if key == "MODE" {
Some("PROD".to_string())
} else {
None
}
}

// 1. 메인 로직: 반환 타입이 anyhow::Result<()> 입니다.
// 커스텀 Enum 정의가 완전히 사라졌습니다.
fn run_application() -> Result<()> {
println!("1. 설정 파일 읽기 시도...");
// .context()를 사용하면 에러 발생 시 "어떤 작업 중이었는지" 메시지를 추가할 수 있습니다.
// 이는 에러 추적(Backtrace)에 매우 유용합니다.
let content = read_config_file("config.txt")
.context("설정 파일 읽기 실패")?;
println!(" -> 파일 내용: {}", content);

println!("2. 설정 값 파싱 시도...");
let number = parse_config_value(&content)
.context("설정 값 파싱 실패")?;
println!(" -> 파싱된 숫자: {}", number);

println!("3. Option 처리 시도...");
// Option에 대해서도 .context()를 쓰면 바로 Result로 변환됩니다.
// None일 경우 자동으로 에러가 생성됩니다.
let mode = get_env_var("USER_KEY")
.context("USER_KEY 환경변수가 없습니다")?;
println!(" -> 모드: {}", mode);

Ok(())
}

fn main() {
// anyhow는 Debug 출력({:?}) 시 에러 체인(Stack trace)을 예쁘게 보여줍니다.
match run_application() {
Ok(_) => println!("\n[Main] 프로그램 정상 종료"),
Err(e) => {
println!("\n[Main] 프로그램 에러 종료!");
// {:#}를 사용하면 context와 원본 에러를 모두 출력해줍니다.
println!("에러 발생: {:#}", e);
}
}
}
_______________________________________________________________________

vi Cargo.toml
[package]
name = "option_result_with_anyhow"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0"

$ cargo run
1. 설정 파일 읽기 시도...
   -> 파일 내용: 42
2. 설정 값 파싱 시도...
   -> 파싱된 숫자: 42
3. Option 처리 시도...

[Main] 프로그램 에러 종료!
에러 발생: USER_KEY 환경변수가 없습니다


Chapter4. 다양한 표준 Trait 사용법
이장에서는 다양한 Rust 표준(or utility) Trait 즉, DefaultDrop, SizedCopyClone,  Dreref/DerefMut, AsRef/AsMut, Borrow/BorrowMut, From/Into, TryFrom/TryInto, ToOwned 의 사용법을 이해하는 시간을 가져보고자 한다. 일부 트레이트는 쉽게 이해가 되지만, 또 어떤 것은 선뜻 이해가 가질 않는다(비슷 비슷한 내용이 좀 헷갈린다). 아마도 이러한 다양한 표준 Trait의 개념이, (앞으로 5장에서 소개하는) Smart Pointer의 개념과 더불어, Rust에서 가장 어려운 내용이 아닐까 싶기도 하다. 😓

[그림 4.1] 트레이드(Trait)와 구조체(Struct)의 관계

<몇가지 표준 Trait에 대한 보충 설명>
Rust는 기본적으로 아래와 같이 다양한 표준 Trait를 제공한다. 이들이 필요한 이유는 사용자 정의 type(구조체(struct)나 열거형(enum))에 대해서도 기본 type을 사용할 때와 유사한 동작 즉, 변수 할당(assignment) 시 복사/복제, 객체 초기화, 객체에 할당된 memory 해제(destructor), 참조와 역참조(referencing & dereferencing), 각종 type conversion 등을 자유롭게 수행할 수 있도록 하기 위해서이다. 충분한 내용 전달을 위해서는 각각의 트레이트 마다 자세한 배경 설명이 필요하지만, 지면 관계상 간단히 요점만 정리하고 넘어가고자 한다. 😂

1) Default Trait : Rust의 일부 type(변수)들은 default 값을 가지고 있다. 즉, 정수 변수는 0, 실수 변수는 0.0, bool 변수는 false,  String 변수는 "" 값, Vector 변수는 empty 값, 그리고 Option은 None을 default 값으로 갖는다. Default trait가 필요한 이유는, 사용자가 정의한 struct나 enum type에 대한 초기 설정을 하고 싶기 때문이다. 사실, default() 연관 함수(associate function)와 비슷한 함수로는 new( )가 있다. default( )가 new( )와 다른 점은 parameter가 필요 없다는 점이다. Default trait를 구현하는 방법은 다음과 같다.

trait Default {
      fn default() -> Self//객체 자신인 Self를 return한다는 점에 주목할 필요가 있다.
}

예를 들어, String type의 경우, Default trait를 아래와 같이 구현할 수 있다.

impl Default for String {
      fn default() -> Self {
            String::new()
      }
}

실제로 default() 함수는 구조체명::default( ) 형태로 호출하여 사용한다.

2) Sized Trait Sized 트레이트는 컴파일 타임에 타입의 크기(Byte)를 정확히 알 수 있는지를 나타내는 마커 트레이트(Marker Trait - { } 내가 비어 있는 Trait)이다. 대부분의 타입(i32, 구조체 등)은 기본적으로 Sized를 구현하므로 직접 명시할 일은 거의 없지만, 주로 제네릭(Generics)이나 트레이트 객체(Trait Objects)를 다룰 때 제한을 두기 위해 사용한다.

a) 제네릭 타입의 크기를 강제할 때 (T: Sized)
기본적으로 Rust의 제네릭은 T: Sized가 암묵적으로 적용되어 있다. 하지만 크기가 확정되지 않은 타입(DST: Dynamically Sized Types, 예: [T], str, dyn Trait)을 제네릭으로 넘기고 싶지 않을 때 명시적으로 제한할 수 있다. 

b) 크기를 알 수 없는 타입을 허용할 때 (T: ?Sized)
?Sized는 "Sized 트레이트를 구현할 수도 있고, 안 할 수도 있다"는 뜻으로, 제네릭 타입 파라미터가 Sized 제한을 해제(Relax)할 때 사용한다. &str이나 &[T]처럼 크기를 런타임에 아는 참조 타입을 받을 때 필수적이다.

[그림 4.2] Sized and Unsized value간의 비교 [출처 - 참고문헌 10]

3) Copy Trait (묵시적 복사): 힙(Heap) 메모리를 사용하지 않고 스택(stack) 메모리에만 할당되는 변수는 대입 연산자를 이용하여 값을 할당(예: a = b)할 경우, 소유권이 이동(move)되지 않고 복사(copy)가 이루어지게 된다. 이러한 type들은 사실은 내부적으로는 아래와 같이 Copy 마커 트레이트(Marker Trait)를 구현하고 있다.
📌 마커 트레이트(Marker Trait)는 내부에 아무런 함수나 type도 없는 trait를 의미한다. 이를 사용하는 이유는 특정 type(struct or enum)을 우리가 관심있어하는 어떠한 것으로 구분(mark)하고 싶기 때문이다.

trait Copy: Clone { }   //이런식으로 { } 내부에 내용이 없는 껍데기 trait를 마커 트레이트(Marker Trait)라고 한다.
impl Copy for MyType { }  //마커 트레이트(Marker Trait) 내에는 type 및 연관함수나 method가 없으므로 이를 구현할 내용도 당연히 없다.

4) Clone Trait (명시적 복사): 한편 힙(Heap) 메모리에도 resource가 할당되는 변수의 경우에는 대입 연사자를 사용할 경우, 소유권이 이동(move)되는 것을 원칙으로 한다. 하지만, 소유권 이동 대신 stack & heap에 할당된 resource를 모두 복사(clone)하고 싶은 경우를 위해 Clone Trait가 마련되었다.

trait Clone: Sized //Clone trait를 사용하기 위해서는 힙(Heap) 메모리에 할당된 resource의 size가 명확한 경우에만 가능하다. 이런 경우 Clone trait는 Sized trait를 확장한다고 표현한다.
      fn clone(&self) -> Self;
      fn clone_from(&mut self, source: &Self) {
            *self = source.clone()
      }
}

실제로 Clone Trait를 사용을 위해서는, 대입문의 우측에서 객체.clone()  형태로 method 호출을 해 주어야 한다.

5) Drop Trait: Drop trait는 C++의 소멸자(destructor)에 해당하는 것으로, 변수의 소유권이 해제되는 시점(예: block이 끝나는 시점)에는 drop method가 자동 호출된다. 만일 변수의 수명이 다하는 시점이 되기 전에, 소유권을 해제하고자 한다면 drop( ) method를 직접 호출해 주면 된다.

trait Drop {
      fn drop(&mut self);
}

6) Deref/DerefMut TraitDeref와 DerefMut 트레이트는 스마트 포인터나 래퍼(Wrapper) 구조체를 일반 참조자(&T)처럼 동작하게 만들고 싶을 때 사용한다. 핵심은 사용자가 * 연산자나 .(dot) 메서드 호출을 통해 내부 데이터에 투명하게 접근할 수 있게 하여 코드의 가독성과 편의성을 높이는 것이다.

trait Deref {
      type Target: ?Sized;
      fn deref(&self) -> &Self::Target;
}

trait DerefMut: Deref {
      fn deref_mut(&mut self) -> &mut Self::Target;
}

a) 언제 사용하나 ?
  • 커스텀 스마트 포인터 구현: Box<T>, Rc<T>, Arc<T>와 같이 힙(Heap) 메모리를 관리하거나 소유권을 제어하는 구조체를 만들 때, 내부 데이터에 직접 접근하기 위해 Deref를 구현한다.
  • 래퍼 타입(Wrapper Type)의 편의성 향상: 데이터를 구조체로 감싸고(newtype 패턴), 그 래퍼 타입을 마치 원본 타입처럼 다루고 싶을 때 사용한다.
  • Deref 강제 변환(Coercion) 활용: 함수가 &T를 요구할 때 MyWrapper<T>를 직접 전달할 수 있게 하여, 사용자가 매번 .get() 같은 메서드를 호출하지 않도록 할 때 사용한다.
b) Deref vs DerefMut 차이
  • Deref (Immutable): 역참조 연산자(*) 사용 시 불변 참조(&Target)를 반환한다. .(dot) 연산자를 통한 불변 메서드 호출에 사용된다.
  • DerefMut (Mutable): 역참조 연산자(*) 사용 시 가변 참조(&mut Target)를 반환한다. 데이터 수정이 필요한 경우 사용하며, Deref가 먼저 구현되어야 한다.

7) AsRef/AsMut TraitAsRef와 AsMut 트레이트는 타입을 다른 참조 타입(&T 또는 &mut T)으로 유연하게 변환하여 함수의 인수 타입을 다변화하고 API 사용성을 높일 때 사용한다. 소유권을 가져오지 않고 참조만 필요로 하는 제네릭 함수에서 다양한 입력 타입(예: String과 &str)을 동시에 허용하고자 할 때 유용하다.

trait AsRef<T: ?Sized> {
      fn as_ref(&self) -> &T;
}

trait AsMut<T: ?Sized> {
      fn as_mut(&mut self) -> &mut T;
}

<사용 예제>  //아래 사용 예제를 보는 것이 AsRef Trait를 이해하는데 도움을 준다.
// AsRef<str>을 구현하는 type T를 파라미터로 하는 generic 함수 예
fn is_hello<T: AsRef<str>>(s: T) {
    // T로 부터 &str reference 값을 얻기 위해 명확히 .as_ref() 함수 호출
    assert_eq!("hello", s.as_ref());
}

fn main() {
    let s = "hello";
    is_hello(s);  //string literal(&str)을 함수에 전달

    let s = "hello".to_string();
    is_hello(s);  //소유권이 있는 String 값을 함수에 전달 - is_hello() 함수 내에서는 소유권 이동 없이 reference이용하여 이후 처리함.
}

8) From/Into TraitFrom과 Into 트레이트는 항상 성공하는 타입 간의 안전한 데이터 변환을 위해 사용한다. 재밌게도 From을 구현하면 Into는 자동으로 구현되며, 주로 코드 가독성 향상, 함수 인자의 유연성 확보, 사용자 정의 타입 간 변환에 활용된다.

trait Into<T>: Sized {
      fn into(self) -> T;
}
trait From<T>: Sized {
      fn from(T) -> Self;
}

<사용 예제>  //아래 사용 예제를 보는 것이 From/Into Trait를 이해하는데 도움을 준다.
use std::convert::From;

#[derive(Debug)]
struct Number {
    value: i32,
}
//i32를 Number struct로 변환하는 From trait 구현
impl From<i32> for Number {
    fn from(item: i32) -> Self {
        Number { value: item }
    }
}

fn main() {
    // from trait 사용
    let num_from = Number::from(30);
    println!("My number created with From: {:?}", num_from);

    // into trait 사용(앞서 정의하지 않았지만 rust compiler에서 자동으로 생성해 줌)
    let int_val = 50;
    let num_into: Number = int_val.into();
    println!("My number created with Into: {:?}", num_into);
}

9) ToOwned Trait ToOwned 트레이트는 빌린 데이터(borrowed data)로부터 소유권을 가진 데이터(owned data)를 생성(복제)해야 할 때 사용한다. Clone 트레이트와 비슷하지만, &T에서 T를 만드는 것뿐만 아니라 &str을 String으로, &[T]를 Vec<T>로 바꾸는 것처럼 빌린 데이터의 "소유 형태(Owned type)"가 다를 때 주로 사용된다.

trait ToOwned {
      type Owned: Borrow<Self>;
      fn to_owned(&self) -> Self::Owned;
}
_________________________________________________________________

지금까지 설명한 표준 Trait의 구체적인 사용 방법을 예제를 통해 확인해 보도록 하자.

Keywords: Utility Trait, Drop, Sized, Clone, Copy, Dreref/DerefMut, Default, AsRef/AsMut, Borrow/BorrowMut, From/Into, TryFrom/TryInto, ToOwned

<예제#1 -  다양한 Utility Trait 사용 방법>
_______________________________________________________________________
use std::borrow::Borrow;
use std::convert::TryFrom;
use std::ops::{Deref, DerefMut};
use std::rc::Rc;

// ==========================================
// 1. Default
// 기본값을 정의하는 트레이트입니다.
// ==========================================
#[derive(Debug)]
struct GameConfig {
volume: i32,
resolution: String,
}

impl Default for GameConfig {
fn default() -> Self {
Self {
volume: 50,
resolution: String::from("1920x1080"),
}
}
}

// ==========================================
// 2. Clone & Copy
// Clone: 명시적인 복사 (.clone()) / 힙 메모리 데이터 복사 가능
// Copy: 암묵적인 비트 단위 복사 (stack-only), 소유권 이동이 일어나지 않음
// ==========================================
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}

#[derive(Debug, Clone)] // String을 포함하므로 Copy 불가능
struct Person {
name: String,
}

// ==========================================
// 3. Drop
// 스코프를 벗어날 때 실행되는 소멸자 로직을 정의합니다.
// ==========================================
struct CustomSmartPointer {
data: String,
}

impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!(">>> [Drop] 메모리 해제됨: {}", self.data);
}
}

// ==========================================
// 4. Deref & DerefMut
// 스마트 포인터가 내부 데이터의 메서드에 접근할 수 있게 해줍니다 (* 연산자 오버로딩).
// ==========================================
struct MyBox<T>(T);

impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<T> DerefMut for MyBox<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

// ==========================================
// 5. From & Into
// 타입 변환을 위한 트레이트입니다. From을 구현하면 Into는 자동으로 구현됩니다.
// ==========================================
#[derive(Debug)]
struct NumberWrapper(i32);

impl From<i32> for NumberWrapper {
fn from(item: i32) -> Self {
NumberWrapper(item)
}
}

// ==========================================
// 6. TryFrom & TryInto
// 실패할 수 있는 타입 변환입니다. Result를 반환합니다.
// ==========================================
#[derive(Debug)]
struct EvenNumber(i32);

impl TryFrom<i32> for EvenNumber {
type Error = &'static str;

fn try_from(value: i32) -> Result<Self, Self::Error> {
if value % 2 == 0 {
Ok(EvenNumber(value))
} else {
Err("짝수가 아닙니다!")
}
}
}

// ==========================================
// 7. AsRef & AsMut
// 비용이 적게 드는 참조 변환(Reference-to-Reference conversion)을 수행합니다.
// ==========================================
fn print_length<T: AsRef<str>>(s: T) {
// T가 String이든 &str이든 상관없이 &str로 취급
println!("길이: {}", s.as_ref().len());
}

// ==========================================
// 8. Borrow & BorrowMut
// 데이터 구조(HashMap 등)에서 키를 조회할 때 유용합니다.
// 소유한 데이터(String)를 참조 형태(&str)로 빌릴 수 있게 해줍니다.
// Eq, Hash, Ord가 원본과 빌린 형태에서 동일하게 동작해야 한다는 계약이 있습니다.
// ==========================================
fn check_key<K, Q>(key: &K, query: &Q)
where
K: Borrow<Q>, // K 타입을 Q 타입으로 빌릴 수 있어야 함
Q: PartialEq + ?Sized,
{
if key.borrow() == query {
println!("키가 일치합니다!");
} else {
println!("키가 다릅니다.");
}
}

// ==========================================
// 9. ToOwned
// Borrow된 데이터(예: &str)에서 소유권이 있는 데이터(예: String)를 생성합니다.
// Clone의 일반화된 형태입니다.
// ==========================================
fn make_owned(s: &str) -> String {
s.to_owned() // &str -> String 변환 (내부적으로 복사 발생)
}

// ==========================================
// 10. Sized
// 컴파일 타임에 크기가 알려진 타입을 나타내는 마커 트레이트입니다.
// ?Sized는 크기가 알려지지 않을 수도 있음(DST)을 나타냅니다.
// ==========================================
fn generic_sized<T: Sized>(t: T) {
println!("이 타입은 컴파일 타임에 크기가 정해져 있습니다.");
// std::mem::size_of_val(&t); // 가능
}

fn generic_maybe_unsized<T: ?Sized>(t: &T) {
println!("이 타입은 크기가 동적일 수 있으므로 참조로만 다룹니다.");
}


fn main() {
println!("=== Rust Utility Traits Demo ===\n");

// 1. Default
let conf = GameConfig::default();
println!("[Default] 기본 설정: {:?}", conf);

// 2. Clone & Copy
let p1 = Point { x: 10, y: 20 };
let p2 = p1; // Copy 발생 (p1 여전히 사용 가능)
println!("[Copy] p1: {:?}, p2: {:?}", p1, p2);

let person1 = Person { name: "Alice".into() };
let person2 = person1.clone(); // Clone (깊은 복사)
// let person3 = person1; // 이 줄을 활성화하면 person1은 소유권 이동으로 사용 불가
println!("[Clone] {:?} 복제됨", person2);

// 3. Drop
{
let _ptr = CustomSmartPointer { data: String::from("중요한 데이터") };
println!("[Drop] 스코프 내부");
} // 여기서 Drop 호출됨
println!("[Drop] 스코프 외부");

// 4. Deref & DerefMut
let mut my_box = MyBox(String::from("Hello"));
// MyBox에는 len()이 없지만, Deref를 통해 String의 len() 호출 가능
println!("[Deref] 길이: {}", my_box.len());
// DerefMut을 통해 내부 String 수정 가능
my_box.push_str(" World");
println!("[DerefMut] 내용: {}", *my_box);

// 5. From & Into
let num = NumberWrapper::from(100);
let num2: NumberWrapper = 200.into();
println!("[From/Into] {:?}, {:?}", num, num2);

// 6. TryFrom & TryInto
let even = EvenNumber::try_from(4);
let odd: Result<EvenNumber, _> = 5.try_into();
println!("[TryFrom] 짝수 성공: {:?}", even);
println!("[TryInto] 홀수 실패: {:?}", odd);

// 7. AsRef
println!("[AsRef] String 전달:");
print_length(String::from("Rust"));
println!("[AsRef] &str 전달:");
print_length("Programming");

// 8. Borrow
let owner = String::from("key_value");
// String을 소유하고 있지만 &str로 비교 가능 (Borrow 트레이트 덕분)
print!("[Borrow] ");
check_key(&owner, "key_value");

// 9. ToOwned
let borrowed_str: &str = "im borrowed";
let owned_string: String = make_owned(borrowed_str);
println!("[ToOwned] 소유권 생성: {:?}", owned_string);

// 10. Sized vs ?Sized
let x = 10;
generic_sized(x);
let slice: &str = "Dynamic Size";
generic_maybe_unsized(slice); // str은 Sized가 아니지만 ?Sized로 허용
}
_______________________________________________________________________

$ cargo run

=== Rust Utility Traits Demo ===

[Default] 기본 설정: GameConfig { volume: 50, resolution: "1920x1080" }
[Copy] p1: Point { x: 10, y: 20 }, p2: Point { x: 10, y: 20 }
[Clone] Person { name: "Alice" } 복제됨
[Drop] 스코프 내부
>>> [Drop] 메모리 해제됨: 중요한 데이터
[Drop] 스코프 외부
[Deref] 길이: 5
[DerefMut] 내용: Hello World
[From/Into] NumberWrapper(100), NumberWrapper(200)
[TryFrom] 짝수 성공: Ok(EvenNumber(4))
[TryInto] 홀수 실패: Err("짝수가 아닙니다!")
[AsRef] String 전달:
길이: 4
[AsRef] &str 전달:
길이: 11
[Borrow] 키가 일치합니다!
[ToOwned] 소유권 생성: "im borrowed"
이 타입은 컴파일 타임에 크기가 정해져 있습니다.
이 타입은 크기가 동적일 수 있으므로 참조로만 다룹니다.
_______________________________________________________________________

트레이트 객체(Trait Object)는 서로 다른 타입들이지만 동일한 트레이트를 구현하고 있을 때, 이들을 하나의 컬렉션(예: 벡터)에 담거나 함수에서 동적으로 처리할 때 사용되는 개념이다. 트레이트 객체를 이용하여, 가장 흔한 예제인 UI 컴포넌트 시스템을 구현하여 보자. Button과 SelectBox는 서로 다른 타입이지만, 둘 다 화면에 그려져야 하므로 Draw 트레이트를 공통으로 구현한다. 이와 관련한 트레이트 객체(Box<dyn Draw> 부분)의 memory layout을 그림으로 표현해 보면 다음과 같은데, 이어서 나오는 코드와 함께 동작 원리를 파악해 보기 바란다.


[그림 4.3] Trait Object memory layout
📌 그림 좌측의 data와 vptr과 우측의 관련 link(stack 상의 data와 vtable에 표현된 Impl trait 객체)를 합쳐서 trait object라고 부른다.

Keywords: Trait, Trait Object, Box<dyn Trait>

<예제#2 -  Trait Objecvt 사용 방법>
_______________________________________________________________________

// 1. 공통 동작을 정의하는 트레이트 정의
trait Draw {
fn draw(&self);
}

// 2. Button 구조체 정의
struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}

// Button에 Draw 트레이트 구현
impl Draw for Button {
fn draw(&self) {
println!(
"버튼 그리기: [ {} ] (크기: {}x{})",
self.label, self.width, self.height
);
}
}

// 3. SelectBox 구조체 정의
struct SelectBox {
pub width: u32,
pub height: u32,
pub options: Vec<String>,
}

// SelectBox에 Draw 트레이트 구현
impl Draw for SelectBox {
fn draw(&self) {
println!(
"선택 상자 그리기: 폭 {}px, 높이 {}px, 옵션: {:?}",
self.width, self.height, self.options
);
}
}

// 4. 화면(Screen) 구조체
// 여기서 핵심은 'Box<dyn Draw>'입니다.
// 이는 "Draw 트레이트를 구현한 어떤 타입이든 힙(Heap)에 할당된 포인터로 저장하겠다"는 의미입니다.
struct Screen {
// 제네릭(Vec<T>)을 쓰면 한 가지 타입만 담을 수 있지만,
// 트레이트 객체(Box<dyn Draw>)를 쓰면 여러 타입을 섞어서 담을 수 있습니다.
components: Vec<Box<dyn Draw>>,
}

impl Screen {
fn run(&self) {
println!("--- 화면 렌더링 시작 ---");
for component in self.components.iter() {
// 런타임에 각 컴포넌트의 구체적인 타입에 맞는 draw() 메서드가 호출됩니다 (Dynamic Dispatch).
component.draw();
}
println!("--- 화면 렌더링 종료 ---");
}
}

fn main() {
let screen = Screen {
components: vec![
// Button 인스턴스를 Box로 감싸서 트레이트 객체로 만듦
Box::new(Button {
width: 75,
height: 10,
label: String::from("확인"),
}),
// SelectBox 인스턴스를 Box로 감싸서 트레이트 객체로 만듦
Box::new(SelectBox {
width: 200,
height: 20,
options: vec![
String::from("네"),
String::from("아니오"),
String::from("취소"),
],
}),
// 다른 Button 추가 (서로 다른 타입이 한 벡터에 공존 가능)
Box::new(Button {
width: 50,
height: 10,
label: String::from("종료"),
}),
],
};

screen.run();
}
_______________________________________________________________________

$ cargo run

--- 화면 렌더링 시작 ---
버튼 그리기: [ 확인 ] (크기: 75x10)
선택 상자 그리기: 폭 200px, 높이 20px, 옵션: ["네", "아니오", "취소"]
버튼 그리기: [ 종료 ] (크기: 50x10)
--- 화면 렌더링 종료 ---


Chapter5. Smart Pointer 사용법
Rust code를 보다 보면, Box<T>, Rc<T>, Weak<T>, Cell<T>, RefCell<T>, Ref<T>, RefMut<T>, Cow<T>, Arc<T>, Mutex<T>, MutexGuard<T> 등 이해하기 어려운 스마트 포인터들이 많이 등장한다(흔히 사용하는 String, Vec도 사실은 smart pointer이다).

"Smart pointers are essentially structs that implement the Deref and Drop traits."
(스마트 포인터가 되려면 Deref 트레잇과 Drop 트레잇을 struct 내에 담고 있어야 한다)



[그림 5.1] Rust Vector Smart Pointer memory layout [출처 - 참고문헌 10]


<몇가지 Smart Pointer에 대한 보충 설명>
1) Box<T>는 C/C++의 malloc(), new() 등의 함수로 heap 영역에 공간을 확보하는 것과 동일한 개념으로 보면 된다. heap에 할당된 data에 대한 소유자는 당연히 Box<T>로 선언된 변수 하나 뿐이다.

let mut box = Box::new(0);
println!("{}", *box + 100);

2) Rc<T>는 Reference Count(strong reference count)를 기반으로 하는 smart pointer로써, (single thread 환경에서) shared ownership(복수개의 소유권)을 목적으로 만들어졌다. 한편 Arc<T>는 multi thread 환경에서 동일한 목적을 수행하기 위해 만들어 졌다. 두 방법 모두 immutable한 특징이 있다(즉, data를 수정할 수는 없다). Rc<T>는 보통 RefCell<T>, Arc<T>는 보통 Mutex<T>를 감싸는 형태로 사용하지만, Box<T>처럼 단독으로 사용할 수도 있다.
📌 Rc<T>, Arc<T>는 "변수의 소유자는 하나이다"라는 Rust 소유권 규칙에 위배되는 개념이다(위배되지만 필요하기 때문에 등장한 개념이다).

let rc = Rc::new(200);
println!("{}", rc);

3) Weak<T>(weak reference count)가 필요한 경우는 아래 설명과 같이, Linked list 같은 데서 2개의 Rc<T>를 이용해 노드간 상호 참조 시, Cyclic 연결(순환 연결)로 인한 memory leak 발생 문제(reference count가 0이 되지 못해 해제할 수 없는 상황 발생)를 해결하고자 할 때이다. Weak<T>는 Rc<T>나 Arc<T>와는 달리, heap 상의 data를 가리키기는 하나, 소유권이 없다. 참고로, Weak<T>로 data를 접근하기 위해서는 Rc<T>나 Arc<T>로 승격(upgrade() method를 호출)해 주어야만 한다.

Case#1 - Rc<T>를 이용해 상호 참조 시, memory leak 발생
Object A holds an Rc to Object B
Object B holds an Rc to Object A

Case#2 - 이 경우, 한쪽을 Weak<T>로 설정하여 문제 해결
Object A holds an Rc to Object B
Object B holds an Weak to Object A

4) RefCell<T>immuable로 선언된 struct 내의 특정 필드에 대해, runtime에 수정(mutable) 가능하게 하기 위한 용도(interior mutablility/내부 가변성라고 함)로 사용한다. 한편, RefCell<T>가 reference를 기반으로 한다면, 동일한 목적으로 사용되는 Cell<T>는 Copy를 기반으로 한다.

특이하게도 RefCell<T>는 빌림 검사를 compile time이 아니라, borrow() 또는 borrow_mut() 메서드를 호출하는 시점 즉, runtime에 진행한다. 또한, borrow() 메서드를 호출할 경우는 Ref<T>가, borrow_mut() 메서드를 호출할 경우에는 RefMut<T>가 결과로 return 된다. 주의할 사항은, 소유권 규칙을 위배하는 형태로 borrow()/borrow_mut() method를 호출될 경우, panic이 발생하게 된다는 것이다.
📌 borrow()/borrow_mut() 메서드를 호출한 결과로 Ref<T>, RefMut<T>가 나오는 것은, 마치 (뒤에서 설명할) Mutex(T)의 lock() 메서드 호출 결과로 MutexGuard<T>가 return되는 것과 유사하다.

________________________________________________________________________
struct MessageLogger { message_count: RefCell<usize>, history: RefCell<Vec<String>>, }

impl MessageLogger { fn new() -> Self { MessageLogger { message_count: RefCell::new(0), history: RefCell::new(Vec::new()), }

fn log(&self, message: &str) { let mut count = self.message_count.borrow_mut(); //RefMut<T>가 return됨.
*count += 1; //Deref coertion에 의해 내부 data에 직접 접근 가능(중요) self.history.borrow_mut().push(format!("#{}: {}", *count, message)); } }
________________________________________________________________________

5) Rc<T>와 RefCell<T>는 single thread 환경에서 사용하며, multi thread 환경에서는 Arc<T>와 Mutex<T>로 교체되어야 한다. 즉, Rc <-> Arc, RefCell <-> Mutex or RwLock<T>

6) Mutex<T>는 multi-thread 환경에서 RefCell<T>와 같은 기능(변수 수정 기능)이 필요할 때 사용하는 개념이다. Rust의 Mutex<T>는 언뜻 보기에 기존 programming language에서 보았던 mutex 개념과는 다소 차이가 있게 느껴진다. 먼저, lock을 얻기 위해 lock() method를 호출하는데, 그 결과로 (특이하게도) Result로 감싸진 MutexGuard<T> 즉, LockResult<MutexGuard<T>>를 얻게 된다. MutexGuard<T>는 smart pointer의 일종으로, 이것을 이용하여 T data에 직접 접근하게 되는데, T value가 LockResult<>로 감싸져 있으므로, 사용 전에 unwrap()해 주어야 한다. 한편, lock이 해제(release)되는 시점은 당연히 MutexGuard<T>가 scope를 벋어날 때이다.

________________________________________________________________________
let counter = Arc::new(Mutex::new(0u32));
let counter_clone_for_thread = Arc::clone(&counter);
{
      let mut num_guard = counter_clone_for_thread.lock().unwrap();   //MutexGuard<T> 추출
      *num_guard += 1;  //Deref coertion에 의해 data에 직접 접근 가능
}
________________________________________________________________________

7) RwLock<T>는 복수개의 reader와 1개의 writer 상황에서 Mutex<T>를 대신하여 사용되는 smart pointer로 readl lock인 read(), write lock인 write() method를 사용하여 lock을 획득하는 점이 Mutex<T>와는 다르다. Writer가 lock을 획득한 상태가 아니라면, 여러개의 Reader가 동시에 lock을 획득하는 것이 가능하다. RwLock<T>는 write 보다 read가 빈번한 상황에서 Mutex<T>를 대신하여 사용된다.
 
________________________________________________________

이 장에서는 양방향 연결 리스트 코드와 Mutex를 사용하는 thread 예제 코드를 통해서 이러한 개념들이 왜, 그리고 어떻게 사용되는지를 파악해 보고자 한다.

Keywords: Rc, Weak, RefCell, Box, Ownership, Borrow, Reference, struct, Generics, Trait, doubly linked list

[그림 5.2] 양방향 연결 리스트 - RefCell<T>, Rc<T> and Weak<T> 사용


<예제#1 - Rc, Weak, RefCell 등을 이용하여 양방향 연결 리스트 만들기>
_______________________________________________________________________
use std::rc::{Rc, Weak};
use std::cell::RefCell;

/// 이중 연결 리스트의 각 노드를 정의합니다.
/// `next`는 소유권을 공유하기 위해 `Rc`를 사용하고,
/// `prev`는 순환 참조를 방지하기 위해 `Weak`를 사용합니다.
struct Node<T> {
data: T,
next: Option<Rc<RefCell<Node<T>>>>,
prev: Option<Weak<RefCell<Node<T>>>>,
}

impl<T> Node<T> {
fn new(data: T) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Node {
data,
next: None,
prev: None,
}))
}
}

/// 이중 연결 리스트 구조체입니다.
/// 특정 값을 찾아 삭제하기 위해 T에 PartialEq 제약 조건이 필요할 수 있습니다.
pub struct DoublyLinkedList<T> {
head: Option<Rc<RefCell<Node<T>>>>,
tail: Option<Rc<RefCell<Node<T>>>>,
length: usize,
}

impl<T: PartialEq> DoublyLinkedList<T> {
/// 비어있는 새 리스트를 생성합니다.
pub fn new() -> Self {
DoublyLinkedList {
head: None,
tail: None,
length: 0,
}
}

/// 리스트의 길이를 반환합니다.
pub fn len(&self) -> usize {
self.length
}

/// 리스트의 맨 앞에 아이템을 추가합니다.
pub fn push_front(&mut self, data: T) {
let new_node = Node::new(data);

match self.head.take() {
Some(old_head) => {
new_node.borrow_mut().next = Some(Rc::clone(&old_head));
old_head.borrow_mut().prev = Some(Rc::downgrade(&new_node));
self.head = Some(new_node);
}
None => {
self.tail = Some(Rc::clone(&new_node));
self.head = Some(new_node);
}
}
self.length += 1;
}

/// 리스트의 맨 뒤에 아이템을 추가합니다.
pub fn push_back(&mut self, data: T) {
let new_node = Node::new(data);

match self.tail.take() {
Some(old_tail) => {
new_node.borrow_mut().prev = Some(Rc::downgrade(&old_tail));
old_tail.borrow_mut().next = Some(Rc::clone(&new_node));
self.tail = Some(new_node);
}
None => {
self.head = Some(Rc::clone(&new_node));
self.tail = Some(new_node);
}
}
self.length += 1;
}

/// 리스트의 맨 앞 아이템을 삭제하고 반환합니다.
pub fn pop_front(&mut self) -> Option<T> {
self.head.take().map(|old_head| {
self.length -= 1;
match old_head.borrow_mut().next.take() {
Some(new_head) => {
new_head.borrow_mut().prev = None;
self.head = Some(new_head);
}
None => {
self.tail = None;
}
}
Rc::try_unwrap(old_head).ok().unwrap().into_inner().data
})
}

/// 리스트의 맨 뒤 아이템을 삭제하고 반환합니다.
pub fn pop_back(&mut self) -> Option<T> {
self.tail.take().map(|old_tail| {
self.length -= 1;
match old_tail.borrow_mut().prev.take() {
Some(new_tail_weak) => {
let new_tail = new_tail_weak.upgrade().unwrap();
new_tail.borrow_mut().next = None;
self.tail = Some(new_tail);
}
None => {
self.head = None;
}
}
Rc::try_unwrap(old_tail).ok().unwrap().into_inner().data
})
}

/// 전달받은 값과 일치하는 첫 번째 노드를 삭제합니다.
/// 삭제 성공 시 true, 값을 찾지 못하면 false를 반환합니다.
pub fn remove(&mut self, data: T) -> bool {
let mut current = self.head.clone();

while let Some(node) = current {
if node.borrow().data == data {
// 노드를 찾았으므로 연결 수정
let prev_weak = node.borrow().prev.clone();
let next_rc = node.borrow().next.clone();

// 1. 이전 노드의 next를 현재 노드의 next로 연결
if let Some(ref weak) = prev_weak {
if let Some(prev_node) = weak.upgrade() {
prev_node.borrow_mut().next = next_rc.clone();
}
} else {
// 이전 노드가 없다면 현재 노드가 head임
self.head = next_rc.clone();
}

// 2. 다음 노드의 prev를 현재 노드의 prev로 연결
if let Some(ref next_node) = next_rc {
next_node.borrow_mut().prev = prev_weak;
} else {
// 다음 노드가 없다면 현재 노드가 tail임
self.tail = prev_weak.and_then(|w| w.upgrade());
//w.upgrade()는 Weak<T> -> Rc<T>로 upgrade하기 위해 사용
}

self.length -= 1;
return true;
}
// 다음 노드로 이동
current = node.borrow().next.clone();
}
false
}
}

fn main() {
let mut list = DoublyLinkedList::new();

println!("--- 초기 데이터 추가 ---");
list.push_back(10);
list.push_back(20);
list.push_back(30);
list.push_back(40);
// 상태: [10, 20, 30, 40]

println!("리스트 길이: {}", list.len());

println!("--- 중간 삭제 테스트 (20 제거) ---");
if list.remove(20) {
println!("20 삭제 성공");
}

println!("--- 경계 조건 삭제 테스트 (10, 40 제거) ---");
list.remove(10); // Head 삭제
list.remove(40); // Tail 삭제

println!("남은 리스트 길이: {}", list.len());
println!("마지막 남은 값 (예상 30): {:?}", list.pop_front());
println!("리스트가 비었는가? {:?}", list.pop_front().is_none());
}
_______________________________________________________________________

$ cargo run
--- 초기 데이터 추가 ---
리스트 길이: 4
--- 중간 삭제 테스트 (20 제거) ---
20 삭제 성공
--- 경계 조건 삭제 테스트 (10, 40 제거) ---
남은 리스트 길이: 1
마지막 남은 값 (예상 30): Some(30)
리스트가 비었는가? true


아래 예제는 multi-thread 환경에서 Mutex<T>와 MutexGuard<T>를 이용하여 shared data를 변경하는 내용을 보여주기 위해 만들어 보았다.

[그림 5.3] Mutex<T>와 MutexGuard<T>


<예제#2 - Thread, Mutex/MutexGuard를 사용하기>
_______________________________________________________________________
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

fn main() {
// 1. 데이터(0)를 Mutex로 감싸서 상호 배제(Mutual Exclusion)를 보장합니다.
// 2. 이를 다시 Arc(Atomic Reference Count)로 감싸서 여러 스레드가 소유권을 공유할 수 있게 합니다.
// (일반 Rc는 스레드 안전하지 않기 때문에 Arc를 사용해야 합니다.)
let counter = Arc::new(Mutex::new(0));
// 생성된 스레드 핸들을 저장할 벡터
let mut handles = vec![];

println!("--- 스레드 10개를 생성하여 카운트 시작 ---");

for i in 0..10 {
// Arc::clone을 사용하여 참조 카운트를 증가시키고, 각 스레드에 소유권을 복제해줍니다.
let counter_clone = Arc::clone(&counter);

let handle = thread::spawn(move || {
// 스레드 내부 로직
// [중요] 락 획득 시도
// .lock() 메서드는 Result<MutexGuard<T>, ...>를 반환합니다.
// unwrap()을 통해 얻은 'num' 변수가 바로 'MutexGuard<i32>' 타입입니다.
let mut num = counter_clone.lock().unwrap();

// MutexGuard는 스마트 포인터이므로, 역참조(*)를 통해 내부 데이터에 접근합니다.
*num += 1;
println!("스레드 #{} 작업 중... (현재 값: {})", i, *num);
// 잠시 대기 (경합 상황 시뮬레이션용)
thread::sleep(Duration::from_millis(10));

});
// [중요] 스코프가 끝나는 시점에 'num'(MutexGuard)변수가 Drop 됩니다.
// 이때 자동으로 락이 해제(Unlock)되어 다른 스레드가 접근할 수 있게 됩니다.

handles.push(handle);
}

// 모든 스레드가 작업을 마칠 때까지 메인 스레드 대기
for handle in handles {
handle.join().unwrap();
}

// 최종 결과 확인
// 여기서도 데이터 값을 읽기 위해 lock()을 걸어야 합니다.
println!("--- 작업 완료 ---");
println!("최종 결과: {}", *counter.lock().unwrap());
}
_______________________________________________________________________

$ cargo run

--- 스레드 10개를 생성하여 카운트 시작 ---
스레드 #1 작업 중... (현재 값: 1)
스레드 #2 작업 중... (현재 값: 2)
스레드 #0 작업 중... (현재 값: 3)
스레드 #3 작업 중... (현재 값: 4)
스레드 #4 작업 중... (현재 값: 5)
스레드 #5 작업 중... (현재 값: 6)
스레드 #6 작업 중... (현재 값: 7)
스레드 #7 작업 중... (현재 값: 8)
스레드 #8 작업 중... (현재 값: 9)
스레드 #9 작업 중... (현재 값: 10)
--- 작업 완료 ---
최종 결과: 10


Chapter6. Threads와 비동기 프로그래밍
Rust는 다른 programming language 처럼 당연히 thread(rust의 thread는 kernel thread와 1:1 matching함) 기능을 제공한다. 뿐만아니라 성능 향상을 목적으로 green thread 기반의 비동기 통신(예: tokio crate)도 함께 지원한다. 이번 장에서는 thread간의 message passing 기법을 적절히 활용하는 초간단 채팅 program(TCP client, server)을 하나 만들어 봄으로써 이러한 개념들이 왜, 그리고 어떻게 사용되는지를 파악해 보고자 한다. 참고로, client/server 간에는 AES-256-GCM 기반의 암호 통신(암호키는 ECDH로 실시간 교환 후 사용)을 하도록 하였다.

“Do not communicate by sharing memory; instead, share memory by communicating.”
(thread간의 통신 시 memory를 공유하는 방법 보다는 message passing 방법을 사용하자!)

Keywords: thread spawn, channel, async/await, tokio, trait object, tcp client/server, aes-256-gcm, ecdh



[그림 6.1] Thread channel 개념


[그림 6.2] 간단한 채팅 program 아키텍쳐


<예제 - TCP Client & Server 기반의 간단한 채팅 program>
_______________________________________________________________________
// src/ecdhkey.rs
// 이 모듈은 Elliptic Curve Diffie-Hellman (P-256) 키 교환 로직을 담당합니다.

use p256::{
ecdh::EphemeralSecret,
PublicKey,
};
//use rand_core::OsRng;
use crate::OsRng;
use hkdf::Hkdf;
use sha2::Sha256;

// 공개키를 주고받기 쉽도록 바이트 배열(SEC1 인코딩)로 정의
pub type PubKeyBytes = Vec<u8>;

pub struct EcdhKey {
secret: EphemeralSecret,
public_key: PublicKey,
}

impl EcdhKey {
// 1. 내 일회용 키 쌍(비공개키, 공개키) 생성
pub fn create() -> Self {
let secret = EphemeralSecret::random(&mut OsRng);
let public_key = PublicKey::from(&secret);
Self { secret, public_key }
}

// 내 공개키를 바이트로 변환 (상대방에게 전송용)
pub fn public_key_bytes(&self) -> PubKeyBytes {
// 압축된 형식(33bytes)으로 변환
self.public_key.to_sec1_bytes().to_vec()
}

// 2. 상대방의 공개키와 내 비밀키를 조합하여 공유 비밀(Shared Secret) 생성
// 생성된 비밀값으로 32바이트 AES 키를 유도하여 반환
pub fn derive_aes_key(self, other_pubkey_bytes: &[u8]) -> Result<[u8; 32], String> {
// 상대방 공개키 디코딩
let other_pk = PublicKey::from_sec1_bytes(other_pubkey_bytes)
.map_err(|_| "상대방 공개키 형식이 잘못되었습니다.".to_string())?;

// Diffie-Hellman 연산 수행
let shared_secret = self.secret.diffie_hellman(&other_pk);

// HKDF를 사용하여 공유 비밀에서 안전한 AES-256 키 추출
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.raw_secret_bytes());
let mut okm = [0u8; 32];
hkdf.expand(b"chat-handshake-v1", &mut okm)
.map_err(|_| "키 유도 실패".to_string())?;

Ok(okm)
}
}
_______________________________________________________________________

// src/bin/server.rs

use tokio::net::TcpListener;
use tokio::sync::broadcast;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use aes_gcm::{aead::{Aead, KeyInit}, Aes256Gcm, Nonce};
use aes_gcm::AeadCore;
use base64::{engine::general_purpose, Engine as _};
use rand::{rngs::OsRng, RngCore};

// ecdh.rs 파일을 모듈로 불러옵니다. (파일 경로가 ../ecdh.rs 라고 가정)
#[path = "../ecdh/ecdhkey.rs"]
mod ecdhkey;
//mod ecdh;
//use ecdh::ecdhkey;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
println!("🚀 채팅 서버(ECDH Key Exchange)가 시작되었습니다.");

// 1. 서버 실행 시, 채팅방 전용 랜덤 키(Room Key) 생성 (이 키로 대화함)
let mut room_key_bytes = [0u8; 32];
OsRng.fill_bytes(&mut room_key_bytes);
let room_key_vec = room_key_bytes.to_vec(); // 클론하여 태스크로 넘기기 위해 Vec 사용
// 서버 로그용 복호화 객체
let server_room_cipher = Aes256Gcm::new(&room_key_bytes.into());

let (tx, _rx) = broadcast::channel(100);

loop {
let (mut socket, addr) = listener.accept().await?;
println!("✨ 클라이언트 접속 시도: {}", addr);

let tx = tx.clone();
let mut rx = tx.subscribe();
let room_key = room_key_vec.clone();
let server_room_cipher = server_room_cipher.clone();

tokio::spawn(async move {
let (reader, mut writer) = socket.split();
let mut reader = BufReader::new(reader);

// ==========================================
// [ECDH 핸드셰이크 단계]
// ==========================================
// 1. 서버의 임시 키 쌍 생성
let server_ecdh = ecdhkey::EcdhKey::create();
let server_pub_b64 = general_purpose::STANDARD.encode(server_ecdh.public_key_bytes());
// 2. 클라이언트에게 서버 공개키 전송
if let Err(_) = writer.write_all(format!("{}\n", server_pub_b64).as_bytes()).await {
return;
}

// 3. 클라이언트로부터 공개키 수신 대기
let mut client_pub_line = String::new();
if reader.read_line(&mut client_pub_line).await.unwrap_or(0) == 0 {
return; // 연결 끊김
}
let client_pub_bytes = match general_purpose::STANDARD.decode(client_pub_line.trim()) {
Ok(b) => b,
Err(_) => return,
};

// 4. 핸드셰이크 키(Session Key) 유도
let session_key = match server_ecdh.derive_aes_key(&client_pub_bytes) {
Ok(k) => k,
Err(e) => {
eprintln!("키 교환 실패: {}", e);
return;
}
};

// 5. 유도된 세션 키로 'Room Key'를 암호화하여 클라이언트에게 전송
// (이 과정이 끝나면 이제 둘 다 Room Key를 알게 됨)
let session_cipher = Aes256Gcm::new(&session_key.into());
let nonce = Aes256Gcm::generate_nonce(&mut OsRng); // 96-bits unique
let encrypted_room_key = session_cipher.encrypt(&nonce, room_key.as_slice()).unwrap();
// 전송 포맷: Base64( Nonce + EncryptedRoomKey )
let mut payload = nonce.to_vec();
payload.extend_from_slice(&encrypted_room_key);
let payload_b64 = general_purpose::STANDARD.encode(payload);
if let Err(_) = writer.write_all(format!("{}\n", payload_b64).as_bytes()).await {
return;
}
println!("🔒 [{}] 핸드셰이크 완료 및 Room Key 전달됨", addr);


// ==========================================
// [메인 채팅 루프 (Room Key 사용)]
// ==========================================
let mut line = String::new();
loop {
tokio::select! {
// 메시지 수신 (암호화된 상태)
result = reader.read_line(&mut line) => {
if result.unwrap_or(0) == 0 { break; }

// 로깅: 서버도 Room Key가 있으므로 복호화해서 내용을 볼 수 있음
let trimmed = line.trim();
if let Ok(data) = general_purpose::STANDARD.decode(trimmed) {
if data.len() > 12 {
let (nonce, cipher) = data.split_at(12);
if let Ok(pt) = server_room_cipher.decrypt(Nonce::from_slice(nonce), cipher) {
println!("수신 [{}]: {}", addr, String::from_utf8_lossy(&pt));
}
}
}

// 브로드캐스트 (암호문 그대로 전달)
let msg = format!("[{}]: {}", addr, line);
let _ = tx.send((msg, addr));
line.clear();
}

// 다른 사람의 메시지 전송
result = rx.recv() => {
if let Ok((msg, other_addr)) = result {
if addr != other_addr {
let _ = writer.write_all(msg.as_bytes()).await;
}
}
}
}
}
println!("👋 클라이언트 접속 종료: {}", addr);
});
}
}

_______________________________________________________________________

// src/bin/client.rs

use tokio::net::TcpStream;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use aes_gcm::{aead::{Aead, KeyInit}, Aes256Gcm, Nonce};
use base64::{engine::general_purpose, Engine as _};
use rand::{rngs::OsRng, RngCore};

// ecdh.rs 파일을 모듈로 불러옵니다.
#[path = "../ecdh/ecdhkey.rs"]
mod ecdhkey;
//mod ecdh;
//use super::ecdh::ecdhkey;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut socket = TcpStream::connect("127.0.0.1:8080").await?;
println!("connecting...");

let (reader, mut writer) = socket.split();
let mut reader = BufReader::new(reader);

// ==========================================
// [ECDH 핸드셰이크 단계]
// ==========================================
// 1. 서버 공개키 수신
let mut server_pub_line = String::new();
reader.read_line(&mut server_pub_line).await?;
let server_pub_bytes = general_purpose::STANDARD.decode(server_pub_line.trim())?;

// 2. 내 임시 키 쌍 생성 및 공개키 전송
let client_ecdh = ecdhkey::EcdhKey::create();
let client_pub_b64 = general_purpose::STANDARD.encode(client_ecdh.public_key_bytes());
writer.write_all(format!("{}\n", client_pub_b64).as_bytes()).await?;

// 3. 세션 키 유도 (핸드셰이크 암호화용)
let session_key = client_ecdh.derive_aes_key(&server_pub_bytes)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
let session_cipher = Aes256Gcm::new(&session_key.into());

// 4. 암호화된 Room Key 수신 및 복호화
let mut room_key_line = String::new();
reader.read_line(&mut room_key_line).await?;
let room_key_packet = general_purpose::STANDARD.decode(room_key_line.trim())?;
let (nonce_bytes, ciphertext) = room_key_packet.split_at(12);
let room_key_bytes = session_cipher.decrypt(Nonce::from_slice(nonce_bytes), ciphertext)
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "Room Key 복호화 실패"))?;
// 5. 채팅용 암호화 객체 생성
// (이제부터 이 키로 모든 채팅 메시지를 암호화/복호화합니다)
let room_cipher = Aes256Gcm::new_from_slice(&room_key_bytes)
.map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "Invalid Key Size"))?;

println!("✅ 보안 핸드셰이크 성공! 안전한 채팅을 시작합니다.");

// ==========================================
// [메인 채팅 루프]
// ==========================================
let mut stdin = BufReader::new(tokio::io::stdin());
let mut socket_line = String::new();
let mut input_line = String::new();

loop {
tokio::select! {
// 메시지 수신 (Room Key로 복호화)
result = reader.read_line(&mut socket_line) => {
if result? == 0 { break; }

if let Some((sender, content)) = parse_message(&socket_line) {
if let Ok(data) = general_purpose::STANDARD.decode(content.trim()) {
if data.len() > 12 {
let (nonce, cipher) = data.split_at(12);
match room_cipher.decrypt(Nonce::from_slice(nonce), cipher) {
Ok(pt) => println!("{}: {}", sender, String::from_utf8_lossy(&pt)),
Err(_) => println!("{} (복호화 실패)", sender),
}
}
}
} else {
print!("{}", socket_line);
}
socket_line.clear();
}

// 메시지 전송 (Room Key로 암호화)
result = stdin.read_line(&mut input_line) => {
if result? == 0 { break; }
let plaintext = input_line.trim_end();
if !plaintext.is_empty() {
let mut nonce_bytes = [0u8; 12];
OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);

let ciphertext = room_cipher.encrypt(nonce, plaintext.as_bytes()).expect("Enc Fail");

let mut payload = nonce_bytes.to_vec();
payload.extend_from_slice(&ciphertext);
let b64 = general_purpose::STANDARD.encode(payload);
writer.write_all(format!("{}\n", b64).as_bytes()).await?;
}
input_line.clear();
}
}
}
Ok(())
}

fn parse_message(line: &str) -> Option<(&str, &str)> {
let parts: Vec<&str> = line.splitn(2, "]: ").collect();
if parts.len() == 2 {
Some((parts[0].trim_start_matches('['), parts[1]))
} else {
None
}
}
_______________________________________________________________________

$ vi Cargo.toml
[package]
name = "chatserver_aesgcm"
version = "0.1.0"
edition = "2024"

[[bin]]
name = "chatserver"
path = "src/bin/chat_server.rs"

[[bin]]
name = "chatclient"
path = "src/bin/chat_client.rs"

[dependencies]
tokio = { version = "1", features = ["full"] }
aes-gcm = "0.10"
base64 = "0.22"
rand = "0.8"
p256 = { version = "0.13", features = ["ecdh"] } # 타원곡선 암호
hkdf = "0.12"  # 키 유도 함수
sha2 = "0.10"  # 해시 함수
generic-array = "1"
_______________________________________________________

$ cargo build --bin chatserver
$ cargo build --bin chatclient

$ ./chatserver 
🚀 채팅 서버(ECDH Key Exchange)가 시작되었습니다.
✨ 클라이언트 접속 시도: 127.0.0.1:43612
🔒 [127.0.0.1:43612] 핸드셰이크 완료 및 Room Key 전달됨
수신 [127.0.0.1:43612]: hello
수신 [127.0.0.1:43612]: hi
수신 [127.0.0.1:43612]: how are you?

$ ./chatclient 
connecting...
✅ 보안 핸드셰이크 성공! 안전한 채팅을 시작합니다.
hello
hi
how are you?

_______________________________________________________________________

지금까지 Chapter 2 ~ 6에서 소개한 예제 program은 아래 link에서 확인 가능하다. 😎


정리하자면, 필자는 이번 posting을 통해서,
  1) 초심자들이 Rust를 study해야 하는 이유를 설명하고,
  2) Rust의 핵심 concept 5-6가지를 소개하므로써 Rust에 대한 대략적인 이해를 돕고자 하였다(보다 구체적인 사항은 관련 서적을 참조해야 한다).
  3) 또한, AI를 통해 코드를 생성해 봄으로써, 요즘 나오는 AI의 능력을 엿볼 수 있는 시간도 가져 보았다(앞서 제시한 예제 코드를 면밀히 분석해 보기 바란다).

정리를 해 놓고 보니, 이곳 저곳에서 부족한 내용이 많이 눈에 띈다. 😓 언제나 그렇듯이 부족한 부분은 다음 시간에 좀 더 보충할 것을 기약하며, 이번 posting을 마치고자 한다. 끝까지 읽어주셔서 감사드린다. 👋

May the source be with you!


Epilogue
(일반 대중 입장에서 볼 때) ChatGPT로 부터 시작된 AI 기술이 하루가 다르게 발전하고 있다. 어디서 부터 들여다 보아야 할지 감이 오지 않을 정도다. 😓

[그림 E.1] AI 혁명의 시작(1) - Attention Is All You Need 논문에서 등장한 Transformer 아키텍쳐


[그림 E.2] AI 혁명의 시작(2) - 다양한 LLM의 등장
(워낙 빠른 속도로 진행되다 보니, 그림에는 Gemini3 등 2025년에 등장한 내용이 포함되어 있지 않음)

[그림 E.3] AI 혁명의 시작(3) - from LLM to Multi Agents

하지만, 이러한 거대한 변화의 흐름에 대한 우려의 목소리가 있는 것도 사실이다.
역사학자이면서 베스트셀러 작가로도 유명한 유발 하라리 교수의 AI에 대한 경고 메시지(?)가 시사하는 바가 큰 것 같다. 💣💣💣


하지만, AI가 가져다 줄 미래에 대해 단순히 걱정만 하고 가만히 앉아만 있을 것인가 ? 

<무시무시한 AI와 개발자가 나가야할 방향>

"이제 개발자는 끝났다!" 정말일까 ?

아래에 기술한 내용 중에 오류(필자의 편견)가 있을 수도 있겠다. 필자 개인의 짧은 견해이니 감안해서 읽어 주시기 바란다. 😂

Arc::new(Mutex::new(1)) 에세이를 써 주고, 이미지를 만들어 주고, 동영상을 만들어 주고, ppt 문서를 만들어 주고, web page를 만들어 주고, 코드를 자동으로 알아서 만들어 주는 등, 예전에는 상상할 수 없었던 일들이 벌어지고 있다.

Arc::new(Mutex::new(2)) 이미 개발자들은 Github Copilot, ChatGPT, Claude, Gemini  등(사실 이것 보다 훨씬 더 많은 도구가 사용되고 있다)을 통해 개발 생산성을 높이는 방법을 접하고 있다.

Arc::new(Mutex::new(3)) 하지만, 생성형 AI(LLM - 거대 언어 모델)가 만들어낸 코드는 실제로 compile이 안되는 경우도 있고, 동작상에 문제가 있는 경우도 있다. 따라서 이를 사람이 검증해 보고, 문제점을 찾아 해결해 줘야만 한다.

Arc::new(Mutex::new(4)) 또한, 대규모의 시스템 설계 & 구현을 AI가 단독으로 수행하는 것은 아직 무리가 있다.

가령, Chrome web browser를 염두해 두고 "웹 브라우져를 만들어 줄래"라고 해 보지만, Chrome web browser 같은 것이 뚝딱 만들어지지는 않는다. Chrome web browser를 만들고자 한다면, 최초에 정확한 요구 사항을 입력해 줘야하는 것은 물론이고, 이후 AI가 최초에 생성한 코드를 기반으로, 문제점을 파악한 후 AI에게 수정 요청을 세밀하게 반복해 주는 과정을 거쳐야만 한다(이렇게 해도 정말로 chrome web browser가 만들어 질 수 있을지는 미지수다).

이는 마치, 주니어 개발자에게 자신이 해야 할 일에 대해서 정확히 설명해 주고, 중간 중간에 개발한 내용에 대해 의견(feedback)을 주어, 원하는 결과치를 얻을 수 있도록 하는 일련의 과정과 비슷해 보인다. 다만 차이가 있다면, 주니어 개발자와 일할 때는 그들의 기분 상태나 표정 변화를 신경써 가며 feedback을 주어야 하지만, AI에게는 그럴 필요가 없다는 점이다. 또한 주니어 개발자에게는 충분한 시간(하루 혹은 일주일)을 주고 기다려야 하지만, AI의 경우는 단 1분만에 결과치를 만들어 낼 수 있다는 점이 다를 뿐이다. 

아무튼, 일을 시킨 시니어 개발자가 해당 job에  대해 정확하게 파악하고 있지 못하다면, Chrome web browser 같은 것을 개발하는 것은 사실 상 불가능에 가깝다고 볼 수 있다.

Arc::new(Mutex::new(5)) 또 다른 예로, (본의 아니게 특정 회사를 언급해서 좀 그렇지만)Broadcom이나 Marvell 사에서 만든 switching chip(ASIC) 관련 SDK 문서를 참조하여, network switch s/w를 개발하는 중인데, 개발 중간에 막히는 부분을 AI에게 물어본다고 해서, AI가 과연 시원하게 원하는 결과를 알려 줄까 하는 것이다. 물론 관련 기술을 AI가 training 받은 상태라면 얘기가 달라질 지도 모르지만 말이다.

Arc::new(Mutex::new(6)) 또는 Smart Phone용 s/w를 만들다가 갑자기 죽어 버리는 문제가 있는데, 이를 AI에게 물어본다면 알아서 원인을 찾아줄 지 의문이 든다. 물론 증상을 구체적으로 파악한 후, 세밀하게 물어본다면 원하는 답에 근접한 뭔가를 얻을 수 있을지도 모른다.

Arc::new(Mutex::new(7)) 현업에서 개발자들이 하는 일들 중에는 단순한 업무도 있겠으나, 생각보다 복잡하고 난해하며 사람의 손길이 많이 필요한 것들이 많다고 보아야 할 것이다. AI는 개발자에게 분명 커다란 도움을 줄 수 있는 훌륭한 도구임에는 틀림없어 보이지만, 개발하려는 전체 job에 필요한 일부 모듈에 대해서 도움을 받을 수는 있어도, 전체 job을 AI에게만 의존하기에는 (아직은) 사실상 무리라고 보아야 할 것이다.

Arc::new(Mutex::new(8)) AI를 이용하여 원하는 코드를 만들 수는 있겠지만, 개발자가 그 내용을 제대로 이해하지 못한다면, 어느 순간엔가 AI가 덕지 덕지 만들어 놓은 프랑켄슈타인 같은 결과물과 마주한 채로, 더는 어찌해야 할지도 모르는 혼돈의 늪에서 허우적 댈지도 모를 일이다. 이것이 지금 이순간 programming language를 열심히 배워야 하는 이유인 것이다. 😋

Arc::new(Mutex::new(9)) AI 시대가 되면 더 이상 개발자의 역할이 사라질 것이라고 생각하겠지만, 실상은 그렇지 않으며, 오히려 code를 제대로 보고 이해할 수 있는 능력과 여러 분야를 두루 아우를 수 있는 경험치가 어느때 보다도 절실히 필요한 시점이라고 말할 수 있겠다.

이제 시작인 AI가 앞으로 어디까지 발전하게 될 지, 기대와 걱정이 공존하는 것이 사실이다. 하지만 지금은 막연하게 AI가 가져올 미래에 대해서 걱정만 할게 아니라, (이미 많은 개발자가 그렇게 하고 있는 것 처럼) 앞으로 AI를 최대한 잘 활용하여 보다 생산성을 높일 수 있는 방안을 모색해 보는 것이 맞지 않나 생각해 본다. 😎

이렇게 몇자 적어 보았지만, AI를 사용하면 할수록 점점 더 AI의 능력치(?)와 가능성에 압도되는 것도 사실이다. 또한, AI에 취해 생각하지 않는 copy & paste 개발자로 전락하지는 않을까 그것이 두렵다. 😂
__________________________________________


References
[9] Rust in action, Timothy Samuel McNamara, MANNING
[10] Programming Rust, Jim Blandy, Jason Orendorff, Oreilly
[11] https://murraytodd.medium.com/diving-deeper-into-rusts-option-result-solving-the-nesting-problem-2c648ac95e34
[12] https://www.apriorit.com/dev-blog/system-rust-asynch-vs-c-coroutines
[13] https://wikidocs.net/blog/@laniakea/1869/
[14] And, Google with Gemini ~


Slowboot