Functional Programming

최광일·2023년 4월 4일
0
post-thumbnail

쏙쏙 들어오는 함수형 코딩 책 리뷰

"이렇게 복잡한 것이 필요한 것인가요?"
"저는 그냥 브라우저에서 작동하는 GUI를 만들려고 하는 것인데요"

웹 어플리케이션을 만들기 위해서는 다양한 복잡성을 다뤄야 합니다.

  • 비동기 웹 요청
  • 호출 결과를 합쳐야 하는 여러 API 응답
  • 예측 불가능한 사용자의 액션
  • ...

그럼 복잡성은 필연적일까요? 아닙니다.

복잡성은 바꾸지 않으려고 하는 선택들로부터 생깁니다.

그래서 우리에게는 복잡성을 다룰 수 있는 좋은 프로그래밍 기술이 필요합니다.

Part 1


  • 1 부수 효과와 순수 함수
    • 부수 효과와 순수 함수의 차이점을 이해하고 활용한다.
  • 2 액션, 계산 데이터의 구분
    • 코드를 '액션', '계산', '데이터'로 구분하여 테스트, 재사용, 유지보수를 용이하게 한다.
  • 3 함수의 계층형 설계
    • 함수의 계층형 설계를 통해 테스트, 재사용, 유지보수 등의 비기능적 요구사항을 만족할 수 있다.

1-1 부수 효과와 순수 함수


부수 효과와 순수 함수는 다음과 같다.

  • 부수 효과
    • 부수 효과란 암묵적 입력과 출력이다
    • 부수 효과는 함수의 주요 기능인 리턴값을 계산하는 일이 아님
  • 순수 함수
    • 순수 함수란 명시적 입력값인 인자와 출력값인 리턴값을 가진다
    • 순수 함수는 함수의 주요 기능인 동일한 인자 값에 동일한 리턴값을 주는 것임

세금을 계산하여 UI를 업데이트하는 예시를 통해, 부수 효과와 순수 함수의 차이를 알아보자

let total = 10;

function updateTaxDom() {
 	setTaxDom(total * 0.1); 
}

문제가 없어보이는 코드지만, 위의 코드는 재사용과 테스트가 힘들다.

  • 재사용
    • 함수가 전역변수에 의존하지 않아야 한다
    • 세금 계산이 DOM을 사용하는 곳에서 실행된다고 가정하면 안된다
    • 함수가 결과값을 리턴해야 한다
  • 테스트
    • 함수가 전역변수에 의존하지 않아야 한다
    • DOM을 업데이트하는 테스트는 까다롭다
    • DOM 업데이트와 비즈니스 규칙은 분리되어야 한다

위의 코드를 개선해보자

let total = 10;

function updateTaxDom() {
	setTaxDom(calcTax(total));
}

function calcTax(amount) {
	return amount * 0.1;
}

이제 위에서 말한 문제점은 calcTax 함수에서 해결되었다.

  • 재사용
    • (O) 함수가 전역변수에 의존하지 않아야 한다.
    • (O) 세금 계산이 DOM을 사용하는 곳에서 실행된다고 가정하면 안된다.
    • (O) 함수가 결과값을 리턴해야 한다.
  • 테스트
    • (O) 함수가 전역변수에 의존하지 않아야 한다.
    • (O) DOM 업데이트와 비즈니스 규칙은 분리되어야 한다.

무슨 문제가 존재하였으며, 어떻게 해결되었을까?

테스트와 재사용성은 입출력과 관련이 있다.

  • 암묵적 입출력이 있는 함수는 테스트 및 재사용 하기가 힘들다
  • 명시적 입출력이 있는 함수는 테스트 및 재사용 하기가 쉽다

위의 코드를 다시 살펴보면 무엇이 암묵적이고 명시적인지 확인할 수 있다.

function updateTaxDom() {
	setTaxDom(calcTax(total);); // 암묵적 출력(부수효과 DOM 업데이트)
}

function calcTax(amount) { // 명시적 입력(인자)
	return amount * 0.1	   // 명시적 출력(리턴값)
}

위에서 다음을 알아보았다.

  • 부수 효과와 순수 함수
  • 암묵적 입출력과 명시적 입출력
  • 코드의 테스트와 재사용성을 높이는 방법

이렇게만 알아본다면 부수 효과는 나쁜것이라고 생각할 수 있지만, 부수효과는 프로그래밍의 본질이다

부수효과 예시는 다음의 것들이 있다.

  • DOM 업데이트
  • 이메일 보내기
  • DB 업데이트

