[4/20] 조건문과 재귀
글을 시작하기에 앞서 해당 시리즈는 Allen Downey, Ben Lauwens의 저서인 Think Julia: How to Think Like a Computer Scientist 를 바탕으로 작성된 글임을 알려드립니다.
이 포스트는 Conditionals and Recursion를 한글로 요약 정리한 글입니다.
조건문과 재귀
이번 장의 가장 핵심적인 주제는 if
문이지만, 그 전에 간단한 연산자 두 개를 먼저 소개하려고 한다.
버림 나눗셈(floor division)과 나머지(Modulus)
버림 나눗셈(floor division)이란 두 수를 나눈 후 나머지를 버리는 계산을 의미하며, 연산자 기호는 ÷
이다. 예를 들어, 105분동안 상영하는 영화가 있다고 해보자. 우리는 영화가 대략 몇 시간이 걸리는지 알기 위해 60분으로 나누지만, 일반적인 나눗셈은 소수점까지 모두 반환한다.
1 |
|
그러나 우리는 일반적으로 시간을 소수점으로 표현하지는 않는다. 버림 나눗셈은 나머지인 소수점을 모두 버리고 정수만 반환한다.
1 |
|
위의 1시간을 제외하고 남은 시간이 몇 분인지 알고 싶다면 105분에서 1시간을 빼면 된다.
1 |
|
이외에도 나눗셈에서 나머지만 반환해주는 나머지 연산자 %
를 사용할 수 있다.
1 |
|
Tip 나머지 연산자는 생각보다 유용하다, 예로 한 숫자를 다른 숫자로 나눌 수 있는지 파악해야 할 때, 두 수의 나머지 연산 결과가 0인지로 확인할 수 있다. 또한 정수에서 가장 오른쪽의 숫자만 추출할 때도 사용가능하다. 이 경우, %10
을 사용하면 된다.
불 표현식
불 표현식(Boolean Expressions)은 true
또는 false
를 나타내는 식이며, 연산자는 ==
이다. 불 표현식의 예시를 보면, 연산자는 ==
를 기분으로 양 옆의 두 피연산자들을 비교한 후 같으면 true
를, 다르면 false
를 반환한다.
1 |
|
true
와 false
은 문자열이 아니라 Bool
이라는 데이터 타입에 속한다.
1 |
|
사용된 ==
연산자는 관계 연산자들 중 하나이다. 다른 관계 연산자들은 다음과 같다.
1 |
|
주의 줄리아 기호는 일반적인 수학 기호와는 조금 다르다. 줄리아에서 =
는 대입 연산자이고, ==
는 관계 연산자이다. 두 기호는 비슷해 보이지만 완전히 다르다.
논리 연산자
논리 연산자는 and 연산자인 &&
, or 연산자인 ||
, not 연산자인 !
총 세 가지가 있다. 논리 연산자들의 사용방법은 아래의 코드에서 확인해보자.
1 |
|
위의 코드는 &&
연산자이다. 위의 예시는 x가 0보다 크고 10보다 작으면 이 문장은 true
를 반환하는 것을 의미한다.
1 |
|
다음 코드는 ||
연산자이다. 위의 코드를 해석한다면, n값이 2나 3으로 나누었을 때 나머지가 0인 숫자면 해당 코드는 true
를 반환한다.
1 |
|
마지막으로 !
연산자이며, 이는 불 표현식을 무효화한다. 따라서 위의 예시를 본다면 x가 y보다 작거나 같은 경우에 true
를 반환하고, x가 y보다 크다면 false
를 반환한다.
조건문(Conditional) 실행
유용한 프로그램을 작성하려면 우리는 프로그램의 상태를 확인하고 그에 따라 알맞게 변경하는 기능이 필요하다. 조건문은 우리에게 이런 기능을 제공한다. 가장 간단한 형식은 If
문이다.
1 |
|
If
문 뒤에 쓰여 있는 불 표현식을 조건이라고 한다. 해당 조건이 참이라면 밑의 명령문이 작동되지만, 조건이 참이 아니라면 아무 일도 일어나지 않는다.
If
문이 함수 정의와 동일한 구조를 가진 경우를 복합 구문(compound statements)이라고 한다. 여기서 함수 정의를 다시 상기해본다면 헤더로 시작하여 본문을 작성 한후 마지막에 키워드 end
끝나는 구조이다.
복합 구문에 들어가는 명령문의 수는 제한이 없으며, 명령문이 아예 없는 본문도 작성이 가능하다 (하지만 본문이 없으면 어떤 변화도 일어나지는 않는다).
1 |
|
대체(Alternative) 실행
If
문의 두 번째 형식은 대체 실행(Alternative Execution)이다. 이 구조에서는 두 가지의 조건이 있으며, 각각의 조건에 따라 실행되는 코드가 결정된다. 해당 예시는 아래와 같다.
1 |
|
위의 코드는 숫자인 x가 짝수인지 홀수인지 구분하여 알려주는 프로그램이다. x를 2로 나누어 나머지가 0이 된다면 짝수이고, 0이 아니라면 홀수이다. 이 간단한 원리를 사용하여 조건에 충족하면 true
가 되어 첫 번째 본문을 실행하고, false
라면 두 번째 본문을 실행한다.
연결 조건문(Chained Conditionals)
때로는 두 개 이상의 조건이 주어지는 경우도 있다. 이럴 때에는 조건을 연결하는 연결조건문 구조가 사용된다. 해당 구조의 코드는 아래와 같다.
1 |
|
위에 사용한 elseif
문의 수는 제한이 없다. 또한 else
문을 쓰려면 반드시 끝에 입력해야 하지만, 필요없다면 작성하지 않아도 된다.
1 |
|
각 조건들은 순서대로 작동한다. 예를 들어 첫 번째 조건이 거짓이면 다음 조건으로 넘어가며, 만약 참인 조건을 찾으면 해당 본문이 실행되며 If
문은 종료된다. 그렇기 때문에 만약 2개 이상의 조건이 참인 경우에도 더 위에 작성되어 있는 조건만 실행된다.
중첩 조건문(Nested Conditionals)
하나의 If
문에도 다른 If
문이 중첩될 수 있다. 이전 예제를 가져와서 확인하면 다음과 같다.
1 |
|
겉에 있는 If
문은 간단한 구조를 가지고 있다. 하지만 else
문에 또 다시 If
문을 작성함으로서 두 개의 조건문을 중첩시킨 것을 확인할 수 있다.
이럽 중첩 조건문은 들여쓰기를 통해서 연결지점들을 구분해두지만, 코드를 빠르게 이해하기는 어렵다. 그렇기에 코드를 작성할 때 중첩 조건문은 피하는 것이 좋다.
논리 연산자를 이용하여 중첩 조건문을 단순화할 수 있다. 아래 예시를 보자.
1 |
|
위의 조건문은 중첩 조건문으로서 두 개의 조건을 모두 충족해야만 실행된다.
1 |
|
하지만 &&
연산자를 사용하면 단일 조건문으로 해결할 수 있다. 또한 줄리아는 아래와 같이 더 간결한 구문도 제공한다.
1 |
|
재귀(Recursion)
한 함수가 다른 함수를 호출하는 것은 충분히 가능한 일이다. 그렇다면 함수가 자기 자신을 호출하는 것도 가능할까? 위 질문에 대한 답은 가능하다는 것이다. 어떻게 가능한지 아래의 예시로 확인해보자.
1 |
|
위의 함수는 n
이 0이거나 음수인 경우 "Blastoff!"를 출력하며, 아닌 경우에는 n
을 출역한 후 n-1
을 인수로 갖는 countdown()
를 호출한다.
위의 함수는 어떤 결과를 보여줄까?
1 |
|
위의 예시를 보면, 조건문 원리를 이해할 수 있다.
- 3은 0보다 크기 때문에
else
문으로 넘어가서 3을 출력한 후 자기 자신을 호출하여 2를 인수로 제공한다. - 2 또한 0보다 크기 때문에
else
문으로 넘어가고 위와 같은 과정을 반복한다. - 1 또한 0보다 크기 때문에 위와 같다.
- 0은
If
문 조건에 해당하기 때문에 해당 본문이 실행된다. ("Blastoff!"를 호출한다.)
스스로를 호출하는 함수를 실행하는 과정을 재귀(Recursion)라고 한다. 다른 예로 문자열을 n
번 출력하는 함수를 만들어보자.
1 |
|
해당 예시는 n
이 n <= 0
이면 함수를 종료한다. 함수가 종료되면 그 결과가 사용자에게 반환되며, 나머지 명령문들은 실행되지 않는다. n
이 양수라면 나머지 명령문들이 실행되며, 해당 명령문의 작동방식은 countdown()
과 유사하다.
무한 재귀(infinite recursion)
도착점이 제공되지 않은 재귀는 함수 호출을 계속하며 종료되지 않는다. 이런 현상을 무한 재귀(infinite recursion)라고 한다. 실제로 무한 재귀는 일반적으로 잘 사용되지 않는다. 아래 코드는 무한재귀의 간단한 예시이다.
1 |
|
대부분의 프로그래밍 환경에서 무한 재귀가 있는 프로그램이 영원히 실행되지는 않는다. 줄리아는 최대 재귀 수준에 도달하면 오류 메시지를 보낸 후 실행을 중단한다.
1 |
|
위의 오류를 보면 해당 함수는 80000번을 반복했다. 대부분 무한 재귀는 함수를 잘못 사용해서 발생하기 때문에 해당 오류를 만난다면 함수의 사용법을 확인해야 한다.
키보드 입력(Keyboard Input)
줄리아는 readline()
이라는 내장 함수를 제공하여 프로그램을 중지하고 사용자가 무언가를 입력할 때까지 기다린다. 사용자가 내용을 입력하고 RETURN
또는 ENTER
를 누르면 해당 변수에 값이 적용된다. 이후 readline()
은 사용자가 문자열로 입력한 내용을 반환한다.
1 |
|
세미 콜론(;)을 사용하면 같은 줄에 여러 문장을 넣을 수 있다. REPL에서는 마지막 명령문의 결괏값만을 호출한다. 또한 사용자가 정수를 입력할 것으로 예상되면, 반환 값을 정수로 변환하여 가져올 수 있다.
1 |
|
하지만 만약 사용자가 입력한 데이터 타입과 다른 형태로 변환하면, 오류가 발생한다.
1 |
|
나중에 위와 같은 종류의 오류들을 처리하는 방법을 살펴볼 것이다.
디버깅
문법이나 런타임 오류가 발생하면, 오류 메시지는 과도하게 많은 정보들을 제공한다. 따라서 오류 메시지를 정확하게 파악하는 것도 중요하다. 가장 유용한 부분들은 다음과 같다.
- 오류의 종류는 무엇인가?
- 어디서 발생한 오류인가?
문법 오류는 찾기 쉽지만 몇 가지 문제가 있다. 일반적으로 오류 메시지들은 문제가 발견된 위치를 제공하지만, 실제 오류가 일어난 위치와 다를 수 있다.
런타임 오류도 마찬가지이다. 다음 예시로 확인해보자. 다음 예시는 신호 대 잡음비를 데시벨로 계산하는 공식을 구현한 것이다.
1 |
|
위의 코드를 수행하면 결과는 다음과 같다.
1 |
|
해당 결과는 예상했던 것과 다르다.
오류를 찾기 위해서, 변수인 ratio
의 값을 확인해보는 것이 좋을 것이다. 확인해보니 ratio
는 0으로 밝혀졌다. 문제는 3번째 줄에서 버림 나눗셈을 하여 나머지들을 제거해버리는 데서 온 것으로 확인되었다.