다익스트라, 왜 goto에 시비(?)를 거는가?

일반입력 :2004/10/10 00:35

지디넷코리아

최근 프로그래밍을 배우기 시작한 사람들을 제외하고는 거의 대부분 goto에 대한 고민을 한 번쯤 해봤을 것이다. 우리가 goto에 대해 갖는 느낌은 꽤 막연하다. 주변에서 안 좋다, 안 좋다는 이야기를 하니 쓰면 안 될 것 같은데, 그러면서도 자신이 매일 접하는 코드에서 goto가 아예 안 보이는 것도 아니다. 그러면서 스스로는 초보 프로그래머들에게 이런 이야기를 한다.

goto 썼네? 이건 스파게티 코드야.

goto가 나쁘다고 생각하는 사람들에게 goto가 왜 나쁠까요?하고 묻는다면 프로그램을 읽을 때 진행 과정이 여기저기로 점프해서 따라 읽기 힘들어져., goto 나쁘다고 책에 많이 나와 있는 것 같은데 한 번 찾아봐. 이 정도의 단편적이거나 간접적인 정보에 근거한 대답이 대부분이었고, 명쾌한 답은 듣지 못했다.

물론, goto 사용을 추천하는 사람은 드물어서, 막연히 나쁘다는 대열에 끼여 goto를 쓰지 않는(혹은 최소한 공개적으로는 쓰지 않으려고 노력하는) 프로그래밍을 해왔다. 그리고 바쁜 일상에 쫓겨 이 대답에 대한 답을 찾기를 잊어버린 채 지금까지 지내왔다. 자기 명료화를 포기한 채.

goto 문은 해롭다?

1968년 CACM(Communications of ACM) 편집자에게 보내는 편지로 출판된 이 논문은 프로그래밍사에 있어 역사적인 의미를 갖는다. 이 논문으로 인해 구조적 프로그래밍에 대한 관심이 크게 일기 시작했고, 여러 언어의 형성에 큰 영향을 끼쳤다. 이 논문이 나오고, 이후 10년도 넘게, 아니 지금까지도 이 논문에 대한 논쟁은 그치지 않고 있다. 이 논문의 핵심은 다음 두 문장에 잘 드러나고 있다.

① 우리는(우리의 한계를 알고 있는 현명한 프로그래머처럼) 정적인 프로그램과 동적인 진행과정 간의 개념적인 간격을 최소화하기 위해 최선을 다해야 하고, 그렇게 해서 (텍스트 공간에 펼쳐진) 프로그램과 (시간상에 펼쳐진) 진행 과정 사이의 대응 관계가 가능한 한 명백하게 해야 한다.

② 어떠한 구문(clause)이든, 프로그래머 독립적 좌표계(programmer independent coordinate system)가 진행과정을 여전히 유익하고 편리하게 설명해 줄 수 있어야 한다는 요구 사항을 만족시켜야 한다.

먼저 ①번 문장을 설명해 보자. 다익스트라는 프로그래밍이 목표로 하는 것은 프로그램 자체가 아니고 그 프로그램이 생성할 동적인 진행과정이라는 점에 주목한다. 우리는 올바른 프로그램을 만드는 것보다 올바른 진행과정을 만들어 내는 데 포커스를 맞춰야 한다. 그런데 우리가 직접 만지는 것은 프로그램이고, 거기서 동적인 진행과정을 만들어내는 것은 컴퓨터다. 그러므로 프로그램과 과정 간에 상응성이 개재한다.

우리는 정적인 프로그램을 만지면서 실제로는 그 프로그램이 만들어낼 동적인 과정을 상정해야 한다. 문제는 여기에서 발생한다. 프로그램은 공간(텍스트 공간)에 존재하고 과정은 시간에 존재한다. 하지만 인간의 인지 구조는 정적인 관계를 이해하는 데 더 적합하게 되어 있다. 시간에 따라 변하는 동적인 과정을 시각화하는 것은 인간으로서는 매우 어려운 일이다. 이것이 프로그래밍이 어려운 이유다 ― 덕분에 버그가 생기기도 한다.

