시니어 파이썬 개발자가 되기위한 7가지 개념! 🐍
- -
파이썬 스크립트 작성, 프로젝트 빌드, 애플리케이션 배포 경험이 있으신가요? 좋습니다! 하지만 파이썬을 *정말로* 깊이 이해한다는 것은 언어의 핵심 메커니즘과 고급 기능을 파악하는 데 있습니다. 여기, 평범한 파이썬 코더와 숙련된 파이썬 전문가를 구분 짓는 7가지 중요한 개념을 소개합니다. 만약 이 개념들이 익숙하고 직관적으로 느껴진다면, 축하드립니다! 당신은 이미 파이썬을 "꽤 잘 다루는" 수준을 넘어섰을 가능성이 높습니다. 🎉
1. 메타클래스 (Metaclasses): 클래스를 동적으로 만들기 ✨
# 모든 메서드가 설명을 위한 docstring을 갖도록 강제하는 메타클래스
class DocstringRequiredMeta(type):
def __new__(cls, name, bases, dct):
print(f"--- 메타클래스 실행: '{name}' 클래스 생성 중 ---")
# 클래스의 속성(메서드 등)을 순회
for key, value in dct.items():
# 함수/메서드인데 docstring이 없는 경우
if callable(value) and not getattr(value, '__doc__'):
raise TypeError(f"메서드 '{key}'에는 설명(docstring)이 반드시 필요합니다.")
print("--- 모든 메서드 검증 완료 ---")
# 검증 후, 부모 메타클래스(type)를 통해 클래스 생성
return super().__new__(cls, name, bases, dct)
# 메타클래스를 사용하여 API 핸들러 클래스 정의
class APIHandler(metaclass=DocstringRequiredMeta):
def get_user(self, user_id):
"""지정된 ID의 사용자 정보를 조회합니다."""
print(f"사용자 {user_id} 정보 조회...")
# ... 실제 로직 ...
return {"id": user_id, "name": "Dummy User"}
def process_data(self, data):
# 아래 메서드는 docstring이 없어서 TypeError 발생!
# """데이터를 처리합니다.""" # 이 줄 주석 해제하면 정상 동작
print("데이터 처리 중...")
# ... 실제 로직 ...
return {"status": "processed"}
# 사용 예시 (클래스 정의 시점에서 에러 발생)
try:
# APIHandler 클래스 정의 시 process_data 메서드 때문에 에러 발생
handler = APIHandler()
user = handler.get_user(123)
print(user)
except TypeError as e:
print(f"\n클래스 정의 중 오류 발생: {e}")
# process_data에 docstring을 추가하면 정상적으로 클래스가 생성됩니다.
print("\n(만약 process_data에 docstring이 있었다면) 클래스 인스턴스 생성 및 메서드 호출 가능")
🚀 왜 중요할까요?
메타클래스는 Django ORM이나 SQLAlchemy 같은 프레임워크에서 모델 정의 시 규칙을 강제하거나, 클래스 생성 시점에 코드를 자동으로 추가하는 등 고급 패턴 구현에 핵심적인 역할을 합니다. 코드의 일관성을 유지하고 반복 작업을 줄이는 데 매우 유용합니다.
2. 컨텍스트 매니저 (Context Managers): with 문의 진정한 힘 📁➡️🔧
with open(...) 구문은 파일 처리 시 매우 유용하지만, 컨텍스트 매니저의 활용 범위는 훨씬 넓습니다. 데이터베이스 연결, 네트워크 소켓, 락(Lock) 등 명시적인 설정(setup)과 정리(teardown)가 필요한 모든 종류의 리소스를 안전하고 깔끔하게 관리할 수 있습니다.
예시: 임시 API 세션을 관리하는 컨텍스트 매니저
import time
import random
# 가상의 외부 API 클라이언트 라이브러리라고 가정
class ExternalServiceClient:
def __init__(self, api_key):
self.api_key = api_key
self._session_active = False
print(f"클라이언트 초기화 (API 키: ...{api_key[-4:]})")
def connect(self):
print("API 서버에 연결 시도 중...")
time.sleep(0.5) # 네트워크 지연 시뮬레이션
self._session_active = True
print("✅ 연결 성공! 세션 활성화됨.")
return self # 연결된 클라이언트 객체 반환
def disconnect(self):
print("API 서버 연결 종료 중...")
time.sleep(0.2)
self._session_active = False
print("🔌 연결 종료됨.")
def call_api(self, endpoint):
if not self._session_active:
raise ConnectionError("API 세션이 활성화되지 않았습니다.")
print(f"📞 엔드포인트 '{endpoint}' 호출 중...")
time.sleep(0.3)
if random.random() < 0.2: # 20% 확률로 실패
raise TimeoutError("API 호출 시간 초과")
return {"data": f"Response from {endpoint}"}
# 컨텍스트 매니저 클래스
class ApiSessionManager:
def __init__(self, api_key):
self.api_key = api_key
self.client = None
def __enter__(self):
print("\n[컨텍스트 시작] __enter__ 호출됨")
self.client = ExternalServiceClient(self.api_key)
# 연결된 클라이언트 객체를 반환하여 'with' 블록 내에서 사용 가능하게 함
return self.client.connect()
def __exit__(self, exc_type, exc_val, exc_tb):
# exc_type: 예외 타입 (예외 없으면 None)
# exc_val: 예외 값 (예외 없으면 None)
# exc_tb: 트레이스백 (예외 없으면 None)
print("\n[컨텍스트 종료] __exit__ 호출됨")
if exc_type:
print(f"⚠️ 예외 발생: {exc_type.__name__} - {exc_val}")
print(" (리소스 정리만 수행)")
else:
print(" 정상적으로 블록 실행 완료.")
if self.client:
self.client.disconnect() # 리소스 정리 (연결 해제)
# True를 반환하면 'with' 블록 밖으로 예외가 전파되지 않음
# False나 None을 반환 (또는 아무것도 반환 안함)하면 예외 전파됨
# return False # 예외를 다시 발생시키려면
# 사용 예시
api_key = "secr3t_k3y_12345"
try:
print("--- API 세션 시작 ---")
# ApiSessionManager가 클라이언트 연결 및 해제를 자동으로 관리
with ApiSessionManager(api_key) as api_client:
print("\n[with 블록 내부]")
response1 = api_client.call_api("/users")
print(f" 응답 1: {response1}")
response2 = api_client.call_api("/products")
print(f" 응답 2: {response2}")
# 아래 줄 주석 해제 시 일부러 예외 발생
# response3 = api_client.call_api("/invalid_endpoint_that_fails")
print("\n--- API 세션 정상 종료 ---")
except (ConnectionError, TimeoutError) as e:
print(f"\n--- API 호출 중 문제 발생 ---")
print(f"처리된 예외: {e}")
except Exception as e:
print(f"\n--- 예상치 못한 오류 발생 ---: {e}")
🚀 왜 중요할까요?
컨텍스트 매니저를 사용하면 리소스 누수(파일 핸들, 네트워크 소켓 등을 닫지 않는 경우)를 방지하고, 예외 발생 시에도 안정적으로 리소스를 정리할 수 있습니다. 트랜잭션 관리, 임시 설정 변경 등 다양한 상황에서 코드의 안정성과 가독성을 크게 높여줍니다.
3. 파라미터가 있는 데코레이터: 맞춤형 기능 추가 🎁
데코레이터는 함수를 감싸 기능을 추가하는 강력한 도구입니다. 여기에 파라미터를 전달할 수 있게 만들면, 재사용 가능하고 설정 가능한 데코레이터를 만들 수 있습니다. 이를 위해서는 함수를 세 단계로 중첩하는 패턴이 일반적입니다.
예시: 실행 시간을 로깅하고 임계값 초과 시 경고하는 데코레이터
import time
import functools # 원본 함수의 메타데이터 유지를 위해 사용
def log_execution_time(threshold_seconds=1.0):
"""
함수 실행 시간을 측정하고, 지정된 임계값(threshold_seconds)을 초과하면
경고 로그를 출력하는 데코레이터 팩토리.
"""
# 1. 데코레이터 팩토리: 파라미터(threshold_seconds)를 받음
def decorator(func):
# 2. 실제 데코레이터: 꾸밀 함수(func)를 받음
@functools.wraps(func) # 원본 함수의 이름, docstring 등 유지
def wrapper(*args, **kwargs):
# 3. 래퍼 함수: 원본 함수의 인자를 받아 실행 전후 로직 추가
start_time = time.perf_counter() # 정밀한 시간 측정 시작
print(f"--- 함수 '{func.__name__}' 실행 시작 ---")
try:
result = func(*args, **kwargs) # 원본 함수 실행
end_time = time.perf_counter()
elapsed_time = end_time - start_time
print(f"--- 함수 '{func.__name__}' 실행 완료 (소요 시간: {elapsed_time:.4f}초) ---")
# 임계값 초과 확인
if elapsed_time > threshold_seconds:
print(f"⚠️ 경고: '{func.__name__}' 실행 시간({elapsed_time:.4f}초)이 임계값({threshold_seconds}초)을 초과했습니다!")
return result
except Exception as e:
print(f"🚨 오류: 함수 '{func.__name__}' 실행 중 예외 발생 - {e}")
raise # 원본 예외를 다시 발생시킴
return wrapper
return decorator
# 데코레이터 사용 예시
@log_execution_time(threshold_seconds=0.5) # 0.5초 초과 시 경고
def process_large_data(size_mb):
"""대용량 데이터 처리 시뮬레이션"""
print(f"{size_mb}MB 데이터 처리 중...")
# 처리 시간 시뮬레이션 (0.1초 ~ 1.1초 사이)
simulated_delay = 0.1 + random.random()
time.sleep(simulated_delay)
print("데이터 처리 완료.")
return {"status": "success", "processed_mb": size_mb}
@log_execution_time(threshold_seconds=0.2) # 0.2초 초과 시 경고
def quick_calculation(a, b):
"""빠른 계산 수행"""
print("빠른 계산 수행 중...")
time.sleep(0.05) # 짧은 지연
return a + b
# 함수 호출
print("\n=== 대용량 데이터 처리 호출 ===")
process_large_data(500)
print("\n=== 빠른 계산 호출 ===")
quick_calculation(10, 20)
print("\n=== 다시 대용량 데이터 처리 (경고 발생 가능) ===")
process_large_data(1000) # 이번엔 경고가 나올 확률 높음
🚀 왜 중요할까요?
파라미터화된 데코레이터는 로깅 레벨 설정, 재시도 횟수 지정, API 속도 제한 설정, 접근 권한 검사 등 다양한 시나리오에서 코드 중복 없이 유연하게 기능을 추가하고 재사용할 수 있게 해줍니다.
4. asyncio를 이용한 동시성 처리: I/O 기다림의 효율화 ⚡
파이썬의 asyncio는 스레드나 프로세스 없이도 동시성(Concurrency)을 구현하는 강력한 방법입니다. 특히 네트워크 요청, 파일 입출력 등 I/O 작업이 많은 애플리케이션에서 빛을 발합니다. async/await 문법과 이벤트 루프를 통해 non-blocking I/O를 효율적으로 관리합니다.
예시: 여러 비동기 작업(데이터베이스 조회, 외부 API 호출) 동시 실행
import asyncio
import time
import random
async def query_database(query):
"""데이터베이스 조회 시뮬레이션 (비동기 I/O 대기)"""
print(f"⏳ DB 조회 시작: {query}")
# 실제로는 비동기 DB 드라이버가 I/O 대기
delay = random.uniform(0.5, 1.5) # 0.5초 ~ 1.5초 대기
await asyncio.sleep(delay)
print(f"✅ DB 조회 완료: {query} (결과: ...)")
return f"Result for '{query}' after {delay:.2f}s"
async def call_external_api(service_name):
"""외부 API 호출 시뮬레이션 (비동기 네트워크 I/O 대기)"""
print(f"📡 외부 API 호출 시작: {service_name}")
# 실제로는 aiohttp 같은 라이브러리가 I/O 대기
delay = random.uniform(0.8, 2.0) # 0.8초 ~ 2.0초 대기
await asyncio.sleep(delay)
print(f"✅ 외부 API 응답 수신: {service_name}")
return f"Response from {service_name} after {delay:.2f}s"
async def main_async_task():
"""메인 비동기 실행 함수"""
start_time = time.perf_counter()
print("--- 비동기 작업 시작 ---")
# 여러 비동기 작업을 태스크로 생성 (바로 실행되지 않고 예약됨)
task_db_users = asyncio.create_task(query_database("SELECT * FROM users"))
task_api_weather = asyncio.create_task(call_external_api("Weather Service"))
task_db_products = asyncio.create_task(query_database("SELECT * FROM products"))
task_api_stock = asyncio.create_task(call_external_api("Stock Price Service"))
# 모든 태스크가 완료될 때까지 동시에 실행하고 기다림
print("--- 작업 동시 실행 대기 중... ---")
results = await asyncio.gather(
task_db_users,
task_api_weather,
task_db_products,
task_api_stock
)
end_time = time.perf_counter()
print("\n--- 모든 비동기 작업 완료 ---")
print(f"총 소요 시간: {end_time - start_time:.2f}초")
print("\n--- 결과 ---")
for i, result in enumerate(results):
print(f"결과 {i+1}: {result}")
# 비동기 이벤트 루프를 실행하여 main_async_task 코루틴 실행
if __name__ == "__main__":
# Python 3.7+ 에서는 asyncio.run() 사용 권장
asyncio.run(main_async_task())
# 만약 위 작업들을 순차적으로 실행했다면 총 3.6초(0.5+0.8+0.5+0.8) 이상 걸렸을 것!
# 동시 실행 덕분에 가장 오래 걸리는 작업 시간(약 2.0초) 정도로 단축됨.
🚀 왜 중요할까요?
asyncio는 수많은 동시 I/O 작업을 스레드를 사용하는 것보다 훨씬 적은 메모리와 CPU 오버헤드로 처리할 수 있게 해줍니다. 현대적인 웹 프레임워크(FastAPI, Sanic 등), 네트워크 서비스, 실시간 데이터 처리 시스템 구축에 필수적입니다.
5. 디스크립터 (Descriptors): 속성 접근 제어 마스터 🔍
파이썬의 @property, @classmethod, @staticmethod와 같은 내장 기능들은 사실 디스크립터 프로토콜을 기반으로 구현되어 있습니다. 디스크립터를 직접 구현하면 클래스 속성에 접근(get, set, delete)할 때 특정 로직(유효성 검사, 값 변환, 로깅 등)을 실행하도록 섬세하게 제어할 수 있습니다.
예시: 양수 값만 허용하는 속성을 위한 디스크립터
class PositiveNumber:
"""
속성에 양수(0 포함)만 할당되도록 강제하는 디스크립터.
"""
def __set_name__(self, owner_class, property_name):
# 디스크립터가 할당된 속성 이름을 저장 (예: 'width', 'height')
# 이 이름은 인스턴스의 __dict__에 값을 저장/조회할 때 사용됨
print(f"--- 디스크립터 초기화: '{owner_class.__name__}' 클래스의 '{property_name}' 속성 ---")
self.property_name = property_name
def __get__(self, instance, owner_class):
# 속성 값을 읽으려고 할 때 호출됨
# instance: 디스크립터를 포함하는 객체 (예: Rectangle 객체 r)
# owner_class: 디스크립터를 포함하는 클래스 (예: Rectangle 클래스)
if instance is None:
# 클래스 자체에서 속성에 접근할 때 (e.g., Rectangle.width)
return self
# 인스턴스의 내부 딕셔너리에서 값을 찾아 반환 (없으면 None)
value = instance.__dict__.get(self.property_name, None)
print(f" [__get__] '{self.property_name}' 값 조회: {value}")
return value
def __set__(self, instance, value):
# 속성에 값을 할당하려고 할 때 호출됨
print(f" [__set__] '{self.property_name}'에 '{value}' 할당 시도...")
# 유효성 검사: 숫자인지, 0 이상인지 확인
if not isinstance(value, (int, float)):
raise TypeError(f"'{self.property_name}' 속성에는 숫자만 할당할 수 있습니다.")
if value < 0:
raise ValueError(f"'{self.property_name}' 속성에는 0 이상의 값만 할당할 수 있습니다.")
# 유효성 검사 통과 시, 인스턴스의 __dict__에 값 저장
instance.__dict__[self.property_name] = value
print(f" [__set__] 성공: '{self.property_name}' = {value}")
# 디스크립터를 사용하는 클래스
class Rectangle:
# width와 height 속성에 PositiveNumber 디스크립터 인스턴스를 할당
width = PositiveNumber()
height = PositiveNumber()
def __init__(self, width, height):
print(f"\n--- Rectangle 인스턴스 생성 ({width}, {height}) ---")
# 생성자에서 할당 시에도 디스크립터의 __set__이 호출됨
self.width = width
self.height = height
print("--- 인스턴스 생성 완료 ---")
def area(self):
# 넓이 계산 시 디스크립터의 __get__이 호출됨
print("\n--- 넓이 계산 시작 ---")
return self.width * self.height
# 사용 예시
print("=== 유효한 값으로 객체 생성 ===")
r1 = Rectangle(10, 20)
print(f"사각형 넓이: {r1.area()}")
print("\n=== 속성 값 변경 (유효) ===")
r1.width = 15 # __set__ 호출 -> 유효성 검사 통과
print(f"변경된 너비: {r1.width}") # __get__ 호출
print("\n=== 유효하지 않은 값 할당 시도 (음수) ===")
try:
r1.height = -5 # __set__ 호출 -> ValueError 발생
except ValueError as e:
print(f"오류 발생: {e}")
print("\n=== 유효하지 않은 값 할당 시도 (문자열) ===")
try:
r1.width = "invalid" # __set__ 호출 -> TypeError 발생
except TypeError as e:
print(f"오류 발생: {e}")
🚀 왜 중요할까요?
디스크립터는 ORM(Object-Relational Mapper) 라이브러리에서 모델 필드의 데이터 유효성 검사, 타입 변환, 데이터베이스 컬럼 매핑 등을 구현하는 데 핵심적으로 사용됩니다. @property보다 더 복잡한 속성 관리 로직을 재사용 가능한 형태로 만들 수 있습니다.
6. __slots__를 이용한 메모리 최적화 💾
기본적으로 파이썬 클래스의 인스턴스는 속성을 동적으로 저장하기 위해 __dict__라는 딕셔너리를 사용합니다. 하지만 수백만 개의 작은 인스턴스를 생성해야 하는 경우, 이 __dict__가 차지하는 메모리 오버헤드가 상당할 수 있습니다. __slots__를 클래스에 정의하면, 인스턴스가 사용할 속성을 미리 고정하고 __dict__를 생성하지 않아 메모리 사용량을 크게 줄일 수 있습니다.
예시: Point 클래스의 메모리 사용량 비교
import sys
# 일반적인 Point 클래스 (각 인스턴스가 __dict__를 가짐)
class PointRegular:
def __init__(self, x, y):
self.x = x
self.y = y
self.label = "default" # 동적 속성 추가 가능
# __slots__를 사용한 Point 클래스 (__dict__ 없음)
class PointSlotted:
# 인스턴스가 가질 속성을 문자열 리스트로 명시
__slots__ = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
# __slots__에 없는 속성은 기본적으로 추가 불가
# self.label = "default" # 이 줄은 AttributeError 발생시킴
# 많은 수의 인스턴스 생성
num_instances = 100000
print(f"--- {num_instances:,}개의 인스턴스 생성 비교 ---")
# Regular Point 인스턴스 리스트 생성
regular_points = [PointRegular(i, i*2) for i in range(num_instances)]
# Slotted Point 인스턴스 리스트 생성
slotted_points = [PointSlotted(i, i*2) for i in range(num_instances)]
# 대표 인스턴스 크기 비교 (환경/버전에 따라 약간 다를 수 있음)
sample_regular = regular_points[0]
sample_slotted = slotted_points[0]
print(f"\n--- 단일 인스턴스 크기 비교 (sys.getsizeof) ---")
size_regular = sys.getsizeof(sample_regular)
# Regular 인스턴스는 __dict__도 가지고 있음
size_regular_dict = sys.getsizeof(sample_regular.__dict__)
size_slotted = sys.getsizeof(sample_slotted)
# Slotted 인스턴스는 __dict__가 없음
# sys.getsizeof(sample_slotted.__dict__) # AttributeError 발생
print(f"PointRegular 인스턴스 크기: {size_regular} 바이트 (내부 __dict__ 크기: {size_regular_dict} 바이트)")
print(f"PointSlotted 인스턴스 크기: {size_slotted} 바이트 (__dict__ 없음)")
print(f"메모리 절약 (인스턴스당): 약 {size_regular - size_slotted} 바이트")
# 전체 리스트의 대략적인 메모리 사용량 비교 (gc 모듈 등으로 더 정확히 측정 가능)
# 여기서는 단순 계산으로 추정
total_mem_regular_approx = size_regular * num_instances
total_mem_slotted_approx = size_slotted * num_instances
print(f"\n--- 전체 인스턴스 예상 메모리 (단순 계산) ---")
print(f"Regular Points 총 메모리: 약 {total_mem_regular_approx / (1024**2):.2f} MB")
print(f"Slotted Points 총 메모리: 약 {total_mem_slotted_approx / (1024**2):.2f} MB")
print(f"총 메모리 절약 추정치: 약 {(total_mem_regular_approx - total_mem_slotted_approx) / (1024**2):.2f} MB")
# 동적 속성 추가 시도
print("\n--- 동적 속성 추가 가능 여부 ---")
sample_regular.z = 100 # Regular 인스턴스는 __dict__가 있어 가능
print(f"PointRegular에 'z' 속성 추가 가능: {sample_regular.z}")
try:
sample_slotted.z = 100 # Slotted 인스턴스는 __slots__에 없으므로 불가능
except AttributeError as e:
print(f"PointSlotted에 'z' 속성 추가 시도 중 오류: {e}")
🚀 왜 중요할까요?
__slots__는 대규모 데이터 처리, 과학 계산, 게임 개발 등 수많은 객체를 메모리에 로드해야 하는 상황에서 메모리 사용량을 최적화하고 속성 접근 속도를 약간 향상시키는 데 유용합니다. 다만, 동적으로 속성을 추가할 수 없고 다중 상속 시 주의가 필요하다는 제약 사항을 이해하고 사용해야 합니다.
7. GIL (Global Interpreter Lock) 이해하기: 파이썬 동시성의 한계와 극복 🔒
GIL은 CPython(가장 널리 사용되는 파이썬 구현체)의 내부 메커니즘으로, 한 번에 단 하나의 스레드만이 파이썬 바이트코드를 실행할 수 있도록 제어하는 락(Lock)입니다. 이 때문에 순수하게 CPU 연산만 많이 하는 작업(CPU-bound task)의 경우, 여러 스레드를 사용해도 멀티코어 CPU의 이점을 완전히 활용하지 못하고 실제로는 병렬(parallel) 실행이 아닌 동시(concurrent) 실행에 그칩니다.
I/O 작업(네트워크, 디스크 읽기/쓰기 등) 중에는 스레드가 GIL을 해제하므로, I/O-bound 작업에서는 스레딩이 여전히 효과적입니다. CPU-bound 작업의 진정한 병렬 처리를 위해서는 multiprocessing 모듈(별도의 프로세스를 사용하므로 GIL 제약을 받지 않음)이나 C 확장 모듈 등을 사용해야 합니다.
예시: CPU 집약적 작업(소수 찾기)을 스레딩 vs 멀티프로세싱으로 실행 시간 비교
import time
import threading
import multiprocessing
import math
# CPU를 많이 사용하는 작업: 주어진 범위 내의 소수 개수 찾기
def count_primes_in_range(start, end):
count = 0
# print(f"Worker (PID: {multiprocessing.current_process().pid}, TID: {threading.get_ident()}) starting range {start}-{end}")
if start < 2: start = 2
for num in range(start, end + 1):
is_prime = True
# 간단한 소수 판별 로직 (최적화되지 않음)
for i in range(2, int(math.sqrt(num)) + 1):
if num % i == 0:
is_prime = False
break
if is_prime:
count += 1
# print(f"Worker (PID: {multiprocessing.current_process().pid}) finished range {start}-{end}, found {count} primes.")
return count
# 실행 설정
MAX_NUMBER = 200000 # 소수를 찾을 최대 숫자 (값을 늘리면 차이가 더 명확해짐)
NUM_WORKERS = 4 # 사용할 스레드 또는 프로세스 개수
chunk_size = MAX_NUMBER // NUM_WORKERS
# 작업을 나눌 범위 계산
ranges = []
for i in range(NUM_WORKERS):
range_start = i * chunk_size + 1
range_end = (i + 1) * chunk_size if i < NUM_WORKERS - 1 else MAX_NUMBER
ranges.append((range_start, range_end))
print(f"--- 소수 찾기 작업 (1 ~ {MAX_NUMBER:,}) ---")
print(f"사용할 워커 수: {NUM_WORKERS}")
print(f"작업 분할 범위: {ranges}")
# 1. 스레딩 사용 (GIL의 영향으로 병렬 처리 효과 미미)
print(f"\n--- {NUM_WORKERS}개 스레드로 실행 시작 ---")
start_time_thread = time.perf_counter()
threads = []
thread_results = [0] * NUM_WORKERS # 스레드 결과를 저장할 리스트 (직접 반환 어려움)
# 스레드 결과 저장을 위한 헬퍼 함수
def thread_worker(index, start, end):
thread_results[index] = count_primes_in_range(start, end)
for i in range(NUM_WORKERS):
t = threading.Thread(target=thread_worker, args=(i, ranges[i][0], ranges[i][1]))
threads.append(t)
t.start()
for t in threads:
t.join() # 모든 스레드가 끝날 때까지 대기
total_primes_thread = sum(thread_results)
end_time_thread = time.perf_counter()
print(f"스레딩 총 소요 시간: {end_time_thread - start_time_thread:.2f} 초")
print(f"찾은 소수 개수 (스레딩): {total_primes_thread:,}")
print("-" * 30)
# 2. 멀티프로세싱 사용 (GIL 우회하여 병렬 처리 가능)
print(f"\n--- {NUM_WORKERS}개 프로세스로 실행 시작 ---")
start_time_process = time.perf_counter()
# 프로세스 풀을 사용하여 작업을 분배하고 결과 수집
with multiprocessing.Pool(processes=NUM_WORKERS) as pool:
# 각 프로세스에 count_primes_in_range 함수와 인자(범위)를 전달
process_results = pool.starmap(count_primes_in_range, ranges)
total_primes_process = sum(process_results)
end_time_process = time.perf_counter()
print(f"멀티프로세싱 총 소요 시간: {end_time_process - start_time_process:.2f} 초")
print(f"찾은 소수 개수 (멀티프로세싱): {total_primes_process:,}")
# 결과 비교 (멀티코어 환경에서는 멀티프로세싱이 훨씬 빠름)
print("\n--- 실행 시간 비교 ---")
if end_time_process - start_time_process < end_time_thread - start_time_thread:
print("✅ 멀티프로세싱이 스레딩보다 더 빨랐습니다 (CPU-bound 작업에서 예상된 결과).")
else:
print("🤔 스레딩과 멀티프로세싱 속도 차이가 크지 않거나 스레딩이 더 빨랐습니다 (작업량이 적거나 시스템 환경에 따라 다를 수 있음).")
🚀 왜 중요할까요?
GIL의 존재와 그 영향을 이해하는 것은 파이썬 애플리케이션의 성능을 최적화하는 데 매우 중요합니다. 작업의 특성(CPU-bound vs I/O-bound)에 따라 스레딩, 멀티프로세싱, asyncio 중 적절한 동시성 모델을 선택하는 능력이 고성능 애플리케이션 개발의 핵심입니다.
🤔 최종 도전: 개념들을 제대로 이해하고 따라오셨나요?
이 7가지 개념이 더 이상 낯설거나 어렵게 느껴지지 않는다면, 당신은 이미 파이썬 생태계에서 고급 레벨로 나아가고 있는 것입니다. 하지만 진정한 전문성은 이 도구들을 언제, 왜 사용해야 하는지를 아는 데서 나옵니다.
- 프레임워크 수준의 추상화나 클래스 생성 로직 제어가 필요하다면 메타클래스를 고려해 보세요.
- 수많은 네트워크 요청이나 파일 처리를 효율적으로 다뤄야 한다면 asyncio가 강력한 해답이 될 수 있습니다.
- 데이터 모델의 유효성 검사나 속성 접근 시 특정 로직을 일관되게 적용하고 싶다면 디스크립터가 유용합니다.
- 메모리 사용량이 매우 중요하고 객체 구조가 고정적이라면 __slots__를 활용해 보세요.
- 성능 병목 현상을 분석할 때 GIL의 영향을 인지하고, 작업 유형에 맞는 동시성 전략(스레딩, 멀티프로세싱, asyncio)을 선택하세요.
파이썬의 장점은 그 유연성과 강력함에 있습니다. 하지만 모든 문제에 가장 복잡한 해결책이 필요한 것은 아닙니다. 이 고급 개념들을 현명하게, 적재적소에 사용하는 것이 중요합니다.
꾸준히 실험하고, 코드를 읽고, 배우면서 '파이썬을 꽤 잘하는' 수준을 넘어 진정한 '파이썬 전문가'로 성장해 나가시기를 응원합니다! 💪
함께보면 좋은글
시니어 파이썬 개발자로 나아가기위한 10가지 개념
AI 웹앱 개발자로서 Python은 이제 너무 중요한 언어인것 같습니다. 주력 언어로 Javascript와 Python은 계속 이어질것 같고 Python을 좀 더 딥하게 이해하고 숙달하기 위해 중요 개념들을 정리해봅니다
intelloper.tistory.com
시니어 개발자들은 이런거 안한대요. (개발자 물경력 방지)
시니어 개발자들은 이런거 안한대요. (개발자 물경력 방지)
주니어 개발자에서 멈춰 있을수만은 없다! 연차는 쌓이는데 자신의 실력도 쌓여야겠죠? 시니어 개발자들은 어떻게 하는지 탐구해봅시다.모든 시니어 개발자도 처음에는 기초적인 코딩 실력과
intelloper.tistory.com
[Python 팁] list는 아는데 deque 모르면 주니어.
[Python 팁] list는 아는데 deque 모르면 주니어.
우리는 매일 코드에서 리스트(list)를 사용하죠. 하지만 많은 개발자들이 아주 간단하면서도 코드를 더 빠르고 효율적으로 만들어 줄 리스트 트릭을 놓치고 있다는 사실, 알고 계셨나요? 🤔저도
intelloper.tistory.com
'Python > 정보' 카테고리의 다른 글
[Modern Python Project] 현대적 파이썬 프로젝트 관리, 개발법에 대하여 (0) | 2025.03.14 |
---|---|
[Python 3.13] AI와 ML 혁신을 위한 새로운 기능들 (0) | 2025.02.22 |
시니어 개발자들은 이런거 안한대요. (개발자 물경력 방지) (0) | 2025.02.10 |
시니어 파이썬 개발자로 나아가기위한 10가지 개념 (0) | 2025.01.15 |
Python 3.14 출시 - 꼭 알아야 할 5가지 주요 기능 (0) | 2025.01.15 |
당신이 좋아할만한 콘텐츠
소중한 공감 감사합니다
포스팅 주소를 복사했습니다
이 글이 도움이 되었다면 공감 부탁드립니다.