← 개념 목록

구조적 프로그래밍

개념

goto문을 배제하고 순차, 선택, 반복이라는 세 가지 제어 구조만으로 프로그램의 흐름을 구성하는 프로그래밍 패러다임

개요

구조적 프로그래밍은 프로그램의 제어 흐름을 순차, 선택, 반복이라는 세 가지 기본 구조만으로 구성하도록 제한하는 프로그래밍 패러다임입니다. 소스 코드를 블록 단위로 조직하여 프로그램이 위에서 아래로 읽히도록 만드는 것을 지향하며, 임의의 지점으로 실행 흐름을 옮기는 goto문의 사용을 배제하는 것을 핵심 목표로 삼았습니다.

이 패러다임은 1960년대 후반부터 1970년대 초반 사이 컴퓨터 과학의 중심 화두였습니다. 에츠허르 데이크스트라가 1968년 발표한 서한 “goto문은 해롭다고 간주된다”와 코라도 뵘, 주세페 야코피니가 1966년 발표한 정리가 이론적, 실천적 토대를 제공했습니다. 이후 구조적 프로그래밍의 원칙은 소프트웨어 개발의 상식으로 흡수되어, 오늘날에는 별도로 논의되는 주제가 아니라 대부분의 절차적 언어가 기본으로 제공하는 문법 그 자체가 되었습니다.

구조적 프로그래밍은 종종 모듈러 프로그래밍과 혼동되지만 엄밀히는 서로 다른 개념입니다. 넓은 의미에서 구조적이라는 표현은 모듈화되고 효율적이며 이해와 수정이 쉽다는 뉘앙스를 담고 있지만, 좁은 의미의 구조적 프로그래밍은 제어 흐름을 세 가지 구조로 한정한다는 훨씬 구체적인 규율을 가리킵니다.

역사적 배경

소프트웨어 위기와 1968년 NATO 소프트웨어 공학 회의

1960년대 들어 컴퓨터 하드웨어의 성능은 빠르게 향상되었지만, 소프트웨어 개발 방법론은 그 속도를 따라가지 못했습니다. 대규모 프로젝트가 일정과 예산을 초과하면서도 요구 사항을 충족하지 못하는 사례가 반복되었고, 이러한 상황을 가리켜 소프트웨어 위기라는 표현이 사용되었습니다. 1968년 서독 가르미슈에서 열린 NATO 소프트웨어 공학 회의는 이 문제를 공식적으로 논의한 최초의 국제적 자리였으며, 여기서 소프트웨어 공학이라는 용어 자체가 처음 제안되었습니다. 이 회의에는 데이크스트라, 호어, 앨런 펄리스, 피터 나우어, 니클라우스 비르트 등 이후 구조적 프로그래밍 운동을 이끈 인물들이 다수 참석했습니다.

회의 참석자들은 각기 다른 조직에서 유사한 문제를 겪고 있다는 사실을 확인했고, 이는 프로그램 설계와 검증에 관한 보다 엄밀한 방법론이 필요하다는 공감대로 이어졌습니다. 데이크스트라를 비롯한 일부 참석자는 이후 프로그램의 정당성 증명, 구조적 프로그래밍, 동시성 프로그래밍, 이를 뒷받침하는 프로그래밍 언어 설계에 집중하는 방향으로 나아갔습니다.

뵘과 야코피니의 1966년 정리

1964년 코라도 뵘은 순차와 반복만으로 구성된 튜링 완전 언어 P′′을 정의했습니다. 1966년 뵘은 주세페 야코피니와 함께 이 결과를 확장한 논문 “Flow Diagrams, Turing Machines and Languages with Only Two Formation Rules”를 커뮤니케이션스 오브 더 에이씨엠에 발표했습니다. 이 논문은 이후 구조적 프로그램 정리 또는 뵘-야코피니 정리라는 이름으로 알려지게 되었습니다.

논문은 두 부분으로 구성됩니다. 야코피니가 담당한 첫 부분에서는 임의의 제어 흐름 그래프가 순차, 선택, 반복만을 사용하는 구조화된 그래프로 재작성될 수 있음을 증명했습니다. 뵘이 담당한 두 번째 부분에서는 P′′과 같이 순차와 반복만을 형성 규칙으로 갖는 언어도 튜링 기계와 동등한 계산 능력을 가진다는 점을 보였습니다. 데이비드 하렐은 1980년 발표한 논문 “On Folk Theorems”에서 이 논문이 대단히 자주 인용되지만 정작 세밀하게 읽히는 경우는 드물다고 지적하며, 실제로 널리 퍼진 형태의 정리는 폰 노이만과 클레이니의 초기 계산 이론까지 거슬러 올라가는 더 단순한 결과가 민간 전승처럼 재구성된 것이라고 주장했습니다.

데이크스트라의 “goto문은 해롭다고 간주된다” 서한

1968년 데이크스트라는 커뮤니케이션스 오브 더 에이씨엠에 짧은 독자 투고를 제출했습니다. 편집자는 이 투고에 제목이 없다는 점을 고려해 “Go To Statement Considered Harmful”이라는 표제를 붙여 게재했습니다. 이 서한에서 데이크스트라는 goto문을 제한 없이 사용하면 프로그램의 진행 상태를 설명할 수 있는 의미 있는 좌표계를 찾기가 극도로 어려워진다고 주장했습니다. 그는 프로그래머의 역량이 자신이 작성한 코드에 포함된 goto문의 밀도에 반비례한다는 도발적인 표현으로 논쟁에 불을 붙였습니다.

