포스트

[Python 100일 챌린지] Day 65 - 컨텍스트 매니저

[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__ 호출됨

이렇게 동작해요! 🎬

  1. with 문 시작 → __enter__() 호출해요
  2. as 변수에 반환값을 저장해요
  3. 블록 실행해요
  4. 블록 종료 → __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"))

실무 활용! 🎯

  • 데이터베이스 연결 풀
  • 스레드 풀
  • 제한된 리소스 관리

💡 오늘의 핵심 요약

  1. 컨텍스트 매니저:
    • 리소스를 안전하게 관리해요
    • __enter____exit__ 메서드 구현해요
    • with 문으로 사용해요
  2. @contextmanager:
    1
    2
    3
    4
    5
    
    @contextmanager
    def my_context():
        # setup
        yield resource
        # cleanup
    
  3. contextlib 유틸리티:
    • suppress: 예외 무시해요
    • redirect_stdout: 출력 리다이렉션해요
  4. 활용 사례:
    • 📂 파일/DB 연결 관리
    • 🔄 트랜잭션
    • ⏱️ 타이머
    • 🌍 임시 설정

🧪 연습 문제

문제 1: 로깅 컨텍스트 매니저

함수 호출을 로깅하는 컨텍스트 매니저를 작성하세요.

1
2
3
4
5
6
7
with log_execution("데이터 처리"):
    # 작업 수행
    pass

# 출력:
# [시작] 데이터 처리
# [종료] 데이터 처리 (0.1234초)
💡 힌트

단계별 힌트:

  1. @contextmanager 데코레이터를 사용해요
  2. time.time()으로 시작 시간을 기록해요
  3. try-finally로 항상 종료 시간을 출력해요
  4. 예외 발생 시에도 로깅해야 해요

핵심 키워드: @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))

어떻게 동작할까요?

  1. yield 전: 시작 시간 기록하고 시작 메시지 출력해요
  2. yield: 작업 블록 실행해요
  3. finally: 종료 시간 출력해요 (예외 발생 여부 관계없이)

문제 2: 임시 파일 컨텍스트 매니저

임시 파일을 생성하고 자동으로 삭제하는 컨텍스트 매니저를 작성하세요.

💡 힌트

단계별 힌트:

  1. tempfile.NamedTemporaryFile을 사용해요
  2. delete=False로 설정해서 직접 삭제를 관리해요
  3. 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}")

실무 활용!

  • 테스트에서 임시 파일 필요할 때
  • 중간 결과 저장
  • 임시 캐시

📝 오늘 배운 내용 정리

  1. with 문: 리소스를 자동으로 관리해주는 파이썬의 강력한 기능이에요
  2. __enter____exit__: 컨텍스트 매니저의 핵심 메서드예요
  3. @contextmanager: 클래스 없이 간편하게 컨텍스트 매니저를 만들어요
  4. 실무 활용: 파일, DB, 트랜잭션, 타이머, 환경 변수 등 다양하게 활용돼요

🔗 관련 자료


📚 이전 학습

Day 64: 이터레이터와 제너레이터 심화 ⭐⭐⭐

어제는 이터레이터 프로토콜과 itertools 모듈을 마스터했어요!

📚 다음 학습

Day 66: 함수형 프로그래밍 기초 ⭐⭐⭐

내일은 map, filter, reduce 등 함수형 프로그래밍의 기초를 배워요!


“리소스 관리도 자동화하면 편해져요!” 🚀

Day 65/100 Phase 7: 고급 파이썬 개념 #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.