16-1. 심화 사례: 수명 기반 빌림과 상태 구조체 설계 🔴
학습 목표:
- C++의 위험한 생포인터(Raw Pointer) 저장 패턴을 Rust의 안전한 수명(Lifetime) 기반 빌림 시스템으로 전환하는 방법을 배웁니다.
- 모든 기능을 독점하는 '거대 객체(God Object)'를 작고 독립적인 조합 가능한 상태 구조체로 분해하여 유지보수성을 극대화합니다.
- 플러그인 시스템 등 확장이 필요한 시점에 **트레이트 객체(Trait Objects)**를 적절히 활용하는 기준을 세웁니다.
1. 프레임워크 포인터 저장 vs 컨텍스트 빌림
C++에서는 하위 모듈이 상위 프레임워크를 참조하기 위해 생포인터를 내부에 저장하곤 합니다. 이는 프레임워크가 먼저 소멸될 경우 댕글링 포인터를 유발하는 시한폭탄이 됩니다.
- C++ 방식:
m_pFramework포인터를 멤버 변수로 저장하고 무작정 신뢰함 (런타임 위험). - Rust 방식: 포인터를 저장하지 않고, 실행 시점(
execute)에 필요한 데이터만 담은 **컨텍스트(Context)**를 짧게 빌려와 사용함 (컴파일 타임 안전).
#![allow(unused)] fn main() { // Rust: 실행 시점에만 필요한 데이터를 짧게 빌려 쓰는 패턴 pub struct DiagContext<'a> { pub log: &'a mut EventLog, // 프레임워크의 로그 시스템 빌림 pub config: &'a Config, // 설정값 읽기 전용 빌림 } pub trait DiagModule { // 메서드 호출 시에만 컨텍스트를 넘겨받으므로 // 모듈 내부에 프레임워크 포인터를 영구 저장할 필요가 없습니다. fn execute(&mut self, ctx: &mut DiagContext) -> anyhow::Result<()>; } }
2. 거대 객체(God Object)의 해체와 조합(Composition)
시간이 흐를수록 모든 기능을 다 가진 거대한 클래스는 리팩터링이나 단위 테스트가 불가능해지는 '스파게티'가 됩니다.
- 해결책: 관련 있는 필드끼리 묶어 별도의 상태 구조체로 분리하고, 메인 프레임워크는 이들을 조합하여 관리합니다.
#![allow(unused)] fn main() { // 기능별로 응집된 작은 상태 구조체들 struct GpuState { /* GPU 온도, 부하 등 */ } struct NetworkState { /* 패킷 카운트, 지연 시간 등 */ } struct HealthMonitor { /* 시스템 전체 건전성 지표 */ } // 프레임워크는 이들을 조립(Composition)하는 역할만 수행 struct DiagFramework { gpu: GpuState, net: NetworkState, monitor: HealthMonitor, } }
- 테스트 용이성:
GpuState만 따로 떼어내서 목(Mock) 데이터를 넣어 테스트할 수 있습니다. - 영향도 분리: GPU 로직을 수정하다가 실수로 네트워크 엔진을 망가뜨리는 사고를 방지합니다.
- 가독성:
self.gpu.temp와 같이 데이터의 소속과 의도가 명확해집니다.
3. 트레이트 객체(dyn Trait)의 황금률
모든 것을 열거형(enum)으로 처리할 수는 없습니다. 새로운 모듈이 지속적으로 추가되는 플러그인 시스템 같은 경우엔 동적 디스패치(dyn Trait)가 정답입니다.
| 상황 | 권장 패턴 | 결정 이유 |
|---|---|---|
| 도형 (원, 사각형) | enum Shape | 종류가 한정되어 있고 성능이 최우선일 때 |
| 진단 모듈들 | Box<dyn DiagModule> | 외부에서 새로운 로직을 계속 추가(확장)해야 할 때 |
| 테스트 시 DB | &dyn Database | 실제 DB와 테스트용 Mock DB를 교체해야 할 때 |
💡 실무자의 조언: "직역의 유혹을 뿌리치세요"
C++의 vector<unique_ptr<Base>>를 보고 무의식적으로 Vec<Box<dyn Trait>>를 쓰지 마세요. "이 타입들이 정말로 무한히 확장되어야 하는가?"라고 자문해 보세요. 만약 정해진 몇 가지 타입뿐이라면 **열거형(Enum)**이 성능과 메모리 효율, 안전성 면에서 압승입니다.
📌 요약
- 컨텍스트 빌림 패턴으로 댕글링 포인터의 싹을 자르세요.
- 거대 클래스는 작은 상태 구조체들의 조합으로 분해하세요.
- **
dyn Trait**는 꼭 확장이 필요한 시점에만 선별적으로 도입하세요. - 데이터와 행위의 소유권을 명확히 분리하는 것이 Rust 설계의 핵심입니다.