12 입출력 - IO

이번장은 IO, FilePath등의 입출력 장치와 관련된 모듈에 대해 소개합니다.

This chapter is a quick introduction to input/output mechanisms in Elixir and related modules, like IO, File and Path.

이번장은 앞에서 살펴본 Getting Started 가이드에서 이미 살짝 맛본 내용들입니다. 하지만, 입출력 시스템은 Elixir와 VM에 대한 몇가지 철학을 살펴 볼 수 있게 해줍니다.

We had originally sketched this chapter to come much earlier in the getting started guide. However, we noticed the IO system provides a great opportunity to shed some light on some philosophies and curiosities of Elixir and the VM.

12.1 입출력 모듈 - The IO module

Elixir의 IO 모듈은 표준 입출력(:stdio), 표준 오류(:stderr), 파일 그리고 다른 입출력 장치를 읽고 쓸 수 있는 기본 메카니즘입니다. 모듈 사용법은 직관적입니다:

The IO module in Elixir is the main mechanism for reading and writing to the standard io (:stdio), standard error (:stderr), files and other IO devices. Usage of the module is pretty straight-forward:

iex> IO.puts "hello world"
"hello world"
:ok
iex> IO.gets "yes or no? "
yes or no? yes
"yes\n"

기본적으로 입출력 모듈의 함수는 표준 입력과 표준 출력을 사용합니다. :stderr 인자를 전달하여 표준 오류 장치로 출력 할 수 있습니다:

By default, the functions in the IO module use the standard input and output. We can pass the :stderr as argument to write to the standard error device:

iex> IO.puts :stderr, "hello world"
"hello world"
:ok

12.2 파일 모듈 - The File module

File모듈은 파일을 입출력 장치로 열 수 있는 기능을 가지고 있습니다. 기본적으로 파일은 바이너리 모드로 열립니다. IO모듈의 IO.binread/2IO.binwrite/2를 사용하여 파일에 데이터를 쓰거나 읽을수 있습니다:

The File module contains functions that allows us to open files as IO devices. By default, files are opened in binary mode, which requires developers to use the specific IO.binread/2 and IO.binwrite/2 functions from the IO module:

