Fortran · 기본
계산으로 배우는 현대 Fortran
조건과 반복 - 이름 붙은 do 루프
if·else if, select case 와 범위, do 카운터 루프·간격, do while, exit·cycle, 이름 붙은 루프로 바깥 루프 빠져나가기
개발자KR · 원고 갱신
이 장에서 배우는 것
지금까지 만든 프로그램은 위에서 아래로 한 줄씩 실행되고 끝났다. 관측 자료를 다루려면 두 가지 능력이 더 필요하다. 하나는 값에 따라 실행할 문장을 고르는 능력이고, 다른 하나는 같은 계산을 여러 번 되풀이하는 능력이다. 앞 장에서 정수 나눗셈과 실수 오차를 다뤘으니, 이 장에서는 그 값을 비교하고 세는 방법으로 넘어간다. 하루 24시간의 기온과 강수량을 분류하고 집계하는 작은 프로그램으로 이 두 가지를 익힌다.
- if, else if, else 로 기온을 구간별로 분류한다.
- select case 로 정수 값을 낱개·범위·열린 범위로 나눈다.
- do 카운터 루프의 시작값, 끝값, 간격(step)을 정하고 반복 횟수를 미리 계산한다.
- do while, exit, cycle 로 조건에 따라 반복을 이어가거나 건너뛰거나 끝낸다.
- 이름 붙은 루프로 이중 루프를 한 번에 빠져나간다.
문제 상황
작은 관측소에서 사흘 치 시간별 자료가 쌓였다. 담당자가 확인하려는 것은 대략 다음과 같다.
- 하루 중 추운 시간, 서늘한 시간, 온화한 시간, 더운 시간이 각각 몇 시간인가.
- 비는 시간당 몇 mm 부터 강하다고 볼 것이며, 날마다 약한 비·보통 비·강한 비 시간이 몇 시간씩인가.
- 누적 강수량이 20 mm 에 처음 도달한 시각은 언제인가.
- 사흘을 통틀어 처음으로 강한 비가 내린 날과 시각은 언제인가. 찾았으면 더 볼 필요가 없다.
손으로 하면 표를 눈으로 훑으며 줄을 긋는 일이다. 컴퓨터에서는 "시간을 하나씩 돌며, 값을 보고, 갈래를 고른다"는 구조로 옮긴다. 이 구조가 조건문과 반복문이다. 아직 배열을 배우지 않았으므로 자료는 파일에서 읽지 않고 간단한 식으로 만든다. 기온은 14시에 가장 높고 양쪽으로 시간당 1도씩 내려가는 모양이며, 비는 16시를 중심으로 7시간 동안 내리고 날짜가 뒤일수록 세다.
조건 분기: if 와 select case
비교와 논리 연산
분기는 참 또는 거짓이 되는 식(논리식)이 있어야 한다. 비교에는 <, <=, >, >=, ==(같다), /=(다르다)를 쓴다. 여러 조건은 .and.(그리고), .or.(또는), .not.(아니다)로 묶는다. 점으로 감싼 이 모양이 Fortran 의 논리 연산자 표기다.
if, else if, else
가장 기본 형태는 다음과 같다. 조건 뒤에 then 을 붙이고 end if 로 닫는다.
if (temp < 10.0) then
n_cold = n_cold + 1
else if (temp < 16.0) then
n_cool = n_cool + 1
else
n_warm = n_warm + 1
end if
조건은 위에서 아래로 차례대로 검사하고, 처음으로 참이 되는 갈래 하나만 실행한 뒤 end if 다음으로 간다. 두 번째 조건 temp < 16.0 에는 "10도 이상"이라는 말이 적혀 있지 않지만, 첫 조건이 이미 걸러냈으므로 10도 이상인 값만 여기까지 온다. 그래서 구간 경계는 위에서부터 한 번씩만 쓰면 된다. 조건 순서를 거꾸로 두면 앞의 조건이 뒤의 값을 모두 삼켜 버린다.
조건이 하나이고 실행할 문장도 하나라면 한 줄 형태 if (조건) 문장 을 쓸 수 있다. 이 경우에는 then 과 end if 가 없다. 뒤에서 if (precip == 0) cycle 로 쓴다.
select case
한 값을 여러 후보와 견주는 경우에는 select case 가 읽기 쉽다. 이 장의 강수량 분류가 그런 경우다.
select case (precip)
case (0)
n_none = n_none + 1
case (1:2)
n_light = n_light + 1
case (3:9)
n_mod = n_mod + 1
case (10:)
n_heavy = n_heavy + 1
end select
case (1:2) 는 1 이상 2 이하를 뜻한다. 양 끝이 모두 포함된다. 한쪽을 비우면 열린 범위가 되어, (10:) 은 10 이상 전부이고 (:0) 은 0 이하 전부다. 쉼표로 낱개를 나열할 수도 있다(case (1, 3, 5)). 어느 case 에도 맞지 않는 값은 case default 가 받는다. case 범위끼리는 겹치면 안 되고, 겹치면 컴파일러가 오류로 알려 준다. if 체인과 달리 "먼저 맞는 것"을 고르는 방식이 아니라 서로 겹치지 않는 후보 중 하나를 고르는 방식이다.
select case 의 대상은 정수, 문자, 논리 값이어야 한다. 실수는 쓸 수 없다. 앞 장에서 본 것처럼 실수는 오차가 있어서 "정확히 같은 값"이라는 비교가 어울리지 않기 때문이다. 기온은 실수라서 if 체인으로, 강수량은 mm 단위 정수라서 select case 로 처리한다.
| 도구 | 판단 대상 | 어울리는 경우 | 제약 |
|---|---|---|---|
| if / else if | 모든 논리식 | 실수 비교, 서로 다른 변수를 섞은 조건 | 조건 순서가 결과에 영향을 준다 |
| select case | 정수·문자·논리 한 값 | 한 값을 낱개·범위로 나누는 분류 | 실수는 쓸 수 없고 범위가 겹치면 안 된다 |
반복: do 루프의 여러 모양
카운터 루프와 간격
횟수를 알 때는 카운터 루프(counter loop)를 쓴다. do hour = 0, 23 은 변수 hour 를 0부터 23까지 1씩 늘리며 본문을 24번 실행한다. 세 번째 값을 주면 간격이 된다. do hour = 0, 23, 6 은 0, 6, 12, 18 이고, 음수 간격 do hour = 18, 0, -6 은 18, 12, 6, 0 으로 내려간다. 반복 횟수는 다음 식으로 정해지며 0 보다 작으면 한 번도 돌지 않는다.
횟수 = max( (끝값 - 시작값 + 간격) / 간격 , 0 ) (정수 나눗셈)
시작값 0, 끝값 23, 간격 6 이면 (23 + 6) / 6 = 4 로 네 번이다. 끝값 23 은 이 값에 도달해야 포함되는 상한일 뿐이어서 마지막 반복의 값은 18 이다. 그리고 루프가 끝난 뒤 카운터 변수에는 "다음에 쓰였을 값"이 남는다. do hour = 0, 23 이 끝나면 hour 는 24 다. 이 값에 기대는 코드는 읽기 어려우니, 필요한 값은 루프 안에서 따로 저장해 두는 편이 낫다.
do while
횟수를 모르고 조건이 참인 동안만 돌고 싶을 때는 do while 을 쓴다. 조건은 매 반복 앞에서 검사하므로 처음부터 거짓이면 본문은 한 번도 실행되지 않는다. 카운터 루프와 달리 변수를 올려 주는 일을 직접 해야 한다.
cum = 0
hour = 0
do while (cum < 20 .and. hour <= 23)
cum = cum + max(0, 4 - abs(hour - 16)) * 3
hour = hour + 1
end do
조건에 hour <= 23 을 함께 둔 이유는 20 mm 에 끝내 닿지 않는 자료가 와도 루프가 멈추도록 하는 안전장치다.
exit 와 cycle
두 문장은 본문 중간에서 흐름을 바꾼다. cycle 은 이번 반복의 남은 부분을 건너뛰고 다음 반복으로 간다. exit 는 반복 자체를 끝내고 end do 다음으로 간다. "비가 안 온 시간은 건너뛰고(cycle), 8 mm 를 넘으면 그만 본다(exit)"처럼 예외 처리를 앞에서 해치우면 본문이 깊게 들여쓰이지 않아 읽기 쉽다.
| 형태 | 변수 값 | 횟수 | 용도 |
|---|---|---|---|
| do h = 0, 23 | 0, 1, ..., 23 | 24 | 하루를 한 시간씩 |
| do h = 0, 23, 6 | 0, 6, 12, 18 | 4 | 간격을 두고 표본 추출 |
| do h = 18, 0, -6 | 18, 12, 6, 0 | 4 | 거꾸로 세기 |
| do while (조건) | 직접 갱신 | 미리 모름 | 조건이 깨질 때까지 |
이름 붙은 루프로 바깥까지 나가기
루프 안에 루프가 있을 때 exit 는 자신이 속한 가장 안쪽 루프만 끝낸다. 사흘 치 자료에서 처음 나온 강한 비를 찾는다고 하자. 안쪽(시간) 루프에서 찾은 뒤 exit 만 쓰면 그 날의 시간 루프만 끝나고 바깥(날짜) 루프는 다음 날로 넘어가 버린다. 이때 루프에 이름을 붙인다(named loop).
search: do day = 1, 3
do hour = 0, 23
...
if (precip >= 10) exit search
end do
end do search
이름은 이름: do 로 달고, 닫을 때도 end do 이름 으로 같은 이름을 쓴다. exit search 는 안쪽이 아니라 이름이 가리키는 루프를 끝낸다. cycle 이름 도 같은 방식으로 바깥 루프의 다음 반복으로 간다. 이름은 이중 이상의 루프에서 어느 루프를 가리키는지 분명하게 해 주므로, 안쪽 exit 만으로 충분한 경우에도 붙여 두면 읽는 사람에게 친절하다.
완성 코드
아래 프로그램은 위의 모든 요소를 한 파일에 담았다. 파일 이름은 main.f90 이다. 같은 강수량 식을 여러 곳에 적은 이유는 아직 식을 이름 붙여 재사용하는 방법을 다루지 않았기 때문이다. 이 반복은 뒤에서 함수로 정리한다.
main.f90
program control_flow
implicit none
character(len=*), parameter :: row_fmt = '(i0, 5(a, i0), a)'
integer :: day, hour, precip, cum, day_sum
integer :: n_cold, n_cool, n_mild, n_warm
integer :: n_none, n_light, n_mod, n_heavy
integer :: checked, found_day, found_hour
real :: temp, temp_sum
n_cold = 0
n_cool = 0
n_mild = 0
n_warm = 0
temp_sum = 0.0
do hour = 0, 23
temp = 20.0 - real(abs(hour - 14))
temp_sum = temp_sum + temp
if (temp < 10.0) then
n_cold = n_cold + 1
else if (temp < 16.0) then
n_cool = n_cool + 1
else if (temp < 20.0) then
n_mild = n_mild + 1
else
n_warm = n_warm + 1
end if
end do
print '(a, i0, a, i0, a, i0, a, i0, a)', '추움 ', n_cold, '시간, 서늘 ', n_cool, &
'시간, 온화 ', n_mild, '시간, 더움 ', n_warm, '시간'
print '(a, f5.2, a)', '1일차 평균 기온: ', temp_sum / 24.0, ' 도'
print '(a)', '6시간 간격 기온:'
do hour = 0, 23, 6
temp = 20.0 - real(abs(hour - 14))
print '(i2, a, f5.1, a)', hour, '시 ', temp, ' 도'
end do
do day = 1, 3
n_none = 0
n_light = 0
n_mod = 0
n_heavy = 0
day_sum = 0
do hour = 0, 23
precip = max(0, 4 - abs(hour - 16)) * day
day_sum = day_sum + precip
select case (precip)
case (0)
n_none = n_none + 1
case (1:2)
n_light = n_light + 1
case (3:9)
n_mod = n_mod + 1
case (10:)
n_heavy = n_heavy + 1
end select
end do
print row_fmt, day, '일차: 없음 ', n_none, ', 약함 ', n_light, ', 보통 ', n_mod, &
', 강함 ', n_heavy, ', 합계 ', day_sum, ' mm'
end do
cum = 0
hour = 0
do while (cum < 20 .and. hour <= 23)
cum = cum + max(0, 4 - abs(hour - 16)) * 3
hour = hour + 1
end do
print '(a, i0, a, i0, a)', '3일차 누적 20 mm 도달: ', hour - 1, '시 (누적 ', cum, ' mm)'
print '(a)', '2일차 비가 온 시간 (8 mm 이상이면 중단):'
day = 2
do hour = 0, 23
precip = max(0, 4 - abs(hour - 16)) * day
if (precip == 0) cycle
print '(i2, a, i2, a)', hour, '시 ', precip, ' mm'
if (precip >= 8) exit
end do
checked = 0
found_day = 0
found_hour = -1
search: do day = 1, 3
do hour = 0, 23
checked = checked + 1
precip = max(0, 4 - abs(hour - 16)) * day
if (precip >= 10) then
found_day = day
found_hour = hour
exit search
end if
end do
end do search
print '(a, i0, a, i0, a, i0)', '첫 강한 비(10 mm 이상): ', found_day, '일차 ', &
found_hour, '시, 검사한 시간 수 ', checked
end program control_flow
줄별 해설
row_fmt는 이름 붙은 상수(parameter)로 둔 서식 문자열이다. 서식i0는 정수를 빈칸 없이 필요한 자릿수만큼 쓴다.5(a, i0)는 "문자열 하나, 정수 하나" 묶음을 다섯 번 되풀이하라는 뜻이다. 긴 서식을 한 번만 적으려고 따로 뺐다.- 선언부의
n_cold등 개수 변수는 정수,temp와temp_sum은 실수다. 개수는 정수로 세고 합은 실수로 모은다. - 첫
do hour = 0, 23블록에서 기온은20.0 - real(abs(hour - 14))다. 정수abs(hour - 14)를real로 실수로 바꿔 20.0 에서 뺀다. 14시에 20.0, 0시에 6.0 이 나온다. - if 체인의 경계는 10.0, 16.0, 20.0 이다. 마지막
else는 20.0 이상을 받는다. 개수 변수는 반복 전에 0 으로 초기화했다. 초기화하지 않으면 값이 정해지지 않은 변수를 읽게 된다. - 평균은 합 330.0 을 24.0 으로 나눈 13.75 다. 서식
f5.2는 전체 5칸, 소수 둘째 자리까지다. do hour = 0, 23, 6은 0, 6, 12, 18 시만 처리한다. 서식i2는 두 칸이므로 한 자리 숫자 앞에 공백이 하나 붙는다.- 세 번째 블록은 이중 루프다. 바깥이 날짜, 안쪽이 시간이다.
max(0, 4 - abs(hour - 16)) * day는 16시에 4 mm, 거기서 한 시간 멀어질 때마다 1 mm 씩 줄어 0 에서 멈추는 값에 날짜를 곱한 것이다. 날마다 개수 변수를 0 으로 되돌리는 위치가 안쪽 루프 앞이라는 점을 확인한다. select case (precip)는 정수 하나로 갈래를 고른다.(0),(1:2),(3:9),(10:)은 서로 겹치지 않고 0 이상의 모든 정수를 덮는다.- do while 블록에서는 시간이 지날 때마다 3일차 강수량을 더한다. 누적이 20 이상이 되면 조건이 거짓이 되어 멈춘다. 멈춘 시점에
hour는 이미 한 칸 더 올라가 있으므로 출력에서hour - 1을 쓴다. if (precip == 0) cycle로 비가 오지 않은 시간을 건너뛰고,if (precip >= 8) exit로 8 mm 이상이 나오면 출력한 뒤 반복을 끝낸다. 출력한 다음에 검사하므로 8 mm 인 16시는 목록에 포함된다.search: do day = 1, 3이후가 이름 붙은 루프다.checked는 검사한 시간 수를 세고, 찾으면 날짜와 시각을 저장한 뒤exit search로 두 루프를 한 번에 벗어난다.found_hour의 처음 값 -1 은 "못 찾음"을 나타내는 표시로 쓰려고 둔 값이다.- 줄 끝의
&는 문장이 다음 줄에 이어진다는 표시다. 문자열 안에서 끊지 않고 쉼표 뒤에서 끊었다.
실행 결과
$ gfortran -std=f2018 -Wall main.f90 -o control_flow
$ ./control_flow
추움 4시간, 서늘 11시간, 온화 8시간, 더움 1시간
1일차 평균 기온: 13.75 도
6시간 간격 기온:
0시 6.0 도
6시 12.0 도
12시 18.0 도
18시 16.0 도
1일차: 없음 17, 약함 4, 보통 3, 강함 0, 합계 16 mm
2일차: 없음 17, 약함 2, 보통 5, 강함 0, 합계 32 mm
3일차: 없음 17, 약함 0, 보통 6, 강함 1, 합계 48 mm
3일차 누적 20 mm 도달: 16시 (누적 30 mm)
2일차 비가 온 시간 (8 mm 이상이면 중단):
13시 2 mm
14시 4 mm
15시 6 mm
16시 8 mm
첫 강한 비(10 mm 이상): 3일차 16시, 검사한 시간 수 65
숫자가 맞는지 직접 확인해 볼 수 있다. 기온 분류의 4, 11, 8, 1 을 더하면 24 시간이다. 날마다 없음·약함·보통·강함의 합도 24 시간이다(17 + 4 + 3 + 0 처럼). 검사한 시간 수 65 는 24 + 24 + 17 이다. 3일차 0시부터 16시까지 17칸을 보고 멈췄다는 뜻이다. 실행 환경에 대한 자세한 옵션은 GNU Fortran 문서에서 확인할 수 있다.
실무에서 자주 틀리는 것
1. select case 에 실수를 넣는다
기온 같은 실수를 그대로 select case 에 넣으면 컴파일 단계에서 거부된다.
select case (temp)
case (:9.9)
n_cold = n_cold + 1
end select
범위를 나누는 경계가 소수라면 if 체인을 쓴다. 정수 칸으로 충분하다면 반올림한 정수를 넣는다.
select case (nint(temp))
case (:9)
n_cold = n_cold + 1
case (10:15)
n_cool = n_cool + 1
case default
n_warm = n_warm + 1
end select
nint 는 가장 가까운 정수로 반올림한다. 이 경우 9.6 은 10 이 되어 경계가 처음 의도와 달라질 수 있으니 반올림이 괜찮은지 먼저 따져 본다.
2. do while 안에서 조건 변수를 갱신하지 않는다
카운터 루프는 변수를 자동으로 올리지만 do while 은 그렇지 않다. 다음은 끝나지 않는다.
hour = 0
do while (hour <= 23)
cum = cum + max(0, 4 - abs(hour - 16))
end do
hour 가 변하지 않아 조건이 영원히 참으로 남는다. 터미널에서는 Ctrl+C 로 멈춘다. 갱신 문장을 본문에 넣는다.
hour = 0
do while (hour <= 23)
cum = cum + max(0, 4 - abs(hour - 16))
hour = hour + 1
end do
3. 안쪽 루프의 exit 로 전체가 끝났다고 믿는다
날짜가 5일이라고 하고 다음 코드를 보자.
do day = 1, 5
do hour = 0, 23
precip = max(0, 4 - abs(hour - 16)) * day
if (precip >= 10) exit
end do
end do
3일차에서 찾아도 exit 는 시간 루프만 끝낸다. 4일차와 5일차를 계속 검사하고, 찾은 값을 따로 저장하지 않았다면 결과는 마지막 날의 상태가 된다. 루프에 이름을 붙이고 이름으로 나간다.
search: do day = 1, 5
do hour = 0, 23
precip = max(0, 4 - abs(hour - 16)) * day
if (precip >= 10) exit search
end do
end do search
4. 카운터 변수를 루프 안에서 바꾼다
"비가 안 오는 시간은 한 칸 더 건너뛰자"는 생각에 카운터를 직접 올리면 컴파일러가 오류로 막는다.
do hour = 0, 23
if (precip == 0) hour = hour + 1
end do
카운터 루프의 변수는 루프가 관리하므로 본문에서 값을 대입할 수 없다. 건너뛰는 폭을 직접 정하고 싶다면 do while 로 바꾸거나, 간격을 인수로 주는 형태 do hour = 0, 23, 2 를 쓴다.
한눈에 보기
| 문법 | 하는 일 | 기억할 점 |
|---|---|---|
| if / else if / else / end if | 처음 참인 갈래 하나를 실행한다 | 조건은 위에서 아래로 검사하며 순서가 결과를 바꾼다 |
| if (조건) 문장 | 한 문장만 조건부로 실행한다 | then 과 end if 를 쓰지 않는다 |
| select case / case (a:b) | 정수·문자 값을 낱개·범위로 나눈다 | 양 끝 포함, (10:) 은 10 이상, 실수는 불가 |
| do i = s, e, k | 횟수가 정해진 반복 | 끝난 뒤 변수는 한 칸 넘어간 값, 본문에서 대입 불가 |
| do while (조건) | 조건이 참인 동안 반복 | 조건 변수를 직접 갱신한다 |
| cycle / exit | 이번 반복을 건너뛴다 / 반복을 끝낸다 | 가장 안쪽 루프에만 적용된다 |
| 이름: do ... end do 이름 | exit·cycle 의 대상을 지정한다 | 이름은 열 때와 닫을 때 모두 쓴다 |
연습 문제
do hour = 20, 2, -6은 몇 번 돌며hour는 어떤 값을 차례로 가지는가. 루프가 끝난 뒤hour의 값은 얼마인가.- 시각
hour(0 이상 23 이하 정수)를 새벽(0~5), 오전(6~11), 오후(12~17), 밤(18~23)으로 나눠 이름을 출력하는 select case 를 써라. 범위 밖의 값도 처리하라. - 본문의 do while 을 2일차 자료로 바꿔서, 누적 강수량이 20 mm 이상이 되는 첫 시각과 그때의 누적값을 구하는 코드를 써라. 결과는 얼마인가.
- 이름 붙은 루프를 사용해 1일차부터 3일차 중 하루 합계가 40 mm 이상인 첫 날을 찾는 코드를 써라. 하루 합계는 본문의 강수량 식으로 구한다.
정답과 해설
1번. 횟수는 (2 - 20 + (-6)) / (-6) = (-24) / (-6) = 4 이므로 네 번이다. 값은 20, 14, 8, 2 다. 다음 값 -4 는 끝값 2 보다 작아서 도달하지 못하고, 루프가 끝난 뒤 hour 는 -4 다.
2번.
select case (hour)
case (0:5)
print '(a)', '새벽'
case (6:11)
print '(a)', '오전'
case (12:17)
print '(a)', '오후'
case (18:23)
print '(a)', '밤'
case default
print '(a)', '범위 밖'
end select
범위는 양 끝을 포함하므로 5 와 6 사이에 빈틈이 없다. case default 가 -1 이나 24 같은 값을 받는다.
3번.
cum = 0
hour = 0
do while (cum < 20 .and. hour <= 23)
cum = cum + max(0, 4 - abs(hour - 16)) * 2
hour = hour + 1
end do
print '(a, i0, a, i0, a)', '16시 확인: ', hour - 1, '시, 누적 ', cum, ' mm'
2일차의 시간별 값은 13시 2, 14시 4, 15시 6, 16시 8 이다. 누적은 2, 6, 12, 20 이 되고, 16시에 20 이 되어 조건 cum < 20 이 거짓이 된다. 결과는 16시, 누적 20 mm 이다. 본문의 3일차(누적 30)와 달리 정확히 20 이 되므로 경계를 포함하는지 비교 연산자로 확인하는 연습이 된다.
4번.
days: do day = 1, 3
day_sum = 0
do hour = 0, 23
day_sum = day_sum + max(0, 4 - abs(hour - 16)) * day
end do
if (day_sum >= 40) then
print '(a, i0, a, i0, a)', '40 mm 이상인 첫 날: ', day, '일차 (', day_sum, ' mm)'
exit days
end if
end do days
날마다 합계는 16, 32, 48 이므로 3일차에서 조건이 처음 참이 된다. 이 코드는 바깥 루프에서 exit 하므로 이름이 꼭 필요하지는 않지만, 안쪽 루프 뒤에 있는 if 라서 어느 루프를 끝내는지 이름이 분명하게 보여 준다. 합계를 내는 안쪽 루프를 앞에 두고 판단을 바깥 루프에서 한다는 순서도 눈여겨본다. 판단을 안쪽 루프에 넣으면 하루 합계가 완성되기 전에 검사하게 된다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.