Python for AI, Embedded/Python: Core & Automation

파이썬(Python) 테스트부터 배포까지 완벽 가이드: unittest, pytest 활용법과 PyPI 배포 스크립트 작성법

임베디드 친구 2025. 7. 7. 20:03
반응형

지난 시간까지 우리는 파이썬의 풍부한 내장 라이브러리를 활용하고 프로젝트의 독립성을 지켜주는 가상환경(venv) 구축법을 함께 다루어 보았습니다. 이제 코드를 짜는 방법을 넘어, 우리가 만든 소프트웨어가 현업이나 상용 환경에서 '진짜 에러 없이 완벽하게 돌아가는지' 증명하고 배포해야 하는 단계를 마주하게 됩니다. 내가 짠 코드가 조금만 수정되어도 기존 기능이 망가지거나, 다른 컴퓨터로 옮겼을 때 의존성 문제로 실행되지 않는 경험을 한 번쯤 해보셨을 것입니다. 제품의 신뢰성을 극대화하는 유닛 테스트와 TDD(테스트 주도 개발), 대규모 프로젝트를 체계적으로 나누는 패키지 설계 기법, 그리고 완성된 프로그램을 전 세계 누구나 사용할 수 있도록 배포하는 오픈소스 패키징 기술까지 오늘 소프트웨어 공장에서 낱낱이 파헤쳐 보겠습니다.

Generated by Gemini AI.

📌 핵심 요약 3줄

  • 유닛 테스트(Unit Test)는 소스코드의 최소 단위인 함수와 메서드를 검증하는 방어벽이며, 전통적인 unittest와 간결한 pytest 프롬프트가 대표적입니다.
  • TDD(테스트 주도 개발)는 '실패(Red) -> 성공(Green) -> 리팩토링(Blue)' 구조의 순환 사이클을 통해 결함 없는 클린 코드를 유도하는 개발 문화입니다.
  • setuptools와 setup.py 스크립트를 빌드 도구로 삼아 패키징하면, 작성한 라이브러리를 PyPI 저장소에 등록하여 전 세계 사용자가 pip install로 다운로드할 수 있게 됩니다.

1. 한눈에 보는 테스트 프레임워크 비교 및 프로젝트 아키텍처

파이썬 개발 생태계에서 양대 산맥을 이루는 테스트 도구의 특징과 배포를 위한 핵심 개념들을 표로 정리했습니다.

① 파이썬 대표 테스트 프레임워크 특징 비교

비교 항목 unittest 모듈 (기본 내장) pytest 프러그인 (외부 확장)
설치 여부 파이썬 설치 시 표준 라이브러리로 자동 탑재 pip install pytest로 별도 설치 필요
코드 문법 스타일 객체지향형 (Class 정의 및 TestCase 상속 필수) 함수형 (일반 함수 구조 및 파이썬 순정 assert문 사용)
에러 검증 메서드 self.assertEqual(), self.assertTrue() 등 단순 비교 연산자 (assert a == b, assert x in y)
실무 추천도 기본 기능 위주의 레거시 시스템에 적합 간결한 코드와 강력한 확장 플러그인으로 트렌디한 대세

② 모듈 vs 패키지 vs 패키징 개념 한 장 정리

용어 물리적 형태 핵심 개념 및 목적
모듈 (Module) 단일 파이썬 파일 (.py) 관련 있는 함수, 변수, 클래스를 하나의 파일에 모아 관리
패키지 (Package) 디렉토리 폴더 (안에 __init__.py 포함) 여러 모듈을 하나의 네임스페이스로 묶어 계층 구조로 관리
패키징 (Packaging) 압축 배포판 파일 (.tar.gz 또는 .whl) 외부 의존성과 메타데이터를 묶어 배포 가능한 상태로 빌드

2. 유닛 테스트 작성하기: unittest와 pytest 패턴

하나의 기능을 검증하기 위해 두 가지 도구가 소스코드를 어떻게 요리하는지 직접 비교해 보면 본인 프로젝트에 맞는 도구를 선택하기 수월해집니다.