이 서한은 프로그래밍 커뮤니티를 두 진영으로 나누었습니다. 한쪽은 goto가 충분히 유용한 도구이며 이를 금지하자는 주장이 지나치다고 보았고, 다른 한쪽은 가독성 있고 논리적이며 구조화된 코드가 필요하다고 믿었습니다. 이후 도널드 크누스와 로버트 플로이드, 윌리엄 울프 등이 참여한 후속 논쟁이 여러 해에 걸쳐 이어졌습니다. 흥미롭게도 데이크스트라 자신도 이 서한을 발표하기 불과 3년 전인 1965년 논문에서는 goto문을 사용한 적이 있었습니다.

달, 데이크스트라, 호어의 Structured Programming (1972)

구조적 프로그래밍이라는 용어를 실제로 만들고 이를 체계화한 것은 데이크스트라가 1969년 완성한 논문 “Notes on Structured Programming”이었습니다. 이 논문은 정식 출간 전부터 학계에 회람되며 이미 상당한 영향력을 얻었고, 1972년 올레-요한 달, 데이크스트라, C.A.R. 호어가 공동 저술한 단행본 Structured Programming의 첫 번째이자 가장 중요한 장으로 정식 수록되었습니다. 이 책의 두 번째 장은 호어가 자료 구조 설계에 유사한 원리를 적용하는 방법을 다루었고, 세 번째 장에서 달이 서술한 접근 방식은 이후 객체 지향 프로그래밍의 초기 형태로 재조명되기도 했습니다.

데이크스트라는 2001년 작성한 회고 EWD1308에서 IBM이 구조적 프로그래밍이라는 용어의 인기를 이용해 이를 사실상 가로챘으며, 할런 밀스의 손을 거치며 원래의 개념이 단순히 goto문을 없애는 수준으로 축소되었다고 불만을 토로했습니다. 이는 구조적 프로그래밍의 본래 의도가 단순한 문법적 금지 조항이 아니라 프로그램의 정당성을 증명 가능한 형태로 구성하는 방법론이었음을 보여주는 대목입니다.

1958-1960ALGOL 58와 ALGOL60 등장, 블록 구조도입1964코라도 뵘, P′′ 언어로순차와 반복만으로튜링 완전성 증명1966뵘-야코피니 정리발표1968데이크스트라,goto문 서한 발표와NATO 소프트웨어공학 회의1969할런 밀스,뉴욕타임스프로젝트에 구조적방법론 적용1970니클라우스 비르트,Pascal 언어 발표1971비르트, 단계적정제에 관한 논문발표1972달, 데이크스트라,호어의 StructuredProgramming 출간1974크누스, goto문에관한 절충적 논문발표1976매케이브, 순환복잡도 논문 발표1987프랭크 루빈의공개서한과데이크스트라의 반박구조적 프로그래밍 연표

이론적 기초: 구조적 프로그램 정리

정리의 두 가지 형태

구조적 프로그램 정리는 관점에 따라 두 가지 형태로 서술됩니다. 첫 번째는 언어 생성 관점으로, 모든 부분 재귀 함수는 순차와 반복만을 사용하는 프로그램으로 계산될 수 있다는 것입니다. 이 형태에서는 선택 구조조차 필수가 아니며, 조건에 따른 분기는 반복문을 통해 인코딩될 수 있습니다. 두 번째는 프로그램 변환 관점으로, 임의의 흐름도 즉 제어 흐름 그래프는 순차, 선택, 반복만을 사용하는 구조화된 프로그램으로 변환될 수 있다는 것입니다. 이 형태는 프로그램 설계보다는 컴파일러 최적화나 레거시 코드의 구조화 같은 프로그램 변환 작업과 더 밀접한 관련이 있습니다.

정리가 성립한다는 사실 자체는 구조적 프로그래밍 운동에서 처음 발견된 것이 아닙니다. 순차, 선택, 반복이라는 세 구조는 중앙 처리 장치의 명령어 실행 주기나 튜링 기계의 동작을 기술하기에도 충분한 것으로, 어떤 의미에서는 프로세서가 메모리에서 읽어 들이는 명령이 구조적으로 작성되지 않았더라도 결과적으로는 언제나 구조화된 프로그램을 실행하고 있는 셈입니다. 다만 이 결과가 널리 알려지고 프로그래밍 실무에 적용된 계기는 뵘과 야코피니의 1966년 논문이었기 때문에, 정리의 공적은 관례적으로 이들에게 돌아갑니다.

증명의 개요와 보조 변수 문제

뵘-야코피니 정리의 직접적인 증명 과정을 실제 프로그램에 적용하면 원래 없었던 지역 변수가 추가로 필요해지고, 경우에 따라 코드가 중복되는 문제가 발생할 수 있습니다. 이러한 코드 중복 문제는 이른바 하프 루프 문제라는 이름으로도 불립니다. 뵘과 야코피니는 이러한 보조 변수의 필요성이 일반적으로 불가피하다고 추측했으며, 이 추측은 이후 애쉬크로프트와 마나가 1972년, 코사라주가 1973년 각각 독립적으로 증명하며 확인되었습니다.

파스칼 언어는 이러한 문제로부터 자유롭지 않은 대표적인 사례로 언급됩니다. 에릭 로버츠가 인용한 1980년의 실증 연구에 따르면, 파스칼이 제공하는 제어 구조만을 사용해 배열에서 특정 원소를 찾는 것과 같은 비교적 간단한 문제조차 학생 프로그래머의 상당수가 올바르게 풀어내지 못했습니다.

정리의 한계에 대한 후속 연구

코사라주는 다중 레벨 탈출을 허용하는 반복문이 보조 변수를 도입하지 않고도 모든 결정적 흐름도를 표현할 수 있는 조금 더 강력한 구조라는 점을 보이고, 탈출 가능한 레벨의 수에 따른 엄격한 계층 구조가 존재함을 증명했습니다. 즉 레벨 m까지 탈출을 허용하는 반복 프로그램으로는 표현할 수 있지만 레벨 m-1까지만 허용하는 프로그램으로는 표현할 수 없는 프로그램이 항상 존재한다는 것입니다.

