포스트

[Python 100일 챌린지] Day 47 - 로깅 시스템

[Python 100일 챌린지] Day 47 - 로깅 시스템

logging.info("철수님 로그인")app.log에 시간까지 자동 기록! 😊

print()는 화면에만 보이지만, logging은 파일로 영구 저장! “2025-04-16 10:30:15 [ERROR] 결제 실패” 처럼 시간, 레벨, 메시지 모두 기록합니다!

(40-50분 완독 ⭐⭐⭐)

🎯 오늘의 학습 목표

📚 사전 지식


🎯 학습 목표 1: 로깅의 개념과 필요성 이해하기

1.1 로깅이란?

로깅(Logging): 프로그램 실행 중 발생하는 이벤트를 기록하는 것

1
2
3
# print() vs logging
print("사용자 로그인")        # 화면에만 출력, 프로그램 종료 시 사라짐
logging.info("사용자 로그인")  # 파일에 영구 저장 + 시간/레벨 자동 기록

1.2 print() vs logging

구분 print() logging
저장 화면에만 출력 파일로 영구 저장 가능
시간 기록 수동으로 추가 자동 기록
레벨 구분 없음 DEBUG, INFO, WARNING, ERROR, CRITICAL
운영 환경 사용 어려움 필수
필터링 불가능 레벨별 필터링 가능

1.3 로깅이 필요한 상황

1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ print로는 부족한 상황
def process_order(order_id):
    print(f"주문 처리 시작: {order_id}")  # 서버 재시작하면 사라짐!
    # ...
    print("주문 처리 완료")

# ✅ logging 사용
import logging

def process_order(order_id):
    logging.info(f"주문 처리 시작: {order_id}")  # 파일에 영구 저장!
    # ...
    logging.info("주문 처리 완료")

로깅이 필수인 경우:

  • 서버 애플리케이션 (24시간 실행)
  • 오류 추적이 필요한 경우
  • 사용자 행동 분석
  • 보안 감사 로그

🎯 학습 목표 2: logging 모듈 기초 사용법 배우기

2.1 기본 로깅

1
2
3
4
5
6
7
8
9
10
11
import logging

# 기본 설정 - INFO 레벨 이상만 출력
logging.basicConfig(level=logging.INFO)

# 로그 출력
logging.debug("디버그 메시지")      # 출력 안됨 (INFO보다 낮음)
logging.info("정보 메시지")         # INFO:root:정보 메시지
logging.warning("경고 메시지")      # WARNING:root:경고 메시지
logging.error("오류 메시지")        # ERROR:root:오류 메시지
logging.critical("치명적 오류")     # CRITICAL:root:치명적 오류

💡 level=logging.INFO로 설정하면 INFO 이상(INFO, WARNING, ERROR, CRITICAL)만 출력됩니다. DEBUG는 출력되지 않습니다.

2.2 로그 레벨

레벨 숫자 용도
DEBUG 10 상세한 디버그 정보 (개발 중에만 사용)
INFO 20 일반 정보 (프로그램 시작/종료, 주요 이벤트)
WARNING 30 경고 (문제가 될 수 있는 상황, 프로그램은 계속 실행)
ERROR 40 오류 (기능이 실패했지만 프로그램은 계속 실행)
CRITICAL 50 치명적 오류 (프로그램 중단이 필요한 심각한 오류)
1
2
3
4
5
6
# 레벨별 사용 예시
logging.debug("사용자 입력값: {'name': 'Alice', 'age': 25}")  # 개발 디버깅용
logging.info("서버가 포트 8080에서 시작되었습니다")            # 일반 정보
logging.warning("디스크 용량이 10% 미만입니다")               # 경고
logging.error("데이터베이스 연결 실패")                       # 오류
logging.critical("시스템 메모리 부족으로 종료합니다")          # 치명적

🎯 학습 목표 3: 로그 레벨과 포맷 설정하기

3.1 포맷 지정

1
2
3
4
5
6
7
8
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

logging.info("애플리케이션 시작")
# 2025-04-16 10:00:00 - root - INFO - 애플리케이션 시작

3.2 주요 포맷 속성

