Rust 프로그래밍 14장 - 누적에서 계산으로
객체지향방식은 당연하게도 객체에 집중하며, 그 객체가 가지고 있는 메소드와 속성에 집중합니다. 객체안에는 데이터가 보관되며,변경이 있을 경우 그 데이터를 메소드를 통해 업데이트 해주면서 코드가 진행됩니다.
함수형방식은 데이터의 input/output와 그것을 행하는 함수에 집중합니다. 함수는 상태를 갖지 않습니다.
각각의 장단점이 있습니다.
현재 세상은 객체지향방식에서 함수형방식으로의 이동이 발생하고 있습니다.
하지만 그것은 단지 보조재로써의 역할 정도에 머물러 있을뿐 대중적으로는 여전히 객체지향방식이 양으로 압도하긴 합니다.
(그나마 자바스크립트가 객체지향방식이 비교적 아니라서 ㅎㅎ.. 코틀린이 분투하지만 여전히 코틀린도 함수형 느낌보다는 객체지향 느낌으로 더 사용되는 듯 하며, 스칼라,클로저,하스켈같은 것들의 점유율은 극히 적기 때문에..양에선 차이가 많이 납니다)
이 글에서는 객체지향방식의 누적되는 방식으로의 코드에서 반환하는 함수형 느낌의 코드로 변환되는 과정을 지켜보며
말보다는 코드로써 이 방식에 대해서 느껴 봅니다.
“누적하는 객체에서 반환으로” 라는 제목과 컨텐츠는 Java to Kotlin이라는 명저에서 가져왔으며,
이 글에서는 Kotlin 대신 Rust로 설명합니다. (Kotlin은 제 주력이라 질려서)
목차
문제 정의
구조체 및 데이터 모델 정의
누적 방식 구현
함수형 접근 방식 구현
누적 방식에서 발생할 수 있는 오류
불변 데이터를 사용하여 해결하는 방법
결론
1. 문제 정의
여행사의 특정 여행상품에서 총비용이 얼마나 나오는지 계산하는 기능을 어떻게 구현 해야 할 까요?
여행상품은 여행경로(Route)가 있으며 경로에는 여러Location과 그 안의 Accommodation과 Transportation을 포함합니다. 그 모든 비용을 계산해야 합니다.
2. 구조체 및 데이터 모델 정의
2.1. Money와 Currency
Money는 금액과 통화 단위를 나타내는 구조체입니다.
#[derive(Clone, Debug, PartialEq, Eq, Hash)] // HashMap을 사용하려면 Eq와 Hash 필요
enum Currency {
USD,
EUR,
GBP,
}
#[derive(Clone, Debug, PartialEq)] //복제,디버깅표식,비교 가능
struct Money {
amount: f64, // rust_decimal 혹은 i64,u64를 사용하는게 나음
currency: Currency,
}
impl Money {
fn new(amount: f64, currency: Currency) -> Self {
Money { amount, currency }
}
fn add(&mut self, other: &Money) {
if self.currency == other.currency {
self.amount += other.amount;
} else {
panic!("Cannot add different currencies");
}
}
fn to_money(&self) -> Money {
Money::new(self.amount, self.currency.clone())
}
}
clone을 derive해주면 clone 메소드를 사용할 수 있습니다. clone을 사용하면 f64는 copy(c언어에서 memcpy 같은)가 되고 Currency는 clone이 호출됩니다.
&mut 를 통해 자신의 값을 변경 할 수 있습니다.
other: &Money를 통해 참조를 얻어옵니다.그냥 Money라고 쓰면 이동되어 기존것이 없어집니다. (Rust언어 처음 하는 분이 가장 골때리는게 Move입니다. 대부분 다른 언어는 이 개념이 없거든요 C++ 말고. Rust언어에서 처음 쓰면 Move되서 기존것이 없어지는 마법이 때문에 당황하실듯)
2.2. Location, Accommodation, Transportation
Location은 각 여행지의 이름과 그에 관련된 숙박비, 교통비를 포함합니다.
#[derive(Clone)]
struct Accommodation {
cost: Money,
}
impl Accommodation {
fn add_cost(&self, calculator: &mut CostSummaryCalculator) {
calculator.add_cost(self.cost.clone()); // clone 안해주면 자기것이 도망갑니다.
}
}
#[derive(Clone)]
struct Location {
name: String,
accommodations: Vec<Accommodation>,
transportation_cost: Money,
}
impl Location {
fn add_costs_to(&self, calculator: &mut CostSummaryCalculator) {
for accommodation in &self.accommodations {
accommodation.add_cost(calculator);
}
calculator.add_cost(self.transportation_cost.clone());
}
}
2.3. Route
Route는 여러 Location을 포함하고, 각 Location에 대해 비용을 계산할 수 있도록 합니다.
#[derive(Clone)]
struct Route {
locations: Vec<Location>,
}
impl Route {
fn add_costs_to(&self, calculator: &mut CostSummaryCalculator) {
for location in &self.locations {
location.add_costs_to(calculator);
}
}
}
음 뭐 여기까진 별거 없습니다.
아…&self.locations 이런게 좀 걸리긴 하네요. 이것도 Rust의 특징인데 self.locations로 하면 어떻게 될까요?
self.locations는 다신 사용할 수 없게 됩니다. 날라가서 도망쳐요 Move Move
반드시 참조로 전달하여 소유권 이동을 방지하여야 합니다. (소유권을 이동시키고 싶을때 제외하고)
3. 누적 방식 구현
누적 방식은 각 Location의 Accommodation과 Transportation 비용을 순차적으로 더하는 방식입니다. 아래 예시에서는 비용을 누적하는 CostSummaryCalculator 구조체를 구현하고, Route에 포함된 Location의 모든 비용을 계산하는 방법을 보여줍니다.
3.1. CostSummaryCalculator
비용을 누적하는 CostSummaryCalculator 구조체입니다.
#[derive(Clone)]
struct CostSummaryCalculator {
currency_totals: std::collections::HashMap<Currency, Money>,
}
impl CostSummaryCalculator {
fn new() -> Self {
CostSummaryCalculator {
currency_totals: std::collections::HashMap::new(),
}
}
// 비용 추가
fn add_cost(&mut self, cost: Money) {
self.currency_totals
.entry(cost.currency.clone())
.or_insert(Money::new(0.0, cost.currency.clone()))
.add(&cost);
}
// 총합 계산
fn summarise(&self) -> CostSummary {
let total = self.currency_totals.values().cloned().fold(Money::new(0.0, Currency::USD), |acc, cost| {
let mut acc = acc.clone();
acc.add(&cost);
acc
});
CostSummary { total }
}
// 리셋
fn reset(&mut self) {
self.currency_totals.clear();
}
}
3.2. CostSummary
비용 요약을 반환하는 구조체입니다.
#[derive(Clone)]
struct CostSummary {
total: Money,
}
3.3. 사용 예시
fn main() {
let accommodation1 = Accommodation {
cost: Money::new(100.0, Currency::USD),
};
let accommodation2 = Accommodation {
cost: Money::new(150.0, Currency::USD),
};
let location1 = Location {
name: "Paris".to_string(),
accommodations: vec![accommodation1],
transportation_cost: Money::new(50.0, Currency::USD),
};
let location2 = Location {
name: "London".to_string(),
accommodations: vec![accommodation2],
transportation_cost: Money::new(60.0, Currency::USD),
};
let route = Route {
locations: vec![location1, location2],
};
let mut calculator = CostSummaryCalculator::new();
route.add_costs_to(&mut calculator);
let summary = calculator.summarise();
println!("Total cost: {}", summary.total.amount);
}
4. 함수형 접근 방식 구현
함수형 접근에서는 map, fold, zip 등의 함수형 도구를 사용하여 데이터를 처리합니다. fold는 누적 계산을 하는 데 유용하고, map은 각 항목을 변환하는 데 사용됩니다.
4.1. 함수형 접근 예시
use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
enum Currency {
USD,
EUR,
KOR,
}
#[derive(Clone, Debug, PartialEq)]
struct Money {
amount: f64,
currency: Currency,
}
impl Money {
fn new(amount: f64, currency: Currency) -> Self {
Money { amount, currency }
}
// 핵심!! add는 합산된 새 Money를 리턴
fn add(&self, other: &Money) -> Money {
if self.currency == other.currency {
Money::new(self.amount + other.amount, self.currency.clone())
} else {
panic!("Cannot add different currencies")
}
}
fn to_money(&self) -> Money {
Money::new(self.amount, self.currency.clone())
}
pub fn display(&self) -> String {
format!("{} {:?}", self.amount / 100.0, self.currency)
}
}
#[derive(Clone)]
struct Accommodation {
cost: Money,
}
impl Accommodation {
fn calculate_total_cost(&self, cost: Money) -> Money {
cost.add(&self.cost)
}
}
#[derive(Clone)]
struct Location {
name: String,
accommodations: Vec<Accommodation>,
transportation_cost: Money,
}
impl Location {
// 인자로 넘어온 costs에 자신의 데이터를 계산해서 넣어 줌 Visitor패턴 향기가..
fn aggregate_costs(&self, costs: Vec<Money>) -> Vec<Money> {
let accommodations_costs = self
.accommodations
.iter()
.map(|accommodation| accommodation.calculate_total_cost(Money::new(0.0, self.transportation_cost.currency.clone())))
.collect::<Vec<Money>>();
let all_costs = [accommodations_costs, vec![self.transportation_cost.clone()]].concat();
[costs, all_costs].concat() // 여기서 매개변수로 들어온 costs 백터는 없어지고 새로운 벡터가[costs, all_costs].concat() 를 통해서 만들어저 리턴!!! 매우 중요
}
}
#[derive(Clone)]
struct Route {
locations: Vec<Location>,
}
impl Route {
fn collect_costs(&self, costs: Vec<Money>) -> Vec<Money> {
self.locations.iter().fold(costs, |acc, location| location.aggregate_costs(acc))
}
}
#[derive(Clone)]
struct CostSummaryCalculator; // 상태를 갖지 않음
impl CostSummaryCalculator {
fn summarise(costs: Vec<Money>) -> CostSummary { // 오직 데이터를 받아 계산만 수행
let total = costs.into_iter().fold(Money::new(0.0, Currency::USD), |acc, cost| acc.add(&cost));
CostSummary { total }
}
}
#[derive(Clone)]
struct CostSummary {
total: Money,
}
fn main() {
let accommodation1 = Accommodation {
cost: Money::new(100.0, Currency::USD),
};
let accommodation2 = Accommodation {
cost: Money::new(150.0, Currency::USD),
};
let location1 = Location {
name: "Paris".to_string(),
accommodations: vec![accommodation1],
transportation_cost: Money::new(50.0, Currency::USD),
};
let location2 = Location {
name: "London".to_string(),
accommodations: vec![accommodation2],
transportation_cost: Money::new(60.0, Currency::USD),
};
let route = Route {
locations: vec![location1, location2],
};
let costs = route.collect_costs(Vec::new());
let summary = CostSummaryCalculator::summarise(costs);
println!("Total cost: {}", summary.total.amount);
}
변경된 점:
함수형 스타일:
collect_costs 는 더 이상
&mut self를 사용하지 않고, 모든 비용을 인자로 받아서 새로운 벡터를 반환합니다.calculate_total_cost와
summarise는 상태를 변경하지 않고, 새로운 데이터를 생성하는 방식으로 변경되었습니다.
상태 변경 없음:
CostSummaryCalculator는 상태를 변경하지 않고,add_cost에서 매개변수로 받은 비용 벡터를 새로 생성해서 반환합니다.summarise는 비용 목록을 받아서 새로운 총합을 계산한 후CostSummary를 반환합니다.
Vec<Money>로 비용 누적:각 위치(
Location)와 경로(Route)에서 발생하는 비용들은Vec<Money>형태로 누적됩니다. 각 단계에서 aggregate_costs와 collect_costs는 비용 벡터를 받아서 새로운 비용을 더한 후 새로운 벡터를 반환합니다.
함수형 스타일의 이점:
불변성: 데이터를 수정하지 않고 새로운 데이터를 생성하는 방식으로, 프로그램이 더 안전하고 예측 가능해집니다.
순수 함수: 함수가 외부 상태를 변경하지 않으므로, 각 함수는 입력값에만 의존하고, 사이드 이펙트가 없습니다.
iter() vs into_iter() 차이점
메서드 | 설명 |
|---|---|
| "불변 참조 (&T)로 요소를 순회" → 원본 데이터를 그대로 둠 |
| "소유권을 가져와서 (T) 순회" → 원본 데이터 소유권을 넘김 |
이 코드에서 iter()와 into_iter()를 선택한 이유
self.accommodations.iter()accommodations는Vec<Accommodation>iter()를 사용하여&Accommodation참조를 반환 → 원본 데이터 유지map()을 사용하여 새로운Money값을 계산
self.locations.iter()locations는Vec<Location>iter()를 사용하여&Location참조 반환 → 원본locations을 변경하지 않음fold()내부에서location.collect_costs()호출 (새로운Vec<Money>반환)
costs.into_iter()costs는Vec<Money>into_iter()를 사용하여Money값 자체를 소비 →fold()에서Money의 소유권을 넘기고add()호출불필요한 복사를 피하고 성능 최적화
5. 누적 방식에서 발생할 수 있는 오류
누적하는 방식에서는 아래와 같은 오류가 발생할 수 있습니다.
5.1. 초기값 설정 안 함
Money를 누적할 때, 초기값을 설정하지 않으면 누적 값이 계속 쌓일 수 있습니다. 예를 들어, 이전에 추가된 값이 그대로 남아서 잘못된 결과를 초래할 수 있습니다.
예시
let mut calculator = CostSummaryCalculator::new();
// 초기값 없이 누적을 시작하면 이전 상태가 유지될 수 있음
route.add_costs_to(&mut calculator);
해결 방법
초기값을 설정해주거나, 누적된 값을 초기화하는 방법을 사용해야 합니다.
5.2. 다른 통화 간 누적 오류
Money를 누적할 때, 서로 다른 통화 간에 더하려고 하면 오류가 발생합니다. 예를 들어, USD와 EUR을 같은 Money에 더하려고 하면 오류가 발생할 수 있습니다.
해결 방법
누적할 때, 같은 통화만을 다루거나, ExchangeRates를 사용하여 다른 통화 간 변환을 미리 수행해야 합니다.
5.3 동시성 문제 (멀티스레드 환경에서)
만약 여러 스레드에서 CostSummaryCalculator의 값을 동시에 업데이트한다면, 동시성 문제로 인해 잘못된 누적 결과가 나올 수 있습니다. Rust는 기본적으로 멀티스레드 환경에서 안전성을 보장하지만, mut 참조를 여러 스레드에서 동시에 다루면 문제가 될 수 있습니다.
이 문제는 Mutex나 RwLock과 같은 동기화 기법을 통해 해결할 수 있습니다.
use std::sync::{Arc, Mutex};
let calculator = Arc::new(Mutex::new(CostSummaryCalculator::new(
Currency::USD,
ExchangeRates
)));
let calculator_clone = Arc::clone(&calculator);
std::thread::spawn(move || {
let mut calc = calculator_clone.lock().unwrap();
calc.add_cost(Money::new(100.0, Currency::USD));
});
이런 방식으로 멀티스레드 환경에서 안전하게 값을 업데이트할 수 있습니다.
6. 불변 데이터를 사용하여 해결하는 방법
불변 데이터는 상태 변경을 방지하여 오류를 줄여줍니다. 위에서 사용한 Money 구조체는 불변으로 설계되어 있기 때문에, add 메서드를 통해 값을 추가할 때마다 새로운 Money 객체를 반환합니다. 이는 불변성을 유지하며 계산을 계속할 수 있게 해줍니다.
불변성을 유지한 코드 예시
let total = route.locations.iter().map(|location| {
location.accommodations.iter().map(|accommodation| accommodation.cost.clone())
.chain(std::iter::once(location.transportation_cost.clone()))
}).flatten().fold(Money::new(0.0, Currency::USD), |acc, cost| {
acc.add(&cost)
});
여기서 acc.add(&cost)는 acc를 변경하지 않고, 새로운 Money 객체를 반환합니다. 이 방식은 상태 변경을 최소화하고 불변성을 유지하는 방식입니다.
7. 결론
이번 튜토리얼에서는 Rust에서 여행 경로와 관련된 비용을 누적하는 방식과 함수형 접근 방식을 사용하여 계산하는 방법을 배웠습니다. map, fold, zip 등의 함수형 도구를 활용하면 데이터를 효과적으로 처리할 수 있으며, 불변성을 유지하면서 오류를 줄일 수 있습니다. 누적 방식에서 발생할 수 있는 오류와 이를 불변 데이터를 사용하여 해결하는 과정을 설명하였습니다.
부록1. Eq와 PartialEq차이는?
1. PartialEq (부분 동등성, 반사적이지 않아도 됨)
PartialEq은 "부분 동등성"(partial equality)을 의미합니다. 즉, 일부 값에 대해 비교가 불가능할 수도 있습니다.
예를 들어, **부동소수점(f32, f64)**의 경우 NaN(Not a Number) 값이 존재하기 때문에 == 연산이 항상 성립하지 않습니다.
#[derive(PartialEq, Debug)]
struct Point {
x: f64,
y: f64,
}
fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = Point { x: 1.0, y: 2.0 };
println!("{}", p1 == p2); // true
}
✔ PartialEq은 == 연산을 정의할 수 있게 해준다.
❌ 그러나 PartialEq만으로는 Eq 트레이트를 만족하지 않는다.
let nan1 = f64::NAN;
let nan2 = f64::NAN;
println!("{}", nan1 == nan2); // false (NaN은 자기 자신과도 같지 않다!)
💡 f64 같은 타입은 Eq 트레이트를 구현할 수 없음.
2. Eq (완전 동등성, 반사성을 보장해야 함)
Eq는 **완전한 동등성(Reflexive equality)**을 의미하며, PartialEq을 상속하는 트레이트입니다.
즉, x == x가 항상 true여야 합니다.
💡 Eq는 추가적인 메서드를 제공하지 않으며, PartialEq의 특성을 강화하는 역할을 합니다.
#[derive(PartialEq, Eq, Debug)]
struct User {
id: u32,
name: String,
}
fn main() {
let u1 = User { id: 1, name: "Alice".to_string() };
let u2 = User { id: 1, name: "Alice".to_string() };
println!("{}", u1 == u2); // true
}
✔ User 구조체는 Eq을 만족하기 때문에 반사성이 보장됨.
🚨 하지만 Eq는 PartialEq을 상속하기 때문에 PartialEq 없이 단독으로 사용할 수 없음.
3. Eq를 사용할 수 없는 경우
다음과 같은 경우에는 Eq을 구현할 수 없습니다.
NaN을 포함하는 부동소수점(f32,f64)비교 연산이 정의되지 않은 경우
예를 들어, f64는 PartialEq만 구현 가능하며 Eq는 구현할 수 없습니다.
fn main() {
let x = f64::NAN;
println!("{}", x == x); // false (자기 자신과도 같지 않음!)
}
User에서 Eq없어도 될 거 같은데?
네, Eq 없이도 u1 == u2 비교가 가능합니다.
왜냐하면 PartialEq만으로도 == 연산이 가능하기 때문입니다.
즉, 아래 코드처럼 Eq 없이도 정상적으로 동작합니다.
use std::collections::HashSet;
#[derive(PartialEq, Eq, Hash, Debug)]
struct User {
id: u32,
name: String,
}
fn main() {
let mut users = HashSet::new();
users.insert(User { id: 1, name: "Alice".to_string() });
println!("{:?}", users);
}
그럼 Eq는 언제 필요한가?
Eq 트레이트는 **완전한 동등성(Reflexive Equality, x == x가 항상 참이어야 함)**을 요구합니다.
하지만 Rust에서 Eq는 특별한 역할을 합니다.
✔ Eq이 필요한 경우
HashMap의 키로 사용하려면Eq이 필요함HashMap에서 키의 동등성 비교를 위해Eq이 필요합니다.HashSet도 마찬가지입니다.
use std::collections::HashSet; #[derive(PartialEq, Eq, Hash, Debug)] struct User { id: u32, name: String, } fn main() { let mut users = HashSet::new(); users.insert(User { id: 1, name: "Alice".to_string() }); println!("{:?}", users); }여기서
User를HashSet의 요소로 사용하려면Eq이 필요합니다.또한
Hash트레이트도 필요합니다.
Rust가
Eq을 자동으로 추가하지 않기 때문에 명시적으로 요구하는 경우어떤 경우에는
Eq이 필요하다고 명확하게 나타내야 합니다.하지만 기본적으로
PartialEq만으로도==연산은 가능하기 때문에 대부분의 경우Eq을 생략해도 문제없습니다.
결론
PartialEq만 있어도==연산이 가능하다.Eq은HashMap,HashSet의 키로 사용할 때 필요하다.정수, 문자열 같은 값들은
Eq을 추가해도 큰 의미가 없지만,HashMap등을 사용하면 필요할 수 있다.따라서
Eq을 추가하는 것은 **"이 타입은 완전한 동등성을 가진다"**는 것을 명확히 하는 역할을 합니다.
부록2. 함수형은 새로운 객체(백터)등을 만들잖아 그냥 객체형으로 누적한것에 비해 성능적으로 큰 문제 없을까?
함수형 프로그래밍에서 새로운 객체나 벡터를 계속해서 만드는 방식이 성능에 미치는 영향을 고려하는 것은 중요한 문제입니다. 특히 반복적으로 객체를 생성하거나, 큰 데이터를 다룰 때 성능에 차이가 있을 수 있습니다.
1. 함수형 방식의 성능 문제
함수형 스타일에서 중요한 특징은 불변성(immutability)입니다. 이를 구현하려면 매번 새로운 객체나 컬렉션을 생성해야 하죠. 예를 들어, fold나 map 등 함수형 함수들이 새로운 벡터를 생성하면서 결과를 누적하는 방식입니다. 이로 인해 몇 가지 성능 문제가 발생할 수 있습니다:
객체 복사 비용:
Vec<Money>처럼 큰 데이터를 다룰 때, 매번 복사 비용이 들 수 있습니다. 특히 매번 새로운 벡터를 생성하는 방식이 메모리 사용을 증가시킬수 있습니다.불필요한 메모리 할당: 기존 객체를 변경하지 않고 새 객체를 생성하기 때문에 메모리 할당이 반복적으로 발생할 수 있습니다.
Vec::push나Vec::concat처럼 기존 벡터의 데이터를 새로 할당된 벡터에 복사하는 방식은 성능에 영향을 미칠 수 있습니다.
2. 객체형으로 누적하는 방식의 장점
객체형으로 누적하는 방식에서는 새로운 객체를 만들지 않고 기존 객체를 수정하거나 참조를 사용하여 데이터를 누적하는 방식입니다. 이 방식의 장점은 다음과 같습니다:
메모리 재사용: 기존 객체를 계속해서 수정하므로, 새로운 메모리 할당이 적고 메모리 사용이 효율적입니다.
성능 최적화: 불변성을 유지하지 않기 때문에, 복사 비용이 없고 단순히 객체를 참조하거나 값을 직접 수정하는 방식으로 성능이 향상될 수 있습니다.
3. Rust에서의 성능 고려
Rust에서는 객체가 불변일 때 불필요한 복사를 방지하고, 소유권(ownership)과 참조를 잘 관리하면 성능을 최적화할 수 있습니다. 다만, 여러 벡터를 계속 복사하고 결합하는 작업을 반복할 경우 성능 저하가 발생할 수 있습니다. 하지만 Rust의 메모리 안전성과 소유권 규칙 덕분에 잘 최적화된 코드에서는 성능 저하가 최소화될 수 있습니다.
4. 성능을 비교하는 방법
작은 데이터: 작은 데이터셋에서는 성능 차이가 거의 나지 않거나 미미합니다. 함수형 방식에서도 큰 성능 저하 없이 사용할 수 있습니다.
큰 데이터: 데이터셋이 커지면 복사 비용이나 메모리 할당이 성능에 영향을 미칠 수 있습니다. 이때는 기존 객체를 변경하는 방식이 더 효율적일 수 있습니다.
5. Rust에서 성능 최적화
&mut참조 사용: 불변 데이터 대신 가변 참조(&mut)를 사용하여 값을 수정하면 매번 새로운 객체를 생성하지 않고도 누적할 수 있습니다.Vec::push대신Vec::extend:concat대신Vec::extend를 사용하면 벡터를 결합할 때 더 효율적으로 처리할 수 있습니다.concat은 새로운 벡터를 생성하고 값을 복사하는 방식이지만,extend는 기존 벡터에 요소를 추가하는 방식입니다.
함수형 스타일을 유지하면서도 불필요한 메모리 할당을 줄이는 방법을 고려할 수 있습니다.
✅ Vec::extend 사용하기 (concat 대신)
costs.extend(accommodations_costs); costs.push(self.transportation_cost.clone());concat대신extend를 사용하면 기존 벡터에 새로운 데이터를 추가할 수 있어 새로운 벡터를 만들지 않아도 됨.
✅ &mut을 활용하여 기존 벡터 수정하기
fn add_costs_to(&self, costs: &mut Vec<Money>) {
for accommodation in &self.accommodations {
accommodation.add_cost(costs);
}
costs.push(self.transportation_cost.clone());
}
기존 벡터를 변경(
&mut)하면 새로운 벡터를 할당하지 않아도 되므로 성능이 향상됨.
✅ iter_mut() 사용하기
self.accommodations.iter_mut().for_each(|a| a.add_cost(costs));
self.accommodations.iter_mut().for_each(|a| a.add_cost(costs));
iter_mut()을 사용하면 기존 벡터의 요소를 직접 수정 가능.
6. 결론
작은 데이터셋에서는 함수형 방식도 충분히 효율적이고 성능 문제는 미미할 것입니다. (위의 예제 느낌의 수준에선 차이 없음)
큰 데이터셋에서는 기존 객체를 수정하는 방식이 성능 면에서 더 유리할 수 있습니다. 따라서 상황에 따라 상태를 변경하는 방식(객체형 누적)이 더 적합할 수 있습니다.
성능을 최적화하려면 데이터의 크기와 처리 방식에 따라 적절한 접근 방식을 선택해야 합니다. Rust에서는 Vec와 같은 자료구조의 복사와 할당을 관리하는 데 신경을 쓴다면 성능 문제를 최소화할 수 있습니다.
부록3. 코틀린 코드 (함수형 느낌)
// 통화 단위를 정의하는 Enum
enum class Currency {
USD, EUR, GBP
}
// 금액과 통화를 포함하는 Money 클래스
data class Money(val amount: Double, val currency: Currency) {
fun add(other: Money): Money {
require(currency == other.currency) { "Cannot add different currencies" }
return Money(amount + other.amount, currency)
}
override fun toString(): String = "$amount $currency"
}
// 숙박 비용을 나타내는 클래스
data class Accommodation(val cost: Money) {
fun addCost(cost: Money): Money = cost.add(this.cost)
}
// 여행지(Location)에는 숙박과 교통비가 포함됨
data class Location(
val name: String,
val accommodations: List<Accommodation>,
val transportationCost: Money
) {
fun addCostsTo(costs: List<Money>): List<Money> {
val accommodationCosts = accommodations.map { it.addCost(Money(0.0, transportationCost.currency)) }
return costs + accommodationCosts + transportationCost
}
}
// 여행 경로(Route)는 여러 Location을 포함
data class Route(val locations: List<Location>) {
fun addCostsTo(costs: List<Money>): List<Money> {
return locations.fold(costs) { acc, location -> location.addCostsTo(acc) }
}
}
// 비용 합산을 담당하는 객체 (함수형 스타일로 단순 계산)
object CostSummaryCalculator {
fun summarise(costs: List<Money>): CostSummary {
val total = costs.fold(Money(0.0, Currency.USD)) { acc, cost -> acc.add(cost) }
return CostSummary(total)
}
}
// 비용 요약을 위한 데이터 클래스
data class CostSummary(val total: Money)
// 실행 예제
fun main() {
val accommodation1 = Accommodation(Money(100.0, Currency.USD))
val accommodation2 = Accommodation(Money(150.0, Currency.USD))
val location1 = Location("Paris", listOf(accommodation1), Money(50.0, Currency.USD))
val location2 = Location("London", listOf(accommodation2), Money(60.0, Currency.USD))
val route = Route(listOf(location1, location2))
val costs = route.addCostsTo(emptyList())
val summary = CostSummaryCalculator.summarise(costs)
println("Total cost: ${summary.total}")
}
부록4. 순수함수형 코드 (하스켈)
-- 통화 단위를 나타내는 Enum
data Currency = USD | EUR | GBP deriving (Show, Eq)
-- 금액과 통화를 포함하는 Money 타입
data Money = Money { amount :: Double, currency :: Currency } deriving (Show, Eq)
-- Money를 더하는 함수 (같은 통화만 가능)
addMoney :: Money -> Money -> Money
addMoney (Money amt1 cur1) (Money amt2 cur2)
| cur1 == cur2 = Money (amt1 + amt2) cur1
| otherwise = error "Cannot add different currencies"
-- 숙박 비용을 나타내는 타입
newtype Accommodation = Accommodation { cost :: Money } deriving (Show)
-- 숙박 비용을 가져오는 함수
getAccommodationCost :: Accommodation -> Money
getAccommodationCost (Accommodation money) = money
-- 여행지(Location)는 숙박과 교통비 포함
data Location = Location
{ name :: String
, accommodations :: [Accommodation]
, transportationCost :: Money
} deriving (Show)
-- 특정 여행지의 총 비용을 계산하는 함수
calculateLocationCost :: Location -> Money
calculateLocationCost (Location _ accs transport) =
foldl addMoney transport (map getAccommodationCost accs)
-- 여행 경로(Route)는 여러 Location을 포함
newtype Route = Route { locations :: [Location] } deriving (Show)
-- 전체 여행 경로의 총 비용을 계산하는 함수
calculateRouteCost :: Route -> Money
calculateRouteCost (Route locs) =
foldl addMoney (Money 0 USD) (map calculateLocationCost locs)
-- 실행 예제
main :: IO ()
main = do
let accommodation1 = Accommodation (Money 100 USD)
accommodation2 = Accommodation (Money 150 USD)
location1 = Location "Paris" [accommodation1] (Money 50 USD)
location2 = Location "London" [accommodation2] (Money 60 USD)
route = Route [location1, location2]
totalCost = calculateRouteCost route
putStrLn ("Total cost: " ++ show totalCost)