표준 라이브러리: 파일과 스트림
표준 라이브러리: 파일과 스트림 (Standard library: Files and streams)
Ada는 파일 입출력(I/O)에 대해 서로 다른 접근 방식을 제공해요:
- 텍스트 I/O(Text I/O) — 텍스트 형식의 파일 I/O를 지원하며, 콘솔에 정보 표시를 포함해요
- 순차 I/O(Sequential I/O) — 특정 데이터 타입에 대해 순차적 방식으로 작성된 이진 형식의 파일 I/O를 지원해요
- 직접 I/O(Direct I/O) — 특정 데이터 타입에 대한 이진 형식의 파일 I/O를 지원하지만, 파일의 어떤 위치에도 접근할 수 있어요
- 스트림 I/O(Stream I/O) — 이진 형식의 파일을 사용해 여러 데이터 타입(무한 타입의 객체 포함)의 정보 I/O를 지원해요
다음 표는 방금 본 기능들의 요약을 제시해요:
| 파일 I/O 옵션 | 형식 | 임의 접근 | 데이터 타입 |
|---|---|---|---|
| Text I/O | text | string type | |
| Sequential I/O | binary | single type | |
| Direct I/O | binary | ✓ | single type |
| Stream I/O | binary | ✓ | multiple types |
다음 절들에서 이 I/O 접근 방식들에 대한 자세한 내용을 논의할게요.
본문
텍스트 I/O (Text I/O)
이 과정의 대부분에서 Put_Line 프로시저를 사용해 콘솔에 정보를 표시했어요. 하지만 이 프로시저는 File_Type 매개변수도 받아들여요. 예를 들어 이 매개변수를 명시적으로 설정해 표준 출력과 표준 오류 사이를 선택할 수 있어요:
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_Std_Text_Out is
begin
Put_Line (Standard_Output, "Hello World #1");
Put_Line (Standard_Error, "Hello World #2");
end Show_Std_Text_Out;
이 매개변수를 사용해 어떤 텍스트 파일에도 정보를 쓸 수도 있어요. 쓰기를 위한 새 파일을 만들려면 Create 프로시저를 사용해, 나중에 Put_Line에(예: Standard_Output 대신) 전달할 수 있는 File_Type 요소를 초기화해요. 정보 쓰기를 마친 후 Close 프로시저를 호출해 파일을 닫을 수 있어요.
텍스트 파일에서 정보를 읽으려면 유사한 방법을 사용해요. 하지만 파일을 열 때 출력 파일(Out_File) 대신 입력 파일(In_File)임을 지정해야 해요. 또한 Put_Line 프로시저를 호출하는 대신 Get_Line 함수를 호출해 파일에서 정보를 읽어요.
새 텍스트 파일에 정보를 쓴 다음 같은 파일에서 다시 읽는 예시를 볼게요:
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_Simple_Text_File_IO is
F : File_Type;
File_Name : constant String := "simple.txt";
begin
Create (F, Out_File, File_Name);
Put_Line (F, "Hello World #1");
Put_Line (F, "Hello World #2");
Put_Line (F, "Hello World #3");
Close (F);
Open (F, In_File, File_Name);
while not End_Of_File (F) loop
Put_Line (Get_Line (F));
end loop;
Close (F);
end Show_Simple_Text_File_IO;
Create와 Close 프로시저 외에도 표준 라이브러리는 Reset 프로시저를 포함하는데, 이름이 시사하듯 파일의 모든 정보를 재설정(지움)해요. 예를 들어:
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_Text_File_Reset is
F : File_Type;
File_Name : constant String := "simple.txt";
begin
Create (F, Out_File, File_Name);
Put_Line (F, "Hello World #1");
Reset (F);
Put_Line (F, "Hello World #2");
Close (F);
Open (F, In_File, File_Name);
while not End_Of_File (F) loop
Put_Line (Get_Line (F));
end loop;
Close (F);
end Show_Text_File_Reset;
이 프로그램을 실행하면 파일에 첫 문자열("Hello World #1")을 썼음에도 Reset 호출 때문에 지워진 것을 알 수 있어요.
읽기나 쓰기를 위해 파일을 여는 것 외에도 기존 파일을 열어 추가(append)할 수도 있어요. Open 프로시저를 Append_File 옵션으로 호출해 이걸 해요.
Open 프로시저를 호출할 때 지정된 파일이 없으면 예외가 발생해요. 따라서 그 문맥에서 예외를 처리해야 해요. 다음 예시는 파일을 삭제한 다음 같은 파일을 읽기 위해 열려고 시도해요:
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_Text_File_Input_Except is
F : File_Type;
File_Name : constant String := "simple.txt";
begin
-- 출력 파일을 열고 삭제
Create (F, Out_File, File_Name);
Delete (F);
-- 삭제된 파일을 열려고 시도
Open (F, In_File, File_Name);
Close (F);
exception
when Name_Error =>
Put_Line ("File does not exist");
when others =>
Put_Line
("Error while processing input file");
end Show_Text_File_Input_Except;
이 예시에서 Create 호출로 파일을 만들고 Delete 호출로 삭제해요. Delete 호출 후에는 더 이상 File_Type 요소를 사용할 수 없어요. 파일을 삭제한 후 존재하지 않는 파일을 열려고 시도해 Name_Error 예외가 발생해요.
순차 I/O (Sequential I/O)
이전 절에서 텍스트 파일 I/O에 대한 자세한 내용을 제시했어요. 여기서는 이진 형식으로 파일 I/O를 하는 방법을 논의할게요. 처음 탐구할 패키지는 Ada.Sequential_IO 패키지예요. 이 패키지는 제네릭이므로 파일 I/O에 사용할 데이터 타입으로 인스턴스화해야 해요. 그렇게 한 후 이전 절에서 본 것과 같은 프로시저들(Create, Open, Close, Reset, Delete)을 사용할 수 있어요. 하지만 Get_Line과 Put_Line 프로시저를 호출하는 대신 Read와 Write 프로시저를 호출할게요.
다음 예시에서 부동 소수점 타입에 대해 Ada.Sequential_IO 패키지를 인스턴스화해요:
with Ada.Text_IO;
with Ada.Sequential_IO;
procedure Show_Seq_Float_IO is
package Float_IO is
new Ada.Sequential_IO (Float);
use Float_IO;
F : Float_IO.File_Type;
File_Name : constant String :=
"float_file.bin";
begin
Create (F, Out_File, File_Name);
Write (F, 1.5);
Write (F, 2.4);
Write (F, 6.7);
Close (F);
declare
Value : Float;
begin
Open (F, In_File, File_Name);
while not End_Of_File (F) loop
Read (F, Value);
Ada.Text_IO.Put_Line
(Float'Image (Value));
end loop;
Close (F);
end;
end Show_Seq_Float_IO;
복잡한 정보를 읽고 쓰는 데도 같은 접근 방식을 사용해요. 다음 예시는 Boolean과 부동 소수점 값을 포함하는 레코드를 사용해요:
with Ada.Text_IO;
with Ada.Sequential_IO;
procedure Show_Seq_Rec_IO is
type Num_Info is record
Valid : Boolean := False;
Value : Float;
end record;
procedure Put_Line (N : Num_Info) is
begin
if N.Valid then
Ada.Text_IO.Put_Line
("(ok, "
& Float'Image (N.Value) & ")");
else
Ada.Text_IO.Put_Line
("(not ok, -----------)");
end if;
end Put_Line;
package Num_Info_IO is new
Ada.Sequential_IO (Num_Info);
use Num_Info_IO;
F : Num_Info_IO.File_Type;
File_Name : constant String :=
"float_file.bin";
begin
Create (F, Out_File, File_Name);
Write (F, (True, 1.5));
Write (F, (False, 2.4));
Write (F, (True, 6.7));
Close (F);
declare
Value : Num_Info;
begin
Open (F, In_File, File_Name);
while not End_Of_File (F) loop
Read (F, Value);
Put_Line (Value);
end loop;
Close (F);
end;
end Show_Seq_Rec_IO;
예시에서 보듯 부동 소수점 타입에 사용한 것과 같은 접근 방식을 이 레코드의 파일 I/O에 사용할 수 있어요. 레코드 타입에 대해 Ada.Sequential_IO 패키지를 인스턴스화하면 파일 I/O 연산이 같은 방식으로 수행돼요.
직접 I/O (Direct I/O)
직접 I/O는 Ada.Direct_IO 패키지에 있어요. 이 메커니즘은 방금 제시한 순차 I/O와 유사하지만 파일의 어떤 위치에도 접근할 수 있게 해줘요. 패키지 인스턴스화와 대부분의 연산은 순차 I/O와 매우 유사해요. 이전 절에서 제시한 Show_Seq_Float_IO 애플리케이션을 Ada.Direct_IO 패키지를 사용하도록 다시 작성하려면 Ada.Sequential_IO 패키지의 인스턴스를 Ada.Direct_IO 패키지로 바꾸기만 하면 돼요. 다음이 새 소스 코드예요:
with Ada.Text_IO;
with Ada.Direct_IO;
procedure Show_Dir_Float_IO is
package Float_IO is new Ada.Direct_IO (Float);
use Float_IO;
F : Float_IO.File_Type;
File_Name : constant String :=
"float_file.bin";
begin
Create (F, Out_File, File_Name);
Write (F, 1.5);
Write (F, 2.4);
Write (F, 6.7);
Close (F);
declare
Value : Float;
begin
Open (F, In_File, File_Name);
while not End_Of_File (F) loop
Read (F, Value);
Ada.Text_IO.Put_Line
(Float'Image (Value));
end loop;
Close (F);
end;
end Show_Dir_Float_IO;
순차 I/O와 달리 직접 I/O는 파일의 어떤 위치에도 접근할 수 있어요. 하지만 파일에 정보를 추가하는 옵션은 제공하지 않아요. 대신 같은 File_Type 요소를 통해 파일에 읽기와 쓰기를 모두 허용하는 Inout_File 모드를 제공해요.
파일의 어떤 위치에든 접근하려면 Set_Index 프로시저를 호출해 새 위치/인덱스를 설정해요. Index 함수를 사용해 현재 인덱스를 검색할 수 있어요. 예시를 볼게요:
with Ada.Text_IO;
with Ada.Direct_IO;
procedure Show_Dir_Float_In_Out_File is
package Float_IO is new Ada.Direct_IO (Float);
use Float_IO;
F : Float_IO.File_Type;
File_Name : constant String :=
"float_file.bin";
begin
-- 입력/출력을 위해 파일 열기
Create (F, Inout_File, File_Name);
Write (F, 1.5);
Write (F, 2.4);
Write (F, 6.7);
-- 인덱스를 이전 위치로 설정하고
-- 값 덮어쓰기
Set_Index (F, Index (F) - 1);
Write (F, 7.7);
declare
Value : Float;
begin
-- 인덱스를 파일 시작으로 설정
Set_Index (F, 1);
while not End_Of_File (F) loop
Read (F, Value);
Ada.Text_IO.Put_Line
(Float'Image (Value));
end loop;
Close (F);
end;
end Show_Dir_Float_In_Out_File;
이 예시를 실행하면 파일에 이전에 쓴 6.7 대신 7.7이 들어 있는 것을 볼 수 있어요. 다시 쓰기 전에 인덱스를 이전 위치로 변경해 값을 덮어썼죠.
이 예시에서 Inout_File 모드를 사용했어요. 이 모드를 사용해, 파일을 닫고 읽기 위해 다시 열지 않고도 파일에서 읽기 전에 인덱스를 초기 위치로 되돌렸죠(Set_Index (F, 1)).
스트림 I/O (Stream I/O)
이진 형식의 파일 I/O에 대한 이전 접근 방식들(순차 및 직접 I/O)은 모두 단일 데이터 타입(인스턴스화한 타입)에 특화돼 있어요. 이 접근 방식을 사용해 단일 데이터 타입의 객체를 쓸 수 있는데, 그 객체는 (잠재적으로 많은 필드가 있는) 배열이나 레코드일 수 있어요. 하지만 서로 다른 데이터 타입을 포함하는 파일을 만들고 처리해야 하거나, 무한 타입의 객체가 필요하다면 이 접근 방식으로는 충분하지 않아요. 대신 스트림 I/O를 사용해야 해요.
스트림 I/O는 이전 접근 방식들과 몇 가지 유사점을 공유해요. 여전히 Create, Open, Close 프로시저를 사용해요. 하지만 File_Type 요소를 통해 파일에 직접 접근하는 대신 Stream_Access 요소를 사용해요. 정보를 읽고 쓰려면 읽거나 쓰는 데이터 타입의 'Read 또는 'Write 애트리뷰트를 사용해요.
이전 절의 Show_Dir_Float_IO 프로시저를 직접 I/O 대신 스트림 I/O를 사용하는 버전을 볼게요:
with Ada.Text_IO;
with Ada.Streams.Stream_IO;
use Ada.Streams.Stream_IO;
procedure Show_Float_Stream is
F : File_Type;
S : Stream_Access;
File_Name : constant String :=
"float_file.bin";
begin
Create (F, Out_File, File_Name);
S := Stream (F);
Float'Write (S, 1.5);
Float'Write (S, 2.4);
Float'Write (S, 6.7);
Close (F);
declare
Value : Float;
begin
Open (F, In_File, File_Name);
S := Stream (F);
while not End_Of_File (F) loop
Float'Read (S, Value);
Ada.Text_IO.Put_Line
(Float'Image (Value));
end loop;
Close (F);
end;
end Show_Float_Stream;
Create 호출 후 Stream 함수를 호출해 해당 Stream_Access 요소를 검색해요. 그런 다음 이 스트림을 사용해 Float 타입의 'Write 애트리뷰트를 통해 파일에 정보를 써요. 파일을 닫고 읽기 위해 다시 연 후 다시 해당 Stream_Access 요소를 검색하고 Float 타입의 'Read 애트리뷰트를 통해 파일에서 정보를 읽어요.
스트림을 사용해 같은 파일 안에 서로 다른 데이터 타입을 포함하는 파일을 만들고 처리할 수 있어요. 문자열 같은 무한 데이터 타입도 읽고 쓸 수 있어요. 하지만 무한 데이터 타입을 사용할 때는 그 무한 데이터 타입의 'Input과 'Output 애트리뷰트를 호출해야 해요: 이 애트리뷰트들은 객체의 실제 데이터 외에 경계(bounds)나 판별자(discriminant)에 대한 정보도 써요.
다음 예시는 서로 다른 길이의 문자열과 부동 소수점 값을 혼합한 파일 I/O를 보여줘요:
with Ada.Text_IO;
with Ada.Streams.Stream_IO;
use Ada.Streams.Stream_IO;
procedure Show_String_Stream is
F : File_Type;
S : Stream_Access;
File_Name : constant String :=
"float_file.bin";
procedure Output (S : Stream_Access;
FV : Float;
SV : String) is
begin
String'Output (S, SV);
Float'Output (S, FV);
end Output;
procedure Input_Display (S : Stream_Access) is
SV : String := String'Input (S);
FV : Float := Float'Input (S);
begin
Ada.Text_IO.Put_Line (Float'Image (FV)
& " --- " & SV);
end Input_Display;
begin
Create (F, Out_File, File_Name);
S := Stream (F);
Output (S, 1.5, "Hi!!");
Output (S, 2.4, "Hello world!");
Output (S, 6.7, "Something longer here...");
Close (F);
Open (F, In_File, File_Name);
S := Stream (F);
while not End_Of_File (F) loop
Input_Display (S);
end loop;
Close (F);
end Show_String_Stream;
스트림 I/O를 사용할 때 쓰는 데이터의 타입을 나타내는 정보는 파일에 기록되지 않아요. 파일이 서로 다른 타입의 데이터를 포함한다면, 읽을 때 파일에서 참조하는 타입을 쓸 때와 같은 순서로 참조해야 해요. 그렇지 않으면 얻는 정보가 손상돼요. 불행히도 강한 데이터 타입(strong data typing)은 이 경우 도움이 되지 않아요. 파일 I/O를 위한 간단한 프로시저(위 예시처럼)를 작성하면 파일 형식이 일관되도록 보장하는 데 도움이 될 수 있어요.
직접 I/O처럼 스트림 I/O 지원도 파일의 어떤 위치에도 접근할 수 있게 해줘요. 하지만 그렇게 할 때는 새 인덱스의 위치가 기대하는 데이터 타입과 일관적인지 매우 주의해야 해요.