함수형 프로그래밍 - 액션 / 계산 / 데이터

📚쏙쏙 들어오는 함수형 코딩📚 | Chapter03. 액션과 계산, 데이터의 차이 알기 ➰ Chapter04. 액션에서 계산 빼내기 ➰ Chapter05. 더 좋은 액션 만들기

함수형 프로그래밍 (Functional Programming)

  • 부수 효과 (Side Effect)

    함수에서 결괏값을 주는 것 외에 하는 행동

  • 순수 함수 (= 수학 함수, Pure Function)

    부수 효과 없이 결괏값이 인자에만 의존하는 함수

    • 참조 투명성(Referential Transparency)

      어떤 표현식을 그 결과값으로 대체해도 프로그램의 동작이 바뀌지 않는 성질을 의미하며, 순수함수의 이론적 근거가 됨.

      // 참조가 투명한 함수 (→ 계산)
      const add = (a, b) => a + b;
      
      // 참조가 투명하지 않은 함수 (→ 액션)
      const addAndLog = (a, b) => {
      	console.log(a + b); // 외부에 영향 → 부수효과
      	return a + b;
      };
      

즉, 함수형 프로그래밍이란 부수 효과 없이(혹은 부수 효과를 피하며) 순수 함수를 사용하는 프로그래밍 스타일을 의미함.

다만, 실용적인 측면에서 side effect는 소프트웨어의 실행 관점에서 필수적이고, 함수형 프로그래밍은 side effect를 잘 다룰 수 있기에 실용적임. 따라서 함수형 프로그래밍을 학문적인 지식이 아닌 기술과 개념으로 볼 필요가 있음.

함수형 사고

  1. **액션(Action)**과 계산(Calculation), **데이터(Data)**를 구분해서 생각하는 것 → 계층형 설계

  2. 함수 자체를 인자로 넘기고, 리턴하고, 변수에 담는 방식으로 코드를 추상화하는 것 → 일급 추상

을 중요하게 여김.

계층형 설계 (Stratified Design)

변경 가능성에 따라 코드를 나눠, 가장 자주 바뀌는 것을 상단에 배치함. 일반적으로, 비즈니스 규칙 → 도메인 규칙 → 기술 스택 순서대로 상단에 배치됨.

각 계층은 그 아래에 있는 계층을 기반으로 만들어지기 때문에, 각 계층에 있는 코드는 더 안정적인 기반 위에서 작성할 수 있으며,
상단에 배치할 수록 의존성이 거의 없기 때문에 계층형 설계로 짜여진 코드는 테스트, 재사용, 유지보수가 용이함.

일급 추상 (First-class Abstraction)

액션을 값처럼 다루는 것. 즉, 함수 자체를 인자로 넘기고, 리턴하고, 변수에 담는 방식으로 코드를 추상화하는 것

일급 추상의 대표적인 활용 패턴은 커링(Currying)과 함수 합성(Function Composition)임.

// 커링: 여러 인자를 받는 함수를 인자를 하나씩 받는 함수들의 체인으로 변환하는 것  
const filterByRank = rank => coupons =>   
	coupons.filter(c => c.rank === rank);  
  
// 부분 적용: 계산 함수를 미리 만들어 재사용  
const getBest = filterByRank('best');  
const getGood = filterByRank('good');  
  
// 함수 합성: 작은 계산들을 조합  
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);  
const processSubscriber = pipe(  
	getSubscriberRank, // 계산  
	selectCoupons, // 계산  
	buildEmail // 계산  
);  

함수형 프로그래밍과 분산 시스템

타임라인 다이어그램: 코드의 실행 순서와 타이밍을 시각화해서, 타임라인 간 공유 자원 충돌이나 순서 문제를 발견하는 도구

분산 시스템이란 여러 프로세스 또는 서버가 네트워크를 통해 협력하는 구조를 의미함.

단일 서버에서는 코드가 순서대로 실행되지만, 분산 시스템에서는 순서에 대한 보장이 없기 때문에

  • 타이밍 문제: 어떤 서비스가 먼저 응답할 지 알 수 없음.
  • 공유 자원 문제: 여러 서비스가 같은 DB를 동시에 읽고 씀.
  • 재시도 문제: 네트워크 오류로 같은 요청이 두 번 실행될 수 있음.
  • 순서 보장 문제: 메세지가 보낸 순서대로 도착하지 않을 수 있음.

위와 같은 문제들이 존재함.

