실행시간 오류는 컴퓨터 시스템의 자원 부족으로 발생하기도 한다. 운영 체제는 가능한 한 프로그램에 자원을 충분히 제공하려 노력하지만, 물리적 한계는 극복할 수 없다. 시스템 자원은 여러 프로그램이 공유하며, 프로그램은 운영 체제에 자원을 요청해 빌려 쓴 뒤 다시 반환한다. 만약 어떤 프로그램이 자원을 빌려가기만 하고 반납하지 않는 일을 반복한다면, 머지 않아 전체 시스템 자원이 소진되어 그 프로그램은 물론 다른 프로그램에까지 영향을 미칠 것이다.

시스템 자원에는 프로세서, 메모리, 저장 매체(입출력 전송과 저장 공간), 네트워크 통신 등 대부분의 장치가 포함된다. 파이썬 프로그래머는 프로세서나 메모리의 관리에 대해서는 직접적으로 신경쓰지는 않아도 된다. 그 외의 장치를 다룰 때는 그 장치를 제어하는 라이브러리의 문서에서 취급 방법을 확인해야 한다. 장치마다 차이는 있겠지만 공통되는 점은 자원을 요청하여 사용한 후 다시 반환하는 작업, 즉 뒷정리를 해야 한다는 점이다.

뒷정리가 깔끔해야 좋다는 것은 누구나 알지만, “까먹기” 쉽다는 것이 문제다. 초보 프로그래머도 시스템 자원을 획득한 뒤에 반환하는 것을 종종 잊어버린다. 이 절에서는 파일을 열고 닫는 과정을 살펴보며, 뒷정리를 잊어버리지 않도록 예방하는 방법을 알아본다.

9.5.1 파일 열고 닫기

파일은 뒷정리가 필요한 대표적인 시스템 자원이다. 파일은 읽거나 쓰기 전, 먼저 열어야 하고, 사용을 마친 후에는 닫아야 한다. 파일을 여는 것은 자원을 요청하는 것이고, 파일을 닫는 것은 뒷정리에 해당된다. 파일을 열고 닫는 것 자체는 쉽다. open() 함수로 파일을 열어 파일 객체를 얻고, 파일 객체의 close() 메서드를 호출해 파일을 닫으면 된다.

다음은 텍스트 파일을 열고, 파일에서 숫자를 읽어 합계를 구해 출력하고, 파일을 닫는 프로그램이다.

코드 9-43 파일을 열고, 사용하고, 닫는 프로그램

total = 0                 # 합계를 저장할 변수
file = open('years.txt')  # 파일 열기
for line in file:         # 파일의 숫자를 한 행씩 합한다
    total += int(line)
print(total)              # 결과 출력
file.close()              # 파일 닫기

years.txt 파일이 작성되어 있다면, 이 프로그램은 그 파일 내용을 똑같이 화면에 출력한다.

실행 결과:

1789
1848
1905
1917
1968
today

파일을 자유롭게 다루려면 이 외에도 알아야 할 점이 있는데, 11.4 파일과 디렉터리에서 알아볼 것이다. 지금은 파일을 열고 닫는 과정에 집중하며 이어지는 내용을 살펴보자.

9.5.2 try 문에서 뒷정리하기

파일을 열고 닫는 게 뭐 그리 어려울까? 닫는 것을 잊어버리지만 않으면 되는 데. 하지만 생각보다 그 간단한 것을 놓치기가 쉽다. 코드 9-43은 읽어들이는 파일에서 각 행이 모두 숫자로만 되어 있다면 문제 없이 동작할 것이다. 하지만 파일에 숫자가 아닌 텍스트가 있다면 int(line)을 평가할 때 ValueError 예외가 발생한다. try 문을 이용해 이 예외를 처리하자.

코드 9-44 예외를 처리했지만 문제가 생겼다

try:
    total = 0                 # 합계를 저장할 변수
    file = open('years.txt')  # 파일 열기
    for line in file:         # 파일의 숫자를 한 행씩 합한다
        total += int(line)

except ValueError:            # ValuError 예외 처리
    print('숫자가 아닌 문자열이 있네요!')

else:                         # 예외가 발생하지 않은 경우
    print(total)              # 결과 출력
    file.close()              # 파일 닫기

