Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

루비 StringIO 완벽 가이드: 동작 원리부터 실전 활용법까지

IO 객체(파일, 소켓 등)처럼 동작하지만, 문자열처럼 자유롭게 제어할 수 있는 객체를 찾고 계신가요?

그렇다면 StringIO가 바로 그 답입니다.

지금부터 실용적인 예제와 함께 반드시 알아두어야 할 주의사항까지 살펴보겠습니다.

기본 사용 예제

StringIO 객체는 다음과 같이 생성합니다.

io = StringIO.new("abc")

생성된 객체에는 gets, read, each_line 같은 메서드를 사용해 데이터를 읽을 수 있습니다.

가장 유용하게 쓰이는 메서드들을 표로 정리했습니다.

메서드 설명
gets 입력을 한 줄씩 읽음
read 지정한 바이트 수만큼 읽음(기본값은 전체)
each_line 블록을 받아 각 줄을 순회
each_char 블록을 받아 각 문자를 순회
<< 데이터를 끝에 추가
rewind 내부 위치 포인터를 처음으로 되돌림
string StringIO 객체의 내용을 실제 문자열로 반환

StringIO에는 위치 포인터(position pointer)가 있다는 점에 주목하세요.

이 포인터는 파일 객체와 마찬가지로 지금까지 몇 바이트를 읽었는지 추적합니다.

따라서 getsread 같은 메서드를 호출할 때마다 해당 크기만큼 데이터를 반환하고 포인터를 앞으로 이동시킵니다.

map이나 each_line 같은 Enumerable 메서드 역시 위치 포인터를 이동시키므로 항상 염두에 두어야 합니다.

io.each_line { |line| puts line }

포인터를 처음 위치로 되돌리고 싶다면 rewind 메서드를 사용하면 됩니다.

io.pos
# 45

io.rewind

io.pos
# 0

여기까지가 기본 내용입니다.

이제 StringIO의 실전 활용 사례를 보여드리겠습니다. 그 전에 먼저 짚고 넘어갈 것이 하나 있습니다.

StringScanner와는 무엇이 다를까?

StringIO의 기능을 살펴봤는데, 사실 루비 표준 라이브러리에는 문자열 관련 클래스가 하나 더 있습니다.

바로 StringScanner입니다.

이름도 비슷하고 메서드도 유사해서 헷갈리기 쉽지만, 차이점을 명확히 구분해 드리겠습니다.

핵심은 다음과 같습니다.

StringIO 객체는 다른 IO 객체(File, Socket 등)를 대체할 수 있는 반면, StringScanner는 파싱 작업, 즉 텍스트를 여러 토큰으로 나누어 의미를 해석하는 용도로 설계되었습니다.

물론 이름에 "String"이 들어간 것 외에도 공통점이 있습니다. 두 클래스 모두 내부적으로 위치 포인터를 사용한다는 점입니다.

표준 입출력 대체하기

명령줄 애플리케이션에서 Kernel#gets 메서드로 사용자 입력을 받는 코드를 작성한다고 가정해 봅시다.

이 코드를 테스트하려면 매번 손으로 직접 입력값을 넣어야 하는 불편함이 있습니다.

그렇다면 자동화된 테스트는 포기해야 할까요?

아니요, 전혀 그렇지 않습니다!

바로 여기서 StringIO가 빛을 발합니다. 테스트용 입력값으로 StringIO 객체를 초기화한 뒤, $stdin이 가리키는 표준 입력 객체를 교체하면 됩니다(gets를 호출하면 루비는 이 $stdin에서 사용자 입력을 찾습니다).

예제:

io     = StringIO.new("input")
$stdin = io

gets
# input

이 기법은 puts 같은 메서드의 출력을 캡처하는 데에도 활용할 수 있습니다.

출력 메서드는 '표준 출력(standard output)'이라 불리는 기본 출력 장치에 결과를 출력합니다. 루비에서 이것은 IO 객체로 표현되며, 이를 StringIO 객체로 교체할 수 있습니다.

예제:

io      = StringIO.new("")
$stdout = io

# $stdout에 출력
puts "Jesus Castello is from Spain & likes to help people learn Ruby."

# 원래 값 복원
$stdout = STDOUT

io.rewind
io.read
# "Jesus Castello is from Spain & likes to help people learn Ruby."

입력 버전보다 절차가 조금 더 복잡한데, 그 이유는 원래의 STDOUT 객체를 반드시 복원해야 하고, 출력 내용을 읽기 위해 StringIO의 포인터도 되감아야 하기 때문입니다.

참고로 대부분의 테스트 프레임워크는 이 작업을 대신 해주는 메서드를 제공합니다(Minitest의 assert_output, RSpec의 output 매처 등). 하지만 내부에서 어떤 일이 일어나는지 이해하고 있으면 언제나 큰 도움이 됩니다. 🙂

마무리

이번 글에서는 실제 IO 객체를 흉내 내어 그 자리를 대신할 수 있는 StringIO 클래스에 대해 알아보았습니다.

이는 화면에 출력을 작성하거나 터미널을 통해 사용자 입력을 필요로 하는 클래스를 테스트할 때 특히 유용합니다.

StringIO의 흥미로운 활용 사례를 알고 계시다면 댓글로 알려주세요. 그리고 더 많은 사람들이 볼 수 있도록 이 글을 공유하는 것도 잊지 마세요!