본문으로 건너뛰기

07 신뢰할 수 없는 코드를 쓰면서 불변성 지키기

이번 장에서 살펴볼 내용
  • 레거시 코드나 신뢰할 수 없는 코드로부터 내 코드를 보호하기 위해 방어적 복사를 만듭니다.
  • 얕은 복사와 깊은 복사를 비교합니다.
  • 카피-온-라이트와 방어적 복사를 언제 사용하면 좋은지 알 수 있습니다.

카피-온-라이트를 적용할 수 없는 코드를 함께 사용해야할 때도 있습니다. 바꿀 수 없는 라이브러리나 레거시 코드가 데이터를 변경한다면 카피-온-라이트를 적용할 수 없습니다. 어떻게 이런 코드에 불변 데이터를 전달 할 수 있을까요?

레거시 코드와 불변성

지금까지 장바구니에 관련된 코드는 모두 카피-온-라이트를 적용해 불변성을 유지했습니다. 하지만 블랙 프라이데이 행사 코드는 많은 곳에서 장바구니 데이터를 변경합니다. 잘 동작하긴 하지만 오랜전에 만든 코드라 당장 바꿀 시간이 없습니다. 그래서 레거시 코드에 쓸 수 있는 안전한 인터페이스가 필요합니다.

function add_item_to_cart(name, price) {
var item = make_cart_item(name, price);
shopping_cart = add_item(name, price);
var total = calc_total(shopping_cart);
set_cart_total_dom(total);
update_shipping_icons(shopping_cart);
update_tax_dom(total);
black_friday_promotion(shopping_cart);
}

추가된 함수를 호출하면 카피-온-라이트 원칙을 지킬 수 없습니다. 그리고 black_friday_promotion() 함수를 고칠 수도 없습니다. 다행히 카피-온-라이트 원칙을 지키면서 안전하게 함수를 사용할 수 있는 다른 원칙이 있습니다. 이 원칙을 방어적 복사(defensive copy) 라고 합니다. 방어적 복사를 사용해 데이터를 바꾸는 코드와 데이터를 주고받아 봅시다.

우리가 만든 카피-온-라이트 코드는 신뢰할 수 없는 코드와 상호작용해야 합니다.

우리가 만든 코드는 불변성이 지켜지는 **안전지대(safe zone)**에 있습니다. 안전지대에 있는 코드는 걱정 없이 쓸 수 있습니다.

블랙 프라이데이 행사 함수는 안전지대 밖에 있습니다. 따라서 블랙 프라이데이 함수의 입력과 출력을 통해 안전지대에 있는 코드와 데이터를 주고받아야 합니다.

안전지대 밖으로 나가는 데이터는 잠재적으로 바뀔 수 있습니다. 신뢰할 수 없는 코드가 데이터를 바꿀 수 있기 때문입니다. 마찬가지로 신뢰할 수 없는 코드에서 안전지대로 들어오는 데이터 역시 잠재적으로 바뀔 수 있습니다. 신뢰할 수 없는 코드가 계속 데이터 참조를 가지고 있기 때문에 언제든 바뀔 수 있습니다.

카피-온-라이트 패턴으로 이 문제를 해결할 수 없습니다. 카피-온-라이트 패턴은 데이터를 바꾸기 전에 복사합니다. 무엇이 바뀌는지 알기 때문에 무엇을 복사해야 할지 예상할 수 있습니다. 반면, 레거시 코드는 어떤 일이 일어날지 정확히 알 수 없습니다. 그래서 데이터가 바뀌는 것을 완벽히 막아주는 원칙이 필요합니다. 이 원칙을 방어적 복사라고 합니다.

방어적 복사는 원본이 바뀌는 것을 막아 줍니다.

신뢰할 수 없는 코드와 데이터를 주고받는 문제를 푸는 방법은 복사본을 만드는 것입니다.

안전지대로 들어오고 나가는 데이터의 복사본을 만드는 것이 방어적 복사본을 만드는 것이 방어적 복사가 동작하는 방식입니다. 안전지대에 불변성을 유지하고 바뀔 수도 있는 데이터가 안전지대로 들어오지 못하도록 하는 것이 방어적 복사의 목적입니다.

방어적 복사 규칙

규칙1: 데이터가 안전한 코드에서 나갈 때 복사하기

  1. 불변성 데이터를 위한 깊은 복사본을 만듭니다.
  2. 신뢰할 수 없는 코드로 복사본을 전달합니다.

규칙2: 안전한 코드로 데이터가 들어올 때 복사하기

  1. 변경될 수도 있는 데이터가 들어오면 바로 깊은 복사본을 만들어 안전한 코드로 전달합니다.
  2. 복사본을 안전한 코드에서 사용합니다.