iex> {:ok, file} = File.open "hello", [:write]
{:ok, #PID<0.47.0>}
iex> IO.binwrite file, "world"
:ok
iex> File.close file
:ok
iex> File.read "hello"
{:ok, "world"}

파일은 :utf8 인코딩으로 열수도 있고 IO모듈의 다른 함수에서도 사용할 수 있습니다:

A file can also be opened with :utf8 encoding which allows the remaining functions in the IO module to be used:

iex> {:ok, file} = File.open "another", [:write, :utf8]
{:ok, #PID<0.48.0>}

한편, 파일을 열거나 읽고 쓰는 함수는 File모듈이 파일 시스템상에서 동작하는 기능을 많이 가지고 있습니다. 예를들어 File.rm/1은 파일을 삭제하는 데 사용할 수 있고, File.mkdir/1은 디렉토리를 만들고 File.mkdir_p/1은 부모 디렉토리가 없으면 생성해주고 File.cp_r/2File.rm_rf/2는 파일과 디렉토리를 재귀적으로 복사하고 삭제합니다.

Besides functions for opening, reading and writing files, the File module has many functions that work on the file system. Those functions are named after their UNIX equivalents. For example, File.rm/1 can be used to remove files, File.mkdir/1 to create directories, File.mkdir_p/1 creates directories guaranteeing their parents exists and there is even File.cp_r/2 and File.rm_rf/2 which copy and remove files and directories recursively.

File모듈의 함수에는 2가지 종류의 관습이 있습니다. 하나는 !(뱅이라고 읽습니다)이고 다른 하나는 뱅이 없습니다. 예를들어 "hello"라는 파일을 읽을 때, 위의 예에서는 !없이 사용했습니다. 몇 가지 새로운 예제를 시도해봅시다:

You will also notice that functions in the File module have two variants, one with ! (bang) in its name and others without. For example, when we read the "hello" file above, we have used the one without !. Let's try some new examples:

iex> File.read "hello"
{:ok, "world"}
iex> File.read! "hello"
"world"
iex> File.read "unknown"
{:error, :enoent}
iex> File.read! "unknown"
** (File.Error) could not read file unknown: no such file or directory

파일이없는 경우 !가 붙은 버전에서는 오류가 발생하고 있습니다. 즉 !이 없는 버전은 패턴 매칭을 이용하여 다양한 형태의 처리를 위해 선택합니다. 그러나 만약 파일이 존재할것같으면, 뱅을 다는것이 예외 오류 메시지를 알 수 있기 때문에 유용합니다.즉 절대로 이렇게 쓰지 마십시요:

Notice that when the file does not exist, the version with ! raises an error. That said, the version without ! is preferred when you want to handle different outcomes with pattern matching. However, if you expect the file to be there, the bang variation is more useful as it raises a meaningful error message. That said, never write:

{:ok, body} = File.read(file)

대시 아래와 같이:

Instead write:

case File.read(file) do
  {:ok, body} -> # handle ok
  {:error, r} -> # handle error
end

또는

or

File.read!(file)

12.3 Path 모듈 - The Path module

파일 모듈의 함수들은 인자로 경로를 포함해야합니다. 대부분의 경우 이러한 경로는 바이너리이며 Path모듈로 조작할수 있습니다:

The majority of the functions in the File module expects paths as arguments. Most commonly, those paths will be binaries and they can be manipulated with the Path module:

iex> Path.join("foo", "bar")
"foo/bar"
iex> Path.expand("~/hello")
"/Users/jose/hello"

지금까지 파일 시스템을 통한 입출력의 주요 모듈에 대해 살펴봤습니다. 이제는 좀 더 고급 입출력에 대해 이야기하겠습니다. 이 섹션에서는 Elixir 코드를 작성할 필요가 없습니다. 그러니 편하게 살펴보세요. 하지만 VM에서 입출력 시스템이 어떻게 구현되는지에 대한 overview가 제공됩니다.

With this we have covered the main modules for doing IO and interacting with the file system. Next we will discuss some curiosities and advanced topics regarding IO. Those sections are not necessary to write Elixir code, so feel free to skip them, but they do provide an overview of how the IO system is implemented in the VM and other curiosities.

12.4 프로세스와 그룹 리더 - Processes and group leaders

File.open/2가 PID를 포함한 튜플을 리턴하는것을 눈치채셨을겁니다:

You may have noticed that File.open/2 returned a tuple containing a PID:

iex> {:ok, file} = File.open "hello", [:write]
{:ok, #PID<0.47.0>}

실제로는 입출력 모듈이 프로세스와 같이 동작하고 있기 때문입니다. IO.write(pid, binary)라고 쓰면 입출력 모듈은 메시지를 원하는 오퍼레이션(operation)과 함께 프로세스에 보냅니다. 만약 내 자신(own)의 프로세스를 사용하면 무슨일이 있어나는지 살펴봅시다:

That's because the IO module actually works with processes. When you say IO.write(pid, binary), the IO module will send a message to the process with the desired operation. Let's see what happens if we use our own process:

iex> pid = spawn fn ->
...>  receive do: (msg -> IO.inspect msg)
...> end
#PID<0.57.0>
iex> IO.write(pid, "hello")
{:io_request, #PID<0.41.0>, #PID<0.57.0>, {:put_chars, :unicode, "hello"}}
** (ErlangError) erlang error: :terminated

IO.write/2를 호출하면 IO 모듈로 요청이 보내진걸 볼수 있습니다. 그러고나서는 실패합니다. 왜냐면 IO module이 뭔가 결과 같은것을 원했기 때문입니다.

After IO.write/2, we can see the request sent by the IO module printed, which then fails since the IO module expected some kind of result that we did not supply.

StringIO모듈은 문자열에 대해 입출력 장치 메시지 구현을 제공합니다:

The StringIO module provides an implementation of the IO device messages on top of a string:

iex> {:ok, pid} = StringIO.open("hello")
{:ok, #PID<0.43.0>}
iex> IO.read(pid, 2)
"he"

입출력 장치와 프로세스를 모델링하여 Erlang VM은 동일한 네트워크의 다른 노드간에 파일 프로세스를 교환하거나 두 노드간에 파일을 읽고 쓸 수 있습니다. 모든 입출력 장치을 소유하는 특별한 그룹 리더가 있습니다.

By modelling IO devices with processes, the Erlang VM allows different nodes in the same network to exchange file processes to read/write files in between nodes. Of all IO devices, there is one that is special to each process, called group leader.

:stdio에 쓸때 실제로는 메시지를 그룹 리더한테 보내고 있습니다. 그룹 리더는 STDIO 파일 디스크립터로 써 넣습니다:

When you write to :stdio, you are actually sending a message to the group leader, which writes to STDIO file descriptor:

iex> IO.puts :stdio, "hello"
hello
:ok
iex> IO.puts Process.group_leader, "hello"
hello
:ok

그룹 리더는 프로세스마다 설정될 수 있고 다른상황에서도 사용될 수 있습니다. 예를들어 원격 단말에서 코드를 실행할 때 원격 노드의 메시지는 요청한 터미널로 다시 보내져서 출력될 수 있습니다.

The group leader can be configured per process and is used in different situations. For example, when executing code in a remote terminal, it guarantees messages in a remote node are redirected and printed in the terminal that triggered the request.

12.5 iodatachardata - iodata and chardata

위에서 본 예제들에서는 파일에 쓸 때 바이너리/문자열을 사용했습니다. "Binaries, strings and char lists"에서 문자열은 단순히 바이트인 반면에 문자 리스트는 코드 포인트를 가지는 리스트라고 했습니다.

In all examples above, we have used binaries/strings when writing to files. In the chapter "Binaries, strings and char lists", we mentioned how strings are simply bytes while char lists are lists with code points.

IOFile모듈의 함수는 인자로 리스트를 넘길수 있습니다. 뿐만 아니라 숫자와 바이너리가 섞인 리스트의 리스트도 가능합니다:

The functions in IO and File also allow lists to be given as arguments. Not only that, they also allow a mixed list of lists, integers and binaries to be given:

iex> IO.puts 'hello world'
hello world
:ok
iex> IO.puts ['hello', ?\s, "world"]
hello world
:ok

하지만 이건 좀 조심해야 합니다. 리스트는 IO 장치의 인코딩에 의존하여 바이트 덩어리 또는 문자의 덩어리로 표현됩니다. 만약 파일이 인코딩없이 열리면 파일은 raw 모드로 열리게 되며, bin*로 시작하는 IO모듈에 있는 함수들을 사용해야 합니다. 이 함수들은 인자로 iodata를 써야합니다, 즉 숫자로 표현되는 바이트 및 바이너리들을 사용해야 합니다.

However, this requires some attention. A list may represent either a bunch of bytes or a bunch of characters and which one to use depends on the encoding of the IO device. If the file is opened without encoding, the file is expected to be in raw mode, and the functions in the IO module starting with bin* must be used. Those functions expect an iodata as argument, i.e. it expects a list of integers representing bytes and binaries to be given.

반면에 :stdio 또는 :utf8 인코딩을 지정하여 열린 파일은 IO 모듈의 함수들의 인자로 char_data를 사용할 수 있습니다, 즉 문자와 문자열로 이루어진 리스트를 인자로 넘길수 있습니다.

On the other hand, :stdio and files opened with :utf8 encoding work with the remaining functions in the IO module and those expect a char_data as argument, i.e. they expect a list of characters or strings to be given.

비록 사소한 차이에 불과하지만 리스트를 함수에 넘길때 이러한 디테일에 대해 유념해야 할지 모릅니다. 바이너리는 이미 바이트 표현되어 있기 때문에 항상 raw 데이터로 봐야합니다.

Although this is a subtle difference, you only need to worry about those details if you intend to pass lists to those functions. Binaries are already represented by the underlying bytes and as such their representation is always raw.

지금까지 입출력 장치와 입출력과 관련된 기능을 살펴봤습니다. IO, File, PathStringIO 4가지 Elixir 모듈에 대해 배웠을 뿐만아니라 어떻게 VM이 입출력 메커니즘을 잘 사용하는지, 어떻게 입출력 작업에 (char와 io의) 데이터를 활용하는지 배웠습니다.

This finishes our tour of IO devices and IO related functionality. We have learned about four Elixir modules, IO, File, Path and StringIO, as well as how the VM uses processes for the underlying IO mechanisms and how to use (char and io) data for IO operations.