본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2026. 4. 13. · 0 Views
Python 기본 사항 프로젝트 구조와 타이핑 완벽 가이드
AI 에이전트 엔지니어를 위한 Python 프로젝트 구조 설계부터 타입 힌팅까지, 초보자가 반드시 알아야 할 핵심 기초를 다룹니다. 깔끔한 프로젝트 구조와 타입 안전성으로 생산성을 높여보세요.
목차
- 가상환경과 패키지 관리
- 프로젝트 구조의 기본 원칙
- 모듈과 패키지 임포트
- 타입 힌팅 기본
- 기본 데이터 타입과 컬렉션 타입
- 함수 작성과 독스트링
- main 함수와 스크립트 실행 패턴
- 환경 변수와 설정 관리
- 에러 처리와 예외 관리
- 파이썬 스타일 가이드와 코드 품질
- 실전 프로젝트 구조 종합 적용
- 다음 단계를 위한 마무리
1. 가상환경과 패키지 관리
어느 날 김개발 씨가 노트북을 새로 바꾸고 첫 프로젝트를 실행해봤습니다. 그런데 "ModuleNotFoundError"라는 끔찍한 에러가 화면에 가득 찼습니다.
분명 어제까지 잘 되던 코드인데 말이죠.
가상환경은 프로젝트마다 독립적인 Python 실행 환경을 만드는 기술입니다. 마치 각 프로젝트마다 전용 작업실을 할당받는 것과 같습니다.
이렇게 하면 프로젝트 간 패키지 충돌을 막고, 의존성을 깔끔하게 관리할 수 있습니다.
다음 코드를 살펴봅시다.
# 가상환경 생성 및 활성화
python -m venv myenv
source myenv/bin/activate # macOS/Linux
# 패키지 설치와 의존성 파일 생성
pip install requests openai
pip freeze > requirements.txt
# 다른 환경에서 동일하게 복원
pip install -r requirements.txt
# 가상환경 비활성화
deactivate
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 오늘 아침, 새 노트북에서 어제까지 잘 되던 AI 챗봇 프로젝트를 실행하려다가 멈칫했습니다.
화면에 빨간 글씨로 "ModuleNotFoundError: No module named 'openai'"라는 에러가 뜬 것입니다. "어제까지 분명히 됐는데..." 김개발 씨는 당황했습니다.
그때 옆자리 박시니어 씨가 커피를 홀짝이며 다가왔습니다. "아, 가상환경을 안 만드셨군요.
이건 초보자들이 가장 먼저 겪는 벽이에요." 가상환경이란 정확히 무엇일까요? 쉽게 비유하자면, 가상환경은 마치 레고 박스와 같습니다.
여러 프로젝트를 진행할 때 각각의 레고 조각을 섞어버리면 원하는 모델을 조립하기 어렵습니다. A 프로젝트에서 쓰는 레고 조각과 B 프로젝트에서 쓰는 조각이 다를 수 있으니까요.
가상환경은 프로젝트마다 전용 레고 박스를 제공하여 조각이 섞이지 않게 해줍니다. 가상환경이 없던 시절에는 어땠을까요?
개발자들은 시스템 전체에 패키지를 설치했습니다. 프로젝트 A에서는 requests 버전 2.28을 쓰고, 프로젝트 B에서는 버전 2.31을 쓴다면?
충돌이 발생합니다. 한쪽은 동작하지 않게 됩니다.
프로젝트가 10개쯤 되면 의존성 지옥이라 불리는 상황에 빠지게 됩니다. 바로 이런 문제를 해결하기 위해 가상환경이 등장했습니다.
가상환경을 사용하면 프로젝트마다 독립적인 패키지 공간이 생깁니다. 프로젝트 A의 requests 2.28과 프로젝트 B의 requests 2.31이 공존할 수 있습니다.
팀원들과 동일한 환경을 공유하는 것도 가능해집니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 python -m venv myenv 명령어로 가상환경을 생성합니다. myenv는 가상환경의 이름으로, 관행적으로 이 이름을 많이 사용합니다.
다음으로 source myenv/bin/activate로 환경을 활성화합니다. 터미널 프롬프트 앞에 (myenv)가 붙으면 활성화된 것입니다.
pip install로 필요한 패키지를 설치한 뒤, pip freeze > requirements.txt로 현재 상태를 파일에 기록합니다. 이 파일이 바로 프로젝트의 "패키지 목록"입니다.
새 환경에서는 pip install -r requirements.txt 하나로 모든 패키지를 복원할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 AI 에이전트 프로젝트를 개발한다고 가정해봅시다. openai 1.0, langchain 0.1, numpy 1.24 등 수십 개의 패키지가 필요합니다.
가상환경이 없으면 다른 프로젝트와 충돌할 가능성이 큽니다. 최근에는 uv나 poetry 같은 현대적인 도구도 많이 사용합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 가상환경 없이 글로벌에 패키지를 설치하는 것입니다.
이렇게 하면 프로젝트를 이전하거나 팀원과 공유할 때 문제가 발생합니다. 반드시 프로젝트마다 가상환경을 만드는 습관을 들여야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.
"아, 그래서 requirements.txt를 깃허브에 올리라고 하셨군요!" 가상환경을 제대로 설정하면 어디서든 동일한 환경에서 코드를 실행할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 가상환경은 프로젝트 루트에 만들되, .gitignore에 반드시 추가하세요
- requirements.txt는 항상 최신 상태로 유지하는 것이 좋습니다
2. 프로젝트 구조의 기본 원칙
김개발 씨가 첫 팀 프로젝트에 참여했을 때, 선배가 공유한 레포지토리를 열었더니 파일이 수십 개가 널려 있었습니다. "이걸 다 읽어야 하나..." 막막한 그 순간, 박시니어 씨가 다가왔습니다.
프로젝트 구조는 코드 파일을 논리적으로 organized하는 방법입니다. 마치 도서관이 책을 장르별로 분류해서 진열하는 것과 같습니다.
좋은 구조는 코드를 찾고, 이해하고, 유지보수하는 것을 훨씬 쉽게 만들어줍니다.
다음 코드를 살펴봅시다.
# 권장 Python 프로젝트 기본 구조
my_project/
README.md # 프로젝트 설명서
requirements.txt # 의존성 목록
.gitignore # Git 제외 파일
src/ # 소스 코드 디렉토리
__init__.py # 패키지 초기화
main.py # 진입점
config.py # 설정 관리
utils/ # 유틸리티 모듈
__init__.py
helpers.py
tests/ # 테스트 코드
test_main.py
docs/ # 문서
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 첫 팀 프로젝트에 배정받아 흥분했지만, 레포지토리를 클론받고 나서 멈칫했습니다.
app.py, main.py, test.py, utils.py, utils2.py... 파일 이름만 봐도 무엇이 뭔지 알 수 없었습니다.
박시니어 씨가 옆에서 화면을 보며 말했습니다. "이 프로젝트는 급하게 만들다 보니 구조가 엉망이 됐어요.
우리가 새로 리팩토링해볼까요?" 프로젝트 구조란 정확히 무엇일까요? 쉽게 비유하자면, 프로젝트 구조는 마치 집의 도면과 같습니다.
거실, 침실, 주방이 명확하게 구분된 집에서는 생활하기 편합니다. 반면 모든 물건이 한 방에 쌓여 있다면 필요한 것을 찾기도 어렵고 청소도 힘듭니다.
코드도 마찬가지입니다. 역할에 따라 파일을 분리하면 전체를 이해하기 훨씬 쉬워집니다.
프로젝트 구조가 없던 시절에는 어땠을까요? 초보 개발자들은 흔히 모든 코드를 한 파일에 작성합니다.
프로젝트가 작을 때는 문제가 없습니다. 하지만 기능이 10개, 20개로 늘어나면 파일이 수천 줄이 됩니다.
"이 함수가 어디 있더라?"를 찾느라 하루가 갑니다. 코드를 수정하다가 다른 기능을 망가뜨리는 일도 흔합니다.
바로 이런 문제를 해결하기 위해 체계적인 프로젝트 구조가 필요합니다. src/ 디렉토리에는 실행 가능한 소스 코드를 넣습니다.
tests/에는 테스트 코드를 분리해서 넣습니다. docs/에는 프로젝트 문서를 저장합니다.
이렇게 분리하면 각자의 역할이 명확해집니다. 위의 구조를 한 단계씩 살펴보겠습니다.
README.md는 프로젝트의 얼굴입니다. 프로젝트가 무엇인지, 어떻게 실행하는지, 누가 만들었는지 적습니다.
requirements.txt는 앞서 배운 가상환경의 의존성 목록입니다. .gitignore는 Git에 올리지 않을 파일을 지정합니다.
src/main.py는 프로그램의 시작점입니다. src/config.py는 API 키, 데이터베이스 설정 등 환경 설정을 관리합니다.
src/utils/에는 여러 곳에서 재사용되는 유틸리티 함수들을 모아둡니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 AI 에이전트 서비스를 개발한다고 가정해봅시다. 에이전트 로직은 src/agents/, 프롬프트 템플릿은 src/prompts/, API 엔드포인트는 src/api/로 분리할 수 있습니다.
새 팀원이 합류했을 때 이 구조만 보면 프로젝트 전체를 빠르게 파악할 수 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 과도한 디렉토리 분리입니다. 파일이 3개뿐인데 10개의 폴더를 만들면 오히려 혼란스럽습니다.
프로젝트 크기에 맞는 적절한 수준의 구조를 유지하는 것이 중요합니다. 처음에는 단순하게 시작하고, 필요에 따라 점진적으로 분리하세요.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨와 함께 프로젝트를 리팩토링한 김개발 씨는 감탄했습니다.
"이렇게 정리하니까 전체가 한눈에 들어오네요!" 좋은 프로젝트 구조는 혼자 일할 때는 크게 체감되지 않지만, 팀으로 일할 때 그 진가를 발휘합니다. 여러분도 처음부터 체계적인 구조로 프로젝트를 시작하는 습관을 들여보세요.
실전 팁
💡 - 프로젝트는 작게 시작하고 필요에 따라 점진적으로 폴더를 추가하세요
- init.py는 빈 파일이라도 반드시 만들어야 Python이 패키지로 인식합니다
3. 모듈과 패키지 임포트
김개발 씨가 작성한 코드에서 자신이 만든 함수를 다른 파일에서 불러쓰려고 했습니다. 그런데 "ImportError"가 떴습니다.
"같은 프로젝트인데 왜 못 불러오지?" 김개발 씨의 고민이 시작되었습니다.
모듈과 패키지 임포트는 다른 Python 파일에 있는 코드를 현재 파일에서 사용하는 메커니즘입니다. 마치 레스토랑에서 다른 부서의 재료를 빌려와 요리하는 것과 같습니다.
이를 통해 코드 재사용성을 극대화하고 중복을 줄일 수 있습니다.
다음 코드를 살펴봅시다.
# src/utils/helpers.py - 유틸리티 함수 정의
def format_response(text: str) -> str:
return text.strip().capitalize()
def validate_input(data: dict) -> bool:
return "message" in data
# src/main.py - 모듈 임포트와 사용
from src.utils.helpers import format_response, validate_input
user_input = {"message": " hello world "}
if validate_input(user_input):
result = format_response(user_input["message"])
print(result) # "Hello world"
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 이번에는 날씨 정보를 가져오는 기능을 만들고 있었습니다.
API 호출 함수는 한 파일에, 데이터 처리 함수는 다른 파일에 넣고 싶었는데, 도저히 연결이 되지 않았습니다. 박시니어 씨가 자리에서 일어나며 말했습니다.
"Python의 임포트 시스템을 이해하면 이 문제는 쉽게 풀려요. 따라와 보세요." 모듈과 패키지 임포트란 정확히 무엇일까요?
쉽게 비유하자면, 임포트는 마치 도서관에서 책을 빌리는 것과 같습니다. 내가 직접 책을 다시 쓸 필요 없이, 이미 누군가 잘 정리해둔 지식을 가져다 쓰면 됩니다.
Python에서 .py 파일 하나가 곧 모듈 하나입니다. 여러 모듈을 담은 폴더가 바로 패키지입니다.
임포트를 모를 때는 어땠을까요? 같은 함수를 여러 파일에 복사해서 붙여넣어야 했습니다.
버그를 수정하려면 복사한 모든 파일을 찾아 일일이 고쳐야 합니다. 10군데에 복사했는데 3군데를 놓치면?
묵묵히 버그가 살아남아 언젠가 큰 사고를 일으킵니다. 바로 이런 문제를 해결하기 위해 임포트 시스템이 있습니다.
함수를 한 곳에 정의하고, 필요한 곳에서 임포트해서 사용하면 됩니다. 버그 수정은 한 곳만 고치면 끝입니다.
코드의 중복이 줄어들고, 유지보수가 훨씬 쉬워집니다. 위의 코드를 한 줄씩 살펴보겠습니다.
helpers.py 파일에 format_response와 validate_input 두 함수를 정의했습니다. 이 파일이 바로 하나의 모듈입니다.
main.py에서 from src.utils.helpers import format_response, validate_input으로 필요한 함수만 선택적으로 불러옵니다. from A import B 형식은 A라는 모듈에서 B라는 특정 요소만 가져오는 방식입니다.
전체 모듈을 가져올 때는 import src.utils.helpers 형식을 쓸 수도 있습니다. 하지만 필요한 것만 가져오는 것이 코드를 더 명확하게 만듭니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 AI 에이전트 프로젝트를 개발한다고 가정해봅시다.
LLM 호출은 llm_client.py, 프롬프트 관리는 prompts.py, 응답 파싱은 parser.py로 분리할 수 있습니다. 각 모듈에서 임포트로 서로를 연결하면 코드가 깔끔하게 유지됩니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 순환 임포트입니다.
A 파일이 B를 임포트하고, B 파일이 다시 A를 임포트하면 프로그램이 시작조차 되지 않습니다. 의존성 방향을 한쪽으로만 유지하는 것이 중요합니다.
공통 기능은 별도의 utils/ 패키지로 분리하면 이 문제를 피할 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 듣고 임포트를 수정한 김개발 씨는 드디어 코드가 실행되는 것을 확인했습니다. "이제야 이 프로젝트 구조가 이해가 가네요!" 모듈과 패키지 임포트는 Python의 기본 중 기본입니다.
이 개념을 제대로 이해하면 대규모 프로젝트도 자신감 있게 구조화할 수 있습니다. 여러분도 코드를 기능별로 분리하고 임포트로 연결하는 연습을 해보세요.
실전 팁
💡 - 임포트는 파일 상단에 모아두는 것이 파이썬 컨벤션입니다
- 순환 임포트를 피하려면 의존성 방향을 일관되게 유지하세요
4. 타입 힌팅 기본
김개발 씨가 팀원이 작성한 함수를 호출하려는데, 파라미터로 뭘 넣어야 할지 알 수 없었습니다. 코드를 뒤져봐도 주석이 없고, 함수 이름만으로는 유추하기 어려웠습니다.
"이거 문자열인가, 숫자인가..."
타입 힌팅은 변수와 함수에 데이터 타입을 명시적으로 표시하는 기능입니다. 마치 식재료 라벨에 "유통기한: 2026년 12월"이라고 적어두는 것과 같습니다.
코드를 읽는 사람이 어떤 값이 들어와야 하는지 즉시 알 수 있습니다.
다음 코드를 살펴봅시다.
# 타입 힌팅이 적용된 함수 정의
def calculate_price(
base_price: float,
discount_rate: float = 0.0,
is_vip: bool = False
) -> float:
if is_vip:
discount_rate += 0.1
return base_price * (1 - discount_rate)
# 호출 시 IDE가 타입을 검증해줌
total: float = calculate_price(10000, 0.05, True)
name: str = "AI 에이전트 코스"
count: int = 16
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 팀원이 작성한 API 클라이언트 코드를 사용해야 했는데, 함수 시그니처가 이랬습니다.
def process(data, config, options):. 도대체 data에 뭘 넣어야 할까요?
딕셔너리? 문자열?
리스트? 박시니어 씨가 웃으며 말했습니다.
"이 코드 작성한 사람이 타입 힌팅을 안 해놨네요. 우리가 추가해볼까요?" 타입 힌팅이란 정확히 무엇일까요?
쉽게 비유하자면, 타입 힌팅은 마치 택배 상자에 내용물 라벨을 붙이는 것과 같습니다. "이 상자에는 전자제품이 들어있습니다"라고 적혀 있으면, 수령인은 조심해서 다루고, 보관할 때도 적절한 곳에 둡니다.
코드에서도 마찬가지입니다. "이 변수에는 문자열이 들어갑니다"라고 표시하면, 다른 개발자는 함부로 숫자를 넣지 않습니다.
타입 힌팅이 없던 시절에는 어땠을까요? Python은 동적 타입 언어라서 변수에 어떤 타입이든 자유롭게 넣을 수 있습니다.
편리하지만, 프로젝트가 커지면 함수가 반환하는 값의 타입을 매번 추측해야 합니다. 런타임에 "AttributeError: 'int' object has no attribute 'split'" 같은 에러를 만날 일도 잦습니다.
바로 이런 문제를 해결하기 위해 Python 3.5부터 타입 힌팅이 도입되었습니다. 타입 힌팅을 사용하면 코드를 읽는 사람이 함수의 입출력을 즉시 파악할 수 있습니다.
IDE가 잘못된 타입을 전달하면 실시간으로 경고해줍니다. 코드를 작성할 때 자동완성도 훨씬 정확해집니다.
위의 코드를 한 줄씩 살펴보겠습니다. calculate_price 함수의 파라미터에 : float, : bool 같은 타입 표시가 붙어 있습니다.
이것이 바로 타입 힌팅입니다. discount_rate: float = 0.0처럼 기본값과 함께 타입을 지정할 수도 있습니다.
-> float는 이 함수가 float 값을 반환한다는 의미입니다. 함수 밖에서도 total: float = calculate_price(...)처럼 변수에 타입을 명시할 수 있습니다.
물론 Python은 이 힌트를 강제하지 않습니다. 실행 시에는 무시되지만, 개발 도구가 이 정보를 활용하여 더 나은 경험을 제공합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 AI 에이전트 프로젝트에서 LLM의 응답을 처리하는 함수를 만든다고 가정해봅시다.
def parse_llm_response(response: dict) -> list[dict]:처럼 타입을 명시하면, 팀원들이 이 함수를 사용할 때 어떤 형태의 데이터를 넣고 받을 수 있는지 즉시 알 수 있습니다. mypy 같은 정적 분석 도구를 사용하면 런타임 전에 타입 오류를 잡을 수도 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 곳에 과도한 타입 힌팅을 적용하는 것입니다.
단순한 스크립트나 프로토타입에는 오히려 코드가 번잡해질 수 있습니다. 또한 typing 모듈의 Optional, Union, Callable 같은 복잡한 타입을 너무 일찍 도입하면 학습 곡선이 가파라집니다.
기본 타입인 str, int, float, bool, list, dict부터 시작하세요. 다시 김개발 씨의 이야기로 돌아가 봅시다.
팀원의 함수에 타입 힌팅을 추가한 김개발 씨는 VS Code의 자동완성이 마법처럼 작동하는 것을 보고 감탄했습니다. "이걸 왜 진작 안 썼을까요!" 타입 힌팅은 Python 코드의 품질을 한 단계 높여주는 강력한 도구입니다.
여러분도 오늘부터 함수 정의에 타입을 명시하는 습관을 들여보세요.
실전 팁
💡 - 함수 정의에는 반드시 파라미터 타입과 반환 타입을 명시하세요
- 복잡한 타입은 typing 모듈의 Optional, Union 대신 Python 3.10+의 X | Y 문법을 사용하세요
5. 기본 데이터 타입과 컬렉션 타입
김개발 씨가 AI 모델의 설정을 관리하는 코드를 작성하던 중이었습니다. 모델 이름은 문자열, 온도는 소수, 사용 가능한 모델 목록은 리스트...
"이걸 어떻게 깔끔하게 표현하지?" 김개발 씨의 고민이 시작되었습니다.
컬렉션 타입은 리스트, 딕셔너리, 튜플 같이 여러 값을 담는 데이터 구조에 타입을 명시하는 방법입니다. 마치 서랍장에 "여기에는 양말, 저기에는 셔츠"라고 라벨을 붙이는 것과 같습니다.
어떤 종류의 데이터가 담길지 미리 알면 코드가 훨씬 안전해집니다.
다음 코드를 살펴봅시다.
from typing import Optional
# 기본 타입과 컬렉션 타입 조합
model_config: dict[str, str | int | float] = {
"name": "gpt-4",
"temperature": 0.7,
"max_tokens": 4096,
}
available_models: list[str] = ["gpt-4", "claude-3", "gemini"]
system_prompt: Optional[str] = None
version_info: tuple[str, int, int] = ("1.0", 3, 10)
# 타입 힌팅이 적용된 함수
def get_model(name: str, configs: list[dict]) -> dict:
for config in configs:
if config["name"] == name:
return config
return {}
김개발 씨는 입사 3개월 차 주니어 개발자입니다. AI 에이전트의 설정을 관리하는 데이터 클래스를 작성하고 있었습니다.
설정 항목이 다양한 타입을 가지고 있어서, 어떻게 깔끔하게 표현할지 고민이 되었습니다. 박시니어 씨가 화면을 보며 말했습니다.
"컬렉션 타입을 제대로 활용하면 설정 구조가 한눈에 들어와요." 컬렉션 타입이란 정확히 무엇일까요? 쉽게 비유하자면, 컬렉션 타입은 마치 창고의 선반 시스템과 같습니다.
A 선반에는 "전자제품만", B 선반에는 "식품만"이라고 라벨을 붙여두면, 물건을 넣고 찾을 때 실수가 줄어듭니다. Python의 리스트, 딕셔너리, 튜플도 마찬가지입니다.
"이 리스트에는 문자열만 들어간다"고 명시하면, 다른 타입의 값이 들어오는 것을 방지할 수 있습니다. 컬렉션 타입을 모를 때는 어땠을까요?
단순히 data = []라고만 작성하면 이 리스트에 무엇이 들어갈지 알 수 없습니다. 문자열인지, 숫자인지, 딕셔너리인지...
코드를 실행해봐야 확인할 수 있습니다. 팀원이 이 변수를 사용할 때마다 타입을 추측해야 합니다.
바로 이런 문제를 해결하기 위해 컬렉션 타입 힌팅이 필요합니다. list[str]은 "문자열만 담는 리스트"를 의미합니다.
dict[str, int]는 "문자열 키에 정수 값을 가진 딕셔너리"입니다. tuple[str, int, int]는 "문자열, 정수, 정수의 세 요소를 가진 튜플"입니다.
이처럼 컬렉션 내부의 요소 타입까지 명시하면 데이터 구조가 명확해집니다. 위의 코드를 한 줄씩 살펴보겠습니다.
model_config 변수는 dict[str, str | int | float] 타입입니다. 키는 문자열이고, 값은 문자열, 정수, 또는 소수 중 하나입니다.
| 기호는 Python 3.10에서 도입된 Union 타입 문법으로, "이 중 하나"를 의미합니다. available_models는 list[str]로 문자열만 담는 리스트입니다.
system_prompt는 Optional[str]로, 문자열이거나 None일 수 있습니다. AI 설정에서 프롬프트가 선택 사항일 때 자주 사용하는 패턴입니다.
get_model 함수는 list[dict] 타입의 configs를 받아서 dict를 반환합니다. 이 시그니처만 보면 어떤 데이터가 들어오고 나가는지 명확합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 AI 에이전트가 여러 도구를 사용할 때, 도구 정의를 list[dict[str, Any]]로 관리할 수 있습니다.
각 도구가 이름, 설명, 파라미터 스키마를 딕셔너리로 가지고 있으니까요. 타입을 명시하면 팀원들이 이 데이터 구조를 이해하는 데 훨씬 적은 시간이 듭니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 Any 타입을 남발하는 것입니다.
dict[str, Any]는 사실상 타입 힌팅의 의미가 거의 없습니다. 가능하면 구체적인 타입을 명시하는 것이 좋습니다.
또한 Optional을 쓸 때는 함수 안에서 None 체크를 반드시 수행해야 런타임 에러를 막을 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
컬렉션 타입을 적용한 설정 클래스를 본 팀원은 "이거 보기 좋네요!"라며 감탄했습니다. 김개발 씨는 뿌듯한 미소를 지었습니다.
컬렉션 타입은 Python의 기본 데이터 구조를 더 안전하고 명확하게 만들어줍니다. 여러분도 변수를 선언할 때 내부 요소의 타입까지 함께 명시하는 습관을 들여보세요.
실전 팁
💡 - Python 3.10+에서는 Union[str, int] 대신 str | int 문법을 사용하세요
- Optional[X]은 X | None과 동일하며, 값이 없을 수 있는 경우에 사용합니다
6. 함수 작성과 독스트링
김개발 씨가 자신이 2주 전에 작성한 함수를 다시 열어봤습니다. "이게 뭘 하는 함수지?" 변수 이름도 의미가 모호하고, 주석도 없었습니다.
2주 전의 자신에게 한숨을 쉬는 김개발 씨였습니다.
독스트링은 함수, 클래스, 모듈의 목적과 사용법을 설명하는 내장 문서입니다. 마치 가전제품의 사용 설명서와 같습니다.
코드 안에 직접 문서를 작성하므로, 코드와 문서가 분리되지 않는다는 것이 큰 장점입니다.
다음 코드를 살펴봅시다.
def create_agent_prompt(
task: str,
tools: list[str],
persona: str = "helpful assistant"
) -> str:
"""에이전트에게 전달할 시스템 프롬프트를 생성합니다.
Args:
task: 에이전트가 수행할 작업 설명
tools: 에이전트가 사용 가능한 도구 목록
persona: 에이전트의 페르소나 (기본값: helpful assistant)
Returns:
완성된 시스템 프롬프트 문자열
"""
tool_list = ", ".join(tools)
return f"당신은 {persona}입니다. 작업: {task}. 사용 가능한 도구: {tool_list}"
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 2주 전에 급하게 작성한 유틸리티 함수를 다시 보려는데, 도저히 이해가 되지 않았습니다.
def p(t, l, s="default"): ... 이게 무슨 파라미터일까요?
박시니어 씨가 옆에서 턱을 괴며 말했습니다. "함수 이름도 중요하지만, 독스트링을 안 달아놓으면 나중에 본인이 고생해요.
저도 초보 때 그랬거든요." 독스트링이란 정확히 무엇일까요? 쉽게 비유하자면, 독스트링은 마치 레스토랑의 메뉴판과 같습니다.
요리 이름만 보면 어떤 재료가 들어있는지 알기 어렵습니다. 하지만 설명이 적혀 있으면 주문하기도 쉽고, 알레르기가 있는 재료도 피할 수 있습니다.
코드에서도 마찬가지입니다. 함수가 무엇을 하는지, 어떤 값을 넣어야 하는지, 무엇을 반환하는지 적어두면 사용자가 한눈에 파악할 수 있습니다.
독스트링이 없던 시절에는 어땠을까요? 함수 바깥에 주석을 달거나, 별도의 위키 페이지에 설명을 적었습니다.
하지만 코드가 수정되면 주석은 업데이트되지 않는 경우가 대부분입니다. 문서와 코드가 점점 어긋나면서, 결국 아무도 문서를 믿지 않게 됩니다.
바로 이런 문제를 해결하기 위해 독스트링이 있습니다. 독스트링은 함수 본체 안에 직접 작성합니다.
코드와 문서가 같은 위치에 있으므로, 수정할 때 함께 업데이트하기 쉽습니다. IDE에서 함수 이름에 마우스를 올리면 독스트링이 팝업으로 표시됩니다.
help() 함수로도 언제든 확인할 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
함수 정의 바로 아래에 큰따옴표 세 개 """로 감싼 텍스트가 독스트링입니다. 첫 줄에는 함수의 목적을 한 문장으로 요약합니다.
Args: 섹션에는 각 파라미터의 의미와 타입을 설명합니다. Returns: 섹션에는 반환값에 대해 설명합니다.
Google 스타일 독스트링이라 불리는 이 형식은 파이썬 커뮤니티에서 널리 사용됩니다. NumPy 스타일이나 Sphinx 스타일도 있지만, 팀에서 하나의 스타일을 정하고 일관되게 사용하는 것이 중요합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 AI 에이전트 프로젝트에서 프롬프트를 생성하는 함수를 만든다고 가정해봅시다.
독스트링이 잘 작성되어 있으면, 팀원들이 이 함수를 사용할 때 매번 코드 내부를 들여다볼 필요가 없습니다. 심지어 Sphinx 같은 도구로 독스트링에서 자동 API 문서를 생성할 수도 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 독스트링에 구현细节을 적는 것입니다.
독스트링은 "어떻게"가 아니라 "무엇을" 설명하는 곳입니다. 내부 로직이 변경되면 독스트링도 수정해야 하므로, 불필요한 내용은 적지 마세요.
또한 코드와 동일한 내용을 단순히 반복하는 독스트링은 의미가 없습니다. "이 함수는 이름을 반환합니다" 같은 것은 함수 이름만 봐도 알 수 있습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 모든 함수에 독스트링을 추가한 김개발 씨는 이제 VS Code에서 팝업 설명을 확인하며 즐겁게 코딩하고 있습니다.
"이제 2주 뒤에 봐도 무슨 함수인지 알겠네요!" 좋은 독스트링은 코드의 품질을 보여주는 지표입니다. 여러분도 오늘부터 함수를 작성할 때 독스트링을 함께 달아보세요.
실전 팁
💡 - 독스트링의 첫 줄에는 함수의 목적을 한 문장으로 간결하게 작성하세요
- 팀 내에서 Google 스타일, NumPy 스타일 등 하나의 형식을 정하고 일관되게 사용하세요
7. main 함수와 스크립트 실행 패턴
김개발 씨가 작성한 파이썬 스크립트를 다른 모듈에서 임포트했더니, 임포트만 했는데 코드가 실행되어 버렸습니다. "나는 함수만 가져다 쓰고 싶은데 왜 실행이 되지?" 당황한 김개발 씨의 사건이 시작되었습니다.
if name == "main" 패턴은 스크립트가 직접 실행될 때만 코드를 실행하고, 임포트될 때는 실행하지 않도록 제어하는 Python의 핵심 패턴입니다. 마치 건물의 메인 스위치와 같아서, 직접 방문했을 때만 전등이 켜지도록 할 수 있습니다.
다음 코드를 살펴봅시다.
# src/agents/chat_agent.py
def initialize_agent(config: dict) -> dict:
agent = {"model": config["model"], "tools": config["tools"]}
print(f"에이전트 초기화 완료: {agent['model']}")
return agent
def run_agent(agent: dict, query: str) -> str:
return f"[{agent['model']}] 응답: {query}에 대한 답변입니다."
# 직접 실행할 때만 테스트 코드가 동작
if __name__ == "__main__":
test_config = {"model": "gpt-4", "tools": ["search", "calc"]}
agent = initialize_agent(test_config)
print(run_agent(agent, "Python이란?"))
김개발 씨는 입사 3개월 차 주니어 개발자입니다. AI 에이전트 모듈을 테스트하려고 다른 파일에서 임포트했는데, 테스트 코드가 자동으로 실행되어 버렸습니다.
화면에 "에이전트 초기화 완료"라는 메시지가 임포트만 했는데 출력된 것입니다. 박시니어 씨가 커피잔을 내려놓으며 말했습니다.
"아, if __name__ == "__main__" 패턴을 안 쓰셨군요. 이건 Python을 배울 때 꼭 알아야 하는 필수 패턴이에요." if __name__ == "__main__"이란 정확히 무엇일까요?
쉽게 비유하자면, 이 패턴은 마치 전시회의 "관람객 전용 출입문"과 같습니다. 직접 전시회에 방문하면 안내 데스크가 작동하지만, 전시품 사진을 다른 곳에 빌려주는 것만으로는 안내 데스크가 필요 없습니다.
마찬가지로, 파일을 직접 실행하면 메인 코드가 돌아가지만, 다른 파일에서 임포트만 하면 메인 코드는 건너뜁니다. 이 패턴을 모를 때는 어떤 문제가 생길까요?
파일을 직접 실행할 때와 임포트할 때의 동작이 같습니다. 테스트용 print 문이 임포트할 때마다 실행됩니다.
데이터베이스 연결 코드가 임포트 시점에 실행되어 리소스를 낭비합니다. 모듈 단위 테스트가 불가능해집니다.
이런 문제는 프로젝트가 커질수록 심각해집니다. 바로 이런 문제를 해결하기 위해 if __name__ == "__main__" 패턴이 있습니다.
Python은 파일을 직접 실행할 때 __name__ 변수를 "__main__"으로 설정합니다. 반면 임포트할 때는 __name__이 모듈의 파일 경로가 됩니다.
이 차이를 활용하면 실행 여부를 완벽하게 제어할 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
initialize_agent와 run_agent는 일반 함수입니다. 이 함수들은 임포트해도 아무 동작을 하지 않습니다.
문제가 되는 부분은 아랫쪽의 실행 코드입니다. if __name__ == "__main__": 블록 안에 있는 코드는 이 파일을 직접 실행할 때만 동작합니다.
다른 파일에서 from src.agents.chat_agent import initialize_agent라고 임포트하면, 함수만 가져오고 테스트 코드는 실행되지 않습니다. 마치 원하던 대로 동작합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 AI 에이전트 프로젝트에서 여러 에이전트 모듈을 개발한다고 가정해봅시다.
chat_agent.py, search_agent.py, code_agent.py 각각에 if __name__ == "__main__" 패턴으로 독립적인 테스트를 넣을 수 있습니다. 각 모듈을 개별적으로 실행해서 테스트하면서도, 메인 애플리케이션에서는 임포트만 깔끔하게 할 수 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 실행 코드를 이 블록 안에 넣지 않는 것입니다.
파일 최상단에 config = load_config() 같은 코드를 두면 임포트할 때마다 실행됩니다. 파일 최상단에는 import 문과 함수/클래스 정의만 두고, 실행 코드는 반드시 if __name__ == "__main__" 블록 안에 넣으세요.
다시 김개발 씨의 이야기로 돌아가 봅시다. 모든 모듈에 이 패턴을 적용한 김개발 씨는 드디어 깔끔하게 모듈을 임포트할 수 있게 되었습니다.
"이 패턴 하나로 이렇게 달라지다니!" if __name__ == "__main__"은 Python 개발자라면 반드시 알아야 하는 기본 패턴입니다. 여러분도 오늘부터 스크립트 파일에 이 패턴을 적용해 보세요.
실전 팁
💡 - 파일 최상단에는 import와 정의만 두고, 실행 코드는 반드시 main 블록 안에 넣으세요
- main 블록 안에는 간단한 테스트나 데모 코드를 넣어 모듈 단위 테스트로 활용하세요
8. 환경 변수와 설정 관리
김개발 씨가 팀 프로젝트의 코드를 깃허브에 푸시했는데, 옆에서 박시니어 씨가 소리쳤습니다. "잠깐!
API 키가 그 코드에 들어있어요!" 김개발 씨는 얼굴이 하얗게 변했습니다. OpenAI API 키가 온 세상에 공개된 것입니다.
환경 변수는 코드 외부에서 설정 값을 주입하는 메커니즘입니다. 마치 은행의 금고 암호를 코드에 적어두지 않고, 따로 안전한 곳에 보관하는 것과 같습니다.
API 키, 데이터베이스 비밀번호 같은 민감한 정보를 코드와 분리하여 관리할 수 있습니다.
다음 코드를 살펴봅시다.
import os
from dotenv import load_dotenv
# .env 파일에서 환경 변수 로드
load_dotenv()
# 환경 변수 사용 (기본값 지정 가능)
api_key: str = os.getenv("OPENAI_API_KEY", "")
model_name: str = os.getenv("MODEL_NAME", "gpt-4")
max_tokens: int = int(os.getenv("MAX_TOKENS", "4096"))
debug_mode: bool = os.getenv("DEBUG", "false").lower() == "true"
# 설정 검증
if not api_key:
raise ValueError("OPENAI_API_KEY 환경 변수가 설정되지 않았습니다.")
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 첫 팀 프로젝트에서 AI 에이전트를 개발하던 중, OpenAI API 키를 코드에 직접 적어버렸습니다.
다행히 박시니어 씨가 커밋 전에 발견했지만, 김개발 씨는 식은땀을 흘렸습니다. "API 키를 코드에 적어두면 절대 안 돼요.
깃허브 전체가 키를 크롤링하는 봇들로 가득하거든요." 박시니어 씨의 말에 김개발 씨는 고개를 끄덕였습니다. 환경 변수란 정확히 무엇일까요?
쉽게 비유하자면, 환경 변수는 마치 식당의 비밀 레시피와 같습니다. 레시피를 메뉴판에 적어두면 모든 손님이 볼 수 있습니다.
하지만 주방장의 머릿속이나 비밀 수첩에만 적어두면 안전합니다. 코드에서도 API 키, 비밀번호 같은 민감한 값을 코드 밖에서 관리해야 합니다.
환경 변수를 모를 때는 어떤 문제가 생길까요? API 키가 코드에 하드코딩되면 보안 사고로 이어집니다.
개발 환경과 운영 환경의 설정을 바꾸려면 코드를 수정해야 합니다. 팀원마다 다른 설정을 사용하려면 코드를 매번 수정해서 커밋해야 합니다.
이는 혼란과 버그의 원인이 됩니다. 바로 이런 문제를 해결하기 위해 환경 변수가 필요합니다.
os.getenv() 함수로 환경 변수를 읽어옵니다. 코드에는 어떤 값이 들어올지 모르게 되므로, 보안이 유지됩니다.
개발 환경과 운영 환경에서 같은 코드로 다른 설정을 사용할 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
from dotenv import load_dotenv()은 프로젝트 루트의 .env 파일에서 환경 변수를 읽어와서 시스템 환경 변수로 설정합니다. os.getenv("OPENAI_API_KEY", "")는 환경 변수를 읽고, 없으면 빈 문자열을 기본값으로 사용합니다.
os.getenv("MAX_TOKENS", "4096")는 문자열을 반환하므로 int()로 변환해야 합니다. 불리언 값도 "true" 문자열을 비교해서 변환합니다.
마지막으로 if not api_key:로 필수 설정이 누락되지 않았는지 검증합니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 AI 에이전트 프로젝트에서 OPENAI_API_KEY, PINECONE_API_KEY, DATABASE_URL 같은 수십 개의 환경 변수를 관리해야 할 수 있습니다. .env 파일에 로컬 설정을 적어두고, 서버에서는 운영체제의 환경 변수 설정 기능을 사용합니다.
.env 파일은 반드시 .gitignore에 추가하여 버전 관리에서 제외해야 합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 .env 파일을 깃허브에 커밋하는 것입니다. 한 번 공개된 API 키는 즉시 폐기해야 합니다.
또한 .env.example 파일을 만들어 필수 환경 변수의 목록과 예시 값을 적어두면, 새 팀원이 프로젝트를 설정할 때 큰 도움이 됩니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
.env 파일로 설정을 분리하고 .gitignore에 추가한 김개발 씨는 안도의 한숨을 쉬었습니다. "이제 API 키가 노출될 일은 없겠죠?" 환경 변수는 프로덕션 환경에서 필수적인 보안 관행입니다.
여러분도 오늘부터 민감한 정보를 코드에서 분리하는 습관을 들여보세요.
실전 팁
💡 - .env 파일은 절대 깃허브에 커밋하지 마세요. .env.example로 템플릿만 공유하세요
- 필수 환경 변수가 누락되면 앱 시작 시점에 명확한 에러를 발생시키세요
9. 에러 처리와 예외 관리
김개발 씨가 작성한 AI 에이전트가 갑자기 멈췄습니다. 화면에 빨간 글씨로 "ConnectionError"가 찍혀 있었습니다.
사용자에게는 "서버 오류가 발생했습니다"라는 끔찍한 메시지가 떠 있었고, 김개발 씨는 당황했습니다.
예외 처리는 프로그램 실행 중 발생하는 에러를 감지하고 우아하게 대응하는 메커니즘입니다. 마치 비행기의 조종사가 난기류를 만났을 때 안전 절차에 따라 대응하는 것과 같습니다.
예상치 못한 상황에서도 프로그램이 비정상 종료되지 않도록 보호합니다.
다음 코드를 살펴봅시다.
import json
def call_llm_api(prompt: str) -> dict:
"""LLM API를 호출하고 응답을 파싱합니다."""
try:
# API 호출 (실제로는 requests나 openai 라이브러리 사용)
response = simulate_api_call(prompt)
return json.loads(response)
except json.JSONDecodeError as e:
print(f"응답 파싱 실패: {e}")
return {"error": "invalid_response", "raw": response}
except ConnectionError as e:
print(f"API 연결 실패: {e}")
return {"error": "connection_failed"}
except Exception as e:
print(f"예상치 못한 오류: {e}")
return {"error": "unknown"}
def simulate_api_call(prompt: str) -> str:
return '{"result": "응답입니다"}'
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 배포한 AI 에이전트 서비스에 갑자기 사용자 불만이 쏟아졌습니다.
"서버 오류가 발생했습니다"라는 메시지만 보이고, 정작 무엇이 문제인지 알 수 없었습니다. 박시니어 씨가 로그를 확인하며 말했습니다.
"API 서버가 일시적으로 응답하지 않았는데, 에러 처리가 안 돼 있어서 사용자에게까지 전파됐어요." 예외 처리란 정확히 무엇일까요? 쉽게 비유하자면, 예외 처리는 마치 자동차의 안전벨트와 에어백과 같습니다.
사고가 나면 자동차가 즉시 멈추지 않고, 안전장치가 작동하여 탑승자를 보호합니다. 코드에서도 네트워크 오류, 파일 없음, 잘못된 입력 같은 "사고"가 언제든 발생할 수 있습니다.
예외 처리는 이런 상황에서 프로그램이 멈추지 않고 적절히 대응하도록 보호합니다. 예외 처리가 없던 시절에는 어땠을까요?
에러가 발생하면 프로그램이 즉시 종료됩니다. 사용자는 빈 화면이나 알 수 없는 에러 메시지를 보게 됩니다.
에러의 원인을 파악하려면 로그를 뒤지는 수밖에 없습니다. 서비스가 멈추면 사용자는 이탈하고, 비즈니스에 손실이 발생합니다.
바로 이런 문제를 해결하기 위해 try-except 구문이 있습니다. try 블록 안에 에러가 발생할 수 있는 코드를 넣습니다.
except 블록에서 특정 에러를 잡아서 처리합니다. 이렇게 하면 에러가 발생해도 프로그램이 계속 실행될 수 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. try: 블록 안에서 API를 호출하고 응답을 파싱합니다.
여기서 두 가지 에러가 발생할 수 있습니다. json.loads()가 실패하면 json.JSONDecodeError가 발생합니다.
API 서버에 연결할 수 없으면 ConnectionError가 발생합니다. 각각의 except 블록에서 에러 종류별로 다른 대응을 합니다.
파싱 실패면 원본 응답을 포함해서 반환하고, 연결 실패면 별도 메시지를 반환합니다. 마지막 except Exception은 예상치 못한 모든 에러를 잡는 안전망입니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 AI 에이전트가 외부 API를 호출할 때, 네트워크가 불안정할 수 있습니다.
try-except로 임시 오류를 감지하고 재시도 로직을 구현할 수 있습니다. 또한 사용자에게는 "잠시 후 다시 시도해주세요" 같은 친절한 메시지를 보여주고, 내부적으로는 상세한 로그를 기록하여 나중에 원인을 파악할 수 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 except Exception:만 써서 모든 에러를 삼키는 것입니다.
이렇게 하면 실제 버그도 숨겨져서 디버깅이 불가능해집니다. 구체적인 예외부터 순서대로 처리하고, Exception은 최후의 안전망으로만 사용하세요.
또한 except:처럼 에러 종류를 생략하면 심지어 키보드 인터럽트까지 잡혀서 프로그램이 종료되지 않습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
예외 처리를 추가한 서비스는 이제 API 오류가 발생해도 사용자에게 친절한 메시지를 보여줍니다. 김개발 씨는 로그에서 오류 원인도 쉽게 찾을 수 있게 되었습니다.
"이제 사용자가 당황할 일이 없겠네요!" 예외 처리는 프로덕션 코드에서 선택이 아닌 필수입니다. 여러분도 에러가 발생할 수 있는 곳에 적절한 예외 처리를 추가해 보세요.
실전 팁
💡 - 구체적인 예외부터 처리하고, Exception은 최후의 안전망으로만 사용하세요
- except 블록에서는 최소한의 로그를 기록하여 나중에 원인을 파악할 수 있게 하세요
10. 파이썬 스타일 가이드와 코드 품질
김개발 씨가 처음으로 코드 리뷰를 받았습니다. 그런데 선배의 코멘트가 빼곡했습니다.
"들여쓰기가 4칸이 아니에요", "변수 이름에 카멜케이스를 쓰지 마세요", "줄 길이가 120자를 넘었어요"... 김개발 씨는 "코드가 잘 돌아가면 되는 거 아닌가요?"라고 생각했습니다.
PEP 8은 Python 공식 스타일 가이드로, 코드의 일관성과 가독성을 위한 규칙 집합입니다. 마치 한 나라의 도로교통법과 같아서, 모두가 같은 규칙을 따르면 혼란 없이 원활하게 소통할 수 있습니다.
개인의 취향이 아닌 커뮤니티의 합의입니다.
다음 코드를 살펴봅시다.
# PEP 8 준수 예시
from typing import Optional
import os
def calculate_agent_cost(
model_name: str,
input_tokens: int,
output_tokens: int,
cost_per_1k: float = 0.03,
) -> float:
"""에이전트의 API 사용 비용을 계산합니다."""
total_tokens = input_tokens + output_tokens
# 소수점 둘째 자리까지 반올림
cost = (total_tokens / 1000) * cost_per_1k
return round(cost, 2)
# 상수는 대문자 스네이크케이스
MAX_RETRY_COUNT = 3
DEFAULT_MODEL = "gpt-4"
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 처음으로 코드 리뷰를 받았는데, 의외로 스타일에 대한 지적이 많았습니다.
"이건 기능과 상관없잖아요..."라고 속으로 중얼거렸지만, 박시니어 씨가 진지하게 설명하기 시작했습니다. "코드는 컴퓨터보다 사람이 읽는 시간이 더 길어요.
스타일이 일관되지 않으면 코드를 읽는 데 시간이 배로 듭니다." PEP 8이란 정확히 무엇일까요? 쉽게 비유하자면, PEP 8은 마치 영어의 맞춤법과 같습니다.
"colour"과 "color"가 섞인 글을 읽으면 어색합니다. 비록 뜻은 알 수 있어도, 읽는 흐름이 끊깁니다.
코드도 마찬가지입니다. 한 프로젝트 안에서 스타일이 제각각이면 읽는 사람이 계속 문맥을 전환해야 합니다.
PEP 8이 없던 시절에는 어땠을까요? 각 개발자가 자신의 스타일을 고집했습니다.
한 사람은 2칸 들여쓰기를 쓰고, 다른 사람은 탭을 씁니다. 변수 이름도 userName, user_name, UserName이 섞여 있습니다.
오픈소스 프로젝트에 기여하려면 먼저 그 프로젝트의 스타일을 배워야 했습니다. 바로 이런 문제를 해결하기 위해 PEP 8이 만들어졌습니다.
PEP 8은 들여쓰기, 명명 규칙, 줄 길이, 임포트 순서 등을 정의합니다. 들여쓰기는 4칸 공백을 사용합니다.
함수와 변수는 snake_case, 클래스는 CamelCase, 상수는 UPPER_SNAKE_CASE를 사용합니다. 한 줄은 79자(최대 120자)를 넘지 않는 것을 권장합니다.
위의 코드를 한 줄씩 살펴보겠습니다. 함수 이름 calculate_agent_cost는 모두 소문자에 단어 사이를 밑줄로 연결한 snake_case입니다.
파라미터도 같은 규칙을 따릅니다. 상수인 MAX_RETRY_COUNT와 DEFAULT_MODEL은 모두 대문자로 작성합니다.
함수 정의와 함수 본체 사이에는 빈 줄을 두 줄 띄웁니다. 임포트는 표준 라이브러리, 서드파티, 로컬 패키지 순으로 그룹을 나눕니다.
각 그룹 사이에 빈 줄을 하나씩 넣습니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 AI 에이전트 프로젝트에서 5명의 개발자가 함께 작업한다고 가정해봅시다. 모두가 PEP 8을 따르면 코드 리뷰 시 스타일에 대한 논의를 줄일 수 있고, 누가 작성한 코드든 동일한 방식으로 읽을 수 있습니다.
flake8, black, ruff 같은 자동 도구를 사용하면 스타일 검사와 자동 포매팅이 가능합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 스타일에만 집중하는 것입니다. PEP 8은 중요하지만, 코드의 기능과 구조가 우선입니다.
스타일을 고치다가 로직을 망가뜨리면 안 됩니다. 또한 기존 프로젝트에 합류했다면, 그 프로젝트의 기존 스타일을 따르는 것이 PEP 8을 강제하는 것보다 중요합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. PEP 8을 적용한 김개발 씨의 코드는 이제 팀원들도 읽기 쉬워졌습니다.
코드 리뷰 시간도 절반으로 줄었습니다. "스타일이 통일되니까 확실히 다르네요!" 좋은 코드 스타일은 혼자만의 이기적인 습관이 아니라, 팀을 위한 배려입니다.
여러분도 black이나 ruff 같은 도구를 사용해 코드 스타일을 자동화해 보세요.
실전 팁
💡 - ruff나 black 같은 포매터를 사용하면 스타일을 자동으로 일관되게 맞출 수 있습니다
- 기존 프로젝트에 합류할 때는 프로젝트의 기존 스타일을 우선 따르세요
11. 실전 프로젝트 구조 종합 적용
김개발 씨는 지금까지 배운 모든 것을 종합해서 첫 본격적인 프로젝트를 만들어보려고 합니다. 가상환경, 프로젝트 구조, 타입 힌팅, 독스트링, 환경 변수, 예외 처리...
"이걸 다 어떻게 하나로 엮지?" 김개발 씨의 두 번째 도전이 시작되었습니다.
실전 프로젝트 구조는 지금까지 배운 모든 기초를 종합하여 하나의 완성된 프로젝트로 엮는 방법입니다. 마치 각 재료를 개별적으로 공부한 요리사가 드디어 첫 요리를 완성하는 것과 같습니다.
AI 에이전트 엔지니어로서 실제로 사용할 수 있는 프로젝트 뼈대를 만들어봅니다.
다음 코드를 살펴봅시다.
# ai_agent_project/ 프로젝트 전체 구조
ai_agent_project/
.env.example # 환경 변수 템플릿
.gitignore # Git 제외 파일
requirements.txt # 의존성 목록
README.md # 프로젝트 설명
src/
__init__.py
main.py # 진입점 (__main__ 패턴 적용)
config.py # 환경 변수 로드
agents/
__init__.py
base.py # 에이전트 기본 클래스
chat.py # 채팅 에이전트
utils/
__init__.py
logger.py # 로깅 유틸리티
tests/
__init__.py
test_chat_agent.py
# src/config.py - 설정 관리
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
API_KEY: str = os.getenv("OPENAI_API_KEY", "")
MODEL: str = os.getenv("MODEL_NAME", "gpt-4")
DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 지난 몇 주간 배운 것을 정리하며, 박시니어 씨에게 물었습니다.
"이걸 다 실제 프로젝트에 어떻게 적용하죠?" 박시니어 씨가 미소를 지으며 말했습니다. "직접 하나 만들어봅시다.
AI 에이전트 프로젝트 뼈대를 함께 세워보죠." 실전 프로젝트 구조란 정확히 무엇일까요? 쉽게 비유하자면, 실전 프로젝트 구조는 마치 건축의 도면과 같습니다.
벽돌(함수), 시멘트(모듈), 페인트(스타일)를 개별적으로 배웠다면, 이제 집 전체의 도면을 그려야 합니다. 어디에 벽을 세우고, 어디에 문을 달지 전체를 조망하는 것이 실전 프로젝트 구조입니다.
프로젝트 뼈대를 세울 때 무엇을 고려해야 할까요? 첫째, 가상환경으로 프로젝트를 격리합니다.
requirements.txt에 의존성을 기록합니다. 둘째, src/ 디렉토리에 소스 코드를, **tests/**에 테스트를 분리합니다.
셋째, 모든 함수에 타입 힌팅과 독스트링을 적용합니다. 넷째, 환경 변수로 민감한 설정을 관리합니다.
다섯째, if name == "main" 패턴으로 실행을 제어합니다. 위의 프로젝트 구조를 한 단계씩 살펴보겠습니다.
최상단에는 .env.example, .gitignore, requirements.txt, README.md가 있습니다. 이 네 파일은 모든 Python 프로젝트에 필수적입니다.
src/ 안에는 기능별로 디렉토리를 분리합니다. agents/에는 에이전트 관련 코드, utils/에는 유틸리티 코드를 넣습니다.
config.py에서 환경 변수를 한 번에 로드하고, Config 클래스로 관리합니다. 이렇게 하면 다른 모듈에서 from src.config import Config로 설정에 접근할 수 있습니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 새로운 AI 에이전트 기능을 추가한다고 가정해봅시다.
src/agents/ 아래에 새 파일을 만들고, tests/에 테스트를 추가하면 됩니다. 환경 변수만 새로 추가하면 설정이 완료됩니다.
이 뼈대가 있으면 팀원들이 새 기능을 추가할 때 일관된 방식으로 작업할 수 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 처음부터 완벽한 구조를 만들려고 하는 것입니다. 구조는 프로젝트의 필요에 따라 점진적으로 발전합니다.
처음에는 파일 3-4개로 시작하고, 기능이 늘어나면 디렉토리를 추가하는 것이 좋습니다. 과한 초기 구조는 오히려 진입 장벽이 됩니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨와 함께 프로젝트 뼈대를 완성한 김개발 씨는 뿌듯했습니다.
가상환경 설정부터 예외 처리까지, 배운 모든 것이 하나의 프로젝트로 자연스럽게 연결되었습니다. "드디어 진짜 프로젝트를 만들 준비가 됐네요!" 지금까지 배운 모든 것은 AI 에이전트 엔지니어로 성장하기 위한 단단한 기초입니다.
여러분도 이 뼈대를 복사해서 나만의 AI 에이전트 프로젝트를 시작해 보세요.
실전 팁
💡 - 프로젝트는 작게 시작하고 기능이 늘어날 때마다 구조를 점진적으로 발전시키세요
- 새 프로젝트를 시작할 때마다 이 뼈대를 템플릿처럼 복사해서 사용하면 시간이 절약됩니다
12. 다음 단계를 위한 마무리
김개발 씨는 드디어 Python 기초의 마지막 페이지를 넘겼습니다. 가상환경부터 프로젝트 구조, 타입 힌팅, 예외 처리까지...
"이제 뭘 더 배워야 하지?" 김개발 씨의 눈빛은 이미 다음 단계를 향하고 있었습니다.
Python 기초를 마스터한 다음 단계는 테스트, 재현성, 프로덕션 함정을 배우는 것입니다. 마치 운전을 배울 때 기어 변속을 마스터한 후, 고속도로 주행법을 배우는 것과 같습니다.
기초가 탄탄해야 실전에서도 흔들리지 않습니다.
다음 코드를 살펴봅시다.
# 이번 카드뉴스에서 배운 핵심 요약
fundamentals = {
"가상환경": "프로젝트마다 독립적인 패키지 공간 생성",
"프로젝트 구조": "src/tests/docs 논리적 분리",
"모듈 임포트": "코드 재사용과 순환 참조 방지",
"타입 힌팅": "변수와 함수의 데이터 타입 명시",
"독스트링": "함수의 목적과 사용법을 내장 문서로 작성",
"__main__ 패턴": "직접 실행과 임포트 구분",
"환경 변수": "API 키 등 민감한 정보를 코드 외부에서 관리",
"예외 처리": "에러를 감지하고 우아하게 대응",
"PEP 8": "Python 공식 코드 스타일 가이드 준수",
}
for key, value in fundamentals.items():
print(f"- {key}: {value}")
"AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 두 번째 카드뉴스입니다. 지난 1화에서는 에이전트 AI 엔지니어가 되기 위한 전체 로드맵과 학습 방법을 소개했습니다.
이번에는 그 로드맵의 첫 단추인 Python 기본 사항을 배웠습니다. 김개발 씨는 이제 Python 프로젝트를 처음부터 끝까지 스스로 세울 수 있습니다.
가상환경을 만들고, 프로젝트 구조를 잡고, 타입 힌팅으로 코드를 명확하게 작성하고, 환경 변수로 보안을 지키고, 예외 처리로 안전성을 확보할 수 있습니다. 하지만 이것은 시작에 불과합니다.
프로젝트가 커지면 새로운 문제들이 등장합니다. "이 함수가 맞게 동작하는지 어떻게 확인하지?" "동료 컴퓨터에서는 왜 다르게 동작하지?" "배포했더니 로컬에서는 되던 게 안 돼!" 이런 문제들은 기초만으로는 해결하기 어렵습니다.
다음 카드뉴스에서는 Python 고급 - 테스트, 재현성, 프로덕션 함정을 다룹니다. 테스트 작성법부터 시작해서, 코드가 어디서든 동일하게 동작하게 만드는 재현성, 그리고 로컬에서는 잘 되는데 배포하면 깨지는 프로덕션 함정까지.
이 주제들은 실무 AI 에이전트 엔지니어가 반드시 마스터해야 하는 고급 기술입니다. 기초가 탄탄하면 고급 주제도 자연스럽게 이해할 수 있습니다.
이번 카드뉴스에서 배운 가상환경, 타입 힌팅, 예외 처리 같은 기초가 다음 단계의 든든한 밑거름이 될 것입니다. 김개발 씨는 박시니어 씨에게 말했습니다.
"다음에는 테스트를 배우는 건가요?" 박시니어 씨가 고개를 끄덕였습니다. "맞아요.
프로덕션에서 돌아가는 코드를 작성하려면 테스트는 필수거든요." 여러분도 이번 카드뉴스에서 배운 기초를 복습하면서, 다음 단계를 준비해 보세요. Python은 배울수록 깊이 있는 언어입니다.
기초를 확실히 다져두면 앞으로의 학습이 훨씬 수월해질 것입니다.
실전 팁
💡 - 배운 내용을 바로 실제 프로젝트에 적용해보세요. 이론만으로는 한계가 있습니다
- 다음 카드뉴스에서는 테스트, 재현성, 프로덕션 함정을 다룹니다. 기초를 복습하며 준비하세요
- 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 2/16편입니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
프레임워크 선택 LangGraph vs CrewAI vs AutoGen 완벽 가이드
AI 에이전트 개발을 위한 세 가지 핵심 프레임워크를 비교 분석합니다. 각 프레임워크의 특징, 장단점, 실무 선택 기준을 초급 개발자도 이해할 수 있도록 설명합니다.
Day 6 학습 루프 이해하기
LLM이 실제로 어떻게 학습하는지 학습 루프의 핵심 원리를 단계별로 살펴봅니다. Forward Pass, Loss 계산, Backward Pass, 파라미터 업데이트까지 한 사이클의 전 과정을 이해합니다.
Day 5 Baseline 모델 만들기
복잡한 모델에 앞서 가장 단순한 Baseline 모델을 직접 만들어봅니다. 아무런 기교 없이 순수하게 다음 토큰을 예측하는 모델을 구현하면서, 언어모델의 가장 기본 구조를 이해합니다.
Day 4 학습용 샘플 데이터 만들기
LLM을 학습시키기 위한 샘플 데이터를 직접 만들어봅니다. 작은 텍스트 말뭉치를 준비하고, 토크나이저로 변환한 뒤 PyTorch 텐서로 만드는 전체 과정을 단계별로 배웁니다.
Day 2 PyTorch 기본기 정리
LLM을 직접 만들기 위해 꼭 알아야 할 PyTorch의 핵심 개념을 정리합니다. 텐서, 자동 미분, 옵티마이저까지 모델 학습의 기초를 다집니다.