Python Logging

print vs logger: 효과적인 디버깅과 로그 관리

Python에서 디버깅과 로그 관리를 위해 print 함수와 logging 모듈을 사용할 수 있다. print는 간단한 디버깅에 적합하지만, logging은 로그 레벨 제어, 파일 저장, 타임스탬프 자동 기록 등 프로덕션 환경에 필수적인 기능을 제공한다. Streamlit과 같은 웹 애플리케이션 환경에서도 logging을 활용하면 체계적인 로그 관리와 문제 추적이 가능하다.

Engineering
Python
Logging
Debugging
저자

Kwangmin Kim

공개

2026년 02월 14일

3 logging 모듈

Python 표준 라이브러리의 logging 모듈은 체계적인 로그 관리 시스템을 제공한다.

import logging

logger = logging.getLogger(__name__)
logger.info("프로그램 시작")
logger.warning("메모리 부족")
logger.error("파일 없음")

3.1 로그 레벨

로그의 중요도를 5단계로 구분한다.

레벨 용도 예시
DEBUG 10 상세한 디버깅 정보 변수 값, 함수 호출 순서
INFO 20 일반 정보 작업 시작/종료, 정상 동작
WARNING 30 경고 메시지 비권장 사용, 리소스 부족
ERROR 40 에러 발생 예외 처리, 작업 실패
CRITICAL 50 치명적 오류 시스템 중단, 데이터 손실
import logging

logger = logging.getLogger(__name__)

logger.debug("변수 x = 10")  # 개발 시에만
logger.info("사용자 로그인 성공")  # 정상 동작
logger.warning("API 호출 지연")  # 주의 필요
logger.error("DB 연결 실패")  # 오류 발생
logger.critical("디스크 공간 부족")  # 긴급 상황

3.2 기본 설정

3.2.1 간단한 설정

import logging

# 기본 설정
logging.basicConfig(
    level=logging.INFO,  # INFO 이상만 출력
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger(__name__)
logger.info("애플리케이션 시작")

출력 예시:

2026-02-14 10:30:45,123 - __main__ - INFO - 애플리케이션 시작

3.2.2 파일 저장

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('app.log'),  # 파일에 저장
        logging.StreamHandler()  # 콘솔에도 출력
    ]
)

logger = logging.getLogger(__name__)
logger.info("로그가 파일과 콘솔 모두에 기록됨")

3.3 실용 예제

3.3.1 함수 실행 로깅

import logging

logger = logging.getLogger(__name__)

def process_data(data):
    logger.info(f"데이터 처리 시작: {len(data)}건")
    
    try:
        result = []
        for item in data:
            if item is None:
                logger.warning(f"None 값 발견, 건너뜀")
                continue
            
            processed = transform(item)
            result.append(processed)
        
        logger.info(f"처리 완료: {len(result)}건 성공")
        return result
    
    except Exception as e:
        logger.error(f"처리 실패: {e}", exc_info=True)
        raise

3.3.2 파일 작업 로깅

import logging
from pathlib import Path

logger = logging.getLogger(__name__)

def load_json_safe(file_path: Path):
    """JSON 파일을 안전하게 로드"""
    if not file_path.exists():
        logger.warning(f"파일 없음: {file_path}")
        return []
    
    try:
        with open(file_path, encoding='utf-8') as f:
            data = json.load(f)
        logger.info(f"파일 로드 성공: {file_path.name}")
        return data
    
    except json.JSONDecodeError as e:
        logger.error(f"JSON 파싱 실패: {file_path.name} - {e}")
        return []
    
    except IOError as e:
        logger.error(f"파일 읽기 실패: {file_path.name} - {e}")
        return []

3.3.3 API 호출 로깅

import logging
import requests

logger = logging.getLogger(__name__)

def fetch_data(url):
    logger.info(f"API 호출: {url}")
    
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        
        logger.info(f"API 성공: {response.status_code}")
        return response.json()
    
    except requests.Timeout:
        logger.error("API 타임아웃")
        return None
    
    except requests.HTTPError as e:
        logger.error(f"HTTP 오류: {e.response.status_code}")
        return None
    
    except Exception as e:
        logger.critical(f"예상치 못한 오류: {e}", exc_info=True)
        return None

3.4 고급 설정

3.4.1 로거 계층 구조

import logging

# 모듈별 로거 생성
main_logger = logging.getLogger('myapp')
db_logger = logging.getLogger('myapp.database')
api_logger = logging.getLogger('myapp.api')

