조컴퓨터

챕터 7. 신뢰할 수 없는 코드를 쓰면서 불변성 지키기 본문

책읽기/쏙쏙 들어오는 함수형 코딩

챕터 7. 신뢰할 수 없는 코드를 쓰면서 불변성 지키기

챠오위 2022. 8. 11. 00:36

챕터 7에서 중점적으로 볼 내용은 ? 

- 레거시 코드나 신뢰할 수 없는 코드로부터 내 코드를 보호하기 위해 방어적 복사를 만들기

- 얕은 복사와 깊은 복사 비교

- 카피-온-라이트와 방어적 복사를 언제 사용하면 좋은지

 

 

레거시 코드와 불변성

MegaMart 는 블랙 프라이데이 세일을 준비하기로 한다. 이전부터 MegaMart 코드에 많은 기능이 추가되어 왔다. 지금은 잘 동작하고 있고 비즈니스를 위해 코드를 유지하는 것이 중요하다.

 

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

 

블랙 프라이데이 행사를 위해 add_item_to_cart() 함수에 코드 한 줄을 추가해야 한다.

 

function add_item_to_cart(name, price) {

   var item = make_cart_item(name, price);

   shopping_cart = add_item(shopping_cart, item);

   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) 에 있다. 

 

블랙 프라이데이 행사 함수는 안전지대 밖에 있지만, 요구 사항을 구현하려면 우리가 만든 코드에서 안전하지 않은 함수를 써야 한다. 따라서 블랙 프라이데이 함수의 입력과 출력을 통해 안전지대에 있는 코드와 데이터를 주고 받아야 한다.

 

여기서 문제가 발생한다. 안전지대 밖으로 나가는 데이터는 잠재적으로 바뀔 수 있다. 신뢰할 수 없는 코드가 데이터를 바꿀 수 있기 때문이다. 마찬가지로 신뢰할 수 없는 코드에서 안전지대로 들어오는 데이터 역시 잠재적으로 바뀔 수 있다. 신뢰할 수 없는 코드가 계속 데이터 참조를 가지고 있기 때문에 언제든 바뀔 수 있다. 문제는 불변성을 지키면서 데이터를 주고 받는 방법을 찾아야 한다.

 

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

 

 

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

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

 

바뀔 수도 있는 데이터가 신뢰할 수 없는 코드에서 안전지대로 들어온다. 들어온 데이터로 깊은 복사본을 만들고 변경 가능한 원본은 버린다. 신뢰할 수 있는 코드만 복사본을 쓰기 때문에 데이터는 바뀌지 않는다. 이런 방법으로 들어오는 데이터를 보호할 수 있다. 

 

안전지대에서 나가는 데이터도 바뀌면 안된다. 안전지대 밖으로 나가는 데이터는 신뢰할 수 없는 코드가 값을 변경할 수 있어서 변경하지 못하도록 해야 한다. 그렇게 하려면 나가는 데이터도 깊은 복사본을 만들어 내보내면 된다. 이렇게 하면 나가는 데이터를 보호할 수 있다.

 

들어오고 나가는 데이터의 복사본을 만드는 것이 방어적 복사가 동작하는 방식이다. 안전지대에 불변성을 유지하고, 바뀔 수도 있는 데이터가 안전지대에 들어오지 못하도록 하는 것이 방어적 복사의 목적이다. 이 원칙을 블랙 프라이데이 코드에 적용해 보자.

 

 

방어적 복사 구현하기

인자로 들어온 값이 변경될 수도 있는 함수를 사용하지만 불변성은 지켜야 한다. 방어적 복사를 사용하면 데이터가 바뀌는 것을 막아 불변성을 지킬 수 있다. 원본이 바뀌지 않도록 막아주기 때문에 방어적이라고 한다. 

 

black_friday_promotion() 함수는 인자로 받은 장바구니 값을 바꾼다. 장바구니 값을 넘기기 전에 깊은 복사를 해서 함수에 넘긴다. 이렇게 하면 인자로 넘긴 원본이 바뀌지 않는다.

 

원래 코드 데이터를 전달하기 전에 복사
function add_item_to_cart(name, price) {
   var item = make_cart_item(name, price);
   shopping_cart = add_item(shopping_cart, item);
   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);
}
function add_item_to_cart(name, price) {
   var item = make_cart_item(name, price);
   shopping_cart = add_item(shopping_cart, item);
   var total = calc_total(shopping_cart);
   set_cart_total_dom(total);
   update_shipping_icons(shopping_cart);
   update_tax_dom(total);
   var cart_copy = deepCopy(shopping_cart);
   black_friday_promotion(cart_copy);
}

 

