I/O는 입력(Input)과 출력(Output)을 의미하는 용어입니다.
입력이란 컴퓨터, Ruby 메서드, 심지어 우리 뇌처럼 어떤 대상으로 들어오는 모든 데이터와 정보를 말합니다.
입력의 예시:
- 키보드로 누르는 키
- 마우스 클릭
- 읽고 있는 책
반면 출력은 입력의 결과로 나오는 모든 것을 의미합니다.
출력의 예시:
- 1 + 1의 계산 결과
- 읽은 기사를 요약해 작성한 글
- 커피
Ruby에서 I/O라고 하면 일반적으로 파일 읽기, 네트워크 소켓 작업, 화면에 정보 출력을 가리킵니다.
IO 클래스 이해하기
IO는 Ruby의 클래스이기도 합니다.
File, Socket 같은 객체들의 부모 클래스 역할을 합니다.
모든 IO 객체가 공통으로 갖는 연산:
- open / close (열기 / 닫기)
- read (읽기)
- write (쓰기)
IO 객체는 파일 디스크립터(file descriptor)를 기반으로 동작합니다.
컴퓨터에서 실행 중인 모든 프로세스는 내부 정보 테이블을 가지고 있으며, 이 테이블에는 파일 디스크립터 목록이 포함되어 있습니다.
파일 디스크립터란 정수와 파일 이름 사이의 매핑입니다.
Linux에서는 다음 명령어로 파일 디스크립터 목록을 확인할 수 있습니다.
명령어:
# 다른 창에서 irb가 실행 중이라고 가정 ls -lh /proc/`pgrep irb`/fd/ # lrwx------ 1 jesus jesus 64 Feb 3 15:54 0 -> /dev/pts/3 # lrwx------ 1 jesus jesus 64 Feb 3 15:54 1 -> /dev/pts/3 # lrwx------ 1 jesus jesus 64 Feb 3 15:54 2 -> /dev/pts/3
Ruby에서는 sysopen 메서드로 새로운 파일 디스크립터를 생성할 수 있습니다.
예시:
fd = IO.sysopen('/dev/null', 'w')
# 10
이제 이 파일 디스크립터(10)로 통신 채널을 열 수 있습니다.
다음과 같이요:
io = IO.new(fd)
그리고 데이터를 쓸 수도 있습니다:
io << 'orange'
여기서 주목할 점은 IO가 파일 디스크립터가 실제 파일에 매핑되었는지, 하드 디스크 같은 장치인지, 아니면 네트워크 소켓인지 전혀 신경 쓰지 않는다는 것입니다.
쓰기용 파일 디스크립터 여는 방법
sysopen으로 새 파일 디스크립터를 만들거나 File.open으로 파일을 열 때는 몇 가지 옵션을 지정할 수 있습니다.
기본값은 읽기 전용(read-only)입니다.
모드를 직접 지정할 수 있습니다:
r(읽기, 기본값)w(쓰기)w+(읽기 & 쓰기)a(덧붙이기)
예시:
fd = IO.sysopen('/dev/tty1', 'w+')
이 코드는 /dev/tty1을 읽기 + 쓰기 모드로 엽니다.
3가지 표준 IO 스트림
새 프로그램(에디터, 터미널, Ruby 인터프리터 등)을 실행하면 기본적으로 3개의 통신 채널, 즉 3개의 파일 디스크립터가 열립니다.
바로 다음과 같습니다:
- stdin (입력)
- stdout (출력)
- stderr (에러)
Ruby에서는 상수 형태나 전역 변수 형태로 이들에 바로 접근할 수 있습니다.
예시:
$stdout << "abc\n"
$stdout에 쓰는 것은 puts 메서드를 호출하는 것과 같습니다.
표준 출력(standard output)은 터미널과 연결되어 있습니다.
lrwx------ 1 jesus jesus 64 Feb 3 15:54 0 -> /dev/pts/3
위의 /dev/pts/3는 터미널이 화면에 텍스트를 표시하기 위해 읽어들이는 가상 장치입니다.
표준 에러(standard error) 역시 출력에 사용되지만, 터미널이 이 내용을 더 큰 글씨나 다른 색상 등 일반 텍스트와 구분되는 방식으로 표시할 수 있게 해줍니다.
warn 메서드를 사용하면 표준 에러로 출력할 수 있습니다.
표준 입력(standard input)은 사용자 입력을 읽어들이는 곳입니다.
gets 메서드로 표준 입력에서 데이터를 읽을 수 있습니다.
STDOUT 플러시 및 버퍼링 비활성화 방법
IO 객체는 내부 버퍼를 가지고 있습니다.
일정량의 입력 데이터가 읽히기 전에 먼저 저장됩니다.
그래서 가끔 puts를 호출했는데도...
프로그램이 종료되거나 버퍼가 가득 차기 전까지는 아무것도 화면에 나타나지 않는 경우가 생깁니다!
버퍼링은 다음과 같이 비활성화할 수 있습니다:
$stdout.sync = true
또는 버퍼를 수동으로 플러시할 수도 있습니다:
$stdout.flush
버퍼링을 비활성화하면 기다릴 필요 없이 즉시 출력 결과를 확인할 수 있습니다.
청크 단위로 읽기
IO 객체에서 데이터를 읽을 때는 스트림이 END-OF-FILE(EOF) 신호로 끝나거나, 스트림이 닫히거나, 지정한 바이트 수만큼 받을 때까지 read를 계속하게 됩니다.
네트워크 프로그래밍이나 대용량 파일을 다룰 때는 작은 청크(chunk) 단위로 읽는 것이 유용합니다.
청크 단위 읽기는 다음과 같이 구현할 수 있습니다:
data = "" until io.eof? data << io.read(2048) end
이 코드를 사용하면 전체 다운로드가 완료될 때까지 기다리지 않고, 데이터가 도착하는 대로 바로 처리할 수 있습니다.
eof?는 "블로킹(blocking)"된다는 점에 주의하세요.
블로킹이란 운영체제가 새로 읽을 데이터가 있다고 알려줄 때까지 코드가 대기 상태로 들어간다는 의미입니다.
양방향 통신을 위한 IO 파이프 사용법
Ruby는 파이프(pipe)를 생성할 수 있게 해줍니다.
파이프는 서로 연결된 한 쌍의 파일 디스크립터입니다.
이는 프로세스 간 통신(IPC, Inter-Process Communication)에 활용됩니다.
예시:
read, write = IO.pipe
fork 메서드(Windows에서는 사용 불가)와 함께 사용하면 현재 프로세스의 복사본을 만들 수 있습니다.
이렇게 만들어진 두 프로세스는 파이프를 통해 서로 통신할 수 있습니다.
예시:
if fork
write.close
puts "Message received: #{read.gets}"
read.close
else
read.close
write << "Buy some bananas!"
write.close
end
실행 결과:
# Message received: Buy some bananas!
Kernel open 메서드 사용 시 주의점
Ruby에는 매우 유연한 open 메서드가 있습니다.
파일을 열거나 URL에서 데이터를 읽고, 심지어 외부 명령을 실행하는 파이프까지 열 수 있습니다.
예시:
open "abc.txt"
하지만 바로 이런 범용성 때문에 이 메서드는 사용을 피해야 합니다.
하나의 메서드가 이렇게 다양한 일을 할 수 있다는 것 자체가 보안상 위험 요소가 됩니다.
대신 용도에 맞는 전용 open 메서드를 사용하는 것이 좋습니다.
예시:
File.open # 파일 열기 IO.popen # 프로세스 열기 URI.parse(...).open # URL 열기
이들은 모두 IO 객체를 반환합니다.
마무리
지금까지 Ruby의 입출력에 대해 알아보았습니다! 파일 디스크립터, IO 클래스, 버퍼링, 청크 단위 읽기, 표준 IO 스트림까지 핵심 개념을 모두 살펴보았습니다.

이 글이 도움이 되었다면 주변에 공유해 주세요.
읽어주셔서 감사합니다.