제 5 부 — 자료 구조와 파일



14장 · 문자열과 파일 입출력


Text, Records, and Persistence



[!attention] 이 장의 목표


지금까지는 수치 연산 결과를 컴퓨터 화면에 단순히 출력하거나, 6장에서 다룬 기초적인 서식 제어를 통해 csv 파일로 저장하는 것에 집중했다. 그러나 실무에서는 그 반대 방향, 즉 외부에 이미 존재하는 데이터 파일(실험, 관측 자료)을 프로그램 내부로 정밀하게 읽어 들이는 공정(data ingestion)이 훨씬 많다.

이 장에서는 다음의 내용을 배운다.


[!important] 14.1 문자형 연산과 내장 함수

Fortran의 기본 문자형(character)은 선언 시점에 크기가 결정되는 고정 길이(fixed-length)를 기본으로 채택하고 있다. 예를 들어 character(len=10) :: name으로 선언한 변수는 메모리 내에 정확히 10 글자 만을 담을 수 있는 정적 메모리 공간으로 할당된다.

이 공간에 10자보다 더 짧은 문자열을 대입하면, 시스템은 남은 빈 공간의 오른쪽을 모두 공백(space)으로 채워 넣는다. 만약 선언된 len보다 더 긴 문자열을 대입하면 상자를 초과하는 뒷부분은 잘려 나간다(truncation).

문자열 기본 제어 규격과 특성

문자열을 선언하고, 결합하고, 필요한 영역만 추출하는 기술은 파일 이름을 동적으로 생성하거나 로그 메시지를 기록하고 설정 파일(namelist)의 경로 데이터를 파싱할 때 필수적으로 요구된다. 각 제어 단위의 구체적인 작동 방식은 다음과 같다.

부분 문자열 인덱스 접근 시 주의 사항이 있다.

[예제] 문자열 검색·정렬 내장 함수와 파일 확장자 정밀 추출

외부 입력 파이프라인 설계나 데이터 가공 시 빈번하게 수행하는 작업 중 하나는 파일 경로 분석과 포맷 정렬이다.

이 예제는 다양한 문자열 내장 함수를 활용하여 데이터 경로 문자열의 형식을 분석하고, 파일명의 마지막 점(.)의 위치를 역방향으로 찾아 확장자만 잘라내는 설계 기법을 보여준다.

%%writefile char_basics.f90
program char_basics
   implicit none
   character(len=10) :: first = "Grace"
   character(len=10) :: last  = "Hopper"
   character(len=21) :: full

   full = trim(first) // " " // trim(last)

   print *, "first      = [", first, "]"
   print *, "len(first) = ", len(first)
   print *, "trim       = [", trim(first), "]"
   print *, "full       = [", trim(full), "]"
   print *, "substring  = [", full(1:5), "]"
   print *, "len_trim(first)   = ", len_trim(first)
end program char_basics

컴파일·실행:

!gfortran -O2 -std=f2018 -Wall char_basics.f90 -o char_basics
!./char_basics

실행 결과:

 first      = [Grace     ]
 len(first) =           10
 trim       = [Grace]
 full       = [Grace Hopper]
 substring  = [Grace]
 len_trim(first) =              5

Fortran이 제공하는 핵심 문자열 내장 함수들의 작동 방식은 다음과 같다.

[예제] 검색·정렬 함수와 확장자 추출

character(len=*) 형태의 가변 정적 상수 선언과 역방향 탐색 지시어인 back=.true.의 역할을 알아 보자.

%%writefile char_intrinsics.f90
program char_intrinsics
   implicit none
   character(len=*), parameter :: text = "  fortran 2018  "
   character(len=*), parameter :: path = "data/input.csv"
   integer :: dot

   print *, "len      =", len(text)
   print *, "len_trim =", len_trim(text)
   print *, "adjustl  = [", trim(adjustl(text)), "]"
   print *, "index    =", index(path, ".")
   print *, "scan     =", scan(path, "/.")
   print *, "verify   =", verify("12345x", "0123456789")
   print *, "repeat   = [", repeat("ab", 3), "]"

   dot = index(path, ".", back=.true.)
   print *, "extension= [", path(dot+1:), "]"
end program char_intrinsics

컴파일·실행:

!gfortran -O2 -std=f2018 -Wall char_intrinsics.f90 -o char_intrinsics
!./char_intrinsics

실행 결과:

 len      =          16
 len_trim =          14
 adjustl  = [fortran 2018]
 index    =          11
 scan     =           5
 verify   =           6
 repeat   = [ababab]
 extension= [csv]

[!note] 프로그램 하단의 아래 두 구문은 외부 데이터 파일들을 다룰 때 항상 재활용되는 코드 패턴이다.

