프로그래밍의 세계 2강. 같은 문제, 다른 사고방식
패러다임은 무엇을 할 수 있는지를 바꾸지 않습니다. 무엇을 하기 어렵게 만들지를 바꿉니다. 그 불편함이 곧 설계 철학입니다.
풀고 시작
명령의 순서로 세상을 기술하기
가장 오래된 사고방식은 절차형(procedural)입니다. 메모리에 값을 두고, 명령을 순서대로 실행하며, 그 값을 하나씩 바꿔 나갑니다. 이 방식이 자연스러운 이유는 하드웨어가 실제로 그렇게 생겼기 때문입니다. 폰 노이만 구조의 프로세서는 프로그램 카운터가 가리키는 명령을 읽고 실행한 뒤 다음으로 넘어가는 일을 반복하니까요. 절차형 언어는 그 동작을 거의 그대로 옮겨 놓은 셈입니다. 그래서 초기 프로그램은 아무 곳으로나 뛸 수 있는 goto로 흐름을 엮었고, 코드가 길어질수록 실행 경로를 사람이 따라갈 수 없게 되었습니다. 1968년 에츠허르 다익스트라가 goto의 무분별한 사용을 공개적으로 문제 삼으면서 순차와 분기와 반복이라는 정해진 구조만으로 프로그램을 짜자는 구조적 프로그래밍이 자리를 잡습니다. 흐름을 통제한 첫 번째 승리였습니다.
상태가 왜 그렇게 골칫거리일까
그런데 흐름을 정돈해도 남는 문제가 있습니다. 바로 상태(state)입니다. 어떤 함수가 무슨 일을 하는지 알려면 그 함수만 읽어서는 부족하고, 호출되는 시점에 바깥 변수들이 어떤 값이었는지까지 알아야 합니다. 함수가 계산 결과 말고 다른 것을 건드리는 일, 즉 전역 변수 수정이나 파일 쓰기 같은 일을 부수효과(side effect)라고 부릅니다. 부수효과가 많은 코드는 같은 입력에 다른 결과를 내므로 테스트하기 어렵고, 버그가 났을 때 원인을 특정하기도 어렵습니다. 동시성에서는 이 문제가 폭발합니다. 여러 스레드가 같은 메모리를 동시에 읽고 쓰면 실행 순서에 따라 결과가 달라지는 경쟁 상태(race condition)가 생기고, 이 결함은 재현이 어려워 가장 잡기 힘든 부류로 꼽힙니다. 멀티코어가 기본이 된 뒤 함수형이 다시 주목받은 실질적 이유가 여기 있습니다.
값을 바꾸지 않는다는 선택
함수형(functional) 패러다임은 문제의 뿌리를 끊는 쪽을 택합니다. 데이터를 만든 뒤에는 고치지 않는 불변성(immutability)을 기본으로 삼고, 값을 바꾸는 대신 바뀐 값을 가진 새 데이터를 만듭니다. 그리고 계산은 순수 함수로 표현합니다. 이 조합의 이득은 참조 투명성(referential transparency)이라는 성질입니다. 어떤 식을 그 결과값으로 바꿔 써도 프로그램의 의미가 달라지지 않는다는 뜻이고, 덕분에 사람은 코드를 수학의 식처럼 대체하며 추론할 수 있고 컴파일러는 마음 놓고 순서를 바꾸거나 병렬로 돌릴 수 있습니다. 물론 공짜는 아닙니다. 화면 출력도 파일 저장도 전부 부수효과이므로, 효과가 전혀 없는 프로그램은 아무 일도 하지 못합니다. 그래서 실무의 함수형은 효과를 없애는 대신 프로그램의 가장자리로 몰아넣고 안쪽은 순수하게 유지하는 전략을 씁니다. 값을 매번 새로 만드는 비용은 내부를 공유하는 지속 자료구조로 줄입니다.
객체지향이 약속한 것과 받은 비판
객체지향(object-oriented)은 또 다른 답을 내놓습니다. 상태를 없애는 대신 잘게 가두자는 것이죠. 1960년대 시뮬라(Simula)가 시뮬레이션 대상을 객체로 다루면서 씨앗이 뿌려졌고, 앨런 케이가 스몰토크에서 이 사고를 밀어붙였습니다. 케이 본인은 나중에 자기가 강조한 것은 클래스나 상속이 아니라 객체 사이의 메시지 전달이었다고 여러 차례 밝혔습니다. 핵심 장치는 두 가지입니다. 캡슐화는 데이터와 그것을 다루는 절차를 한 덩어리로 묶고 내부 표현을 감춰서, 안쪽 구현이 바뀌어도 바깥이 영향을 받지 않게 합니다. 다형성은 같은 호출에 객체마다 다르게 반응하게 해서, 부르는 쪽이 상대의 구체적인 종류를 몰라도 되게 만듭니다. 비판도 만만치 않습니다. 상속을 깊게 쌓으면 부모의 사소한 변경이 자식 전체를 무너뜨리고, 상태가 수많은 객체에 흩어지면 결국 전역 상태를 조각내 숨긴 것과 다르지 않습니다. 얼랭을 만든 조 암스트롱은 바나나 하나를 원했는데 바나나를 든 고릴라와 정글 전체가 딸려 왔다는 말로 이 얽힘을 꼬집었습니다.
언어들은 서로를 베끼는 중입니다
최근 흐름은 승자를 가리는 쪽이 아니라 섞이는 쪽입니다. 자바는 람다와 스트림을 들여왔고, 파이썬과 C#은 패턴 매칭을 문법에 들이고 값을 고칠 수 없는 레코드 타입을 표준으로 갖췄으며, 자바스크립트는 배열을 제자리에서 고치는 대신 새 배열을 돌려주는 방식이 관용이 되었습니다. 러스트는 소유권 규칙으로 누가 값을 바꿀 수 있는지를 컴파일 시점에 못 박아, 가변성을 없애는 대신 통제하는 제3의 길을 보여 줍니다. 그래서 오늘의 실무적 태도는 이렇습니다. 패러다임은 언어에 붙은 이름표가 아니라 문제마다 꺼내 드는 도구이고, 상태 변화가 본질인 영역에서는 객체가, 데이터 변환이 본질인 영역에서는 순수 함수가 유리합니다. 다음 강에서는 이 모든 패러다임이 공통으로 기대고 있는 무기, 추상화를 다룹니다.
인출 문제
생각해볼 질문 (정답 없음)
- 언어들이 서로의 기능을 흡수해 하나같이 다중 패러다임이 되어 가는 흐름은 성숙일까요, 아니면 각 패러다임이 지키던 제약이 주던 이득을 잃는 일일까요.
이전: 프로그래밍의 세계 1강 · 다음: 프로그래밍의 세계 3강