Statement-Level Control Structures
8장의 주제는 문장 단계 제어 구조입니다. 프로그램의 제어 흐름, 또는 실행 순서는 여러 단계에서 검사할 수 있습니다. 프로그램에서 발생할 수 있는 제어 흐름은 다음과 같이 3가지로 나눌 수 있습니다.
- 식 내 제어 흐름 -> 연산자 우선순위 및 관련성
- 문장 간 제어 흐름 -> 문장 단계 제어 구조
- 유닛 간 제어 흐름 -> 프로시저 호출
식 내에서의 제어 흐름은 7장에서 다루었고, 이번 장에서는 프로그램 내의 문장들 사이에서 발생하는 제어 흐름에 대해 알아보겠습니다.
연산을 표현하기 위해서는 시퀀스, 선택 및 논리 반복문 절대적으로 필요하다는 이론적 결과가 있습니다.
명령형 언어에서 연산은 식을 수행하고 결과 값을 변수에 할당하는 과정입니다. 그러나 결과 값은 배정문 만으로 얻기 힘들며, 일반적으로 제어문이 추가적으로 필요합니다. 명령형 언어에서 제어는 제어 흐름 경로 중에서 선택하는 것(조건문), 특정 문장을 반복적으로 실행하는 것(반복문)으로 구성되어 있습니다.
초기 프로그래밍 언어인 Fortran의 제어문은 기계의 명령어와 직접적으로 관련되어 있었습니다. 따라서 언어의 설계보다는 명령어 설계의 결과였습니다. 당시에는 프로그래밍의 어려움에 대해서 사람들이 체감하지 못했기 때문에 이것이 문제가 없다고 느꼈지만, 현대에서 이러한 제어문은 부적절하다고 판단하고 있습니다.
프로그래밍 언어가 발전해오면서, 순서도로 표현할 수 있는 모든 알고리즘은 단 두 개의 제어문만으로 코딩이 가능하다는 것이 증명되었습니다. 하나는 if를 비롯한 조건문이고, 나머지 하나는 for를 비롯한 반복문입니다.
많은 제어문은 작성력을 향상시킬 수 있습니다. 그러나 제어문의 수가 많아질수록 언어의 단순성은 낮아집니다. 그렇다면 단순성을 크게 떨어트리지 않고 작성력을 높이기 위해서는 언어를 얼마나 확장해야 할까요? 이 부분은 이렇다! 할 만한 정확한 정답을 찾기 힘듭니다. 제어문이 너무 적으면 단순한 제어문을 반복적으로 사용해야하기 때문에 오히려 가독성을 낮출 수 있습니다. (ex. switch 문의 부재 - if 문의 과도한 사용)
순서도에서 선택문과 반복문은 슬라이드의 그림과 같이 표현합니다.
ALGOL 60 언어에서는 단일 문장으로 추상화할 수 있는 문장의 모음을 복합문으로, 복합문에 변수 선언이 포함된 것을 블록이라고 정의합니다. Pascal 언어는 ALGOL 60의 설계를 따라 복합문을 정의하지만, 블록을 허용하지는 않습니다. C 언어는 중괄호를 사용하여 복합문과 블록을 모두 무제한으로 허용하고 있습니다.
복합문의 설계에는 단 한 가지 고려 사항이 있습니다. 바로 제어문이 여러 개의 진입점을 가질 수 있는가에 대해서입니다. 대부분의 컴퓨터과학자는 제어문이 여러 개의 진입점을 갖는 것은 복잡하므로 가독성이 떨어지는데 비해, 유연성이 그다지 늘어나지 않는다고 생각하고 있습니다.
선택문(Selection Statement)은 프로그램에서 두 개 이상의 실행 경로 가운데 하나를 선택할 수 있는 문장입니다. 선택문을 일반적으로 2방향 선택문과 다방향 선택문으로 나뉩니다.
먼저 2방향 선택문부터 다뤄보도록 하겠습니다. 대부분 언어에서 2방향 선택문의 구조는 매우 유사하지만, 다음과 같은 설계 고려 사항에 따른 미묘한 차이가 존재합니다.
- 선택을 제어하는 식의 타입은 무엇인가? (대부분의 언어에서는 불리안 식, C 언어에서는 산술 표현식)
- 단일 명령문만 선택할 수 있습니까, 아니면 명령문의 그룹도 선택할 수 있습니까?
- 다른 선택자의 ‘then’ 문에 중첩된 선택자의 의미를 어떻게 지정해야 합니까?
모든 명령형 언어에는 단방향 선택기(Single-Way Selector)가 포함되어 있는데, 대부분 양방향 선택기의 하위 형태로 구현되어 있습니다. 그러나 BASIC과 Fortran은 예외이므로, 이 부분을 잠시 짚고 넘어가겠습니다.
Fortran 언어에서 If(불리안 식) 문에서는 단일 선택만 가능하고, 중첩이 허용되지 않습니다. 이것은 GOTO 문의 사용을 촉진합니다. 매우 단순하지만, 유연성이 부족하다는 단점이 있습니다.
복합문은 문장의 그룹을 조건부로 실행하기 위한 간단한 방법입니다, ALGOL 60 언어에서는 begin과 end를 통해 복합문의 그룹을 명세합니다.
Fortran 77, 90을 포함하여 ALGOL 60의 영향을 받은 대부분의 언어는 복합문을 선택할 수 있는 단방향 선택기를 제공하고 있습니다.
양방향 선택기(Two-Way Selector)는 두 가지 제어 경로 중 하나를 선택하는 것을 허용하는 제어문입니다. ALGOL 60에서는 if-then-else를 이용하여 이것을 구현했습니다.
또한 선택자를 중첩할 수도 있습니다. 그러니 중첩 선택자들 간에 모호함이 발생한다는 문제가 있습니다. 예를 들어, 슬라이드에 있는 코드처럼 if - if - then - else 문을 사용했을 때, else가 어떤 if 문에 대해 수행되는지가 문제입니다. (들여쓰기를 보고 첫 번째 if 문과 매칭되야 할 것으로 착각하실 수 있는데, Python 같은 특수한 언어를 제외하고 들여쓰기는 언어에 아무런 영향을 미치지 않습니다)
대부분의 명령형 언어에서는 else 문이 가장 가까이 있고 짝이 없는 then 절과 짝을 이루도록 지정합니다.
ALGOL 60 언어에서 if 문은 else 문이 then에 직접 연결될 수 없고, 반드시 복합문에 넣어야 합니다.
if 문의 마지막 절이 then이든 else이든 복합문이 아닌 경우, 전체 선택 구조의 끝을 표시하는 구문 요소가 없습니다. 만약 선택 구조의 끝을 나타내는 특수어를 사용한다면 중첩 선택문의 모호함을 해결하고, 가독성을 향상시킬 수 있습니다. Modula-2 언어에서는 END, Fortran 77에서는 END IF를 사용하여 if 문의 끝을 나타냅니다.
다중 선택기(Multiple Selector)는 여러 개의 문장이나 문장 그룹 중 하나를 선택하는 제어문입니다. 선택기의 일반화 개념으로 이해하시면 됩니다. 다중 선택기의 설계 고려 사항은 다음과 같습니다.
- 선택을 제어하는 문장의 형식과 타입은 무엇인가?
- 단일 명령문만 선택할 수 있습니까, 아니면 명령문의 그룹도 선택할 수 있습니까?
- 전체 구성이 구문 구조로 캡슐화되어야 합니까?
- 선택 가능한 단일 명령문만 포함하도록 구조를 통한 실행 흐름을 제한해야 합니까? (ex. switch - break)
- 만약 표현되지 않는 선택자 식의 값이 존재할 경우, 어떻게 처리할 것인가? (ex. 1, 2, 3 선택인데 조건식이 4가 나온 경우)
Fortran에서는 슬라이드의 코드와 같이 GOTO 문을 이용하여 다중 선택기를 사용했습니다.
현대의 다중 선택기는 Case 문을 이용하고 있습니다. ALGOL-W 에서 다중 선택기의 구조는 캡슐화되어 있으며, 선택 가능한 단일 세그먼트를 제공합니다. 실행하는 문장은 조건식에 값에 의해 선택된 문장들입니다.
Pascal 언어에서 선택 가능한 세그먼트에는 라벨이 지정되어 있습니다. 조건식은 정수, 불리안, 문자와 같은 순서 타입(Ordinal Type)으로 구성되어 있습니다. 조건식이 수행되고 나서 값이 세그먼트의 상수와 비교됩니다. 상수 목록은 조건식과 동일한 타입이어야 하며, 목록들 간에 상호 베타적이지만 완전할 필요는 없습니다. (ex. 1, 2, 3 과 같이 겹치는 부분이 없어야 하며 상수 목록이 정수 전체를 포함할 필요는 없음)
C 언어에서 제어식과 상수식은 모두 정수 타입입니다. 또한 세그먼트 별로 암묵적인 분기를 지원하지 않습니다. (즉, 명시적으로 세그먼트 별로 break를 나타내야 정확하게 분기됩니다)
만약 순서 타입이 아닌 불리안 식을 기반으로 선택을 하는 경우에는 중첩된 양방향 선택기를 사용하여 다중 선택기처럼 만들 수 있습니다. 예를 들어, Ada 언어나 Fortran 90 에서 불리안 식과 else if를 사용한 코드가 슬라이드에 나와 있습니다. (Python도 이와 비슷하게 구현됩니다)
반복문(Iterative Statement)은 명령어, 또는 명령어 모음이 여러 횟수만큼 실행되도록 하는 문장입니다. 반복문을 흔히 루프(Loop)라고도 부릅니다. 반복문은 반복적 구조보다는 재귀 같은 것을 통해 함수형 언어로 수행되는 경우가 많습니다. 처음 개발된 반복문은 배열에 포함된 데이터를 처리하기 위해 개발되었기 때문에, 배열과 직접적인 관련이 있습니다.
반복문의 설계 고려 사항은 다음과 같습니다.
- 반복이 어떻게 제어되는가? (논리, 계수, 또는 그 두 가지의 혼합형)
- 제어 메커니즘이 루프 어느 부분에 위치해야 하는가? (앞부분, 뒷부분, 또는 사용자 마음대로)
먼저 계수로 제어하는 반복문에 대해 알아보겠습니다. 이러한 반복문은 초기 값(Initial), 종료 값(Terminal), 단계 크기(Stepsize)로 구성되어 있습니다. (이것들을 루프 매개변수라고 부릅니다) 계수로 제어하는 반복문은 종종 기계 명령어에 의해 지원됩니다. 이 때 설계 고려 사항은 다음과 같습니다.
- 루프 변수의 유형과 범위는 무엇인가? (정수, 문자, 열거형, 부동 소수점 등)
- 루프 종료 시 루프 변수에는 어떤 값이 들어가는가?
- 루프 변수, 또는 루프 매개변수가 루프 내에서 변경되는 것이 허용되는가? 그렇다면 그 변경 사항이 루프 제어에 영향을 주는가?
- 반복의 완료 검사는 루프의 맨 위에서 이루어지는가, 맨 아래에서 이루어지는가?
- 루프 매개변수는 한 번만 평가되는가? 아니면 각 반복에 대해 매번 평가되는가?
Fortran IV 언어에서 반복문은 DO를 이용하여 사용합니다. 루프 변수에 대한 검사는 반복문의 수행 후 발생하며(Post-test), 초기 값, 종료 값, 단계 크기 변수는 부호 없는 정수, 또는 양수 값을 갖는 단순 정수 변수로 제한됩니다.
만약 루프가 정상적으로 종료한다면 루프 변수의 값은 정의되지 않으며, 만약 비정상으로 종료된다면 가장 최근에 할당된 값이 저장됩니다.
루프 변수와 루프 매개변수는 루프 내부에서 변경할 수 없기 때문에 루프 매개변수를 두 번 이상 평가할 필요가 없습니다. (즉, 반복문이 실행되기 전에 몇 번 반복을 해야하는지 예측할 수 있음)
또한 Fortran IV 에서 루프 내부는 단일 명령어만 사용이 가능하기 때문에, 명령어 집합을 반복하고 싶다면 GOTO 문을 활용해야 합니다.
Fortran 77과 90에서 DO 문은 많이 변경되었습니다.
먼저 루프 변수에 대한 검사는 반복문의 수행 전 발생하며(Pretest), 루프 변수는 정수, 실수, 그리고 확장 실수 타입에 대해서도 가능합니다. 루프 매개변수는 표현식으로 나타내는 것이 허용되며, 양수 값과 음수 값을 모두 가질 수 있습니다.
루프는 루프 매개변수가 아니라 반복 횟수에 따라 제어됩니다. 따라서 루프 내에서 루프 매개변수가 변경되더라도, 이것은 루프 제어에 영향을 미치지 않습니다. 즉, 반복 횟수는 사용자가 접근할 수 없는 내부 변수입니다.
DO 반복문에 진입하는 것은 DO 문을 사용해서만 입력할 수 있습니다. (단일 진입 구조)
ALGOL 60에서 반복문은 계수기와 불리안 식으로 루프를 제어할 수 있습니다.
또한 for의 모든 표현식은 루프 문의 모든 반복이나 실행에 대해 평가됩니다.
Ada 언어에서 반복문은 비교적 간단한 계수기-제어 사전 검사 루프입니다. reverse는 discrete_range의 범위를 역순으로 루프 변수에 할당한다는 의미이고, discrete_range는 1~10 이나 Monday~Friday와 같이 정수나 열거 타입의 부분 범위를 말합니다.
Ada 언어의 가장 큰 특징은 루프 변수의 범위입니다. 루프 변수는 루프 안에서만 묵시적으로 선언되고, 루프 종료 후에는 무시됩니다. 예를 들어, 슬라이드에 나와 있는 코드에서 COUNT는 실수형 변수로 1.35가 할당됩니다. 그런데 for 문의 루프 변수의 이름도 동일하게 COUNT입니다. 이 상황에서, 처음 선언한 COUNT 변수는 for 문에 영향을 받지 않습니다. 즉, for 문이 끝나도 COUNT 변수에는 여전히 실수값 1.35가 저장됩니다. 또한 루프 변수에는 루프 내부의 값을 할당할 수 없습니다.
다음으로 가장 친근한 언어인 C 언어의 경우를 살펴보겠습니다. C 언어도 Ada 언어와 마찬가지로 사전 검사 계수 루프 구조입니다. C 언어의 for 문이 어떻게 구성되어 있는지는 다들 아실테니 생략하도록 하겠습니다. C 언어의 for 문은 두 번째 표현식의 값이 0이면 종료됩니다. 그리고 그 외에는 루프 내부의 명령어, 또는 명령어 집합이 실행됩니다.
또한 C 언어 for 문의 루프 매개변수들은 모두 선택적입니다. 예를 들어, 두 번째 식을 생략한다면 무한 루프로써 간주되고, 첫 번째와 세 번째 식이 생략되면 루프 변수를 고려하지 않습니다. 먄악 첫 번째 식만 생략되면 단순히 초기화가 일어나지 않는다는 것을 의미합니다.
반복 제어가 불리안 식에 기반하는 반복문을 논리 제어 루프(Logically Controlled Loop)라고 합니다. 논리 제어 루프는 계수기 제어 루프보다 일반적입니다. 즉, 모든 계수기 제어 루프는 논리 제어 루프로 표현할 수 있지만, 그 반대는 불가능합니다. 논리 제어 루프에서의 설계 고려 사항은 다음과 같습니다.
- 제어가 사전 검사(Pretest)인가 사후 검사(Post-test)인가?
- 논리 제어 루프가 계수기 제어 루프의 특별한 형식인가, 아니면 전혀 다른 문법을 가지는가?
예를 들어, Pascal, Modula-2, C 기반 언어들은 사전 검사와 사후 검사 논리 제어 루프를 모두 가지고 있습니다. 예를 들어, C 언어의 while 문은 사전 검사 논리 제어 루프이고, do-while 문은 사후 검사 논리 제어 루프입니다.
상황에 따라서는 프로그래머 루프 제어를 위한 검사를 루프의 처음이나 마지막이 아닌 곳을 선택해야할 수도 있습니다. 몇몇 언어는 이러한 기능을 제공하는데, 이것을 사용자 지정 루프 제어(User-located Loop Control)라고 합니다. 구현 자체는 간단하지만, 역시 이전 루프문과 마찬가지로 설계 고려 사항이 존재합니다.
- 조건 매커니즘이 탈출에 필수적이어야 하는가?
- 탈출할 때는 하나의 루프만 탈출해야 하는가, 아니면 포괄하는 루프를 모두 탈출해야 하는가?
Ada 언어에서는 루프문에 레이블을 붙일 수 있습니다. 따라서 루프문 내에서 특정 레이블 루프를 지정하여 탈출하는 것이 가능합니다.
C 언어에서는 루프문에 레이블을 붙일 수 없기 때문에 무조건 가장 가까이 있는 루프문만을 지정할 수 있습니다. break를 통해 가장 가까이 있는 루프문을 탈출할 수 있으며, continue를 통해 루프문을 탈출하지 않고 현재 반복에서 나머지 명령어를 생략하는 방식이 가능합니다.
자료구조에 기반한 반복문도 있습니다. 이 때 루프는 계수기나 불리안 식이 아니라 자료구조의 원소 수에 의해 제어됩니다.
Java 언어에서는 배열의 값이나 Iteratable 인터페이스를 통해 구현된 객체를 통해 for 문을 제어할 수 있습니다. 예를 들어, 문자열을 포함하는 myList라는 이름의 ArrayList 컬렉션이 있는 경우, 조건문에 이를 나타내면 각 원소를 myElement로 설정하며 반복문을 수행합니다.
C# 언어도 마찬가지로 문자열 자료형인 String을 이용하여 foreach 문을 제어하는 것이 가능합니다.
무조건 분기문(Unconditional Branch)는 실행 제어를 프로그램의 지정된 위치로 이동시킵니다.
무조건 분기문의 대표적인 명령어는 바로 GOTO 문으로, 프로그램의 실행 흐름을 제어하는 가장 강력한 명령문이지만 이 힘으로 인해 사용을 위험하게 만듭니다. (큰 힘에는 큰 책임이 따른다…)
명령어의 실행 순서가 작성된 순서와 거의 동일할 때 가독성이 가장 좋습니다. (일반적으로 위해서 아래) 따라서 이러한 무조건 분기문은 가독성을 매우 낮출 수 있습니다. 그러한 문제점으로 인해 Modula-2, Bliss, CLU 등과 같은 언어는 GOTO 문이 없이 설계되었습니다. (+ Java, Python, Ruby) 여담으로 GOTO 문의 위험성을 알린 사람이 바로 다익스트라 알고리즘으로 유명한 에드가 다익스트라(Edger Dijkstra)입니다.
그러나 현재 가장 많이 사용되는 언어들에는 GOTO 문이 포함되어 있습니다. (대표적으로 C 언어)
반복문에서 다루었던 루프 탈출 명령어는 사실 일종의 GOTO 문입니다. 그러나 이들은 상당히 역할이 제한된 GOTO 문이므로 가독성에 악영향을 미치지 않고, 오히려 가독성을 향상시키기도 합니다. 왜냐하면 이것들을 사용하지 않는다면 이해하기가 더 어렵거나 부자연스러운 코드가 나타날 수 있기 때문입니다.
무조건 분기문을 사용하기 위해서는 어느 위치로 이동할지 표시할 수 있어야 합니다. 이 때 많은 언어들이 코드 앞에 레이블을 붙이는 것으로 해결합니다. ALGOL 60 언어나 C 언어는 식별자를 이용하여 레이블을 붙이고, Fortran 언어나 Pascal은 음이 아닌 상수 정수를 이용하여 나타냅니다. PL/I은 특이하게 변수를 이용한 레이블을 허용합니다.
이러한 레이블을 허용하는 대부분의 언어에서는 레이블의 사용을 어느 정도 제한하고 있습니다. 만약 Pascal 언어에서 레이블은 변수처럼 정의되지만, 그것을 매개변수로 전달하거나, 저장하거나, 수정할 수 없습니다. 또한 Pascal에서의 GOTO 문은 실행이 시작되었고 아직 종료되지 않은 경우를 제외하고, 제어 구조의 복합문의 명령어를 대상으로 지정할 수 없습니다.
예를 들어, 슬라이드의 왼쪽에서는 GOTO 100을 지정했지만, 레이블 100을 포함하고 있는 while 문은 이미 실행이 종료되었기 때문에 불가능합니다. 오른쪽과 같이 실행은 되었으나 종료되지 않은 경우에만 지정이 가능합니다.
마지막으로 보호 명령(Guarded Command)에 대해 알아보겠습니다. 보호 명령은 동시성 프로그래밍(Concurrent Programming)을 위해 도입된 개념이며, 다익스트라가 선택문과 비슷한 구조로 만들었습니다. 보호 명령의 각 줄은 한 개의 불리안 식(보호)와 한 개의 명령어, 또는 명령어 묶음으로 구성됩니다.
형태는 다중 선택문과 유사하지만, 이 표현식에 도달한다면 모든 불리안 식이 평가됩니다. 둘 이상의 식이 참인 경우, 해당 명령어 중 하나가 비결정적으로(=무작위로) 선택됩니다. 만약 참이 하나도 없다면 프로그램을 종료시키는 실행 시간 오류가 발생합니다. 이것은 프로그래머가 모든 가능성을 고려하도록 강요합니다.
예를 들어, 슬라이드에 나와 있는 것처럼 Ada 언어로 보호 명령을 구현한 코드를 보겠습니다. 만약 i = 0이고 j = 1이라면, 첫 번째와 세 번째 명령어 중 무작위가 선택되어 수행됩니다. 만약 i = j이고 i가 0이 아니면, 어떤 조건도 참이 아니기 때문에 실행 시간 오류가 발생합니다.
보호 명령을 사용하면 프로그램을 좀 더 깔끔하게 작성할 수 있다는 장점이 있습니다. 예를 들어, 슬라이드 아래에 나와 있는 코드는 4개의 정수 q1, q2, q3, q4가 주어져 있을 때 이를 오름차순으로 정렬하는 코드입니다. 이것은 반복문이기 때문에 모든 불리안 식이 거짓이면 루프가 종료되는 방식입니다. 만약 이것을 보호 명령을 사용하지 않고 구현한다면 정렬 알고리즘을 사용해야 하는데, 이것은 상당히 많은 코드를 요구하는 문제가 있습니다.
이 순서도는 보호 명령을 사용한 조건문과 반복문이 어떤 과정을 통해 수행되는지 나타내는 그림입니다. 조건문은 모든 불리안 식이 거짓이면 실행 시간 에러를 내고, 두 개 이상의 조건이 참이면 그 중 무작위로 선택하여 실행합니다. 반복문은 모든 불리안 식이 거짓이면 빠져나오고, 두 개 이상의 조건이 참이면 그 중 무작위로 선택하여 실행합니다. 물론 한 개의 조건만 참이면 그 명령어만 실행합니다.
8장의 내용은 여기까지입니다. 읽어주셔서 감사합니다!
Leave a comment