그러면 우리는 인간에게 적합한 정적인 공간과 실제 프로그램이 돌아갈 동적인 시간 사이의 대응성을 최대한 명백하게 할 방법을 찾아야 한다. 그러면 ②번 문장은 어떤 의미일까?

09:

10: int a = 1;

11: int b = 2;

@12: int c = 3;

13:

배정문으로만 이루어진 앞의 프로그램을 실행한 후 프로세스가 @ 부분에 도달한 시점을 상상해 보자. 이 코드에는 goto 문이 존재하지 않으므로 @은 전체 과정 중에서 현재 진행 시점을 표시하기에 충분하다. 우연치 않게 텍스트 공간의 표현이 시간상의 표현과 정확히 대응한다. 설명을 위해 다음 코드를 보자.

10: LABEL1:

@11: int a = 1;

12:

13: int b = 2;

14: goto LABEL1:

15:

다음과 같이 현재 진행 시점이 @ 부분에 도달한 상황을 가정하자. 이 코드에서 @의 위치에 도달했을 때 이 프로그램의 흐름이 13라인을 거쳤는지, 거치지 않았는지 이 표시(@)만으로는 불분명하다. 즉, 이 코드가 만들어내는 진행과정이 10, 11, 12, 13, 14, 10, 11, …인데, 여기에서 지금 진행이 첫 번째 11 위치에까지 와 있는 것인지, 아니면 두 번째 11 위치에 와 있는 것인지 이 정보만으로는 부족하다는 이야기다. 현재까지 몇 개의 라인을 처리했는지 하는 명령어 카운터(일종의 정규화된 시계) 정보가 필요하다. 하지만 이 방식에는 심각한 문제가 있는데 조금 후에 상술하겠다.

이와 같이 프로그램의 진행 과정을 기술(characterize)하기 위해 필요한 정보(예컨대 @ 표시 또는 명령어 카운터)를 프로그래머 독립적 좌표계라고 한다. 왜 프로그래머 독립적 좌표계가 중요할까? 왜 프로그램의 진행과정을 기술(특성화)하는 것이 중요할까?

변수에 담긴 값의 의미를 제대로 해석하는 것이 프로그램 이해나 수정 혹은 작성에 있어 가장 중요한 일 중 하나라는 점에 이견이 있는 사람은 없을 것이다. 그런데 모든 변수는 현재 진행과정의 어느 시점에 와있느냐에 따라 그 값의 해석이 결정된다.

다익스트라는 자신의 논문에서 ‘방안에 있는 사람 숫자’ 예를 든다. n이라는 변수가 현재 방안에 있는 사람의 숫자를 담고 있다고 하자. 지금 방안에 아무도 없다면 n은 0이 된다. 그런데, 만약 사람이 한 명 들어오면 현재 n의 숫자에 1을 더해 1이 될 것이다. 여기에서, 만약 사람은 들어왔고 내가 n을 증가시키기 직전의 시점을 상정해 보면 그 때 n은 ‘현재 방안의 사람의 숫자 - 1’이 된다. 변수의 해석이 달라지는 것이다.

변수에 담긴 값의 의미를 해석할 수 있으려면 전체 과정에서 지금 어느 시점에 도달해 있는지 알 수 있어야 한다. 따라서 프로그램의 진행과정을 기술할 수 있다는 것이 큰 의미를 갖는 것이며, 이런 특성화를 가능케 해주는 프로그래머 독립적 좌표계가 중요한 것이다.

이것은 다익스트라가 프로그램 증명(program correctness proof)을 올바른 프로그램을 만드는, 그리고 프로그램이 올바른지 증명하는 도구로 사용하기 때문에 각별한 의미가 있다. 다익스트라의 방법은 특정 실행 시점에 있어 어떤 조건(invariant)을 만족시켜야 한다는 식으로 프로그램을 올바르게 만들어 나간다.

예를 들어 ‘10번 라인과 11번 라인의 사이에서 s는 1부터 n까지의 합이어야 한다’는 조건(일종의 assert라고 할 수 있다)을 꼬리표 달듯이 달아놓을 수 있게 되는 것이며, 이게 되면 프로그래밍을 수학 증명 작성하듯이 엄격하게 작성할 수 있다. 예를 들어보자.