따라서, 부수 효과 함수와 순수 함수를 잘 구분하고 분리하는 것이 중요하다.

1-2 액션, 계산 데이터


액션, 계산, 데이터의 정의는 다음과 같다

  • 액션
    • 부수 효과가 있는 함수
    • 호출 횟수와 실행 시점에 의존하는 것
    • ex. 전역변수 변경, 이메일 보내기, DB 읽기
  • 계산
    • 순수 함수
    • 호출 횟수와 실행 시점에 의존하지 않는 것
    • ex. 최댓값 찾기, 이메일 주소가 올바른지 확인하기
  • 데이터
    • 이벤트에 대한 사실
    • ex. DB API로 읽은 데이터, 사용자 입력 값

액션, 계산, 데이터의 구분은 코드 뿐만 아니라, 일상에서 자주 하는 '장보기'에도 적용할 수 있다.

일반적인 장보기 과정을 살펴보면 다음과 같다.

  • 1 냉장고 확인하기 (액션)
  • 2 운전해서 상점으로 가기 (액션)
  • 3 필요한 것 구입하기 (액션)
  • 4 운전해서 집으로 오기 (액션)

위의 과정의 각각은 호출 횟수와 실행 시점에 의존하므로 모두 '액션'이다.

이와 같은 액션들에는 사실 데이터와 계산이 때로 우리 머리속에서 자동으로 일어난다.

  • 1 필요한 재고 (데이터)
  • 2 냉장고 확인하기 (액션)
  • 3 현재 재고 (데이터)
  • 4 장보기 목록 (데이터) = 필요한 재고 - 현재 재고 (계산)
  • 5 상점 위치 및 가는 경로 (데이터)
  • 5 운전해서 상점으로 가기 (액션)
  • ...
  • 6 필요한 것 구입하기 (액션)
  • 7 운전해서 집으로 오기 (액션)

이처럼, 어떤 행위에는 액션, 계산, 데이터를 구분하여 찾을 수 있고 풍부한 모델을 만들 수 있다.

또한, 부수 효과와 순수 함수에서 알아본 것 처럼 액션보다는 계산이 많을수록 테스트, 재사용, 유지보수가 용이하다.

다음의 절차를 통해 부수 효과를 일으키는 액션을 순수 함수인 계산으로 변경할 수 있다.

  • 1 액션에서 서브루틴을 함수로 추출하기
  • 2 추출한 서브루틴 함수에서 암묵적 입력과 출력을 명시적으로 바꾸기

장바구니 기능을 하는 함수 예시를 통해 이를 알아보자

const shopping_cart = [];
let shopping_cart_total = 0;
 
function calc_cart_total() {    // 액션
    shopping_cart_total = 0;
    for(let i = 0; i < shopping_cart.length; i++) {
        const item = shopping_cart[i];
        shopping_cart_total += item.price;
    }
 
    set_cart_total_dom();       // 액션
    update_shipping_icons();    // 액션
    update_tax_dom();           // 액션
}

위의 코드에서 먼저 액션에서 서브루틴을 추출하면 다음과 같다.

const shopping_cart = [];
let shopping_cart_total = 0;
 
function calc_cart_total() {
    calc_total();               // 액션
    set_cart_total_dom();       // 액션
    update_shipping_icons();    // 액션
    update_tax_dom();           // 액션
}
 
function calc_total() {
    shopping_cart_total = 0;                        // 암묵적 출력 (shopping_cart_total)
    for(let i = 0; i < shopping_cart.length; i++) {  // 암묵적 입력 (shopping_cart)
        const item = shopping_cart[i];
        shopping_cart_total += item.price;          // 암묵적 출력 (shopping_cart_total)
    }
}

액션에서 서브루틴인 calc_total을 추출했지만, 해당 함수는 아직 계산이 아니라 액션이다

그 이유는, 함수에 암묵적 입력과 암묵적 출력이 존재하여 코드 외부에 영향을 주기 때문이다.

따라서 서브루틴 함수를 액션에서 계산으로 바꾸면 다음과 같다.

const shopping_cart = [];
let shopping_cart_total = 0;
 
function calc_cart_total() {
    shooping_cart_total = calc_total(shopping_cart);    // 계산
    set_cart_total_dom();                               // 액션
    update_shipping_icons();                            // 액션
    update_tax_dom();                                   // 액션
}
 
function calc_total(cart) {     // 명시적 입력 (cart)
    let total = 0;              // 지역변수 사용                     
    for(let i = 0; i < cart.length; i++) {
        const item = cart[i];
        total += item.price;
    }
    return total;               // 명시적 출력 (total)
}