반면, 함수형 프로그래밍의 핵심 중 하나인 계산(Calculation)은 부수 효과가 없기 때문에,

  • 각 함수가 독립적으로 실행 가능하고,
  • 실행 순서와 무관하게 결과가 동일하고,
  • race condition이 발생하지 않고,
  • lock 없이 병렬화가 가능하다

는 점에서 병렬 실행이 안전하다는 점에서 분산 시스템의 타이밍 문제와 순서 보장 문제를 해결하고, 멱등성(Idempotency)를 만족하기 때문에 재시도 문제를 해결함.

  • 멱등성(Idempotency)

    여러번 실행해도 결과가 같은 성질을 의미함.

    // 멱등한 함수 (계산)
    const setUserActive = (user) => ({...user, active: true});
    
    // 멱등하지 않은 함수 (계산)
    const setUserActive = (user) => ({...user, active: !user.active});
    

또한 함수형에서는 데이터를 수정하지 않고 새로 만들기 때문에 데이터가 불변성(Immutability)을 띄는데, 이는 분산 시스템에서의 공유 자원 문제를 해결함.

타임라인 컷팅

fp01-02.png

여러 타임라인이 동시에 진행될 때 서로 순서를 맞추는 고차 동작(high-order operation)


Chapter 03. 액션과 계산, 데이터의 차이 알기

데이터 (Data)

이벤트에 대한 사실, 일어난 일의 결과를 기록한 것

var subscriber = {
	email: "sam@pmail.com",
	rec_count: 16,
};

var rank1 = "best";
var rank2 = "good";

var coupon = {
	code: "10PERCENT",
	rank: "bad"
}

var message = {
	from: "newsletter@coupondog.co",
	to: "sam@pmail.com",
	subject: "Your weekly coupons inside",
	body: "Here are your coupons ..."
}

함수형에서는 데이터의 불변성을 중시하는데, 이를 유지하기 위해

  • 카피-온-라이트(Copy-On-Write): 변경할 때 복사본을 만드는 방식

    const addItem = (cart ,item) => [...cart, item];
    const removeItem = (cart, idx) => [...cart.slice(0, idx), ...cart.slice(idx + 1)];
    

    원본을 건드리지 않고 새 배열을 반환함.

  • 방어적 복사 (Defensive Copy): 보관하려고 하는 데이터의 복사본을 만듦

    const config = Object.freeze({host: 'localhost', port: 3000});
    const deepCopy = (obj) => structuredClone(obj)
    

    얕은 불변성을 강제하거나, 깊은 불변성은 structuredClone을 활용함.

의 원칙을 사용함.

데이터는 계산 혹은 액션과 비교해보았을 때,

  • 직렬화 가능

    직렬화가 가능하기 때문에, 전송하거나 디스크에 저장했다가 읽기 쉬움

  • 동일성 비교에 용이

  • 유연한 해석

의 장점을 지니지만, 해석이 반드시 필요하다는 점에서의 단점이 존재함.

따라서 함수형에서는 데이터를 쉽게 해석할 수 있도록 표현하기를 강조함.

계산 (Calculation)

입력값으로 출력값을 만드는 것, 실행 시점이나 횟수에 관계없이 항상 같은 입력에 대한 같은 출력을 리턴함(순수함수)

function subCouponRank(subscriber) {
	if (subscriber.rec_count >= 10)
		return "best";
	else
		return "good";
}
function selectCouponsByBank(coupons, rank) {
	var ret = [];
	for (var c = 0; c < coupons.length, c++) {
		var coupon = coupons[c];
		if (coupon.rank === rank)
			ret.push(coupon.code);
	}
	return ret
}
function emailForSubscriber(subscriber, goods, bests) {
	var rank = subCouponRank(subscriber);
	if (rank === "best") 
		return {
			from: "newsletter@coupondog.co",
			to: subscriber.email,
			subject: `Your ${rank} weekly coupons inside`,
			body: `Here are your ${rank} coupons: ` + bests.join(", ")
		}
	else 
		return {
			from: "newsletter@coupondog.co",
			to: subscriber.email,
			subject: `Your ${rank} weekly coupons inside`,
			body: `Here are your ${rank} coupons: ` + goods.join(", ")
		}
}

function emailsforSubscribers(subscribers, goods, bests) {
	var emails = [];
	for (var s = 0; s < subscribers.length; s++) {
		var subscriber = subscribers[s];
		var eamil = emailForSubscriber(s, goods, bests);
		emails.push(email);
	}
	return emails;
}

또한, 계산은 액션과 달리 부수효과가 없고, 같은 입력이면 항상 같은 결과를 출력하기 때문에

  • 동시에 실행되는 것,
  • 과거에 실행되었던 것이나 미래에 실행될 것, (재시도)
  • 실행 횟수