09: ...

10: if (x<0) a=x

else a=-x;

@11: ...

앞의 코드의 실행 과정이 @이라는 텍스트 인덱스(textual index) 지점(공간상이든 시간상이든)에 도달했을 때 변수 a의 값에 대해 무조건 a=

하지만 goto를 사용하는 경우를 생각해 보자. 이 경우 앞서 설명한 바와 같이 단일한 텍스트 인덱스(@)로는 부족하고 명령어 카운터가 있어야 한다. 그러나 이 방식은 변수 값의 의미를 해석하는 데 있어 전혀 유용하지 못하다. 명령어 카운터를 통해 지금까지 몇 개의 명령이 수행됐는지를 알아봤자, 그것을 근거로 특정 변수 값의 의미를 해석하거나 변수 값이 특정한 의미를 갖게 되는 지점의 좌표점들을 찾기란 극도로 어려운 일이기 때문이다(‘현재까지 n개의 명령을 실행했다’는 것에는 구체적인 의미가 결여되어 있기 때문에 추상적인 사용이 매우 어렵다).

프로그래머 독립적 좌표계가 변수 값의 의미를 해석하는 데 도움이 되기 때문에 유용하다고 한다면, 그러한 유용함을 제공하지 못하는 좌표계는 비록 그것이 진행 과정을 정확히 기술할 수 있다고 하더라도 무의미하다. 다익스트라는 goto의 도입이 유용한 프로그래머 독립적 좌표계를 사용할 수 없게 만든다는 점에서 해롭다고 말하고 있다.

이 논문의 핵심을 살펴보았는데, 독자들도 모두 동의하겠지만 이 논문은 단순히 ‘goto 문을 없애라’는 수준이 아니다. 다익스트라 스스로도 자신이 제안하는 구조적 프로그래밍이 단순히 goto 문 제거 정도로 도매급으로 팔리는 데 심한 불쾌감을 표현했다. 구조적 프로그래밍은 물론이고, 이 논문 역시 시간과 공간의 갭을 줄여 프로그래머가 실수를 덜하고 올바른 프로그램을 좀더 쉽게 만들게 도와주는 것이 중요하다고 역설한다.

여담이지만 이 논문의 제목 「Go to Statement Considered Harmful」에서 ‘×× Considered Harmful’이라는 유행어 패턴이 생겼다. 심지어는 “‘Goto Considered Harmful’ Considered Harmful” 혹은 거기에 한 단계 더 들어간 제목(Goto Considered Harmful Considered Harmful Considered Harmful)도 있다. 하지만 이 제목 자체는 다익스트라가 지은 것이 아니다. 파스칼을 만든 니클라우스 워쓰(Niklaus Wirth) 교수가 자의적으로 지은 것이다.

논쟁

이 논문 이후에 CACM 역사상 가장 활발한 논쟁이 오고 갔다. CACM뿐만 아니라 학계·업계 전반에 걸쳐 아주 뜨거운 토론 주제였다. 이 논문에 반대하는 사람들, 즉 goto 문을 사용하자는 사람들은 어떠한 주장을 했는지 살펴보자. 여기에서 주목해야 할 것은 이러한 주장을 하는 사람들도 goto 문을 쓰는 게 더 좋은 경우가 있다는 정도의 주장을 했을 뿐, goto 문을 많이 쓰는 게 좋다는 주장을 한 적은 없었다.

이러한 주장을 하는 사람들은 goto 문을 쓰는 게 더 좋은 경우에 대한 몇 가지 예를 보임으로써 그들의 주장에 대한 근거를 제시했다. 이를테면 루프에서 벗어나기(배열에서 특정 값을 찾아내는 등), 에러와 같은 예외 상황을 처리하는 코드(파일 처리 중 디스크 오류를 처리하는 등), 여러 개의 리소스를 할당하고 해제하기(메모리를 할당하고 에러가 나면 곧바로 해제하는 등), 중복 코드 제거/더 짧은 코드/가독성 향상, 성능 향상(goto를 이용하면 플래그 비교가 필요 없어지는 경우가 많다) 등의 예가 있다.