Python
 
# [Pattern A] 파이썬 내장 unittest 방식
import unittest

def add(x, y):
    return x + y

class TestMathOperations(unittest.TestCase):
    def test_add(self):
        # 예상 결과와 실제 함수의 반환값을 엄격하게 대조합니다.
        self.assertEqual(add(2, 3), 5)
        self.assertEqual(add(-1, 1), 0)

if __name__ == '__main__':
    unittest.main()
Python
 
# [Pattern B] 서드파티 pytest 방식 (더 직관적이고 깔끔함)
def multiply(x, y):
    return x * y

def test_multiply():
    # 복잡한 매칭 메서드 없이 순수 파이썬 예약어인 assert만으로 통제합니다.
    assert multiply(2, 3) == 6
    assert multiply(-1, 5) == -5

# 실행은 터미널 창에 'pytest 파일명.py'만 치면 자동 감지하여 리포트를 뽑아줍니다.

3. 결함 없는 코드를 만드는 문화: TDD(테스트 주도 개발)

TDD는 기능 구현 코드를 단 한 줄도 적기 전에, '이 함수가 성공하면 이런 값을 뱉을 것이다'라는 가상의 시나리오(테스트)부터 먼저 작성하는 역발상 방법론입니다.

Python
 
# 1단계: 실패(Fail)하는 테스트를 먼저 작성 (is_even 함수가 아직 없으므로 당연히 실패)
def test_is_even():
    assert is_even(4) == True
    assert is_even(7) == False

# 2단계: 테스트를 통과(Pass)시킬 목적으로 '가장 최소한의 뼈대 코드'만 우선 구현
def is_even(n):
    return n % 2 == 0

# 3단계: 리팩토링(Refactor) 과정을 통해 중복을 제거하고 가독성을 고도화 (여기서는 충분히 최적화됨)

4. 스탠다드 패키지 아키텍처 및 PyPI 빌드 배포 자동화

엔터프라이즈 환경에서 표준으로 채택하는 폴더 레이아웃 구조와, 이를 패키징 파일로 말아 올리는 setup.py 청사진입니다.

표준 템플릿 디렉토리 구조

Plaintext
 
my_project/
├── main.py
├── setup.py           # 배포 자동화 메타데이터 설정 파일
├── utils/             # 커스텀 패키지 폴더
│   ├── __init__.py    # 파이썬 3.3 이후 생략 가능하나 패키지 명시용으로 권장
│   └── math_ops.py    # 모듈
└── tests/             # 테스트 전용 폴더
    └── test_math_ops.py

setup.py 스크립트 작성

Python
 
from setuptools import setup, find_packages

setup(
    name='software_factory_project',  # PyPI에 등록될 유일무이한 패키지 이름
    version='1.0.0',
    packages=find_packages(),          # 소스 내 모든 패키지 폴더 자동 수집
    install_requires=[                 # 이 프로그램이 작동하는 데 필요한 외부 라이브러리 지정
        'requests>=2.28.0',
    ],
    entry_points={
        'console_scripts': [
            'factory-run=main:main',   # 터미널에 factory-run 입력 시 main.py의 main 함수 호출
        ],
    },
    author='Software Factory',
    description='파이썬 프로젝트 패키징 테스트 패키지',
)

터미널 빌드 및 오픈소스 배포 명령어 3단계

Bash
 
# 1단계: 배포용 아카이브(.tar.gz) 및 휠(.whl) 파일 생성
python setup.py sdist bdist_wheel

# 2단계: 안전한 PyPI 전송 도구인 twine 설치
pip install twine