한편 덱스터 코젠과 웨이-룽 더스틴 쩡은 2008년 발표한 논문에서, 일차 술어가 아닌 순수한 명제 논리 수준에서 정리를 재구성하면 뵘-야코피니 정리가 성립하지 않는다는 사실을 보였습니다. 이들은 어떤 while 프로그램으로도 표현할 수 없는 세 개의 상태를 가진 결정적 명제 흐름도가 존재함을 예시로 제시했습니다. 이러한 후속 연구들은 정리가 상당히 기술적인 문맥에서 성립하는 결과이며, 통속적으로 알려진 것보다 훨씬 정교한 제약 위에서 성립한다는 점을 잘 보여줍니다.

세 가지 기본 제어 구조

구조적 프로그램 정리에 따라 하나의 프로그램은 순차, 선택, 반복이라는 세 가지 제어 구조의 조합으로 구성됩니다. 이 절에서는 각 구조의 정의와 함께, 언어 특정 없이 개념을 설명하기 위한 흐름도를 제시합니다. 코드 예시는 별도의 언어가 지정되지 않은 경우의 관례에 따라 TypeScript로 작성했습니다.

순차

순차는 문장이 작성된 순서대로 위에서 아래로 실행되는 구조입니다. 대부분의 언어는 여러 문장을 하나의 단위처럼 다루기 위한 블록 개념을 제공하며, 블록 안에 별도의 흐름 제어가 없다면 문장들은 그대로 순서대로 실행됩니다.

시작

문장 1

문장 2

문장 3

종료

function calculateTotal(price: number, quantity: number, taxRate: number): number {
  const subtotal = price * quantity;
  const tax = subtotal * taxRate;
  const total = subtotal + tax;
  return total;
}

선택

선택은 프로그램의 상태에 따라 서로 다른 블록 가운데 하나가 실행되는 구조입니다. if, then, else와 같은 키워드로 표현되는 경우가 많으며, 조건에 따른 각 경로는 정확히 하나의 진입점과 하나의 종료점을 가져야 한다는 것이 원래의 규율입니다.

거짓

시작

조건

문장 A 실행

문장 B 실행

종료

function classify(score: number): string {
  if (score >= 90) {
    return "A";
  } else if (score >= 80) {
    return "B";
  } else {
    return "C 이하";
  }
}

반복

반복은 프로그램이 특정 상태에 도달할 때까지 하나의 블록을 되풀이해서 실행하는 구조입니다. while, repeat, for, do-until과 같은 키워드로 표현됩니다. 원론적으로 반복 구조는 정확히 하나의 진입점과 하나의 종료점을 갖도록 설계되지만, 대부분의 현대 언어는 break문과 같은 조기 종료를 허용합니다.

거짓

시작

조건 확인

반복 본문 실행

종료

function sumUntilLimit(values: number[], limit: number): number {
  let sum = 0;
  for (const value of values) {
    if (sum + value > limit) {
      break;
    }
    sum += value;
  }
  return sum;
}

블록의 개념

뵘-야코피니 정리 자체에는 블록이라는 개념이 명시적으로 포함되어 있지 않지만, 실제 프로그래밍 언어는 거의 예외 없이 여러 문장을 하나의 단위로 묶는 블록 문법을 제공합니다. C 계열 언어와 커리 브레이스 계열 언어는 중괄호로, PL/I와 파스칼은 BEGIN과 END로, 파이썬은 들여쓰기로 블록을 표시합니다. ALGOL 68처럼 각 구조마다 서로 다른 여닫는 키워드 쌍을 사용하는 언어도 있습니다.

핵심 설계 원칙

단일 진입-단일 종료 원칙

구조적 프로그래밍의 초기 규율 가운데 하나는 각 블록이 정확히 하나의 진입점과 하나의 종료점만을 가져야 한다는 단일 진입-단일 종료 원칙입니다. 이 원칙은 함수를 재귀적으로 더 작은 단위로 분해해 각 단위의 정당성을 개별적으로 증명할 수 있게 한다는 점에서 데이크스트라가 중요하게 여겼던 요소였습니다. 다만 대부분의 현대 언어는 함수 중간에서 값을 반환하는 return문이나 반복문 중간에서 빠져나오는 break문처럼, 엄밀히는 이 원칙에 어긋나는 조기 종료 기능을 제공합니다.

하향식 설계와 단계적 정제

구조적 프로그래밍은 알고리즘을 개발하고 서술하는 방법론이기도 합니다. 이 방법론은 문제 전체를 하향식으로 정식화한 뒤, 이를 더 작은 블록이나 모듈로 나누고, 각 블록을 더 이상 세분화할 필요가 없을 때까지 단계적으로 정제해 나가는 절차로 구성됩니다. 니클라우스 비르트는 1971년 발표한 논문 “Program Development by Stepwise Refinement”에서 이 절차를 여덟 여왕 문제라는 하나의 예제를 통해 상세히 보여주었습니다. 이 논문은 프로그램 설계를 위한 하향식 방법을 최초로 공식화한 문헌으로 평가받으며, 프레드 브룩스가 저서 맨먼스미신에서 논의하기도 했습니다.

비르트의 방법은 겉보기에는 단순합니다. 프로그램이 해야 할 일을 상위 수준에서 서술한 뒤, 각 단계마다 하나의 설계 결정을 내리면서 이를 점차 실행 가능한 코드로 구체화해 나가는 것입니다. 그는 논문의 결론에서, 짧은 프로그램 하나를 개발하는 과정에 대한 상세한 설명조차 상당히 긴 이야기가 된다는 사실이 신중한 프로그래밍은 결코 사소한 주제가 아님을 보여준다고 정리했습니다.

모듈화와의 관계