이처럼 다음과 같이 부수 효과를 일으키는 액션을 순수 함수인 계산으로 변경할 수 있다

  • 1 액션에서 서브루틴을 함수로 추출하기
  • 2 추출한 서브루틴 함수에서 암묵적 입력과 출력을 명시적으로 바꾸기

1-3 함수의 계층형 설계


계층형 설계는 소프트웨어를 계층으로 구성하는 기술이다.

함수의 계층형 설계는 각 계층의 함수를 바로 아래 계층에 있는 함수를 이용해 만드는 것이다.

함수의 각 계층에는 여러 목적이 존재할 수 있다.

  • 비즈니스
    • 비즈니스 로직 수준 - ex.세금 계산 함수
  • 도메인
    • 기능 수준 - ex. 장바구니 기능 동작 함수
  • 기술
    • 언어 수준 - ex. 자바스크립트 loop 함수

계층형 설계 패턴과 도구는 다음과 같다.

  • 패턴
    • 패턴1: 직접 구현
      • 각 함수가 나타내는 문제를 함수 본문에서 적절한 구체화 수준에서 해결한다.
    • 패턴2: 추상화 벽
      • 인터페이스를 사용하여 코드를 고수준의 추상화 단계만 생각할 수 있다.
    • 패턴3: 작은 인터페이스
      • 인터페이스를 작게 만들어서 재사용과 테스트가 용이하도록 한다.
    • 패턴4: 편리한 계층
      • 언제 패턴을 적용하고 언제 패턴을 멈춰야 하는지를 알아야 한다.
      • 현재 작업하는 코드가 편리하다고 느낀다면 설계는 조금 멈춰도 된다.
  • 도구
    • 함수 호출 그래프
      • 각 계층의 함수들이 하위 계층의 함수를 호출하는 것을 나타내는 그래프

계층형 설계를 통해 테스트, 재사용, 유지보수 등의 비기능적 요구사항을 만족할 수 있다.

  • 테스트 가능성
    • 함수 호출 그래프에서 상위 계층에서 많이 호출되는 함수를 테스트 하는것이 더 가치가 있다.
  • 재사용성
    • 함수 호출 그래프에서 하위 계층에 호출하는 함수가 적을수록 재사용하기 좋다.
  • 유지보수성
    • 함수호출 그래프에서 상위 계층에서 적게 호출되는 함수가 바꾸기 쉽다.

Part 2


  • 1 일급
    • 일급 값에 대한 이해를 통해 함수적으로 재사용하는 방법을 이해한다.
  • 2 타임라인
    • 타임라인이란 시간에 따라 코드의 실행이 진행되는 것을 의미한다.
    • 모든 프로그래밍 언어는 각각 암묵적으로 시간에 대한 모델을 가지고 있다.
    • 암묵적 시간 모델이란 실행 순서와 반복을 항상 동일하게 보장하지 않는다.
    • 따라서, 복수의 타임라인에서 문제가 발생하기 때문에 해결해야 한다.
  • 2 아키텍쳐
    • T.B.D

2-1 일급


언어에서 일급에 대한 정의는 다음과 같다

  • 일급인 것은 변수나 배열이나 객체에 할당할 수 있다.
  • 일급인 것은 함수의 파라미터로 전달할 수 있다.
  • 일급인 것은 함수의 리턴값으로 사용할 수 있다.

자바스크립트에서 일급과 일급이 아닌 것은 다음과 같다

  • 일급인 것
    • 숫자, 문자열, 불리언값, 함수, 배열, 객체, ...
  • 일급이 아닌 것
    • 연산자 (+, ..), 키워드 (if, for, ...), ...

일급인 것을 통해 코드를 다음과 같이 리팩토링 하여 재사용의 이점을 얻을 수 있다.

  • 1 함수 이름에 있는 암묵적 인자를 일급 값인 함수 인자로 바꾸어 함수 본문을 재사용하기
  • 2 함수를 인자로 전달(콜백)하여 고차함수 만들어서 함수 본문을 재사용하기

먼저, 함수 이름에 있는 암묵적 인자의 예시는 다음과 같다.

function setPriceByName(cart, name, price) {
    const item = cart(name);
    const newItem = objectSet(item, 'price', price);
    const newCart = objectSet(cart, name, newItem);
    return newCart;
}
 
function setQuantityByName(cart, name, quantity) {
    const item = cart(name);
    const newItem = objectSet(item, 'quantity', price);
    const newCart = objectSet(cart, name, newItem);
    return newCart;
}
 