# 3단계: 공식 저장소에 배포판 업로드 (PyPI 계정 토큰 필요)
twine upload dist/*

5. 개발을 위한 팁

  • CI/CD 파이프라인에 테스트 코드를 무조건 연동하세요: 로컬 컴퓨터에서 개발자가 수동으로 테스트 코드를 돌리는 방식은 사람의 망각 때문에 실수가 생기기 마련입니다. GitHub Actions나 GitLab CI 같은 현대적인 형상 관리 시스템을 도입해 보세요. 내가 코드를 작성하고 git push를 날리는 순간, 클라우드 서버가 방금 작성한 pytest 환경을 독립적으로 자동 실행하도록 트리거를 걸어두는 방식입니다. 만약 단 하나의 테스트 케이스라도 실패하면 마스터 브랜치로의 합류(Merge)가 자동으로 차단되도록 방어막을 구축하는 것이 상용 서비스 안정성 관리의 기초입니다.
  • 배포 테스트는 전용 연습장(TestPyPI)을 경유하세요: setup.py 설정을 완벽하게 끝냈다고 생각해도 실제 PyPI 서버에 업로드해 보면 파일 누락이나 경로 꼬임 문제로 다운로드가 불가능한 경우가 부지기수입니다. 한번 등록한 버전 번호(예: 1.0.0)는 PyPI 생태계 규칙상 절대 수정하거나 재업로드할 수 없습니다. 따라서 공식 서버에 올리기 전에, 전 세계 개발자들을 위한 샌드박스 연습 공간인 TestPyPI 저장소로 twine upload --repository testpypi dist/* 명령을 먼저 날려 다운로드 및 가동 여부를 예행연습하는 것이 베테랑의 코딩 매너입니다.

6. 흔히 하는 실수

  • 패키지 내부에서 절대 경로 가득한 import를 남발하는 구조: 프로젝트가 점점 커지면서 utils/math_ops.py 모듈이 다른 폴더에 있는 파일을 가져와 쓸 때, 개발자 로컬 환경의 경로를 기준으로 import C:\Users\Admin\my_project\... 식으로 작성하면 패키징 배포 후 완전히 박살 나게 됩니다. 다른 유저가 내 패키지를 내려받아 설치하면 완전히 다른 시스템 경로에 모듈이 배치되기 때문입니다. 패키지 내부 모듈 간의 연결을 정의할 때는 반드시 from . import math_ops 또는 from ..core import base 와 같은 상대 경로(Relative Import) 기법을 명확하게 지켜주어야 배포 후 이식성이 보장됩니다.
  • 의존성 라이브러리의 버전 범위를 누락하여 배포하는 행위: setup.py 스크립트의 install_requires 항목에 단순히 'requests'라고만 적어서 배포하는 실수를 흔히 봅니다. 지금 당장은 잘 돌아갈지 몰라도, 몇 년 뒤 requests 라이브러리에 완전히 하위 호환성이 깨지는 메이저 업데이트가 일어나면 내 코드는 아무것도 손대지 않았는데 갑자기 실행이 불가능해집니다. 따라서 최소한의 작동 보장 범위를 지켜주기 위해 requests>=2.28.0, <3.0.0 처럼 상하한선 버전을 타이트하게 락(Lock)을 걸어 지정해 두어야 오랜 시간이 지나도 망가지지 않는 좀비 같은 패키지를 완성할 수 있습니다.

💡 맺음말

이번 포스팅에서는 내가 열심히 작성한 알고리즘과 비즈니스 로직을 완벽한 상태로 검증하는 유닛 테스트 체계부터 전 세계 파이썬 생태계의 구성원으로 소스코드를 공유하는 오픈소스 배포 아키텍처까지 꼼꼼하게 빌드해 보았습니다. 완벽한 예외 처리와 촘촘한 테스트 코드가 받쳐주고 규칙적인 배포 체계가 연동되는 순간, 여러분의 사이드 프로젝트는 비로소 상용 아키텍처의 완성도를 갖추게 됩니다.

테스트 도중 AssertionError가 발생해 풀리지 않거나 setup.py 빌드 과정에서 종속성 충돌 에러로 먹통이 되었다면 망설이지 말고 아래 댓글 창에 질문을 남겨주세요. 상세히 답변해 드리겠습니다. 감사합니다!

반응형