이제 black_friday_promotion() 함수에 결과를 받아야 한다. 복사본을 전달해 black_friday_promotion() 함수가 변경한 cart_copy 가 결과값이다. 어떻게 해야 cart_copy 를 안전하게 쓸 수 있는가 ? cart_copy 는 바뀌지 않는가 ? 나중에 cart_copy 의 참조를 가진 black_friday_promotion() 함수가 cart_copy 값을 바꾼다면 어떻게 될까 ? 아마 버그로 발견될 것이다. 이 문제를 해결하려면 우리가 만든 코드로 들어오는 데이터에 방어적 복사를 적용해야 한다.

 

데이터를 전달하기 전에 복사 데이터를 전달하기 전후에 복사
function add_item_to_cart(name, price) {
   var item = make_cart_item(name, price);
   shopping_cart = add_item(shopping_cart, item);
   var total = calc_total(shopping_cart);
   set_cart_total_dom(total);
   update_shipping_icons(shopping_cart);
   update_tax_dom(total);
   var cart_copy = deepCopy(shopping_cart);
   black_friday_promotion(cart_copy);
}
function add_item_to_cart(name, price) {
   var item = make_cart_item(name, price);
   shopping_cart = add_item(shopping_cart, item);
   var total = calc_total(shopping_cart);
   set_cart_total_dom(total);
   update_shipping_icons(shopping_cart);
   update_tax_dom(total);
   var cart_copy = deepCopy(shopping_cart);
   black_friday_promotion(cart_copy);
   shopping_cart = deepCopy(cart_copy);
}

 

이것이 방어적 복사의 패턴이다. 복사본을 직접 만들어 데이터를 보호했다. 데이터가 시스템에서 나가기 전에 복사하고 들어올 때도 복사했다.

 

복사할 때는 깊은 복사(deep copy) 를 해야 하는데 어떻게 구현하는지 알아보자.

 

 

방어적 복사 규칙

방어적 복사는 데이터를 변경할 수도 있는 코드와 불변성 코드 사이에 데이터를 주고받기 위한 원칙이다. 여기에 두 가지 규칙이 있다.

 

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

1. 불변성 데이터를 위한 깊은 복사본을 만든다.

2. 신뢰할 수 없는 코드로 복사본을 전달한다.

 

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

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

2. 복사본을 안전한 코드에서 사용한다.

 

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

 

방어적 복사에 대해 더 알아보자. 블랙 프라이데이 코드를 조금 더 살펴보자. 방어적 복사와 관련된 코드를 감싸면(wrapping) 더 좋은 코드가 될 것 같다.

 

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

 

 

신뢰할 수 없는 코드 감싸기

나중에 add_item_to_cart() 함수를 보면 복사본을 만든 이유를 모를 수 있다. 또한, black_friday_promotion() 함수가 다시 필요할 수도 있다. 그리고 다시 만든다고 해도 방어적 복사를 잘 만들기 어렵다. 따라서 방어적 복사 코드를 분리해 새로운 함수로 만들어 두는게 좋을 것이다.

 

원래 코드 데이터를 전달하기 전후에 복사
function add_item_to_cart(name, price) {
   var item = make_cart_item(name, price);
   shopping_cart = add_item(shopping_cart, item);
   var total = calc_total(shopping_cart);
   set_cart_total_dom(total);
   update_shipping_icons(shopping_cart);
   update_tax_dom(total);
   var cart_copy = deepCopy(shopping_cart);
   black_friday_promotion(cart_copy);
   shopping_cart = deepCopy(cart_copy);
}
function add_item_to_cart(name, price) {
   var item = make_cart_item(name, price);
   shopping_cart = add_item(shopping_cart, item);
   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(shopping_cart);
   black_friday_promotion(cart_copy);
   return deepCopy(cart_copy);
}

 

이제 문제 없이 black_friday_promotion() 함수를 사용할 수 있다. 이 함수를 사용하면 데이터가 바뀌지 않는다. 코드가 하는 일이 더 명확하다.

 

 

방어적 복사가 익숙할 수도 있습니다

방어적 복사는 일반적인 패턴이다. 몇 가지 예를 들어보자.

 

웹 API 속에 방어적 복사

대부분의 웹 기반 API(Application Programming Interface) 는 암묵적으로 방어적 복사를 한다.

 

JSON 데이터가 API 에 요청으로 들어왔다고 해보자. 클라이언트는 데이터를 인터넷을 통해 API 로 보내려고 직렬화한다. 이때 JSON 데이터는 깊은 복사본(deep copy) 이다. 서비스가 잘 동작한다면 JSON 으로 응답한다. 이때 JSON 도 역시 깊은 복사본이다. 서비스에 들어올 때와 나갈 때 데이터를 복사한 것이다.

 

