기본 콘텐츠로 건너뛰기

추천 가젯

배민·쿠팡이츠 과징금 6319억 원 잠정, 주문할 때 달라지는 건?

공정거래위원회가 11월 9일 배달의민족과 쿠팡이츠 운영사에 시정명령을 내리고 잠정 과징금 총 6319억 원을 부과하기로 한 결정을 공개했습니다. 주문하는 사람에게 직접 닿는 쟁점은 같은 음식점의 메뉴 가격과 최소주문금액을 앱마다 다르게 정할 수 있는지, 화면에 뜨는 예상 시간이 배달 방식별로 어떻게 계산됐는지입니다. 2024년 3월 쿠팡이츠 무료배달 도입 당시 자료. 쿠팡 뉴스룸 제공 공정위가 문제 삼은 메뉴 가격과 최소주문금액 공정위는 10월 29일 배민 운영사 우아한형제들에 3311억 원, 쿠팡에 3008억 원의 잠정 과징금을 부과하기로 결정했습니다. 공정위에 따르면 두 업체는 음식점에 메뉴 가격이나 최소주문금액을 경쟁 앱보다 불리하지 않게 맞추라고 요구했습니다. 예를 들어 한 음식점이 수수료가 더 낮은 앱에서 메뉴를 더 싸게 팔려 해도, 다른 앱이 같은 가격을 요구하면 앱별 가격 경쟁이 어려워집니다. 공정위는 이런 요구가 배달앱 간 경쟁을 약화시켰다고 판단했습니다. 배민의 배달 예상 시간과 쿠팡이츠의 수수료 배민의 가게배달은 음식점이 배달을 맡고, 배민의 자체배달은 플랫폼이 배달까지 맡습니다. 공정위는 배민이 두 방식의 예상 시간을 서로 다른 기준으로 계산하면서 그 차이를 충분히 알리지 않았다고 봤습니다. 또 가게배달의 화면 노출 축소 등을 문제 삼아 가게배달 중개수수료율을 다시 정하도록 했습니다. 쿠팡이츠에는 음식점이 부담한 할인액까지 포함해 중개·결제 수수료를 계산하는 약관 조항을 고치도록 했습니다. 무료배달 혜택과 메뉴 가격은 언제 달라질까 이번 발표에서 메뉴 가격 인하나 무료배달 혜택 변경 날짜는 확인되지 않았습니다. 쿠팡 와우 멤버십과 쿠팡이츠 서비스 결합에 관한 별도 쟁점은 공정위가 다시 심사하기로 했습니다. 보도에 따르면 우아한형제들은 위법성이 없다는 설명이 받아들여지지 않은 데 아쉬움을 표했고, 쿠팡은 와우매장 정책의 취지와 과징금 산정 방식에 대한 입장을 밝혔습니다. 두 회사 모두 의결서를 검토하겠다고 했습니다. 주문할 때는...

파이썬 logging 모듈 완전 정복 📜 — 콘솔·파일·회전 로그까지

파이썬 logging 모듈 완전 정복 📜 — 콘솔·파일·회전 로그까지 logging 아이콘

파이썬 logging 모듈 완전 정복 📜

운영 서비스에서는 print보다 logging이 정답입니다. 레벨 제어, 포맷팅, 파일 저장, 로테이팅(회전), 비동기 처리까지 지원해 개발·테스트·프로덕션 어디서든 같은 인터페이스로 로그를 다룰 수 있습니다.

1) 로그 레벨과 가장 쉬운 사용법

import logging

logging.basicConfig(level=logging.INFO)
logging.debug("디버그")           # 표시 안 됨(레벨 INFO 이상만)
logging.info("정보 로그입니다")
logging.warning("경고!")
logging.error("에러 발생")
logging.critical("치명적 오류")

2) 포맷과 기본 설정

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)

logger = logging.getLogger("myapp")
logger.info("포맷이 적용된 로그")

3) 파일 로그 & 로테이팅

3-1. 파일로 기록

import logging

file_handler = logging.FileHandler("app.log", encoding="utf-8")
file_handler.setFormatter(logging.Formatter(
    "%(asctime)s %(levelname)s [%(name)s] %(message)s"))

logger = logging.getLogger("myapp")
logger.setLevel(logging.INFO)
logger.addHandler(file_handler)

logger.info("파일에 적재됩니다")

3-2. 크기 기준 회전: RotatingFileHandler

import logging
from logging.handlers import RotatingFileHandler

