운영 체제는 프로그램이 필요로 하는 자원을 충분히 제공하려고 노력하지만, 자원은 한정되어 있다. 프로세서, 메모리, 저장 매체(입출력 전송과 저장 공간) 등의 시스템 자원은 여러 프로그램이 공유하기 때문에 하나의 프로그램이 독점해서는 안 된다.

프로세서 자원은 운영 체제가 관리해주기 때문에 신경쓰지 않아도 된다. 메모리 자원도 파이썬 인터프리터가 필요한 만큼 빌렸다가 반납하는 일을 대신해 준다. 하지만 그 외의 장치(저장 매체의 파일, 네트워크 포트 등)를 다룰 때는 필요한 주의가 필요하다. 즉, 사용한 후에 뒷정리를 해야 한다.

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

9.5.1 파일 열고 닫기

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

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

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

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-44는 읽어들이는 파일에 숫자가 아닌 텍스트가 있는 경우, int(line)을 평가할 때 ValueError 예외가 발생한다. 다음은 try 문을 이용해 이 예외를 처리한 것이다.

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

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 절에 파일을 닫는 코드를 빠트리지 않고 추가할 수 있을까? 파일을 닫는 동일한 코드가 여러 절에 중복될 텐데, 중복 코드를 하나로 통일할 방법은 없을까?

finally 절

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

try:
    예외가 발생할 수 있는 코드 블록
    ...

except 예외종류 as 이름:
    예외종류에 해당하는 예외가 발생했을 때 실행할 코드 블록
    ...

(필요에 따라 except 절을 추가로 작성)

else:
    예외가 발생하지 않은 경우 실행할 코드 블록

finally:
    항상 마지막으로 실행할 코드 블록

파일을 닫는 명령을 finally 절에만 작성해두면 언제든 파일이 닫힐 것이다. 코드 9-45를 다음과 같이 수정할 수 있다.

코드 9-46 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 절에서 마무리한다는 것을 기억하자.

개념 정리

  • try 문 안에서 뒷정리가 필요한 자원을 사용하는 경우, finally 절에서 뒷정리해야 한다.

9.5.3 with 문으로 편하게 살자

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

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

with 객체 as 이름:
    본문

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

코드 9-47 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 문은 객체의 종류에 관계없이 “알아서” 준비와 뒷정리를 해 준다. 그 비밀은 with 문이 객체에 정의된 이중 밑줄 메서드를 호출하는 것이다. with 문은 객체의 준비를 위해 객체의 __enter__() 메서드를 호출하고, 뒷정리를 위해 객체의 __exit__() 메서드를 호출한다.

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

코드 9-48 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 절을 사용해 항상 뒷정리가 이루어질 수 있도록 하자.

개념 정리

  • with 문을 이용하면 뒷정리가 자동으로 된다.

이 장에서는 오류의 개념과 대처 방법, 예외 처리 방법 등을 알아보았다. 오류는 프로그래밍을 할 때 수없이 마주하게 되는 상황이다. 오류를 두려워하지 말고 재미난 퍼즐로 받아들여 열심히 풀어 나가자.