해당 게시글은 mostly-adequate-guide 의 시리즈들을 번역했습니다.
해당게시글의 저작권은 mostly-adequate-guide 의 저작권인 Attribution-ShareAlike 4.0 International 을 준수합니다.
🤖 AI가 요약한 글이예요 !
이 글은 함수형 프로그래밍의 기본 개념을 소개합니다.
객체 지향 프로그래밍의 잠재적 문제점을 간단한 예시로 보여주고,
상태 변경과 부수 효과가 코드 이해를 어렵게 만들 수 있음을 지적합니다.
함수형 접근 방식이 어떻게 더 간결하고 예측 가능한 코드를 만드는지 설명하며,
수학적 원리가 프로그래밍에 어떻게 도움이 되는지 강조합니다.
챕터 01: 우리는 대체 무엇을 하고 있는 걸까요?
소개
안녕하세요! 저는 프랭클린 프리즈비 교수입니다. 만나서 반갑습니다. 저는 여러분에게 함수형 프로그래밍에 대해 조금 가르쳐 드리기로 되어 있어서, 우리는 함께 시간을 보낼 것입니다. 제 얘기는 이쯤 하고, 여러분은 어떠신가요? 저는 여러분이 자바스크립트 언어에 적어도 조금은 익숙하고, 객체 지향 경험이 약간 있으며, 스스로를 일하는 프로그래머라고 생각하시기를 바랍니다. 곤충학 박사 학위가 필요하거나 그러진 않습니다. 단순히 몇 가지 버그를 찾고 스스로 해결할 수 있을 정도면 됩니다!
저는 여러분들이 함수형 프로그래밍 경험이 있다고 가정하지 않겠습니다. 섣불리 한 가정에서 오는 여러 부작용들이 어떤 일이 발생하는지 잘 알고 있기 때문입니다. 저는 여러분들이 가변 상태(mutable state) 에 의한 부수 효과(side effects) 를 경험했거나 원칙 없는 설계로 작업할 때 발생하는 몇 가지 어려움들을 겪었을 거라 생각합니다.
가변 상태(Mutable State): 프로그램 실행 중에 값이 변경될 수 있는 데이터를 의미합니다. 예를 들어, 객체의 속성이나 변수의 값이 직접 수정될 수 있는 경우입니다.
부수 효과(Side Effects): 함수가 결과값을 반환하는 것 외에 외부 상태를 변경하거나 외부 세계와 상호작용하는 것을 말합니다. 예를 들어, 전역 변수를 수정하거나, 파일에 쓰거나, 네트워크 요청을 보내는 등이 있습니다.
이제 제대로 소개를 마쳤으니, 계속 진행해 봅시다.
이 챕터의 목적은 함수형 프로그램을 작성할 때 우리가 추구하는 것이 무엇인지 감을 잡게 해드리는 것입니다. 다음 챕터들을 이해하기 위해서는 프로그램이 함수형이라는 것이 무엇을 의미하는지에 대한 아이디어가 있어야 합니다. 그렇지 않으면 우리는 목적 없이 마구잡이로 코드를 작성하고, 무슨 수를 써서든 객체를 피하려고 할 것입니다. 참으로 어설픈 노력이죠. 우리는 코드를 던질 명확한 과녁이 필요하고, 거친 물살을 헤쳐 나갈 때 필요한 천상의 나침반 같은 것이 필요합니다.
이제, 일반적인 프로그래밍 원칙들이 있습니다. 다양한 약어 신조들이 어떤 애플리케이션의 어두운 터널을 통과하도록 우리를 안내합니다: DRY(반복하지 말 것), YAGNI(필요 없을 거야), 느슨한 결합과 높은 응집도, 최소 놀람의 원칙, 단일 책임 원칙 등등이 있습니다.
DRY (Don't Repeat Yourself): 코드 중복을 피하라는 원칙입니다. 동일한 로직이 여러 곳에 반복되면 유지보수가 어려워집니다.
YAGNI (You Ain't Gonna Need It): 현재 필요하지 않은 기능은 미리 구현하지 말라는 원칙입니다. 미래의 요구사항을 예측하여 과도하게 설계하는 것을 방지합니다.
느슨한 결합과 높은 응집도 (Loose Coupling, High Cohesion): 모듈 간의 의존성은 낮추고(느슨한 결합), 모듈 내부의 요소들은 서로 밀접하게 관련되어야 한다(높은 응집도)는 설계 원칙입니다.
최소 놀람의 원칙 (Principle of Least Surprise): 시스템의 동작이 사용자가 예상하는 방식과 일치해야 한다는 원칙입니다.
단일 책임 원칙 (Single Responsibility Principle): 클래스나 모듈은 단 하나의 책임(변경 이유)만 가져야 한다는 원칙입니다.
제가 수년간 들어온 모든 지침을 일일이 나열하며 여러분을 지치게 하지는 않겠습니다... 요점은 그것들이 함수형 환경에서도 유효하다는 것입니다. 비록 우리의 궁극적인 목표와는 단지 접선 관계에 불과하지만요. 제가 더 나아가기 전에 지금 여러분이 느끼셨으면 하는 것은, 우리가 키보드를 두드릴 때의 의도입니다; 우리의 함수형 이상향(functional Xanadu) 말입니다.
짧은 만남
약간의 광기로 시작해 봅시다. 여기 갈매기 애플리케이션이 있습니다. 무리가 합쳐지면 더 큰 무리가 되고, 번식하면 함께 번식하는 갈매기 수만큼 증가합니다. 자, 이것은 좋은 객체 지향 코드를 의도한 것이 아닙니다. 주의하세요. 이것은 우리의 현대적인, 할당 기반 접근 방식의 위험을 강조하기 위해 여기에 있습니다. 보시죠:
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
가 영구적으로 변경되어 버렸습니다. 불쌍한 flockA
. 이것은 IT 세계의 무정부 상태입니다! 이것은 야생 동물의 산수입니다!
이 프로그램을 이해하지 못하셔도 괜찮습니다. 저도 마찬가지입니다. 여기서 기억해야 할 점은 상태와 가변 값은 이렇게 작은 예제에서도 따라가기 어렵다는 것입니다.
다시 시도해 봅시다. 이번에는 좀 더 함수형 접근 방식을 사용합니다:
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
로 바꿔 봅시다.
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
그리고 그것으로 우리는 고대의 지식을 얻습니다:
// 결합 법칙 (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));
아 네, 그 오래되고 충실한 수학적 속성들이 유용하게 쓰일 것입니다. 만약 여러분이 그것들을 바로 떠올리지 못했더라도 걱정하지 마세요. 우리 중 많은 사람들에게는 이러한 산술 법칙을 배운 지 꽤 오래되었습니다. 이 속성들을 사용하여 우리의 작은 갈매기 프로그램을 단순화할 수 있는지 봅시다.
// 원래 코드
add(multiply(flockB, add(flockA, flockC)), multiply(flockA, flockB));
// 항등원 속성을 적용하여 불필요한 덧셈 제거
// (add(flockA, flockC) == flockA)
add(multiply(flockB, flockA), multiply(flockA, flockB));
// 분배 법칙을 적용하여 결과 도출
multiply(flockB, add(flockA, flockA));
훌륭합니다! 우리는 호출 함수 외에는 사용자 정의 코드를 한 줄도 작성할 필요가 없었습니다. 완전성을 위해 여기에 add
와 multiply
정의를 포함했지만, 실제로는 작성할 필요가 없습니다. 우리는 분명히 기존 라이브러리에서 제공하는 add
와 multiply
를 가지고 있을 것입니다.
여러분은 "이렇게 수학적인 예제를 앞에 내세우다니 정말 허수아비 공격 같군요"라고 생각할 수도 있습니다. 또는 "실제 프로그램은 이렇게 간단하지 않고 그런 식으로 추론할 수 없습니다"라고 생각할 수도 있습니다. 저는 이 예제를 선택했습니다. 왜냐하면 우리 대부분은 이미 덧셈과 곱셈에 대해 알고 있기 때문에, 수학이 여기서 우리에게 얼마나 유용한지 쉽게 알 수 있기 때문입니다.
절망하지 마세요. 이 책 전반에 걸쳐, 우리는 약간의 범주론(category theory), 집합론(set theory), 그리고 람다 대수(lambda calculus) 를 뿌리고, 우리의 갈매기 무리 예제와 같은 우아한 단순성과 결과를 달성하는 실제 예제를 작성할 것입니다.
범주론(Category Theory): 수학적 구조와 그들 사이의 관계를 추상적으로 다루는 이론입니다. 함수형 프로그래밍에서는 타입과 함수를 각각 객체와 사상으로 보고, 이들의 합성과 변환을 다루는 데 사용됩니다.
집합론(Set Theory): 집합의 개념과 그 속성을 연구하는 수학의 한 분야입니다. 함수형 프로그래밍의 타입 시스템이나 데이터 구조를 이해하는 데 기초가 됩니다.
람다 대수(Lambda Calculus): 계산 가능성에 대한 형식 시스템으로, 함수 정의, 함수 적용, 변수 바인딩 등을 다룹니다. 함수형 프로그래밍 언어의 이론적 기반이 됩니다.
수학자일 필요도 없습니다. 마치 "일반적인" 프레임워크나 API를 사용하는 것처럼 자연스럽고 쉽게 느껴질 것입니다.
위의 함수형 유사체와 같은 방식으로 완전한 일상적인 애플리케이션을 작성할 수 있다는 말을 들으면 놀랄 수도 있습니다. 건전한 속성을 가진 프로그램. 간결하면서도 추론하기 쉬운 프로그램. 매번 바퀴를 재발명하지 않는 프로그램. 무법은 범죄자에게는 좋지만, 이 책에서는 수학의 법칙을 인정하고 준수하기를 원할 것입니다.
우리는 모든 조각이 서로 정중하게 맞아떨어지는 경향이 있는 이론을 사용하기를 원할 것입니다. 우리는 우리의 특정 문제를 일반적이고 조합 가능한 비트들로 표현한 다음, 우리 자신의 이기적인 이익을 위해 그 속성들을 이용하기를 원할 것입니다. 명령형 프로그래밍(imperative programming) 의 "뭐든지 괜찮아" 접근 방식보다는 약간 더 많은 규율이 필요할 것입니다 (우리는 "명령형"의 정확한 정의를 책의 뒷부분에서 다룰 것이지만, 지금은 함수형 프로그래밍이 아닌 모든 것으로 간주하세요). 원칙에 입각한 수학적 프레임워크 내에서 작업하는 것의 보상은 정말로 여러분을 놀라게 할 것입니다.
명령형 프로그래밍(Imperative Programming): 프로그램의 상태를 변경하는 문장들을 사용하여 계산을 수행하는 프로그래밍 패러다임입니다. 어떻게(How) 작업을 수행할지 명시하는 데 중점을 둡니다. (예: C, Java, Python의 일반적인 절차적/객체지향적 사용 방식)
우리는 우리의 함수형 북극성의 희미한 빛을 보았지만, 우리의 여정을 정말로 시작하기 전에 파악해야 할 몇 가지 구체적인 개념이 있습니다.