Devin.KR

모듈 - 코드를 파일로 나누기

개발자KR 조회 0

이 장에서 배우는 것

앞 장에서는 계산을 서브루틴과 함수로 나누고 intent 로 인자의 역할을 밝혔다. 그 함수들은 아직 모두 한 파일 안에 있다. 프로그램이 커지면 한 파일이 길어지고, 같은 함수를 다른 프로그램에서 쓰려면 복사해야 한다. Fortran 은 이 문제를 모듈(module)로 푼다. 모듈은 상수, 변수, 함수를 한 이름 아래 묶어 별도 파일에 두고, 필요한 프로그램이 그 이름으로 불러 쓰게 하는 단위다.

이 장에서는 기상 관측소의 하루치 기온 계산을 세 파일로 나눈다. 상수를 모은 모듈, 통계 함수를 모은 모듈, 그것을 불러 쓰는 주 프로그램이다. 파일이 여럿이 되면 컴파일하는 순서와 컴파일러가 만드는 .mod 파일도 알아야 하므로 그 부분까지 다룬다.

  • module 과 use 로 코드를 파일 단위로 나누고 다시 불러 쓴다.
  • public 과 private 로 밖에 보일 이름과 숨길 이름을 정한다.
  • 모듈 상수(parameter)와 모듈 변수의 차이를 구분한다.
  • use ..., only: 로 필요한 이름만 가져온다.
  • 여러 파일을 올바른 순서로 컴파일하고 .mod 파일이 하는 일을 설명한다.

문제 상황

관측소 담당자가 하루 24시간의 기온에서 평균과 영하로 내려간 시간 수를 구하는 프로그램을 만들었다. 처음에는 파일 하나에 상수 정의, 평균 함수, 결빙 시간 함수, 출력이 모두 들어 있었다. 그런데 다음과 같은 일이 생긴다.

  • 강수량 보고서를 만드는 두 번째 프로그램도 같은 평균 함수와 같은 정밀도 설정이 필요하다. 복사해 붙이면 한쪽만 고치고 다른 쪽을 잊는다.
  • 결빙 기준 온도 0.0 이 여러 곳에 숫자로 흩어져 있어, 기준을 바꾸려면 파일 전체를 뒤져야 한다.
  • 함수가 몇 번 불렸는지 세는 내부 카운터가 있는데, 주 프로그램의 다른 부분이 실수로 이 값을 바꿔 버린다.

세 문제의 공통점은 "어디까지가 공용이고 어디부터가 내부 사정인가"가 코드에 드러나 있지 않다는 점이다. 모듈은 이 경계를 파일과 키워드로 표시하게 해 준다.

모듈의 기본 구조

module 과 use

모듈은 module 이름 으로 시작해 end module 이름 으로 끝난다. 선언부에는 상수와 변수를 쓰고, contains 아래에는 그 모듈에 속한 함수와 서브루틴을 쓴다. 이 구조는 주 프로그램의 구조와 거의 같다.

module station_const
  implicit none
  integer, parameter :: HOURS_PER_DAY = 24
end module station_const

다른 프로그램 단위는 맨 앞(implicit none 보다 앞)에 use station_const 를 쓰면 이 이름들을 쓸 수 있다. 이것을 모듈을 "사용한다"고 한다. use 한 줄이 모듈의 선언을 그대로 끌어오기 때문에, 복사할 필요 없이 한 곳만 고치면 모든 사용처가 바뀐다.

모듈 안의 함수는 자동으로 명시적 인터페이스(explicit interface)를 갖는다. 컴파일러가 인자의 개수와 종류를 호출하는 쪽에서 검사할 수 있다는 뜻이다. 앞 장에서 intent 로 밝힌 의도도 이 검사의 대상이 된다. 서브루틴과 함수를 모듈에 넣으라는 권고는 이 이유가 가장 크다.

모듈 상수와 모듈 변수

parameter 속성을 붙인 이름은 값을 바꿀 수 없는 상수(named constant)다. 정밀도를 정한 kind 값, 결빙 기준 온도, 하루의 시간 수처럼 프로그램 전체가 공유하는 값은 모듈 상수로 두기에 알맞다.

