News Feed

불변성이 코드 세계의 ‘서부 개척 시대’를 끝냈다

컨텐츠 정보

  • 조회 493

본문

1970년대, 필자는 BASIC으로 프로그래밍을 시작했다. 그 시절에는 코드의 이곳저곳을 자유롭게 이동하고 이전 위치로 되돌아올 수 있는 새로운 언어 기능인 GOSUB을 발견하고 큰 감동을 받았다.

당시 언어에는 변수는 있었지만 상수는 없었다. 매개변수 개념도 없었고, GOSUB으로 충분하다고 여겨졌다. 범위(scope) 개념은 전혀 존재하지 않았다. 모든 것이 전역 변수였다. 말 그대로 혼돈이었다.

불변성은 그러한 ‘와일드 웨스트’ 시절의 정반대 개념이다. 모든 것이 허용되던 시기에서, 이제는 ‘모든 것을 고정시키고 무대 뒤에서 어떤 것도 흔들리지 않게 하라’는 철학으로 옮겨왔다.
처음 상수 개념을 접했을 때는 왜 그런 것이 필요한지조차 의문이었다.

불변성 개념은 여전히 과소평가되는 경향이 있다. 과거의 잔재 때문일까? 하지만 이제 우리는 모든 시스템이 극도로 복잡해지고 있다는 사실을 인지하고 있으며, 불변성이 좋고 깔끔한 코드의 초석임을 알고 있다.

오늘날에는 대규모, 복잡한 멀티쓰레드 애플리케이션이 일반적이며, 과거 방식은 매우 성가신 결과를 초래한다.

변화는 해롭다

초보 개발자가 가장 먼저 배워야 할 교훈 중 하나는 전역 변수는 선을 넘는 범죄라는 것이다. 변수가 럭비공처럼 이리저리 전달되고 그 상태가 도중에 아무 데서나 바뀔 수 있다면, 실제로 그렇게 바뀌게 된다. 결과적으로 이는 좌절과 분노를 유발한다. 전역 변수는 결합도(coupling)를 만들어내며, 깊고 넓은 결합도야말로 개발에 있어 가장 심각한 문제다.

처음에는 불변성이 이상하게 느껴진다. 변수를 없애다니, 말이 되나? 당연히 뭔가는 바뀌어야 하지 않나? 주문 수량이나 합계를 어떻게 추적할 수 있겠는가?

피자 주문을 예로 들어보자. 페퍼로니 피자를 주문하면 처음부터 그렇게 구워지길 원한다. 어제의 야채 피자 조각에서 버섯을 긁어내고 페퍼로니를 얹어 다시 데워준다면 끔찍할 것이다.

변수와 함수도 마찬가지다. 기존 조각을 재활용하지 말고, 매번 새로운 조각을 만들어야 한다. 수정된 조각이 아니라 새로운 조각을 원한다는 것이다.

불변성은 사고방식의 강력한 전환이다. ‘존재하는 것을 변경하기’에서 ‘기존 것을 바꾸지 않고 새로 만들기’로 사고를 전환해야 한다. 이 단순한 전환만으로도 코드는 훨씬 더 안전하고 이해하기 쉬워진다.

순수 함수는 바람직하다

불변성의 핵심은 순수 함수 개념에 있다. 순수 함수는 동일한 입력에 대해 항상 동일한 출력을 반환하는 함수다. 이는 결정론적(deterministic)이라고 불리며, 출력이 100% 입력에 의해 결정된다.

좀 더 단순하게 말하자면, 순수 함수는 부작용이 없는 함수다. 사용자 모르게 무언가를 변경하지 않는다.

다음과 같은 경험은 대부분 해봤을 것이다.

function addPie(items: string[]) {  items.push("Apple Pie"); // side effect!  return items;}const order = ["Burger", "Fries"];const before = order;const updated = addPie(order);console.log("before:", before); // ["Burger", "Fries", "Apple Pie"] ← oopsconsole.log("updated:", updated); // ["Burger", "Fries", "Apple Pie"]

예를 들어, addPie 함수가 있다고 하자. 이 함수는 순수하지 않으므로 부작용이 존재한다. 넘긴 배열 items 자체를 변경하게 되고, 따라서 before 참조도 바뀌게 된다. 이런 상황은 개발자가 예상하지 못할 수 있으며, 데이터가 공유되는 상황에서는 가변성이 문제를 더욱 증폭시킨다.

하지만 불변성을 제공하는 함수라면 어떻게 될까?

function addPieImmutable(items: string[]) {  return [...items, "Apple Pie"]; // no side effects, new array}const order = ["Burger", "Fries"];const before = order;const updated = addPieImmutable(order);console.log("before:", before);   // ["Burger", "Fries"] stableconsole.log("updated:", updated); // ["Burger", "Fries", "Apple Pie"]

이 경우, before 참조는 그대로 유지된다. 기존 주문을 갱신하는 대신, 새로운 주문 객체를 생성(updated)했기 때문이다.

변화는 일어난다

위 예시는 단순하지만, 두 번째 버전에서는 경쟁 조건이나 데이터 충돌이 절대 발생할 수 없다. 기존 주문은 바뀌지 않기 때문이다.
불변성은 ‘변화가 없다’는 뜻이 아니다. 한 번 생성된 값은 바뀌지 않는다는 뜻이다. 여전히 ‘변경’은 일어나지만, 그것은 이름을 새로운 값에 다시 바인딩하는 방식으로 이뤄진다.

‘이전 상태’와 ‘이후 상태’의 개념은 실행 취소, 감사 추적, 상태 변경 이력 기록과 같은 기능을 원할 때 반드시 필요하다.

GOSUB이 혁신으로 느껴졌던 과거가 이제는 참으로 소박하게 느껴진다. 지금의 큰 혁신은 불변성과 순수 함수가 전역 변수로 인한 예측 불가능한 문제를 제거하고 코드에 일관성과 안정성을 제공한다는 사실이다.
dl-itworldkorea@foundryco.com

관련자료

댓글 0
등록된 댓글이 없습니다.
Member Rank