function setTaxByName(cart, name, tax) {
    const item = cart(name);
    const newItem = objectSet(item, 'tax', price);
    const newCart = objectSet(cart, name, newItem);
    return newCart;
}

함수의 본문은 동일하지만, 변경하려는 값에 따라 함수가 존재한다.

이때, 변경하려는 값이 함수 이름에 존재하며 해당 값은 암묵적 인자이다.

만약 cart의 값을 변경하는 요구사항이 많아진다면, 함수의 개수도 그에 비례하여 증가한다.

따라서, 함수 이름에 존재하는 암묵적 인자를 일급 값인 함수 인자로 바꾸면 다음과 같다

function setFieldByName(cart, name, field, value) {
    const item = cart(name);
    const newItem = objectSet(item, 'field', value);
    const newCart = objectSet(cart, name, newItem);
    return newCart;
}
 
setFieldByName(cart, 'shoe', 'price', 13);
setFieldByName(cart, 'shoe', 'quantity', 3);
setFieldByName(cart, 'shoe', 'tax', 3.45);

이제 더 이상 cart의 값을 변경하는 요구사항이 많아져도, 함수는 재사용이 가능하다.

.

다음으로, 함수를 인자로 전달하여 고차함수 만드는 예시는 다음과 같다.

try {
    saveUserData(user);
} catch (error) {
    logToSnapErrors(error);
}
 
try {
    fetchProduct(productId);
} catch (error) {
    logToSnapErrors(error);
}

중요한 코드에 try/catch로 감싸서 로그를 남긴다면, try/catch가 계속해서 중복된다.

따라서, 함수를 인자로 전달하여 고차함수를 만들면 다음과 같다.

// v1
function withLogging(f) {
    return {
        try {
            f();
        } catch(error) {
            logToSnapErrors(error);
        }
    }
}
 
withLogging(function() { saveUserData(user) });
withLogging(function() { fetchProduct(productId) });

함수를 인자로 넘겨서 중복을 없앴지만, 여전히 함수를 인자로 넘기는 부분에 중복이 존재한다.

따라서 해당 중복을 제거하기 위해 다음과 같이 처리할 수 있다.

// v2
function wrapLogging(f) {
    return function(arg) {
        try {
            f(arg)
        } catch(error) {
            logToSnapErrors(error);
        }
    }
}
 
const saveUserDateWithLogging = wrapLogging(saveUserData);
saveUserDateWithLogging(user);
 
const fetchProductWithLogging = wrapLogging(fetchProduct);
fetchProductWithLogging(productId);

2-2 타임라인


타임라인이란 시간에 따라 코드의 실행이 진행되는 것을 의미한다.

모든 프로그래밍 언어는 각각 암묵적으로 시간에 대한 모델을 가지고 있다.

시간 모델이란, 코드의 실행 순서와 반복에 대한 방식이며, 여러 방법이 존재한다.

  • 단일 스레드, 동기
    • 순서대로 실행
    • 동시 실행 불가능 (blocking)
  • 단일 스레드, 비동기
    • 순서대로 실행
    • 동시 실행 가능 (non-blocking)
  • 멀티스레드
    • 순서대로 실행 X
    • 동시 실행 가능
  • 메시지 패싱 프로세스
    • 순서대로 실행 X
    • 동시 실행 가능
      자바스크립트는 단일 스레드, 비동기 방식을 사용한다. 즉, 동시에 코드 실행이 가능하기 때문에 여러 타임라인이 존재할 수 있다.

하지만, 암묵적 시간 모델이란 실행 순서와 반복을 항상 동일하게 보장하지 않는다는 것이며, 이는 어플리케이션에서 필요한 실행 방식과 딱 맞을 일이 거의 없다.

복수의 타임라인에서 다음의 경우 문제가 발생할 가능성이 존재한다.

  • 공유하는 자원이 있는 경우
  • 실행 순서가 보장되지 않는 경우

따라서, 복수의 타임라인에서 문제가 발생할 경우 다음과 같이 해결할 수 있다.

  • 공유하는 자원이 있는 경우
    • 전역 변수를 지역 변수로 바꾸기
    • 암묵적 입력을 인자로 바꾸기
  • 실행 순서가 보장되지 않는 경우
    • 복수의 비동기 실행이 순서대로 진행되도록 하기 (queue)
    • 복수의 비동기 실행이 모두 진행된 후에 새로운 타임라인에서 진행되도록 하기 (cut)

2-3 아키텍쳐


T.B.D

0개의 댓글