모던지 / 컴퓨터과학 / 프로그래밍의 세계 2강. 같은 문제, 다른 사고방식

프로그래밍의 세계 2강. 같은 문제, 다른 사고방식

패러다임은 무엇을 할 수 있는지를 바꾸지 않습니다. 무엇을 하기 어렵게 만들지를 바꿉니다. 그 불편함이 곧 설계 철학입니다.

풀고 시작

문제 1. 어떤 함수를 순수 함수(pure function)라고 부르려면 무엇이 성립해야 할까요?
순수 함수의 조건은 두 가지로, 입력이 같으면 출력이 같아야 하고 외부의 변수나 파일 같은 바깥 세계를 건드리지 않아야 합니다. 전역 변수를 읽는 순간 첫 조건이 깨지고, 로그를 남기는 것은 그 자체가 바깥을 바꾸는 부수효과입니다. 재귀냐 반복이냐는 순수성과 무관한 구현 취향입니다.

명령의 순서로 세상을 기술하기

가장 오래된 사고방식은 절차형(procedural)입니다. 메모리에 값을 두고, 명령을 순서대로 실행하며, 그 값을 하나씩 바꿔 나갑니다. 이 방식이 자연스러운 이유는 하드웨어가 실제로 그렇게 생겼기 때문입니다. 폰 노이만 구조의 프로세서는 프로그램 카운터가 가리키는 명령을 읽고 실행한 뒤 다음으로 넘어가는 일을 반복하니까요. 절차형 언어는 그 동작을 거의 그대로 옮겨 놓은 셈입니다. 그래서 초기 프로그램은 아무 곳으로나 뛸 수 있는 goto로 흐름을 엮었고, 코드가 길어질수록 실행 경로를 사람이 따라갈 수 없게 되었습니다. 1968년 에츠허르 다익스트라가 goto의 무분별한 사용을 공개적으로 문제 삼으면서 순차와 분기와 반복이라는 정해진 구조만으로 프로그램을 짜자는 구조적 프로그래밍이 자리를 잡습니다. 흐름을 통제한 첫 번째 승리였습니다.

상태가 왜 그렇게 골칫거리일까

그런데 흐름을 정돈해도 남는 문제가 있습니다. 바로 상태(state)입니다. 어떤 함수가 무슨 일을 하는지 알려면 그 함수만 읽어서는 부족하고, 호출되는 시점에 바깥 변수들이 어떤 값이었는지까지 알아야 합니다. 함수가 계산 결과 말고 다른 것을 건드리는 일, 즉 전역 변수 수정이나 파일 쓰기 같은 일을 부수효과(side effect)라고 부릅니다. 부수효과가 많은 코드는 같은 입력에 다른 결과를 내므로 테스트하기 어렵고, 버그가 났을 때 원인을 특정하기도 어렵습니다. 동시성에서는 이 문제가 폭발합니다. 여러 스레드가 같은 메모리를 동시에 읽고 쓰면 실행 순서에 따라 결과가 달라지는 경쟁 상태(race condition)가 생기고, 이 결함은 재현이 어려워 가장 잡기 힘든 부류로 꼽힙니다. 멀티코어가 기본이 된 뒤 함수형이 다시 주목받은 실질적 이유가 여기 있습니다.

값을 바꾸지 않는다는 선택

함수형(functional) 패러다임은 문제의 뿌리를 끊는 쪽을 택합니다. 데이터를 만든 뒤에는 고치지 않는 불변성(immutability)을 기본으로 삼고, 값을 바꾸는 대신 바뀐 값을 가진 새 데이터를 만듭니다. 그리고 계산은 순수 함수로 표현합니다. 이 조합의 이득은 참조 투명성(referential transparency)이라는 성질입니다. 어떤 식을 그 결과값으로 바꿔 써도 프로그램의 의미가 달라지지 않는다는 뜻이고, 덕분에 사람은 코드를 수학의 식처럼 대체하며 추론할 수 있고 컴파일러는 마음 놓고 순서를 바꾸거나 병렬로 돌릴 수 있습니다. 물론 공짜는 아닙니다. 화면 출력도 파일 저장도 전부 부수효과이므로, 효과가 전혀 없는 프로그램은 아무 일도 하지 못합니다. 그래서 실무의 함수형은 효과를 없애는 대신 프로그램의 가장자리로 몰아넣고 안쪽은 순수하게 유지하는 전략을 씁니다. 값을 매번 새로 만드는 비용은 내부를 공유하는 지속 자료구조로 줄입니다.

객체지향이 약속한 것과 받은 비판

