Python Context Manager에 대해서 알아보자
파이썬을 사용하다 보면, try/except 구문을 자주 사용한다. try/except 구문은 에러가 발생할 수 상황에서 예외사항을 발견해서 별도의 처리를 할 때 유용하다. 예컨대, ValueError 일 때는 에러를 로그에 남기고 싶다면, try/except 구문을 통해 이를 구현할 수 있다.
그런데 코드를 작성하다 보면, 에러가 나든 아니면 에러가 나지 않든 상관없이 실행하고 싶은 구문이 있을 수 있다. 예컨대, DB 연결을 맺는다고 해보자. 개발자는 정상적으로 작업을 끝내거나 에러가 발생하더라도 연결했던 DB 커넥션은 정리하고 싶을 수 있다. 왜냐하면 서버와 DB의 연결 입장에서는 서버가 에러가 발생하든 중요하지 않기 때문이다. 누가 먼저 끊어주기만을 기다릴지도 모른다.
이 문제를 해결하기 위해서는 간단하게 try/except 에 finally 부분을 추가하면 된다. Finally는 try/except 구문에서 어떤 결과가 발생하든 반드시 실행된다. 따라서 결과와 상관없이 실행해야 할 코드가 있다면 finally 쪽에 추가하면 된다.
그런데, 문제는 똑같은 작업이 여러 번 필요할 때 발생한다. 매번 동일한 구문을 복잡하게 여러 번 사용해야 한다. File을 열고 닫는 부분을 100군데 사용해야 한다고 가정해 보자. 그러면 매번 할 때마다 try/except/finally 구문을 사용해야 한다. 만약 그중에 하나라도 실수한다면, 동일한 결과를 얻지 못할 수도 있다.
이때 사용할 수 있는 게 바로 Context Manager(컨텍스트 매니저) 이다.
컨텍스트 매니저는 리소스 관리를 간소화하고 코드의 가독성을 향상시키는 강력한 도구이다. with 문과 함께 사용되는 객체로, with 블록 내에서 리소스에 안전하게 접근하고 사용할 수 있으며, 블록 종료 시 리소스가 자동으로 정리된다.
컨텍스트 매니저의 장점은 다음과 같다.
리소스 관리 간소화: 리소스 할당 및 해제를 자동화하여 코드의 간결성과 명확성을 향상시킨다.
예외 처리 강화: with 블록 내에서 예외가 발생하더라도 리소스가 정확하게 해제된다.
코드 가독성 향상: 리소스 관리 코드를 with 블록으로 분리하여 코드의 가독성을 높인다.
오류 감소: with 블록 외부에서 리소스를 직접 관리하지 않기 때문에 오류 가능성이 감소한다.
위와 같은 장점 덕분에 컨텍스트 매니저는 다양한 분야에서 활용된다. 대표적인 활용 사례는 다음과 같다.
파일 처리: 파일 열고 닫는 작업을 with 블록으로 간결하게 처리한다.
데이터베이스 연결: 데이터베이스 연결 및 해제를 자동화하여 리소스 낭비를 방지한다.
네트워크 연결: 소켓 연결 및 해제를 컨텍스트 매니저로 관리하여 코드를 간소화한다.
잠금 구현: with 블록 내에서 리소스에 대한 잠금을 설정하고 해제하여 동시 접근 문제를 해결한다.
임시 디렉터리 생성: with 블록 내에서 임시 디렉터리를 생성하고 사용 후 자동으로 삭제한다.
문맥 관리: with 블록 내에서 특정 설정을 적용하고 블록 종료 시 원래 설정으로 복원한다.
테스트 환경 설정: with 블록 내에서 테스트 환경을 설정하고 블록 종료 시 환경을 정리한다.
그렇다면 컨텍스트 매니저를 구현하려면 어떻게 해야 할까.
앞서 설명했던 것과 같이 컨텍스트 매니저를 with 블록과 함께 사용된다. with 블록은 특정 객체가 주어지면, 해당 객체의 __enter__ 함수를 실행한다. 이는 with 블록으로 이제 진입하겠다는 의미라고 볼 수 있다. 만약 __enter__함수가 정의되어 있지 않으면 에러가 발생한다.
또한, with 블록을 빠져나오게 되면, __exit__함수를 호출한다. 이는 finally와 유사한 역할을 하는데, 에러가 나더라도 __exit__함수는 실행된다. 또한 with 블록 내에서 발생한 에러에 대해 어떻게 처리할지도 __exit__ 함수에서 정의할 수 있다. __exit__함수에서 에러 핸들링을 하기 때문에 실제로 에러가 발생했는지에 대한 정보를 파라미터로 전달받아야 한다. 따라서 with 블록이 종료될 때, (error_type, error_value, traceback)과 같이 세 개의 파라미터가 __exit__ 함수로 전달된다.
다음은 간단하게 만들어 본 샘플 컨텍스트 매니저이다. 여타 다른 객체와 같이 class로 정의하고, 생성자를 추가한다. 그리고 __enter__와 __exit__ 함수를 정의하면, 컨텍스트 매니저의 역할을 할 수 있는 객체가 된다.
실행결과를 보면 실행되는 함수의 순서를 알 수 있다.
- 먼저 MyContextManager() 객체가 생성되면서 생성자인 __init__ 함수가 호출된다.
- with 블록이 실행되면서 __enter__ 함수가 실행된다. __enter__ 함수의 반환값을 obj 심벌이 가리키게 된다.
- with 블록 안의 내용이 실행된다.
- with 블록이 종료되면서 __exit__ 함수가 실행된다.
여기서 한 가지 알아두어야 할 점이 있다. 바로, as obj로 받아서 with 블록 안에서 사용한 리소스는 정리될 수 있지만, obj라는 심벌은 여전히 남아 있다는 점이다. 심벌이 저장되어 있는지 확인하기 위해서, globals() 혹은 locals()라는 함수를 실행하여 딕셔너리를 구하고, key 중에 "obj"라는 이름이 있는지 확인해 보면 다음과 같다.
이번에는 직접 활용가능한 컨텍스트 매니저를 만들어보려고 한다. 로그 관리 컨텍스트 매니저에서는 print()로 출력을 할때, stdout이 아니라, 특정 파일 경로로 저장되도록 하고자 한다.
LogManager를 시작할 때, 미리 로그를 저장할 파일에 대한 경로를 전달받는다. with 블록이 시작될때, 증분으로 로그가 쌓일 수 있도록 'a' 모드로 파일을 열어 파일 객체를 생성한다. 해당 파일 객체를 현재 sys.stdout으로 설정한다. 왜냐하면, print() 함수가 내부적으로 sys.stdout에 값을 쓰도록 되어 있다.
LogManager의 with 블록이 종료되면 파일 객체를 close하고, sys.stdout도 원래의 객체로 돌려놓는다. 그래야 그 다음에 print 함수를 실행했을때, 터미널로 정상적으로 값을 쓸 수 있다. 만약 이 부분을 변경하지 않는다면 print 할때 에러가 발생할 것이다. 왜냐하면 with 문의 종료로 인해 파일 객체는 아미 closed 된 상태인데, 해당 파일에 쓰기를 하려고 했기 때문이다.
요컨대, 상태를 일시적으로 변경했다가 다시 원상 복귀하거나, 리소스를 정리해야하는 경우에 컨텍스트 매니저를 활용하면 간편하게 문제를 해결할 수 있다. 컨텍스트 매니저는 일반 클래스와 동일하게 정의할 수 있지만, 내부적으로 __enter__와 __exit__ 함수가 정의되어 있어야 한다. with 블록이 시작되면 __enter__ 함수가 호출되고, with 블록이 종료되면 __exit__ 함수가 실행된다. __exit__ 함수에서 에러가 발생하는 경우까지 모두 핸들링한다.
https://peps.python.org/pep-0343/
잘못된 내용은 댓글로 남겨주시면 빠르게 정정하겠습니다. 많은 가르침 부탁드립니다.
이메일로 블로그 받아보기: https://growthminder.substack.com/subscribe