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

루비의 낯선 Errno 예외, 제대로 이해하기

루비의 예외 계층 구조를 한 번이라도 살펴본 적이 있다면, 뭔가 낯선 부분을 발견했을 것입니다. RuntimeErrorNoMethodError 같은 익숙한 예외들 사이에 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자식 프로세스를 조작하려 했지만 자식 프로세스가 존재하지 않습니다.
ENOMEMRAM이 부족하여 더 이상 가상 메모리를 할당할 수 없습니다.
EACCES접근 거부. 파일 권한이 요청된 작업을 허용하지 않습니다.
ENOTBLKHDD 같은 장치(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빈 디렉터리여야 하는데 디렉터리가 비어 있지 않습니다. 보통 디렉터리 삭제를 시도할 때 발생합니다.
EPROCLIMfork 시도로 인해 사용자별 생성 가능한 프로세스 수 한도(RLIMIT_NPROC)를 초과하게 됩니다.