개발자들은 프로그램이 실행되도록 만드는 데 많은 시간을 쓰다 보니, 프로그램이 어떻게 종료되는지는 쉽게 간과하곤 합니다. 하지만 이것은 매우 중요합니다. 프로그램이 올바르게 종료되면 관리가 훨씬 수월해지고, 표준 DevOps 도구들과의 연동도 원활해집니다.
Ruby 프로그램을 종료하는 방법은 다양합니다. 아래에서 몇 가지를 소개하고, 각 방법의 세부 사항과 이를 활용해 애플리케이션을 유닉스 환경에서 '예의 바른 프로그램'으로 만드는 방법까지 살펴보겠습니다.
# 메서드 호출로 종료하기
exit
exit!
abort("She cannot take any more of this, Captain!")
# ...또는 예외를 잡지 않아서 종료하기
raise("Destroyed...")
fail
# ...또는 프로그램이 자연스럽게 끝나도록 두기 :)
종료 코드(Exit Code)의 이해
Linux와 macOS(OSX)에서 실행하는 모든 프로그램은 실행이 끝날 때 종료 상태 코드(exit status code)를 반환합니다. 평소에는 이 코드를 직접 볼 일이 없지만, 운영체제는 내부적으로 이 코드를 확인하여 프로그램이 정상적으로 종료되었는지, 아니면 오류가 발생했는지 판단합니다.
bash를 사용한다면 방금 실행한 프로그램의 종료 코드를 $? 환경 변수로 확인할 수 있습니다. 아래 예시는 실패했을 때와 성공했을 때의 종료 코드를 보여줍니다.
% ls this-file-doesnt-exist
ls: this-file-doesnt-exist: No such file or directory
% echo $?
1
% ls .
bm.rb
% echo $?
0
일반적으로 유닉스 계열 시스템의 규칙은 다음과 같습니다. 0은 성공, 0이 아닌 값은 실패를 의미합니다.
bash에서 && 연산자를 사용해 본 적이 있다면 이미 종료 상태 코드를 활용한 것입니다. 아래 예시에서는 프로그램 A가 성공한 경우에만 프로그램 B를 실행하라고 bash에 지시합니다.
% a && b
이 기법은 다양하게 응용할 수 있습니다. 예를 들어 에셋을 미리 컴파일한 뒤 CDN에 업로드하는 작업을 생각해 볼 수 있습니다.
% rake assets:precompile && rake cdn:upload_assets
Ruby에서 종료 상태 지정하기
Ruby는 종료 상태 처리의 대부분을 자동으로 해줍니다. 프로그램이 정상적으로 종료되면 "성공" 상태를 반환하고, 처리되지 않은 예외(uncaught exception)로 인해 실패하면 "실패" 상태를 반환합니다. 아래는 예외를 발생시키는 스크립트의 종료 코드를 확인하는 예입니다.
% ruby err.rb
err.rb:1:in `<main>': goodbye (RuntimeError)
% echo $?
1
그런데 처리되지 않은 예외 없이 깔끔하게 종료하면서도 "실패" 코드를 반환하고 싶다면 어떻게 할까요? 또는 커스텀 코드와 함께 오류 메시지를 반환하고 싶다면요?
다행히 방법은 아주 간단합니다. exit 함수에 인자를 전달하면 됩니다. 인자는 불리언(Boolean) 또는 정수(Integer)일 수 있습니다. 불리언이라면 true가 성공을 의미하고, 정수라면 0이 성공을 의미합니다.
exit(true) # "성공" 코드로 종료
exit(0) # "성공" 코드로 종료
exit(false) # "실패" 코드로 종료
exit(1) # "실패" 코드로 종료
exit(436) # 커스텀 실패 코드로 종료
exit의 내부 동작 원리
exit 메서드는 사실 SystemExit 예외를 발생시키는 방식으로 동작합니다. 이 예외가 처리되지 않으면 다른 예외와 마찬가지로 프로그램이 중단됩니다. 꽤 흥미로운 방식인데, 몇 가지 흥미로운 결과를 낳기도 합니다.
만약 모든 SystemExit 예외를 삼켜버린다면 어떻게 될까요? exit 메서드가 동작하지 않게 됩니다. 아래 예시에서 이를 확인할 수 있습니다.
begin
exit 100
rescue SystemExit => e
puts "Tried to exit with status #{ e.status }"
end
puts "...but it never exited, because we swallowed the exception"
# 출력 결과:
# Tried to exit with status 100
# ...but it never exited, because we swallowed the exception
이것이 바로 Exception을 절대 rescue해서는 안 되는 또 하나의 이유입니다. SystemExit는 Exception을 상속하기 때문에, Exception을 삼켜버리면 exit 기능이 망가집니다.
begin
exit
rescue Exception # 절대 이렇게 하지 마세요
puts "I just broke the exit function!"
end
사람이 읽을 수 있는 오류 메시지 출력하기
종료 코드는 기계에게 유용하지만, 우리 인간에게는 설명이 담긴 텍스트가 더 유용합니다. 다행히 운영체제는 오류 메시지 같은 출력을 위한 전용 스트림을 제공합니다. 바로 STDERR입니다.
STDERR에는 일반 IO 객체에 쓰듯이 작성할 수 있습니다. 아래 예시에서는 오류 메시지를 출력한 뒤 "오류" 상태로 종료합니다.
STDERR.puts("ABORTED! You forgot to BAR the BAZ")
exit(false)
역시 Ruby답게, STDERR에 메시지를 쓰고 오류 코드와 함께 종료하는 더 간결한 방법이 있습니다. 바로 abort 메서드를 사용하는 것입니다.
# STDERR에 메시지를 출력하고 오류 상태 코드로 종료합니다.
abort("ABORTED! You forgot to BAR the BAZ")
at_exit를 활용한 콜백 등록
Ruby는 프로그램이 종료될 때 호출되는 핸들러(handler)를 등록할 수 있게 해줍니다. 핸들러는 여러 개 등록할 수 있으며, 등록된 순서의 역순으로 호출됩니다. 사용 예시는 다음과 같습니다.
at_exit do
puts "handler 1"
end
at_exit do
puts "handler 2"
end
# 종료 시 "handler2\nhandler1" 순서로 출력됩니다.
흥미롭게도 종료 핸들러는 종료 코드를 덮어쓸 수 있습니다. 핸들러 내부에서 exit를 호출하면 되는데, 무한 루프에 빠질 것 같지만 실제로는 그렇지 않습니다. :)
at_exit do
exit 100
end
exit 0
# 이 프로그램은 종료 코드 100으로 종료됩니다.
콜백을 호출하지 않고 종료하고 싶다면 exit! 메서드를 사용하면 됩니다. 다만 의존하는 gem이나 서드파티 코드가 해당 콜백에 의존하고 있다면 문제가 발생할 수 있으니 주의해야 합니다.
흥미로운 이야기: at_exit의 창의적 활용
사실 많은 라이브러리들이 at_exit를 창의적이고, 어떤 사람들은 "해키(hacky)"라고 표현할 만한 방식으로 활용하고 있습니다. 대표적인 예가 Sinatra입니다. Sinatra는 at_exit 훅을 웹 애플리케이션을 부팅하는 수단으로 사용합니다. 이를 통해 서버를 시작하기 전에 모든 코드가 로드되었음을 보장할 수 있습니다. 실제 코드는 다음과 같습니다.
module Sinatra
class Application < Base
...
at_exit { Application.run! if $!.nil? && Application.run? }
end
Arkency 블로그의 "Are we abusing at_exit?"라는 글에서 이 주제에 대한 심도 있는 논의를 확인할 수 있으니, 관심 있는 분들은 참고해 보시기 바랍니다.