Rust 배우기 – 04. 객체지향의 해체: 러스트에는 클래스가 없다
단호하게 말씀드립니다. 러스트(Rust)에는 class 키워드가 존재하지 않습니다. 객체지향 프로그래밍(OOP)의 핵심인 상속(extends)의 개념도 없습니다.
Java, Python, C++ 등 전통적인 OOP 언어는 데이터(상태)와 메서드(행위)를 하나의 덩어리인 클래스로 묶어서 관리합니다. 하지만 러스트는 이 둘을 철저하고 엄격하게 분리합니다.
1. 데이터와 행위의 철저한 분리 (struct + impl)
러스트는 데이터와 로직이 뒤섞이는 것을 구조적으로 차단합니다.
상태 (Data) 정의 먼저 struct를 사용해 에이전트가 가질 데이터만을 정의합니다. 이 블록 내부에는 어떠한 실행 로직도 포함될 수 없습니다.
|
1 2 3 4 5 6 |
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">Agent</span> { id: <span class="hljs-type">Option</span><Uuid>, config: Settings, client: reqwest::Client, } |
행위 (Behavior) 정의 데이터를 조작하는 메서드들은 impl(Implementation) 블록을 완전히 분리하여 작성합니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<span class="hljs-keyword">impl</span> <span class="hljs-title class_">Agent</span> { <span class="hljs-comment">// 1. 연관 함수 (Associated Function) - 클래스의 정적(Static) 메서드와 동일</span> <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">new</span>(config: Settings) <span class="hljs-punctuation">-></span> <span class="hljs-type">Result</span><<span class="hljs-keyword">Self</span>> { <span class="hljs-title function_ invoke__">Ok</span>(<span class="hljs-keyword">Self</span> { id: <span class="hljs-literal">None</span>, config, client: reqwest::Client::<span class="hljs-title function_ invoke__">new</span>(), }) } <span class="hljs-comment">// 2. 인스턴스 메서드 (데이터 변경/소유권 이전)</span> <span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">run</span>(<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-></span> <span class="hljs-type">Result</span><()> { <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">register</span>().<span class="hljs-keyword">await</span>?; <span class="hljs-comment">// ... 생략 ...</span> } <span class="hljs-comment">// 3. 인스턴스 메서드 (읽기 전용)</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">send_heartbeat</span>(&<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-></span> <span class="hljs-type">Result</span><()> { <span class="hljs-keyword">let</span> <span class="hljs-variable">id</span> = <span class="hljs-keyword">self</span>.id.<span class="hljs-title function_ invoke__">context</span>(<span class="hljs-string">"Agent is not registered..."</span>)?; <span class="hljs-comment">// ... 생략 ...</span> } } |
이러한 분리는 메모리 레이아웃을 투명하게 만들고, 데이터의 크기와 형태를 예측 가능하게 합니다. 그렇다면 기존 클래스 문법에 익숙한 개발자들은 이 구조를 어떻게 읽어야 할까요?
2. 러스트의 클래스 대체 문법 분석
위 impl Agent 블록을 객체지향의 클래스와 비교하면 러스트만의 명시적인 규칙이 드러납니다.
생성자(Constructor)의 부재 러스트에는 new라는 특별한 키워드가 없습니다. 단지 pub fn new()라는 일반 함수를 만들어 생성자 역할을 관례적으로 수행하게 할 뿐입니다. 이 함수는 매개변수로 self를 받지 않으므로, Java의 static 메서드나 Python의 @staticmethod와 완벽히 동일하게 동작합니다. 이를 러스트에서는 연관 함수(Associated Function)라고 부릅니다. 반환 타입의 Self(대문자)는 Agent 구조체 자체를 의미하는 타입 별칭입니다.
암묵적 this의 배제, 명시적 self 클래스의 인스턴스 메서드는 객체 자신을 가리키는 this를 암묵적으로 가집니다. 러스트는 이 암묵성을 허용하지 않습니다. 메서드가 인스턴스 데이터에 접근하려면 첫 번째 매개변수로 반드시 self(소문자)를 명시해야 합니다.
send_heartbeat(&self): 이 메서드는 에이전트의 데이터를 읽기만 하겠다는 명시적 선언입니다 (공유 참조).run(mut self): 이 메서드는 에이전트의 데이터를 변경하거나 소유권을 완전히 가져가겠다는 선언입니다 (가변 참조 및 소유권 이전).
클래스의 구조를 해체하고 명시성을 확보했습니다. 그렇다면 객체지향의 또 다른 축인 ‘상속’은 어떻게 해결할까요?
3. 상속(Inheritance)의 종말과 트레잇(Trait)의 도입
“부모 클래스의 기능을 물려받는 상속은 어떻게 합니까?” 러스트는 객체지향의 가장 큰 골칫거리인 깊은 상속 트리(Deep Inheritance Tree)를 치명적인 디자인 결함으로 간주합니다. 상속 대신 트레잇(Trait)이라는 개념을 도입하여, “어떤 행동을 할 수 있는가(Interface)”만 정의하고 이를 조합(Composition)하는 방식을 취합니다.
증명된 안정성 (Evidence)
러스트는 클래스라는 거대한 모놀리식(Monolithic) 덩어리 대신, 다음 세 가지 요소를 독립적으로 다룹니다.
- 순수한 데이터 (
struct) - 데이터 전용 행위 (
impl) - 공통 행위 규약 (
impl Trait for Struct)
이러한 수평적 조합은 상속으로 인한 예기치 못한 사이드 이펙트를 원천 차단합니다.
스택의 지배자: 러스트의 원시 타입(Primitive Types) 해부
파이썬이나 루비처럼 모든 것을 객체(Object)로 취급하는 언어의 환상에서 벗어나십시오. 러스트의 원시 타입은 객체가 아닙니다. 컴파일 타임에 크기가 고정되며, CPU 레지스터와 스택(Stack) 메모리에 직접 꽂히는 가장 빠르고 순수한 데이터 블록입니다.
클래스조차 배제하는 러스트가 메모리를 어떻게 1바이트 단위로 통제하는지, 그 기초 공사인 원시 타입(Primitive Type)의 두 가지 축—스칼라(Scalar)와 컴파운드(Compound)—을 분석하겠습니다.
1. 스칼라 타입 (Scalar Types): 단일 값의 최소 단위
스칼라 타입은 단 하나의 값을 표현합니다. 러스트는 시스템 프로그래밍 언어답게 데이터의 크기를 명시적으로 강제합니다.
- 정수형 (Integer):
i8,u8부터i128,u128까지 메모리 크기를 타입명에 직접 명시합니다.- Metric: 운영체제 아키텍처(32bit/64bit)에 따라 크기가 변하는
isize와usize는 주로 컬렉션의 인덱싱이나 메모리 주소 계산에 사용됩니다.
- Metric: 운영체제 아키텍처(32bit/64bit)에 따라 크기가 변하는
- 부동 소수점 (Float): IEEE-754 표준을 따르는
f32와f64를 제공합니다. 모호성을 없애기 위해 기본 타입은 항상f64로 추론됩니다. - 논리형 (Boolean): 1바이트 크기를 가지는
bool(true/false)입니다. - 문자형 (Char) – 주의: C언어의
char는 1바이트지만, 러스트의char는 4바이트입니다. ASCII뿐만 아니라 유니코드 스칼라 값을 완벽히 표현하기 위해 공간을 희생하고 정확도를 택했습니다.
2. 컴파운드 타입 (Compound Types): 힙(Heap) 없는 그룹화
여러 값을 하나의 타입으로 묶지만, 동적 메모리 할당(Heap)을 절대 발생시키지 않는 스택 기반의 복합 타입입니다.
- 튜플 (Tuple): 서로 다른 타입의 값을 하나로 묶습니다. 길이가 컴파일 타임에 고정됩니다.
let data: (i32, f64, u8) = (500, 6.4, 1);- 함수가 여러 값을 반환해야 할 때 오버헤드 없이 스택을 통해 데이터를 전달하는 핵심 수단입니다.
- 배열 (Array): 같은 타입의 값을 하나로 묶습니다. 크기가 늘어나거나 줄어들지 않습니다.
let list: [i32; 5] = [1, 2, 3, 4, 5];- 동적 크기가 필요하다면 원시 타입이 아닌 힙에 할당되는
Vec<T>(벡터)를 사용해야 합니다.
3. 핵심 동작 원리: Copy 트레잇과 소유권 면제
러스트의 가장 악명 높은 진입 장벽은 ‘소유권(Ownership)’과 ‘이동(Move)’입니다. 그러나 원시 타입을 다룰 때는 이 제약이 느껴지지 않습니다. 그 이유는 무엇일까요?
모든 원시 타입은 Copy 트레잇(Trait)이 기본적으로 구현되어 있습니다. 스택에 저장되는 이 작고 고정된 크기의 데이터들은, 값을 다른 변수에 대입할 때 소유권을 이전(Move)하는 것보다 메모리를 그대로 비트 단위로 복사(Copy)하는 것이 압도적으로 빠르기 때문입니다. 할당 해제를 걱정할 힙(Heap) 영역의 데이터가 없으므로 이중 해제(Double Free) 버그도 발생하지 않습니다.
증명된 성능 (Evidence)
원시 타입은 메모리 할당자(Allocator)를 거치지 않습니다. O(1)의 속도로 스택(Stack) 프레임에 기록되며, CPU 캐시 라인(Cache Line)에 완벽하게 적재됩니다. 파이썬 정수 객체가 타입 정보, 참조 카운트 등을 위해 28바이트 이상을 낭비할 때, 러스트의 u32는 정확히 4바이트의 메모리만 점유합니다. 이것이 시스템 프로그래밍에서 예측 가능한 성능(Predictable Performance)을 달성하는 첫 번째 지표입니다.
무관용 원칙: 러스트의 사칙연산과 타입 엄격성
컴파일러가 여러분의 의도를 알아서 ‘짐작’해 주길 바라는 습관은 버리십시오. 자바스크립트나 C에서는 정수와 실수를 더하면 알아서 변환되지만, 러스트의 사칙연산은 데이터 타입의 불일치를 컴파일 단계에서 철저히 박살 냅니다.
원시 타입들이 어떻게 연산되는지, 그리고 러스트가 런타임 버그를 막기 위해 연산 과정에 어떤 제약을 걸어두었는지 코드로 증명하겠습니다.
사칙연산 샘플 코드
아래는 기본적인 더하기, 빼기, 곱하기, 나누기, 나머지 연산을 수행하는 코드입니다. 데이터 타입이 다른 변수 간의 연산 시 발생하는 문제와 해결책을 포함했습니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<span class="hljs-keyword">fn</span> <span class="hljs-title function_">main</span>() { <span class="hljs-comment">// 1. 정수형 사칙연산 (기본 추론: i32)</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">a</span> = <span class="hljs-number">10</span>; <span class="hljs-keyword">let</span> <span class="hljs-variable">b</span> = <span class="hljs-number">3</span>; <span class="hljs-keyword">let</span> <span class="hljs-variable">sum</span> = a + b; <span class="hljs-comment">// 13</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">difference</span> = a - b; <span class="hljs-comment">// 7</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">product</span> = a * b; <span class="hljs-comment">// 30</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">quotient</span> = a / b; <span class="hljs-comment">// 3 (소수점 버림)</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">remainder</span> = a % b; <span class="hljs-comment">// 1</span> <span class="hljs-comment">// 2. 부동소수점 연산 (기본 추론: f64)</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">x</span> = <span class="hljs-number">10.0</span>; <span class="hljs-keyword">let</span> <span class="hljs-variable">y</span> = <span class="hljs-number">3.0</span>; <span class="hljs-keyword">let</span> <span class="hljs-variable">float_div</span> = x / y; <span class="hljs-comment">// 3.3333333333333335</span> <span class="hljs-comment">// 3. 타입 불일치와 명시적 강제 변환 (Casting)</span> <span class="hljs-comment">// ❌ 에러 발생 코드: let error = a + x; </span> <span class="hljs-comment">// (컴파일러: expected integer, found floating-point number)</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">mixed_math</span> = (a <span class="hljs-keyword">as</span> <span class="hljs-type">f64</span>) + x; <span class="hljs-comment">// 20.0 (정수를 f64로 명시적 변환)</span> <span class="hljs-built_in">println!</span>(<span class="hljs-string">"정수 나눗셈: {}"</span>, quotient); <span class="hljs-built_in">println!</span>(<span class="hljs-string">"실수 나눗셈: {}"</span>, float_div); <span class="hljs-built_in">println!</span>(<span class="hljs-string">"혼합 연산: {}"</span>, mixed_math); } |
이 단순한 코드에 숨겨진 러스트의 3가지 핵심 설계 원칙을 해부합니다.
1. 암묵적 형변환(Implicit Type Casting)의 완전한 차단
let error = a + x;는 타 언어에서 자연스럽게 20.0을 반환합니다. 하지만 러스트는 expected i32, found f64라는 컴파일 에러를 던지며 빌드를 멈춥니다.
서로 다른 타입이 메모리 상에서 차지하는 크기와 표현 방식이 완전히 다르기 때문입니다. 러스트는 성능 저하와 정밀도 손실을 개발자 몰래 발생시키지 않습니다. 연산을 원한다면 반드시 as 키워드를 사용해 a as f64로 개발자가 명시적으로 캐스팅해야 합니다.
2. 정수 나눗셈의 철저한 절사 (Truncation)
정수 10을 3으로 나누면 3.333...이 아니라 3이 됩니다. 이것은 반올림(Round)이 아니라 소수점 이하를 완전히 버리는 절사(Truncate)로 향하는 제로(0) 규칙을 따릅니다. 정확한 소수점 결과를 원한다면 데이터 자체가 처음부터 f32나 f64로 선언되어 있어야 합니다.
3. 오버플로우(Overflow) 방어 메커니즘
코드에는 보이지 않지만 러스트의 사칙연산이 가진 가장 강력한 무기입니다. 만약 u8(0~255) 타입 변수가 255를 가지고 있을 때 + 1을 하면 어떻게 될까요?
- Debug 모드 (
cargo build): 연산 시 즉시 Panic(프로그램 강제 종료)을 발생시킵니다. 데이터 오염을 런타임에 방치하지 않습니다. - Release 모드 (
cargo build --release): 패닉을 일으키지 않고0으로 돌아가는 2의 보수 래핑(Two’s complement wrapping)을 수행합니다. - Metric: 만약 의도적으로 래핑, 포화(Saturating), 혹은 오버플로우 여부를 체크해야 한다면
+연산자 대신checked_add(),saturating_add()같은 명시적 메서드를 강제합니다.
증명된 로직 (Evidence)
러스트의 +나 / 같은 연산자는 마법이 아닙니다. 내부적으로는 표준 라이브러리의 std::ops::Add, std::ops::Div 같은 트레잇(Trait)의 구현체일 뿐입니다. i32의 Add 트레잇은 오직 i32만을 파라미터로 받도록 하드코딩되어 있습니다. 이 철저한 트레잇 기반 연산이 타입 불일치로 인한 런타임 크래시를 컴파일 타임에 100% 제거합니다.
경계의 통제: 러스트의 네임스페이스와 모듈 시스템
착각하지 마십시오. 러스트에는 namespace라는 키워드가 존재하지 않습니다. C++처럼 파일 전역에 임의의 네임스페이스를 씌우는 느슨한 방식은 러스트의 철학과 맞지 않습니다. 대신, 파일 시스템과 1:1로 매칭되며 철저한 접근 제어(Visibility)를 강제하는 ‘모듈(Module) 트리’ 시스템을 사용합니다.
러스트에서 네임스페이스의 역할은 mod 키워드가 수행합니다. 코드의 충돌을 막고, 캡슐화를 강제하며, 의존성을 격리하는 러스트의 네임스페이스 선언과 사용법을 해부하겠습니다.
1. 네임스페이스의 선언: mod 키워드
러스트에서 네임스페이스를 만드는 가장 기본적인 방법은 mod 블록을 선언하는 것입니다. 이는 트리 구조의 노드(Node) 역할을 합니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<span class="hljs-comment">// 1. 인라인 모듈 선언 (한 파일 내에서 네임스페이스 분리)</span> <span class="hljs-keyword">mod</span> database { <span class="hljs-comment">// database 네임스페이스 내부</span> <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">connect</span>() { <span class="hljs-built_in">println!</span>(<span class="hljs-string">"DB 연결 완료"</span>); } <span class="hljs-comment">// 중첩 네임스페이스 (Sub-module)</span> <span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> models { <span class="hljs-keyword">pub</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">User</span> { <span class="hljs-keyword">pub</span> id: <span class="hljs-type">u32</span>, } } } <span class="hljs-comment">// 2. 파일/디렉토리 기반 모듈 선언</span> <span class="hljs-comment">// 컴파일러에게 "network.rs" 또는 "network/mod.rs" 파일을 </span> <span class="hljs-comment">// network라는 네임스페이스로 컴파일하라고 지시합니다.</span> <span class="hljs-keyword">mod</span> network; |
C++과 결정적으로 다른 점은, 파일 자체가 암묵적인 네임스페이스(모듈)가 된다는 것입니다. network.rs 파일을 만들면 그 안의 코드는 자동으로 network:: 네임스페이스에 종속됩니다.
2. 가시성 제어: 프라이버시 우선주의 (Privacy by Default)
러스트의 네임스페이스가 가진 가장 강력하고 엄격한 특징은 모든 요소가 기본적으로 비공개(Private)라는 것입니다. 네임스페이스를 선언했다고 해서 외부에서 마음대로 접근할 수 없습니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<span class="hljs-keyword">mod</span> server { <span class="hljs-comment">// ❌ pub이 없으므로 server 네임스페이스 외부에서 접근 불가</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">internal_logic</span>() { <span class="hljs-built_in">println!</span>(<span class="hljs-string">"내부 로직"</span>); } <span class="hljs-comment">// ✅ pub 키워드로 명시적 공개 (Public)</span> <span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">start</span>() { <span class="hljs-title function_ invoke__">internal_logic</span>(); <span class="hljs-comment">// 같은 네임스페이스 내부에서는 접근 가능</span> <span class="hljs-built_in">println!</span>(<span class="hljs-string">"서버 시작"</span>); } } <span class="hljs-keyword">fn</span> <span class="hljs-title function_">main</span>() { <span class="hljs-comment">// server::internal_logic(); // 컴파일 에러: private function</span> server::<span class="hljs-title function_ invoke__">start</span>(); <span class="hljs-comment">// 정상 작동</span> } |
pub 키워드를 붙이지 않으면 외부 네임스페이스에서는 절대 호출할 수 없습니다. 이는 개발자의 실수로 내부 구현(Implementation Details)이 외부에 노출되는 것을 컴파일 타임에 원천 차단합니다.
3. 네임스페이스의 사용: 경로와 use 바인딩
선언된 네임스페이스의 요소를 사용할 때는 :: (경로 분리자)를 사용합니다. 매번 긴 경로를 적는 것을 방지하기 위해 use 키워드로 현재 스코프에 바인딩(Import)합니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<span class="hljs-comment">// 1. 절대 경로 사용 (crate는 프로젝트의 루트를 의미)</span> crate::database::models::User; <span class="hljs-comment">// 2. use 키워드를 통한 네임스페이스 바인딩 (Import)</span> <span class="hljs-keyword">use</span> crate::database::models::User; <span class="hljs-keyword">use</span> std::collections::HashMap; <span class="hljs-comment">// 표준 라이브러리 네임스페이스</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">main</span>() { <span class="hljs-comment">// 긴 경로 없이 바로 User 구조체 사용 가능</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">new_user</span> = User { id: <span class="hljs-number">1</span> }; <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">map</span> = HashMap::<span class="hljs-title function_ invoke__">new</span>(); } |
crate::: 프로젝트 루트(최상위 네임스페이스)부터 시작하는 절대 경로.super::: 현재 네임스페이스의 부모 네임스페이스로 한 단계 올라가는 상대 경로 (파일 시스템의../와 동일).self::: 현재 네임스페이스를 가리키는 상대 경로 (파일 시스템의./와 동일).
증명된 캡슐화 (Evidence)
러스트의 mod와 pub 조합은 단순한 이름 충돌 방지를 넘어선 설계적 도구입니다. 의존성이 얽히는(Spaghetti Code) 현상을 막고, 공개 API의 표면적(Surface Area)을 최소화합니다. 컴파일러는 이 모듈 트리를 분석하여, 사용되지 않는(Dead Code) private 함수들을 정확히 찾아내어 최적화 시 완전히 제거(Strip)해버립니다. 이는 바이너리 크기를 줄이고 성능을 극대화하는 제로 코스트 추상화(Zero-cost Abstraction)의 기반이 됩니다.