객체지향(object-oriented)은 또 다른 답을 내놓습니다. 상태를 없애는 대신 잘게 가두자는 것이죠. 1960년대 시뮬라(Simula)가 시뮬레이션 대상을 객체로 다루면서 씨앗이 뿌려졌고, 앨런 케이가 스몰토크에서 이 사고를 밀어붙였습니다. 케이 본인은 나중에 자기가 강조한 것은 클래스나 상속이 아니라 객체 사이의 메시지 전달이었다고 여러 차례 밝혔습니다. 핵심 장치는 두 가지입니다. 캡슐화는 데이터와 그것을 다루는 절차를 한 덩어리로 묶고 내부 표현을 감춰서, 안쪽 구현이 바뀌어도 바깥이 영향을 받지 않게 합니다. 다형성은 같은 호출에 객체마다 다르게 반응하게 해서, 부르는 쪽이 상대의 구체적인 종류를 몰라도 되게 만듭니다. 비판도 만만치 않습니다. 상속을 깊게 쌓으면 부모의 사소한 변경이 자식 전체를 무너뜨리고, 상태가 수많은 객체에 흩어지면 결국 전역 상태를 조각내 숨긴 것과 다르지 않습니다. 얼랭을 만든 조 암스트롱은 바나나 하나를 원했는데 바나나를 든 고릴라와 정글 전체가 딸려 왔다는 말로 이 얽힘을 꼬집었습니다.

언어들은 서로를 베끼는 중입니다

최근 흐름은 승자를 가리는 쪽이 아니라 섞이는 쪽입니다. 자바는 람다와 스트림을 들여왔고, 파이썬과 C#은 패턴 매칭을 문법에 들이고 값을 고칠 수 없는 레코드 타입을 표준으로 갖췄으며, 자바스크립트는 배열을 제자리에서 고치는 대신 새 배열을 돌려주는 방식이 관용이 되었습니다. 러스트는 소유권 규칙으로 누가 값을 바꿀 수 있는지를 컴파일 시점에 못 박아, 가변성을 없애는 대신 통제하는 제3의 길을 보여 줍니다. 그래서 오늘의 실무적 태도는 이렇습니다. 패러다임은 언어에 붙은 이름표가 아니라 문제마다 꺼내 드는 도구이고, 상태 변화가 본질인 영역에서는 객체가, 데이터 변환이 본질인 영역에서는 순수 함수가 유리합니다. 다음 강에서는 이 모든 패러다임이 공통으로 기대고 있는 무기, 추상화를 다룹니다.

인출 문제

문제 1. 구조적 프로그래밍이 goto의 자유로운 사용을 제한한 이유는 무엇일까요?
문제는 성능이나 번역 가능성이 아니라 사람의 이해 가능성이었습니다. 임의의 도약이 섞이면 코드의 어느 지점에 어떤 상태로 도달했는지 추적할 수 없어서, 다익스트라는 순차와 분기와 반복이라는 정해진 구조만 쓰자고 주장했습니다.
문제 2. 부수효과가 많은 코드가 특히 위험해지는 상황은 언제일까요?
공유 상태를 여러 스레드가 동시에 읽고 쓰면 실행 순서에 따라 결과가 달라지는 경쟁 상태가 생기고, 재현되지 않아 추적이 어렵습니다. 컴파일 시간이나 인자 개수는 가독성 문제일 뿐이고, 구현 방식 자체는 부수효과의 위험도와 관계가 없습니다.
문제 3. 참조 투명성이 프로그램에 주는 실질적 이득은 무엇일까요?
참조 투명성은 어떤 식을 그 값으로 치환해도 프로그램의 의미가 보존된다는 성질이라서, 사람은 수식처럼 대체하며 읽고 컴파일러는 순서를 바꾸거나 병렬화할 수 있습니다. 불변성은 값을 새로 만들기 때문에 메모리를 오히려 더 쓸 수도 있습니다.
문제 4. 객체지향에 대한 비판으로 실제로 제기되어 온 것은 무엇일까요?
취약한 기반 클래스 문제는 오래 지적되어 온 실제 비판이며, 상태가 여러 객체에 흩어지면 전역 상태를 조각내 숨긴 것과 같다는 지적도 함께 나옵니다. 나머지 보기들은 다형성과 캡슐화가 원래 해결하려던 바를 거꾸로 뒤집어 놓은 서술입니다.

생각해볼 질문 (정답 없음)

  • 언어들이 서로의 기능을 흡수해 하나같이 다중 패러다임이 되어 가는 흐름은 성숙일까요, 아니면 각 패러다임이 지키던 제약이 주던 이득을 잃는 일일까요.

이전: 프로그래밍의 세계 1강 · 다음: 프로그래밍의 세계 3강

모던지 · 궁금하면 모던지 GitHub · 2026-09-10