1
2
3
4
5
6
7
# %(asctime)s      - 시간
# %(name)s         - 로거 이름
# %(levelname)s    - 로그 레벨
# %(message)s      - 메시지
# %(filename)s     - 파일명
# %(lineno)d       - 줄 번호
# %(funcName)s     - 함수명

🎯 학습 목표 4: 파일 로거와 핸들러 활용하기

💡 핸들러(Handler)란? 로그를 “어디에” 출력할지 결정하는 객체입니다.

  • FileHandler: 파일에 저장
  • StreamHandler: 화면(콘솔)에 출력
  • RotatingFileHandler: 파일에 저장하되, 크기가 커지면 자동으로 새 파일 생성

4.1 파일에 저장

1
2
3
4
5
6
7
8
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    filename='app.log',
    filemode='a'  # 'w'는 덮어쓰기, 'a'는 추가
)

logging.info("파일에 기록됨")

4.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
32
import logging

# 로거 생성
# __name__은 현재 모듈 이름입니다 (예: 'main', 'myapp.utils')
# 이렇게 하면 어느 모듈에서 로그가 발생했는지 알 수 있습니다
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

# 포맷터 - 로그 출력 형식을 지정합니다
formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# 출력 예: 2025-04-16 10:30:15 - __main__ - INFO - 서버 시작

# 파일 핸들러 - INFO 이상만 파일에 저장
file_handler = logging.FileHandler('app.log')
file_handler.setLevel(logging.INFO)  # INFO, WARNING, ERROR, CRITICAL만 파일에
file_handler.setFormatter(formatter)

# 콘솔 핸들러 - DEBUG 이상 모두 화면에 출력
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)  # 모든 레벨 화면에 출력
console_handler.setFormatter(formatter)

# 로거에 핸들러 추가
logger.addHandler(file_handler)
logger.addHandler(console_handler)

# 사용
logger.debug("디버그 (콘솔만)")       # 화면에만 출력 (파일 X)
logger.info("정보 (파일+콘솔)")       # 화면 + 파일 둘 다
logger.error("오류 (파일+콘솔)")      # 화면 + 파일 둘 다

💡 핸들러마다 레벨을 다르게 설정할 수 있습니다. 개발 중에는 콘솔에서 DEBUG까지 보고, 파일에는 INFO 이상만 저장하는 것이 일반적입니다.

4.3 로그 로테이션 (크기 기반)

💡 로그 로테이션이란? 로그 파일이 계속 커지면 디스크가 가득 찹니다. 로테이션은 파일 크기가 일정 이상이 되면 자동으로 새 파일을 만들고, 오래된 파일은 삭제합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from logging.handlers import RotatingFileHandler

logger = logging.getLogger(__name__)

# 10MB마다 새 파일, 최대 5개 백업
# app.log → app.log.1 → app.log.2 → ... → app.log.5 (가장 오래된 건 삭제)
handler = RotatingFileHandler(
    'app.log',
    maxBytes=10*1024*1024,  # 10MB
    backupCount=5           # 최대 5개 파일 유지
)

formatter = logging.Formatter('%(asctime)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

4.4 로그 로테이션 (시간 기반)

1
2
3
4
5
6
7
8
9
from logging.handlers import TimedRotatingFileHandler

# 매일 자정에 새 파일
handler = TimedRotatingFileHandler(
    'app.log',
    when='midnight',
    interval=1,
    backupCount=30  # 30일 보관
)

4.5 실전 로깅 설정 함수

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
def setup_logging(log_file='app.log', level=logging.INFO):
    """로깅 설정"""
    logger = logging.getLogger()
    logger.setLevel(level)

    # 포맷
    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )

    # 파일 핸들러 (로테이션)
    file_handler = RotatingFileHandler(
        log_file,
        maxBytes=10*1024*1024,
        backupCount=5
    )
    file_handler.setFormatter(formatter)
    logger.addHandler(file_handler)

    # 콘솔 핸들러
    console_handler = logging.StreamHandler()
    console_handler.setFormatter(formatter)
    logger.addHandler(console_handler)

# 사용
setup_logging('myapp.log', level=logging.DEBUG)

4.6 모듈별 로거

1
2
3
4
5
6
7
8
9
10
11
12
13
# module1.py
import logging
logger = logging.getLogger(__name__)

def function1():
    logger.info("module1.function1 실행")