parameter 가 없는 모듈 변수는 값을 바꿀 수 있다. 모듈 변수는 프로그램이 실행되는 동안 값이 유지되고, 그 모듈을 쓰는 모든 곳이 같은 변수를 본다. 편리하지만 어디서든 바꿀 수 있다는 뜻이기도 하다. 그래서 모듈 변수는 꼭 필요한 경우에만 쓰고, 바깥에서 건드리지 못하게 private 으로 숨기는 편이 안전하다. 이 장의 호출 횟수 카운터가 그 예다.

보이는 것과 숨기는 것

public 과 private

모듈 안의 이름은 기본적으로 모두 공개(public)다. 모듈 맨 앞에 private 한 줄을 쓰면 기본값이 비공개로 바뀌고, 밖에 보일 이름만 public :: 목록에 적는다. 이 방식이면 새 이름을 추가해도 실수로 공개되지 않는다. 이 책에서는 모든 모듈을 이 형태로 쓴다.

모듈 밖에서는 public 으로 지정한 이름만 쓸 수 있고 private 이름에 접근하면 컴파일 오류가 난다.

비공개 이름은 같은 모듈 안의 함수끼리는 자유롭게 쓸 수 있다. 밖에서 쓰려 하면 컴파일 단계에서 오류가 나므로, 실행 중에 몰래 값이 바뀌는 사고를 막는다. 나중에 내부 구현을 바꿀 때도 비공개 이름은 밖에서 쓰고 있지 않다는 사실이 보장된다.

use only 와 이름 바꾸기

use 모듈 만 쓰면 모듈의 공개 이름이 모두 들어온다. 이름이 많아지면 내 변수와 이름이 겹치거나, 어떤 이름이 어느 모듈에서 왔는지 알기 어렵다. use 모듈, only: 이름1, 이름2 로 필요한 것만 쓰는 습관을 들이면 이 두 문제가 함께 줄어든다.

가져오면서 이름을 바꿀 수도 있다. only: avg => mean_temp 는 모듈의 mean_temp 를 이 파일 안에서 avg 라는 이름으로 부르겠다는 뜻이다. 두 모듈에 같은 이름이 있을 때 이 방법으로 충돌을 피한다.

모듈 이름을 다루는 문장과 그 효과
문장위치효과권장
private모듈 선언부 맨 앞기본값을 비공개로 바꾼다항상 쓴다
public :: a, b모듈 선언부a, b 만 밖에 보인다항상 쓴다
use m사용하는 쪽 맨 앞m 의 공개 이름을 모두 가져온다짧은 예제에서만
use m, only: a사용하는 쪽 맨 앞a 만 가져온다기본으로 쓴다
use m, only: x => a사용하는 쪽 맨 앞a 를 x 라는 이름으로 가져온다충돌할 때

여러 파일 컴파일과 .mod 파일

모듈을 별도 파일에 두면 컴파일 순서가 중요해진다. gfortran 은 모듈 파일을 컴파일할 때 그 모듈의 공개 이름, 상수 값, 함수 인터페이스를 담은 .mod 파일을 함께 만든다. use 문이 있는 파일을 컴파일할 때는 이 .mod 파일을 읽어 정보를 얻는다. 따라서 모듈을 쓰는 파일보다 모듈 파일이 먼저 컴파일되어야 한다.

모듈 파일을 먼저 컴파일해 .mod 를 만들고 그 .mod 를 읽는 main.f90 을 마지막에 컴파일한다.

이 장의 의존 관계는 다음과 같다. temp_stats 가 station_const 를 쓰고, 주 프로그램이 둘 다 쓴다. 그래서 순서는 station_const.f90, temp_stats.f90, main.f90 이다. 한 명령에 파일을 이 순서로 나열하면 gfortran 이 왼쪽부터 처리한다.

gfortran -std=f2018 -Wall station_const.f90 temp_stats.f90 main.f90 -o report

파일이 많을 때는 파일마다 -c 옵션으로 목적 파일(.o)만 만들고, 마지막에 하나로 묶기도 한다. 이 경우에도 컴파일 순서는 같다.

gfortran -std=f2018 -Wall -c station_const.f90
gfortran -std=f2018 -Wall -c temp_stats.f90
gfortran -std=f2018 -Wall -c main.f90
gfortran station_const.o temp_stats.o main.o -o report