이 규칙을 따르면 불변성 원칙을 지키면서 신뢰할 수 없는 코드와 상호작용할 수 있습니다.

첫 번째 규칙과 두 번째 규칙은 순서에 관계없이 쓸 수 있습니다. 어떤 경우는 먼저 데이터가 나가고 나중에 들어올 수도 있습니다. 신뢰할 수 없는 라이브러리 함수를 사용할 때 그렇습니다. 반대로 먼저 데이터가 들어오고 나중에 나갈 수도 있습니다. 공유 라이브러리를 만들 때 그렇습니다.

방어적 복사와 관련된 코드를 감싸면 더 좋은 코드가 될 것 같습니다.

그리고 어떤 경우에는 들어오는 데이터가 없거나 나가는 데이터가 없을 수도 있습니다.

신뢰할 수 없는 코드 감싸기

블랙 프라이데이 행사를 위한 코드에 방어적 복사를 잘 적용했습니다. 방어적 복사 코드를 분리해 새로운 함수로 만들어 두면 좋을 것 같습니다.

function add_item_to_cart(name, price) {
var item = make_cart_item(name, price);
shopping_cart = add_item(name, price);
var total = calc_total(shopping_cart);
set_cart_total_dom(total);
update_shipping_icons(shopping_cart);
update_tax_dom(total);
shopping_cart = black_friday_promotion_safe(shopping_cart);
}

function black_friday_promotion_safe(cart) {
var cart_copy = deepCopy(cart);
black_friday_promotion(cart_copy);
return deepCopy(cart_copy);
}

카피-온-라이트와 방어적 복사를 비교해 봅시다.

카피-온-라이트

언제 쓰나요?

통제할 수 있는 데이터를 바꿀 때 카피-온-라이트를 씁니다.

어디서 쓰나요?

안전지대 어디서나 쓸 수 있습니다. 사실 카피-온-라이트가 불변성을 가진 안전지대를 만듭니다.

복사 방식

얕은 복사(상대적으로 비용이 적게 듭니다)

규칙

  1. 바꿀 데이터의 얕은 복사를 만듭니다.
  2. 복사본을 변경합니다.
  3. 복사본을 리턴합니다.
방어적 복사

언제 쓰나요?

신뢰할 수 없는 코드와 데이터를 주고받아야 할때 방어적 복사를 씁니다.

어디서 쓰나요?

안전지대의 경계에서 데이터가 오고 갈 때 방어적 복사를 씁니다.

복사 방식

깊은 복사(상대적으로 비용이 많이 듭니다)

규칙

  1. 안전지대로 들어오는 데이터에 깊은 복사를 만듭니다.
  2. 안전지대에서 나가는 데이터에 깊은 복사를 만듭니다.

깊은 복사는 얕은 복사보다 비쌉니다.

깊은 복사는 원본과 어떤 데이터 구조도 공유하지 않는 것이 얕은 복사와 차이점입니다. 중첩된 모든 객체나 배열을 복사합니다. 얕은 복사에서는 바뀌지 않는 값이라면 원본과 복사본이 데이터를 공유합니다.

깊은 목사는 모든 것을 복사합니다. 데이터가 변경되면 안 되지만 신뢰할 수 없는 코드가 변경할지도 모른다면 깊은 복사를 사용해야 합니다.

깊은 복사는 비싸기에 모든 곳에 쓰지 않습니다. 카피-온-라이트를 사용할 수 없는 곳에서만 사용합니다.

결론

불변성을 유지할 수 있는 강력하고 더 일반적인 원칙인 방어적 복사에 대해 배웠습니다. 불변성을 스스로 구현할 수 있기 때문에 더 강력합니다. 하지만 더 많은 데이터를 복사해야하므로 비용이 많이 듭니다. 그래서 카피-온-라이트와 함께 사용하면 필요할 때 언제든 적용할 수 있는 강력함과 얕은 복사로 인한 효율성에 대한 장점을 모두 얻을 수 있습니다.

요점 정리

  • 방어적 복사는 불변성을 구현하는 원칙입니다. 데이터가 들어오고 나갈 때 복사본을 만듭니다.
  • 방어적 복사는 깊은 복사를 합니다. 그래서 카피-온-라이트보다 비용이 더 듭니다.
  • 카피-온-라이트와 다르게 방어적 복사는 불변성 원칙을 구현하지 않은 코드로부터 데이터를 보호해 줍니다.
  • 복사본이 많이 필요하지 않기 때문에 카피-온-라이트를 더 많이 사용합니다. 방어적 복사는 신뢰할 수 없는 코드와 함께 사용할 때만 사용합니다.
  • 깊은 복사는 위에서 아래로 중첩된 데이터 전체를 복사합니다. 얕은 복사는 필요한 부분만 최소한으로 복사합니다.