[!attention] 이 장의 목표
- 모듈 구성 — 모듈을 만들고 use로 불러 쓴다.
- 캡슐화로 데이터 보호 — public과 private로 공개 범위를 정한다.
- 코드 묶기 — 모듈 변수와 contains로 관련 코드를 한곳에 모은다.
- 범위 이해 — 서브모듈과 범위(scope)를 참고 수준에서 파악한다.
지금까지는 하나의 프로그램 파일 안에 모든 구성 요소를 전부 담아 작성했다. 변수, 상수, 함수, 서브루틴이 한곳에 모여 있더라도 프로그램의 규모가 작을 때는 큰 문제가 발생하지 않는다. 그러나 코드의 크기가 커지고 복잡해지면 크게 두 가지 구조적인 문제에 직면하게 된다.
첫째, 동일한 코드를 여러 프로그램이 공유하여 재사용하고자 할 때 한계가 발생한다. 원주율 상수인 pi 값이나 사다리꼴 적분 루틴을 세 개의 서로 다른 프로그램에서 똑같이 사용하고 있다고 가정해 보자. 이를 매번 복사하고 붙여넣는 방식을 사용하면, 나중에 수식을 수정하거나 버그를 고칠 때 일부 파일을 빠뜨려 코드 간의 불일치가 생기기 쉽다. 따라서 단 한 번만 정의해 두고 여러 코드에서 안전하게 가져다 쓸 수 있는 표준적인 장치가 필요하다.
둘째, 프로그램 내부의 고유한 상태 정보를 함부로 건드리지 못하도록 안전하게 격리하고 싶을 때가 있다. 예를 들어, 데이터의 통계를 계산하는 함수 내부에 합계 변수를 가지고 있는 경우, 이 내부 변수를 외부에서 임의의 값으로 변경해 버리면 전체 통계 계산값이 달라진다. 따라서 외부에서는 정해진 통로(함수나 서브루틴)를 통해서만 접근하도록 허용하고, 내부 변수는 숨겨서 접근을 차단할 필요가 있다.
이런 두 가지 과제를 해결해 주는 도구가 바로 모듈(module)이다. 모듈은 공통으로 사용되는 상수, 사용자 정의 자료형, 변수, 그리고 다양한 프로시저들을 하나의 독립된 단위로 묶어 둔 코드 패키지이다.
[!important] 12.1 모듈의 구조와 use
모듈은 그 자체로 실행되지 않고, 다른 프로그램이 가져다 쓰는 독립된 부품 역할을 한다. 모듈을 활용하는 본질적인 이유는 코드의 재사용성을 높이고, 데이터를 안전하게 보호하기 위함이다.
모듈은 관련 있는 데이터, 구조체 타입, 프로시저를 하나로 묶어 관리하는 Fortran의 가장 핵심적인 구조이다. 모듈의 기본 골격은 다음과 같이 module과 end module 블록을 사용하여 설계한다.
module module_name
! 선언부 및 프로시저 포함
end module [module_name]
다른 프로그램이나 또 다른 모듈에서 해당 모듈을 불러올 때는 use 문을 사용한다. 이때 use 문은 반드시 프로그램이나 모듈 선언부의 맨 위, implicit none보다도 먼저 작성해야 한다.
호출하는 쪽에서 모듈을 불러오는 방식은 다음과 같이 분류할 수 있다.
전체 가져오기 방식을 사용하면 use module_name과 같이 기술하며, 해당 모듈이 공개(public)한 모든 요소를 제한 없이 가져온다.
선택 가져오기 방식을 사용하면 use module_name, only: name1 [, name2 ...]와 같이 기술하며, 필요한 요소만 지정해 가져온다. 이 방식은 코드 간의 의존 관계가 명확해지고 예기치 못한 이름 충돌을 방지할 수 있어 적극 권장된다.
이름 변경(rename) 방식을 사용하면 use module_name, only: new_name => old_name과 같이 기술하며, 가져올 요소의 이름이 현재 프로그램 내의 다른 변수명과 충돌할 때 이름을 새롭게 바꿔서 가져올 수 있다.
[!warning] [흔한 실수]
use문은implicit none보다 앞에use문은 선언부의 맨 위,implicit none보다 위에 와야 한다. 순서를 바꾸면Error: USE statement at (1) cannot follow IMPLICIT NONE statement at (2)로 컴파일이 거부된다.
use ... only)물리학과 기하학에서 자주 사용하는 상수를 하나의 모듈에 모아 정의하고, 메인 프로그램에서 필요한 상수만 선택적으로 가져와 활용하는 예제이다. 수치 계산의 정밀도가 중요한 상수를 다루므로 iso_fortran_env 모듈의 real64(배정밀도) 타입을 지정하여 선언한다.
%%writefile constants_mod.f90
module constants_mod
use iso_fortran_env, only: real64
implicit none
private
public :: real64
public :: pi, two_pi, deg_to_rad
real(real64), parameter :: pi = 3.141592653589793_real64
real(real64), parameter :: two_pi = 2.0_real64 * pi
real(real64), parameter :: deg_to_rad = pi / 180.0_real64
end module constants_mod
program use_constants
use constants_mod, only: real64, pi, deg_to_rad
implicit none
real(real64) :: angle_deg, angle_rad
angle_deg = 60.0_real64
angle_rad = angle_deg * deg_to_rad
print '(a, f12.8)', "pi = ", pi
print '(a, f8.2, a, f12.8)', "deg ", angle_deg, " -> rad ", angle_rad
print '(a, f12.8)', "sin(60deg)= ", sin(angle_rad)
end program use_constants
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall constants_mod.f90 -o use_constants
!./use_constants
실행 결과:
pi = 3.14159265
deg 60.00 -> rad 1.04719755
sin(60deg)= 0.86602540
하나의 소스코드 파일 안에 module과 program을 함께 배치하여 컴파일하면, 컴파일러는 의존성 구조에 따라 module을 먼저 컴파일한 뒤 이를 참조하는 program 영역을 그 다음에 빌드한다.
모듈 내부에서 외부로부터 가져온 real64 타입을 public :: real64 구문을 통해 다시 외부로 공개했다. 이처럼 다른 모듈에서 가져온 식별자 이름을 재공개하면, 이를 호출하는 메인 프로그램 측에서는 iso_fortran_env 모듈을 번거롭게 직접 불러올 필요 없이 constants_mod 하나만 참조하는 것만으로 데이터 정밀도 설정(real64)까지 한 번에 해결할 수 있어 코드가 간결해진다.
[!important] 12.2 식별자 충돌 방지와 이름 변경(rename) 기법
모듈을 사용하여 계산 프로그램을 설계하다 보면, 서로 다른 모듈에 동일한 이름의 상수나 변수(예: pi)가 동시에 선언되어 이름 충돌(name conflict)이 발생할 수 있다.
이를 방지하기 위해, Fortran 은 use 문장 뒤에 화살표 연산자 =>를 써서, 가져올 변수의 이름을 프로그램 단위 내부에서 사용할 새로운 이름으로 변경하여 수령하는 이름 변경(rename) 을 제공한다.
=>) 이름 변경 활용%%writefile rename_demo.f90
module geo_constants
implicit none
real, parameter :: pi = 3.14159265
end module geo_constants
module phys_constants
implicit none
real, parameter :: pi = 3.1415927
real, parameter :: c_light = 2.99792458e8
end module phys_constants
program rename_demo
use geo_constants, only: geo_pi => pi
use phys_constants, only: phys_pi => pi, c_light
implicit none
print '(a, f12.8)', "geo_pi = ", geo_pi
print '(a, f12.8)', "phys_pi = ", phys_pi
print '(a, es12.4)', "c_light = ", c_light
end program rename_demo
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall rename_demo.f90 -o rename_demo
!./rename_demo
실행 결과:
geo_pi = 3.14159274
phys_pi = 3.14159274
c_light = 2.9979E+08
화살표 연산자(=>)의 구조적 방향성: 모듈을 가져올 때 이름 변경은 로컬에서_사용할_새_이름 => 모듈_내부의_원래_이름 으로 기술한다. 예제에서 geo_pi => pi 구문은 geo_constants 모듈 안에 있는 원래 변수 pi를, 메인 프로그램 안에서는 geo_pi라는 별칭으로 충돌없이 호출하겠다는 의미이다.
유연한 선별적 결합: phys_constants를 호출하는 use phys_constants, only: phys_pi => pi, c_light 구문을 살펴보면, 이름을 새로 매핑할 식별자(phys_pi => pi)와 기존 모듈의 명칭을 사용할 식별자(c_light)를 쉼표(,) 하나로 묶어, 단 한 줄의 인터페이스 라인에 섞어서 기술할 수 있다.
실무에서 개발자가 배포한 외부 라이브러리나 모듈을 가져와 조합하는 일이 많다. 이때 각기 다른 파일에 선언된 물리 변수들이 우연히 겹쳐 컴파일 단계에서 오류를 내는 현상을 막기 위해, 소스 코드를 직접 수정하여 변수명을 바꾸는 위험을 감수하는 대신 => 연산자를 전면에 배치하는 것이 코드의 안전성 측면에서 정석적인 코딩 전략이다.
[!warning] [흔한 실수] 모듈 간의 순환 참조 오류
모듈을 설계할 때 가장 주의해야 할 구조적 결함은 순환 참조이다. 예를 들어, 모듈 A가 기능을 구현하기 위해 모듈 B를
use로 불러오고, 동시에 모듈 B 역시 모듈 A의 요소를 사용하기 위해 상호 참조하는 형태를 취하면 컴파일러는 다음 에러 메시지가 출력한다.
Cannot open module file 'b_mod.mod' for reading at (1): No such file or directory컴파일러는 각 모듈을 빌드할 때 참조하는 상대방 모듈의 컴파일 결과물(.mod파일)이 먼저 만들어져 있어야 할 것을 요구하는데, 순환 참조는 두 모듈이 서로를 기다리는 교착 상태에 빠지게 된다.이를 해결하려면, 두 모듈이 공통으로 참조하는 상수나 자료형을 분리하여 제3의 독립된 모듈(예:
common_types_mod)로 만들어 둔다. 그 후 모듈 A와 모듈 B가 각각 이 제3의 모듈을use하도록 한다.
[!important] 12.3 public, private 그리고 캡슐화
모듈이 선언하는 식별자들은 기본적으로 모두 공개(public) 상태이다. 즉, 외부 프로그램에서 use 문으로 불러오면 제약 없이 모두 접근할 수 있다. 그러나 모듈의 규모가 커질수록 외부에 제공할 핵심 기능과 내부 구현을 위해 숨겨야 할 전역 변수 등을 철저히 구분해야 한다. 이런 소프트웨어 설계 기법을 캡슐화(encapsulation) 라고 한다.
모듈을 작성할 때는, 기본 접근성을 private으로 비공개 전환하고, 외부에 노출할 인터페이스만 public으로 선언하여 여는 것을 권장한다. 이 방식을 적용하면 개발자가 새로운 내부 변수나 함수를 추가할 때, 접근성 지정을 실수로 누락하더라도 외부로 불필요한 데이터가 노출되는 것을 방지할 수 있다.
Fortran에서 모듈 내 요소들의 접근성을 지정하는 방법은 크게 '문장 형태'와 '속성 형태' 두 가지가 있다.
모듈 선언부 상단의 독립된 명령문으로 접근성을 선언하는 방식이다. private 또는 public 키워드를 단독으로 선언하면 모듈 전체의 기본(default) 접근성이 변경된다.
모듈 선언부 초입에 private 단어 한 줄을 적어두면, 그 아래에서 선언되는 모든 변수와 프로시저의 기본 접근성이 비공개로 전환된다.
이후, 외부에 공개할 특정 요소들만 골라 public :: 이름1 [, 이름2 ...] 형태로 명시적 명세 목록을 작성한다.
변수나 상수를 선언하는 문장에 접근성 키워드를 속성으로 직접 결합하는 방식이다. 아래와 같이 자료형 선언문 내에 쉼표로 구분하여 기술한다.
real(real64), public :: pi integer, private :: internal_status모듈 내부에서 상태 값을 유지하는 전역 변수 count는 철저히 은폐(private)하고, 값의 초기화, 증가, 현재 상태 조회 기능은 검증된 공개(public) 프로시저 인터페이스를 통해서만 제어하는 예제이다.
%%writefile counter_mod.f90
module counter_mod
implicit none
private
integer :: count = 0
public :: reset_counter, increment, current_count
contains
subroutine reset_counter()
count = 0
end subroutine reset_counter
subroutine increment(step)
integer, intent(in), optional :: step
if (present(step)) then
count = count + step
else
count = count + 1
end if
end subroutine increment
function current_count() result(value)
integer :: value
value = count
end function current_count
end module counter_mod
program demo_counter
use counter_mod, only: reset_counter, increment, current_count
implicit none
call reset_counter()
call increment()
call increment()
call increment(step=5)
print '(a, i0)', "current count = ", current_count()
end program demo_counter
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall counter_mod.f90 -o demo_counter
!./demo_counter
실행 결과:
current count = 7
이 구조에서 내부 핵심 상태 변수인 count는 private 속성으로 비공개되어 있기 때문에, 메인 프로그램 영역에서 count = 10과 같이 직접 값을 수정하려고 하면 컴파일러가 빌드를 거부한다.
호출 측 문맥에서는 오직 모듈이 공식적으로 제공하는 세 개의 공개 프로시저만을 조합하여 상태를 안전하게 제어한다. 인수를 생략하여 기본값 1씩 두 번 증가시킨 뒤, 키워드 인수를 활용하여 값을 5만큼 추가로 더한 연산 과정($1 + 1 + 5 = 7$)이 캡슐화된 내부에서 정확하게 수행됨을 확인할 수 있다.
[!note] 호스트 결합(Host Association) Fortran에서 상위 구조(호스트)에 선언된 자원을 하위 구조(내부 프로시저)가 그대로 물려받아 사용하는 변수 공유 메커니즘을 뜻한다.
모듈 선언부에 정의한 상수, 변수, 사용자 정의 자료형은 해당 모듈의
contains구문 아래에 포함된 모든 프로시저(함수 및 서브루틴)에서 별도의 선언 없이 자동으로 인식된다.1. 호스트 결합 비사용 함수가 일을 할 때마다 외부에서 매번
pi값을 알려줘야 한다.
Fortran ! pi를 매번 인수로 넘겨주어야 하는 번거로움 function calc_area(r, pi) result(area) real :: r, pi, area area = pi * r**2 end function calc_area2. 호스트 결합 사용 모듈 내에 상수를 고정해 두면,
contains아래 함수는 인수로pi를 받지 않아도 원래 함수 안에 있던 것처럼 가져다 쓸 수 있다.```Fortran module circle_mod real :: pi = 3.141592 ! <--- 공용 참조!
contains
function calc_area(r) result(area) real :: r, area ! pi를 인수로 받지 않았지만, 위에 선언되어 있으므로 그냥 그냥 보인다! area = pi * r**2 end function calc_area
end module circle_mod ```
private으로 감춰진 모듈 내부 변수 count에 메인 프로그램이 정해진 인터페이스를 거치지 않고 임의로 값을 직접 대입하려고 시도할 때, 컴파일러가 이를 어떻게 차단하는지 확인해 보자.
%%writefile bad_access.f90
program bad_access
use counter_mod
implicit none
count = 100
print *, count
end program bad_access
먼저, 이전 예제에서 작성한 demo_counter.f90을 오브젝트 파일로 컴파일한 뒤, 이를 연동하여 메인 프로그램을 컴파일한다.
!gfortran -std=f2018 -c demo_counter.f90
!gfortran -std=f2018 bad_access.f90 demo_counter.o -o bad_access
컴파일러 메시지:
bad_access.f90:5:8:
5 | count = 100
| 1
Error: Symbol 'count' at (1) has no IMPLICIT type
변수 count가 모듈 내부에서 private 속성으로 선언되어 있기 때문에, 메인 프로그램에서 use counter_mod 문장으로 모듈을 불러오더라도 식별자 count는 현재 프로그램 안으로 들어오지 못한다.
결과적으로 메인 프로그램 입장에서는 count라는 변수가 아예 존재하지 않는 상태가 되며, count는 자료형이 지정되지 않은 선언으로 판정되어 컴파일 단계에서 차단된다. 결과적으로, 캡슐화 장벽이 내부 상태 정보를 안전하게 보호하였다.
[!important] 12.4 모듈 변수와 contains
contains 구문은 모듈의 선언부와 실제 연산을 수행하는 프로시저부를 구조적으로 분리하는 역할을 한다. 이 contains 구문에서 선언되는 모든 변수와 상수를 모듈 변수라고 부르며, 다음 두 가지 특성을 지닌다.
전역 데이터 공유: 모듈에 포함된 모든 함수와 서브루틴은 호스트 결합을 통해 동일한 모듈 변수에 제약 없이 접근할 수 있다. 따라서, 프로시저 간 복잡한 인수를 전달하지 않고도 내부 상태를 공유할 수 있다.
데이터의 지속성: 모듈 변수는 프로그램 실행부터 종료까지 메모리에 항상 유지되는 긴 수명을 가진다. 변수 선언 시 지정한 초깃값은 프로그램 시작 시 단 한 번만 설정되므로, 함수가 여러 번 반복 호출되더라도 내부 상태나 누적된 값을 계속 유지할 수 있다.
모듈 변수와 프로시저를 구조화는 문법 규칙은 다음과 같다.
module ⟨이름⟩
implicit none
private
⟨모듈 변수 선언⟩ ← contains 위: 모든 프로시저가 공유, 수명 길다
public :: ...
contains
⟨function / subroutine 들⟩ ← 모듈 변수를 인수 없이 직접 사용
end module ⟨이름⟩
contains 상단부에서는 데이터를 은폐하고 보호하기 위한 모듈 변수 선언과 외부 노출을 제어하는 접근성을 기술한다.
contains 하단부 에서는 실제 로직을 수행하는 프로시저들을 배치하며, 이 프로시저들은 위쪽에 선언된 모듈 변수를 자유롭게 읽고 쓰며 연산을 수행한다.
이 예제는 실시간으로 값을 하나씩 전달받을 때마다 데이터의 개수, 평균, 분산을 동적으로 갱신해 주는 모듈이다. 통계 계산을 유지하기 위한 핵심 데이터인 합계(total), 제곱합(total_sq), 그리고 데이터 개수(n)를 모듈 변수로 선언하고 private 속성으로 숨겨 외부의 임의 조작을 원천 차단한다.
%%writefile running_stats_mod.f90
module running_stats_mod
use iso_fortran_env, only: real64
implicit none
private
integer :: n = 0
real(real64) :: total = 0.0_real64
real(real64) :: total_sq = 0.0_real64
public :: stats_reset, stats_add, stats_count, stats_mean, stats_variance
contains
subroutine stats_reset()
n = 0
total = 0.0_real64
total_sq = 0.0_real64
end subroutine stats_reset
subroutine stats_add(x)
real(real64), intent(in) :: x
n = n + 1
total = total + x
total_sq = total_sq + x * x
end subroutine stats_add
function stats_count() result(c)
integer :: c
c = n
end function stats_count
function stats_mean() result(m)
real(real64) :: m
if (n == 0) then
m = 0.0_real64
else
m = total / real(n, real64)
end if
end function stats_mean
function stats_variance() result(v)
real(real64) :: v, m
if (n < 2) then
v = 0.0_real64
else
m = stats_mean()
v = (total_sq - real(n, real64) * m * m) / real(n - 1, real64)
end if
end function stats_variance
end module running_stats_mod
program demo_stats
use running_stats_mod
use iso_fortran_env, only: real64
implicit none
real(real64) :: samples(5) = [2.0_real64, 4.0_real64, 4.0_real64, &
6.0_real64, 4.0_real64]
integer :: i
call stats_reset()
do i = 1, size(samples)
call stats_add(samples(i))
end do
print '(a, i0)', "count = ", stats_count()
print '(a, f8.4)', "mean = ", stats_mean()
print '(a, f8.4)', "variance = ", stats_variance()
end program demo_stats
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall running_stats_mod.f90 -o demo_stats
!./demo_stats
실행 결과:
count = 5
mean = 4.0000
variance = 2.0000
입력 데이터 $[2, 4, 4, 6, 4]$의 평균은 4이다. 표본분산은 편차 제곱합을 자유도 $n - 1$로 나눈 수식 $\frac{(2-4)^2 + (4-4)^2 + (4-4)^2 + (6-4)^2 + (4-4)^2}{5-1} = \frac{4+0+0+4+0}{4} = 2$가 되어 연산 결과와 일치한다.
이 프로그램의 핵심은 메인 프로그램(호출자) 측에서 내부의 합계 변수나 제곱합 변수의 존재를 알지 못한 채, 오직 stats_add라는 단 하나의 공개된 통로만을 이용하여 데이터를 안전하게 누적했다는 점이다. 모듈 변수가 각 프로시저 호출 사이에도 메모리 상에 계속 살아남아 지속적으로 누적 연산을 이어가는 Fortran 모듈의 특징을 보여준다.
이 예제에서는 수치 적분 루틴을 calculus_mod 모듈로 분리한다. 본문 프로그램은 그 모듈을 가져다 cos(x) 를 누적 적분하고, 결과를 csv 로 저장한다. 정확한 답은 sin(x) 이므로 이 둘을 함께 저장해 비교하고, 마지막에 시각화한다.
%%writefile integrate_demo.f90
module calculus_mod
use iso_fortran_env, only: real64
implicit none
private
public :: real64
public :: linspace, cumulative_trapz
contains
subroutine linspace(a, b, x)
real(real64), intent(in) :: a, b
real(real64), intent(out) :: x(:)
integer :: i, n
n = size(x)
do i = 1, n
x(i) = a + (b - a) * real(i - 1, real64) / real(n - 1, real64)
end do
end subroutine linspace
subroutine cumulative_trapz(x, y, c)
real(real64), intent(in) :: x(:), y(:)
real(real64), intent(out) :: c(:)
integer :: i
c(1) = 0.0_real64
do i = 2, size(x)
c(i) = c(i - 1) + 0.5_real64 * (y(i) + y(i - 1)) * (x(i) - x(i - 1))
end do
end subroutine cumulative_trapz
end module calculus_mod
program integrate_demo
use calculus_mod
implicit none
integer, parameter :: n = 200
real(real64) :: x(n), y(n), c(n)
real(real64) :: two_pi
integer :: i, u
two_pi = 8.0_real64 * atan(1.0_real64)
call linspace(0.0_real64, two_pi, x)
y = cos(x)
call cumulative_trapz(x, y, c)
open(newunit=u, file="integral.csv", status="replace", action="write")
write(u, '(a)') "x,numerical,exact"
do i = 1, n
write(u, '(f12.6, ",", f12.6, ",", f12.6)') x(i), c(i), sin(x(i))
end do
close(u)
print '(a)', "integral.csv written"
print '(a, es12.4)', "max abs error = ", maxval(abs(c - sin(x)))
end program integrate_demo
컴파일·실행:
!gfortran -O2 -std=f2018 -Wall integrate_demo.f90 -o integrate_demo
!./integrate_demo
실행 결과:
integral.csv written
max abs error = 8.3074E-05
!head ./integral.csv 명령으로 생성 파일을 열어보면 아래와 같다.
x,numerical,exact
0.000000, 0.000000, 0.000000
0.031574, 0.031566, 0.031569
0.063148, 0.063100, 0.063106
0.094721, 0.094572, 0.094580
0.126295, 0.125949, 0.125960
0.157869, 0.157201, 0.157214
0.189443, 0.188296, 0.188312
0.221017, 0.219203, 0.219222
0.252590, 0.249892, 0.249913
이제 파이썬으로 시각화 하자.
import csv
import matplotlib.pyplot as plt
x, num, exact = [], [], []
with open("integral.csv") as f:
reader = csv.reader(f)
next(reader) # skip header
for row in reader:
x.append(float(row[0]))
num.append(float(row[1]))
exact.append(float(row[2]))
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, exact, linewidth=3, alpha=0.4, label="exact: sin(x)")
ax.plot(x, num, linewidth=1.2, label="numerical (trapz)")
ax.set_xlabel("x")
ax.set_ylabel("integral of cos(x)")
ax.set_title("Module routine (Fortran) + Visualization (Python)")
ax.legend()
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig("ch12_integral.png", dpi=120)
plt.show()
아래 그림에서 수치 적분 곡선이 최대 오차는 약 8.3×10⁻⁵ 로 sin(x) 곡선과 거의 일치한다. 적분 루틴은 calculus_mod 안에 있고 주 프로그램은 그것을 가져다 쓰기만 한다.
![[Pasted image 20260714113647.png]]
[!important] 오류 학습
모듈을 처음 쓸 때 거의 모두가 한 번씩 겪는 두 가지 오류가 있다. 하나는 컴파일러가 즉시 찾아 주지만, 더 심각한 오류는 컴파일은 통과해도 계산 결과에 오류가 나는 것이다.
기하학용 상수 모듈과 물리학용 상수 모듈이 각각 정의되어 있고, 두 모듈 모두 pi라는 동일한 이름(식별자)을 외부에 공개(public)하고 있는 상황을 가정해 보자. 두 모듈의 기능이 모두 필요하여 only 없이 전체 가져오기(use) 방식을 사용하면 다음과 같이 이름 충돌 문제가 발생한다.
%%writefile clash.f90
module geo_mod
implicit none
real, parameter :: pi = 3.14159265
end module geo_mod
module phys_mod
implicit none
real, parameter :: pi = 3.1415927
end module phys_mod
program clash
use geo_mod
use phys_mod
implicit none
print *, pi
end program clash
위 코드는 특별한 문제가 없어 보이지만, 컴파일 단계에서 다음과 같이 에러가 발생한다.
!gfortran -std=f2018 -Wall clash.f90 -o clash
clash.f90:15:14:
15 | print *, pi
| 1
Error: Name 'pi' at (1) is an ambiguous reference to 'pi' from module 'geo_mod'
두 개 이상의 모듈이 동일한 식별자 이름을 외부로 노출하고 있을 때, 주 프로그램에서 이를 무분별하게 가져오면, 식별자 이름 충돌이 발생한다. 이 상태에서, 컴파일러는 이것이 geo_mod에서 온 상수인지 phys_mod에서 온 상수인지 결정하지 못하여 모호한 참조(ambiguous reference) 오류를 출력한다.
이 문제는 아래와 같이 선택적 가져오기와 => 연산자를 활용하여 프로그램 내부에서 사용할 고유한 지역 이름을 다르게 부여함으로써 해결할 수 있다.
program clash_fixed
use geo_mod, only: geo_pi => pi
use phys_mod, only: phys_pi => pi
implicit none
print *, geo_pi, phys_pi
end program clash_fixed
이처럼 코드를 수정하면, 모듈에서 가져온 두 상수가 주 프로그램 문맥 내에서 geo_pi와 phys_pi라는 독립된 지역 이름으로 분리되므로, 컴파일러가 대상을 명확하게 식별할 수 있다.
실행 결과:
3.14159274 3.14159274
[!important] 요약
module 이름 … end module 이름. 그 자체로 실행되지 않는 재사용 부품이다.use 모듈(전부), use 모듈, only: a, b(선택), use 모듈, only: 새이름 => 원래이름(이름 바꾸기).use 는 implicit none 보다 먼저 온다.private 로 기본을 닫고 public :: ... 로 공개할 것만 연다. 이것이 캡슐화의 핵심이다.contains: 명세부와 프로시저부의 경계다. 위쪽의 모듈 변수는 모든 프로시저가 공유하고 수명이 길다(암묵적 save).-c 로 먼저 컴파일한 뒤 링크한다.submodule (부모) 이름 에 둔다. 구현은 긴 형태 또는 짧은 module procedure 형태로 작성한다.iso_fortran_env 의 real64 와 _real64 리터럴로 적는다.[!important] 연습 문제
units_mod 을 만들고, 프로그램에서 use ..., only: 로 한 상수만 가져와 환산값을 출력하라.circle_const 와 sphere_const 가 모두 pi 를 공개한다고 하자. 두 모듈을 모두 가져오되 => 로 이름을 구분해 충돌 없이 출력하는 프로그램을 작성하라.decrement 를 추가하고 공개하라. 증가·감소를 섞어 호출한 뒤 최종값을 출력하라.count 를 public 으로 바꾸면 어떤 일이 가능해지는지 설명하고, 그것이 왜 캡슐화 관점에서 바람직하지 않은지 서술하라.call_count 를 둔 모듈을 만들어, 그 모듈의 어떤 프로시저가 호출될 때마다 1 씩 증가시키고 횟수를 조회하는 함수를 공개하라.euler_e(자연상수 e, real64)를 추가하고, 프로그램에서 exp(1.0_real64) 와 비교 출력하라.vec_mod 을 만들어 내적을 직접 구현한 함수와 2-노름을 구하는 함수를 공개하라. 길이 4 벡터로 검증하되, 내장 dot_product 와 값이 같은지 확인하라.running_stats_mod 을 재사용해, 서로 다른 두 데이터 집합에 대해 각각 평균·분산을 구하는 프로그램을 작성하라. 두 번째 집합을 처리하기 전 반드시 초기화하라.calculus_mod 을 재사용해 1/(1+x²) 을 누적 적분하고 결과를 csv 로 내보내라. Python 으로 그려 0 에서 x 까지의 적분이 arctan(x) 에 가까운지 확인하라.