[Python 100일 챌린지] Day 65 - 컨텍스트 매니저
파일을 닫는 걸 깜빡한 적 있나요? 🚪🔒
파일을 열고 닫지 않으면 메모리 누수가 발생해요!
with문은 이런 문제를 한 방에 해결해줘요. 파일, 데이터베이스 연결, 네트워크 소켓… 모든 리소스를 자동으로 정리해줘요! Django ORM, SQLAlchemy 같은 프레임워크들도 컨텍스트 매니저를 적극 활용하고 있답니다. 😊오늘은
with문의 비밀을 파헤치고, 나만의 컨텍스트 매니저를 만들어봐요!(30분 완독 ⭐⭐⭐)
🎯 오늘의 학습 목표
📚 사전 지식
- Phase 5: 파일 입출력과 예외 처리
- Phase 4: 클래스와 매직 메서드
🎯 학습 목표 1: with 문과 컨텍스트 매니저 이해하기
한 줄 설명
컨텍스트 매니저 = 자동으로 정리해주는 집사 🤵♂️🧹
문 열고(시작), 일 하고, 문 닫는(정리) 걸 자동으로 해줘요!
1.1 문제 상황: 리소스 관리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 나쁜 예: 파일을 닫지 않음 ❌
file = open('data.txt', 'r')
data = file.read()
# file.close()를 잊어버림!
# 조금 나은 예: try-finally 사용
file = open('data.txt', 'r')
try:
data = file.read()
finally:
file.close() # 항상 실행됨
# 최선: with 문 사용 ✅
with open('data.txt', 'r') as file:
data = file.read()
# 자동으로 file.close() 호출!
💡 실생활 비유: 호텔 방문할 때, 들어가면 불이 자동으로 켜지고(enter), 나가면 자동으로 꺼지는(exit) 것처럼,
with문은 시작과 끝을 자동으로 관리해줘요!
왜 with를 쓸까요? 🤔
- 파일을 닫는 걸 깜빡하지 않아요
- 예외가 발생해도 안전하게 정리돼요
- 코드가 깔끔해요
1.2 컨텍스트 매니저의 장점
1
2
3
4
5
6
# 여러 리소스 관리
with open('input.txt', 'r') as infile, \
open('output.txt', 'w') as outfile:
for line in infile:
outfile.write(line.upper())
# 두 파일 모두 자동으로 닫힘 ✨
실무에서 이렇게 써요! 💼
- 파일 입출력
- 데이터베이스 연결
- 네트워크 소켓
- 뮤텍스 락
🎯 학습 목표 2: enter__와 __exit 메서드
한 줄 설명
__enter__와 __exit__ = with 문의 핵심 엔진 ⚙️🔧
이 두 메서드만 구현하면 어떤 클래스든 with 문에서 사용할 수 있어요!
2.1 기본 컨텍스트 매니저
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MyContext:
"""가장 기본적인 컨텍스트 매니저"""
def __enter__(self):
"""with 블록 진입 시 호출"""
print("__enter__ 호출됨")
return self
def __exit__(self, exc_type, exc_value, traceback):
"""with 블록 종료 시 호출"""
print("__exit__ 호출됨")
# exc_type: 예외 타입
# exc_value: 예외 값
# traceback: 트레이스백
return False # 예외를 재발생
# 사용
with MyContext() as ctx:
print("with 블록 내부")
print(f"ctx: {ctx}")
# 출력:
# __enter__ 호출됨
# with 블록 내부
# ctx: <__main__.MyContext object at ...>
# __exit__ 호출됨
이렇게 동작해요! 🎬
with문 시작 →__enter__()호출해요as변수에 반환값을 저장해요- 블록 실행해요
- 블록 종료 →
__exit__()호출해요 (예외 발생 여부 관계없이!)
2.2 예외 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class SafeContext:
"""예외를 처리하는 컨텍스트 매니저"""
def __enter__(self):
print("리소스 획득 🔓")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("\n리소스 해제 🔒")
if exc_type is not None:
print(f"예외 발생: {exc_type.__name__}: {exc_value}")
return True # 예외를 억제 (전파하지 않음)
return False
# 정상 실행
print("=== 정상 실행 ===")
with SafeContext():
print("정상 작업")
# 예외 발생
print("\n=== 예외 발생 ===")
with SafeContext():
print("작업 중...")
raise ValueError("오류 발생!")
print("이 줄은 실행되지 않음")
print("프로그램 계속 실행") # 예외가 억제되어 실행됨
💡 핵심:
__exit__가True를 반환하면 예외를 억제하고,False를 반환하면 예외를 전파해요!
2.3 실전: 파일 매니저
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class FileManager:
"""파일을 안전하게 관리하는 컨텍스트 매니저"""
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
print(f"📂 파일 열기: {self.filename}")
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_value, traceback):
if self.file:
print(f"📂 파일 닫기: {self.filename}")
self.file.close()
if exc_type is not None:
print(f"❌ 오류 발생: {exc_value}")
return False
# 사용
with FileManager('test.txt', 'w') as f:
f.write("Hello, World!\n")
f.write("Context Manager Test\n")
실무 활용! 🎯
- 파일이 항상 닫혀요
- 예외 발생해도 안전해요
- 코드가 간결해요
🎯 학습 목표 3: contextlib 모듈 활용하기
한 줄 설명
contextlib = 컨텍스트 매니저의 지름길 🛤️✨
클래스 없이 데코레이터로 간편하게 컨텍스트 매니저를 만들어요!
3.1 @contextmanager 데코레이터
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from contextlib import contextmanager
@contextmanager
def my_context():
"""제너레이터를 사용한 컨텍스트 매니저"""
print("Setup") # __enter__
try:
yield "리소스 객체"
finally:
print("Teardown") # __exit__
# 사용
with my_context() as resource:
print(f"사용 중: {resource}")
# 출력:
# Setup
# 사용 중: 리소스 객체
# Teardown
얼마나 간편한가요? 🎉
- 클래스 없이 함수로 만들어요
yield전은__enter__예요yield후는__exit__예요
💡 실생활 비유: 전기밥솥처럼, 시작 버튼 누르고(yield 전), 밥 먹고(yield), 자동으로 보온 모드(yield 후)로 전환돼요!
3.2 실전: 타이머 컨텍스트 매니저
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from contextlib import contextmanager
import time
@contextmanager
def timer(name):
"""실행 시간을 측정하는 컨텍스트 매니저 ⏱️"""
start = time.time()
print(f"[{name}] 시작")
try:
yield
finally:
end = time.time()
print(f"[{name}] 완료: {end - start:.4f}초")
# 사용
with timer("데이터 처리"):
time.sleep(0.5)
result = sum(range(1000000))
# 출력:
# [데이터 처리] 시작
# [데이터 처리] 완료: 0.5234초
실무에서 이렇게 써요! 💡
- 성능 측정
- 프로파일링
- 벤치마크
3.3 실전: 데이터베이스 트랜잭션
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from contextlib import contextmanager
class Database:
"""데이터베이스 연결 시뮬레이션"""
def __init__(self):
self.connected = False
self.in_transaction = False
def connect(self):
self.connected = True
print("📡 데이터베이스 연결")
def disconnect(self):
self.connected = False
print("📡 데이터베이스 연결 해제")
def begin_transaction(self):
self.in_transaction = True
print("🔄 트랜잭션 시작")
def commit(self):
self.in_transaction = False
print("✅ 트랜잭션 커밋")
def rollback(self):
self.in_transaction = False
print("❌ 트랜잭션 롤백")
@contextmanager
def transaction(self):
"""트랜잭션 컨텍스트 매니저"""
self.begin_transaction()
try:
yield self
self.commit()
except Exception as e:
self.rollback()
raise
# 사용
db = Database()
db.connect()
# 정상 트랜잭션
print("\n=== 정상 트랜잭션 ===")
with db.transaction():
print(" 작업 1 수행")
print(" 작업 2 수행")
# 오류 발생 트랜잭션
print("\n=== 오류 트랜잭션 ===")
try:
with db.transaction():
print(" 작업 1 수행")
raise ValueError("오류 발생!")
print(" 작업 2 수행") # 실행되지 않음
except ValueError:
print(" 예외 처리됨")
db.disconnect()
실무 활용! 💼
- 데이터베이스 트랜잭션
- 작업이 성공하면 커밋, 실패하면 롤백해요
- 데이터 일관성을 보장해요
3.4 suppress: 예외 무시
1
2
3
4
5
6
7
8
9
10
11
12
13
from contextlib import suppress
# 파일이 없어도 오류 발생하지 않음 🛡️
with suppress(FileNotFoundError):
with open('nonexistent.txt', 'r') as f:
print(f.read())
print("프로그램 계속 실행")
# 여러 예외 무시
with suppress(FileNotFoundError, PermissionError, IOError):
with open('file.txt', 'r') as f:
print(f.read())
언제 쓸까요? 🤔
- 파일이 없어도 괜찮을 때
- 예외를 조용히 무시하고 싶을 때
- 옵션 파일 읽기 등
3.5 redirect_stdout: 출력 리다이렉션
1
2
3
4
5
6
7
8
9
10
11
from contextlib import redirect_stdout
import io
# 출력을 문자열로 캡처 📝
output = io.StringIO()
with redirect_stdout(output):
print("Hello")
print("World")
print(f"캡처된 출력: {output.getvalue()}")
# 캡처된 출력: Hello\nWorld\n
실무 활용! 🎯
- 테스트에서 출력 검증
- 로그 캡처
- 출력 리다이렉션
🎯 학습 목표 4: 실전 컨텍스트 매니저 예제
예제 1: 디렉토리 변경
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from contextlib import contextmanager
import os
@contextmanager
def change_directory(path):
"""일시적으로 디렉토리 변경"""
original = os.getcwd()
print(f"📁 디렉토리 변경: {original} → {path}")
try:
os.chdir(path)
yield
finally:
os.chdir(original)
print(f"📁 디렉토리 복원: {path} → {original}")
# 사용
print(f"현재 디렉토리: {os.getcwd()}")
with change_directory('/tmp'):
print(f"작업 디렉토리: {os.getcwd()}")
# 작업 수행
print(f"복원된 디렉토리: {os.getcwd()}")
실무 활용! 💼
- 임시 디렉토리에서 작업할 때
- 파일 처리 후 원래 위치로 돌아올 때
예제 2: 환경 변수 임시 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from contextlib import contextmanager
import os
@contextmanager
def temporary_env(**kwargs):
"""환경 변수를 임시로 설정 🌍"""
old_env = {}
# 환경 변수 저장 및 설정
for key, value in kwargs.items():
old_env[key] = os.environ.get(key)
os.environ[key] = value
print(f"✅ 환경 변수 설정: {key}={value}")
try:
yield
finally:
# 환경 변수 복원
for key, value in old_env.items():
if value is None:
del os.environ[key]
else:
os.environ[key] = value
print(f"🔄 환경 변수 복원: {key}")
# 사용
with temporary_env(DEBUG='true', LOG_LEVEL='INFO'):
print(f"DEBUG={os.environ.get('DEBUG')}")
print(f"LOG_LEVEL={os.environ.get('LOG_LEVEL')}")
print(f"DEBUG={os.environ.get('DEBUG')}") # None
실무 활용! 🎯
- 테스트 환경 설정
- 임시 설정 변경
- 개발/운영 환경 전환
예제 3: 성능 프로파일러
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from contextlib import contextmanager
import time
import functools
class Profiler:
"""성능 프로파일링 ⏱️"""
def __init__(self):
self.stats = {}
@contextmanager
def profile(self, name):
"""코드 블록의 실행 시간 측정"""
start = time.time()
try:
yield
finally:
elapsed = time.time() - start
if name not in self.stats:
self.stats[name] = []
self.stats[name].append(elapsed)
def report(self):
"""프로파일링 결과 출력 📊"""
print("\n=== 프로파일링 결과 ===")
for name, times in self.stats.items():
avg = sum(times) / len(times)
total = sum(times)
print(f"{name}:")
print(f" 호출 횟수: {len(times)}")
print(f" 평균 시간: {avg:.4f}초")
print(f" 총 시간: {total:.4f}초")
# 사용
profiler = Profiler()
for i in range(3):
with profiler.profile("작업 A"):
time.sleep(0.1)
with profiler.profile("작업 B"):
time.sleep(0.05)
profiler.report()
실무 활용! 💡
- 성능 병목 찾기
- 코드 최적화
- 벤치마크
예제 4: 리소스 풀 관리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from contextlib import contextmanager
from queue import Queue
class ResourcePool:
"""리소스 풀 관리 🏊"""
def __init__(self, create_resource, pool_size=3):
self.pool = Queue(maxsize=pool_size)
for _ in range(pool_size):
self.pool.put(create_resource())
@contextmanager
def acquire(self):
"""리소스 획득"""
resource = self.pool.get()
print(f"🔓 리소스 획득 (남은 개수: {self.pool.qsize()})")
try:
yield resource
finally:
self.pool.put(resource)
print(f"🔒 리소스 반환 (남은 개수: {self.pool.qsize()})")
# 사용
class Connection:
_id_counter = 0
def __init__(self):
Connection._id_counter += 1
self.id = Connection._id_counter
print(f" Connection #{self.id} 생성됨")
def query(self, sql):
return f"Connection #{self.id}: {sql}"
# 연결 풀 생성
pool = ResourcePool(Connection, pool_size=2)
# 여러 스레드에서 사용 가능
with pool.acquire() as conn:
print(conn.query("SELECT * FROM users"))
with pool.acquire() as conn:
print(conn.query("INSERT INTO users"))
실무 활용! 🎯
- 데이터베이스 연결 풀
- 스레드 풀
- 제한된 리소스 관리
💡 오늘의 핵심 요약
- 컨텍스트 매니저:
- 리소스를 안전하게 관리해요
__enter__와__exit__메서드 구현해요with문으로 사용해요
- @contextmanager:
1 2 3 4 5
@contextmanager def my_context(): # setup yield resource # cleanup
- contextlib 유틸리티:
suppress: 예외 무시해요redirect_stdout: 출력 리다이렉션해요
- 활용 사례:
- 📂 파일/DB 연결 관리
- 🔄 트랜잭션
- ⏱️ 타이머
- 🌍 임시 설정
🧪 연습 문제
문제 1: 로깅 컨텍스트 매니저
함수 호출을 로깅하는 컨텍스트 매니저를 작성하세요.
1
2
3
4
5
6
7
with log_execution("데이터 처리"):
# 작업 수행
pass
# 출력:
# [시작] 데이터 처리
# [종료] 데이터 처리 (0.1234초)
💡 힌트
단계별 힌트:
@contextmanager데코레이터를 사용해요time.time()으로 시작 시간을 기록해요try-finally로 항상 종료 시간을 출력해요- 예외 발생 시에도 로깅해야 해요
핵심 키워드: @contextmanager, time.time(), try-finally, yield
✅ 정답 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from contextlib import contextmanager
import time
@contextmanager
def log_execution(func_name):
"""함수 실행을 로깅하는 컨텍스트 매니저"""
print(f"[시작] {func_name}")
start = time.time()
try:
yield
except Exception as e:
print(f"[오류] {func_name}: {e}")
raise
finally:
elapsed = time.time() - start
print(f"[종료] {func_name} ({elapsed:.4f}초)")
# 테스트
with log_execution("데이터 처리"):
time.sleep(0.1)
result = sum(range(1000000))
어떻게 동작할까요?
yield전: 시작 시간 기록하고 시작 메시지 출력해요yield: 작업 블록 실행해요finally: 종료 시간 출력해요 (예외 발생 여부 관계없이)
문제 2: 임시 파일 컨텍스트 매니저
임시 파일을 생성하고 자동으로 삭제하는 컨텍스트 매니저를 작성하세요.
💡 힌트
단계별 힌트:
tempfile.NamedTemporaryFile을 사용해요delete=False로 설정해서 직접 삭제를 관리해요finally블록에서os.remove()로 파일을 삭제해요
핵심 키워드: tempfile, os.remove(), @contextmanager
✅ 정답 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from contextlib import contextmanager
import tempfile
import os
@contextmanager
def temporary_file(suffix='.txt'):
"""임시 파일 생성 및 자동 삭제"""
# 임시 파일 생성
temp_file = tempfile.NamedTemporaryFile(
mode='w+',
suffix=suffix,
delete=False
)
filename = temp_file.name
print(f"📝 임시 파일 생성: {filename}")
try:
yield temp_file
finally:
# 파일 닫기 및 삭제
temp_file.close()
if os.path.exists(filename):
os.remove(filename)
print(f"🗑️ 임시 파일 삭제: {filename}")
# 테스트
with temporary_file() as f:
f.write("테스트 데이터\n")
f.write("임시 파일입니다\n")
f.flush()
print(f"파일 위치: {f.name}")
실무 활용!
- 테스트에서 임시 파일 필요할 때
- 중간 결과 저장
- 임시 캐시
📝 오늘 배운 내용 정리
- with 문: 리소스를 자동으로 관리해주는 파이썬의 강력한 기능이에요
__enter__와__exit__: 컨텍스트 매니저의 핵심 메서드예요- @contextmanager: 클래스 없이 간편하게 컨텍스트 매니저를 만들어요
- 실무 활용: 파일, DB, 트랜잭션, 타이머, 환경 변수 등 다양하게 활용돼요
🔗 관련 자료
📚 이전 학습
Day 64: 이터레이터와 제너레이터 심화 ⭐⭐⭐
어제는 이터레이터 프로토콜과 itertools 모듈을 마스터했어요!
📚 다음 학습
Day 66: 함수형 프로그래밍 기초 ⭐⭐⭐
내일은 map, filter, reduce 등 함수형 프로그래밍의 기초를 배워요!
“리소스 관리도 자동화하면 편해져요!” 🚀
Day 65/100 Phase 7: 고급 파이썬 개념 #100DaysOfPython