구조적 프로그래밍은 종종 모듈러 프로그래밍과 함께 언급되지만, 두 개념은 서로 구별됩니다. 모듈러 프로그래밍은 프로그램을 독립적으로 컴파일되거나 재사용 가능한 단위로 나누는 것에 초점을 맞추는 반면, 구조적 프로그래밍은 각 단위 내부의 제어 흐름을 어떻게 구성할 것인가에 관한 규율입니다. 즉 하나의 모듈 내부가 여전히 정돈되지 않은 goto문으로 가득할 수도 있고, 반대로 구조적으로 잘 작성된 코드라도 모듈 경계가 엉성하게 설계될 수도 있습니다. 실무에서는 두 원칙이 함께 적용되는 경우가 많기 때문에 종종 혼동되지만, 개념적으로는 독립적인 축입니다.

goto문 논쟁

데이크스트라의 입장

데이크스트라는 goto문이 그 자체로 지나치게 원시적인 도구여서 프로그램을 어지럽히도록 부추긴다고 보았습니다. 그의 핵심 논지는 goto문의 무분별한 사용이 프로그램의 진행 상태를 추적할 수 있는 명확한 좌표계를 무너뜨린다는 데 있었습니다. 반면 if-then-else나 do-while과 같은 구조에 대응하는 일부 goto 사용은 문제가 없었는데, 이러한 구조를 사용하는 모듈은 재귀적으로 더 작은 단위로 나누어 각각의 정당성을 증명할 수 있었기 때문입니다. 결국 증명 가능한 모듈을 만드는 데 필요한 최소한의 제어 구조 집합이 바로 순차, 선택, 반복이었고, 이것이 구조적 프로그래밍의 출발점이 되었습니다.

크누스의 절충안

도널드 크누스는 프로그램이 정당성 증명을 염두에 두고 작성되어야 한다는 원칙에는 동의했지만, goto문을 완전히 폐지하는 데에는 동의하지 않았습니다. 그는 2018년까지도 자신의 프로그램에서 goto문을 계속 사용해 왔다고 밝힌 바 있습니다. 1974년 발표한 논문 “Structured Programming with go to Statements”에서 크누스는 직접적인 점프가 정당성을 훼손하지 않으면서도 더 명확하고 효율적인 코드를 만드는 사례를 여럿 제시했습니다. 그는 완전한 폐지 대신, 프로그램의 흐름도를 그렸을 때 모든 전방 분기가 왼쪽에, 모든 후방 분기가 오른쪽에 위치하고 분기선끼리 서로 교차하지 않도록 그릴 수 있어야 한다는 느슨한 구조적 제약을 제안했습니다. 컴파일러와 그래프 이론에 밝은 일부 연구자들은 이와 유사한 맥락에서 환원 가능한 흐름 그래프만을 허용하자는 주장을 펼치기도 했습니다.

프랭크 루빈의 1987년 공개서한

구조적 프로그래밍을 둘러싼 논쟁은 원칙이 널리 받아들여진 뒤에도 완전히 가라앉지 않았습니다. 1987년 프랭크 루빈은 컴퓨터 과학 학술지에 “‘GOTO Considered Harmful’ Considered Harmful”이라는 제목의 공개서한을 게재했습니다. 이 서한은 여러 반론을 불러일으켰고, 데이크스트라 역시 루빈뿐 아니라 그에게 응답하며 goto문 사용에 일부 양보하는 태도를 보인 다른 필자들까지 함께 신랄하게 비판하는 답신을 보냈습니다. 20세기가 저물 무렵에는 거의 모든 컴퓨터 과학자가 구조적 프로그래밍의 개념을 배우고 적용하는 것이 유용하다는 데 동의하게 되었으며, 원래 이러한 구조를 갖추지 못했던 FORTRAN, COBOL, BASIC과 같은 고급 언어들도 이후 구조적 제어문을 갖추게 되었습니다.

주요 인물

에츠허르 데이크스트라

에츠허르 데이크스트라는 네덜란드의 컴퓨터 과학자로, 프로그램을 모듈 단위로 구성하는 구조적 프로그래밍 패러다임을 개발한 인물로 알려져 있습니다. 그는 1959년 두 노드 사이의 최단 경로를 구하는 알고리즘을 고안한 것으로도 유명하며, 1960년에는 얍 존네펠트와 함께 ALGOL 60을 위한 최초의 컴파일러를 개발했습니다. 데이크스트라는 프로그래밍을 하나의 수학적 규율로 세우고자 했으며, 이러한 관점은 구조적 프로그래밍뿐 아니라 프로그램 정당성 증명이라는 그의 평생에 걸친 연구 주제 전반을 관통합니다.

코라도 뵘과 주세페 야코피니

코라도 뵘과 주세페 야코피니는 이탈리아의 컴퓨터 과학자로, 1966년 커뮤니케이션스 오브 더 에이씨엠에 발표한 논문을 통해 구조적 프로그램 정리의 원형을 제시했습니다. 뵘은 수학 박사 학위를 받은 뒤 1950년대부터 컴파일러 설계에 관한 초기 연구에 참여했으며, 야코피니와는 이탈리아 피사를 비롯한 여러 대학에서 함께 연구했습니다. 두 사람의 동기는 당시 흐름도를 기반으로 한 프로그래밍 언어들이 지나치게 복잡한 정의와 제약 없는 제어 흐름 때문에 뒤엉킨 구조를 갖고 있다는 문제의식에서 출발했습니다.

니클라우스 비르트

니클라우스 비르트는 스위스의 컴퓨터 과학자로, 1970년 블록 구조 프로그래밍을 가르치기 위한 목적으로 설계된 언어 파스칼을 발표했습니다. 1971년 논문 “Program Development by Stepwise Refinement”는 프로그래밍 교육에 관한 고전으로 평가받으며, 프로그램 설계에 하향식 방법을 최초로 공식화한 문헌으로 여겨집니다. 비르트는 이후에도 모듈라-2, 오베론 등 여러 언어를 설계하며 명시적 선언과 명확한 반복 및 조건문, 잘 정의된 프로시저를 일관되게 강조했습니다.

