Skip to content

1장: 우리가 하고 있는 일이란 무엇인가?

서문

안녕하세요! 프랭클린 프리스비 교수입니다. 만나서 반갑습니다. 여러분에게 함수형 프로그래밍에 대해 가르치기 위해 함께 시간을 보내게 되었습니다. 제 이야기는 이쯤 하고, 여러분은 어떤가요? 최소한 자바스크립트에 어느 정도 익숙하고, 객체 지향 프로그래밍을 조금 경험해본 현업 개발자이시기를 바랍니다. 곤충학 박사학위는 필요없지만, 버그를 찾아서 해결하는 방법은 알고 계셔야 합니다.

여러분이 함수형 프로그래밍 지식이 있을 것이라고 가정하지 않을 것입니다. 「가정」이 오해를 낳을 수 있으니까요. 다만 가변 상태(mutable state), 제한 없는 부작용(side effect), 원칙 없는 설계로 작업할 때 발생하는 문제 상황을 경험해보셨을 것이라 예상합니다. 이제 서로를 알았으니, 본론으로 들어가 보죠.

이 장의 목적은 함수형 프로그램을 작성할 때 우리가 추구하는 것이 무엇인지 느낌을 전하는 데 있습니다. 다음 장을 이해하려면 무엇이 프로그램을 함수형으로 만드는지에 대한 개념이 필요합니다. 그렇지 않으면 객체(object)를 무조건 피하면서 목적 없이 코드를 난잡하게 작성하는 어색한 상황에 빠지게 됩니다. 거친 파도를 헤쳐나갈 천체 컴퍼스처럼, 우리 코드를 명확히 조준할 표적이 필요합니다.

일반적인 프로그래밍 원칙들로는 DRY(반복 금지), YAGNI(필요 없을 것), 느슨한 결합/높은 응집도, 최소 놀람 원칙, 단일 책임 원칙 등 다양한 두문자어 신조들이 있습니다.

여러 해 동안 들었던 모든 가이드라인을 일일이 나열하지는 않겠습니다... 핵심은 이 원칙들이 함수형 환경에서도 유효하지만, 우리의 궁극적 목표와는 표면적으로만 연관되어 있다는 점입니다. 더 진행하기 전에, 키보드를 두드릴 때 우리의 의도가 무엇인지, 함수형 낙원이 어떤 것인지 느껴보시기 바랍니다.

짧은 만남

조금은 비현실적인 예로 시작해봅시다. 갈매기 애플리케이션입니다. 무리가 결합하면 더 큰 무리가 되고, 번식할 때는 교미한 갈매기 수만큼 증가합니다. 주의할 점은 이 코드가 좋은 객체 지향 코드가 아니며, 현대의 할당(assignment) 기반 접근법의 위험을 강조하기 위한 예시입니다.

js
class Flock {
  constructor(n) {
    this.seagulls = n;
  }

  conjoin(other) {
    this.seagulls += other.seagulls;
    return this;
  }

  breed(other) {
    this.seagulls = this.seagulls * other.seagulls;
    return this;
  }
}

const flockA = new Flock(4);
const flockB = new Flock(2);
const flockC = new Flock(0);
const result = flockA
  .conjoin(flockC)
  .breed(flockB)
  .conjoin(flockA.breed(flockB))
  .seagulls;
// 32

이런 끔찍한 괴물 코드를 누가 만들까요? 변화하는 내부 상태를 추적하는 건 현실적으로 불가능합니다. 게다가 결과값도 틀렸습니다! 정답은 16이어야 하지만 flockA는 과정에서 영구적으로 변경되었습니다. 이건 IT 세상의 무정부 상태입니다! 야생 동물 산술이죠!

이 프로그램을 이해하지 못해도 괜찮습니다. 저도 마찬가지니까요. 기억할 점은 상태와 가변값은 이처럼 작은 예제에서도 추적하기 어렵다는 것입니다.

이번에는 함수형 접근법으로 다시 시도해 봅시다:

js
const conjoin = (flockX, flockY) => flockX + flockY;
const breed = (flockX, flockY) => flockX * flockY;