이처럼 웹 API 는 방어적 복사를 한다. 마이크로서비스나 서비스-지향(service-oriented) 시스템이 서로 통신할 때 방어적 복사를 한다는 점은 장점이다. 방어적 복사로 서로 다른 코드와 원칙을 가진 서비스들이 문제없이 통신할 수 있다.

 

얼랭과 엘릭서에서 방어적 복사

얼랭(Erlang) 과 엘릭서(Elixir) 는 방어적 복사를 잘 구현했다. 얼랭에서 두 프로세스가 서로 메시지를 주고받을 때 수신자의 메일박스에 메시지가 복사된다. 또 프로세스에서 데이터가 나갈 때도 데이터를 복사한다. 방어적 복사는 얼랭 시스템이 고가용성을 보장하는 핵심 기능이다.

 

우리 코드도 방어적 복사를 쓰면 얼랭 모듈이나 마이크로서비스와 같은 장점을 얻을 수 있다.

 

 


카피-온-라이트와 방어적 복사는 비슷한 것 같다. 정말 다른 것인가 ? 둘 다 필요한가 ?

- 카피-온-라이트와 방어적 복사 모두 불변성을 유지하기 위해 사용한다. 어떻게 보면 하나만 있어도 될 것 같다. 사실 안전지대에서도 방어적 복사로 불변성을 유지할 수 있다. 

하지만 방어적 복사는 깊은 복사를 한다. 깊은 복사는 위에서 아래로 모든 계층의 중첩된 데이터를 복사하기 때문에 얉은 복사보다 더 비용이 든다. 안전지대에서는 데이터를 전달할 때 많은 복사를 하지 않아도 된다. 많은 복사본 때문에 연산과 메모리를 낭비하는 것을 막으려면 가능한 안전지대에서는 카피-온-라이트를 사용하는 것이 좋다. 그래서 두 원칙은 함께 사용해야 한다.


 

카피-온-라이트와 방어적 복사를 비교해 보자

카피-온-라이트

언제 쓰는가 ? 
통제할 수 있는 데이터를 바꿀 때 카피-온-라이트를 쓴다.

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

복사 방식
얕은 복사

규칙
1. 바꿀 데이터의 얕은 복사를 만든다.
2. 복사본을 변경한다.
3. 복사본을 리턴한다.
방어적 복사

언제 쓰는가 ? 
신뢰할 수 없는 코드와 데이터를 주고받아야 할 때 방어적 복사를 쓴다.

어디서 쓰는가 ?
안전지대의 경계에서 데이터가 오고 갈 때 방어적 복사를 쓴다.

복사 방식
깊은 복사

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

 

 

깊은 복사는 얕은 복사보다 비싸다

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

 

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

 

깊은 복사는 확실히 비싸다. 그래서 모든 곳에 쓰지 않는다. 카피-온-라이트를 사용할 수 없는 곳에서만 사용한다.

 

 

자바스크립트에서 깊은 복사를 구현하는 것은 어렵다

깊은 복사는 단순한 개념이기 때문에 만들기 쉬워 보인다. 하지만 자바스크립트에서는 표준 라이브러리가 좋지 않아 만들기가 어렵다.

그래서 Lodash 라이브러리에 있는 깊은 복사 함수를 쓰는 것을 추천한다. Lodash 에 .cloneDeep() 함수는 중첩된 데이터에 깊은 복사를 한다. Lodash 라이브러리는 많은 개발자가 신뢰하는 라이브러리이다.

 

다음은 완벽하지는 않지만 깊은 복사가 어떻게 동작하는지 보여주는 간단한 구현이다. 다음 구현은 JSON 이 허용하는 모든 타입과 함수에 잘 동작한다.

 

function deepCopy(thing) {

   if (Array.isArray(thing)) {

      var copy = [];

      for (var i = 0; i < thing.length; i++) {

         copy.push(deepCopy(thing[i]));   // 재귀적으로 복사

      }

      return copy;

   } else if (thing === null) {

      return null;

   } else if (typeof thing === "object") {

      var copy = {};

      var keys = Object.keys(thing);

      for (var i = 0; i < keys.length; i++) {

         var key = keys[i];

         copy[key] = deepCopy(thing[key]);   // 재귀적으로 복사

      }

      return copy;

   } else {

      return thing;   // 문자열과 숫자, 불리언, 함수는 불변형이기 때문에 복사할 필요가 없다

   }

}

 

 

출처 : 쏙쏙 들어오는 함수형 코딩(Grokking Simplicity: Taming complex software with functional thinking)