# 부모 로거 설정이 자식에게 상속됨
main_logger.setLevel(logging.INFO)
db_logger.setLevel(logging.DEBUG)  # DB는 더 상세하게

3.4.2 포맷 커스터마이징

import logging

# 상세한 포맷
formatter = logging.Formatter(
    '%(asctime)s | %(name)s | %(levelname)-8s | %(filename)s:%(lineno)d | %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

handler = logging.StreamHandler()
handler.setFormatter(formatter)

logger = logging.getLogger(__name__)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)

출력 예시:

2026-02-14 10:30:45 | myapp.main | INFO     | main.py:42 | 애플리케이션 시작

3.4.3 로그 로테이션

파일 크기나 시간 기준으로 로그 파일을 자동 분할한다.

import logging
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler

# 크기 기준 로테이션 (10MB마다 새 파일)
size_handler = RotatingFileHandler(
    'app.log',
    maxBytes=10*1024*1024,  # 10MB
    backupCount=5  # 최대 5개 파일 유지
)

# 시간 기준 로테이션 (매일 자정)
time_handler = TimedRotatingFileHandler(
    'app.log',
    when='midnight',
    interval=1,
    backupCount=30  # 30일치 보관
)

logger = logging.getLogger(__name__)
logger.addHandler(size_handler)
logger.setLevel(logging.INFO)

4 Streamlit 환경에서의 로깅

4.1 Streamlit의 특성

Streamlit은 웹 애플리케이션이지만 터미널에서 실행되므로, 콘솔 출력을 볼 수 있다.

import streamlit as st

# print는 터미널에 출력됨 (브라우저 아님)
print("터미널에만 표시")

# st.write는 웹 페이지에 출력됨
st.write("브라우저에 표시")

4.3 logger 권장 이유

4.3.1 파일 기반 로그 관리

import logging
import streamlit as st
from pathlib import Path

# 로그 디렉토리 설정
LOG_DIR = Path(__file__).parent / 'logs'
LOG_DIR.mkdir(exist_ok=True)