try 문, except 절, else 절을 적절히 활용해 예외를 처리했다. 그렇다면 이제 이 프로그램에는 문제가 없을까? 아차, except 절에서 파일 닫는 것을 까먹었다! 이처럼 뒷정리는 자칫 잊어버리기가 쉽다. 좀더 주의를 기울여서 except 절에도 파일을 닫도록 하면, 모든 경우에 파일을 꼼꼼하게 닫을 수 있을 것이다.

하지만 의문이 남는다. 처리해야 할 예외의 종류가 많아지면 except 절도 많아질텐데, 그때마다 모든 except 절에 파일을 닫는 코드를 빠트리지 않고 추가할 수 있을까? 더욱이 그렇게 되면 동일한 코드가 여러 절에 중복될 텐데, 중복 코드를 하나로 통일할 방법은 없을까?

finally 절

어떤 작업을 마친 후 뒷정리하는 코드를 작성하기 위해, try 문은 finally 절을 지원한다. ‘finally’는 ‘마지막으로’라는 뜻이며, try 문은 마지막에 항상 finally 절의 코드를 실행한다. else 절은 예외가 발생하지 않았을 때만 실행되지만, finally 절은 예외가 발생했을 때도 실행된다는 점이 다르다. finally 절은 else 절과 마찬가지로 선택사항이며, try 문에서 else 절보다도 아래인 맨 마지막 위치에만 작성할 수 있다. try 문의 작성 양식에 finally 절을 추가해 다음과 같이 정리해두자.

try:
    예외가 발생할 수 있는 명령의 코드 블록
    ...
except 예외종류:
    예외종류에 해당하는 예외가 발생했을 때의 대응 명령 코드 블록
    ...
(필요에 따라 except 절을 추가로 작성)
else:
    예외가 발생하지 않은 경우 이어서 실행할 코드
finally:
    항상 마지막으로 실행할 코드 블록

finally 절은 예외 발생 여부와 관계없이 언제나 실행되므로, 파일을 닫는 명령을 모든 블록 대신 finally 절에만 작성하면 된다. 코드 9-44을 다음과 같이 수정할 수 있다.

코드 9-45 finally 절로 파일 닫기

try:
    total = 0                 # 합계를 저장할 변수
    file = open('years.txt')  # 파일 열기
    for line in file:         # 파일의 숫자를 한 행씩 합한다
        total += int(line)

except ValueError:            # ValuError 예외 처리
    print('숫자가 아닌 문자열이 있네요!')

else:                         # 예외가 발생하지 않은 경우
    print(total)              # 결과 출력

finally:                      # (항상) 마지막으로
    file.close()              # 파일 닫기

else 절과 except 절에서 파일을 닫는 코드를 삭제하고, finally 절에서만 파일을 닫도록 했다. 이제 나중에 except 절이 더 추가되더라도 실수로 파일을 닫지 않을 걱정은 하지 않아도 된다. try 절에서 시작한 작업은 finally 절에서 마무리한다는 것을 기억하자.

9.5.3 with 문으로 편하게 살자

뒷정리를 할 때는 신경써야 할 요소가 많다. 함수 호출, if 문, for 문, while 문, try 문 등 프로그램의 흐름을 제어하는 코드 블록이 복잡하게 뒤엉킨 코드에서는 자원 반환을 빠트리기가 쉽다. 파이썬은 이를 방지하고 프로그래머를 편하게 해주기 위해 뒷정리를 자동으로 처리하는 with 문을 제공한다.

with 문은 어떤 객체의 사용 준비 과정과 뒷정리 과정을 자동으로 실행하는 문법이다. 다음과 같은 간단한 양식으로 작성한다.

with 객체 as 이름:
    본문

with 문에 작성한 객체는 지정한 이름으로 본문에서 사용할 수 있다. with 문은 본문의 내용이 실행되기 전에 객체의 준비 과정을 자동으로 수행하고, 본문의 실행이 끝난 후에 객체의 뒷정리도 자동으로 수행한다. 파일의 경우에는 with 문이 종료될 때 자동으로 파일의 close() 메서드를 실행한다. 코드 9-45를 다음과 같이 수정할 수 있다.

코드 9-46 with 문이 자동으로 파일을 닫아준다

