Python 코드 품질: 디버깅부터 테스트와 자동화까지
Python 디버깅, 로깅, pytest, Ruff, pre-commit과 GitHub Actions를 이용한 품질 관리 흐름을 정리한다.
Python 코드를 작성할 때 중요한 것은 단순히 실행되는 코드를 만드는 것이 아니다.
좋은 코드는 다음 조건을 만족해야 한다.
- 문제가 발생했을 때 원인을 빠르게 찾을 수 있어야 한다.
- 코드 변경 후 기존 기능이 깨지지 않았음을 확인할 수 있어야 한다.
- 팀원이 작성해도 일정한 코드 스타일을 유지해야 한다.
- Git에 잘못된 코드가 들어가기 전에 문제를 발견할 수 있어야 한다.
- 서버나 CI 환경에서도 자동으로 품질을 검증할 수 있어야 한다.
이를 위해 사용되는 대표적인 도구가 다음과 같다.
Debugger / logging
↓
pytest
↓
pytest-cov
↓
Ruff
↓
pre-commit
↓
GitHub Actions
결국 코드 품질 관리의 핵심은
사람이 직접 확인하던 작업을 최대한 자동화하는 것
이라고 볼 수 있다.
코드 품질과 기능을 자동으로 검증해야 한다. 아직 프로젝트를 제대로 해본적이 없어서 크게 와닿지는 않는 걸
1. 디버깅
버그란?
버그(Bug)는 프로그램이 의도한 대로 동작하지 않게 만드는 문제를 의미한다.
대표적으로 다음과 같은 문제가 있다.
- 입력 데이터 오류
- 논리 오류
- 문법 오류
- 타입 오류
- 실행 중 발생하는 예외
일반적인 디버깅 과정은 다음과 같다.
문제 재현
↓
원인 분석
↓
수정
↓
재실행
↓
정상 동작 확인
중요한 것은 단순히 에러를 없애는 것이 아니라 왜 문제가 발생했는지 확인하는 것이다.
CODEX야 도와줘
2. 자주 만나는 Python 오류
안 만나고 싶다.. 오류..
| 오류 | 의미 | 예시 |
|---|---|---|
SyntaxError | Python 문법 오류 | 괄호 또는 콜론 누락 |
NameError | 정의되지 않은 변수 사용 | print(x) |
TypeError | 서로 맞지 않는 타입 연산 | "2" + 2 |
IndexError | 리스트 범위를 벗어난 접근 | items[10] |
KeyError | 존재하지 않는 딕셔너리 키 접근 | data["name"] |
ValueError | 값의 형식이 잘못됨 | int("hello") |
AttributeError | 존재하지 않는 메서드나 속성 사용 | "hello".push() |
ZeroDivisionError | 0으로 나누기 | 10 / 0 |
에러가 발생하면 가장 먼저 Traceback의 마지막 줄을 확인하는 것이 좋다.
원래도 마지막 줄만 본다
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
마지막 줄에는 일반적으로
예외 종류 : 문제 내용
이 표시된다.
그 다음 위쪽으로 올라가면서 어느 파일의 몇 번째 줄에서 문제가 시작되었는지 추적한다.
3. print() 디버깅
개발자의 시간복잡도를 줄여준다. 알고리즘의 시간복잡도만 중요한게 아니라고
가장 간단한 디버깅 방법은 변수 값을 직접 출력하는 것이다.
def add_numbers(a, b):
print(f"DEBUG: a={a}, b={b}")
return a + b
print(add_numbers(3, 5))
출력:
DEBUG: a=3, b=5
8
왜 사용하는가?
프로그램이 실제로 어떤 값을 가지고 실행되는지 빠르게 확인할 수 있다.
특히 작은 프로그램에서는 매우 간단하고 효과적이다!
한계
프로그램이 커지면 다음과 같은 문제가 생긴다.
다 읽어낼 자신 있으면 써보던가 ㅋ
print(data)
print("여기 실행됨")
print(result)
print(user)
print(response)
어떤 출력이 실제 결과이고 어떤 출력이 디버깅용인지 구분하기 어려워진다.
따라서 간단한 확인은 print()로 할 수 있지만 규모가 커지면 Debugger와 logging을 사용하는 것이 좋다.
4. logging
실무 프로그램에서는 print() 대신 logging을 많이 사용한다.
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug("디버깅 메시지")
logging.info("프로그램 실행")
logging.warning("주의가 필요한 상태")
logging.error("오류 발생")
logging.critical("심각한 오류 발생")
로그 레벨은 다음과 같이 구분된다.
DEBUG
↓
INFO
↓
WARNING
↓
ERROR
↓
CRITICAL
왜 사용하는가?
환경에 따라 필요한 로그만 출력할 수 있기 때문이다.
개발 환경에서는
logging.DEBUG
까지 확인하고,
운영 환경에서는
logging.INFO
또는
logging.WARNING
이상만 기록하는 방식으로 사용할 수 있다.
파일에 저장하는 것도 가능하다.
logging.basicConfig(
filename="app.log",
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
)
logging.info("프로그램 시작")
결과:
2026-08-07 09:00:01 - INFO - 프로그램 시작
효과
logging을 사용하면
- 오류 발생 시간
- 프로그램 실행 흐름
- API 요청 결과
- 처리한 데이터 수
- 비정상 상황
등을 기록할 수 있다.
운영 중 문제가 발생했을 때 원인을 추적하기 훨씬 쉬워진다.
5. traceback
예외가 발생한 위치와 호출 흐름을 자세히 확인하고 싶다면 traceback을 사용할 수 있다.
import traceback
try:
result = 10 / 0
except Exception as error:
print("에러 발생:", error)
traceback.print_exc()
단순히
division by zero
만 보는 것이 아니라,
어떤 함수
→ 어떤 파일
→ 몇 번째 줄
→ 어떤 예외
에서 문제가 발생했는지 확인할 수 있다.
다만 대부분의 경우 처리되지 않은 예외는 Python이 기본적으로 traceback을 출력하기 때문에, traceback 모듈은 예외를 잡은 뒤에도 전체 호출 정보를 기록해야 할 때 특히 유용하다.
6. assert
assert는 개발 중 “이 조건은 반드시 참이어야 한다”라는 가정을 검사할 때 사용한다.
def divide(a, b):
assert b != 0, "b는 0이 될 수 없습니다."
return a / b
divide(10, 2)
는 정상 실행된다.
하지만
divide(10, 0)
을 실행하면
AssertionError: b는 0이 될 수 없습니다.
가 발생한다.
주의
assert를 사용자 입력 검증이나 중요한 비즈니스 로직에 사용하는 것은 적절하지 않다.
Python은 최적화 옵션으로 실행할 경우 assert가 제거될 수 있기 때문이다.
따라서 실제 입력 검증은 다음과 같이 작성하는 것이 좋다.
def divide(a, b):
if b == 0:
raise ValueError("b는 0이 될 수 없습니다.")
return a / b
정리하면
assert
→ 개발자가 가정한 내부 상태 검사
if + raise
→ 실제 프로그램 입력과 예외 검증
으로 생각하면 된다.
7. VS Code Debugger
프로그램이 커지면 print()를 계속 추가하는 것보다 Debugger를 사용하는 것이 효율적이다.
VS Code에서는 코드 왼쪽에 Breakpoint를 설정할 수 있다.
def calculate_total(prices):
total = sum(prices)
return total
prices = [1000, 2000, 3000]
result = calculate_total(prices)
total = sum(prices) 부분에 Breakpoint를 걸면 실행이 해당 지점에서 멈춘다.
이때 다음 값을 직접 확인할 수 있다.
prices
total
result
뿐만 아니라
- 변수 타입
- 객체 내부 값
- 리스트 내용
- DataFrame
- 함수 호출 흐름
등도 확인할 수 있다.
데이터 분석에서 특히 유용한 이유
예를 들어 Pandas DataFrame을 다룰 때마다
print(df)
print(df.columns)
print(df.dtypes)
print(df.shape)
를 작성하는 것보다 Breakpoint에서 객체를 직접 확인하는 것이 훨씬 편하다.
즉,
복잡한 프로그램일수록
print()보다 Debugger가 효율적이다.
8. pytest
코드를 수정할 때 가장 무서운 문제는
새로운 기능을 고쳤는데 기존 기능이 망가지는 것
이다.
이를 방지하기 위해 자동화 테스트를 작성한다.
Python에서 가장 많이 사용하는 테스트 도구 중 하나가 pytest이다.
pip install pytest
예를 들어 다음 함수가 있다고 하자.
# src/calculator.py
def add(a: int, b: int) -> int:
return a + b
테스트 코드는 다음과 같이 작성한다.
# tests/test_calculator.py
from src.calculator import add
def test_add():
assert add(1, 2) == 3
실행:
pytest
pytest가 테스트 파일을 자동으로 찾아 실행한다.
9. pytest 테스트 탐색 규칙
pytest는 기본적으로 다음 이름을 테스트로 인식한다.
파일
test_*.py
*_test.py
예:
test_user.py
user_test.py
함수
def test_login():
...
즉 함수 이름이 test_로 시작해야 한다.
특정 테스트만 실행할 수도 있다.
pytest tests/
특정 파일:
pytest tests/test_calculator.py
특정 함수:
pytest tests/test_calculator.py::test_add
상세 결과:
pytest -v
첫 번째 실패에서 중단:
pytest -x
10. pytest를 사용하는 효과
테스트를 작성해두면 코드 수정 후
pytest
한 번으로 기존 기능이 정상인지 확인할 수 있다.
예를 들어 다음 기능이 있다고 하자.
데이터 로딩
데이터 검증
데이터 변환
CSV 저장
API 호출
각 기능에 테스트를 만들어 두면 프로그램을 수정할 때마다 전체 기능을 사람이 직접 실행해볼 필요가 줄어든다.
즉,
코드 수정
↓
pytest 실행
↓
기존 기능 정상 여부 확인
이 가능해진다.
이것이 자동화 테스트의 가장 큰 장점이다.
11. pytest-cov
테스트가 존재한다고 해서 모든 코드가 테스트되고 있다는 의미는 아니다.
어떤 코드가 실제 테스트를 거쳤는지 확인하기 위해 Coverage를 사용한다.
pip install pytest-cov
실행:
pytest tests/ --cov=src --cov-report=term-missing
예:
Name Stmts Miss Cover
-----------------------------------------
src/clean.py 25 3 88%
src/features.py 38 0 100%
-----------------------------------------
TOTAL 63 3 95%
Miss는 테스트되지 않은 코드 수를 의미한다.
따라서 Coverage는 단순히 숫자를 높이는 도구라기보다
어떤 코드 경로에 테스트가 부족한지 찾는 도구
로 사용하는 것이 좋다.
12. Ruff (이걸를 잘 기억하자!)
Python 프로젝트에서는 코드 스타일과 잠재적인 오류를 자동으로 검사하는 Linter를 사용한다.
최근 Python 프로젝트에서는 Ruff가 많이 사용된다.
pip install ruff
검사:
ruff check .
자동 수정:
ruff check . --fix
포매팅:
ruff format .
Ruff는 다음과 같은 작업을 하나의 도구에서 처리할 수 있다.
Lint
Import 정리
일부 오류 자동 수정
Python 코드 Formatting
예를 들어 다음 코드가 있다고 하자.
import os
import sys
x=10
os, sys를 사용하지 않는다면 Ruff가 불필요한 import를 탐지할 수 있다.
Formatting 역시
def add(a,b):
return a+b
같은 코드를
def add(a, b):
return a + b
처럼 일관된 형태로 정리할 수 있다.
효과
사람이 직접
공백이 이상하다
import 순서가 이상하다
사용하지 않는 변수가 있다
스타일이 다르다
같은 문제를 찾을 필요가 줄어든다.
13. Black은 언제 사용하는가?
Black은 대표적인 Python 코드 Formatter다.
pip install black
실행:
black .
검사만 할 경우:
black --check .
Black의 가장 큰 특징은 설정 자유도를 줄이는 대신 누가 실행해도 거의 동일한 형태의 코드가 나온다는 것이다.
다만 Ruff 자체에도 Formatter가 있기 때문에 새로운 프로젝트라면
Ruff lint
+
Ruff format
구성만 사용하는 것도 가능하다.
기존 프로젝트에서 이미 Black을 사용하고 있다면 굳이 변경할 필요는 없다.
14. pre-commit
문제가 있는 코드가 Git에 커밋되기 전에 자동으로 검사하고 싶다면 pre-commit을 사용할 수 있다.
pip install pre-commit
프로젝트 루트에
.pre-commit-config.yaml
파일을 작성한다.
예:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.0
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-yaml
- id: check-merge-conflict
- id: check-added-large-files
- id: debug-statements
설치:
pre-commit install
전체 파일 검사:
pre-commit run --all-files
이후
git commit
을 실행하면 커밋 전에 자동 검사가 실행된다.
검사를 통과하지 못하면 커밋이 중단된다.
참고로 debug-statements는 breakpoint(), pdb 같은 디버거 코드가 남아 있는지 검사하는 Hook이며 일반적인 모든 print()를 차단하는 기능은 아니다.
15. GitHub Actions CI
pre-commit은 개발자의 컴퓨터에서 실행된다.
하지만 개발자가 검사를 실행하지 않거나 설정이 다른 경우도 있다.
따라서 GitHub에서도 다시 테스트를 실행하는 것이 좋다.
이것이 **CI(Continuous Integration)**다.
Developer
↓
git push
↓
GitHub
↓
Ruff
↓
pytest
↓
통과 / 실패
간단한 GitHub Actions 예시는 다음과 같다.
name: Python CI
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Ruff
run: |
ruff check .
ruff format --check .
- name: Pytest
run: pytest
GitHub에 코드를 Push하거나 Pull Request를 만들 때마다 자동으로
Lint
Formatting
Test
를 검사할 수 있다.
16. 추천 프로젝트 구조
작은 프로젝트라도 코드를 다음처럼 분리해두면 관리하기 편하다.
my_project/
│
├── src/
│ └── mymodule/
│ ├── __init__.py
│ └── core.py
│
├── tests/
│ └── test_core.py
│
├── .github/
│ └── workflows/
│ └── ci.yml
│
├── .pre-commit-config.yaml
├── pyproject.toml
├── requirements.txt
└── README.md
역할을 나누면 다음과 같다.
| 경로 | 역할 |
|---|---|
src/ | 실제 프로그램 코드 |
tests/ | 테스트 코드 |
.github/workflows/ | GitHub Actions |
pyproject.toml | Python 도구 설정 |
.pre-commit-config.yaml | 커밋 전 자동 검사 |
requirements.txt | 의존성 관리 |
17. 실제 개발 흐름
코드 품질 도구를 각각 따로 외우기보다는 하나의 개발 과정으로 이해하는 것이 좋다.
1. 코드 작성
↓
2. VS Code Debugger로 문제 확인
↓
3. pytest로 기능 검증
↓
4. Ruff로 코드 검사 및 Formatting
↓
5. pre-commit 자동 검사
↓
6. git commit
↓
7. git push
↓
8. GitHub Actions에서 다시 검증
각 도구의 역할도 명확하다.
| 도구 | 역할 |
|---|---|
| Debugger | 실행 중 문제 원인 분석 |
| logging | 프로그램 실행 기록 |
| pytest | 기능이 정상인지 검증 |
| pytest-cov | 테스트하지 않은 코드 확인 |
| Ruff | 코드 스타일 및 오류 검사 |
| pre-commit | 잘못된 코드의 커밋 방지 |
| GitHub Actions | 서버에서 최종 자동 검증 |
정리
처음 Python을 배울 때는
print()
만으로도 대부분의 문제를 해결할 수 있다.
하지만 프로젝트 규모가 커지고 다른 사람과 코드를 공유하기 시작하면 코드 품질을 사람이 직접 관리하기 어렵다.
따라서 점차 다음 구조로 발전시키는 것이 좋다.
print
↓
Debugger / logging
↓
pytest
↓
Ruff
↓
pre-commit
↓
CI
특히 데이터 분석이나 머신러닝 코드도 예외가 아니다.
좋은 코드의 기준은 단순히
지금 실행되는가?
에서 끝나는 것이 아니라,
나중에 수정해도 정상적으로 동작하는지 검증할 수 있는가?
까지 포함한다.
결국 테스트와 코드 품질 자동화의 목적은 하나
코드를 변경해도 신뢰할 수 있는 개발 환경을 만드는 것!.
Devops, MLops애서 더 소개해야지
COMMENTS
GitHub 계정으로 로그인하여 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions에 공개 저장되며, 작성 내용과 GitHub 프로필 정보가 다른 방문자에게 보일 수 있습니다.