를 고려하지 않아도 된다는 점에서 사용이 편리하며, 그렇기 때문에 테스트와 정적 분석, 조합에 용이함.

🔥 계산(Calculation) 판별하기!

  1. 같은 입력이면 항상 같은 결과인지 확인

  2. 호출 시, 함수 바깥에 영향을 주지 않는지 확인

→ 모두 만족하면 계산!

액션 (Action)

외부 세계에 영향을 주거나 받는 것으로 실행 시점(→순서)과 횟수(→반복)에 의존함.

따라서 계산(Calculation)과 구별하기 위해 “순수하지 않은 함수”, 혹은 “부수효과 함수”라고도 불림.

function sendIssue() {
	var coupons = fetchCouponsFromDB();
	var goodCoupons = selectCouponsByRank(coupons, "good");
	var bestcoupons = selectCouponsByRank(coupons, "best");
	
	var subscribers = fetchSubscribersFromDB();
	var emails = emailsForSubscribers(subscribers, goodCoupons, bestCoupons);
	for (var e = 0 ; e < emails.length; e++) {
		var email = emails[e];
		emailSystem.send(email);
	}
}

액션은 다루기 힘들지만, 필수적이기 때문에, 함수형에서는

  1. 가능한 한 액션을 적게 사용하기
  2. 액션을 가능한 한 작게 만들기
  3. 액션이 외부 세계와 상호작용 하는 것을 제한하기
  4. 액션이 호출 시점과 횟수에 의존하는 것을 제한하기

의 방법을 통해, 액션을 보다 쉽게 사용할 수 있어야 함.

🔥 액션(Action) 판별하기!

  1. 여러번 호출 시, 다른 일이 발생하는지 확인

  2. 호출 시, 함수 외부 어딘가가 바뀌는지 확인

→ 둘 중 하나라도 만족하면 액션!

Chapter 04. 액션에서 계산 빼내기

함수에는 입력과 출력이 존재하는데, 이 입력과 출력이 암묵적이면 해당 함수는 액션이 됨.

  • 명시적 입력과 출력

    명시적 입력은 함수의 파라미터를,
    명시적 출력은 함수의 리턴값을 의미함.

  • 암묵적 입력과 출력 (=side effect)

    암묵적 입력은 파라미터 외의 다른 입력을,
    암묵적 출력은 리턴값 외의 다른 출력을 의미함.

액션은 암묵적인 입력이나 출력이 존재함.

반면, 계산은 정의상으로 부수효과가 없어야 하기 때문에 암묵적 입력 또는 출력이 존재해서는 안 됨.

따라서 액션에서 계산을 분리하는 과정은 아래와 같은 단계를 거침.

function calc_cart_total() {
	shopping_cart_total = 0;
	for (var i = 0; i < shopping_cart.length; i++) {
		var item = shopping_cart[i];
		shpping_cart_total = item.price;
	}
	
	set_cart_total_dom();
	update_shipping_icons();
	update_tax_dom();
}
  1. 계산 코드 찾아 함수로 분리하기 (= 서브루틴 찾기)

    function calc_cart_total() {
    	calc_total();
    	set_cart_total_dom();
    	update_shipping_icons();
    	update_tax_dom();
    }
    
    function calc_total() {
    	shopping_cart_total = 0;
    	for (var i = 0; i < shopping_cart.length; i++) {
    		var item = shopping_cart[i];
    		shpping_cart_total = item.price;
    	}
    }
    
  2. 새 함수에서 암묵적 입력과 출력 찾기

    function calc_total() {
    	shopping_cart_total = 0; // shopping_cart_total: 전역변수를 초기화하고 있음
    	for (var i = 0; i < shopping_cart.length; i++) {
    		var item = shopping_cart[i]; // shopping_cart: 전역 변수를 읽어오고 있음
    		shpping_cart_total = item.price; // shopping_cart_total: 변경하고 있음
    	}
    }
    

    전역 변수를 읽어오는 일은 데이터가 함수 내부로 들어오는 일이기에 입력에 해당하며,

    전역 변수를 변경하는 일 또한 함수 내부에서 데이터가 나가는 일이기 때문에 출력에 해당함.

    또한, 이는 함수 파라미터 혹은 리턴이 아니기 때문에 암묵적 입력과 출력에 해당함.

  3. 암묵적 입력은 인자로 바꾸고 출력은 리턴값으로 변경하기

    function calc_cart_total() {
    	shopping_cart_total = calc_total(shopping_cart);
    	set_cart_total_dom();
    	update_shipping_icons();
    	update_tax_dom();
    }
    
    function calc_total(shopping_cart) {
    	var total = 0;
    	for (var i = 0; i < shopping_cart.length; i++) {
    		var item = shopping_cart[i];
    		total += item.price;
    	}
    	return total;
    }
    

    따라서 지역변수(total) 를 사용하여 리턴하여 명시적 출력으로 변경하고, 전역변수(shopping_cart)를 인자로 받음으로서 명시적 입력으로 변경함.

    함수형에서 불변이어야 하는 것은 함수 외부에서 보이는 값을 의미함. 즉, 함수 안에서 사용되는 지역변수는 값이 변경되어도 함수형에서 강조하는 불변성을 위반하는 게 아님.