C.A.R. 호어와 올레-요한 달

토니 호어는 데이크스트라, 달과 함께 Structured Programming을 공동 저술하며, 자료 구조의 설계에도 구조적 프로그래밍과 유사한 원리를 적용할 수 있음을 보였습니다. 올레-요한 달은 이 책의 세 번째 장에서 데이터와 절차를 하나로 묶어 다루는 접근 방식을 서술했는데, 이는 이후 객체 지향 프로그래밍의 초기 형태로 재조명되는 내용이었습니다. 달은 크리스텐 니가르드와 함께 최초의 객체 지향 언어로 꼽히는 시뮬라를 설계한 인물이기도 합니다.

할런 밀스

할런 밀스는 IBM의 연구원으로, 구조적 프로그래밍 이론을 실제 대규모 프로젝트에 적용해 산업계의 신뢰를 얻는 데 크게 기여했습니다. 그는 최고 프로그래머 팀이라는 개발 조직 모델을 정립했으며, 이후 클린룸 소프트웨어 공학이라는 방법론의 창시자로도 알려져 있습니다. 밀스는 데이크스트라와 로버트 플로이드의 이론적 성과를 자동자 이론, 마르코프 연쇄 기반 소프트웨어 테스트와 결합해 실무에 적용 가능한 형태로 다듬었습니다.

언어와 실제 적용

초기 채택 언어: ALGOL과 Pascal

구조적 프로그래밍의 흐름은 1950년대 말 ALGOL 58과 ALGOL 60의 등장과 함께 시작되었습니다. 특히 ALGOL 60은 블록 구조에 대한 지원을 포함하고 있어 이후 구조적 프로그래밍 언어 설계의 원형이 되었습니다. 니클라우스 비르트가 1970년 설계한 파스칼은 goto문을 의도적으로 배제하지는 않았지만 블록 구조화된 프로그래밍을 가르치기 위한 목적으로 설계되었으며, 이후 구조적 프로그래밍을 대표하는 교육용 언어로 널리 사용되었습니다. 이 밖에도 PL/I, 에이다, RPL 등이 구조적 프로그래밍을 위해 초기에 사용된 언어로 꼽힙니다.

이후 언어들의 계승과 C의 절충

이론적으로는 어떤 언어로도 구조적 프로그래밍을 실천할 수 있지만, 그 이후 등장한 대부분의 절차적 언어는 구조적 프로그래밍을 장려하는 기능을 갖추었고 때로는 goto문처럼 구조를 해칠 수 있는 기능을 의도적으로 배제했습니다. C 언어는 goto문을 계속 포함하되 그 대상을 지역 블록이나 이를 둘러싼 바깥 블록으로 제한함으로써 절충적인 태도를 취한 초기의 대표적 사례입니다.

goto의 자리를 구조적 제어문이 대체하면서도, 대부분의 언어는 정리 자체와는 엄밀히 부합하지 않는 기능들을 함께 제공합니다. 함수 중간에서 값을 돌려주는 return문이 대표적이며, 이는 함수라는 블록이 단일 종료점을 가져야 한다는 원칙과 어긋납니다. 반복문 중간에서 빠져나오는 break문 역시 마찬가지이며, 일부 언어는 가장 안쪽 반복문뿐 아니라 지정한 바깥쪽 반복문까지 한 번에 빠져나오는 레이블 붙은 break를 제공하기도 합니다. 이러한 다중 종료 기능이 필요한 이유는 대체로 두 가지로 요약됩니다. 함수가 더 이상 할 일이 없어졌거나, 계속 진행할 수 없는 예외적인 상황을 만난 경우입니다.

IBM과 뉴욕타임스 프로젝트: 최고 프로그래머 팀

1970년대 들어 구조적 프로그래밍 이론은 IBM 연구원 할런 밀스가 뉴욕타임스의 색인 시스템 개발에 자신의 해석을 적용하면서 산업계에서 중요한 지지자를 얻었습니다. 전통적인 방식으로는 약 30인년이 소요될 것으로 추정되었던 이 프로젝트를, 밀스는 구조적 프로그래밍 기법과 소수의 지원 인력으로 구성된 최고 프로그래머 팀 모델을 활용해 훨씬 짧은 기간에 완료했습니다. 이 프로젝트의 성공은 다른 기업의 관리자들이 구조적 프로그래밍 도입을 정당화하는 근거로 널리 인용되었습니다. 다만 데이크스트라는 밀스의 해석이 원래 발표된 이론과 여러 지점에서 다르다는 점을 비판하기도 했습니다.

관련 개념과의 비교

모듈러 프로그래밍과의 차이

앞서 설명한 대로 구조적 프로그래밍과 모듈러 프로그래밍은 서로 다른 축의 개념입니다. 아래 표는 두 개념이 강조하는 지점의 차이를 정리한 것입니다.

구분 구조적 프로그래밍 모듈러 프로그래밍
초점 하나의 단위 내부의 제어 흐름 프로그램을 독립적인 단위로 분할
핵심 규율 goto 배제, 순차·선택·반복만 사용 컴파일 단위 분리, 인터페이스와 구현의 분리
대표 문법 요소 if, while, for, block 모듈, 패키지, 네임스페이스, 컴파일 단위
관계 하나의 모듈 안에서 지켜야 할 규율 모듈 사이의 관계에 관한 규율

절차적 프로그래밍

