[!attention] 이 장의 목표
- 의도 명시 — intent(in/out/inout)로 인수의 쓰임을 배운다.
- 유연한 호출 — 선택적 인수와 키워드 인수로 호출을 활용한다.
- 배열 전달 — 배열 인수를 안전하게 주고받는다.
- 인터페이스 이해 — 인터페이스 블록과 명시적 인터페이스의 역할을 안다.
10장에서 함수와 서브루틴의 기본적인 정의와 호출 방법을 익혔다. 프로시저가 실제 프로그램에서 제 역할을 수행하려면, 호출하는 측과 호출되는 프로시저 사이에서 데이터 전달을 제어할 수 있어야 한다. 본 장에서는 인수 전달의 세부 규칙과 이를 컴파일러가 검증하도록 만드는 장치인 인터페이스를 다룬다. 핵심 개념은 다음과 같이 요약된다.
intent 속성: 인수의 입력 및 출력 방향을 지정하여 코드의 안정성을 높인다.
선택적 인수와 키워드 인수: 호출 시 인수를 생략하거나 순서를 임의로 재배열하도록 한다.
배열 인수 전달: 다차원 데이터를 메모리 낭비 없이 주고받는 규칙을 다룬다.
명시적 인터페이스: 인수 전달 기능들이 오작동하지 않도록 유효성을 검증한다.
[!important] 11.1 인수의 방향: intent(in/out/inout)
프로시저로 들어온 가인수가 읽기 전용인지, 결과를 내보내는 통로인지, 아니면 들어온 값을 갱신하여 돌려보내는지를 명시하는 속성이 intent이다. 이 속성은 세 가지 값을 가진다.
intent(in): 프로시저가 이 인수를 읽기만 한다. 프로시저 내부에서 값을 바꾸려 하면 컴파일 오류가 발생한다.
intent(out): 프로시저가 이 인수에 결과를 써서 내보낸다. 프로시저로 들어올 때의 기존 값은 정의되지 않은 상태로 취급되므로, 내부에서 값을 읽기 전에 반드시 새로운 값을 먼저 대입해야 한다.
intent(inout): 들어온 값을 읽고 다시 갱신하여 돌려보낸다. 누산기처럼 이전 값에 새로운 계산 결과를 더하는 경우 등에 사용한다.
intent를 지정하는 이유는 컴파일러가 프로그래머의 의도와 다르게 코드가 작성된 부분을 정확히 잡아내어 버그를 미리 막아 주고, 컴파일 최적화에 필요한 정보를 제공하기 때문이다. 본 교재의 모든 가인수에는 intent 속성을 명시하여 선언하도록 한다.
프로시저의 인수(dummy-arg)는 intent 속성을 사용하여 데이터의 흐름과 보호 수준을 명시해야 한다. 기본적인 선언 형식은 다음과 같다.
⟨type⟩, intent(⟨spec⟩) :: ⟨dummy-arg⟩
자료형(type)은 인수의 데이터 타입을 지정하는 항목으로, integer, real(real64), type(node) 등이 이에 해당한다.
데이터 흐름 방향(intent(spec))은 인수가 사용되는 방식을 지정하며, spec 자리에는 세 가지 키워드가 들어갈 수 있다.
in을 기술하면 호출 측에서 값을 전달하기만 하며 프로시저 내에서 수정할 수 없는 읽기 전용이 된다. out을 기술하면 프로시저 내부에서 값을 새로 정의하여 호출 측으로 전달하며, 기존에 가지고 있던 값은 사용할 수 없게 된다. inout을 기술하면 호출 측의 값을 읽어 계산에 활용하고, 프로시저 내에서 이를 수정하여 다시 전달하는 읽기 및 쓰기 모드로 동작한다.인수 이름(dummy-arg)은 프로시저 내부에서 변수처럼 사용할 인수의 명칭을 의미한다.
여러 속성을 조합하여 다음과 같이 선언할 수 있다.
! 실수형 변수를 읽기 전용으로 전달
real(real64), intent(in) :: x
! 정수형 변수를 입출력용으로 전달
integer, intent(inout) :: count
! 파생형 포인터를 입출력용으로 전달
type(node), pointer, intent(inout) :: head
이런 문법 규칙은 프로시저 간의 원치 않는 데이터 간섭을 방지하고, 프로그램 코드의 구조적 안정성을 높이는 역할을 한다.
원의 반지름을 입력받아 넓이와 둘레를 동시에 계산하는 서브루틴을 구현한 예제이다. 입력으로 사용되는 반지름은 intent(in) 속성을 지정하여 안전하게 보호하고, 계산된 두 가지 결과물은 각각 intent(out) 속성을 지정하여 호출 측으로 전달한다.
%%writefile circle.f90
program circle_props
use iso_fortran_env, only: real64
implicit none
real(real64), parameter :: pi = 3.141592653589793_real64
real(real64) :: r, area, perimeter
r = 2.5_real64
call circle(r, area, perimeter)
print '(a, f8.4)', "radius = ", r
print '(a, f8.4)', "area = ", area
print '(a, f8.4)', "perimeter = ", perimeter
contains
subroutine circle(radius, a, p)
real(real64), intent(in) :: radius
real(real64), intent(out) :: a, p
a = pi * radius**2
p = 2.0_real64 * pi * radius
end subroutine circle
end program circle_props
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall circle.f90 -o circle
!./circle
실행 결과:
radius = 2.5000
area = 19.6350
perimeter = 15.7080
가인수 radius에 intent(in) 속성이 부여되어 있으므로, 서브루틴 내부에서 프로그래머의 실수로 반지름 값을 변경하려는 코드를 작성하더라도 컴파일 단계에서 차단된다. 이를 통해 호출하는는 서브루틴이 자신이 넘겨준 원본 변수 r의 값을 절대 변경하지 못하게 보호한다.
만약 intent(in)으로 선언된 가인수 radius에 대해 서브루틴 내부에서 radius = radius + 1.0_real64와 같이 값을 수정하려 시도하면, 컴파일 단계에서 다음 에러 메시지를 출력한다.
Error: Dummy argument 'radius' with INTENT(IN) in variable definition
context (assignment) at (1)
이처럼 intent 속성은 의도하지 않은 잘못된 변수 대입 연산을 컴파일 단계에서 확실하게 걸러내는 안전장치 역할을 한다.
[!warning] [흔한 실수] 실인수와 가인수의 종류(kind) 불일치 오류 호출할 때 넘기는 실인수와 프로시저에서 받는 가인수의 종류(
kind)가 다르면 치명적인 오류가 발생할 수 있다. 예를 들어, 단정밀도(4바이트) 실수를 배정밀도(real(real64), 8바이트) 가인수에 그대로 넘기는 경우이다.따라서 프로그램 전체의 데이터 정밀도는
iso_fortran_env모듈의real64로 명확하게 통일하여 사용하는 것이 안전하다. 소스 코드 내에서 사용하는 실인수 형태의 상수 역시2.0_real64와 같이 가인수의 정의와 완벽히 일치하는 종류의 리터럴 접미사를 붙여 작성해야 한다.
intent(inout)을 활용한 데이터 누적호출 측에서 전달한 기존 변수 값에 새로운 계산 결과를 누적하여 갱신하는 예제로서, 1부터 5까지의 정수를 각각 제곱한 뒤 그 합산 결과를 차례대로 누산하는 과정을 보여준다. 서브루틴 내부에서 가인수 acc는 매번 이전까지 계산된 합산 값을 읽어 들인 후 새로운 값을 갱신하므로 intent(inout) 속성을 지정해야 한다.
%%writefile accumulate_demo.f90
program accumulate_demo
use iso_fortran_env, only: real64
implicit none
real(real64) :: total
integer :: i
total = 0.0_real64
do i = 1, 5
call add_square(total, real(i, real64))
end do
print '(a, f8.1)', "sum of squares 1..5 = ", total
contains
subroutine add_square(acc, x)
real(real64), intent(inout) :: acc
real(real64), intent(in) :: x
acc = acc + x**2
end subroutine add_square
end program accumulate_demo
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall accumulate_demo.f90 -o accumulate_demo
!./accumulate_demo
실행 결과:
sum of squares 1..5 = 55.0
이 예제의 핵심은 가인수 acc에 intent(out)가 아닌 intent(inout) 속성을 사용했다는 점이다. 만약 intent(out)을 쓰면, 서브루틴이 시작되자마자 기존에 total이 가지고 있던 원래 값을 컴퓨터가 깨끗이 지워버린다. 빈 공간으로 만들고 시작하는 셈이다.
그렇게 되면 이전에 더해 두었던 누적 값이 모두 사라지기 때문에, 원래 값에 새로운 값을 계속 더해나가는 누적 계산이 불가능해진다. 반면 intent(inout)를 쓰면 원래 들어있던 값을 그대로 가져와서 거기에 계속 더해나갈 수 있다.
[!warning] [흔한 실수] intent(out)으로 누적을 시도하면 값이 사라진다
intent(out)으로 선언한 상태에서acc = acc + x2와 같은 연산을 수행하면, 더하려는 기준인acc에 어떤 값이 들어있는지 알 수 없으므로(쓰레기 값이 들어있을 수 있음) 그 계산 결과를 전혀 신뢰할 수 없다. 누적 계산처럼 이전의 값을 그대로 이어받아 그 위에 새로운 값을 더해가며 갱신해야 할 때는, 반드시 기존 값을 유지해 주는intent(inout)속성을 사용해야 한다.
[!important] 11.2 선택적 인수와 키워드 인수
지금까지는 프로시저를 호출할 때 가인수의 순서에 맞춰 실인수를 빠짐없이 나열했다. Fortran은 여기에 두 가지 유연한 기능이 추가되어 있다.
키워드 인수(keyword argument): 이름=값 형태로 실인수를 건네면, 정의된 순서와 상관없이 인수를 자유롭게 배치할 수 있다. 인수가 많을 때 호출문의 가독성을 높이고 의미를 분명하게 해 준다.
선택적 인수(optional argument): optional 속성을 붙인 가인수는 호출할 때 생략할 수 있다. 다만 프로시저 안에서는 내장 함수인 present로 그 인수가 실제로 전달되었는지 확인한 뒤에만 사용해야 한다.
이 두 기능을 사용하려면 호출하는 시점에 프로시저의 명시적 인터페이스가 반드시 확보되어 있어야 한다. 내부 프로시저와 모듈 프로시저는 이 조건을 자동으로 만족하므로, contains 문 내부에 둔 프로시저라면 별도의 작업 없이 바로 사용할 수 있다.
선택적 인수를 사용하면 호출부에서 필요에 따라 인수를 생략할 수 있어 프로시저의 활용성이 크게 증대된다. 구현 및 호출 규칙은 다음과 같이 정리할 수 있다.
인수를 선언할 때 다른 속성과 함께 optional 속성을 추가로 지정한다.
⟨type⟩, intent(in), optional :: ⟨dummy-arg⟩
호출부에서 인수를 생략하거나 원래 정의된 순서를 바꾸어 전달할 때는 가인수의 이름을 명시하는 키워드 인수 방식을 사용한다.
call ⟨name⟩(⟨keyword⟩=⟨value⟩ [, ⟨keyword⟩=⟨value⟩]...)
프로시저 내부에서 선택적 인수가 실제로 전달되었는지 안전하게 검사하기 위해 present 함수를 사용한다. 이 함수는 인수가 전달되었으면 참(.true.), 생략되었으면 거짓(.false.)의 논리형 값을 반환한다.
if (present(⟨dummy-arg⟩)) then
! 인수가 전달되었을 때의 동작
end if
설계의 유연성 확보: optional 속성을 활용하면, 기본값 외에 복잡한 추가 설정이 필요한 경우에만 선택적으로 인수를 넘겨받는 방식의 유연한 구조를 설계할 수 있다.
코드의 가독성 향상: 키워드 인수를 사용하면 각각의 값이 어떤 의미를 가지는지 직관적으로 파악할 수 있다.
안전한 참조 필수: 선택적 인수를 다룰 때는 반드시 present 함수로 인수가 존재하는지 먼저 확인한 뒤에 변수를 참조해야 한다. 전달되지 않은(존재하지 않는) 인수를 무작정 참조하면 프로그램이 즉시 비정상 종료되거나 예측 불가능한 메모리 오류가 발생한다.
이런 규칙은 매개변수가 많고 복잡한 프로시저를 안전하고 직관적으로 설계할 때 매우 유용하게 활용된다.
가우스 함수 $e^{-0.5 \left(\frac{x - \mu}{\sigma}\right)^2}$를 계산하되, 중심 $\mu$와 폭 $\sigma$를 선택적으로 지정하도록 구현한 예제이다. 인수를 생략하면 각각 0과 1을 기본값으로 사용한다.
%%write gaussian.f90
program gaussian_demo
use iso_fortran_env, only: real64
implicit none
real(real64) :: y1, y2, y3
y1 = gaussian(1.0_real64)
y2 = gaussian(1.0_real64, mu=0.5_real64)
y3 = gaussian(1.0_real64, mu=0.5_real64, sigma=2.0_real64)
print '(a, f10.6)', "default mu=0, sigma=1 : ", y1
print '(a, f10.6)', "mu=0.5 : ", y2
print '(a, f10.6)', "mu=0.5, sigma=2.0 : ", y3
contains
function gaussian(x, mu, sigma) result(y)
real(real64), intent(in) :: x
real(real64), intent(in), optional :: mu, sigma
real(real64) :: y
real(real64) :: center, width
center = 0.0_real64
width = 1.0_real64
if (present(mu)) center = mu
if (present(sigma)) width = sigma
y = exp(-0.5_real64 * ((x - center) / width)**2)
end function gaussian
end program gaussian_demo
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall gaussian_demo.f90 -o gaussian_demo
!./gaussian_demo
실행 결과:
default mu=0, sigma=1 : 0.606531
mu=0.5 : 0.882497
mu=0.5, sigma=2.0 : 0.969233
주 프로그램에서의 세 가지 호출 방식에 주목한다. 첫 번째 호출은 선택적 인수를 모두 생략하여 기본값으로 계산했고, 두 번째 호출은 mu만 지정했으며, 세 번째 호출은 mu와 sigma를 모두 키워드 인수로 지정했다.
gaussian 함수 내부에서는 present 함수로 각 인수의 전달 여부를 확인한 뒤, 전달된 값 또는 기본값을 적절히 분기하여 연산에 활용한다. 선택적 인수를 present 확인 없이 곧장 연산에 사용하면 심각한 오류가 발생할 수 있다.
[!warning] [흔한 실수] 선택적 인수를
present확인 없이 사용하는 경우optional속성이 붙은 인수는 호출할 때 언제든지 생략될 수 있다. 만약 호출 측에서 생략한 인수를 함수 내부에서present로 검사하지 않고 곧바로 읽거나 연산에 대입하면 Fortran 표준 위반으로, 컴파일 단계에서 발견되지 않더라도, 프로그램 실행 중에 실제 존재하지 않는 메모리 주소에 접근하게 되므로 세그멘테이션 오류(segmentation fault)로 이어진다. 따라서 선택적 인수를 다룰 때는 반드시if (present(arg))조건문으로 인수의 존재를 안전하게 확인한 후에만 참조하도록 코드를 설계해야 한다.
키워드 인수를 사용하면 가인수 인수를 적는 순서를 마음대로 바꿀 수 있다. 원래 정의된 위치나 순서를 신경 쓰지 않아도 되므로 편리하다. 다음은 거듭제곱 함수를 호출할 때 두 인수의 순서를 서로 다르게 뒤바꿔서 전달해도 똑같은 계산 결과가 나오는 것을 보여주는 예제이다.
%%writefile keyword_order.f90
program keyword_order
use iso_fortran_env, only: real64
implicit none
print '(f12.4)', power(base=2.0_real64, exponent=10.0_real64)
print '(f12.4)', power(exponent=10.0_real64, base=2.0_real64)
contains
function power(base, exponent) result(y)
real(real64), intent(in) :: base, exponent
real(real64) :: y
y = base ** exponent
end function power
end program keyword_order
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall keyword_order.f90 -o keyword_order
!./keyword_order
실행 결과:
1024.0000
1024.0000
두 호출문은 인수를 기술한 순서만 다를 뿐 정확히 같은 값을 계산한다.
키워드 인수를 조합하여 사용할 때는 위치 인수가 키워드 인수보다 항상 먼저 와야 한다. 즉, 호출문 내에서 이름=값 형태의 키워드 인수를 한 번이라도 사용하기 시작하면 그 뒤에 오는 모든 인수는 반드시 키워드 형태를 유지해야 한다.
[!warning] [흔한 실수] 키워드 인수를 위치 인수보다 먼저 작성 함수를 정의할 때 가인수의 순서는 상관없다.
function power(exponent, base)로 순서를 바꾸어 정의해도 문법적으로 옳다. 하지만, 함수를 사용할 때(호출할 때) 는 호출문에서 키워드 인수를 위치 인수보다 앞쪽에 배치하면 안 된다. 예를 들어, 아래 코드는 가능하다. 값만 적는 위치 인수를 앞에 두고,이름=값형태의 키워드 인수를 뒤에 두었기 때문이다.
fortran ! 10.0_real64은 순서대로 base에 들어가고, exponent는 키워드로 명시함 print '(f12.4)', power(10.0_real64, exponent=2.0_real64)하지만, 아래 코드는 컴파일 오류가 발생한다. 일단
base=하고 키워드를 쓰기 시작했다면, 그 뒤에 오는2.0_real64도 반드시exponent=2.0_real64처럼 키워드를 붙여 주어야 한다.
Fortran ! 에러! 키워드 인수를 먼저 써버리면 그 뒤에 위치 인수가 올 수 없음 print '(f12.4)', power(base=10.0_real64, 2.0_real64)즉,위치 인수(값만 적기)와키워드 인수(이름=값)를 한 호출문 안에서 섞어 쓸 때는, 값만 적는 것을 무조건 앞에 몰아서 써야 한다.
[!important] 11.3 배열 인수 전달
프로시저에 배열을 인수로 넘기는 방식은 가인수를 어떻게 선언하느냐에 따라 크게 두 가지로 나뉜다.
명시적 형상 배열(explicit-shape array): 가인수를 선언할 때 배열의 크기를 직접 적어주는 방식이다. 대개 배열의 크기를 나타내는 정수형 인수를 별도로 함께 넘겨받아 사용한다. 이는 과거에 주로 사용하던 방식이며, 인터페이스 종류와 상관없이 항상 동작한다.
가정형상 배열(assumed-shape array): 가인수를 선언할 때 배열의 크기 자리를 v(:)와 같이 콜론으로 비워 두는 방식이다. 배열의 실제 크기와 형태(형상)를 호출 측의 실인수로부터 그대로 물려받는다. 프로시저 내부에서는 size, lbound, ubound 같은 내장 함수를 사용해 배열의 정보를 알아낸다.
가정형상 배열은 코드가 간결해지고 크기를 나타내는 인수를 따로 넘길 필요가 없어 현대 Fortran에서 강력히 권장되는 방식이다. 다만 이 기능을 사용하려면 호출하는 시점에 프로시저의 명시적 인터페이스가 반드시 보여야 한다. 내부 프로시저와 모듈 프로시저에서는 이 조건이 자동으로 보장되므로 안심하고 사용할 수 있다.
선언 형식은 다음과 같다. 배열의 크기를 고정하지 않고 콜론(:)을 사용하여 형상 정보를 호출 측으로부터 전달받는다.
type, intent(spec) :: dummy-arg(:)
자료형(type)은 배열 요소들의 데이터 타입을 지정한다.
데이터 흐름 방향(intent(spec))은 배열의 활용 목적과 흐름을 지정한다.
형상 지정자((:))는 이 배열이 호출 측으로부터 형상 정보를 그대로 전달받는 가정형상 배열임을 의미한다.
가정형상 배열을 사용하면 프로시저 안에서 배열 크기를 임의로 단정할 수 없으므로, 다음 내장 함수들을 사용하여 실행 중에 배열 정보를 확인해야 한다.
size(array) 함수는 배열의 전체 요소 개수를 문장으로 반환한다.
lbound(array) 함수는 배열의 하한 인덱스(시작 번호)를 문장으로 반환한다.
ubound(array) 함수는 배열의 상한 인덱스(끝 번호)를 문장으로 반환한다.
주요 장점과 주의할 점은 아래와 같다.
데이터 안전성 확보: 배열의 크기를 컴파일 시점에 고정하지 않고 실행 시점에 실인수와 연동하므로, 배열 범위를 벗어나 메모리에 잘못 접근하는 오류를 방지하는 데 효과적이다.
인덱스 활용의 유연성: lbound와 ubound를 조합하여 제어문을 구성하면, 호출 측에서 배열 인덱스를 1부터 시작했든 혹은 다른 정수부터 시작했든 상관없이 안전하게 배열의 모든 요소를 순회할 수 있다.
명시적 인터페이스 필수: 가정형상 배열을 사용하는 프로시저를 호출할 때는 반드시 명시적 인터페이스가 제공되어야 한다. 그렇지 않으면 컴파일러가 형상 정보를 전달할 방법을 찾지 못해 오류가 발생한다. 주 프로그램 안에 포함되는 내부 프로시저나 모듈 내부에 선언된 프로시저는 이 인터페이스가 자동으로 제공된다.
호출 측에서 넘겨주는 배열의 크기를 프로시저가 미리 알 필요 없이, 전달된 실제 배열의 크기에 맞춰 통계치를 계산하는 예제이다. 서브루틴은 가인수를 v(:)와 같이 가정형상 배열로 선언하여 받고, 내부에서 내장 함수인 size(v)를 사용하여 전체 원소 수를 동적으로 얻는다.
%%writefile array_stats.f90
program array_stats
use iso_fortran_env, only: real64
implicit none
real(real64) :: sample(5) = [4.0_real64, 8.0_real64, 15.0_real64, &
16.0_real64, 23.0_real64]
real(real64) :: avg, sd
call stats(sample, avg, sd)
print '(a, i0)', "n = ", size(sample)
print '(a, f9.4)', "mean = ", avg
print '(a, f9.4)', "std dev = ", sd
contains
subroutine stats(v, mean, stddev)
real(real64), intent(in) :: v(:)
real(real64), intent(out) :: mean, stddev
integer :: n
n = size(v)
mean = sum(v) / real(n, real64)
stddev = sqrt(sum((v - mean)**2) / real(n, real64))
end subroutine stats
end program array_stats
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall array_stats.f90 -o array_stats
!./array_stats
실행 결과:
n = 5
mean = 13.2000
std dev = 6.6151
이 구조에서 stats 서브루틴은 자신에게 전달되는 배열의 길이가 5인지 혹은 500인지 상관하지 않는다. 내장 함수 size(v)가 호출되는 시점의 실제 배열 길이를 정확하게 조사하여 반환하므로, 동일한 서브루틴을 임의의 길이를 가진 모든 배열에 그대로 재사용할 수 있다.
또한, 앞서 학습한 전체 배열 연산 식인 v - mean과 배열 내장 함수인 sum이 가정형상 배열 문맥에서도 아무런 제약 없이 자연스럽게 결합되어 결합도를 낮추는 점에 주목하자.
[!important] 11.4 인터페이스 블럭과 명시적 인터페이스
프로시저를 호출할 때, 컴파일러가 해당 프로시저의 인수 개수, 데이터 종류(kind), 속성 등의 구조를 호출 지점에서 정확하게 파악하고 있다면, 그 인터페이스는 명시적(explicit)이다. 반면 컴파일러가 이를 알지 못하는 상태라면 암시적(implicit) 인터페이스이다. 컴파일러가 인수의 유효성을 엄격하게 검사하고 선택적 인수, 키워드 인수, 가정형상 배열 등의 기능들을 정상적으로 처리하기 위해서는 반드시 명시적 인터페이스가 확보되어야 한다.
명시적 인터페이스는 다음 두 가지 경우에 자동으로 제공된다.
내부 프로시저: program이나 다른 프로시저의 contains 구문 내부에 정의된 프로시저이다.
모듈 프로시저: 모듈의 contains 구문 내부에 정의되어 있으며, 호출 측에서 use 문으로 가져와 사용하는 프로시저이다.
어떤 프로그램 단위에도 속하지 않은 채 소스 코드 파일 수준에 독자적으로 존재하는(external procedure)는 기본적으로 암시적 인터페이스를 가진다. 따라서 외부 프로시저를 안전하게 호출하려면, 호출하는 프로그램 측에서 인터페이스 블록(interface block)을 직접 작성하여 컴파일러에게 해당 프로시저의 형태를 명확히 알려주어야만 명시적 인터페이스로 전환된다.
인터페이스 블록은 호출하려는 프로시저의 외형적 구조를 선언하는 역할을 한다. 기본적인 구성 규격은 다음과 같다.
interface
⟨procedure-header⟩
⟨declarations of dummy arguments⟩
⟨end-statement⟩
end interface
선언부만 기술: 인터페이스 블록 내부에는 프로시저의 헤더(subroutine 또는 function 선언문)와 가인수(dummy arguments)의 자료형 및 속성 선언만 포함한다.
실행문 배제: 실제 연산을 수행하는 프로시저 내부의 실행문, 내부 지역 변수 선언, contains 구문 등은 일절 기술하지 않는다.
본질적 의미: 이 블록은 "해당 프로시저를 호출할 때 사용하는 데이터 타입과 인수의 형태가 이러하다"라는 호출 규약에 대한 선언이다.
주요 특징은 다음과 같다.
호출 안전성 확보: 호출 측에서 전달하는 실인수의 타입과 형상이 프로시저가 요구하는 사양과 정확히 일치하는지 컴파일러가 검사할 수 있게 하므로, 실행 중에 발생할 수 있는 치명적인 메모리 오염이나 비정상 종료를 예방한다.
가정형상 배열 지원: 프로시저 내부에서 배열의 정보를 동적으로 얻어와야 하는 경우, 인터페이스 블록을 통해 배열의 형상 정보가 정상적으로 전달될 수 있음을 컴파일러에게 미리 알려주는 필수 통로가 된다.
벡터의 모든 요소 합이 1이 되도록 정규화(normalization)하는 서브루틴을 외부 프로시저로 별도 작성하고, 메인 프로그램에서 인터페이스 블록을 통해 해당 구조를 컴파일러에게 알리는 예제이다. 가인수로 크기가 고정되지 않은 가정형상 배열을 사용하므로 명시적 인터페이스를 제공하는 인터페이스 블록 작성이 필수적이다.
아래는 주 프로그램 외부에 독립적으로 존재하는 외부 서브루틴 소스코드이다.
%%writefile normalize.f90
subroutine normalize(v)
use iso_fortran_env, only: real64
implicit none
real(real64), intent(inout) :: v(:)
real(real64) :: total
total = sum(v)
if (total /= 0.0_real64) then
v = v / total
end if
end subroutine normalize
다음은 위 외부 서브루틴을 호출하는 메인 프로그램 소스코드이다. interface와 end interface 구문 내부에 외부 서브루틴의 헤더와 가인수 선언부 정보를 그대로 작성하여 포함한다.
%%writefile use_normalize.f90
program use_normalize
use iso_fortran_env, only: real64
implicit none
interface
subroutine normalize(v)
use iso_fortran_env, only: real64
implicit none
real(real64), intent(inout) :: v(:)
end subroutine normalize
end interface
real(real64) :: weights(4) = [2.0_real64, 3.0_real64, &
4.0_real64, 1.0_real64]
call normalize(weights)
print '(a)', "normalized weights:"
print '(4f8.4)', weights
print '(a, f8.4)', "sum = ", sum(weights)
end program use_normalize
위 두 소스코드 파일을 함께 지정하여 컴파일한 뒤 실행한다.
!gfortran -O2 -std=f2018 -Wall normalize.f90 use_normalize.f90 -o use_normalize
!./use_normalize
실행하면 다음과 같이 정상적으로 정규화된 배열 요소와 합산 결과가 출력된다.
normalized weights:
0.2000 0.3000 0.4000 0.1000
sum = 1.0000
메인 프로그램 내부에 선언된 인터페이스 블록 덕분에, 컴파일러는 normalize 서브루틴이 배정밀도 실수형 가정형상 배열 하나를 intent(inout) 속성으로 전달받는다는 사실을 호출 지점에서 정확히 인지한다. 이를 바탕으로 실행 시점에 배열의 실제 크기와 형상 정보를 외부 프로시저로 올바르게 넘겨주어 정상적인 연산이 수행되도록 돕는다.
가우스 함수의 폭을 결정하는 매개변수인 $\sigma$의 값만 다르게 지정하여 연속적으로 호출하면, 하나의 함수로부터 파생되는 다양한 곡선군(family of curves)을 얻을 수 있다.
본 예제에서는 세 가지 서로 다른 $\sigma$ 값에 대한 계산 결과를 하나의 csv 파일로 내보낸 뒤, 이를 시각화하여 선택적 인수와 키워드 인수가 데이터 분석에서 어떻게 유용하게 활용되는지 확인한다. 독립 변수 $x$의 범위를 세분화하여 각 지점마다 $\sigma = 0.5$, $\sigma = 1.0$, $\sigma = 2.0$을 적용한 가우스 함수 값을 한 행에 동시에 기록한다.
%%writefile gauss_family.f90
program gauss_family
use iso_fortran_env, only: real64
implicit none
integer, parameter :: n = 200
real(real64), parameter :: x_min = -6.0_real64, x_max = 6.0_real64
real(real64) :: sigmas(3) = [0.5_real64, 1.0_real64, 2.0_real64]
integer :: i, j, u
real(real64) :: x
open(newunit=u, file="gauss.csv", status="replace", action="write")
write(u, '(a)') "x,s0p5,s1p0,s2p0"
do i = 0, n
x = x_min + (x_max - x_min) * real(i, real64) / real(n, real64)
write(u, '(f9.4)', advance="no") x
do j = 1, size(sigmas)
write(u, '(",", f9.6)', advance="no") gaussian(x, sigma=sigmas(j))
end do
write(u, '(a)') ""
end do
close(u)
print '(a)', "gauss.csv written"
contains
function gaussian(x, mu, sigma) result(y)
real(real64), intent(in) :: x
real(real64), intent(in), optional :: mu, sigma
real(real64) :: y
real(real64) :: center, width
center = 0.0_real64
width = 1.0_real64
if (present(mu)) center = mu
if (present(sigma)) width = sigma
y = exp(-0.5_real64 * ((x - center) / width)**2)
end function gaussian
end program gauss_family
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall gauss_family.f90 -o gauss_family
!./gauss_family
실행 후 생성된 gauss.csv의 앞부분은 다음과 같다.
x,s0p5,s1p0,s2p0
-6.0000, 0.000000, 0.000000, 0.011109
-5.9400, 0.000000, 0.000000, 0.012150
저장된 csv 데이터를 바탕으로 세 개의 열을 하나의 그래프에 겹쳐 그리는 데이터 시각화해 보자.
import csv
import matplotlib.pyplot as plt
x = []
cols = {"s0p5": [], "s1p0": [], "s2p0": []}
with open("gauss.csv") as f:
reader = csv.DictReader(f)
for row in reader:
x.append(float(row["x"]))
for key in cols:
cols[key].append(float(row[key]))
plt.figure(figsize=(8, 4))
labels = {"s0p5": "sigma = 0.5", "s1p0": "sigma = 1.0", "s2p0": "sigma = 2.0"}
for key in cols:
plt.plot(x, cols[key], label=labels[key])
plt.xlabel("x")
plt.ylabel("f(x)")
plt.title("Gaussian family (varied sigma)")
plt.legend()
plt.grid(True)
plt.savefig("gauss.png", dpi=120, bbox_inches="tight")
plt.show()
$\sigma$ 값이 작을수록 중심이 좁고 뾰족하게 솟아오르는 종형 곡선이 나타나며, 값이 클수록 좌우로 넓고 완만한 형태를 그리는 종형 곡선이 하나의 평면 위에 겹쳐서 표현된다. 이처럼 매개변수의 지정 값만 바꾸어 동일한 프로시저를 반복 호출하는 연산 방식은 앞서 살펴본 선택적 인수와 키워드 인수의 설계 목적과 정확히 부합한다.
![[Pasted image 20260727192451.png]]
[!important] 오류 학습
명시적 인터페이스가 가지는 가장 큰 가치는 컴파일 단계에서 인수의 유효성을 엄격하게 검사한다는 점이다. 사각형의 넓이를 구하는 외부 함수 rect_area(width, height)를 메인 프로그램에 인터페이스 블록과 함께 선언해 두고, 호출 시 인수를 하나 누락하는 코드를 작성해 보자.
real(real64) :: a
a = rect_area(3.0_real64) ! height is missing
이 경우 명시적 인터페이스가 확보되어 있으므로 컴파일러는 빌드 단계에서 즉시 다음 에러 메시지를 출력하며 실행 파일 생성을 거부한다.
Error: Missing actual argument for argument 'height' at (1)
만약 인터페이스 블록을 제공하지 않은 채 외부 함수를 호출했다면, 컴파일러는 호출 지점에서 인수의 개수가 맞는지조차 검사하지 못하고 그대로 통과시킨다. 이는 결국 프로그램 실행 중에 메모리 참조 오류나 비정상 종료를 일으키는 원인이 된다.
따라서 명시적 인터페이스를 자동으로 확보하도록 모든 프로시저는 주 프로그램의 내부 프로시저나 모듈 프로시저로 설계하는 것을 원칙으로 삼는다. 부득이하게 외부 프로시저를 사용해야 할 때는 호출 측에 반드시 인터페이스 블록을 명시적으로 작성하여 제공해야 한다.
배열의 모든 원소에 특정 상수를 곱해 크기를 조절하는 서브루틴을 구현한 예제이다. 가인수로 가정형상 배열을 받도록 설계하여 별도의 크기 인수를 넘길 필요가 없도록 구성했다. 이 서브루틴을 외부 프로시저로 분리하고, 메인 프로그램에서는 별도의 인터페이스 선언 없이 그대로 호출하도록 코드를 구성해 본다.
%%writefile ext_proc.f90
subroutine scale_vec(v, factor)
use iso_fortran_env, only: real64
implicit none
real(real64), intent(inout) :: v(:)
real(real64), intent(in) :: factor
v = v * factor
end subroutine scale_vec
다음은 위 외부 서브루틴을 명시적 인터페이스 없이 암시적으로 호출하는 메인 프로그램이다.
%%writefile use_noiface.f90
program use_noiface
use iso_fortran_env, only: real64
implicit none
real(real64) :: a(3) = [1.0_real64, 2.0_real64, 3.0_real64]
call scale_vec(a, 2.0_real64)
print '(3f6.1)', a
end program use_noiface
터미널에서 두 소스코드를 함께 컴파일한다. 컴파일 단계에서는 아무런 경고나 오류 메시지 없이 정상적으로 빌드가 완료된다.
!gfortran -O2 -std=f2018 -Wall ext_proc.f90 use_noiface.f90 -o use_noiface
그러나 생성된 실행 파일을 실행하면, 정상적인 연산 결과를 출력하지 못하고 다음과 같이 세그멘테이션 결함(segmentation fault)을 일으키며 프로그램이 비정상 종료된다.
Program received signal SIGSEGV: Segmentation fault - invalid memory reference.
컴파일이 정상적으로 완료되었음에도 실행 단계에서 메모리 오류가 발생하는 이유는 Fortran의 배열 전달 방식 메커니즘 때문이다. scale_vec 서브루틴의 가인수 v(:)는 크기가 고정되지 않은 가정형상 배열이다. 가정형상 배열은 단순히 메모리의 시작 주소만 전달되는 것이 아니라, 배열의 크기(형상)와 메모리 내 간격(보폭, stride) 정보까지 한데 묶은 '배열 서술자(array descriptor)' 형태로 전달된다.
호출하는 메인 프로그램 측에서 이 복합적인 배열 서술자를 올바르게 생성하여 던져주려면, 호출하려는 목적지 프로시저의 구조를 컴파일 단계에서 완벽히 파악하고 있어야 한다. 즉, 명시적 인터페이스가 필수적으로 요구된다.
그러나 외부 프로시저 형태로 작성된 scale_vec은 메인 프로그램 시점에서 암시적 인터페이스 상태로만 인식된다. 이로 인해 컴파일러는 배열 서술자 대신 단순한 평범한 메모리 시작 주소만을 서브루틴으로 넘겨준다. 반면 인수를 받는 서브루틴 측에서는 넘어온 일반 주소 값을 배열 서술자 구조체로 오해하여 해석하므로, 전혀 엉뚱한 메모리 영역을 참조하게 되어 결국 메모리 접근 오류를 일으키고 멈추게 된다.
이 문제를 안전하게 해결하는 방법은 크게 두 가지가 존재한다.
첫 번째 방법은 호출하는 메인 프로그램 내부에 interface 블록을 명시하여 컴파일러가 배열 서술자를 정상적으로 생성하도록 명시적 인터페이스를 제공하는 것이다. 이 코드를 use_iface.f90 파일로 저장한다.
%%writefile use_iface.f90
program use_iface
use iso_fortran_env, only: real64
implicit none
interface
subroutine scale_vec(v, factor)
use iso_fortran_env, only: real64
implicit none
real(real64), intent(inout) :: v(:)
real(real64), intent(in) :: factor
end subroutine scale_vec
end interface
real(real64) :: a(3) = [1.0_real64, 2.0_real64, 3.0_real64]
call scale_vec(a, 2.0_real64)
print '(3f6.1)', a
end program use_iface
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall ext_proc.f90 use_iface.f90 -o use_iface
!./use_iface
이제 정상으로 동작한다.
2.0 4.0 6.0
훨씬 더 간결하고 권장하는 방법은, 해당 프로시저를 독립된 외부 프로시저로 만들지 않는 것이다. 메인 프로그램의 contains 구문 아래에 내부 프로시저로 배치하거나, 추후 다룰 모듈(module) 내부에 구조화하면 명시적 인터페이스가 자동으로 생성되므로 번거로운 인터페이스 블록을 수작업으로 작성할 필요가 없다.
가정형상 배열이나 선택적 인수, 키워드 인수와 같은 현대적인 Fortran 기능들은 모두 명시적 인터페이스가 확보된 상태를 전제로 작동하며, 구조화되지 않은 외부 프로시저는 이런 전제 조건을 스스로 만족할 수 없으므로 주의하여 설계해야 한다.
[!important] 요약
intent — 모든 가인수에 intent(in/out/inout)을 명시한다. in은 읽기 전용, out은 결과 출력(들어온 값은 신뢰 불가), inout은 읽고 갱신. 위반은 컴파일 단계에서 잡힌다.optional 속성을 붙이면 호출할 때 생략 가능하다. 사용 전에 반드시 present로 확인한다.이름=값 형태로 순서에 얽매이지 않고 호출한다. 위치 인수를 먼저, 키워드 인수를 나중에 둔다.v(:)은 형상을 실인수에서 물려받고 size·lbound·ubound로 알아낸다. 명시적 형상보다 간결하지만 명시적 인터페이스가 필요하다.interface ... end interface 블록으로 직접 제공해야 하며, 선택적 인수·가정형상 배열을 쓰려면 필수다. 명시적 인터페이스가 있어야 컴파일러가 인수를 검사한다.[!important] 연습 문제
섭씨 온도 하나를 intent(in)으로 받아 화씨로 변환한 값을 intent(out)으로 돌려주는 서브루틴 to_fahrenheit를 작성하라. 변환식은 F = C * 9/5 + 32이며, 정수 나눗셈 함정에 주의해 실수 연산으로 계산하라.
두 실수 변수의 값을 맞바꾸는 서브루틴 swap을 작성하라. 두 가인수의 intent를 무엇으로 선언해야 하는지 근거와 함께 밝혀라.
intent(in)으로 선언한 가인수에 값을 대입하는 코드를 일부러 작성해 컴파일하고, gfortran이 출력하는 오류 메시지를 그대로 옮겨 적어라.
밑 base와 지수 exponent를 받아 거듭제곱을 돌려주는 함수를 만들고, 키워드 인수를 이용해 두 가지 다른 순서로 호출해 같은 결과가 나옴을 확인하라.
가정형상 배열 하나를 받아 그 최댓값과 최솟값을 각각 intent(out)으로 돌려주는 서브루틴을 작성하라. 내장 함수 maxval, minval을 사용하라.
가정형상 정수 배열을 받아 size로 원소 수를 출력하는 서브루틴을 작성하고, 길이가 다른 두 배열로 각각 호출해 같은 서브루틴이 양쪽 모두에 동작함을 보여라.
선택적 인수 tol(허용 오차)을 받는 함수 is_close(a, b, tol)을 작성하라. tol이 생략되면 기본값 1.0e-9_real64를 쓰고, 두 실수의 차의 절댓값이 tol 이하이면 참을 돌려준다. 부동소수점 동치 비교를 직접 하는 대신 이 함수를 쓰는 이유를 한두 문장으로 설명하라.
11.3의 stats 서브루틴을 확장하여, 선택적 인수 use_sample(논리형)이 참이면 표본 표준편차(분모 n-1), 생략되거나 거짓이면 모표준편차(분모 n)를 계산하도록 만들어라.
외부 함수 dot(u, v)(두 가정형상 실수 배열의 내적)를 별도 파일에 작성하고, 메인 프로그램에서 인터페이스 블록을 통해 호출하라.
가정형상 배열을 받아 모든 원소를 정해진 구간으로 자르는(clamp) 서브루틴을 작성하되, 하한 lo와 상한 hi를 선택적 인수로 두어 생략 시 해당 방향으로는 자르지 않도록 하라.