const flockA = 4;
const flockB = 2;
const flockC = 0;
const result =
    conjoin(breed(flockB, conjoin(flockA, flockC)), breed(flockA, flockB));
// 16

이번에는 정확한 답을 얻었습니다. 훨씬 적은 코드로요. 함수 중첩이 약간 혼란스럽지만(5장에서 해결할 예정), 그래도 개선되었습니다. 문제를 정확히 명명하면 이점이 있습니다. 커스텀 함수를 자세히 살펴보면 단순 덧셈(conjoin)과 곱셈(breed)을 수행하고 있음을 발견할 수 있습니다.

이 두 함수는 이름 외에 특별한 점이 없습니다. 실제 동작을 드러내기 위해 multiply(곱셈)와 add(덧셈)로 함수명을 변경해 보죠.

js
const add = (x, y) => x + y;
const multiply = (x, y) => x * y;

const flockA = 4;
const flockB = 2;
const flockC = 0;
const result =
    add(multiply(flockB, add(flockA, flockC)), multiply(flockA, flockB));
// 16

이를 통해 고대의 지혜를 얻습니다:

js
// associative
add(add(x, y), z) === add(x, add(y, z));

// commutative
add(x, y) === add(y, x);

// identity
add(x, 0) === x;

// distributive
multiply(x, add(y,z)) === add(multiply(x, y), multiply(x, z));

오랜 신뢰를 받는 이 수학적 속성들이 유용하게 쓰일 겁니다. 당장 머릿속에 떠오르지 않아도 괜찮습니다. 이 산술 법칙들을 배운 지 오래된 분들이 많을 테니까요. 이 속성들을 이용해 갈매기 프로그램을 단순화할 수 있는지 살펴봅시다.

js
// Original line
add(multiply(flockB, add(flockA, flockC)), multiply(flockA, flockB));

// Apply the identity property to remove the extra add
// (add(flockA, flockC) == flockA)
add(multiply(flockB, flockA), multiply(flockA, flockB));

// Apply distributive property to achieve our result
multiply(flockB, add(flockA, flockA));

훌륭합니다! 호출 함수 외에 커스텀 코드를 전혀 작성할 필요가 없었습니다. add와 multiply 정의는 완전성을 위해 포함했으나 실제로 작성할 필요는 없습니다 - 기존 라이브러리에서 제공할 테니까요.

「수학적 예시를 앞에 내세우다니 허수아비 논증 같다」거나 「실제 프로그램은 이렇게 단순하지 않고 그런 식으로 추론할 수 없다」고 생각할 수 있습니다. 이 예시를 선택한 이유는 대부분이 덧셈/곱셈을 이미 알고 있어 수학이 여기서 얼마나 유용한지 쉽게 보여주기 위함입니다.

좌절하지 마세요. 이 책 전반에 걸쳐 범주론, 집합론, 람다 계산법을 적용해 갈매기 무리 예시처럼 우아한 단순함과 결과를 달성하는 실제 예제들을 작성할 것입니다. 수학자가 될 필요도 없습니다. 일반 프레임워크나 API 사용처럼 자연스럽고 쉬울 겁니다.

위 함수형 유사체와 유사한 일상 애플리케이션을 완전히 작성할 수 있다고 하면 놀라실 수 있습니다. 건전한 속성을 가진 프로그램, 간결하면서도 추론하기 쉬운 프로그램, 매번 바퀴를 재발명하지 않는 프로그램을 말이죠. 무법은 범죄자에게 좋지만, 이 책에서는 수학 법칙을 인정하고 준수할 것입니다.

모든 조각이 정중하게 어우러지는 이론을 사용할 것입니다. 특정 문제를 재사용 가능한 구성 요소로 표현한 다음, 이들의 속성을 활용할 것입니다. '무엇이든 허용'하는 명령형 접근 방식보다 더 많은 수련이 필요하겠지만(명령형의 정확한 정의는 차후 설명), 원칙 있는 수학적 프레임워크에서 작업하는 보상은 여러분을 놀라게 할 겁니다.

함수형 북극성의 빛을 보았지만, 본격적인 여정을 시작하기 전에 몇 가지 구체적인 개념을 이해해야 합니다.

2장: 일급 함수