루비의 예외 계층 구조를 한 번이라도 살펴본 적이 있다면, 뭔가 낯선 부분을 발견했을 것입니다. RuntimeError나 NoMethodError 같은 익숙한 예외들 사이에 Errno::*라는 이상한 항목이 끼어 있습니다.
Exception
StandardError
...
SystemCallError
Errno::*
...
디스크가 가득 찬 상태에서 파일에 쓰기를 시도하거나, 불안정한 네트워크 환경에서 API 호출을 해본 경험이 있다면 이런 오류를 직접 마주해봤을 겁니다. 실제로 지금 당장, 존재하지 않는 파일을 열어보는 것만으로도 이 예외를 재현할 수 있습니다.
irb> File.open("badfilename.txt")
Errno::ENOENT: No such file or directory @ rb_sysopen - badfilename.txt
from (irb):9:in `initialize'
from (irb):9:in `open'
from (irb):9
from /Users/snhorne/.rbenv/versions/2.1.0/bin/irb:11:in `<main>'
그렇다면 Errno 예외는 도대체 무엇이며, 왜 다른 예외들과 다르게 취급될까요?
루비를 운영체제에 적응시키는 어댑터
Errno 예외는 본질적으로 하나의 어댑터(adapter)입니다. 운영체제가 내놓는 오류를 루비의 예외 체계와 연결해 주는 다리 역할을 하죠. 운영체제는 루비와 완전히 다른 방식으로 오류를 처리하기 때문에, 어떤 형태로든 어댑터가 반드시 필요합니다.
루비에서는 오류가 대부분 예외(exception) 형태로 보고됩니다. 반면 운영체제의 오류는 단순한 정수 값입니다. 그래서 루비는 발생 가능한 운영체제 오류 하나하나에 대응하는 예외 클래스를 정의하고, 이 클래스들을 모두 Errno라는 이름의 모듈(module) 안에 모아 두었습니다.
IRB에서 이 모듈에 속한 모든 예외를 확인할 수 있습니다. 그런데 그 개수가 어마어마합니다!
irb> Errno.constants
=> [:NOERROR, :EPERM, :ENOENT, :ESRCH, :EINTR, :EIO, :ENXIO, :E2BIG, :ENOEXEC, :EBADF, :ECHILD, :EAGAIN, :ENOMEM, :EACCES, :EFAULT, :ENOTBLK, :EBUSY, :EEXIST, :EXDEV, :ENODEV, :ENOTDIR, :EISDIR, :EINVAL, ...]
# 실제로는 이 외에도 150개가 넘는 상수가 정의되어 있습니다.
그런데 도대체 왜 이렇게 암호문 같은 이름을 쓰는 걸까요? ENOENT가 "파일을 찾을 수 없음"이라는 뜻이라는 걸 어떻게 짐작할 수 있겠습니까?
…사실 그 답은 아주 단순합니다.
libc에서 그대로 가져온 이름들
Errno 모듈을 처음 만든 사람은 C 표준 라이브러리(libc)에 정의된 에러 이름을 그대로 베껴 왔습니다. 즉 ENOENT는 C 언어에서 하나의 매크로(macro) 이름으로, 파일을 찾지 못할 때 운영체제가 반환하는 정수형 에러 코드를 담고 있는 것이죠.
따라서 각 코드가 정확히 어떤 상황에서 발생하는지 알고 싶다면 C 표준 라이브러리 문서를 찾아보는 것이 가장 확실한 방법입니다. 아래 표는 실무에서 자주 마주치는 에러 코드들을 정리한 것입니다.
| 코드 | 의미 |
|---|---|
| EPERM | 허가되지 않은 연산. 권한이 없으면 파일에 접근할 수 없습니다. |
| ENOENT | 파일이나 디렉터리를 찾을 수 없습니다. |
| EIO | 입출력(I/O) 오류. 주로 물리적인 읽기·쓰기 오류에 사용됩니다. |
| EBADF | 잘못된 파일 디스크립터. 예를 들어 읽기 전용으로 연 파일에 쓰려고 하면 이 오류가 발생합니다. |
| ECHILD | 자식 프로세스를 조작하려 했지만 자식 프로세스가 존재하지 않습니다. |
| ENOMEM | RAM이 부족하여 더 이상 가상 메모리를 할당할 수 없습니다. |
| EACCES | 접근 거부. 파일 권한이 요청된 작업을 허용하지 않습니다. |
| ENOTBLK | HDD 같은 장치(device)가 필요한 자리에 일반 파일을 지정하려고 했습니다. |
| EBUSY | 리소스 사용 중. 공유할 수 없는 시스템 자원이 이미 사용 중입니다. 예를 들어 현재 마운트된 파일시스템의 루트인 파일을 삭제하려고 하면 이 오류가 발생합니다. |
| EEXIST | 파일이 이미 존재함. 새 파일만 지정해야 하는 문맥에서 기존 파일이 지정되었습니다. |
| ENOTDIR | 디렉터리가 필요한 위치에 디렉터리가 아닌 파일이 지정되었습니다. |
| EISDIR | 파일이 디렉터리임. 디렉터리는 쓰기 모드로 열거나, 하드 링크를 생성·삭제할 수 없습니다. |
| EINVAL | 잘못된 인자. 라이브러리 함수에 엉뚱한 인자를 전달하는 등 다양한 문제를 나타낼 때 사용됩니다. |
| EMFILE | 현재 프로세스가 너무 많은 파일을 열어 두어 더 이상 파일을 열 수 없습니다. 중복된 디스크립터도 이 제한에 포함됩니다. |
| EFBIG | 파일이 너무 큼. 파일 크기가 시스템이 허용하는 한도를 초과합니다. |
| ENOSPC | 장치에 남은 공간 없음. 디스크가 가득 차서 파일 쓰기 작업이 실패했습니다. |
| ESPIPE | 유효하지 않은 seek 연산(예: 파이프에 대한 seek). |
| EROFS | 읽기 전용 파일시스템의 내용을 수정하려고 시도했습니다. |
| EPIPE | 깨진 파이프(broken pipe). 파이프 반대편에서 데이터를 읽는 프로세스가 없습니다. |
| ENOTSOCK | 소켓이 필요한 위치에 소켓이 아닌 파일이 지정되었습니다. |
| ENETUNREACH | 원격 호스트가 속한 서브넷에 도달할 수 없어 소켓 작업이 실패했습니다. |
| ENETRESET | 원격 호스트가 다운되어 네트워크 연결이 초기화(reset)되었습니다. |
| ECONNABORTED | 네트워크 연결이 로컬 쪽에서 중단되었습니다. |
| ECONNRESET | 원격 머신 재부팅이나 복구 불가능한 프로토콜 위반 등, 로컬 호스트가 통제할 수 없는 이유로 네트워크 연결이 종료되었습니다. |
| ENOBUFS | 커널의 입출력 버퍼가 모두 사용 중입니다. GNU에서는 이 오류가 항상 ENOMEM과 동의어로 취급되며, 네트워크 작업에서 둘 중 하나가 반환될 수 있습니다. |
| EISCONN | 이미 연결된 소켓에 대해 connect를 다시 시도했습니다. |
| ENOTCONN | 소켓이 아무곳에도 연결되어 있지 않습니다. 전송 목적지를 지정하지 않은 채 소켓으로 데이터를 보내려 할 때 이 오류가 발생합니다. |
| EDESTADDRREQ | 소켓에 기본 목적지 주소가 설정되어 있지 않습니다. connectionless 소켓에서 connect로 목적지를 지정하지 않고 데이터를 전송하려 할 때 발생합니다. |
| ESHUTDOWN | 소켓이 이미 종료(shutdown)되었습니다. |
| ETIMEDOUT | 타임아웃이 지정된 소켓 작업이 제한 시간 내에 응답을 받지 못했습니다. |
| ECONNREFUSED | 원격 호스트가 네트워크 연결을 거부했습니다(보통 요청된 서비스가 실행 중이 아니기 때문입니다). |
| EHOSTDOWN | 요청된 네트워크 연결의 원격 호스트가 다운되어 있습니다. |
| EHOSTUNREACH | 요청된 네트워크 연결의 원격 호스트에 도달할 수 없습니다. |
| ENOTEMPTY | 빈 디렉터리여야 하는데 디렉터리가 비어 있지 않습니다. 보통 디렉터리 삭제를 시도할 때 발생합니다. |
| EPROCLIM | fork 시도로 인해 사용자별 생성 가능한 프로세스 수 한도(RLIMIT_NPROC)를 초과하게 됩니다. |