절차적 프로그래밍은 구조적 프로그래밍에서 파생된 프로그래밍 모델로, 절차 또는 함수라는 이름으로 불리는 호출 가능한 단위를 중심으로 프로그램을 구성합니다. 각 절차는 일련의 계산 단계로 이루어지며, 실행 중 어느 지점에서도 다른 절차나 자기 자신에 의해 호출될 수 있습니다. 구조적 프로그래밍이 제어 흐름의 형태에 대한 규율이라면, 절차적 프로그래밍은 그 흐름을 재사용 가능한 절차 단위로 조직하는 방식에 가깝습니다. 실무에서는 두 개념이 사실상 겹쳐서 사용되는 경우가 많아, 파스칼이나 C처럼 절차 호출과 구조적 제어문을 함께 지원하는 언어를 흔히 절차적 언어라고 부릅니다.

객체 지향 프로그래밍과의 관계

객체 지향 프로그래밍은 데이터를 속성으로, 동작을 메서드로 갖는 객체를 중심으로 프로그램을 구성하는 모델로, 흔히 구조적 프로그래밍의 뒤를 잇는 패러다임으로 여겨집니다. 절차적 프로그래밍이 문제를 해결하는 전체 처리 과정 자체에 초점을 맞춘다면, 객체 지향 프로그래밍은 문제를 서로 협력하는 자율적인 개체들의 집합으로 나누는 데 초점을 맞춘다는 차이가 있습니다. 흥미롭게도 앞서 언급한 대로 Structured Programming의 세 번째 장에서 달이 제시한 접근 방식은 훗날 객체 지향적 사고방식의 초기 형태로 재평가되었으며, 이는 데이크스트라 본인이 훗날 강하게 비판했던 객체 지향 패러다임이 역설적으로 그가 참여한 저작에서 비롯된 흐름과 연결되어 있음을 보여주는 사례로 종종 언급됩니다. 다만 객체 지향 프로그래밍이 지배적인 언어에서도 각 메서드의 내부는 여전히 순차, 선택, 반복이라는 구조적 프로그래밍의 규율을 따르는 것이 일반적입니다.

구분 절차적 프로그래밍 객체 지향 프로그래밍
중심 단위 절차, 함수 객체, 클래스
데이터 처리 데이터와 이를 다루는 절차가 분리됨 데이터와 메서드가 하나의 객체로 캡슐화됨
대표 언어 파스칼, C, 포트란 자바, C#, C++
구조적 프로그래밍과의 관계 구조적 제어 흐름을 절차 단위로 조직 각 메서드 내부에서 구조적 제어 흐름을 그대로 사용

실무상의 세부 쟁점

조기 종료 문제

단일 진입-단일 종료 원칙을 문자 그대로 지키는 것은 실무에서 여러 부작용을 낳을 수 있습니다. 만약 자원을 정리하는 코드가 함수의 마지막 부분에만 있다면, 조기 반환이 일어나는 모든 경로마다 동일한 정리 코드를 반복해서 넣어야 하고, 이는 개발자가 실수로 반환 지점을 빠뜨리는 취약한 구조를 만들기 쉽습니다. 반환문 자체가 없는 파스칼, 리스프, OCaml과 같은 언어는 이러한 문제로부터 비교적 자유롭습니다.

반면 마틴 파울러와 켄트 벡을 비롯한 저자들은 리팩터링을 다루는 저서에서, 단일 종료점을 고수하는 것이 항상 유용한 규칙은 아니라고 주장했습니다. 이들은 명료함이 핵심 원칙이며, 하나의 종료점을 사용할 때 코드가 더 명료하다면 그렇게 하고 그렇지 않다면 그렇게 하지 않아도 된다고 정리했습니다. 이들은 중첩된 조건문으로만 이루어진 함수를 가드절을 사용한 일련의 조건부 반환 또는 예외 발생 문장과, 뒤이어 나오는 하나의 보호되지 않은 공통 처리 블록으로 변환하는 구체적인 절차를 제시하기도 했습니다. 허브 서터와 안드레이 알렉산드레스쿠 역시 2004년 저서에서 단일 종료점 요구가 시대에 뒤떨어진 규칙이라는 입장을 밝혔습니다.

예외 처리

예외 처리는 구조적 프로그래밍의 기본 제어 구조와는 성격이 다른 형태의 흐름 제어입니다. 데이비드 와트는 프로그래밍 언어의 여러 제어 흐름 요소를 시퀀서라는 통일된 틀로 설명하면서, goto와 같은 점프 시퀀서는 목적지가 독자에게 자명하지 않다는 점에서 바람직하지 않은 반면, 반환 시퀀서나 탈출 시퀀서는 그 의도가 문맥만으로도 비교적 명확하다고 분석했습니다. 그는 나아가 예외 시퀀서가 점프나 탈출 시퀀서와도 또 다른 성격을 가진다고 구분했습니다.

예외적인 상황은 대체로 프로그램의 낮은 수준에서 감지되지만, 이를 처리하는 논리는 더 높은 수준의 코드에 자연스럽게 위치하는 경우가 많습니다. 만약 이러한 상황을 상태 플래그로 표현하고 호출자가 이를 매번 검사하도록 강제한다면, 응용 코드가 상태 검사로 어지러워지고 프로그래머가 부주의하게 검사를 빠뜨릴 위험이 있습니다. 예외는 반대로 프로그래머가 명시적으로 처리하지 않는 한 프로그램이 종료되는 기본 동작을 가지므로, 이러한 누락의 위험을 줄여줍니다. 다만 웨슬리 웨이머와 조지 네큘라가 지적한 것처럼, 예외는 프로그래머가 추론하기 어려운 숨겨진 제어 흐름 경로를 만든다는 부작용도 함께 지닙니다. 버트런드 마이어는 이와 다소 결이 다르게, break와 continue가 형태만 바꾼 goto에 지나지 않는다고 보고 이들의 사용을 강하게 만류했습니다.

코루틴과 다중 진입