# module2.py
import logging
logger = logging.getLogger(__name__)

def function2():
    logger.info("module2.function2 실행")

4.7 예외 발생 시 로깅

💡 예외 로깅이 중요한 이유: 프로그램이 오류로 멈추면 “왜 멈췄는지” 알아야 합니다. logging.exception()은 오류 메시지와 함께 전체 에러 추적 정보(traceback)를 자동으로 기록합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import logging

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def divide(a, b):
    try:
        result = a / b
        logging.info(f"{a} / {b} = {result}")
        return result
    except ZeroDivisionError:
        # exception()은 ERROR 레벨 + traceback 자동 포함
        logging.exception("0으로 나누기 오류 발생!")
        return None

# 사용
divide(10, 2)   # 정상
divide(10, 0)   # 오류 발생

출력 결과:

1
2
3
4
5
6
2025-04-16 10:00:00 - INFO - 10 / 2 = 5.0
2025-04-16 10:00:01 - ERROR - 0으로 나누기 오류 발생!
Traceback (most recent call last):
  File "example.py", line 9, in divide
    result = a / b
ZeroDivisionError: division by zero

💡 logging.exception() vs logging.error()

  • logging.error("메시지"): 오류 메시지만 기록
  • logging.exception("메시지"): 오류 메시지 + traceback 전체 기록

예외 처리 블록(except) 안에서는 exception()을 사용하세요!

4.8 JSON 형식 로깅

💡 JSON 로그가 필요한 이유: 일반 텍스트 로그는 사람이 읽기 좋지만, 로그 분석 도구(Elasticsearch, Splunk 등)로 자동 분석하려면 JSON 형식이 편리합니다.

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
import logging
import json
from datetime import datetime

class JSONFormatter(logging.Formatter):
    """로그를 JSON 형식으로 변환하는 포맷터"""

    def format(self, record):
        log_data = {
            'timestamp': datetime.now().isoformat(),
            'level': record.levelname,
            'logger': record.name,
            'message': record.getMessage(),
            'file': record.filename,
            'line': record.lineno
        }

        # 예외 정보가 있으면 추가
        if record.exc_info:
            log_data['exception'] = self.formatException(record.exc_info)

        return json.dumps(log_data, ensure_ascii=False)

# 사용
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

# JSON 포맷터 적용
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)

logger.info("서버 시작")
logger.warning("메모리 사용량 높음")

출력 결과:

1
2
{"timestamp": "2025-04-16T10:00:00", "level": "INFO", "logger": "__main__", "message": "서버 시작", "file": "example.py", "line": 25}
{"timestamp": "2025-04-16T10:00:01", "level": "WARNING", "logger": "__main__", "message": "메모리 사용량 높음", "file": "example.py", "line": 26}

🎯 학습 목표 5: 실전 로깅 패턴 익히기

5.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
27
28
29
30
import logging
from datetime import datetime

# 로거 설정
logger = logging.getLogger('user_activity')
logger.setLevel(logging.INFO)

handler = logging.FileHandler('user_activity.log')
handler.setFormatter(logging.Formatter(
    '%(asctime)s - %(levelname)s - %(message)s'
))
logger.addHandler(handler)

def log_user_action(user_id, action, details=None):
    """사용자 활동 기록"""
    message = f"[User:{user_id}] {action}"
    if details:
        message += f" | {details}"
    logger.info(message)

def log_user_error(user_id, action, error):
    """사용자 관련 오류 기록"""
    logger.error(f"[User:{user_id}] {action} 실패 | Error: {error}")

# 사용 예시
log_user_action("user123", "로그인", "IP: 192.168.1.100")
log_user_action("user123", "상품 조회", "상품ID: PROD001")
log_user_action("user123", "장바구니 추가", "상품ID: PROD001, 수량: 2")
log_user_error("user123", "결제", "카드 인증 실패")
log_user_action("user123", "로그아웃")

user_activity.log 파일 내용:

1
2
3
4
5
2025-04-16 10:00:00 - INFO - [User:user123] 로그인 | IP: 192.168.1.100
2025-04-16 10:00:05 - INFO - [User:user123] 상품 조회 | 상품ID: PROD001
2025-04-16 10:00:10 - INFO - [User:user123] 장바구니 추가 | 상품ID: PROD001, 수량: 2
2025-04-16 10:00:15 - ERROR - [User:user123] 결제 실패 | Error: 카드 인증 실패
2025-04-16 10:00:20 - INFO - [User:user123] 로그아웃