.mod 파일에 대해 알아 둘 점이 세 가지 있다. 첫째, 컴파일러가 읽기 위한 이진 파일이므로 직접 열어 고치지 않는다. 둘째, 컴파일러 종류와 버전에 따라 형식이 달라서 다른 컴파일러가 만든 것을 가져다 쓸 수 없다. 셋째, 모듈 소스를 고쳤는데 .mod 를 다시 만들지 않으면 쓰는 쪽이 옛 정보를 읽는다. 모듈을 고친 뒤에는 그 모듈부터 다시 컴파일한다. .mod 파일을 다른 디렉터리에 모으고 싶으면 -J build 처럼 옵션을 주면 된다. 자세한 옵션은 GNU Fortran 공식 문서에서 확인할 수 있다.

완성 코드

세 파일을 같은 디렉터리에 만든다. 모듈 파일을 먼저, main.f90 을 마지막에 컴파일한다.

station_const.f90

module station_const
  implicit none
  private
  public :: wp, FREEZING_C, STATION_NAME, HOURS_PER_DAY

  integer, parameter :: wp = selected_real_kind(15, 307)
  real(wp), parameter :: FREEZING_C = 0.0_wp
  character(len=*), parameter :: STATION_NAME = "Pine Ridge"
  integer, parameter :: HOURS_PER_DAY = 24
end module station_const

temp_stats.f90

module temp_stats
  use station_const, only: wp, FREEZING_C
  implicit none
  private
  public :: mean_temp, count_frozen, get_call_count

  integer :: n_calls = 0

contains

  subroutine bump_counter()
    n_calls = n_calls + 1
  end subroutine bump_counter

  function mean_temp(t) result(m)
    real(wp), intent(in) :: t(:)
    real(wp) :: m
    call bump_counter()
    m = sum(t) / real(size(t), wp)
  end function mean_temp

  function count_frozen(t) result(n)
    real(wp), intent(in) :: t(:)
    integer :: n
    call bump_counter()
    n = count(t < FREEZING_C)
  end function count_frozen

  function get_call_count() result(n)
    integer :: n
    n = n_calls
  end function get_call_count

end module temp_stats

main.f90

program daily_report
  use station_const, only: wp, STATION_NAME, HOURS_PER_DAY
  use temp_stats, only: mean_temp, count_frozen, get_call_count
  implicit none

  real(wp) :: temps(HOURS_PER_DAY)
  real(wp) :: avg
  integer :: h, n_frozen, n_calls

  do h = 1, HOURS_PER_DAY
    temps(h) = 6.0_wp - 0.5_wp * real(h - 1, wp)
  end do

  avg = mean_temp(temps)
  n_frozen = count_frozen(temps)
  n_calls = get_call_count()

  print '(a,a)', "Station: ", STATION_NAME
  print '(a,i0)', "Hours: ", HOURS_PER_DAY
  print '(a,f8.2)', "Mean temp (C):", avg
  print '(a,i4)', "Frozen hours:", n_frozen
  print '(a,i0)', "Calls: ", n_calls
end program daily_report

줄별 해설

station_const.f90

  • private 와 public :: ... 는 기본을 비공개로 하고 네 이름만 공개한다. 이 모듈에는 비공개로 남는 이름이 없지만, 나중에 이름을 추가할 때를 위해 같은 형식을 유지한다.
  • wp 는 selected_real_kind(15, 307) 로 얻은 정밀도 값이다. 십진 15자리 이상을 보장하는 실수 종류를 고른다. 이 상수를 모듈에 두면 모든 파일이 같은 정밀도를 쓴다. 이 장의 서술은 앞서 다룬 kind 개념에 기대고 있다.
  • FREEZING_C 는 결빙 기준이다. 숫자 0.0 이 여러 곳에 흩어지는 대신 이름 하나로 모인다.
  • character(len=*) 상수는 길이를 값에서 자동으로 정한다. 문자열을 바꿔도 길이를 직접 세지 않아도 된다.

