logger.info()를 한 번 호출했는데 같은 로그가 두 줄 나온다면, 먼저 같은 logger에 handler를 여러 번 추가했는지, 다음으로 부모 logger에도 출력 handler가 있는지를 확인하세요. propagate=False는 부모로의 전달을 막지만, 같은 logger에 붙은 handler 중복까지 없애 주지는 않습니다.
아래 실습은 Python 3.12.14의 새 프로세스에서 확인했습니다. 노트북·웹 프레임워크는 기존 설정이 있을 수 있으므로, 예제의 초기화 코드를 운영 애플리케이션에 그대로 복사하기보다 현재 구성을 먼저 살펴보는 것이 좋습니다.

1. handler와 전파 경로 확인하기
import logging
logger = logging.getLogger("demo.worker")
root = logging.getLogger()
print(logger.name)
print(logger.handlers)
print(logger.propagate)
print(root.handlers)
logger.handlers는 해당 logger에 직접 붙은 handler 목록입니다. demo.worker의 부모 demo에도 handler가 있을 수 있으므로, 루트만 보지 말고 이름의 계층을 따라 확인하세요.
current = logger
while current is not None:
print(current.name, len(current.handlers), current.propagate)
if not current.propagate:
break
current = current.parent
hasHandlers()는 부모의 handler까지 확인합니다. 따라서 이 값이 True라는 사실만으로 해당 logger의 직접 handler가 존재한다고 판단하면 안 됩니다.
2. 자식과 루트에 각각 handler가 있으면 두 번 출력된다
원인을 보기 위해 자식 logger와 루트 logger에 각각 출력 handler를 붙여 봅니다. StringIO로 로그를 모아 출력 순서를 확인합니다. 다음 블록은 다른 로깅 설정이 없는 새 Python 프로세스에서 실행하세요.
from io import StringIO
import logging
stream = StringIO()
root = logging.getLogger()
root.setLevel(logging.INFO)
root_handler = logging.StreamHandler(stream)
root_handler.setFormatter(
logging.Formatter("root: %(message)s")
)
root.addHandler(root_handler)
logger = logging.getLogger("demo.worker")
logger.setLevel(logging.INFO)
logger.propagate = True
child_handler = logging.StreamHandler(stream)
child_handler.setFormatter(
logging.Formatter("child: %(message)s")
)
logger.addHandler(child_handler)
logger.info("hello")
print(stream.getvalue(), end="")
child: hello
root: hello
자식의 handler가 먼저 출력한 후 로그가 부모 방향으로 전달되어 루트의 handler도 출력합니다. 전달된 레코드는 상위 logger 자체의 level과 filter를 다시 거치는 방식이 아니며, 상위 handler의 level과 filter가 적용됩니다. 중복을 없애겠다고 루트 logger의 level만 올리는 접근은 예상과 다를 수 있습니다.
3. 출력 담당 위치에 맞춰 해결하기
애플리케이션이 루트에서 출력을 관리한다면 모듈 logger에 추가한 handler를 제거하고 전파를 유지하는 구성이 자연스럽습니다. 위 실습에서는 다음처럼 바꾸면 됩니다.
logger.removeHandler(child_handler)
child_handler.close()
stream.seek(0)
stream.truncate(0)
logger.info("hello")
print(stream.getvalue(), end="")
root: hello

반대로 특정 logger가 독립적으로 출력을 관리하도록 설계했다면 그 logger의 handler를 유지하고 logger.propagate = False로 전파를 끊을 수 있습니다. 다만 이 경우 상위에서 관리하던 파일 출력이나 수집 경로도 함께 받지 못하므로, 어디서 출력할지 먼저 결정해야 합니다.
4. 초기화 함수가 반복 실행되며 handler가 늘어나는 경우
노트북 셀을 다시 실행하거나 설정 함수를 여러 번 호출하면 매번 새 handler 객체가 추가될 수 있습니다. 같은 logger에 두 handler가 붙어 있다면 전파를 꺼도 두 번 출력됩니다.
import logging
logger = logging.getLogger("standalone.demo")
logger.setLevel(logging.INFO)
logger.propagate = False
def configure_console():
if not logger.handlers:
handler = logging.StreamHandler()
handler.setFormatter(
logging.Formatter("%(levelname)s: %(message)s")
)
logger.addHandler(handler)
configure_console()
configure_console()
logger.info("once")
INFO: once
이 예제는 해당 logger를 직접 소유하고, console handler 하나만 필요하다는 전제입니다. 파일과 console을 동시에 쓰거나 다른 코드가 설정하는 logger에서는 단순히 handler가 있는지만 검사하면 필요한 출력을 빠뜨릴 수 있습니다. 공유 환경에서 동시 초기화가 가능하면 시작 시 한 곳에서 구성하도록 설계하는 편이 좋습니다.
5. basicConfig와 프레임워크 환경의 주의점
logging.basicConfig()는 보통 루트에 handler가 이미 있으면 설정을 새로 적용하지 않습니다. force=True는 기존 루트 handler를 제거하고 닫은 뒤 다시 구성하므로, 프레임워크나 실행 환경이 관리하는 handler를 바꿀 수 있습니다. 중복 로그의 원인을 확인하기 전에 사용하는 만능 해결책으로 삼지 마세요.
모듈에서는 logging.getLogger(__name__)으로 logger를 얻고, 출력 설정은 애플리케이션 시작 지점에서 한 번 관리하는 방식이 유지보수에 유리합니다. 동일한 이름으로 getLogger()를 여러 번 호출해도 같은 프로세스에서는 같은 logger를 돌려주므로, 호출할 때마다 새 handler를 붙일 필요가 없습니다.
handler 경로가 한 개인데도 동일 메시지가 반복된다면 재시도 코드·여러 프로세스·수집기의 중복 입력도 살펴보세요. 메시지 본문이 같다는 사실만으로 원인을 logging 전파로 확정할 수는 없습니다.
핵심 정리: 직접 handler의 개수와 부모로의 전파는 별개의 문제입니다. handler 목록 → 부모 경로 → 설정 함수의 반복 실행 → 실제 이벤트 중복 순서로 확인하면 불필요한 설정 변경을 줄일 수 있습니다.
'개발 > 트러블슈팅' 카테고리의 다른 글
| Python bisect로 중복값 개수 찾기: 왼쪽·오른쪽 경계와 범위 검색 (0) | 2026.10.06 |
|---|---|
| Python heapq 동점에서 TypeError가 날 때: 고유 순번으로 해결하기 (0) | 2026.10.05 |
| Bash 파이프라인에서 오류를 놓칠 때: pipefail과 PIPESTATUS 확인 순서 (0) | 2026.09.28 |
| GitHub Actions matrix 한 작업 실패 뒤 나머지도 취소될 때 (1) | 2026.09.18 |
| Git 줄바꿈 경고가 반복될 때 확인할 CRLF와 LF 설정 (0) | 2026.09.15 |
댓글