5.2 API 요청/응답 로깅

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
import logging
import time

logger = logging.getLogger('api')
logger.setLevel(logging.DEBUG)

handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(
    '%(asctime)s - %(levelname)s - %(message)s'
))
logger.addHandler(handler)

def api_request(endpoint, method="GET", data=None):
    """API 요청 처리 (시뮬레이션)"""
    request_id = f"REQ-{int(time.time())}"

    # 요청 로깅
    logger.info(f"[{request_id}] {method} {endpoint} 요청 시작")
    if data:
        logger.debug(f"[{request_id}] 요청 데이터: {data}")

    start_time = time.time()

    try:
        # 실제로는 여기서 API 호출
        time.sleep(0.1)  # 처리 시간 시뮬레이션

        # 응답 성공
        elapsed = round((time.time() - start_time) * 1000, 2)
        logger.info(f"[{request_id}] {method} {endpoint} 완료 ({elapsed}ms)")
        return {"status": "success"}

    except Exception as e:
        logger.exception(f"[{request_id}] {method} {endpoint} 실패")
        raise

# 사용
api_request("/api/users", "GET")
api_request("/api/orders", "POST", {"product": "A001", "qty": 2})

출력 결과:

1
2
3
4
5
2025-04-16 10:00:00 - INFO - [REQ-1713225600] GET /api/users 요청 시작
2025-04-16 10:00:00 - INFO - [REQ-1713225600] GET /api/users 완료 (102.5ms)
2025-04-16 10:00:00 - INFO - [REQ-1713225601] POST /api/orders 요청 시작
2025-04-16 10:00:00 - DEBUG - [REQ-1713225601] 요청 데이터: {'product': 'A001', 'qty': 2}
2025-04-16 10:00:00 - INFO - [REQ-1713225601] POST /api/orders 완료 (101.3ms)

5.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
import logging
import atexit

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    filename='app.log'
)

def startup():
    """애플리케이션 시작"""
    logging.info("=" * 50)
    logging.info("애플리케이션 시작")
    logging.info(f"Python 버전: 3.x")
    logging.info(f"설정 파일: config.yml")
    logging.info("=" * 50)

def shutdown():
    """애플리케이션 종료"""
    logging.info("=" * 50)
    logging.info("애플리케이션 정상 종료")
    logging.info("=" * 50)

# 종료 시 자동 호출
atexit.register(shutdown)

# 프로그램 시작
startup()
logging.info("메인 로직 실행 중...")
# ... 프로그램 로직 ...

app.log 파일 내용:

1
2
3
4
5
6
7
8
9
2025-04-16 10:00:00 - INFO - ==================================================
2025-04-16 10:00:00 - INFO - 애플리케이션 시작
2025-04-16 10:00:00 - INFO - Python 버전: 3.x
2025-04-16 10:00:00 - INFO - 설정 파일: config.yml
2025-04-16 10:00:00 - INFO - ==================================================
2025-04-16 10:00:01 - INFO - 메인 로직 실행 중...
2025-04-16 10:00:05 - INFO - ==================================================
2025-04-16 10:00:05 - INFO - 애플리케이션 정상 종료
2025-04-16 10:00:05 - INFO - ==================================================

💡 실전 팁 & 주의사항

✅ 로깅 베스트 프랙티스

  1. 모듈별 로거 사용
    1
    2
    3
    
    # 각 모듈마다
    import logging
    logger = logging.getLogger(__name__)
    
  2. 적절한 로그 레벨 선택
    • DEBUG: 개발 중 상세 정보
    • INFO: 일반 정보 (시작/종료)
    • WARNING: 경고 (프로그램은 계속 실행)
    • ERROR: 오류 (기능 실패)
    • CRITICAL: 치명적 오류 (프로그램 중단)
  3. 민감 정보 주의
    1
    2
    3
    4
    5
    
    # ❌ 나쁜 예
    logger.info(f"Password: {password}")
    
    # ✅ 좋은 예
    logger.info("User login attempt")
    