handler = RotatingFileHandler(
    "app_size.log",
    maxBytes=5 * 1024 * 1024,  # 5MB
    backupCount=5              # 최대 백업 파일 수
)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))

logger = logging.getLogger("rotate.size")
logger.setLevel(logging.INFO)
logger.addHandler(handler)

logger.info("크기 기준 회전 설정 완료")

3-3. 시간 기준 회전: TimedRotatingFileHandler

import logging
from logging.handlers import TimedRotatingFileHandler

handler = TimedRotatingFileHandler(
    "app_time.log",
    when="midnight",  # 'S','M','H','D','W0'~'W6','midnight'
    interval=1,
    backupCount=7,
    encoding="utf-8"
)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))

logger = logging.getLogger("rotate.time")
logger.setLevel(logging.INFO)
logger.addHandler(handler)

logger.info("시간 기준 회전 설정 완료")

4) 프로젝트급 설정: dictConfig

import logging
import logging.config

LOGGING = {
  "version": 1,
  "disable_existing_loggers": False,
  "formatters": {
    "std": {
      "format": "%(asctime)s %(levelname)s [%(name)s] %(message)s"
    }
  },
  "handlers": {
    "console": {
      "class": "logging.StreamHandler",
      "level": "INFO",
      "formatter": "std"
    },
    "file": {
      "class": "logging.handlers.TimedRotatingFileHandler",
      "level": "INFO",
      "formatter": "std",
      "filename": "service.log",
      "when": "midnight",
      "backupCount": 14,
      "encoding": "utf-8"
    }
  },
  "loggers": {
    "": {               # root
      "level": "INFO",
      "handlers": ["console", "file"]
    },
    "myapp.db": {       # 모듈별 오버라이드
      "level": "WARNING",
      "propagate": True
    }
  }
}

logging.config.dictConfig(LOGGING)

logger = logging.getLogger("myapp")
logger.info("dictConfig로 중앙 관리!")

5) JSON 로깅 (ELK/CloudWatch 대비)

import json, logging, time

class JSONFormatter(logging.Formatter):
    def format(self, record):
        payload = {
            "ts": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(record.created)),
            "level": record.levelname,
            "logger": record.name,
            "msg": record.getMessage()
        }
        if record.exc_info:
            payload["exc_info"] = self.formatException(record.exc_info)
        return json.dumps(payload, ensure_ascii=False)

logger = logging.getLogger("json")
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)

logger.info({"event": "user_login", "user_id": 123})

6) 고부하 대비: QueueHandler로 비동기

import logging, queue, threading
from logging.handlers import QueueHandler, QueueListener

log_q = queue.Queue(maxsize=10000)
queue_handler = QueueHandler(log_q)

file_handler = logging.FileHandler("async.log", encoding="utf-8")
listener = QueueListener(log_q, file_handler)
listener.start()

logger = logging.getLogger("async")
logger.setLevel(logging.INFO)
logger.addHandler(queue_handler)

logger.info("메인 스레드를 막지 않고 기록합니다")

7) 베스트 프랙티스 & 주의사항

  • print 금지: 운영·스케줄러·서버 작업은 logging만 사용.
  • 레벨 규칙: 개발(디버그↑), 운영(정보↑), 타사 라이브러리 노이즈는 모듈별 레벨로 제어.
  • 로그 로테이션 필수: 크기 또는 시간 기준 중 하나는 반드시 설정.
  • PII/비밀키 마스킹: 포맷터 또는 필터로 민감정보 제거.
  • 멀티프로세스: QueueHandler + 중앙 리스너 프로세스로 수집 권장.
  • 컨테이너: 파일 대신 stdout 로깅 + 런타임 수집(예: Docker, k8s)이 관리에 유리.

요약

  • logging은 운영 필수 기능(레벨·포맷·핸들러·회전)을 제공
  • dictConfig로 프로젝트 전역 설정을 깔끔하게 관리
  • 대용량/분산 환경은 JSON 로깅과 QueueHandler로 확장

자주 묻는 질문(FAQ)

Q1. 모듈마다 로그 레벨을 다르게 하고 싶어요.
A. logging.getLogger("패키지.모듈")별로 레벨을 설정하거나 dictConfig.loggers에서 개별 지정하세요.

Q2. 포매터에서 요청 ID 같은 필드를 넣으려면?
A. LoggerAdapter 또는 Filter로 extra 컨텍스트를 주입해 포맷 문자열에서 %(request_id)s처럼 사용하세요.

댓글

가장 많이 본 글

Icons by Flaticon