일반적으로 함수는 다중 진입을 허용하지 않지만, 코루틴이나 제너레이터처럼 실행을 중단했다가 나중에 이어서 재개할 수 있는 예외적인 구조가 존재합니다. 코루틴은 값을 하나 넘겨주며 제어를 양보한 뒤 다시 호출되면 멈췄던 지점부터 실행을 계속하며, 스트림 처리나 상태 기계, 동시성 프로그래밍에서 흔히 활용됩니다. 코드 실행이라는 관점에서 보면 코루틴에서 값을 양보하는 동작은 함수가 실제로 종료된 것이 아니라는 점에서, 함수를 반환하고 종료하는 조기 종료보다 오히려 구조적 프로그래밍의 정신에 더 가깝다고 볼 수 있습니다. 다만 코루틴은 단일한 호출 스택 대신 여러 함수가 각자의 실행 상태를 동시에 유지한다는 점에서 또 다른 형태의 복잡성을 도입하기도 합니다.

상태 기계와 트램펄린 패턴

파서나 통신 프로토콜을 구현하는 프로그램은 서로 자연스럽게 이어지는 여러 상태를 가지는 경우가 많은데, 이러한 상태 전이는 세 가지 기본 구조만으로는 깔끔하게 표현되지 않을 수 있습니다. 일부 저수준 프로그램은 이런 상태 전이를 새로운 상태로의 직접적인 점프로 구현하기도 합니다. 그러나 각 상태 전이를 별도의 함수로 만들고, 현재 활성화된 상태를 가리키는 변수를 두어 반복문 안에서 해당 함수를 호출하는 방식으로도 동일한 로직을 구조적으로 표현할 수 있습니다. 이러한 기법은 트램펄린이라고 불립니다.

type StateHandler = (input: string) => StateHandler | null;

function idleState(input: string): StateHandler | null {
  return input === "start" ? runningState : idleState;
}

function runningState(input: string): StateHandler | null {
  return input === "stop" ? null : runningState;
}

function runTrampoline(inputs: string[]): void {
  let current: StateHandler | null = idleState;
  for (const input of inputs) {
    if (current === null) {
      break;
    }
    current = current(input);
  }
}

비판과 반론

스파게티 코드라는 용어의 등장

goto문의 무분별한 사용으로 뒤엉킨 제어 흐름을 가리키는 스파게티 코드라는 표현은 정확히 언제 처음 등장했는지 분명하지 않습니다. 마틴 홉킨스는 1972년 goto문을 제거하려는 주된 동기가 결과물이 스파게티 그릇처럼 보이지 않기를 바라는 데 있다고 언급하며 이 비유를 이른 시기에 사용했습니다. 리처드 콘웨이는 1978년 저서에서 프로그램이 스파게티 한 접시와 다를 바 없이 깔끔한 논리 구조를 갖는다는 반어적 표현을 사용했으며, 이 표현은 이듬해 데이비드 그리스와 함께 쓴 책에서도 반복되었습니다. 리처드 해밍은 자신의 강의에서, 이진 코드로 프로그래밍하던 초창기에 이미 누락된 명령을 끼워 넣기 위해 이전 명령을 빈 공간으로의 점프로 바꾸고, 그 빈 공간에 원래 명령과 새로 추가할 명령을 채운 뒤 다시 본래 위치로 돌아가는 점프를 덧붙이는 방식이 흔했으며, 이런 관행이 프로그램을 낯선 곳으로의 점프가 연속되는 구조로 만들었다고 회고했습니다.

스파게티 코드라는 표현이 널리 퍼진 이후에는 라자냐 코드, 라비올리 코드, 피자 코드처럼 파스타에 빗댄 여러 파생 용어도 등장했습니다. 라자냐 코드는 계층 구조로 나누기는 했으나 계층 사이의 결합이 지나치게 강해 한 계층을 수정하면 다른 계층도 함께 손봐야 하는 경우를 가리키며, 라비올리 코드는 개별 클래스 단위에서는 이해하기 쉽지만 전체적으로 조합했을 때는 설계가 불명확해지는 객체 지향 코드를 가리킵니다.

“구조적 프로그래밍은 goto 제거로 축소되었다”는 불만

데이크스트라는 구조적 프로그래밍이라는 개념이 대중화되는 과정에서 본래의 취지가 지나치게 단순화되었다고 여러 차례 불만을 표했습니다. 그는 자신의 논문이 구조적 프로그래밍이라는 표현을 명시적으로 다루지 않았음에도, 그의 서한과 노트가 결합되어 세간에서는 구조적 프로그래밍이 곧 goto문의 폐지와 동일시되었다고 지적했습니다. 그가 보기에 이러한 축소는 프로그램의 정당성을 증명 가능한 방식으로 구성한다는 본래의 목표를 놓치는 것이었습니다.

단일 종료 원칙에 대한 반론

피터 리치는 원칙적으로는 반환 직전에 놓인 단 하나의 throw문조차 단일 종료 원칙을 어기는 것이라고 인정하면서도, 데이크스트라의 규칙이 예외 처리가 프로그래밍 언어의 일반적인 패러다임으로 자리 잡기 이전에 쓰인 것이라는 점을 지적했습니다. 그는 반환 지점 하나에 더해 임의의 수의 throw 지점을 허용하자고 제안했으며, 예외를 감싸서 단일한 종료점을 억지로 만들어내는 해법은 오히려 중첩 깊이를 늘려 이해를 더 어렵게 만든다고 보았습니다. 아빈드 쿠마르 반살은 한 걸음 더 나아가, 예외 처리를 지원하는 언어에서는 for문처럼 예외가 없다면 단일 종료 성질을 갖는 제어 구조조차 실제로는 그 성질을 잃는다고 지적했습니다. 예를 들어 for문의 초기화 식에서 예외가 발생하면 통상적인 조건 검사 지점에는 결코 도달하지 못하기 때문입니다.