Fortran dot = index(path, ".", back=.true.) print *, "extension= [", path(dot+1:), "]"


[!important] 14.2 가변 길이 할당 가능 문자열

현대 Fortran은 고정 길이의 한계를 극복하고, 파일 경로나 외부 텍스트 입력처럼 실행 전에는 길이를 전혀 예측할 수 없는 데이터를 메모리 낭비 없이 다룰 수 있는 가변 길이 할당 가능 문자열(allocatable character) 을 지원한다.

가변 길이 할당 가능 문자열의 문법 규격은 아래와 같다.

character(len=:), allocatable :: s

[예제] 동적 문자열 출력

대입문이 가동될 때마다 변수 s의 문자열 크기가 어떻게 변하는지 확인해 보자.

%%writefile string_test.f90
program string_test
   implicit none
   character(len=:), allocatable :: s
   
   ! 문자열 대입 (s는 길이가 5인 문자열로 자동 할당됨)
   s = "Hello"
   print *, s, " (길이: ", len(s), ")"
   
   ! 더 긴 문자열 대입 (s는 길이가 13인 문자열로 자동 재할당됨)
   s = "Hello, Fortran"
   print *, s, " (길이: ", len(s), ")"
end program string_test

컴파일·실행:

!gfortran -O2 -std=f2018 -Wall string_test.f90 -o string_test
!./string_test

실행 결과:

 Hello  (길이:            5 )
 Hello, Fortran  (길이:           14 )

가변 길이 문자열의 주요 특징은 다음과 같다.

가변 길이 문자열 역시 메모리 할당(allocatable)이 필수적이다. 우변 대입을 통해 자동 할당되도록 하거나, allocate 구문으로 메모리 공간을 미리 확보한 뒤 사용해야 한다. 메모리가 할당되지 않은 상태에서 읽기나 연산을 시도하면 세그멘테이션 오류(segmentation fault)가 발생하므로, 작업 전 변수의 할당 상태를 반드시 확인해야 한다.

