[6/20] 반복
글을 시작하기에 앞서 해당 시리즈는 Allen Downey, Ben Lauwens의 저서인 Think Julia: How to Think Like a Computer Scientist 를 바탕으로 작성된 글임을 알려드립니다.
이 포스트는 Iteration를 한글로 요약 정리한 글입니다.
반복 (Iteration)
이번 장에서는 명령문들을 반복해주는 반복문에 대해서 살펴볼 것이다. 우리는 이미 조건문과 재귀에서 for
루프를 사용한 반복문을 살펴보았다. 이번 장에서는 while
문을 사용하는 방법을 배울 것이다. 그 전에 변수 할당에 대해서 조금 더 알아보자.
재할당 (Reassignment)
제목에서 알 수 있듯이, 한 변수에 값을 여러 번 할당하는 것은 가능하다. 새로운 할당문은 이전의 값을 제거하고 새로운 값으로 변경한다.
1 |
|
처음 코드에서 x
는 값으로 5
를 할당받았지만, 두 번째 코드가 실행된 이후에는 값이 7
로 재할당된다.
이런 재할당의 방식은 다이어그램으로 보면 아래와 같다.

줄리아는 할당에 등호(=
)를 사용하기 때문에 수학식에서의 a``=``b
와 같은 동일성(equal)으로 이해하는 경우가 종종 있다. 하지만 이것은 잘못된 개념 정의이다.
먼저 동일성은 대칭관계이지만 할당은 그렇지 않다. 예를 들어 수학에서 a``=``7
인 경우 7``=``a
도 성립하지만, 줄리아에서는 a``=``7
로 할당한다고 해서 7``=``a
가 옳은 것은 아니다.
또한 수학에서 '동일성의 속성(a proposition of equality)'은 항상 참(true) 또는 거짓(false)이다. 예를 들어 a``=``b
가 참이라면 a
는 항상 b
와 같아야 한다. 하지만 줄리아는 재할당하면 위의 참 가정이 언제든지 깨질 수 있다.
1 |
|
위의 코드에서 볼 수 있듯이 a
을 재할당해도 b
도 같이 재할당되지 않는다. 이는 할당과 동일성이 다르다는 것을 증명한다.
주의 변수를 재할당 하는 것은 종종 유용하지만, 주의해서 사용해야 한다. 너무 자주 변경하면 이후 코드를 읽고 디버깅하기 어려울 수 있다. 또한 이미 생성된 변수와 같은 이름으로 함수를 생성할 수 없다.
변수 업데이트 (Updating Variables)
일반적인 재할당 유형은 업데이트이며, 변수의 새 값은 이전 값에 따라 다르다.
1 |
|
위의 코드는 x
의 현재 값을 얻고, 1을 더한 다음 x
를 새로운 값으로 업데이트한다.
하지만 아직 생성되지 않은 변수를 업데이트 하려고 한다면, 줄리아는 오류를 일으킨다.
1 |
|
변수를 업데이트하기 전에, 간단하게 값을 할당하여 초기화를 해야 한다.
1 |
|
1을 추가하여 변수를 업데이트 하는 것을 증가(increment)라고 하며, 1을 빼는 것을 감소(decrement)라고 한다.
while
문
컴퓨터는 종종 반복적인 일들을 자동으로 사용한다. 오류 없이 동일한 일을 반복하는 것은 컴퓨터가 가장 잘하는 일이며, 사람들이 가장 못하는 일이다.
우리는 이미 재귀를 사용하여 반복하는 countdown()
, printn()
를 보았다. 반복문은 많이 사용되기 때문에 줄리아는 반복문을 더 쉽게 만들 수 있도록 설계되었다. 그 중 하나인 for
문은 우리가 간단한 반복 파트에서 봤기 때문에 while
문 먼저 볼 것이다. 아래 코드는 while
문을 사용하여 작성한 countdown()
이다.
1 |
|
while
문에 대해서 본 적이 없을지라도 위의 코드를 대부분 이해할 수 있을 것이다. n
이 0보다 크다면, n
을 보여주고 n
에서 1을 뺀다. n
이 0이 될 때까지 반복하다가 0이 되면, "Blastoff!"를 반환한다.
더 형식적으로, while
문의 실행 흐름을 정리해보자.
while
문의 조건이 참인지 거짓인지 판별하라- 조건이
true
인 경우 본문을 실행한 후, 다시 1단계로 돌아가라 - 조건이
false
인 경우while
문을 종료하고 다음 명령문을 실행하라
위와 같이 명령문을 실행한 후 다시 앞으로 올라가는 실행 흐름의 종류를 루프(loop)라고 한다.
루프의 본문은 조건이 결국 false
가 되어 루프가 종료되도록 하나 이상의 변수 값을 변경해야 한다. 그러허지 않으면 루프가 영원히 반복되며, 이런 현상을 무한 루프라고 한다.
countdown()
의 경우에서는 루프 본문에서 n
을 변경함으로써 n
이 0이나 음수가 되면 루프가 종료되게 설정하였다. 이는 유한한 루프임이 확실하다.
이제는 다른 루프도 살펴보자.
1 |
|
위 루프의 조건은 n!= 1
이다. 따라서 이 루프는 n
이 1이 되어 false
가 될 때까지 반복될 것이다.
루프가 매번 작동할 때, 프로그램은 n
의 값을 도출한 후 해당 n
이 짝수인지 홀수인지 확인한다. 만약에 n
이 짝수라면 2로 나눠지며, 홀수라면 n
은 n*3 + 1
값으로 대체된다. 예를 들어 seq()
에 인수로 3을 준다면, 결괏값으로 3,10,5,16,8,4,2,1을 도출할 것이다.
n
은 증가하거나 감소하기 때문에 n
이 1이 되어 프로그램이 종료된다는 명백한 증거는 없다. 다만 위의 예시와 같이 n
의 특정 값에 대해서는 종료를 증명할 수 있다. 예를 들어 시작 값이 2의 거듭제곱인 경우 n
은 루프를 통과할 때마다 짝수가 되며, 결국 1이 되어 종료될 수 있다.
여기서 어려운 질문은 과연 프로그램은 n
이 모든 양수 값일 때 종료되는가이다. 지금가지 누구도 저 문제를 증명하거나 반증하지 못했다.
break
문
때때로 함수를 작성하다보면 루프를 끝내는 지점을 처음에 정하지 못할 수도 있다. 그런 경우에는 break
문을 사용하여 루프를 빠져나올 수 있다.
예를 들어, 만약에 사용자가 done
이라고 작성할 때까지 사용자로부터 입력을 받고 싶다고 가정하자. 그렇다면 아래와 같이 코드를 작성할 수 있다.
1 |
|
해당 루프의 조건은 항상 true
이기 때문에, 루프는 break
문에 도달할 때까지 계속 반복된다.
매번 사용자에게 "> "가 표시되며, 사용자가 done
이라고 입력하는 즉시 break
문이 루프를 종료한다. 만약 done
을 입력하지 않으면, 사용자가 입력한 내용을 반영하고 다시 루프의 위로 올라간다.
실행한 결과는 다음과 같다.
1 |
|
이렇게 루프를 작성하는 방법은 루프의 어느 곳에서든 조건을 확인할 수 있고, 멈춰야 하는 지점도 정확히 설정할 수 있기 때문에 일반적으로 사용된다.
continue
문
break
문이 루프를 빠져나가게 한다면, continue
문은 명령문들 중간에서 루프의 시작점으로 올려준다. 예를 통해 확인해보자.
1 |
|
위 코드 실행의 결과는 다음과 같다.
1 |
|
코드를 살펴보면, i
가 3으로 나눠지면 continue
문을 만나서 루프의 처음으로 돌아가며, i
가 3으로 나눠지지 않으면 그대로 print()
로 넘어간다. 따라서 결과에서 3으로 나눠지는 수들은 다 제외되고 출력된 것이다.
제곱근 (Square Roots)
루프는 반복하여 수치 결과를 개선하는 프로그램에도 자주 사용된다.
예로 제곱근을 계산하는 방법 중 하나인 뉴턴의 방법을 보자. 만약 a
라는 수의 제곱근을 알고 싶다면, 아래의 공식을 반복하여 값을 찾을 수 있다.
\[ \begin{equation} y= \frac{1}{2}\left(x+\frac{a}{x}\right) \end{equation} \]
임의로 a
에 4을 할당하고, x
에 3을 할당한다.
1 |
|
위 결과는 4의 제곱근인 2와 가깝다. 만약에 위 과정을 반복한다면, 2와 더 가까운 결과를 얻을 수 있다.
1 |
|
몇 번의 업데이트 이후, 거의 2와 같은 결과를 얻었다.
1 |
|
일반적으로 정답에 도달하는데 걸리는 반복 수를 미리 예측할 수는 없지만, 일정 구간이 지나면 값이 도달하여 변하지 않는 것을 확인할 수 있다.
1 |
|
y == x
에 도달했을 때는 반복문을 멈출 수 있다. 위의 과정을 while
문으로 작성하면 아래와 같다.
1 |
|
대부분의 값은 정상적으로 작동하지만 \[\sqrt{2}\] 와 같은 수들은 Float64
로 표현되지 않기 때문에 피하는게 좋다.
y
와 x
가 정확하게 동일한지 확인하기 위해서는 내장 함수 abs()
를 사용하여 이들의 차이를 절대 값으로 계산하는 것이 더 안전하다.
1 |
|
여기서 ε
(\varepsilon TAB
)은 0.0000001
과 같이 얼마나 가까운지 결정한다.
알고리즘
뉴턴의 방법은 알고리즘 예시 중 하나이다. 알고리즘(Algorithms)은 제곱근 계산과 같이 문제들을 해결하는 기계적인 프로세스이다.
알고리즘이 무엇인지 이해하기 위해서는 알고리즘이 아닌 것들을 먼저 보는 것이 더 효과적일 수 있다. 예로 한 자리 숫자를 곱하는 법을 배울 때, 단순히 곱셈표를 외웠을 것이다. 이런 종류의 지식은 알고리즘이라고 할 수 없다.
그러나 게으른 사람이라면 몇 가지의 요령을 획득했을 것이다. 예를 들어 n과 9의 곱을 찾으려면 n−1을 첫 번째 숫자로, 10−n을 두 번째 숫자로 사용하여 답을 만들 수 있다. 이 식은 한자리 숫자와 9를 곱하는 일반적인 해결방안이다. 이것이 알고리즘이다.
알고리즘의 특징 중 하나는 이를 수행할 지능이 필요하지 않다는 점이다. 알고리즘들은 간단한 규칙에 따라서 진행하는 기계적인 프로세스이다.
알고리즘 실행은 지루하지만 알고리즘 설계는 흥미롭고 도전적이다. 또한 알고리즘 설계는 컴퓨터 과학의 핵심이다.
사람들이 자연스럽게 하는 것들 중 일부는 알고리즘으로 구현하기가 어렵다. 예로 자연어를 이해하는 것 등이다. 우리는 모두 자연어를 이해하고 있지만, 알고리즘적으로 어떻게 이해하는지 설명하는 것은 매우 어렵다.
디버깅
더 큰 프로그램을 작성하기 시작하면 디버깅에 많은 시간을 투자하게 된다. 코드가 많을수록 오류가 발생할 확률도 올라가며 버그가 숨어있을 수 있는 공간도 많아진다.
디버깅 시간을 줄일 수 있는 한 가지 방법은 이분법 디버깅이다. 이분법 디버깅은 프로그램 코드를 중간 단위로 분해하여 확인하면서 문제점을 빠르게 찾는 방법이다. 예를 들어 프로그램 코드가 100줄이라면, 50줄 쯤에서 나눈 다음 그 근처를 확인하는 것이다. 확인하는 방법은 print
문을 넣어 코드가 잘 작동하는지 보는 것이다. 이때 잘 작동한다면 프로그램 후반부에 문제가 있는 것이고, 아니라면 전반부에 문제가 있는 것이다. 이 방식으로 검사하면 100줄을 전부 다 볼 필요 없다.
하지만 실제 상황에서는 프로그램 중간이 무엇인지 명확하지 않을 수 있다. 코드 줄 개수에 맞춰 반으로 나누는 것은 합리적이지 않다. 따라서 프로그램에 오류가 있을 수 있는 장소와 코드를 먼저 점검한 후, 개발자가 생각하는 합리적인 중간점을 찾는 것이 좋다.