병렬 계산을 다루는 일부 현대적 환경에서는 오히려 단일 종료 원칙에 가까운 제약이 유지되기도 합니다. OpenMP의 병렬 구성 요소들은 병렬 블록의 내부에서 외부로 빠져나가는 조기 종료를 허용하지 않으며, 이러한 제약은 break문이나 예외를 포함한 모든 형태의 탈출에 적용됩니다. 다만 점프 대상이 병렬 블록 내부에 있는 경우라면 이러한 제약 없이 사용할 수 있습니다.

유산과 현재적 의의

순환 복잡도와의 관계

토머스 매케이브는 1976년 발표한 논문에서 제어 흐름 그래프에 존재하는 선형 독립 경로의 수를 세는 방식으로 프로그램의 복잡도를 정량화하는 순환 복잡도라는 지표를 제안했습니다. 매케이브는 진입점과 종료점이 각각 하나뿐인 구조적 프로그램의 순환 복잡도가 그 프로그램에 포함된 결정 지점의 수에 1을 더한 값과 정확히 같다는 사실을 증명했습니다. 이는 구조적 프로그래밍의 규율을 따르는 코드일수록 복잡도를 헤아리기가 그만큼 단순해진다는 것을 뜻합니다.

매케이브는 나아가 본질적 복잡도라는 개념도 함께 제시했는데, 이는 프로그램의 흐름 그래프에서 잘 구조화된 부분 구조를 하나의 노드로 계속 축약해 나갔을 때 남는 축약 불가능한 부분의 순환 복잡도를 가리킵니다. 완전히 구조화된 프로그램이라면 이 값은 언제나 1이 되며, 그렇지 않은 프로그램에서는 1보다 큰 값을 가집니다. 이러한 성질 덕분에 본질적 복잡도는 어떤 프로그램이 얼마나 구조적 프로그래밍의 규율에서 벗어나 있는지를 정량적으로 드러내는 지표로 활용됩니다.

현대 프로그래밍 언어에서의 위치

오늘날 순차, 선택, 반복이라는 세 가지 구조는 사실상 모든 절차적, 객체 지향적 언어에 기본적으로 내장되어 있으며, JavaScript와 TypeScript 역시 예외가 아닙니다. goto문은 애초에 이들 언어의 문법에 포함되어 있지 않으며, 대신 조건문, 반복문, 함수, 예외 처리, 필요하다면 레이블 붙은 반복문 정도가 goto가 담당하던 역할을 대체합니다. 다음 예시는 goto 기반의 다중 레벨 탈출을 레이블 붙은 반복문으로 구조화한 형태입니다.

function findPair(matrix: number[][], target: number): [number, number] | null {
  outer: for (let i = 0; i < matrix.length; i++) {
    for (let j = 0; j < matrix[i].length; j++) {
      if (matrix[i][j] === target) {
        return [i, j];
      }
      if (matrix[i][j] > target) {
        continue outer;
      }
    }
  }
  return null;
}

이제 구조적 프로그래밍은 더 이상 별도로 옹호하거나 논쟁할 대상이 아니라, 소프트웨어 개발이라는 활동 자체에 녹아든 공기와 같은 전제가 되었습니다. 다만 조기 반환, 예외 처리, 코루틴, 상태 기계처럼 원래의 엄격한 정리로는 온전히 설명되지 않는 요소들에 대한 논의는 오늘날에도 여전히 이어지고 있으며, 이는 구조적 프로그래밍이 하나의 완결된 규칙집이 아니라 프로그램을 이해 가능하게 만들기 위한 진화하는 실천 원리임을 보여줍니다.


참고 자료

  • Böhm, Corrado; Jacopini, Giuseppe (1966), “Flow Diagrams, Turing Machines and Languages with Only Two Formation Rules”, Communications of the ACM, 9(5), pp. 366-371
  • Dijkstra, Edsger W. (1968), “Letters to the Editor: Go To Statement Considered Harmful”, Communications of the ACM, 11(3), pp. 147-148
  • Dahl, Ole-Johan; Dijkstra, Edsger W.; Hoare, C.A.R. (1972), Structured Programming, Academic Press
  • Wirth, Niklaus (1971), “Program Development by Stepwise Refinement”, Communications of the ACM, 14(4), pp. 221-227
  • Knuth, Donald E. (1974), “Structured Programming with go to Statements”, Computing Surveys, 6(4), pp. 261-301
  • Harel, David (1980), “On Folk Theorems”, Communications of the ACM, 23(7), pp. 379-389
  • Rubin, Frank (1987), “‘GOTO Considered Harmful’ Considered Harmful”, Communications of the ACM, 30(3), pp. 195-196
  • McCabe, Thomas J. (1976), “A Complexity Measure”, IEEE Transactions on Software Engineering, SE-2(4), pp. 308-320
  • Kozen, Dexter; Tseng, Wei-Lung Dustin (2008), “The Böhm-Jacopini Theorem Is False, Propositionally”, Mathematics of Program Construction, Lecture Notes in Computer Science, vol. 5133, Springer, pp. 177-192
  • Dijkstra, Edsger W. (2001), “EWD1308: What Led to ‘Notes on Structured Programming’”, University of Texas at Austin EWD Archive
  • Haigh, Thomas (2010), “Dijkstra’s Crisis: The End of Algol and Beginning of Software Engineering, 1968-72”, Draft for SOFT-EU Project Meeting
  • Wikipedia contributors, “Structured programming”, Wikipedia
  • Wikipedia contributors, “Structured program theorem”, Wikipedia
  • Wikipedia contributors, “Spaghetti code”, Wikipedia
  • Wikipedia contributors, “Niklaus Wirth”, Wikipedia
  • Wikipedia contributors, “Harlan Mills”, Wikipedia
  • Wikipedia contributors, “Cyclomatic complexity”, Wikipedia
  • Encyclopaedia Britannica, “Edsger Dijkstra”, Britannica
  • ACM Digital Library, “Structured Programming” (Guide Books), Association for Computing Machinery
  • IEEE Computer Society, “About Harlan D. Mills”, IEEE Computer Society