[예제] 대입과 결합 연산(//)에 따른 가변 길이 문자열의 동작 방식

기존 고정 길이 문자열의 공백 채움이나 글자 잘림 없이, 데이터의 실제 길이와 결합 연산에 따라 메모리 크기를 유연하게 변형하는 가변 문자열 예제이다.

name 변수의 데이터 대입에 따른 길이 변화와 do 루프 안에서 greeting 변수가 점차 확장되는 과정을 살펴보자.

%%writefile alloc_string.f90
program alloc_string
   implicit none
   character(len=:), allocatable :: name
   character(len=:), allocatable :: greeting
   integer :: i

   name = "Ada"
   print *, "len =", len(name), " [", name, "]"

   name = "Margaret"
   print *, "len =", len(name), " [", name, "]"

   greeting = ""
   do i = 1, 3
      greeting = greeting // "ha"
   end do
   print *, "len =", len(greeting), " [", greeting, "]"
end program alloc_string

컴파일·실행:

!gfortran -O2 -std=f2018 -Wall alloc_string.f90 -o alloc_string
!./alloc_string

실행 결과:

 len =           3  [Ada]
 len =           8  [Margaret]
 len =           6  [hahaha]

[!important] 14.3 입출력 제어 open, close, inquire, newunit

외부 자원과의 데이터 교환은 연결(open) $\rightarrow$ 전송(read/write) $\rightarrow$ 해제(close)의 3단계를 거치며 수행된다. Fortran의 입출력 제어 구문은 다음과 같이 구분된다.

이를 구현하기 위한 파일 제어 구문 규격은 다음과 같다.

! 1. 파일 열기 및 장치 연결
open(newunit = u, file = 이름 [, status = 상태] [, action = 동작] [, form = 형식] [, access = 접근] [, iostat = 정수변수] [, iomsg = 문자변수])

! 2. 파일 닫기 및 장치 해제
close(u)

! 3. 파일 물리 정보 조회
inquire(file = 이름, exist = 논리변수 [, size = 정수변수] [, opened = 논리변수])

입출력 운용 시 주의 사항은 다음과 같다.

파일 동작 지정자(specifier)의 역할과 설정 규격

파일 지정자(specifier)는 파일을 열거나 상태를 조회할 때, 해당 파일의 성격과 메모리 내 처리 방식을 컴파일러에 상세히 규정하는 설정 옵션이다. 파일 제어 명령어의 동작 방식을 구체적으로 지시하여 입출력 시스템의 유연성과 안전성을 확보하는 역할을 한다.

주요 지정자 매개변수들의 역할과 구체적인 분기 값들은 다음과 같다.

1. status (파일 상태 지정자): 파일의 초기 상태를 규정하여 기존 데이터를 보호하거나 임시 자원을 제어한다.

2. action (작업 권한 지정자): 파일에 대한 입출력 작업 권한을 제한하여 부주의한 데이터 손실이나 파일 훼손을 방지한다.

3. iostat (상태 변수 리턴): 파일 입출력 명령 실행 후 작업의 성공 여부를 나타내는 고유 정수 코드를 반환한다.

4. iomsg (오류 메시지 문자열 리턴)

[예제] 파일에 쓰기 및 입출력 오류 방어

수치 해석이나 시뮬레이션 데이터를 물리 디스크에 안전하게 저장하는 동시에, 장치 연결 중에 발생할 수 있는 돌발적인 문제(디스크 용량 초과, 권한 부재 등)를 사전에 통제하는 파일 쓰기 예제이다.

파일 열기(open)가 실패했을 때 iostatiomsg를 통해 비정상 종료를 강제 우회하는 예외 구조를 눈여겨보자.

%%writefile open_write.f90
program open_write
   implicit none
   integer :: u, ios
   character(len=200) :: msg

   open(newunit=u, file="output.txt", status="replace", &
        action="write", iostat=ios, iomsg=msg)
   if (ios /= 0) then
      print *, "open failed: ", trim(msg)
      error stop 1
   end if

   write(u, '(a)') "first line"
   write(u, '(a)') "second line"
   close(u)

   print *, "output.txt written"
end program open_write

컴파일·실행:

!gfortran -O2 -std=f2018 -Wall open_write.f90 -o open_write
!./open_write

실행 결과:

 output.txt written

!cat output.txt 명령을 사용하여 파일 내부를 확인하면 아래와 같다.

 first line 
 second line

[!note] 실무 개발 시 반드시 내재화할 방어 규칙

[예제] inquire를 활용한 파일 존재 여부 및 물리적 크기 사전 진단

inquire 문은 파일을 실제로 열지 않고도, 해당 파일의 존재 유무, 접근 속성, 물리적 장치 상태 및 바이트 크기 등을 미리 스캔하는 편리한 진단 도구이다.

대규모 수치 시뮬레이션 가동 전 외부 격자 데이터 파일이 올바른 경로에 배치되어 있는지 사전 검증함으로써, open 시 발생할 수 있는 시스템 런타임 충돌을 원천적으로 차단할 수 있다.

inquire 제어문이 물리 파일의 존립 상태에 따라 어떠한 내부 값을 반환해 주는지 분석해 보자.

%%writefile inquire_demo.f90
program inquire_demo
   implicit none
   logical :: found
   integer :: fsize

   inquire(file="output.txt", exist=found, size=fsize)
   if (found) then
      print *, "output.txt exists, size =", fsize, "bytes"
   else
      print *, "output.txt not found"
   end if

   inquire(file="missing.txt", exist=found)
   print *, "missing.txt exists:", found
end program inquire_demo

컴파일·실행:

!gfortran -O2 -std=f2018 -Wall inquire_demo.f90 -o inquire_demo
!./inquire_demo

실행 결과:

 output.txt exists, size =          23 bytes
 missing.txt exists: F

텍스트 데이터의 용량을 계산할 때, 파일에 작성된 문자열의 순수 길이를 합산하면 다음과 같다. 첫 번째 줄 "first line"은 순수 알파벳과 공백을 합쳐 10바이트를 차지한다. 두 번째 줄 "second line"은 순수 알파벳과 공백을 합쳐 11바이트를 차지한다. 합산하면 21바이트이다.

그러나 inquire 구문을 통해 운영체제 수준에서 확인한 파일의 실제 물리 크기는 23바이트로 계산된다. 이는 텍스트 파일 저장 시 각 행의 끝에 눈에 보이지 않는 줄 바꿈(개행) 문자가 개당 1바이트씩 추가되어 있기 때문이다.

즉, 첫 번째 줄 10바이트에 개행 문자 1바이트가 더해져 11바이트를 형성한다. 두 번째 줄 11바이트에 개행 문자 1바이트가 추가되어 12바이트가 된다. 최종적으로 총 23바이트의 크기가 된다.

존재하지 않는 파일인 missing.txt에 대해서 시스템은 부재 판정을 내리고 exist 매개변수에 거짓(.false.)을 반환한다.

프로그램 동작의 안정성을 확보하기 위해 파일 자원의 실제 존재 여부를 확인하는 검증 로직을 반드시 전면에 배치해야 한다.

[!warning] [흔한 실수] 왜 exist를 먼저 확인해야 하는가?

흔히 저지르는 치명적인 실수는, 파일의 존재여부를 확인하지 않고 파일 크기(size)나 장치 정보 값부터 믿고 꺼내어 연산에 사용하는 것이다.

본격적인 데이터 조회나 계산을 수행하기 전에 "안전하게 진입할 수 있는 통로가 완벽하게 확보되었는가?"를 먼저 검사하는 프로그래밍을 '가드(guard)패턴'이라고 한다. fortran inquire(file="output.txt", exist=found, size=fsize) ! exist로 파일의 존재 여부 확인 if (found) then ! .true.일 때만 size를 조회 print *, "output.txt exists, size =", fsize, "bytes" else ! .false.이면 size 조회를 건너뛰고 "파일이 없음"을 알림 print *, "output.txt not found" end if
만약 파일이 물리적으로 부재한 상황에서 가드 조건문 없이 곧바로 fsize 변수의 값을 읽어 연산이나 메모리 할당(예: 파일 크기만큼 배열 할당)의 인자로 투입하면 정의되지 않은 동작(undefined behavior), 쓰레기 값(garbage value) 유입, 시스템 크래시(crash) 를 유발할 수 있다.

따라서 아무리 사소한 파일 읽기라도 존재 여부(exist)를 가장 먼저 확인하여 오류 상황을 철저히 배제한 뒤, 참인 경로 안에서만 세부 정보(size, opened 등)를 가공하고 다루는 습관을 기르자.


[!important] 14.4 입출력 접근 방식(Access)과 저장 형식(Form)

Fortran의 파일 입출력 파이프라인은 데이터를 다루는 논리적 경로인 접근 방식(access)과 메모리 데이터를 디스크에 기록하는 물리적 구조인 저장 형식(form)으로 구성된다. 이 두 속성의 조합에 따라 데이터의 처리 속도, 저장 용량, 시스템 간의 호환성이 결정된다.

1. 접근 방식 (access 지정자): 파일 내부의 데이터 포인터를 이동시키며 레코드에 접근하는 방식을 결정하는 설정 옵션이다.

2. 저장 형식 (form 지정자): 메모리에 상주하는 데이터를 디스크에 어떠한 형태로 기록할 것인지 규정하는 설정 옵션이다.

위 두 가지 속성을 조합하여 다음 용도로 사용할 수 있다.

[!note] 파일 포인터 제어 명령: rewindbackspace 순차 접근 모드로 열린 파일 장치 내부에서 데이터의 읽기/쓰기 위치를 제어하기 위해 Fortran은 직관적인 파일 위치 관리 명령어를 제공한다.

이 제어 도구들을 정밀하게 조합하여 데이터 스트리밍 인프라를 구축할 수 있다.

[예제] 비서식(unformatted) 입출력을 활용한 데이터 고속 입출력 및 복원

대규모 배열 데이터를 서식화된 텍스트로 다루면 데이터 입출력 시 서식 변환 오버헤드로 인해 처리 속도가 저하된다.

본 예제에서는 부동소수점 수치를 별도의 문자열 변환 없이 이진 비트 구조 그대로 저장하고 복원하는 비서식 스트림 입출력 방식에 대해 알아본다. access="stream"form="unformatted" 지정자를 조합하여 입출력 성능을 최적화한다.

%%writefile stream_io.f90
program stream_io
   use iso_fortran_env, only: real64
   implicit none
   integer :: u, i
   real(real64) :: v
   real(real64) :: out(5) = [1.0_real64, 2.0_real64, 3.0_real64, &
                             4.0_real64, 5.0_real64]

   open(newunit=u, file="values.bin", access="stream", &
        form="unformatted", status="replace", action="write")
   write(u) out
   close(u)

   open(newunit=u, file="values.bin", access="stream", &
        form="unformatted", status="old", action="read")
   do i = 1, 5
      read(u) v
      print '("v(",i0,") = ",f6.2)', i, v
   end do
   close(u)
end program stream_io

컴파일·실행:

!gfortran -O2 -std=f2018 -Wall stream_io.f90 -o stream_io
!./stream_io

실행 결과:

v(1) =   1.00
v(2) =   2.00
v(3) =   3.00
v(4) =   4.00
v(5) =   5.00

비서식(Unformatted) 입출력 방식의 주요 특징 및 장점

[!note] 컴퓨터 간의 데이터 이식성 한계

비서식(Unformatted) 이진 파일은 입출력 속도와 메모리 효율성이 뛰어나지만, 서로 다른 하드웨어 환경 간 데이터를 교환할 때 호환성(이식성) 문제가 발생할 수 있다.


[!important] 14.5 입출력 오류 예외 처리

외부 환경 설정 파일이나 관측 장비의 원시 텍스트를 읽어 들일 때는 파일 서식의 오류나 예상치 못한 파일 끝(EOF) 도달 등 다양한 예외 상황이 발생할 수 있다. Fortran은 iostat 지정자와 예외 질의 함수를 활용하여, 런타임 오류 발생 시 전체 계산 프로세스가 비정상 종료되는 것을 방지하고 사전에 정의된 예외 처리 시나리오에 따라 프로그램을 안전하게 제어하는 메커니즘을 제공한다.

아래와 같이 readwrite 제어문에 iostat = 정수변수 지시자를 명시하면 입출력 작업의 성공 및 오류 상태가 해당 변수에 정수 코드로 즉시 전달된다.

integer :: ios
! 파일을 읽되, 오류가 발생해도 프로그램이 멈추지 않음
read(u, *, iostat=ios) my_var

if (ios /= 0) then
   ! 오류 처리 로직 수행
end if

iostat 지정자가 오류 발생 시 반환하는 0이 아닌 정수형 에러 코드는 컴파일러종류에 따라 다르다. 따라서 특정 숫자 상수를 하드 코딩하여 오류 종류를 식별하는 방식은 시스템 이식성을 크게 저해한다. 이를 해결하기 위해, 아래의 파일 상태를 검증하는 표준 질의 함수를 사용한다.

이런 예외 제어 기법을 조합하여 아래와 같은 데이터 입력 시스템을 구축할 수 있다.

[예제] 파일 종단(EOF) 감지를 이용한 가변 데이터 판독 구조

대용량 관측 시계열 데이터나 시스템 센서 로그 파일은 실행 시점에 전체 행(line) 수를 미리 알 수 없는 경우가 많다. 이와 같이 데이터 크기가 유동적인 텍스트 파일을 안전하게 읽어 오기 위해, do 무한 루프 내에서 한 줄씩 입출력을 수행하고 표준 질의 함수인 is_iostat_end로 파일 끝(EOF)을 검지하여 정상 탈출하는 방식이 널리 사용된다.

%%writefile read_lines.f90
program read_lines
   implicit none
   integer :: u, ios, count
   character(len=256) :: line

   open(newunit=u, file="output.txt", status="old", action="read")
   count = 0
   do
      read(u, '(a)', iostat=ios) line
      if (is_iostat_end(ios)) exit
      if (ios /= 0) then
         print *, "read error code:", ios
         exit
      end if
      count = count + 1
      print '(i0,": ",a)', count, trim(line)
   end do
   close(u)

   print *, "total lines =", count
end program read_lines

컴파일·실행:

!gfortran -O2 -std=f2018 -Wall read_lines.f90 -o read_lines
!./read_lines

실행 결과:

1: first line
2: second line
 total lines =           2

[!important] 14.6 외부 파일을 통한 런타임 제어: namelist

수치 시뮬레이션의 격자 간격, 총 타임스텝, 물리 상수 등의 경계 조건을 변경할 때마다 소스 코드를 수정하고 재컴파일하는 방식은 대규모 과학 계산 환경에서 비효율적이다. Fortran의 namelist 기능은 핵심 제어 변수들을 하나의 논리적 그룹으로 묶어 관리함으로써, 재컴파일 없이 실행 시점에 외부 설정 텍스트 파일만 수정하여 매개변수를 동적으로 제어할 수 있게 해준다.

namelist를 프로그램 내부에 적용하고 외부 데이터를 읽어오는 과정은은 다음과 같다.

표준 설정 파일(config.nml)의 물리적 구조

외부에서 프로그램을 제어하는 설정 파일(통상 .nml 또는 .cfg 확장자 사용)은 텍스트 편집기 기반의 구조를 가진다. 파일의 시작 부분은 &그룹이름으로 명시하고, 내부에 변수명 = 값 형태로 실험 조건을 할당한 뒤, 끝부분에 /를 배치하여 데이터 블록을 닫아준다. 수치 모델 등에서 표준적으로 사용하는 외부 설정 파일의 포맷 예시는 다음과 같다.

&sim_config
   n_steps = 500
   dt = 0.005
   gravity = 9.80665
/

namelist 기법의 장점과 동작 원리는 다음과 같다.

결과적으로 namelist를 적용하면 프로그램의 연산 로직과 실행 환경 설정이 분리되어 소프트웨어 구조적 안정성을 확보할 수 있다.

[예제] 네임리스트(Namelist)를 활용한 런타임 매개변수 동적 제어

수치 시뮬레이션 프로그램 설계 시 안정성을 확보하는 방법 중 하나는 외부 설정 파일의 읽기 실패나 항목 누락 상황에서도 프로그램이 오작동 없이 실행을 유지하도록 구조화하는 것이다.

본 예제에서는 코드 내부에 기본값(default value)을 사전에 정의한 뒤, 외부 네임리스트 데이터를 읽어 와 해당 매개변수를 동적으로 업데이트하는 방어적 프로그래밍 방식을 제시한다. 파일 개설(open)과 데이터 읽기(read) 단계에서 발생하는 입출력 예외를 안전하게 처리하고 우회하는 제어 구조를 살펴보자.

%%writefile namelist_demo.f90
program namelist_demo
   use iso_fortran_env, only: real64
   implicit none
   integer  :: n_steps
   real(real64) :: dt, gravity
   integer  :: u, ios
   character(len=200) :: msg

   namelist /sim_config/ n_steps, dt, gravity

   n_steps = 100
   dt      = 0.01_real64
   gravity = 9.81_real64

   open(newunit=u, file="config.nml", status="old", &
        action="read", iostat=ios, iomsg=msg)
   if (ios /= 0) then
      print *, "cannot open config: ", trim(msg)
      print *, "using defaults"
   else
      read(u, nml=sim_config, iostat=ios, iomsg=msg)
      if (ios /= 0) print *, "namelist warning: ", trim(msg)
      close(u)
   end if

   print *, "n_steps =", n_steps
   print *, "dt      =", dt
   print *, "gravity =", gravity
end program namelist_demo

컴파일·실행:

!gfortran -O2 -std=f2018 -Wall namelist_demo.f90 -o namelist_demo
!./namelist_demo

실행 결과:

cannot open config: Cannot open file 'config.nml': No such file or directory
using defaults 
n_steps = 100 
dt = 1.0000000000000000E-002 
gravity = 9.8100000000000005

물리적 디스크 상에 설정 파일(config.nml)이 존재하지 않는 예외 상황이 발생했으나, 프로그램이 비정상 종료되지 않고 사전에 정의된 기본값을 유지하며 실행을 지속한다.

외부 설정 파일의 값(n_steps = 500, dt = 0.005 등)을 적용하여 실행하려면, 현재 실행 디렉터리에 config.nml 파일이 존재해야 한다.

%%writefile config.nml
&sim_config
   n_steps = 500
   dt = 0.005
   gravity = 9.80665
/

설정 파일 생성 후 ./namelist_demo를 재실행하면, 시스템은 기본값 대신 config.nml 파일의 매개변수를 자동으로 매칭하여 다음과 같이 변경된 값을 출력한다.

n_steps = 500 
dt = 5.0000000000000001E-003
gravity = 9.8066499999999994

[예제] 외부 데이터 파일을 활용한 이동평균 산출

본 예제는 가변적인 csv 파일 데이터를 판독하여 3점 중심 이동평균(moving average) 필터로 노이즈를 제거한 뒤 결과 데이터를 저장하는 처리 과정을 나타낸다. read 구문으로 csv 파일의 쉼표 구분자를 분리하는 방식과 rewind 명령을 통해 파일 포인터를 첫 위치로 되돌려 동적 배열을 할당하는 제어 흐름을 다룬다.

먼저 날짜와 일별 온도 데이터로 구성된 sensor.csv 파일을 작업 디렉터리에 준비한다. 파일의 내부 구조는 day,temperature 헤더 아래에 14개 행의 측정값이 순차적으로 나열된 형태를 취한다.

%%writefile sensor.csv
day,temperature
1,12.4
2,13.1
3,15.8
4,14.2
5,16.9
6,18.3
7,17.5
8,19.1
9,20.4
10,18.8
11,21.2
12,22.6
13,20.9
14,23.4

제시된 프로그램은 sensor.csv 파일을 1차적으로 탐색하여 총 표본 수를 측정한 뒤 이에 맞추어 동적 배열을 할당한다. 이후 데이터를 배열에 로드하고 3점 이동평균 연산을 수행하여 평활화된 결과값을 smoothed.csv에 기록한다.

%%writefile moving_avg.f90
program moving_avg
   use iso_fortran_env, only: real64
   implicit none
   integer, parameter :: half = 1          ! half-width (1 = 3-point window)
   integer :: u, ios, n, i, lo, hi
   integer,      allocatable :: day(:)
   real(real64), allocatable :: temp(:), smooth(:)
   character(len=200) :: header
   integer :: d
   real(real64) :: t

   ! pass 1: count samples (skip one header line)
   open(newunit=u, file="sensor.csv", status="old", action="read")
   read(u, '(a)') header
   n = 0
   do
      read(u, *, iostat=ios) d, t
      if (is_iostat_end(ios)) exit
      n = n + 1
   end do

   allocate(day(n), temp(n), smooth(n))

   ! pass 2: read actual values
   rewind(u)
   read(u, '(a)') header
   do i = 1, n
      read(u, *) day(i), temp(i)
   end do
   close(u)

   ! centered moving average (edges use whatever fits)
   do i = 1, n
      lo = max(1, i - half)
      hi = min(n, i + half)
      smooth(i) = sum(temp(lo:hi)) / real(hi - lo + 1, real64)
   end do

   ! write result as csv
   open(newunit=u, file="smoothed.csv", status="replace", action="write")
   write(u, '(a)') "day,raw,smooth"
   do i = 1, n
      write(u, '(i0,",",f8.3,",",f8.3)') day(i), temp(i), smooth(i)
   end do
   close(u)

   print '("samples read: ",i0)', n
   print '("smoothed.csv written")'
end program moving_avg

컴파일·실행:

!gfortran -O2 -std=f2018 -Wall moving_avg.f90 -o moving_avg
!./moving_avg

실행 결과:

samples read: 14
smoothed.csv written

이동 평균이 제대로 저장되었는지!head ./smoothed.csv 명령으로 확인해 보자.

day,raw,smooth 
1, 12.400, 12.750 
2, 13.100, 13.767 
3, 15.800, 14.367 
4, 14.200, 15.633 
5, 16.900, 16.467 
6, 18.300, 17.567 
7, 17.500, 18.300 
8, 19.100, 19.000 
9, 20.400, 19.433

마지막으로 "smoothed.csv"를 시각화 해 보자.

import csv
import matplotlib.pyplot as plt

day, raw, smooth = [], [], []
with open("smoothed.csv") as f:
    reader = csv.reader(f)
    next(reader)                      # skip header
    for row in reader:
        day.append(int(row[0]))
        raw.append(float(row[1]))
        smooth.append(float(row[2]))

plt.figure(figsize=(8, 5))
plt.plot(day, raw, "o--", alpha=0.5, label="raw")
plt.plot(day, smooth, "-", linewidth=2, label="3-point average")
plt.xlabel("day"); plt.ylabel("temperature")
plt.title("Fortran smoothing, Python plotting")
plt.legend(); plt.grid(True)
plt.savefig("smoothed.png", dpi=120)
plt.show()

아래 그림을 보면, 연한 점선과 원형 마커(o--)로 표현된 원시 기온 데이터(raw)는 센서 특유의 노이즈가 포함되어 있어 데이터의 거시적 경향성을 식별하기 어렵다. 반면, 3점 이동평균 필터를 거친 굵은 실선(3-point average) 데이터는 단기 변동성과 국소적 노이즈가 효과적으로 제거되어, 시간에 따른 전체적인 기온 상승 곡선이 완만하고 명확한 궤적으로 도출된다.

![[Pasted image 20260714181245.png]]


[!important] 오류 학습

관측 데이터 파일의 첫 행에 포함된 열 이름(header)은 주요 메타데이터 정보를 담고 있으나, 수치 계산을 수행하는 연산부에서는 다루지 않는 문자열이다. 수치 데이터를 읽어 들이기 전 이런 헤더 행을 미리 스킵(skip)하지 않고 수치형 변수로 직접 파싱을 시도하면, 문자열을 수치로 변환하지 못해 런타임 오류가 발생하고 프로그램이 중단된다.

[예제] 헤더 누락 파일 연결 시 런타임 crash

본 예제에서는 헤더 행 누락 시 나타나는 예외 메커니즘을 확인하고, 사전 헤더 스킵 처리 구문을 통해 프로그램의 실행 안정성을 확보하는 방식을 다룬다.

%%writefile skip_header.f90
program skip_header
   implicit none
   integer :: u, d
   real :: t

   open(newunit=u, file="sensor.csv", status="old", action="read")
   read(u, *) d, t          ! first line is the header "day,temperature"
   print *, d, t
   close(u)
end program skip_header

컴파일:

!gfortran -O2 -std=f2018 -Wall skip_header.f90 -o skip_header

실행:

!./skip_header

실행하면 다음과 같이 에러를 출력하고 프로그램은 멈춘다.

At line 7 of file skip_header.f90
Fortran runtime error: Bad integer for item 1 in list input

Error termination. Backtrace: 
#0 0x7cf7ce575960 in ???
#1 0x7cf7ce5764d9 in ???
#2 0x7cf7ce7c05eb in ???
#3 0x7cf7ce7c35aa in ??? 
#4 0x7cf7ce7c42aa in ???
#5 0x5b16160852f6 in MAIN__ 
#6 0x5b161608511e in main

문법적 결함이 없으므로 컴파일 단계는 정상적으로 완료된다. 그러나 프로그램 실행 시 read(u, *) d, t 구문이 작동하는 과정에서 첫 번째 변수 d가 정수형(integer)으로 선언되어 있어 정수 데이터의 입력을 대기한다.

파일의 첫 행에 존재하는 텍스트가 영문 헤더인 "day,temperature"인 경우, 첫 번째 항목인 "day"를 정수로 변환하는 과정에서 형변환 규격 위반이 발생한다. 이에 따라 시스템은 Bad integer for item 1 에러를 반환하고 실행을 비정상 종료하며, 해당 오류의 item 1은 정수 변수 d를 가리킨다.

안전 설계: 헤더 스킵 및 입출력 예외 방어 로직 적용

이런 예외 상황을 방지하기 위해 두 가지 방어적 설계를 적용한다.

첫째, 데이터 파싱을 수행하기 전에 헤더 판독용 문자열 변수(header)를 활용하여 첫 번째 행의 헤더 텍스트를 사전에 읽어 건너뛴다.

둘째, 데이터 읽기 블록에 iostat 지정자를 지정하여 입출력 예외 발생 시 프로그램이 즉시 중단되지 않도록 예외 처리 제어 흐름을 구축한다.

%%writefile skip_header_fixed.f90
program skip_header_fixed
   implicit none
   integer :: u, d, ios
   real :: t
   character(len=200) :: header

   open(newunit=u, file="sensor.csv", status="old", action="read")
   read(u, '(a)') header          ! consume the header line first
   read(u, *, iostat=ios) d, t
   if (ios /= 0) then
      print *, "read failed, code =", ios
   else
      print *, d, t
   end if
   close(u)
end program skip_header_fixed

컴파일·실행:

!gfortran -O2 -std=f2018 -Wall skip_header_fixed.f90 -o skip_header_fixed
!./skip_header_fixed

실행 결과:

           1   12.3999996

read(u, '(a)') header 구문이 파일 첫 행의 헤더 문자열을 판독하여 소모함에 따라, 다음 read 구문은 수치 데이터가 시작되는 두 번째 행으로 진입한다. 이를 통해 첫 번째 날짜 수치 1과 온도 수치 12.4를 예외 없이 정확히 읽어 올 수 있다.

한편, 파일 입출력 시 자주 발생하는 또 다른 예외는 status="old"로 지정된 대상 파일이 디스크 상에 존재하지 않을 때 발생한다. 이때는 read 단계가 아닌 open 제어 단계에서 즉시 런타임 오류가 발생하고 실행이 중단된다.

이런 문제를 예방하기 위해서는 모든 파일 개설(open) 및 읽기(read) 명령에 iostatiomsg 지정자를 함께 배치해야 한다.

예외 발생 시 프로그램이 즉시 종료되는 것을 차단하고, 발생한 에러 원인을 분석하여 사용자에게 경고 메시지를 출력함과 동시에 사전에 준비된 기본값 시스템으로 제어 흐름을 전환하는 구조가 안정성을 높이는 방안이다.


[!important] 요약


[!important] 연습 문제

  1. character(len=20) :: city = "Busan" 을 선언하고 lenlen_trim 의 결과를 출력해 차이를 확인하라.
  2. 사용자에게 성과 이름을 따로 입력받아 // 로 이어 전체 이름을 만들어 출력하라.
  3. 문자열 "2025-06-14" 에서 부분 문자열과 내부 읽기를 이용해 연·월·일을 각각 정수로 분리해 출력하라.
  4. character(len=:), allocatable 문자열에 "a" 를 다섯 번 이어 붙여 "aaaaa" 를 만들고, 각 단계의 len 을 출력하라.
  5. 임의의 파일 이름을 inquire 로 검사해 존재하면 크기를, 없으면 없다는 문구를 출력하는 프로그램을 작성하라.
  6. 텍스트 파일 하나를 만들어 세 줄을 쓰고, 같은 프로그램에서 다시 열어 줄 수를 세어 출력하라.
  7. csv 파일(한 열, 실수 여러 줄)을 읽어 평균과 표준편차를 계산해 화면에 출력하라. 줄 수는 EOF로 판별하라.
  8. namelistn, x_min, x_max 를 읽어 들여 구간을 n 등분한 점들을 csv로 출력하는 프로그램을 작성하라. 설정 파일이 없으면 기본값으로 동작하게 하라.
  9. 실수 배열을 비서식 스트림으로 저장한 뒤, 별도의 프로그램에서 다시 읽어 합을 구하라. 저장과 읽기의 kind 가 같아야 함을 확인하라.
  10. 한 줄에 이름과 점수가 콤마로 적힌 csv를 읽어, 점수가 가장 높은 사람의 이름을 출력하라(문자열과 수치 입력을 한 read 로 처리).