그리고 이런 반론들에 대해 역시 구조주의적 프로그래머 진영에서 반격이 있었다. 하지만 어느 쪽도 설득하거나 설득 당하지는 못했다. 논쟁을 직접 확인하려면 CACM Vol.30 No.3, 5, 6, 7, 8, 11, 12(이 중에는 다익스트라 자신의 답변도 포함되어 있다)를 꼭 보기 바란다. 많은 것을 얻을 수 있을 것이다.

여기서 우리는 도널드 크누스(Donald Knuth) 교수의 「Structured Programming with Goto」라는 논문에 주목하고자 한다. 이 논문의 결론을 요약하자면 다음과 같다.

goto를 써서 효율성도 높아지고 가독성도 높아지는 경우가 있긴 하다. 그렇지만 가급적이면 goto를 쓰는 것보다는 goto보다 더 추상적인 구문을 쓰는 게 좋다. 또는 프로그램을 만들거나 변형하는 프로그램을 이용하는 방법도 있다.

이 논문의 결론이 현재 어느 정도나 실현됐을까? 일단, 대부분의 언어에서는 goto를 흡수하여 상위 수준의 제어문인 break, continue, exception 등을 지원하게 됐다. 덕분에 우리는 자신도 모르게 안전한 goto를 쓰고 있는 셈이다. 하지만 여전히 goto를 쓰는 언어가 남아 있다. 아직 논쟁은 그치지 않았다. 영원히 끝나지 않을지도 모르겠다.

현재적 가치

하늘에 대해 말한 것이 인간의 일로 귀착되지 않는다면 하늘에 대해 잘 말한 것이 아니요, 옛날에 대해 말한 것이 현재를 통해 확인되지 않는다면 옛날에 대해 잘 말한 것이 아니다. ― 최한기, 『신기통』

이 논문의 현재적 가치에 대해 생각해보자(만약 이 논문이 지금 여기에서 아무런 의미가 없다면 볼 필요도 없지 않겠는가?) goto 남용이 문제가 될 수 있는 것과 비슷하게, 우리가 언어를 현명하지 못한 방법으로 사용하고 있는 예가 있는가? 시간과 공간의 인지적 갭이 존재하는, 프로그래머로 하여금 실수하기 쉽게 만드는 그런 예가 아직도 있을 것이다.

이를테면 거리가 멀거나 중첩된 if/else는 goto 문과 비슷한 문제를 갖는다. 왜냐하면 else 부분을 만났을 때 우리는 머리 속에서 if 문에 들어 있던 조건에 대한 부정을 취하는 연산을 수행해야 하고, 중첩된 if 문의 경우 외부의 if 문에 들어 있던 조건과 내부의 if 문에 들어 있는 조건에 대한 AND 연산을 수행해야 한다. 또 if 문과 else 문의 거리가 멀어지면 그 사이에 기억해야 할 것이 많아지게 된다. 결국 인간의 단기 기억 자체는 7+-2 이상을 한번에 담지 못한다는 한계를 넘게 되고, 여기에서 비용이 발생한다. 이런 경우 리펙토링에서 말하는 메쏘드 추출하기(extract method) 기법을 사용할 수 있다.

C++ 계열의 언어에서 지원하는 try/catch 구문 역시 남용될 경우 goto 문과 같은 문제를 갖게 된다. 실제로도 예외를 goto 문처럼 사용하는 경우가 많이 있다(이를테면 루프에서 벗어나기 위해 예외를 던지는 경우). 조슈아 블록(Joshua Bloch)은 『Effective Java』에서 예외를 오직 예외적인 상황에서만 사용해야 한다고 말하고 있다. 예외가 여러 개의 호출 단계를 거쳐 한 번에 흘러나오는 경우 우리는 예외의 출발이나 도착 지점을 찾기 위해 각각의 호출 단계를 순행하거나 역행하여 추적해야 한다. 우리의 머리 속에 담기에는 너무 많은 정보다.

