[Python 시리즈 5편] 함수와 모듈로 코드 나누기

English version

함수와 모듈로 코드 나누기

4편에서 조건과 반복으로 흐름을 만들다 보면 코드가 길어지기 시작합니다. 이때 함수를 사용하면 이름 있는 블록으로 로직을 나눌 수 있고, 모듈로 파일을 분리하면 재사용과 테스트가 쉬워집니다. 이번 글에서는 함수를 정의하는 기본 문법부터 매개변수, 반환값, 모듈 임포트까지 단계별로 살펴봅니다.

이번 글에서 새로 나오는 용어

  1. 매개변수: 함수가 입력 값을 받을 때 사용하는 이름표로 함수 선언부에 적습니다.
  2. 모듈: Python 파일 하나를 가리키는 단위로, 필요한 기능을 임포트해 재사용합니다.
  3. 타입 힌트: name: str처럼 변수나 함수 입출력에 예상 타입을 적어 의도를 밝히는 표기
  4. if name == "main": 파일을 직접 실행할 때만 코드를 돌리고 임포트 시에는 건너뛰게 하는 관용구

핵심 개념

학습 메모

  • 소요 시간: 60분
  • 준비물: 조건·반복 기초, uv 기반 프로젝트
  • 학습 목표: 함수를 정의하고 모듈로 분리해 재사용 가능한 구조 만들기

함수는 로직을 묶는 이름표이고, 모듈은 기능을 파일 단위로 나누는 방법입니다. 두 가지를 조합하면 같은 계산을 여러 곳에서 활용하고, 테스트도 분리할 수 있습니다.

코드로 따라하기

함수 정의 기본

def greet(name: str) -> str:
    return f"안녕하세요, {name}님"

message = greet("지민")
print(message)

함수는 입력 값을 매개변수(parameter)로 받고 처리 결과를 return으로 돌려줍니다. 타입 힌트를 적으면 의도가 더 명확해집니다.

기본값과 키워드 인자

def notify(message: str, urgent: bool = False):
    prefix = "[긴급] " if urgent else "[일반] "
    print(prefix + message)

notify("배포 완료")
notify("에러 발생", urgent=True)

기본값을 지정해 호출 코드를 단순하게 만들 수 있고, urgent=True처럼 키워드 인자를 쓰면 가독성이 좋아집니다.

가변 인자와 키워드 인자

def combine(*values: str, separator: str = ", ") -> str:
    return separator.join(values)

result = combine("A", "B", "C", separator=" | ")
print(result)

*values는 임의 개수의 인자를 튜플로 받습니다. **kwargs를 사용하면 키-값 쌍을 딕셔너리로 받을 수 있습니다.

모듈로 함수 분리하기

프로젝트 구조 예시:

python-playground/
├─ app.py
└─ helpers/
   └─ reports.py

helpers/reports.py에 함수를 정의합니다.

# helpers/reports.py

def summarize_scores(scores: list[int]) -> dict:
    total = sum(scores)
    average = total / len(scores)
    return {"total": total, "average": round(average, 2)}

app.py에서 임포트하여 사용합니다.

from helpers.reports import summarize_scores

scores = [82, 75, 91]
report = summarize_scores(scores)
print(report)

모듈을 분리하면 테스트와 유지보수가 쉬워지고, 다른 스크립트에서도 같은 함수를 재사용할 수 있습니다.

🧱 모듈이란? Python 파일 하나가 곧 모듈입니다. 기능 단위로 파일을 쪼개 두면 임포트해 재사용하거나 테스트할 수 있어 코드가 엉키지 않습니다.

실전 예시: 리포트 생성기 계층 나누기

# services/reports.py
def create_summary(records: list[dict]) -> dict:
    total = len(records)
    completed = sum(1 for record in records if record["done"])
    return {"total": total, "completed": completed}


# cli/report_command.py
from services.reports import create_summary

def render_report(records: list[dict]) -> str:
    summary = create_summary(records)
    return f"전체 {summary['total']}건 중 {summary['completed']}건 완료"

핵심 로직을 services 모듈에 두고, 입출력(터미널, 파일, UI 등)은 별도 모듈에서 담당하면 테스트 범위가 자연스럽게 나뉩니다. 동료와 역할을 나눌 때도 "데이터 처리", "표시"처럼 경계를 명확히 설명할 수 있습니다.

모듈 간 책임 흐름 한눈에 보기

아래 다이어그램은 CLI → 서비스 → 데이터 경로가 어떻게 이어지는지 보여 줍니다. 각 노드는 Python 파일이고, 화살표는 함수 호출 방향입니다.

cli/report_command.py입출력·표시services/reports.py데이터 요약data/CSV·JSONtests/test_reports.py단위 테스트 함수 임포트파일 읽기/쓰기로직 검증

프로젝트를 시각화해두면 모듈이 늘어나도 어디에 기능을 배치할지 빨리 결정할 수 있습니다.

if __name__ == "__main__":

모듈이 직접 실행될 때만 특정 코드를 실행하고, 임포트될 때는 실행하지 않도록 하는 패턴입니다.

def main():
    print("메인 로직 실행")


if __name__ == "__main__":
    main()

이 구조를 쓰면 모듈이 다른 파일에 임포트될 때는 main()이 호출되지 않아 의도치 않은 실행을 막을 수 있습니다.

왜 중요한가

함수로 로직을 나누면 테스트를 독립적으로 작성할 수 있고, 모듈을 나누면 동료와 역할을 나누기도 쉽습니다. 또 if __name__ == "__main__" 패턴을 넣으면 같은 파일이라도 실행 모드와 임포트 모드를 분리해 안정적으로 재사용할 수 있습니다.

실습

  • 따라 하기: helpers 폴더를 만들고 summarize_scores 함수를 그대로 작성한 뒤 app.py에서 임포트해 실행합니다.
  • 확장하기: render_report 같은 표시용 함수를 새 파일에 추가하고 main()에서 문자열만 출력하도록 연결합니다.
  • 디버깅: 모듈 경로를 일부러 틀려 ModuleNotFoundError를 만든 뒤 __init__.py 추가 또는 올바른 상대 경로로 수정합니다.
  • 완료 기준: 최소 두 개 파일에서 함수를 서로 임포트하고 if __name__ == "__main__" 블록 안에서 실행 흐름이 명확히 나뉘었을 때입니다.

마무리

함수와 모듈을 활용하면 코드를 기능별로 나눌 수 있고, 테스트와 재사용이 쉬워집니다. 이렇게 구조화된 코드는 파일 입출력이나 외부 API 연동 같은 작업에서도 큰 도움이 됩니다. 다음 글에서는 파일과 JSON을 다루면서 외부 데이터를 흡수하고 저장하는 흐름을 만들어 보겠습니다.

💬 댓글

이 글에 대한 의견을 남겨주세요