프로그래밍 입문서의 문제점에서 변수의 사용은 나쁜 프로그래밍 습관이니, 프로그래밍 입문서에서 변수 사용을 장려하지 않는 것이 좋겠다는 이야기를 했습니다. 그런데 입문자가 아니라 현업 개발자들조차 변수 사용이 왜 나쁜 습관인지, 왜 복잡한 소프트웨어 작성에 문제가 되는지 잘 모르고 계신 것 같아서 관련 내용을 보강합니다.
일단 “전역 변수(global variable)는 나쁘다”라는 말로 시작하겠습니다. 이 말에는 비교적 이견 없이 많은 개발자들이 동의할 것입니다. 전역 변수의 문제점을 정리해보면
- Non-locality – 변수 범위가 넓어서 이해하기가 어려움. 프로그램 여기 저기서 값을 읽거나 변경할 수 있으므로 사용처를 기억하기 어려움.
- No Access Control or Constraint Checking – 프로그램 어디서나 값을 읽거나 변경 가능하고, 접근 제한이 없어서 사용과 관련 규칙을 잊거나 깨먹기 쉬움.
- Implicit coupling – 프로그램 내 변수와 함수 등 여러 요소들과 커플링이 심해짐.
- Concurrency issues – 동시에 여러 쓰레드가 값을 변경할 수 있으므로 동기화가 필요함.
- Namespace pollution – 전역 변수는 모든 곳에서 보이므로 네임스페임스가 오염됨.
- Memory allocation issues – 전역 변수가 메모리 할당 외에 side effect가 있으면 문제 발생 소지가 큼. (상호 의존 등)
- Testing and Confinement – 테스트를 위한 깨끗한 환경을 만들 수 없으므로 테스트하기가 어려워짐.
우리가 전역 변수의 문제점을 이야기할 때 보통 문제가 “전역”에 있는지 “변수”에 있는지 구분하지 않고 “전역 변수”가 문제라고 이야기합니다. 하지만 각 요소를 하나씩 바꿔서 다시 질문해 보면 어떨까요? 전역 상수는 뭐가 문제일까요? 값을 쓰지 않기 때문에 앞서 언급한 문제점 중 “namespace pollution” 외에 다른 문제는 일어날 수가 없습니다. 반대로 지역 변수는 뭐가 문제일까요? “namespace pollution” 외에는 모든 문제가 일어납니다. 다만, 범위가 작기 때문에 전역 변수에 비해 피해의 규모가 작을 뿐입니다.
지역 변수가 문제가 되는 예를 하나 보겠습니다. 다음 makeAdders()
함수는 인자 n
을 받아서 0을 더해주는 함수, 1을 더해주는 함수, … n
을 더해주는 함수의 배열을 리턴합니다. 따라서 adders[5]
에는 5를 더해주는 함수가 들어 있게 됩니다. add5(2)
는 5+2=7을 리턴할 것으로 예상되지만, 실제로 코드를 돌려보면 놀랍게도 12가 리턴됩니다. 이런 문제가 발생하는 이유는 클로저에서 캡춰한 변수 i
가 루프를 돌면서 계속 갱신되어 루프가 끝날 때 10이 되는데, 그 전에 캡춰한 모든 클로저의 i
값도 10으로 갱신되기 때문입니다.
function makeAdders(n) { var adders = []; for (var i = 0; i < n; i++) { adders.push(function (x) { return i + x; }); } return adders; } var adders = makeAdders(10); var add5 = adders[5]; var r = add5(2); console.log(r); // We expect 7, but 12 is printed.
이 문제는 C#에도 있었습니다. 그래서 C#팀의 Eric Lippert는 Closing over the loop variable considered harmful 글에서 아에 루프 변수를 클로저 내에서 사용하지 말라는 이야기도 했습니다.
그런데 C# 5에서는 루프 변수가 논리적으로 루프에 속하는 것으로 보고, 루프 변수를 클로저에서 캡처하면 항상 새로운 값을 복사해서 주게 바뀌었습니다. 이렇게 바꾸면 이전 버전 C#과 하위호환성이 깨지는데도, 심각한 문제라고 보고 수정을 감행한 겁니다. 이런 예를 보면 C# 언어 설계자들조차 변수와 클로저의 조합이 어떤 문제를 일으킬지 충분히 검토를 못했다는 사실도 알 수 있습니다.
LINQ와 Rx를 만든 Erik Meijer가 최근한 ACMQueue에 기고한 The Curse of the Excluded Middle을 보면 저보다 더 극단적입니다. Scala나 F# 같은 함수 언어조차도 side effect로 인해 심각한 문제를 겪고 있으니 좀 더 근본주의적으로 접근해서 모든 effect를 순수 함수 언어로 관리해야 한다는 주장입니다. 여기서 side effect는 변수뿐만 아니라 IO, exception, 쓰레드 등까지 모두 포함하고 있습니다.
소프트웨어의 복잡성은 상태(state)에 있고, 좋은 프로그래밍은 결국 상태를 효과적으로 관리하는 방법을 찾는 겁니다. 객체지향 프로그래밍은 캡슐화(encapsulation)를 통해 서로 연관된 상태를 한 곳에 모아서 상태 관리의 복잡성을 줄이고, 함수형 프로그래밍은 불변 데이터 타입(immutable data type)을 통해 아에 상태를 만들지 않거나, 모나드와 같은 수학적인 구조를 이용해 상태를 효과적으로 제어합니다.
그런데 생활 코딩뿐만 아니라 대부분의 프로그래밍 입문서는 굳이 변수가 필요하지 않은 부분에서 너무 변수를 많이 사용하는 경향이 있습니다. 덕분에 우리가 초중고 과정을 통해 배운 수학적인 함수의 계산 방법이 훨씬 더 직관적이고 자연스러움에도 불구하고, 프로그래밍 입문 과정을 통해 이런 계산 방식을 잊고 프로그램의 수행을 메모리를 읽고 쓰는 것으로 해석하는 방법을 배우게 됩니다.
변수의 사용을 될 수 있으면 피하는 게 입문자들에게 프로그래밍을 더 쉽게 가르치는 방법이고 더 좋은 프로그래밍 스타일이기도 합니다. 변수는 꼭 필요한 경우에만 제한적으로 사용하는 일종의 “블랙 매직”으로 생각하고, 프로그래밍 과정에서도 될 수 있으면 나중에 다루는 게 오히려 프로그래밍을 제대로 가르치는 방법이라고 생각합니다. SICP에서 mutable data를 251페이지에 가서야 가르치는 이유에 대해서도 생각해 볼 필요가 있습니다.
지금 입문서들은 더 쉽게 가르치는 것처럼 하면서 실은 훨씬 더 어렵게 가르치고 있습니다.
Pingback: 왜 변수가 나쁜가? | 서광열의 코딩 스쿨