따라서 함수형 원칙을 적용하면, 계산은 늘어나고 액션은 줄어들게 되며

이는 리팩토링에 해당함.

리팩터링 (Refactoring)

외부 동작을 그대로 유지하면서 내부 코드 구조를 개선하는 작업

즉, 작동하는 코드를 더 좋은 코드로 만드는 것을 의미함. 여기서의 “더 좋은 코드”란, 코드의 가독성, 유지보수성, 확장성을 높이는 것을 목표로 함.

대표적인 리팩토링 기법으로는,

  1. 함수 추출 (Extract Method)

    긴 함수를 의미 있는 작은 함수들로 분리하는 것

    # Before
    def process_order(order):
        total = 0
        for item in order.items:
            total += item.price * item.qty
        if total > 100:
            total *= 0.9
        print(f"총액: {total}")
    
    # After
    def calculate_total(items):
        return sum(item.price * item.qty for item in items)
    
    def apply_discount(total):
        return total * 0.9 if total > 100 else total
    
    def process_order(order):
        total = apply_discount(calculate_total(order.items))
        print(f"총액: {total}")
    
  2. 매직 넘버 제거 (Replace Magic Number)

    의미 불명의 숫자를 상수로 바꾸는 것

    # Before
    if total > 100:
        total *= 0.9
    
    # After
    DISCOUNT_THRESHOLD = 100
    DISCOUNT_RATE = 0.9
    
    if total > DISCOUNT_THRESHOLD:
        total *= DISCOUNT_RATE
    
  3. 조건문 단순화 (Simplify Conditionals)

    복잡한 조건 분기를 읽기 쉽게 만드는 것

    # Before
    def get_grade(score):
        if score >= 90:
            return "A"
        else:
            if score >= 80:
                return "B"
            else:
                return "C"
    
    # After
    def get_grade(score):
        if score >= 90: return "A"
        if score >= 80: return "B"
        return "C"
    
  4. 중복 코드 제거 (DRY - Don’t Repeat Yourself)

    반복되는 로직을 하나로 통합하는 것

  5. 변수/함수명 개선 (Rename)

    x, tmp, data 같은 모호한 이름을 명확하게 변경하는 것

Chapter 05. 더 좋은 액션 만들기

프로그램에서 액션은 필수적이기 때문에 모든 암묵적 입력이나 출력을 제거할 수는 없으나, 더 나은 프로그램을 위해서는 줄여서 설계할 필요성이 있음.

따라서 비즈니스의 요구사항에 맞춰서 최선의 추상화 단계를 선택해야 함. 더 좋은 액션의 원칙은 아래와 같음.

  1. 암묵적 입력과 출력은 적을수록 좋다.

    어떤 함수에 암묵적 입력과 출력이 있다면, 이는 다른 컴포넌트들과 강하게 연결된 컴포넌트라고 할 수 있음. 이는 해당 컴포넌트를 다른 곳에서 사용할 수 없게 하기 때문에 모듈이 아니게 되고, 그렇기 때문에 코드의 재사용성을 떨어뜨림.

    따라서, 암묵적 입력과 출력을 명시적 입력과 출력으로 바꿔 모듈화된 컴포넌트로 만드는 것이 좋음.

  2. 설계는 엉켜있는 코드를 푸는 것이다.

    함수를 사용하면 관심사 분리가 가능한데, 함수는 인자로 넘기는 값과 그 값을 사용하는 방법으로 분리할 수 있음. 따라서, 하나의 큰 함수보단 작은 함수로 분리해두었을 때,

    • 재사용에 용이하고,
    • 유지보수하기 쉽고,
    • 테스트 하기 쉽다는 점에서

    언제든 조합도 가능하기 때문에 작은 단위로 설계된 함수가 좋은 설계라고 볼 수 있음.