메쏘드 길이가 긴 것도 비슷한 문제가 있다. 일반적으로 메쏘드가 길어지면 자연스럽게 지역 변수의 개수도 늘어나는데, 우리의 머리 속에 담고 있어야 할 정보량이 많아지는 셈이다. 이 경우도 메쏘드 추출하기 기법을 사용할 수 있다.

그러나 메쏘드 길이가 짧다고 해서 항상 문제가 없다는 식으로 생각할 수는 없다. 메쏘드 추출하기 기법 등으로 긴 메쏘드를 짧게 줄였다 하더라도 해당 메쏘드 내의 추상화 수준(abstraction level)이 일치하지 않는다면 결국 우리는 메쏘드가 하는 일을 이해하기 위해 그 메쏘드가 호출하는 하위 메쏘드의 코드를 찾아 읽어야 한다. 이것은 오히려 하나의 긴 메쏘드일 때보다 더 좋지 않은 상황일 수 있다.

병렬성(parallelism)은 어떨까? 시간상의 흐름을 텍스트 공간에 나타내는 것은 어렵다. 더군다나 동시에 여러 개의 흐름이 있고, 이들이 서로 엉켜 있다면 이것을 이해하거나 올바르게 만들어내는 것은 더욱 어려워진다. 이 말은 우리의 인지적 구조와 병렬성 사이에는 큰 간극이 있다는 것을 의미한다.

그래서 멀티 쓰레드 프로그램에서는 버그가 발생하기가 쉽다. 어떻게 해결해야 할까? 존 아우스터하우트(John Ousterhout)는 Thread considered harmful이라는 말을 할 정도로 쓰레드가 남용되고 있다고 말하며, 대부분의 경우 이벤트 주도적 아키텍처(Event Driven Architecture)로 충분하다고 말한다. 호아(C.A.R. Hoare)의 CSP(Communicating Sequential Processes)는 조금 더 추상적인 단위를 쓸 수 있도록 해주며 우리의 인지적 구조와의 간극이 적다.

이런 이야기에 대해 goto를 제거하면 성능이 떨어진다는 반박을 많이 한다. 예컨대 메쏘드 호출이 하나 늘어나거나 예외를 발생시켜야 하는 것 자체가 비용이 든다는 것이다. 하지만 대부분의 경우는 이런 정도는 소탐대실 차원인 경우가 많다. 만약 이런 정도의 마이크로한 튜닝이 필요한 상황이라면 크누스가 말하는 프로그램 변형(transformation)을 사용해 아름다움을 포기하지 않으면서 효율까지도 잡을 수 있다.

고전 읽기의 중요성 확인하길

논문을 직접 읽어보면 보통 사람들이 저자의 아이디어에 대해 이해하는 바와 실제는 큰 차이가 있음을 알게 된다. 본의가 왜곡되어 전달되는 것도 있고, 저자의 정말 중요한 이야기는 빠지고 찌꺼기만 남아서 전달되는 것도 많다. 원류에서 멀어지면 멀어질수록 물은 흐려지게 마련이다. 저자의 아이디어는 훨씬 더 스케일이 크고 파워풀했다. 다익스트라는 어떻게 하지 말라는 얘기 뿐 아니라 대신 어떻게 하라는 긍정적인 이야기도 했다. 그러나 사람들은 더 쉽고 명확해 보이는 어떻게 하지 말라 부분만 기억했다.

우리는 이 논문을 함께 읽고 토론하면서 고전 읽기의 중요성을 다시금 확인할 수 있었다. 우리가 원전을 읽어서 얻으려는 것은 어떤 결과로서의 답이라기보다는 그 사유 과정이 아닐까. 다익스트라가 문제를 논리적으로 분석하고 한 단계 높은 레벨로 일반화해 유용한 결론을 이끌어내는 과정, 이 과정 자체도 우리에게 많은 교훈을 준다. 이 덕분에 goto 논문이 오늘날까지도 유효한 것이 아닐까.

이것으로 「고전을 찾아서」 첫 회를 마치고자 한다. 비록 이 글의 저자가 세 명인 것으로 되어 있긴 하지만, 사실 르네상스 클럽 리더십 트레이닝에 참석해 이 논문을 같이 읽고 토론한 다른 모든 사람이 공동 저자인 셈이다. @