⚠️ 주의사항

  • 로그 파일 크기 관리 (로테이션 필수)
  • 성능에 영향을 줄 수 있으니 과도한 로깅 지양
  • 운영 환경에서는 INFO 이상 레벨만 기록

🧪 연습 문제

문제 1: 모듈별 로거 설정

목표: 여러 모듈에서 사용할 수 있는 로깅 시스템을 구성하세요.

요구사항:

  • 파일과 콘솔에 동시 출력
  • 파일은 INFO 이상, 콘솔은 WARNING 이상
  • 로그 로테이션 적용 (10MB, 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
import logging
from logging.handlers import RotatingFileHandler

def setup_logger(name, log_file='app.log'):
    """로거 설정"""
    logger = logging.getLogger(name)
    logger.setLevel(logging.DEBUG)

    # 포맷
    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )

    # 파일 핸들러 (INFO 이상, 로테이션)
    file_handler = RotatingFileHandler(
        log_file,
        maxBytes=10*1024*1024,  # 10MB
        backupCount=3
    )
    file_handler.setLevel(logging.INFO)
    file_handler.setFormatter(formatter)

    # 콘솔 핸들러 (WARNING 이상)
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.WARNING)
    console_handler.setFormatter(formatter)

    # 핸들러 추가
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)

    return logger

# 사용
logger = setup_logger('my_module')
logger.debug("디버그 (파일X, 콘솔X)")
logger.info("정보 (파일O, 콘솔X)")
logger.warning("경고 (파일O, 콘솔O)")

문제 2: 간단한 로깅 시스템

목표: 아래 요구사항에 맞는 로깅을 설정하세요.

요구사항:

  • my_app.log 파일에 로그 저장
  • 포맷: 시간 - 레벨 - 메시지
  • INFO 레벨 이상만 기록
해답 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import logging

# 기본 설정으로 간단하게!
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    filename='my_app.log',
    filemode='a'  # 추가 모드 (기존 내용 유지)
)

# 사용
logging.debug("이건 기록 안됨")  # INFO보다 낮음
logging.info("프로그램 시작")     # 기록됨
logging.warning("주의!")         # 기록됨
logging.error("오류 발생!")      # 기록됨

# my_app.log 파일 내용:
# 2025-04-16 10:00:00,123 - INFO - 프로그램 시작
# 2025-04-16 10:00:00,124 - WARNING - 주의!
# 2025-04-16 10:00:00,125 - ERROR - 오류 발생!

📝 오늘 배운 내용 정리

개념 설명 예시
로그 레벨 로그의 중요도 DEBUG < INFO < WARNING < ERROR < CRITICAL
핸들러 로그 출력 대상 FileHandler, StreamHandler, RotatingFileHandler
포맷터 로그 출력 형식 '%(asctime)s - %(levelname)s - %(message)s'
로거 로그 기록 객체 logger = logging.getLogger(__name__)
로그 로테이션 자동 파일 관리 RotatingFileHandler, TimedRotatingFileHandler
예외 로깅 오류 + traceback 기록 logging.exception("오류 메시지")
JSON 로깅 구조화된 로그 커스텀 JSONFormatter 클래스

핵심 코드 패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import logging
from logging.handlers import RotatingFileHandler

# 로거 생성
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

# 포맷터
formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

# 파일 핸들러 (로테이션)
file_handler = RotatingFileHandler(
    'app.log', maxBytes=10*1024*1024, backupCount=5
)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

# 로그 기록
logger.info("애플리케이션 시작")
logger.error("오류 발생")

🔗 관련 자료


📚 이전 학습

Day 46: 예외 처리 고급 ⭐⭐⭐

어제는 커스텀 예외, 예외 체이닝, 컨텍스트 매니저 등 고급 예외 처리를 배웠습니다!

📚 다음 학습

Day 48: 디버깅 기법 ⭐⭐⭐

내일은 print 디버깅부터 pdb 디버거까지, 효율적으로 버그를 찾고 수정하는 방법을 배웁니다!


“늦었다고 생각할 때가 가장 빠른 시기입니다!” 🚀

Day 47/100 Phase 5: 파일 처리와 예외 처리 #100DaysOfPython

이제와서 시작하는 Python 마스터하기 - Day 47 완료! 🎉

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.