with open('years.txt') as file:   # 파일을 열고 본문을 실행
    try:
        total = 0                 # 합계를 저장할 변수
        for line in file:         # 파일의 숫자를 한 행씩 합한다
            total += int(line)
    
    except ValueError:            # ValueError 예외 처리
        print('숫자가 아닌 문자열이 있네요!')
    
    else:                         # 예외가 발생하지 않은 경우
        print(total)              # 결과 출력

# with 문이 종료될 때, 파일은 저절로 닫힌다

with 문을 사용함으로써 finally 절과 파일을 닫는 명령은 불필요해졌다. with 문은 예기치 않은 문제로 본문의 실행이 중단되는 경우에도 뒷정리 과정을 자동으로 실행해준다. 덕분에 파일을 언제 닫아야 할지 고심하거나 실수하는 일이 없어진다. 파일과 같이 뒷정리가 필요한 자원은 직접 관리하는 것보다 with 문을 사용하는 것이 권장된다.

with 문의 동작 원리

with 문이 객체의 종류와 관계없이 “알아서” 처리해주는 것이 신기하지 않은가? 그 원리는 이미 8장에서 알아보았다. 바로 객체에 정의된 이중 밑줄 메서드를 호출하는 것이다. with 문은 객체의 준비 과정을 위해 객체의 __enter__() 메서드를 호출하고, 뒷정리를 위해 객체의 __exit__() 메서드를 호출한다.

두 메서드를 정의하여 with 문을 지원하는 클래스를 정의해 보자.

코드 9-47 with 문을 지원하는 클래스 정의하기

class NetworkConnection:
    """네트워크 연결을 나타내는 클래스"""
    
    def __init__(self, url):
        """인스턴스를 초기화한다."""
        self.url = url
        self.is_connected = False
    
    def connect(self):
        """네트워크에 연결한다."""
        print('네트워크에 연결합니다.')
        self.is_connected = True
    
    def disconnect(self):
        """네트워크 연결을 중단한다."""
        print('네트워크 연결을 중단합니다.')
        self.is_connected = False
    
    def read(self):
        """네트워크에서 데이터를 읽어들인다."""
        return '네트워크에서 읽어들인 데이터'
    
    def __enter__(self):
        """객체 사용을 준비한다."""
        self.connect()
        return self
    
    def __exit__(self, exc_type, exc_value, traceback):
        """객체 사용을 마치고 뒷정리한다."""
        self.disconnect()

# 네트워크에 연결하여 데이터를 읽은 후 연결을 종료한다
with NetworkConnection('https://bakyeono.net') as connection:
    print(connection.read())

실행 결과:

네트워크에 연결합니다.
네트워크에서 읽어들인 데이터
네트워크 연결을 중단합니다.

위 코드의 NetworkConnection 클래스는 실제로 네트워크에 접속하는 기능을 수행하지는 않지만, with 문을 통한 자원 관리가 어떤 식으로 이루어지는지 살펴보기에는 적당하다. __enter__() 메서드는 객체를 사용할 수 있도록 준비하여 with 문에 반환하고, __exit__() 메서드는 객체의 뒷정리를 수행한다. __exit__() 메서드의 여러 매개변수는 with 문 안에서 예외가 발생했을 때 처리하기 위한 것이다.

파이썬 용으로 제공되는 시스템 자원을 활용하는 라이브러리는 대부분 with 문을 지원한다. 파일이나 뒷정리가 필요한 자원을 사용할 때는 with 문을 사용하는 것을 당연하게 받아들이고, 간혹 with 문을 쓸 수 없을 때는 try 문에서 finally 절을 사용해 항상 뒷정리가 이루어질 수 있도록 주의하자.

이상으로 프로그래밍 일반의 오류 개념 대처방법, 파이썬에서 예외 처리를 수행하는 방법 등을 전반적으로 알아보았다. 여러분은 오류 해결을 위한 지식과 도구를 가졌고 남은 것은 연습뿐이다. 오류는 프로그래밍 작업에서 숱하게 맞딱드리게 될 상황이니 앞으로 익숙해질 기회는 얼마든지 있을 것이다. 오류를 두려워하지 말고 반가운 퍼즐로 받아들일 수 있기를 바란다.