← 글 목록으로

[쏙쏙 들어오는 함수형 코딩] Ch.05 ~ 06

안소은
study함수형 프로그래밍

Chapter 5. 더 좋은 액션 만들기

1. 비즈니스 요구사항과 함수 맞추기

기계적인 리팩터링이 항상 최선의 구조를 만들어주지는 않는다.

함수 시그니처가 비즈니스 개념을 제대로 반영하는지 확인해야 한다.

코드 스멜(Code Smell) 발견

코드 스멜은 더 큰 문제를 미리 알려주는 신호다

요구사항 : 장바구니에 담긴 제품을 주문할 때 무료 배송인지 확인하는 것


2. 원칙: 암묵적 입출력은 적을수록 좋다

왜 줄여야 하는가?

  • 암묵적 입출력이 있으면 다른 컴포넌트와 강하게 결합
  • 아무 때나 실행할 수 없어 테스트가 어려워짐
  • 계산(calculation)으로 만들 수 없더라도, 줄이면 재사용성↑ 테스트 용이성↑

적용 예시


3. 원칙: 엉켜있는 코드를 풀어라

분리된 것은 언제든 다시 합칠 수 있다. 잘 엉켜있는 것을 푸는 것이 더 어렵다.

함수를 작게 분리하면 좋은 이유

  1. 재사용하기 쉽다 — 가정이 적어서 다양한 곳에 쓸 수 있음
  2. 유지보수하기 쉽다 — 코드가 작아 올바른지 명확히 알 수 있음
  3. 테스트하기 쉽다 — 한 가지 일만 하므로 한 가지만 검증하면 됨

add_item() 분리 예시

add_item()은 사실 cart 구조와 item 구조를 동시에 알고 있다. 책임을 분리하면 각각 독립적으로 확장 가능.

계산 함수 분류

C(cart 동작)와 B(비즈니스 규칙)가 한 함수에 섞여 있으면 코드 스멜이다. 비즈니스 규칙은 cart 구조보다 더 자주 바뀌기 때문에 분리하는 것이 좋다.


Chapter 6. 변경 가능한 데이터 구조에서 불변성 유지하기

1. 동작 분류: 읽기 vs 쓰기


2. 카피-온-라이트(Copy-on-Write) 원칙

카피-온-라이트는 쓰기를 읽기로 바꾼다

3단계

  1. 복사본 만들기 — 배열: .slice() / 객체: Object.assign({}, obj)
  2. 복사본 변경하기 — 원본 대신 복사본을 수정
  3. 복사본 리턴하기 — 반환하면 쓰기가 읽기로 바뀜

배열 예시

// 원래 코드 — 원본을 직접 변경
function remove_item_by_name(cart, name) {
  var idx = null;
  for (var i = 0; i < cart.length; i++) {
    if (cart[i].name === name) idx = i;
  }
  if (idx !== null) cart.splice(idx, 1); // ← 원본 변경
}

// 카피-온-라이트 적용
function remove_item_by_name(cart, name) {
  var new_cart = cart.slice();           // 1. 복사본 만들기
  var idx = null;
  for (var i = 0; i < new_cart.length; i++) {
    if (new_cart[i].name === name) idx = i;
  }
  if (idx !== null) new_cart.splice(idx, 1); // 2. 복사본 변경
  return new_cart;                           // 3. 복사본 리턴
}

// 호출부에서 전역변수에 할당
shopping_cart = remove_item_by_name(shopping_cart, name);

객체 예시

// 기본 패턴
function setPrice(item, new_price) {
  var item_copy = Object.assign({}, item); // 1. 복사본
  item_copy.price = new_price;             // 2. 변경
  return item_copy;                        // 3. 리턴
}

// 재사용 가능한 유틸리티로 추상화
function objectSet(object, key, value) {
  var copy = Object.assign({}, object);
  copy[key] = value;
  return copy;
}

// objectSet을 활용하면 더 간결
function setPrice(item, new_price) {
  return objectSet(item, "price", new_price);
}

function setQuantity(item, new_quantity) {
  return objectSet(item, "quantity", new_quantity);
}

3. 읽기와 쓰기를 동시에 하는 함수 처리

.shift()처럼 값을 제거(쓰기)하면서 동시에 리턴(읽기)하는 경우, 두 가지 방법이 있다.

방법 1. 읽기/쓰기 함수로 분리 (권장)

// 읽기 전용 — 아무것도 바꾸지 않으므로 계산(calculation)
function first_element(array) {
  return array[0];
}

// 쓰기 → 카피-온-라이트 적용
function drop_first(array) {
  var copy = array.slice();
  copy.shift();
  return copy;
}

방법 2. 값 두 개를 리턴

function shift(array) {
  var copy = array.slice();
  var first = copy.shift();
  return {
    first: first,
    array: copy
  };
}
방법 1이 권장되는 이유: 읽기와 쓰기 책임이 명확히 분리되어, 각각 독립적으로 쓸 수도 있고 조합할 수도 있다.

정리!

액션 : 변경 가능한 데이터를 읽는 것

계산 : 변경 불가능한 데이터를 읽는 것

쓰기 : 데이터를 변경 가능한 구조로 만드는 것, 데이터에 쓰기가 없다면 불변형 데이터

쓰기(변형 데이터)를 읽기(불변형 데이터)로 바꾸면 코드에 계산이 많아지고 액션이 줄어듬


4. 상태 교체(Swapping)로 시간에 따른 변화 다루기

모든 값이 불변이더라도 앱 상태는 변해야 한다.

함수형 프로그래밍에서는 전역변수 자체를 새 값으로 교체하는 방식으로 상태 변화를 표현한다.

// 교체 패턴: 읽기 → 바꾸기 → 쓰기
shopping_cart = add_item(shopping_cart, item);
shopping_cart = remove_item_by_name(shopping_cart, "shirt");

shopping_cart는 항상 최신 값을 가리킨다. 이 방식은 되돌리기(undo) 구현도 쉽게 만들어 준다.


5. 중첩된 데이터와 구조적 공유

중첩된 쓰기를 읽기로 바꾸기

장바구니(배열) 안의 제품(객체)을 변경하는 경우 — 가장 안쪽부터 카피-온-라이트 적용, 상위 구조도 함께 복사한다.

function setPriceByName(cart, name, price) {
  var cartCopy = cart.slice();               // 배열 복사
  for (var i = 0; i < cartCopy.length; i++) {
    if (cartCopy[i].name === name)
      cartCopy[i] = setPrice(cartCopy[i], price); // 객체도 복사 (setPrice 내부에서)
  }
  return cartCopy;
}

얕은 복사(Shallow Copy)와 구조적 공유(Structural Sharing)

예시: 장바구니 4개 항목 중 "socks" 가격 변경 시

  • 복사되는 것 → 배열 1개 + socks 객체 1개 (총 2개)
  • 나머지 3개 객체는 원본과 새 배열이 동일한 참조를 공유 → 구조적 공유
중첩된 모든 데이터 구조가 바뀌지 않아야 진짜 불변이다. 변경하려는 값과 상위의 모든 값을 복사해야 한다.

불변 데이터 구조는 충분히 빠르다

  • 얕은 복사라 생각보다 많이 복사하지 않음
  • 가비지 컬렉터가 불필요한 메모리를 효율적으로 처리
  • 성능 문제가 생기면 그때 최적화 (섣부른 최적화 금지)

Related Posts

함께 읽으면 좋은 글