Fortran · 심화
객체와 수치 해석으로 깊어지는 Fortran
모듈 설계 심화 - 공개 범위와 서브모듈
private·public·protected, use 의 only 와 이름 바꾸기, submodule 로 인터페이스와 구현 나누기
개발자KR · 원고 갱신
이 장에서 배우는 것
2차원 금속판의 온도를 계산하는 프로그램은 처음에는 배열 하나와 반복문 몇 개로 시작할 수 있다. 그러나 재료 설정, 온도 초기화, 계산, 결과 저장이 더해지면 어느 부분이 어느 데이터에 접근해도 되는지 정해야 한다. 모든 이름을 공개하면 다른 코드가 내부 상태를 쉽게 바꿀 수 있고, 모든 것을 감추면 필요한 작업도 수행할 수 없다. 모듈 설계는 이 두 요구 사이에서 사용할 이름과 변경할 경로를 정하는 일이다.
이 장에서는 공개 인터페이스(public interface)를 먼저 정하고 구현을 그 뒤에 배치한다. 작은 금속판의 초기 온도를 만들면서 공개 범위, 보호된 변수, 선택적 가져오기, 서브모듈(submodule)을 함께 사용한다. 열 확산 계산식은 아직 넣지 않는다. 이후 계산 기능을 추가할 수 있도록 판의 상태와 조작 절차 사이에 경계를 만드는 것이 이번 예제의 목적이다.
private를 기본값으로 두고 필요한 이름만public으로 공개한다.protected로 모듈 변수의 읽기와 쓰기 권한을 구분한다.use의only와 이름 바꾸기로 의존 관계를 드러낸다.- 부모 모듈에 절차의 인터페이스를 두고 서브모듈에 구현을 둔다.
- 모듈과 서브모듈의 의존 순서에 맞추어 컴파일하고 실행 파일을 연결한다.
문제 상황
금속판의 온도를 temperature(nx, ny)에 저장한다고 하자. 처음에는 실행 프로그램이 배열을 만들고 값을 채운다. 이후 출력 절차와 계산 절차가 같은 배열을 공유하면서 다음과 같은 문제가 생긴다. 출력 코드가 편의를 위해 온도 값을 고치고, 초기화 코드가 재료 설정을 직접 바꾸며, 다른 모듈에서 가져온 이름이 지역 변수와 겹친다. 각각의 수정은 짧아도 상태를 바꾸는 경로가 늘어나면 오류를 찾기 어려워진다.
이번 예제에서는 온도 배열을 판 타입의 비공개 성분으로 둔다. 사용자는 판을 선언할 수 있지만 배열을 직접 수정할 수 없다. 대신 판 초기화와 내부 가열 절차를 호출한다. 재료의 열확산계수는 출력할 수 있도록 공개하되, 양수인지 확인하는 설정 절차를 통해서만 바꾸게 한다. 평균 온도는 조회 함수로 얻는다.
또 하나의 문제는 인터페이스와 구현이 한 파일에 섞인다는 점이다. 호출하는 쪽에서는 인수의 타입과 의미가 중요하지만, 구현을 고치는 쪽에서는 배열 할당과 대입이 중요하다. 부모 모듈에 호출 규약을 남기고 서브모듈에 실행문을 옮기면 두 관심사를 파일 수준에서 구분할 수 있다.
이 예제의 격자는 가로 4칸, 세로 3칸이다. 전체를 20도로 초기화한 뒤 가장자리를 제외한 두 칸을 80도로 설정한다. 열확산계수는 재료 설정으로 보관하고 출력한다. 아직 온도를 시간에 따라 갱신하지 않으므로 이 계수가 평균 온도 계산에 들어가지는 않는다.
공개 범위와 상태 변경 권한
기본은 감추고 필요한 이름을 공개한다
모듈에 공개 범위를 따로 지정하지 않으면 모듈의 이름은 기본적으로 공개된다. 작은 예제에서는 편리하지만 보조 변수와 내부 절차까지 호출 코드의 의존 대상이 될 수 있다. 모듈의 명세 부분에 private를 두고 공개할 이름을 나열하면 검토할 범위가 분명해진다.
module thermal_model
implicit none
private
public :: plate_t, initialize_plate
end module thermal_model
이 조각은 공개 정책의 형태만 보여 준다. 실제 타입과 절차 선언은 뒤의 완성 코드에 들어간다. 인수가 없는 private는 모듈의 기본 접근성을 정한다. public :: 이름은 나열한 이름에 예외를 준다. 이름을 새로 추가해도 자동으로 외부에 드러나지 않으므로 구현을 확장하기 쉽다.
모듈의 접근성과 파생 타입 성분의 접근성은 구분해야 한다. plate_t 자체가 공개되어 있어도 타입 정의 안의 private로 성분을 감출 수 있다. 외부 코드는 type(plate_t) :: plate처럼 변수를 선언하지만 plate%temperature에는 접근할 수 없다. 공개 타입이 내부 표현까지 공개한다는 뜻은 아니다.
다만 비공개 성분을 둔다고 그 객체에 대한 모든 연산을 금지하는 것은 아니다. 예를 들어 같은 타입의 객체 사이에 기본 대입을 할 수 있다. 여기서 막는 것은 외부 코드가 성분 이름을 통해 온도 배열을 직접 조회하거나 수정하는 경로다. 설계의 효과를 설명할 때에는 실제로 제한한 연산을 구체적으로 구분해야 한다.
| 선언 | 적용 대상 | 외부 코드의 사용 |
|---|---|---|
private | 모듈의 기본 접근성 | 따로 공개하지 않은 이름을 가져올 수 없다. |
public :: plate_t | 파생 타입 이름 | 이 타입의 변수를 선언할 수 있다. |
타입 내부의 private | 타입 성분 | 성분 이름으로 직접 접근할 수 없다. |
public, protected | 모듈 변수 | 값을 읽을 수 있지만 외부에서 정의할 수 없다. |
protected는 공개된 변수의 쓰기 경로를 제한한다
열확산계수는 사용자가 현재 설정을 확인할 필요가 있다. 따라서 이름을 공개하되 protected 속성을 함께 붙인다. 이 속성은 모듈 변수에 적용한다. 파생 타입의 개별 성분을 읽기 전용으로 만드는 선언은 아니다.
real(rk), public, protected :: thermal_diffusivity = 0.10_rk
다른 프로그램 단위에서 이 변수를 use로 가져오면 식이나 출력문에서 값을 읽을 수 있다. 그러나 대입문의 왼쪽에 놓거나 intent(out), intent(inout) 인수로 넘겨 값을 바꾸게 할 수 없다. 이름이 공개되어 있다는 사실과 값을 변경할 수 있다는 사실이 분리된다.
변수를 정의한 모듈과 그 후손 서브모듈의 절차에서는 이 값을 변경할 수 있다. 완성 코드의 set_diffusivity는 입력값이 양수인지 검사한 뒤 대입한다. 따라서 호출자는 설정을 요청할 수 있고, 모듈은 상태를 바꾸기 전에 조건을 검사할 수 있다.
protected가 입력값의 타당성을 대신 검사하지는 않는다. 모듈 내부에서 잘못된 값을 대입하면 그 값이 그대로 저장된다. 이 속성은 변경 권한의 경계를 만들고, 검사는 설정 절차가 맡는다. 이번 검사는 양수 조건만 다룬다. 비유한 값까지 포함한 정밀한 수치 검증은 별도의 정책이 필요하다.
use로 의존하는 이름을 명시한다
use thermal_model은 그 모듈에서 접근할 수 있는 공개 이름을 가져온다. 호출 코드가 실제로 사용하는 이름이 몇 개뿐이라면 only로 목록을 제한하는 편이 낫다. 모듈에 새 공개 이름이 생겨도 현재 프로그램의 이름 공간에 자동으로 들어오지 않는다.
use thermal_model, only : plate_t, average_temperature
only는 원래 모듈의 공개 정책을 바꾸지 않는다. 그 문장이 있는 프로그램 단위에서 가져올 이름을 정할 뿐이다. 비공개 이름을 only에 적는다고 접근할 수 있게 되지는 않는다. 공개 범위는 제공하는 쪽의 정책이고, 선택적 가져오기는 사용하는 쪽의 정책이다.
이름 바꾸기는 지역이름 => 원래이름 순서로 쓴다. 완성 코드에서는 plate_t를 plate_type으로, initialize_plate를 create_plate로 가져온다. 호출자는 자신의 코드에서 읽기 좋은 이름을 선택할 수 있고, 원래 모듈의 선언은 그대로 유지된다.
use thermal_model, only : plate_type => plate_t, &
create_plate => initialize_plate
이 문장으로 가져온 타입은 현재 범위에서 plate_type으로 참조한다. 같은 문장에서 원래 이름을 별도로 가져오지 않았으므로 plate_t를 함께 사용할 수 있다고 생각하면 안 된다. 이름 바꾸기는 타입을 새로 만들거나 절차를 복제하지 않는다. 같은 대상을 다른 지역 이름으로 참조한다.
실수 종류를 나타내는 rk도 같은 원리를 사용한다. 부모 모듈은 표준 모듈의 real64를 rk로 가져와 공개한다. 실행 프로그램은 그 rk로 상수의 종류를 지정한다. 모듈의 실수 인수와 호출 코드의 상수가 같은 종류를 사용하므로 인수 종류 불일치를 피할 수 있다.
여러 모듈에서 같은 이름의 절차를 가져와야 할 때도 이름 바꾸기가 유용하다. 단, 이름이 겹친다는 이유만으로 모든 절차에 접두사를 붙일 필요는 없다. 먼저 only로 필요한 이름을 줄이고, 실제 충돌이나 의미상의 혼동이 있는 곳에 지역 이름을 정한다.
서브모듈로 선언과 실행문을 나눈다
부모 모듈은 공개 타입, 변수, 절차의 인수 목록을 선언한다. 서브모듈은 그 절차의 실행문을 제공한다. 호출 프로그램은 부모 모듈만 use한다. 구현 파일의 이름이나 서브모듈 이름을 알아야 호출할 수 있는 구조는 아니다.
부모 모듈의 interface 안에는 module subroutine 또는 module function으로 분리 모듈 절차(separate module procedure)의 인터페이스를 선언한다. 인수의 타입, 종류, 배열 형태, intent, 함수 결과가 이곳에 놓인다. 일반적인 외부 절차 인터페이스 블록과 달리, 여기의 모듈 절차 인터페이스는 부모 모듈의 이름을 참조할 수 있다. 따라서 rk와 plate_t를 그대로 쓴다.
interface
module subroutine set_diffusivity(value)
real(rk), intent(in) :: value
end subroutine set_diffusivity
end interface
구현은 submodule (thermal_model) thermal_model_impl로 시작한다. 괄호 안은 부모 모듈 이름이고 뒤의 이름은 이 서브모듈 이름이다. 서브모듈은 호스트 결합(host association)을 통해 부모의 선언을 참조한다. 부모의 비공개 이름과 타입의 비공개 성분도 구현에 필요한 범위에서 사용할 수 있다.
module procedure set_diffusivity
if (value <= 0.0_rk) error stop 'diffusivity must be positive'
thermal_diffusivity = value
end procedure set_diffusivity
module procedure로 시작하는 구현은 부모에서 정한 인수와 결과 선언을 이어받는다. 이 형태에서는 인수 목록을 다시 쓰지 않는다. 인터페이스의 value를 실행문에서 그대로 사용한다. 선언과 구현을 모두 같은 곳에 적는 일반적인 모듈 절차와 문법이 다르므로 시작문과 종료문을 함께 확인해야 한다.
이 구조는 구현만 고치는 작업과 호출 규약을 바꾸는 작업을 구별한다. 서브모듈의 실행문만 바뀌었다면 부모 인터페이스에 의존하는 호출 코드는 보통 다시 컴파일할 필요가 없고, 변경된 구현을 컴파일한 뒤 다시 연결한다. 인수나 공개 타입이 바뀌면 부모에 의존하는 코드도 다시 컴파일해야 한다. 실제 자동 재빌드 범위는 빌드 도구의 의존 관계 설정에도 달려 있다.
GNU Fortran은 모듈 정보를 .mod, 서브모듈 관련 정보를 .smod 파일에 기록한다. 이 파일들은 소스가 아니며 컴파일러가 생성한다. 다른 컴파일러나 다른 버전과 공통으로 사용할 파일 형식이라고 가정하지 않는다. 실행 파일을 연결할 때는 선언 정보만으로 충분하지 않고 구현의 목적 파일도 필요하다.
완성 코드
세 파일을 같은 디렉터리에 저장한다. 부모 모듈을 먼저, 구현 서브모듈을 다음으로, 실행 프로그램을 마지막으로 둔다. 공개 절차는 판 초기화, 내부 온도 설정, 평균 조회, 재료 설정의 네 가지다. 온도 배열에 대한 직접 접근은 모듈 내부에 남긴다.
thermal_model.f90
module thermal_model
use iso_fortran_env, only : rk => real64
implicit none
private
public :: rk, plate_t
public :: initialize_plate, seed_hot_region
public :: average_temperature, set_diffusivity
real(rk), public, protected :: thermal_diffusivity = 0.10_rk
type :: plate_t
private
real(rk), allocatable :: temperature(:, :)
end type plate_t
interface
module subroutine initialize_plate(plate, nx, ny, initial)
type(plate_t), intent(out) :: plate
integer, intent(in) :: nx, ny
real(rk), intent(in) :: initial
end subroutine initialize_plate
module subroutine seed_hot_region(plate, hot)
type(plate_t), intent(inout) :: plate
real(rk), intent(in) :: hot
end subroutine seed_hot_region
module function average_temperature(plate) result(mean_value)
type(plate_t), intent(in) :: plate
real(rk) :: mean_value
end function average_temperature
module subroutine set_diffusivity(value)
real(rk), intent(in) :: value
end subroutine set_diffusivity
end interface
end module thermal_model
thermal_model_impl.f90
submodule (thermal_model) thermal_model_impl
implicit none
contains
module procedure initialize_plate
if (nx < 3 .or. ny < 3) then
error stop 'plate dimensions must be at least 3'
end if
allocate(plate%temperature(nx, ny))
plate%temperature = initial
end procedure initialize_plate
module procedure seed_hot_region
integer :: nx, ny
if (.not. allocated(plate%temperature)) then
error stop 'plate is not initialized'
end if
nx = size(plate%temperature, 1)
ny = size(plate%temperature, 2)
plate%temperature(2:nx-1, 2:ny-1) = hot
end procedure seed_hot_region
module procedure average_temperature
if (.not. allocated(plate%temperature)) then
error stop 'plate is not initialized'
end if
mean_value = sum(plate%temperature) / &
real(size(plate%temperature), rk)
end procedure average_temperature
module procedure set_diffusivity
if (value <= 0.0_rk) then
error stop 'diffusivity must be positive'
end if
thermal_diffusivity = value
end procedure set_diffusivity
end submodule thermal_model_impl
main.f90
program main
use thermal_model, only : rk, plate_type => plate_t, &
create_plate => initialize_plate, &
seed_hot_region, average_temperature, &
set_diffusivity, thermal_diffusivity
implicit none
type(plate_type) :: plate
call create_plate(plate, 4, 3, 20.0_rk)
write (*, '(a,f8.2)') 'initial mean = ', average_temperature(plate)
call seed_hot_region(plate, 80.0_rk)
call set_diffusivity(0.25_rk)
write (*, '(a,f8.2)') 'heated mean = ', average_temperature(plate)
write (*, '(a,f8.2)') 'diffusivity = ', thermal_diffusivity
end program main
줄별 해설
부모 모듈의 use iso_fortran_env, only : rk => real64는 실수 종류 이름을 한곳에서 정한다. 그 아래의 implicit none은 선언하지 않은 이름을 오류로 다루게 한다. private는 이름을 추가할 때마다 공개 여부를 판단하게 만드는 기본 정책이다.
두 개의 public 문장은 타입과 절차를 공개한다. rk도 공개하므로 호출 프로그램이 모듈과 같은 종류의 실수 상수를 만들 수 있다. thermal_diffusivity는 선언문 자체에 public을 붙였으므로 목록에 다시 적지 않는다. 초기값은 0.10이지만 실행 프로그램이 설정 절차로 0.25로 바꾼다.
type :: plate_t 안의 private는 온도 성분을 감춘다. temperature(:, :)는 할당 가능한 2차원 배열이다. 판을 선언한 직후에는 아직 할당되지 않았으며, 초기화 절차가 실제 크기를 정한다. 공개 타입과 비공개 저장 공간을 함께 사용해 판의 표현을 호출 코드로부터 분리한다.
초기화 인터페이스의 intent(out)은 기존 내용을 이어서 고치는 대신 새 상태를 만든다는 뜻이다. 이미 초기화한 판을 다시 전달하면 진입 시 기존 할당 가능 성분이 해제된다. 따라서 이 구현에서는 이전 배열을 따로 해제하지 않고 새 크기로 할당할 수 있다. 기존 상태를 보존해야 하는 작업에 같은 선언을 사용하면 안 된다.
내부 온도 설정의 intent(inout)은 이미 존재하는 판의 일부를 바꾼다는 뜻이다. 평균 조회의 intent(in)은 입력 판을 정의하지 않겠다는 계약이다. 함수 결과인 mean_value도 부모 인터페이스에 실수 종류까지 선언되어 있다. 호출하는 쪽은 구현 파일을 읽지 않고 이 정보로 절차를 사용할 수 있다.
서브모듈의 초기화 구현은 두 차원이 모두 3 이상인지 검사한다. 내부 영역을 2:nx-1, 2:ny-1로 선택하므로 각 방향에 내부 칸이 하나 이상 있어야 한다. 검사에 실패하면 error stop으로 종료한다. 오류 경로의 진단 형식은 실행 환경마다 달라질 수 있으므로 정상 실행 결과에는 포함하지 않는다.
allocate는 배열 공간을 만들고 다음 대입문은 모든 칸을 같은 온도로 채운다. 크기와 초기값을 한 절차에서 함께 처리하므로 호출자가 할당만 하고 값을 넣지 않는 상태를 줄일 수 있다. 초기화가 끝나면 배열은 할당되어 있고 모든 원소가 정의되어 있다.
가열 구현에서는 allocated를 먼저 확인한다. 할당되지 않은 배열에 size를 적용하지 않도록 순서를 지킨다. 이어서 두 차원의 크기를 구하고 내부 영역만 대입한다. 가로 4칸, 세로 3칸이면 첫 첨자는 2와 3, 둘째 첨자는 2이므로 두 칸이 80도가 된다. 가장자리 열 칸은 20도를 유지한다.
평균 함수는 합계를 원소 수로 나눈다. size의 정수 결과를 real(..., rk)로 변환해 분모의 종류를 명시한다. 계산은 원소의 단순 산술 평균이며 모든 칸이 같은 가중치를 가진다고 가정한다. 서로 다른 면적의 격자를 나타내려면 이 조회 규약부터 다시 검토해야 한다.
재료 설정 구현은 검사 후 보호된 변수에 대입한다. 이 대입이 가능한 이유는 구현이 해당 변수를 정의한 모듈의 후손 서브모듈에 있기 때문이다. 같은 문장을 실행 프로그램에 옮기면 권한 경계 밖의 정의가 되어 허용되지 않는다.
실행 프로그램의 only 목록은 사용한 이름을 모두 보여 준다. 판 선언에는 바꾼 이름인 plate_type을 쓰고 초기화 호출에는 create_plate를 쓴다. 나머지 절차는 원래 이름을 유지한다. 세 출력문은 f8.2로 폭 8칸과 소수 둘째 자리까지의 표현을 지정한다.
실행 결과
세 소스를 한 번에 컴파일할 때도 부모를 앞에 둔다. 다음 명령은 자유 형식 소스에 Fortran 2018 표준 검사와 경고 옵션을 적용한다. 정상 입력을 사용하는 완성 코드는 이 옵션으로 경고 없이 컴파일되도록 구성되어 있다.
gfortran -std=f2018 -Wall thermal_model.f90 thermal_model_impl.f90 main.f90 -o plate_demo
./plate_demo
예상 출력은 다음과 같다. 등호 뒤의 공백에는 문자열의 마지막 공백과 f8.2가 채우는 공백이 함께 포함된다.
initial mean = 20.00
heated mean = 30.00
diffusivity = 0.25
초기 합계는 12칸 곱하기 20으로 240이다. 내부 두 칸을 각각 60도 높이면 합계는 360이 되고, 이를 12로 나누면 평균은 30이다. 열확산계수는 설정 절차가 저장한 0.25를 그대로 출력한다.
구현만 바꾸는 작업을 분리해 확인하려면 다음처럼 목적 파일을 만든다. 처음에는 세 소스를 모두 컴파일하고, 이후 서브모듈의 실행문만 수정했다면 두 번째 컴파일 명령과 마지막 연결 명령을 다시 실행한다.
gfortran -std=f2018 -Wall -c thermal_model.f90
gfortran -std=f2018 -Wall -c thermal_model_impl.f90
gfortran -std=f2018 -Wall -c main.f90
gfortran thermal_model.o thermal_model_impl.o main.o -o plate_demo
실무에서 자주 틀리는 것
공개된 보호 변수를 직접 바꾼다
다음 문장은 외부 코드에서 허용되지 않는다. protected 변수의 공개는 읽기 접근을 제공한다. 쓰기는 모듈이 제공하는 절차로 요청한다.
! 잘못된 코드: 실행 프로그램의 문장
thermal_diffusivity = 0.25_rk
! 고친 코드
call set_diffusivity(0.25_rk)
write (*, '(f8.2)') thermal_diffusivity
변경 가능한 다른 변수에 값을 복사한 뒤 그 복사본을 수정하는 것은 가능하다. 다만 복사본을 바꿔도 모듈의 설정은 바뀌지 않는다. 값의 복사와 원래 변수의 변경을 혼동하지 않아야 한다.
이름을 바꾼 뒤 원래 이름을 사용한다
이름 바꾸기의 왼쪽이 현재 코드에서 사용하는 이름이다. 다음 예제에서 initialize_plate는 이 문장으로 가져온 지역 이름이 아니다.
! 잘못된 코드
use thermal_model, only : create_plate => initialize_plate
! 다른 필요한 선언은 생략
call initialize_plate(plate, 4, 3, 20.0_rk)
! 고친 코드
use thermal_model, only : create_plate => initialize_plate
! 다른 필요한 선언은 생략
call create_plate(plate, 4, 3, 20.0_rk)
이 조각들은 이름 사용만 비교한다. 실제 실행 프로그램에서는 완성 코드처럼 타입과 rk도 가져와 선언해야 한다. 이름 바꾸기는 모듈 전체를 다른 이름으로 묶는 기능이 아니라 개별 가져오기 대상의 지역 이름을 정하는 기능이다.
비공개 성분을 호출 코드에서 수정한다
공개 타입의 변수를 선언할 수 있다고 내부 성분에도 접근할 수 있는 것은 아니다. 다음 대입은 실행 프로그램에서 허용되지 않는다.
! 잘못된 코드
plate%temperature(2:3, 2:2) = 80.0_rk
! 고친 코드
call seed_hot_region(plate, 80.0_rk)
새로운 온도 변경 방식이 필요하면 모듈에 적절한 절차를 추가한다. 그 절차가 검사할 조건과 변경할 영역을 정한다. 당장의 대입을 가능하게 하려고 성분을 공개하면 호출 코드가 저장 방식에 직접 의존하게 된다.
구현 파일을 연결에서 빠뜨린다
부모 모듈의 인터페이스는 호출 코드를 컴파일하는 데 필요한 정보를 제공하지만 실행문을 대신하지 않는다. 다음 명령은 구현의 목적 파일을 빠뜨렸으므로 연결 단계에서 필요한 절차를 찾지 못한다.
! 잘못된 명령
gfortran thermal_model.o main.o -o plate_demo
! 고친 명령
gfortran thermal_model.o thermal_model_impl.o main.o -o plate_demo
반대로 소스 컴파일에서는 부모 모듈이 먼저 필요하다. 연결에 포함할 파일 목록과 컴파일 의존 순서는 서로 다른 문제다. 부모 선언을 먼저 준비하고, 구현을 포함한 모든 목적 파일을 마지막에 연결한다.
한눈에 보기
| 도구 | 정하는 것 | 이번 코드의 적용 | 확인할 점 |
|---|---|---|---|
private | 이름의 접근 범위 | 기본 비공개, 온도 성분 비공개 | 모듈과 성분의 접근성을 구분한다. |
public | 외부에서 사용할 이름 | 타입, 종류, 네 절차 | 호출 코드가 의존할 계약이 된다. |
protected | 모듈 변수의 외부 정의 제한 | 열확산계수 조회 허용 | 값 검사는 설정 절차가 맡는다. |
only | 현재 범위로 가져올 이름 | 실행 프로그램의 사용 목록 | 비공개 이름을 공개하지 않는다. |
=> | 가져온 대상의 지역 이름 | 타입과 초기화 절차 이름 변경 | 왼쪽 이름으로 참조한다. |
submodule | 인터페이스와 구현의 배치 | 부모 선언과 구현 파일 분리 | 구현 목적 파일도 연결한다. |
공개할 이름을 정하는 일과 절차의 구현을 분리하는 일은 함께 사용할 수 있지만 서로 다른 결정이다. 파일을 나눠도 모든 성분을 공개하면 외부 의존이 넓어진다. 반대로 한 파일에 두더라도 기본 비공개 정책과 제한된 절차 목록으로 접근 경계를 만들 수 있다. 먼저 호출자가 해야 할 작업을 정하고, 그 계약이 안정된 뒤 구현 배치를 선택한다.
연습 문제
- 완성 코드의 판 크기를 5칸 곱하기 4칸으로 바꾼다. 초기 온도와 내부 온도는 유지한다. 실행 전에 가열 후 평균을 계산하고 예상 출력과 비교한다.
- 공개 조회 함수
maximum_temperature를 추가한다. 부모 모듈에 인터페이스를 선언하고 서브모듈에 구현한다. 초기화하지 않은 판은 기존 평균 함수와 같은 방식으로 처리한다. - 실행 프로그램에서
thermal_diffusivity를alpha라는 지역 이름으로 가져온다. 조회 출력과 설정 호출을 어떻게 고쳐야 하는지 설명한다. - 초기화한 판을 다른 크기로 다시 초기화하는 코드를 추가한다.
initialize_plate의intent(out)을intent(inout)으로 바꾸고 구현을 그대로 두면 어떤 문제가 생기는지 설명한다.
정답과 해설
1. 크기를 바꾸었을 때의 평균
전체 칸 수는 20이고 내부 칸 수는 (5-2)*(4-2)로 6이다. 나머지 14칸은 20도, 내부 6칸은 80도다. 합계는 14*20 + 6*80 = 760이고 평균은 38이다. 초기 평균과 열확산계수는 그대로다.
call create_plate(plate, 5, 4, 20.0_rk)
initial mean = 20.00
heated mean = 38.00
diffusivity = 0.25
2. 최고 온도 조회 함수
부모 모듈의 공개 목록에 함수 이름을 추가하고 기존 interface 블록 안에 다음 선언을 넣는다. 결과 종류도 기존 조회 함수와 같게 유지한다.
public :: maximum_temperature
! 기존 interface 블록 안에 추가
module function maximum_temperature(plate) result(max_value)
type(plate_t), intent(in) :: plate
real(rk) :: max_value
end function maximum_temperature
서브모듈의 contains 뒤에는 다음 구현을 추가한다. 할당 여부를 확인한 뒤 배열 전체의 최댓값을 구한다.
module procedure maximum_temperature
if (.not. allocated(plate%temperature)) then
error stop 'plate is not initialized'
end if
max_value = maxval(plate%temperature)
end procedure maximum_temperature
실행 프로그램의 only 목록에도 이름을 추가한다. 내부 온도를 설정한 뒤 maximum_temperature(plate)를 f8.2로 출력하면 수치 부분은 80.00이다. 온도 배열을 공개하지 않고도 필요한 조회 기능을 늘릴 수 있다.
3. 보호된 변수의 지역 이름
기존 only 목록의 변수 항목을 다음처럼 바꾼다. 출력문은 지역 이름을 사용하고, 설정 절차 호출은 그대로 둔다. 지역 이름을 바꿔도 원래 변수의 보호 속성은 유지된다.
! only 목록의 변수 항목
alpha => thermal_diffusivity
! 실행문
call set_diffusivity(0.25_rk)
write (*, '(a,f8.2)') 'diffusivity = ', alpha
alpha = 0.25_rk는 여전히 허용되지 않는다. 이름 바꾸기는 접근 권한을 바꾸는 수단이 아니다.
4. 재초기화와 인수 의도
기존 초기화 호출 뒤에 다음 두 문장을 넣으면 판은 3칸 곱하기 3칸으로 다시 만들어지고 평균은 10이 된다. 재초기화 시 기존 온도는 보존되지 않는다.
call create_plate(plate, 3, 3, 10.0_rk)
write (*, '(a,f8.2)') 'reset mean = ', average_temperature(plate)
intent(out)에서는 진입 시 기존 할당 가능 성분이 해제되므로 새 allocate가 가능하다. intent(inout)으로 바꾸면 기존 할당이 유지된다. 구현을 그대로 두고 재초기화하면 이미 할당된 배열을 다시 할당하려 하므로 오류가 발생한다. 그 의도를 선택한다면 할당 여부를 검사하고 기존 공간을 해제하거나 재사용하는 처리를 구현해야 한다.
공개 절차의 선언은 호출 방법뿐 아니라 기존 상태를 어떻게 취급할지도 정한다. 판의 초기화와 조회를 이렇게 명시해 두면 이후 계산 기능을 추가할 때도 상태 변경의 경로를 같은 기준으로 검토할 수 있다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.