temp_stats.f90

  • use station_const, only: wp, FREEZING_C 는 이 모듈이 실제로 쓰는 두 이름만 가져온다. 쓰지 않는 HOURS_PER_DAY 는 들어오지 않는다.
  • public 목록에는 함수 세 개만 있다. n_calls 와 bump_counter 는 목록에 없으므로 비공개다.
  • integer :: n_calls = 0 는 초깃값이 있는 모듈 변수다. 프로그램이 도는 동안 값이 유지되며 모듈 안의 모든 함수가 같은 변수를 본다.
  • bump_counter 는 카운터를 하나 올리는 내부 서브루틴이다. 두 통계 함수가 호출하지만 밖에서는 호출할 수 없다.
  • t(:) 는 길이를 호출한 쪽에서 받아 오는 배열이다. 그래서 24개든 48개든 같은 함수를 쓴다. intent(in) 은 이 함수가 t 를 바꾸지 않는다는 약속이다.
  • count(t < FREEZING_C) 는 조건이 참인 원소의 개수를 센다. 정확히 0.0 인 값은 "미만"이 아니므로 세지 않는다.
  • get_call_count 는 비공개 변수를 읽기만 하는 창구다. 값을 밖에 보여 주되 바꾸지는 못하게 하는 흔한 방식이다.

main.f90

  • 두 use 문은 모두 only 로 필요한 이름만 가져온다. 아래 코드에서 쓰는 이름이 어느 모듈에서 왔는지 이 두 줄만 보면 안다.
  • real(wp) :: temps(HOURS_PER_DAY) 의 배열 크기가 모듈 상수다. 하루의 시간 수가 바뀌면 상수 한 곳만 고친다.
  • 반복문은 첫 시각을 6.0도로 두고 한 시간마다 0.5도씩 내린다. 값이 모두 이진수로 정확히 표현되는 0.5 의 배수라서 계산 결과가 항상 같다.
  • 평균 계산, 결빙 시간 계산, 호출 횟수 읽기를 각각 별도의 문장으로 쓴다. 함수가 내부 카운터를 바꾸므로, 한 출력문 안에서 호출 순서에 기대면 결과가 불확실해진다.
  • 서식 f8.2 는 전체 8칸에 소수점 아래 둘째 자리까지 쓴다. i4 는 정수를 4칸에 오른쪽 맞춤으로, i0 는 필요한 만큼의 칸만 쓴다.

실행 결과

$ gfortran -std=f2018 -Wall station_const.f90 temp_stats.f90 main.f90 -o report
$ ./report
Station: Pine Ridge
Hours: 24
Mean temp (C):    0.25
Frozen hours:  11
Calls: 2

기온은 6.0도에서 시작해 -5.5도까지 24개 값이 있고, 평균은 (6.0 + (-5.5)) / 2 = 0.25 이다. 0도 미만인 값은 -0.5도부터 -5.5도까지 11개다. 13번째 값이 정확히 0.0 이어서 세지 않는다. 호출 횟수 2는 평균과 결빙 시간 함수가 한 번씩 불린 결과다. 명령이 끝나면 디렉터리에 station_const.mod 와 temp_stats.mod 가 생겨 있다.

실무에서 자주 틀리는 것

1. 컴파일 순서를 거꾸로 쓴다

주 프로그램을 먼저 나열하면 컴파일러가 읽을 .mod 가 아직 없다.

gfortran -std=f2018 -Wall main.f90 station_const.f90 temp_stats.f90 -o report

"모듈 파일 station_const.mod 를 열 수 없다"는 치명적 오류와 함께 컴파일이 멈춘다. 모듈 파일을 쓰는 순서대로 앞에 두고 main.f90 을 마지막에 쓴다.

gfortran -std=f2018 -Wall station_const.f90 temp_stats.f90 main.f90 -o report

2. use 만 쓰고 이름이 겹친다

모듈의 공개 이름을 전부 가져온 뒤 같은 이름을 지역 변수로 선언하면 충돌한다.

program demo
  use temp_stats
  implicit none
  real :: mean_temp     ! 모듈의 mean_temp 와 충돌
  mean_temp = 1.0
end program demo

필요한 이름만 가져오면 어떤 이름이 들어오는지 분명하고 충돌 가능성도 줄어든다. 꼭 같은 이름을 써야 하면 이름을 바꿔 가져온다.

program demo
  use temp_stats, only: avg => mean_temp
  implicit none
  real :: mean_temp
  mean_temp = 1.0
end program demo

3. 모듈 상수를 고치려 한다

parameter 로 선언한 이름에는 값을 대입할 수 없다.

use station_const, only: wp, FREEZING_C
...
FREEZING_C = -1.0_wp    ! 컴파일 오류

상수는 이름 그대로 바뀌지 않는다. 계산마다 기준이 달라져야 한다면 기준 값을 함수의 인자로 받는다.

function count_below(t, limit) result(n)
  real(wp), intent(in) :: t(:), limit
  integer :: n
  n = count(t < limit)
end function count_below

4. private 으로 두고 public 목록을 빼먹는다

기본값을 비공개로 바꾼 모듈에서 함수를 만들고 public 목록에 적지 않으면, 쓰는 쪽에서는 그 이름이 모듈에 없는 것처럼 보인다.

module temp_stats
  private
  public :: mean_temp          ! count_frozen 을 빼먹음
  ...
program
  use temp_stats, only: count_frozen   ! 모듈에서 찾을 수 없다는 오류

새 함수를 모듈에 추가할 때는 public 목록에 이름을 적는 일까지 한 묶음으로 기억한다.

한눈에 보기

이 장에서 배운 내용 요약
주제쓰는 법기억할 점
모듈 정의module m ... contains ... end module m함수는 자동으로 명시적 인터페이스를 갖는다
모듈 사용use m, only: a, b프로그램 단위 맨 앞, implicit none 보다 앞
공개 범위private 후 public :: ...새 이름은 목록에 적어야 밖에 보인다
모듈 상수parameter 속성값을 바꿀 수 없고 모든 파일이 공유한다
모듈 변수초깃값이 있는 선언실행 내내 유지되므로 비공개로 숨긴다
컴파일모듈 파일을 먼저, main.f90 을 마지막.mod 는 컴파일러가 만들고 읽는 파일이다

연습 문제

  1. temp_stats 에 하루 기온 차(최고값 빼기 최저값)를 돌려주는 공개 함수 temp_range 를 추가하고, 주 프로그램에서 f8.2 로 출력하라. 이 함수도 호출 횟수에 포함되게 하라.
  2. temp_stats.f90 를 고친 뒤 main.f90 만 다시 컴파일하면 어떤 문제가 생길 수 있는가. 올바른 재컴파일 순서를 쓰라.
  3. 주 프로그램에서 mean_temp 를 average 라는 이름으로 부르도록 use 문을 고쳐 쓰라.
  4. 주 프로그램이 n_calls = 0 으로 카운터를 되돌리는 코드를 쓰면 어떻게 되는가. 카운터를 되돌리는 기능이 필요하다면 어떻게 만들어야 하는가.

정답과 해설

1. 모듈의 public 목록에 temp_range 를 추가하고 함수를 contains 아래에 쓴다.

  function temp_range(t) result(r)
    real(wp), intent(in) :: t(:)
    real(wp) :: r
    call bump_counter()
    r = maxval(t) - minval(t)
  end function temp_range

주 프로그램은 only 목록에 temp_range 를 더하고 print '(a,f8.2)', "Range (C):", temp_range(temps) 를 쓴다. 이 자료에서 최고 6.0, 최저 -5.5 이므로 차는 11.5 이고 출력은 Range (C): 11.50 이다. 단 이 호출은 get_call_count 를 읽은 뒤에 두면 카운트가 2 로 유지되고, 앞에 두면 3 이 된다.

2. main.f90 만 다시 컴파일하면 temp_stats.mod 가 옛 내용 그대로여서, 바뀐 인터페이스를 컴파일러가 모른다. 새 함수를 추가했다면 찾을 수 없다는 오류가 나고, 인자를 바꿨다면 옛 인자 기준으로 검사하게 된다. station_const.f90, temp_stats.f90, main.f90 순서로 모두 다시 컴파일한다. 바뀐 모듈에 의존하는 파일은 함께 다시 컴파일해야 한다.

3. use temp_stats, only: average => mean_temp, count_frozen, get_call_count 로 쓰고, 본문에서는 avg = average(temps) 로 부른다. 화살표 왼쪽이 이 파일에서 쓸 이름이고 오른쪽이 모듈의 원래 이름이다.

4. n_calls 가 비공개이므로 주 프로그램에서는 그 이름을 쓸 수 없다. 암시적 선언을 막는 implicit none 아래에서는 선언되지 않은 이름이라는 오류가 나고, 설령 같은 이름의 지역 변수를 만들어도 모듈의 카운터와는 별개의 변수다. 되돌리는 기능이 필요하면 모듈 안에 subroutine reset_counter() 를 쓰고 public 목록에 추가한다. 이렇게 하면 카운터를 바꾸는 경로가 이 서브루틴 하나로 제한되어, 나중에 값이 이상해졌을 때 살펴볼 곳이 분명하다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.