# 로거 설정
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler(LOG_DIR / 'app.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

def load_data():
    logger.info("데이터 로딩 시작")
    try:
        data = fetch_data()
        logger.info(f"로드 완료: {len(data)}건")
        return data
    except Exception as e:
        logger.error(f"로드 실패: {e}", exc_info=True)
        st.error("데이터를 불러올 수 없습니다")
        return None

st.title("데이터 대시보드")
data = load_data()

4.3.2 사용자 피드백과 로그 분리

import logging
import streamlit as st

logger = logging.getLogger(__name__)

def process_file(uploaded_file):
    # 사용자에게 진행 상황 표시
    with st.spinner("파일 처리 중..."):
        # 내부적으로 상세 로그 기록
        logger.info(f"파일 처리 시작: {uploaded_file.name}")
        
        try:
            data = parse_file(uploaded_file)
            logger.debug(f"파싱 결과: {len(data)}행")
            
            result = transform_data(data)
            logger.info("데이터 변환 완료")
            
            # 사용자에게는 간단한 메시지
            st.success("파일 처리 완료!")
            return result
        
        except Exception as e:
            # 상세한 에러는 로그에
            logger.error(f"처리 실패: {e}", exc_info=True)
            # 사용자에게는 친절한 메시지
            st.error("파일을 처리할 수 없습니다. 형식을 확인해주세요.")
            return None

4.3.3 디버깅 모드 구현

import logging
import streamlit as st

# 사이드바에 디버그 모드 토글
debug_mode = st.sidebar.checkbox("디버그 모드", value=False)

# 디버그 모드에 따라 로그 레벨 조정
if debug_mode:
    logging.getLogger().setLevel(logging.DEBUG)
    st.sidebar.info("디버그 로그 활성화")
else:
    logging.getLogger().setLevel(logging.INFO)

logger = logging.getLogger(__name__)

@st.cache_data
def expensive_computation(param):
    logger.debug(f"계산 시작: param={param}")
    result = complex_calc(param)
    logger.debug(f"계산 완료: result={result}")
    return result

4.3.4 실시간 로그 뷰어

import logging
import streamlit as st
from pathlib import Path

logger = logging.getLogger(__name__)

# 로그 파일 경로
LOG_FILE = Path("logs/app.log")

# 사이드바에 로그 뷰어 추가
with st.sidebar:
    st.subheader("최근 로그")
    if st.button("로그 새로고침"):
        if LOG_FILE.exists():
            with open(LOG_FILE, 'r') as f:
                logs = f.readlines()
                # 최근 20줄만 표시
                st.code('\n'.join(logs[-20:]), language='log')
        else:
            st.info("로그 파일이 없습니다")

# 메인 애플리케이션
st.title("애플리케이션")
if st.button("작업 실행"):
    logger.info("사용자가 작업 실행 버튼 클릭")
    result = perform_task()
    st.success("완료!")

4.4 Streamlit 환경 권장 패턴

import logging
import streamlit as st
from pathlib import Path
from datetime import datetime

# ===== 로거 설정 (파일 상단) =====
LOG_DIR = Path(__file__).parent / 'logs'
LOG_DIR.mkdir(exist_ok=True)

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s | %(name)s | %(levelname)s | %(message)s',
    handlers=[
        logging.FileHandler(LOG_DIR / f'app_{datetime.now():%Y%m%d}.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# ===== 애플리케이션 시작 =====
logger.info("Streamlit 애플리케이션 시작")

st.title("데이터 분석 도구")

# ===== 데이터 로드 =====
@st.cache_data
def load_data():
    logger.info("데이터 로드 시작")
    try:
        data = pd.read_csv('data.csv')
        logger.info(f"데이터 로드 성공: {len(data)}행")
        return data
    except Exception as e:
        logger.error(f"데이터 로드 실패: {e}", exc_info=True)
        st.error("데이터를 불러올 수 없습니다")
        return None

data = load_data()

# ===== 사용자 상호작용 =====
if st.button("분석 실행"):
    logger.info("사용자가 분석 실행 요청")
    
    with st.spinner("분석 중..."):
        try:
            result = analyze(data)
            logger.info("분석 완료")
            st.success("분석이 완료되었습니다")
            st.write(result)
        except Exception as e:
            logger.error(f"분석 실패: {e}", exc_info=True)
            st.error("분석 중 오류가 발생했습니다")

5 비교 요약

항목 print logging
설정 불필요 필요 (한 번만)
사용 난이도 쉬움 중간
로그 레벨 없음 5단계 (DEBUG~CRITICAL)
파일 저장 불가 (리다이렉션 필요) 기본 지원
타임스탬프 없음 자동 기록
포맷 제어 수동 자동
프로덕션 관리 어려움 쉬움 (레벨로 제어)
성능 빠름 약간 느림 (무시 가능)
적합한 환경 학습, 간단한 스크립트 프로덕션, 라이브러리

6 사용 가이드라인

6.2 logger 사용

  • 프로덕션 애플리케이션
  • 라이브러리 및 패키지
  • 웹 애플리케이션 (Django, Flask, Streamlit)
  • 장기 실행 프로세스
  • 팀 프로젝트
import logging

logger = logging.getLogger(__name__)

def main():
    logger.info("애플리케이션 시작")
    try:
        result = process()
        logger.info("처리 완료")
    except Exception as e:
        logger.error("오류 발생", exc_info=True)

6.3 혼용 가능

개발 초기에는 print를 쓰다가, 프로덕션 준비 시 logger로 전환할 수 있다.

# 개발 단계
def debug_function():
    print("디버그 정보")
    result = process()
    print(f"결과: {result}")
    return result

# 프로덕션 단계
import logging
logger = logging.getLogger(__name__)

def debug_function():
    logger.debug("디버그 정보")
    result = process()
    logger.debug(f"결과: {result}")
    return result

7 마이그레이션 전략

7.1 단계별 전환

7.1.1 1단계: 기본 설정 추가

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

7.1.2 2단계: 주요 print를 logger로 변경

# Before
print("에러:", e)

# After
logger.error(f"에러: {e}")

7.1.3 3단계: 파일 저장 추가

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('app.log'),
        logging.StreamHandler()
    ]
)

7.1.4 4단계: 레벨 세분화

# 상황에 맞게 레벨 조정
logger.debug("상세 정보")  # 개발 시
logger.info("일반 정보")   # 운영 시
logger.warning("주의")     # 문제 가능성
logger.error("오류")       # 오류 발생

8 결론

  • 학습 및 간단한 스크립트: print 사용
  • 프로덕션 애플리케이션: logging 필수
  • Streamlit 등 웹 앱: logging으로 체계적 관리
  • 팀 프로젝트: logging으로 일관성 유지

로깅은 초기 설정 비용이 있지만, 장기적으로 디버깅 시간을 크게 단축하고 시스템 안정성을 향상시킨다.

Subscribe

Enjoy this blog? Get notified of new posts by email: