Ver3.0 ๐ Python์ผ๋ก API ์๋ต ์๋ ๊ฒ์ฆ๊ธฐ ๊ฐ๋ฐ

๐ Python์ผ๋ก API ์๋ต ์๋ ๊ฒ์ฆ๊ธฐ ๊ฐ๋ฐ
์น๊ตฌ์ฒ๋ผ ์ฝ๊ฒ ๋ฐฐ์ฐ๋ API ํ ์คํธ ์๋ํ์ ๋ชจ๋ ๊ฒ
๐ฏ ์ API ์๋ต ๊ฒ์ฆ๊ธฐ๊ฐ ํ์ํ ๊น?
์ผ, ์์งํ ๋งํด์ API ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ์ง์ง ๋ต๋ตํ ๋ ๋ง์ง ์์? ๐ค
ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๊ฐ "์ด๊ฑฐ ์ ์ ๋ผ์?"๋ผ๊ณ ๋ฌผ์ด๋ณด๋ฉด, ๋ฐฑ์๋ ๊ฐ๋ฐ์๋ "์ ๋ก์ปฌ์์๋ ์ ๋๋๋ฐ์?"๋ผ๊ณ ๋๋ตํ๋ ๊ทธ ๊ณ ์ ์ ์ธ ์ํฉ ๋ง์ด์ผ. ๊ทธ๋ฆฌ๊ณ ํ์ฐธ์ ๋๋ฒ๊น
ํ๋ค ๋ณด๋ฉด ์๊ณ ๋ณด๋ API ์๋ต ํ์์ด ์ด์ง ๋ฐ๋์ด ์๊ฑฐ๋, ํน์ ์ผ์ด์ค์์๋ง null ๊ฐ์ด ๋์ค๊ฑฐ๋, ์๋๋ฉด ์ํ ์ฝ๋๊ฐ ์์๊ณผ ๋ค๋ฅด๊ฒ ๋์ค๋ ๊ฑฐ์ง.
ํนํ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ
์ฒ๋ฅผ ์ฌ์ฉํ๋ ์์ฆ ๊ฐ์ ์๋์๋ API๊ฐ ์์ญ, ์๋ฐฑ ๊ฐ์ฉ ์กด์ฌํ์์? ๐
๊ฐ๊ฐ์ API๋ฅผ ์ผ์ผ์ด ์๋์ผ๋ก ํ
์คํธํ๋ค๋ ๊ฑด ์ ๋ง ๋นํจ์จ์ ์ด์ผ. ๊ฒ๋ค๊ฐ ์ฌ๋์ด ํ๋ ํ
์คํธ๋ ์ค์๋ ๋ง๊ณ , ๋ฐ๋ณต ์์
์ ์ง์ณ์ ์ค์ํ ๋ฒ๊ทธ๋ฅผ ๋์น ์๋ ์์ด.
๋ง์ฝ ์๋ ๊ฒ์ฆ๊ธฐ๊ฐ ์์๋ค๋ฉด? ๋ฐฐํฌ ์ ์ ๋ฏธ๋ฆฌ ์ก์๋ผ ์ ์์์ ๊ฑฐ์ผ.
API ์๋ต ์๋ ๊ฒ์ฆ๊ธฐ๋ ์ด๋ฐ ๋ฌธ์ ๋ค์ ํด๊ฒฐํด์ค:
โ
์ผ๊ด์ฑ ๋ณด์ฅ - ๋ชจ๋ API๊ฐ ์ ํด์ง ๊ท์น๋๋ก ์๋ตํ๋์ง ์๋์ผ๋ก ํ์ธ
โ
์๊ฐ ์ ์ฝ - ์๋ ํ
์คํธ์ ๋ค์ด๊ฐ๋ ์๊ฐ์ 90% ์ด์ ์ค์ฌ์ค
โ
๋ฒ๊ทธ ์กฐ๊ธฐ ๋ฐ๊ฒฌ - ํ๋ก๋์
๋ฐฐํฌ ์ ์ ๋ฌธ์ ๋ฅผ ๋ฏธ๋ฆฌ ์ก์๋
โ
๋ฌธ์ํ ํจ๊ณผ - ๊ฒ์ฆ ์คํฌ๋ฆฝํธ ์์ฒด๊ฐ API ๋ช
์ธ์ ์ญํ ์ ํจ
โ
ํ๊ท ํ
์คํธ - ์ฝ๋ ๋ณ๊ฒฝ ํ์๋ ๊ธฐ์กด ๊ธฐ๋ฅ์ด ์ ์๋ํ๋์ง ํ์ธ
๐ ๏ธ ๊ธฐ๋ณธ ๊ฐ๋ ๊ณผ ์ค๋น๋ฌผ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํด๋ณผ๊น? ๋จผ์ ์ฐ๋ฆฌ๊ฐ ๋ญ ๋ง๋ค ๊ฑด์ง ๋ช ํํ๊ฒ ์ ๋ฆฌํด๋ณด์. ๐ฏ
API ์๋ต ๊ฒ์ฆ์ ๋ง ๊ทธ๋๋ก API๊ฐ ๋ฐํํ๋ ๋ฐ์ดํฐ๊ฐ ์ฐ๋ฆฌ๊ฐ ๊ธฐ๋ํ๋ ํํ์ ๋ด์ฉ์ธ์ง ํ์ธํ๋ ๊ณผ์ ์ด์ผ.
์๋ฅผ ๋ค์ด๋ณผ๊ฒ. ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ API๊ฐ ์๋ค๊ณ ์น์:
GET /api/users/123
์ด API๋ ๋ค์๊ณผ ๊ฐ์ ์๋ต์ ์ค์ผ ํด:
{
"id": 123,
"name": "ํ๊ธธ๋",
"email": "hong@example.com",
"age": 25,
"created_at": "2024-01-15T10:30:00Z"
}
๊ฒ์ฆ๊ธฐ๋ ์ด๋ฐ ๊ฒ๋ค์ ์ฒดํฌํด:
1๏ธโฃ HTTP ์ํ ์ฝ๋ - 200 OK๊ฐ ๋ง๋?
2๏ธโฃ ์๋ต ํ์ - JSON ํํ๊ฐ ๋ง๋?
3๏ธโฃ ํ์ ํ๋ ์กด์ฌ - id, name, email ๊ฐ์ ํ๋๊ฐ ์๋?
4๏ธโฃ ๋ฐ์ดํฐ ํ์
- id๋ ์ซ์, name์ ๋ฌธ์์ด์ด ๋ง๋?
5๏ธโฃ ๊ฐ์ ๋ฒ์ - age๊ฐ 0๋ณด๋ค ํฌ๊ณ 150๋ณด๋ค ์๋?
6๏ธโฃ ํ์ ๊ท์น - email์ด ์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ธ๊ฐ?
7๏ธโฃ ๋น์ฆ๋์ค ๋ก์ง - created_at์ด ๋ฏธ๋ ๋ ์ง๊ฐ ์๋๊ฐ?
๐ ํ์ํ ์ค๋น๋ฌผ
1. Python 3.7 ์ด์
์ต์ ๋ฒ์ ์ ์ฌ์ฉํ๋ ๊ฒ ์ข์. ํ์
ํํ
๊ฐ์ ๊ธฐ๋ฅ์ ํ์ฉํ ์ ์๊ฑฐ๋ .
2. ํ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
requests - HTTP ์์ฒญ์ ๋ณด๋ด๊ธฐ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
jsonschema - JSON ์คํค๋ง ๊ฒ์ฆ
pytest - ํ
์คํธ ํ๋ ์์ํฌ (์ ํ์ฌํญ์ด์ง๋ง ๊ฐ๋ ฅ ์ถ์ฒ)
pydantic - ๋ฐ์ดํฐ ๊ฒ์ฆ ๋ฐ ์ค์ ๊ด๋ฆฌ
3. ๊ฐ๋ฐ ํ๊ฒฝ
VS Code, PyCharm ๊ฐ์ IDE๊ฐ ์์ผ๋ฉด ํธํด. ๋๋ ๊ฐ์ธ์ ์ผ๋ก VS Code๋ฅผ ์ ํธํ๋๋ฐ, ๊ฐ๋ณ๊ณ ํ์ฅ ๊ธฐ๋ฅ์ด ๋ง๊ฑฐ๋ .
ํฐ๋ฏธ๋์ ์ด๊ณ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์คํํด:
pip install requests jsonschema pytest pydantic python-dotenv
๊ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ ํ์ํ์ง ๊ฐ๋จํ ์ค๋ช
ํ ๊ฒ:
requests ๐
HTTP ์์ฒญ์ ๋ณด๋ด๋ ๊ฐ์ฅ ์ธ๊ธฐ ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ. urllib๋ณด๋ค ํจ์ฌ ์ฌ์ฉํ๊ธฐ ์ฝ๊ณ ์ง๊ด์ ์ด์ง.
jsonschema ๐
JSON ๋ฐ์ดํฐ์ ๊ตฌ์กฐ๋ฅผ ์ ์ํ๊ณ ๊ฒ์ฆํ ์ ์์ด. ๋ณต์กํ ์ค์ฒฉ ๊ตฌ์กฐ๋ ์ฝ๊ฒ ๊ฒ์ฆ ๊ฐ๋ฅํด.
pytest ๐งช
ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํ๊ณ ์คํํ๋ ํ๋ ์์ํฌ. unittest๋ณด๋ค ๊ฐ๊ฒฐํ๊ณ ๊ฐ๋ ฅํด.
pydantic ๐
ํ์
ํํ
์ ํ์ฉํ ๋ฐ์ดํฐ ๊ฒ์ฆ. ๋ฐํ์์ ํ์
์ ์ฒดํฌํ๊ณ ์๋์ผ๋ก ๋ณํ๋ ํด์ค.
python-dotenv ๐
ํ๊ฒฝ ๋ณ์๋ฅผ .env ํ์ผ์์ ์ฝ์ด์ฌ ์ ์๊ฒ ํด์ค. API ํค ๊ฐ์ ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ๊ด๋ฆฌํ ๋ ์ ์ฉํด.
python -m venv venvsource venv/bin/activate (Windows: venv\Scripts\activate)
๐ ์ฒซ ๋ฒ์งธ ๊ฒ์ฆ๊ธฐ ๋ง๋ค๊ธฐ
์ด๋ก ์ ์ด์ฏค ํ๊ณ , ์ด์ ์ง์ง ์ฝ๋๋ฅผ ์์ฑํด๋ณด์! ๊ฐ์ฅ ๊ฐ๋จํ ํํ๋ถํฐ ์์ํ ๊ฒ. ๐
๋จผ์ ํ๋ก์ ํธ ํด๋ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค์ด๋ณด์:
api-validator/
โโโ validators/
โ โโโ __init__.py
โ โโโ base_validator.py
โ โโโ user_validator.py
โโโ tests/
โ โโโ __init__.py
โ โโโ test_user_api.py
โโโ config/
โ โโโ __init__.py
โ โโโ settings.py
โโโ .env
โโโ requirements.txt
โโโ main.py
์ด๋ ๊ฒ ๊ตฌ์กฐํํ๋ฉด ๋์ค์ ํ์ฅํ๊ธฐ๋ ์ฝ๊ณ ๊ด๋ฆฌํ๊ธฐ๋ ํธํด.
์, ์ด์ base_validator.py์ ๊ธฐ๋ณธ์ด ๋๋ ๊ฒ์ฆ๊ธฐ ํด๋์ค๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ:
import requests
from typing import Dict, Any, Optional, List
from datetime import datetime
import json
class BaseAPIValidator:
"""API ์๋ต์ ๊ฒ์ฆํ๋ ๊ธฐ๋ณธ ํด๋์ค"""
def __init__(self, base_url: str, timeout: int = 30):
"""
Args:
base_url: API์ ๊ธฐ๋ณธ URL
timeout: ์์ฒญ ํ์์์ (์ด)
"""
self.base_url = base_url.rstrip('/')
self.timeout = timeout
self.session = requests.Session()
self.last_response = None
def get(self, endpoint: str, params: Optional[Dict] = None,
headers: Optional[Dict] = None) -> requests.Response:
"""GET ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ต์ ๋ฐํ"""
url = f"{self.base_url}/{endpoint.lstrip('/')}"
try:
response = self.session.get(
url,
params=params,
headers=headers,
timeout=self.timeout
)
self.last_response = response
return response
except requests.exceptions.Timeout:
raise Exception(f"์์ฒญ ํ์์์: {url}")
except requests.exceptions.RequestException as e:
raise Exception(f"์์ฒญ ์คํจ: {str(e)}")
def post(self, endpoint: str, data: Optional[Dict] = None,
json_data: Optional[Dict] = None,
headers: Optional[Dict] = None) -> requests.Response:
"""POST ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ต์ ๋ฐํ"""
url = f"{self.base_url}/{endpoint.lstrip('/')}"
try:
response = self.session.post(
url,
data=data,
json=json_data,
headers=headers,
timeout=self.timeout
)
self.last_response = response
return response
except requests.exceptions.RequestException as e:
raise Exception(f"์์ฒญ ์คํจ: {str(e)}")
def validate_status_code(self, response: requests.Response,
expected: int) -> bool:
"""์ํ ์ฝ๋ ๊ฒ์ฆ"""
actual = response.status_code
if actual != expected:
raise AssertionError(
f"์ํ ์ฝ๋ ๋ถ์ผ์น: ์์={expected}, ์ค์ ={actual}"
)
return True
def validate_json_response(self, response: requests.Response) -> Dict:
"""์๋ต์ด ์ ํจํ JSON์ธ์ง ๊ฒ์ฆํ๊ณ ํ์ฑ"""
try:
return response.json()
except json.JSONDecodeError as e:
raise AssertionError(f"์ ํจํ์ง ์์ JSON ์๋ต: {str(e)}")
def validate_required_fields(self, data: Dict,
required_fields: List[str]) -> bool:
"""ํ์ ํ๋ ์กด์ฌ ์ฌ๋ถ ๊ฒ์ฆ"""
missing_fields = []
for field in required_fields:
if field not in data:
missing_fields.append(field)
if missing_fields:
raise AssertionError(
f"ํ์ ํ๋ ๋๋ฝ: {', '.join(missing_fields)}"
)
return True
def validate_field_type(self, data: Dict, field: str,
expected_type: type) -> bool:
"""ํ๋์ ๋ฐ์ดํฐ ํ์
๊ฒ์ฆ"""
if field not in data:
raise AssertionError(f"ํ๋ '{field}'๊ฐ ์กด์ฌํ์ง ์์")
actual_value = data[field]
if not isinstance(actual_value, expected_type):
raise AssertionError(
f"ํ๋ '{field}'์ ํ์
๋ถ์ผ์น: "
f"์์={expected_type.__name__}, "
f"์ค์ ={type(actual_value).__name__}"
)
return True
def validate_response_time(self, response: requests.Response,
max_seconds: float) -> bool:
"""์๋ต ์๊ฐ ๊ฒ์ฆ"""
elapsed = response.elapsed.total_seconds()
if elapsed > max_seconds:
raise AssertionError(
f"์๋ต ์๊ฐ ์ด๊ณผ: {elapsed:.2f}์ด > {max_seconds}์ด"
)
return True
def print_validation_result(self, test_name: str,
passed: bool,
message: str = ""):
"""๊ฒ์ฆ ๊ฒฐ๊ณผ ์ถ๋ ฅ"""
status = "โ
PASS" if passed else "โ FAIL"
print(f"{status} | {test_name}")
if message:
print(f" โโ {message}")
์, ์ฝ๋๊ฐ ์ข ๊ธธ์ง? ๐
ํ์ง๋ง ๊ฑฑ์ ๋ง. ํ๋ํ๋ ๋ฏ์ด๋ณด๋ฉด ๋ณ๊ฑฐ ์์ด.
์ด ํด๋์ค๋ API ๊ฒ์ฆ์ ํ์ํ ๊ธฐ๋ณธ ๊ธฐ๋ฅ๋ค์ ์ ๊ณตํด:
HTTP ์์ฒญ ๋ณด๋ด๊ธฐ - GET, POST ๋ฉ์๋
์ํ ์ฝ๋ ๊ฒ์ฆ - 200, 404 ๊ฐ์ ์ฝ๋ ํ์ธ
JSON ํ์ฑ - ์๋ต์ JSON์ผ๋ก ๋ณํ
ํ๋ ๊ฒ์ฆ - ํ์ ํ๋์ ํ์
์ฒดํฌ
์ฑ๋ฅ ๊ฒ์ฆ - ์๋ต ์๊ฐ ์ธก์
์ด์ user_validator.py์ ์ฌ์ฉ์ API๋ฅผ ๊ฒ์ฆํ๋ ๊ตฌ์ฒด์ ์ธ ํด๋์ค๋ฅผ ๋ง๋ค์ด๋ณด์:
from validators.base_validator import BaseAPIValidator
from typing import Dict, Any
import re
class UserAPIValidator(BaseAPIValidator):
"""์ฌ์ฉ์ API ์ ์ฉ ๊ฒ์ฆ๊ธฐ"""
def validate_user_response(self, user_id: int) -> Dict[str, Any]:
"""์ฌ์ฉ์ ์ ๋ณด ์กฐํ API ๊ฒ์ฆ"""
print(f"\n๐ ์ฌ์ฉ์ ID {user_id} ์กฐํ ๊ฒ์ฆ ์์...")
# 1. API ํธ์ถ
response = self.get(f"/api/users/{user_id}")
# 2. ์ํ ์ฝ๋ ๊ฒ์ฆ
try:
self.validate_status_code(response, 200)
self.print_validation_result("์ํ ์ฝ๋", True, "200 OK")
except AssertionError as e:
self.print_validation_result("์ํ ์ฝ๋", False, str(e))
return {}
# 3. JSON ์๋ต ๊ฒ์ฆ
try:
data = self.validate_json_response(response)
self.print_validation_result("JSON ํ์", True)
except AssertionError as e:
self.print_validation_result("JSON ํ์", False, str(e))
return {}
# 4. ํ์ ํ๋ ๊ฒ์ฆ
required_fields = ['id', 'name', 'email', 'created_at']
try:
self.validate_required_fields(data, required_fields)
self.print_validation_result(
"ํ์ ํ๋", True,
f"๋ชจ๋ ํ๋ ์กด์ฌ: {', '.join(required_fields)}"
)
except AssertionError as e:
self.print_validation_result("ํ์ ํ๋", False, str(e))
# 5. ๋ฐ์ดํฐ ํ์
๊ฒ์ฆ
type_checks = [
('id', int),
('name', str),
('email', str),
('created_at', str)
]
for field, expected_type in type_checks:
try:
self.validate_field_type(data, field, expected_type)
self.print_validation_result(
f"ํ์
๊ฒ์ฆ: {field}", True,
f"{expected_type.__name__}"
)
except AssertionError as e:
self.print_validation_result(
f"ํ์
๊ฒ์ฆ: {field}", False, str(e)
)
# 6. ์ด๋ฉ์ผ ํ์ ๊ฒ์ฆ
if 'email' in data:
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
is_valid_email = re.match(email_pattern, data['email']) is not None
self.print_validation_result(
"์ด๋ฉ์ผ ํ์", is_valid_email,
data['email'] if is_valid_email else "์๋ชป๋ ํ์"
)
# 7. ์๋ต ์๊ฐ ๊ฒ์ฆ
try:
self.validate_response_time(response, 2.0)
elapsed = response.elapsed.total_seconds()
self.print_validation_result(
"์๋ต ์๊ฐ", True, f"{elapsed:.3f}์ด"
)
except AssertionError as e:
self.print_validation_result("์๋ต ์๊ฐ", False, str(e))
return data
def validate_user_list(self, page: int = 1,
per_page: int = 10) -> Dict[str, Any]:
"""์ฌ์ฉ์ ๋ชฉ๋ก ์กฐํ API ๊ฒ์ฆ"""
print(f"\n๐ ์ฌ์ฉ์ ๋ชฉ๋ก ์กฐํ ๊ฒ์ฆ ์์...")
params = {'page': page, 'per_page': per_page}
response = self.get("/api/users", params=params)
# ์ํ ์ฝ๋ ๊ฒ์ฆ
try:
self.validate_status_code(response, 200)
self.print_validation_result("์ํ ์ฝ๋", True)
except AssertionError as e:
self.print_validation_result("์ํ ์ฝ๋", False, str(e))
return {}
# JSON ์๋ต ๊ฒ์ฆ
data = self.validate_json_response(response)
# ํ์ด์ง๋ค์ด์
ํ๋ ๊ฒ์ฆ
pagination_fields = ['data', 'page', 'per_page', 'total']
try:
self.validate_required_fields(data, pagination_fields)
self.print_validation_result("ํ์ด์ง๋ค์ด์
ํ๋", True)
except AssertionError as e:
self.print_validation_result("ํ์ด์ง๋ค์ด์
ํ๋", False, str(e))
# ๋ฐ์ดํฐ ๋ฐฐ์ด ๊ฒ์ฆ
if 'data' in data and isinstance(data['data'], list):
self.print_validation_result(
"๋ฐ์ดํฐ ํ์
", True,
f"๋ฐฐ์ด ๊ธธ์ด: {len(data['data'])}"
)
# ๊ฐ ์ฌ์ฉ์ ๊ฐ์ฒด ๊ฒ์ฆ
if len(data['data']) > 0:
first_user = data['data'][0]
required_user_fields = ['id', 'name', 'email']
try:
self.validate_required_fields(
first_user, required_user_fields
)
self.print_validation_result(
"์ฌ์ฉ์ ๊ฐ์ฒด ๊ตฌ์กฐ", True
)
except AssertionError as e:
self.print_validation_result(
"์ฌ์ฉ์ ๊ฐ์ฒด ๊ตฌ์กฐ", False, str(e)
)
return data
def validate_user_creation(self, user_data: Dict[str, Any]) -> Dict:
"""์ฌ์ฉ์ ์์ฑ API ๊ฒ์ฆ"""
print(f"\nโ ์ฌ์ฉ์ ์์ฑ ๊ฒ์ฆ ์์...")
response = self.post("/api/users", json_data=user_data)
# ์ํ ์ฝ๋ ๊ฒ์ฆ (์์ฑ ์ฑ๊ณต ์ 201)
try:
self.validate_status_code(response, 201)
self.print_validation_result("์ํ ์ฝ๋", True, "201 Created")
except AssertionError as e:
self.print_validation_result("์ํ ์ฝ๋", False, str(e))
return {}
# ์๋ต ๋ฐ์ดํฐ ๊ฒ์ฆ
data = self.validate_json_response(response)
# ์์ฑ๋ ์ฌ์ฉ์์ ID๊ฐ ํ ๋น๋์๋์ง ํ์ธ
if 'id' in data:
self.print_validation_result(
"ID ํ ๋น", True, f"์์ฑ๋ ID: {data['id']}"
)
else:
self.print_validation_result("ID ํ ๋น", False, "ID ์์")
# ์
๋ ฅํ ๋ฐ์ดํฐ๊ฐ ์๋ต์ ํฌํจ๋์ด ์๋์ง ํ์ธ
for key, value in user_data.items():
if key in data and data[key] == value:
self.print_validation_result(
f"ํ๋ ์ผ์น: {key}", True, str(value)
)
else:
self.print_validation_result(
f"ํ๋ ์ผ์น: {key}", False,
f"์์={value}, ์ค์ ={data.get(key)}"
)
return data
์ด์ ์ค์ ๋ก ์ฌ์ฉํ ์ ์๋ ๊ฒ์ฆ๊ธฐ๊ฐ ์์ฑ๋์ด! ๐
์ด ๊ฒ์ฆ๊ธฐ๋ ์ธ ๊ฐ์ง ์ฃผ์ ๊ธฐ๋ฅ์ ์ ๊ณตํด:
1๏ธโฃ ๋จ์ผ ์ฌ์ฉ์ ์กฐํ ๊ฒ์ฆ - ํน์ ์ฌ์ฉ์์ ์ ๋ณด๊ฐ ์ฌ๋ฐ๋ฅธ์ง ํ์ธ
2๏ธโฃ ์ฌ์ฉ์ ๋ชฉ๋ก ์กฐํ ๊ฒ์ฆ - ํ์ด์ง๋ค์ด์
์ด ์ ๋๋ก ์๋ํ๋์ง ํ์ธ
3๏ธโฃ ์ฌ์ฉ์ ์์ฑ ๊ฒ์ฆ - POST ์์ฒญ์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ฒ๋ฆฌ๋๋์ง ํ์ธ
๐จ ๊ณ ๊ธ ๊ฒ์ฆ ๊ธฐ๋ฒ
๊ธฐ๋ณธ์ ์ธ ๊ฒ์ฆ๊ธฐ๋ฅผ ๋ง๋ค์์ผ๋, ์ด์ ์ข ๋ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค์ ์ถ๊ฐํด๋ณด์! ๐ช
JSON Schema๋ JSON ๋ฐ์ดํฐ์ ๊ตฌ์กฐ๋ฅผ ์ ์ํ๋ ๊ฐ๋ ฅํ ๋ฐฉ๋ฒ์ด์ผ. ๋ณต์กํ ์ค์ฒฉ ๊ตฌ์กฐ๋ ์ฝ๊ฒ ๊ฒ์ฆํ ์ ์์ด.
from jsonschema import validate, ValidationError
import jsonschema
class SchemaValidator(BaseAPIValidator):
"""JSON Schema ๊ธฐ๋ฐ ๊ฒ์ฆ๊ธฐ"""
def __init__(self, base_url: str):
super().__init__(base_url)
# ์ฌ์ฉ์ ์คํค๋ง ์ ์
self.user_schema = {
"type": "object",
"properties": {
"id": {"type": "integer", "minimum": 1},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"email": {
"type": "string",
"format": "email"
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150
},
"created_at": {
"type": "string",
"format": "date-time"
},
"profile": {
"type": "object",
"properties": {
"bio": {"type": "string"},
"avatar_url": {"type": "string", "format": "uri"}
}
},
"tags": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": True
}
},
"required": ["id", "name", "email", "created_at"],
"additionalProperties": False
}
def validate_with_schema(self, data: dict, schema: dict) -> bool:
"""์คํค๋ง๋ฅผ ์ฌ์ฉํ ๋ฐ์ดํฐ ๊ฒ์ฆ"""
try:
validate(instance=data, schema=schema)
self.print_validation_result(
"์คํค๋ง ๊ฒ์ฆ", True, "๋ชจ๋ ๊ท์น ํต๊ณผ"
)
return True
except ValidationError as e:
self.print_validation_result(
"์คํค๋ง ๊ฒ์ฆ", False,
f"๊ฒฝ๋ก: {' -> '.join(str(p) for p in e.path)}, "
f"๋ฉ์์ง: {e.message}"
)
return False
def validate_user_with_schema(self, user_id: int) -> bool:
"""์คํค๋ง๋ฅผ ์ฌ์ฉํ ์ฌ์ฉ์ ๊ฒ์ฆ"""
response = self.get(f"/api/users/{user_id}")
if response.status_code != 200:
return False
data = response.json()
return self.validate_with_schema(data, self.user_schema)
JSON Schema์ ์ฅ์ ์ ๋ญ๋๋ฉด:
โจ ์ ์ธ์ - ์ฝ๋๊ฐ ์๋๋ผ ๋ฐ์ดํฐ๋ก ๊ท์น์ ์ ์
โจ ์ฌ์ฌ์ฉ ๊ฐ๋ฅ - ํ ๋ฒ ์ ์ํ๋ฉด ์ฌ๋ฌ ๊ณณ์์ ์ฌ์ฉ
โจ ํ์คํ - ์
๊ณ ํ์ค์ด๋ผ ๋ค๋ฅธ ๋๊ตฌ์๋ ํธํ
โจ ์์ธํ ์๋ฌ - ์ด๋์ ๋ฌธ์ ๊ฐ ์๊ฒผ๋์ง ์ ํํ ์๋ ค์ค
Pydantic์ Python์ ํ์ ํํ ์ ํ์ฉํด์ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ฆํ๊ณ ํ์ฑํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ. ๊ฐ์ธ์ ์ผ๋ก ์ ๋ง ์ข์ํ๋ ๋๊ตฌ ์ค ํ๋์ผ! ๐
from pydantic import BaseModel, EmailStr, HttpUrl, validator
from typing import Optional, List
from datetime import datetime
class UserProfile(BaseModel):
"""์ฌ์ฉ์ ํ๋กํ ๋ชจ๋ธ"""
bio: Optional[str] = None
avatar_url: Optional[HttpUrl] = None
class User(BaseModel):
"""์ฌ์ฉ์ ๋ชจ๋ธ"""
id: int
name: str
email: EmailStr
age: Optional[int] = None
created_at: datetime
profile: Optional[UserProfile] = None
tags: List[str] = []
@validator('age')
def validate_age(cls, v):
"""๋์ด ๊ฒ์ฆ"""
if v is not None and (v < 0 or v > 150):
raise ValueError('๋์ด๋ 0-150 ์ฌ์ด์ฌ์ผ ํฉ๋๋ค')
return v
@validator('name')
def validate_name(cls, v):
"""์ด๋ฆ ๊ฒ์ฆ"""
if len(v.strip()) == 0:
raise ValueError('์ด๋ฆ์ ๋น์ด์์ ์ ์์ต๋๋ค')
return v.strip()
@validator('tags')
def validate_tags(cls, v):
"""ํ๊ทธ ๊ฒ์ฆ"""
# ์ค๋ณต ์ ๊ฑฐ
return list(set(v))
class PydanticValidator(BaseAPIValidator):
"""Pydantic ๊ธฐ๋ฐ ๊ฒ์ฆ๊ธฐ"""
def validate_user_response(self, user_id: int) -> Optional[User]:
"""Pydantic ๋ชจ๋ธ์ ์ฌ์ฉํ ์ฌ์ฉ์ ๊ฒ์ฆ"""
print(f"\n๐ Pydantic ๊ฒ์ฆ ์์: ์ฌ์ฉ์ ID {user_id}")
response = self.get(f"/api/users/{user_id}")
if response.status_code != 200:
self.print_validation_result(
"API ํธ์ถ", False, f"์ํ ์ฝ๋: {response.status_code}"
)
return None
try:
# JSON์ Pydantic ๋ชจ๋ธ๋ก ํ์ฑ
# ์ด ๊ณผ์ ์์ ์๋์ผ๋ก ๊ฒ์ฆ์ด ์ํ๋จ
user = User(**response.json())
self.print_validation_result(
"Pydantic ๊ฒ์ฆ", True,
f"์ฌ์ฉ์: {user.name} ({user.email})"
)
# ์ถ๊ฐ ๋น์ฆ๋์ค ๋ก์ง ๊ฒ์ฆ
if user.created_at > datetime.now():
self.print_validation_result(
"์์ฑ ๋ ์ง", False, "๋ฏธ๋ ๋ ์ง๋ ๋ถ๊ฐ๋ฅ"
)
else:
self.print_validation_result("์์ฑ ๋ ์ง", True)
return user
except Exception as e:
self.print_validation_result(
"Pydantic ๊ฒ์ฆ", False, str(e)
)
return None
Pydantic์ ๋ฉ์ง ์ ๋ค:
๐ฏ ์๋ ํ์
๋ณํ - ๋ฌธ์์ด "123"์ ์๋์ผ๋ก ์ซ์ 123์ผ๋ก ๋ณํ
๐ฏ ์ปค์คํ
๊ฒ์ฆ - @validator ๋ฐ์ฝ๋ ์ดํฐ๋ก ๋ณต์กํ ๊ท์น ์ถ๊ฐ
๐ฏ IDE ์ง์ - ํ์
ํํ
๋๋ถ์ ์๋์์ฑ์ด ์๋ฒฝํ๊ฒ ์๋
๐ฏ ์ง๋ ฌํ - ๋ชจ๋ธ์ ๋ค์ JSON์ผ๋ก ์ฝ๊ฒ ๋ณํ ๊ฐ๋ฅ
API์ ์ฑ๋ฅ๋ ์ค์ํ ๊ฒ์ฆ ์์์ผ. ๋๋ฆฐ API๋ ์ฌ์ฉ์ ๊ฒฝํ์ ๋ง์น๊ฑฐ๋ . ๐
import time
from typing import List, Dict
import statistics
class PerformanceValidator(BaseAPIValidator):
"""์ฑ๋ฅ ๊ฒ์ฆ๊ธฐ"""
def __init__(self, base_url: str):
super().__init__(base_url)
self.response_times: List[float] = []
def measure_response_time(self, endpoint: str,
iterations: int = 10) -> Dict[str, float]:
"""์๋ต ์๊ฐ ์ธก์ """
print(f"\nโฑ๏ธ ์ฑ๋ฅ ํ
์คํธ ์์: {endpoint}")
print(f"๋ฐ๋ณต ํ์: {iterations}ํ")
times = []
for i in range(iterations):
start = time.time()
response = self.get(endpoint)
elapsed = time.time() - start
times.append(elapsed)
print(f" {i+1}ํ: {elapsed:.3f}์ด")
# ํต๊ณ ๊ณ์ฐ
stats = {
'min': min(times),
'max': max(times),
'avg': statistics.mean(times),
'median': statistics.median(times),
'stdev': statistics.stdev(times) if len(times) > 1 else 0
}
print(f"\n๐ ํต๊ณ:")
print(f" ์ต์: {stats['min']:.3f}์ด")
print(f" ์ต๋: {stats['max']:.3f}์ด")
print(f" ํ๊ท : {stats['avg']:.3f}์ด")
print(f" ์ค์๊ฐ: {stats['median']:.3f}์ด")
print(f" ํ์คํธ์ฐจ: {stats['stdev']:.3f}์ด")
# ์ฑ๋ฅ ๊ธฐ์ค ๊ฒ์ฆ
if stats['avg'] > 1.0:
self.print_validation_result(
"ํ๊ท ์๋ต ์๊ฐ", False,
f"{stats['avg']:.3f}์ด (๊ธฐ์ค: 1.0์ด)"
)
else:
self.print_validation_result(
"ํ๊ท ์๋ต ์๊ฐ", True,
f"{stats['avg']:.3f}์ด"
)
return stats
def load_test(self, endpoint: str,
concurrent_requests: int = 10) -> Dict:
"""๋ถํ ํ
์คํธ"""
import concurrent.futures
print(f"\n๐ฅ ๋ถํ ํ
์คํธ ์์: {endpoint}")
print(f"๋์ ์์ฒญ ์: {concurrent_requests}")
def make_request():
start = time.time()
try:
response = self.get(endpoint)
elapsed = time.time() - start
return {
'success': response.status_code == 200,
'time': elapsed,
'status': response.status_code
}
except Exception as e:
return {
'success': False,
'time': 0,
'error': str(e)
}
# ๋์ ์์ฒญ ์คํ
with concurrent.futures.ThreadPoolExecutor(
max_workers=concurrent_requests
) as executor:
futures = [
executor.submit(make_request)
for _ in range(concurrent_requests)
]
results = [
future.result()
for future in concurrent.futures.as_completed(futures)
]
# ๊ฒฐ๊ณผ ๋ถ์
success_count = sum(1 for r in results if r['success'])
success_rate = (success_count / len(results)) * 100
successful_times = [r['time'] for r in results if r['success']]
print(f"\n๐ ๊ฒฐ๊ณผ:")
print(f" ์ฑ๊ณต๋ฅ : {success_rate:.1f}% ({success_count}/{len(results)})")
if successful_times:
print(f" ํ๊ท ์๋ต ์๊ฐ: {statistics.mean(successful_times):.3f}์ด")
print(f" ์ต๋ ์๋ต ์๊ฐ: {max(successful_times):.3f}์ด")
return {
'total': len(results),
'success': success_count,
'success_rate': success_rate,
'results': results
}
์ฑ๋ฅ ํ
์คํธ๋ ํนํ ์ค์ํด. ์๋ํ๋ฉด:
โก ๋ณ๋ชฉ ์ง์ ๋ฐ๊ฒฌ - ์ด๋ API๊ฐ ๋๋ฆฐ์ง ํ์
โก ํ์ฅ์ฑ ๊ฒ์ฆ - ํธ๋ํฝ์ด ์ฆ๊ฐํด๋ ๊ฒฌ๋ ์ ์๋์ง ํ์ธ
โก SLA ์ค์ - ์๋น์ค ์์ค ํ์ฝ์ ์งํค๊ณ ์๋์ง ์ฒดํฌ
โก ์ต์ ํ ํจ๊ณผ ์ธก์ - ๊ฐ์ ์์
์ ํจ๊ณผ๋ฅผ ์ ๋์ ์ผ๋ก ํ์ธ
๐งช pytest์ ํตํฉํ๊ธฐ
์ด์ ์ฐ๋ฆฌ๊ฐ ๋ง๋ ๊ฒ์ฆ๊ธฐ๋ฅผ pytest์ ํตํฉํด์ ์๋ํ๋ ํ ์คํธ ์ค์ํธ๋ฅผ ๋ง๋ค์ด๋ณด์! ๐
๋จผ์ tests/conftest.py ํ์ผ์ ๋ง๋ค์ด์ ๊ณตํต ์ค์ ์ ์ ์ํด:
import pytest
from validators.user_validator import UserAPIValidator
from validators.schema_validator import SchemaValidator
from validators.pydantic_validator import PydanticValidator
import os
from dotenv import load_dotenv
# ํ๊ฒฝ ๋ณ์ ๋ก๋
load_dotenv()
@pytest.fixture(scope="session")
def base_url():
"""ํ
์คํธ์ฉ API ๊ธฐ๋ณธ URL"""
return os.getenv("API_BASE_URL", "https://api.example.com")
@pytest.fixture(scope="session")
def api_key():
"""API ์ธ์ฆ ํค"""
return os.getenv("API_KEY", "")
@pytest.fixture
def user_validator(base_url):
"""์ฌ์ฉ์ API ๊ฒ์ฆ๊ธฐ ์ธ์คํด์ค"""
return UserAPIValidator(base_url)
@pytest.fixture
def schema_validator(base_url):
"""์คํค๋ง ๊ฒ์ฆ๊ธฐ ์ธ์คํด์ค"""
return SchemaValidator(base_url)
@pytest.fixture
def pydantic_validator(base_url):
"""Pydantic ๊ฒ์ฆ๊ธฐ ์ธ์คํด์ค"""
return PydanticValidator(base_url)
@pytest.fixture
def test_user_data():
"""ํ
์คํธ์ฉ ์ฌ์ฉ์ ๋ฐ์ดํฐ"""
return {
"name": "ํ
์คํธ ์ฌ์ฉ์",
"email": "test@example.com",
"age": 25
}
fixture๋ pytest์ ํต์ฌ ๊ธฐ๋ฅ์ด์ผ. ํ ์คํธ์ ํ์ํ ๊ฐ์ฒด๋ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ์ค๋นํด๋๋ ๊ฑฐ์ง. ์ด๋ ๊ฒ ํ๋ฉด ๊ฐ ํ ์คํธ์์ ๋ฐ๋ณต์ ์ผ๋ก ์ค์ ์ฝ๋๋ฅผ ์์ฑํ ํ์๊ฐ ์์ด.
์ด์ tests/test_user_api.py์ ์ค์ ํ ์คํธ๋ฅผ ์์ฑํด๋ณด์:
import pytest
from validators.user_validator import UserAPIValidator
class TestUserAPI:
"""์ฌ์ฉ์ API ํ
์คํธ ํด๋์ค"""
def test_get_user_success(self, user_validator):
"""์ฌ์ฉ์ ์กฐํ ์ฑ๊ณต ํ
์คํธ"""
# Given: ์กด์ฌํ๋ ์ฌ์ฉ์ ID
user_id = 1
# When: ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์กฐํํ๋ฉด
result = user_validator.validate_user_response(user_id)
# Then: ์ฌ๋ฐ๋ฅธ ๋ฐ์ดํฐ๊ฐ ๋ฐํ๋์ด์ผ ํจ
assert result is not None
assert 'id' in result
assert result['id'] == user_id
assert 'name' in result
assert 'email' in result
def test_get_user_not_found(self, user_validator):
"""์กด์ฌํ์ง ์๋ ์ฌ์ฉ์ ์กฐํ ํ
์คํธ"""
# Given: ์กด์ฌํ์ง ์๋ ์ฌ์ฉ์ ID
user_id = 999999
# When: ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์กฐํํ๋ฉด
response = user_validator.get(f"/api/users/{user_id}")
# Then: 404 ์๋ฌ๊ฐ ๋ฐํ๋์ด์ผ ํจ
assert response.status_code == 404
def test_get_user_list(self, user_validator):
"""์ฌ์ฉ์ ๋ชฉ๋ก ์กฐํ ํ
์คํธ"""
# When: ์ฌ์ฉ์ ๋ชฉ๋ก์ ์กฐํํ๋ฉด
result = user_validator.validate_user_list(page=1, per_page=10)
# Then: ํ์ด์ง๋ค์ด์
์ ๋ณด๊ฐ ํฌํจ๋์ด์ผ ํจ
assert 'data' in result
assert 'page' in result
assert 'per_page' in result
assert 'total' in result
assert isinstance(result['data'], list)
def test_create_user(self, user_validator, test_user_data):
"""์ฌ์ฉ์ ์์ฑ ํ
์คํธ"""
# When: ์ ์ฌ์ฉ์๋ฅผ ์์ฑํ๋ฉด
result = user_validator.validate_user_creation(test_user_data)
# Then: ID๊ฐ ํ ๋น๋๊ณ ์
๋ ฅ ๋ฐ์ดํฐ๊ฐ ๋ฐ์๋์ด์ผ ํจ
assert 'id' in result
assert result['name'] == test_user_data['name']
assert result['email'] == test_user_data['email']
def test_user_email_format(self, user_validator):
"""์ด๋ฉ์ผ ํ์ ๊ฒ์ฆ ํ
์คํธ"""
# Given: ์๋ชป๋ ์ด๋ฉ์ผ ํ์
invalid_data = {
"name": "ํ
์คํธ",
"email": "invalid-email", # ์๋ชป๋ ํ์
"age": 25
}
# When: ์ฌ์ฉ์๋ฅผ ์์ฑํ๋ฉด
response = user_validator.post("/api/users", json_data=invalid_data)
# Then: 400 ์๋ฌ๊ฐ ๋ฐํ๋์ด์ผ ํจ
assert response.status_code == 400
@pytest.mark.parametrize("user_id", [1, 2, 3, 4, 5])
def test_multiple_users(self, user_validator, user_id):
"""์ฌ๋ฌ ์ฌ์ฉ์ ์กฐํ ํ
์คํธ"""
# When: ์ฌ๋ฌ ์ฌ์ฉ์๋ฅผ ์กฐํํ๋ฉด
result = user_validator.validate_user_response(user_id)
# Then: ๋ชจ๋ ์ฌ๋ฐ๋ฅธ ํ์์ด์ด์ผ ํจ
assert result is not None
assert 'id' in result
assert 'email' in result
class TestUserAPIPerformance:
"""์ฌ์ฉ์ API ์ฑ๋ฅ ํ
์คํธ"""
def test_response_time(self, user_validator):
"""์๋ต ์๊ฐ ํ
์คํธ"""
# When: ์ฌ์ฉ์๋ฅผ ์กฐํํ๋ฉด
response = user_validator.get("/api/users/1")
# Then: 1์ด ์ด๋ด์ ์๋ตํด์ผ ํจ
assert response.elapsed.total_seconds() < 1.0
@pytest.mark.slow
def test_load_handling(self, user_validator):
"""๋ถํ ์ฒ๋ฆฌ ํ
์คํธ"""
# Given: ๋์ ์์ฒญ 10๊ฐ
from concurrent.futures import ThreadPoolExecutor
def make_request():
return user_validator.get("/api/users/1")
# When: ๋์์ ์์ฒญํ๋ฉด
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(make_request) for _ in range(10)]
results = [f.result() for f in futures]
# Then: ๋ชจ๋ ์ฑ๊ณตํด์ผ ํจ
assert all(r.status_code == 200 for r in results)
class TestUserAPISchema:
"""์คํค๋ง ๊ฒ์ฆ ํ
์คํธ"""
def test_user_schema_validation(self, schema_validator):
"""์ฌ์ฉ์ ์คํค๋ง ๊ฒ์ฆ ํ
์คํธ"""
# When: ์ฌ์ฉ์๋ฅผ ์กฐํํ๊ณ ์คํค๋ง๋ก ๊ฒ์ฆํ๋ฉด
result = schema_validator.validate_user_with_schema(1)
# Then: ์คํค๋ง๋ฅผ ํต๊ณผํด์ผ ํจ
assert result is True
def test_invalid_schema(self, schema_validator):
"""์๋ชป๋ ์คํค๋ง ํ
์คํธ"""
# Given: ์คํค๋ง์ ๋ง์ง ์๋ ๋ฐ์ดํฐ
invalid_data = {
"id": "not-a-number", # ์ซ์์ฌ์ผ ํ๋๋ฐ ๋ฌธ์์ด
"name": "ํ
์คํธ"
}
# When: ์คํค๋ง๋ก ๊ฒ์ฆํ๋ฉด
result = schema_validator.validate_with_schema(
invalid_data,
schema_validator.user_schema
)
# Then: ์คํจํด์ผ ํจ
assert result is False
class TestUserAPIPydantic:
"""Pydantic ๊ฒ์ฆ ํ
์คํธ"""
def test_pydantic_validation(self, pydantic_validator):
"""Pydantic ๊ฒ์ฆ ํ
์คํธ"""
# When: Pydantic์ผ๋ก ๊ฒ์ฆํ๋ฉด
user = pydantic_validator.validate_user_response(1)
# Then: User ๊ฐ์ฒด๊ฐ ๋ฐํ๋์ด์ผ ํจ
assert user is not None
assert hasattr(user, 'id')
assert hasattr(user, 'email')
def test_pydantic_type_conversion(self, pydantic_validator):
"""Pydantic ํ์
๋ณํ ํ
์คํธ"""
# Given: ๋ฌธ์์ด๋ก ๋ ์ซ์
response_data = {
"id": "123", # ๋ฌธ์์ด์ด์ง๋ง ์๋ ๋ณํ๋์ด์ผ ํจ
"name": "ํ
์คํธ",
"email": "test@example.com",
"created_at": "2024-01-15T10:30:00Z"
}
# When: Pydantic ๋ชจ๋ธ๋ก ํ์ฑํ๋ฉด
from validators.pydantic_validator import User
user = User(**response_data)
# Then: ํ์
์ด ์๋ ๋ณํ๋์ด์ผ ํจ
assert isinstance(user.id, int)
assert user.id == 123
์, ํ
์คํธ ์ฝ๋๊ฐ ์์ฒญ ๋ง์ง? ๐
ํ์ง๋ง ์ด๊ฒ ๋ฐ๋ก ์ข์ ํ
์คํธ์ ํน์ง์ด์ผ:
๋ช
ํํ ๊ตฌ์กฐ - Given-When-Then ํจํด์ผ๋ก ์ฝ๊ธฐ ์ฌ์
๋
๋ฆฝ์ฑ - ๊ฐ ํ
์คํธ๊ฐ ์๋ก ์ํฅ์ ์ฃผ์ง ์์
ํฌ๊ด์ฑ - ์ฑ๊ณต ์ผ์ด์ค์ ์คํจ ์ผ์ด์ค ๋ชจ๋ ํ
์คํธ
ํ๋ผ๋ฏธํฐํ - @pytest.mark.parametrize๋ก ์ฌ๋ฌ ์ผ์ด์ค ํ๋ฒ์ ํ
์คํธ
ํ ์คํธ๋ฅผ ์คํํ๋ ๋ฐฉ๋ฒ์ ๊ฐ๋จํด:
# ๋ชจ๋ ํ
์คํธ ์คํ
pytest
# ํน์ ํ์ผ๋ง ์คํ
pytest tests/test_user_api.py
# ํน์ ํด๋์ค๋ง ์คํ
pytest tests/test_user_api.py::TestUserAPI
# ํน์ ํ
์คํธ๋ง ์คํ
pytest tests/test_user_api.py::TestUserAPI::test_get_user_success
# ์์ธํ ์ถ๋ ฅ
pytest -v
# ๋๋ฆฐ ํ
์คํธ ์ ์ธ
pytest -m "not slow"
# ์คํจํ ํ
์คํธ๋ง ์ฌ์คํ
pytest --lf
# ์ปค๋ฒ๋ฆฌ์ง ์ธก์
pytest --cov=validators --cov-report=html
@pytest.mark.slow, @pytest.mark.integration ๊ฐ์ ๋ง์ปค๋ก ํ ์คํธ๋ฅผ ๋ถ๋ฅํ๋ฉด ํ์ํ ํ ์คํธ๋ง ์ ํ์ ์ผ๋ก ์คํํ ์ ์์ด.
2. ํฝ์ค์ฒ ์ค์ฝํ
fixture์ scope๋ฅผ "session", "module", "class", "function"์ผ๋ก ์ค์ ํด์ ์ฌ์ฌ์ฉ ๋น๋๋ฅผ ์กฐ์ ํ ์ ์์ด.
3. ๋ณ๋ ฌ ์คํ
pytest-xdist ํ๋ฌ๊ทธ์ธ์ ์ค์นํ๋ฉด ํ ์คํธ๋ฅผ ๋ณ๋ ฌ๋ก ์คํํด์ ์๊ฐ์ ๋จ์ถํ ์ ์์ด:
pytest -n auto
๐ CI/CD ํ์ดํ๋ผ์ธ ํตํฉ
๊ฒ์ฆ๊ธฐ๋ฅผ ๋ง๋ค์์ผ๋ฉด ์ด์ ์๋ํํด์ผ์ง! CI/CD ํ์ดํ๋ผ์ธ์ ํตํฉํ๋ฉด ์ฝ๋๊ฐ ํธ์๋ ๋๋ง๋ค ์๋์ผ๋ก API ํ ์คํธ๊ฐ ์คํ๋ผ. ๐ฏ
.github/workflows/api-tests.yml ํ์ผ์ ๋ง๋ค์ด๋ณด์:
name: API ๊ฒ์ฆ ํ
์คํธ
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
schedule:
# ๋งค์ผ ์ค์ 9์์ ์๋ ์คํ
- cron: '0 0 * * *'
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, '3.10', '3.11']
steps:
- name: ์ฝ๋ ์ฒดํฌ์์
uses: actions/checkout@v3
- name: Python ${{ matrix.python-version }} ์ค์
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: ์บ์ ์ค์
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: ์์กด์ฑ ์ค์น
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-cov pytest-html
- name: API ํ
์คํธ ์คํ
env:
API_BASE_URL: ${{ secrets.API_BASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
run: |
pytest tests/ \
-v \
--cov=validators \
--cov-report=xml \
--cov-report=html \
--html=report.html \
--self-contained-html
- name: ์ปค๋ฒ๋ฆฌ์ง ์
๋ก๋
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
- name: ํ
์คํธ ๋ฆฌํฌํธ ์
๋ก๋
if: always()
uses: actions/upload-artifact@v3
with:
name: test-report-${{ matrix.python-version }}
path: |
report.html
htmlcov/
- name: ์ฌ๋ ์๋ฆผ
if: failure()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'API ํ
์คํธ ์คํจ! ํ์ธ์ด ํ์ํฉ๋๋ค.'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
์ด ์ํฌํ๋ก์ฐ๋ ๋ค์๊ณผ ๊ฐ์ ์ผ๋ค์ ํด:
๐ ์๋ ํธ๋ฆฌ๊ฑฐ - ์ฝ๋ ํธ์, PR, ์ค์ผ์ค์ ๋ฐ๋ผ ์คํ
๐ ๋ค์ค ๋ฒ์ ํ
์คํธ - Python 3.8~3.11์์ ๋ชจ๋ ํ
์คํธ
๐ ์บ์ฑ - pip ์บ์๋ก ์ค์น ์๊ฐ ๋จ์ถ
๐ ์ปค๋ฒ๋ฆฌ์ง ์ธก์ - ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์๋์ผ๋ก ๊ณ์ฐ
๐ ๋ฆฌํฌํธ ์์ฑ - HTML ํํ์ ์์ ํ
์คํธ ๋ฆฌํฌํธ
๐ ์๋ฆผ - ์คํจ ์ ์ฌ๋์ผ๋ก ์๋ฆผ
GitLab์ ์ฌ์ฉํ๋ค๋ฉด .gitlab-ci.yml์ ๋ง๋ค์ด:
stages:
- test
- report
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
paths:
- .cache/pip
- venv/
before_script:
- python -V
- pip install virtualenv
- virtualenv venv
- source venv/bin/activate
- pip install -r requirements.txt
test:api:
stage: test
image: python:3.10
script:
- pytest tests/ -v --cov=validators --cov-report=xml --cov-report=html
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
paths:
- htmlcov/
expire_in: 1 week
only:
- main
- develop
- merge_requests
test:api:python38:
extends: test:api
image: python:3.8
test:api:python39:
extends: test:api
image: python:3.9
test:api:python311:
extends: test:api
image: python:3.11
pages:
stage: report
dependencies:
- test:api
script:
- mv htmlcov/ public/
artifacts:
paths:
- public
only:
- main
๐จ CI/CD ๋ชจ๋ฒ ์ฌ๋ก
1. ํ๊ฒฝ ๋ณ์ ๊ด๋ฆฌ
๋ฏผ๊ฐํ ์ ๋ณด(API ํค, ๋น๋ฐ๋ฒํธ)๋ ์ ๋ ์ฝ๋์ ํ๋์ฝ๋ฉํ์ง ๋ง๊ณ CI/CD ์์คํ
์ ์ํฌ๋ฆฟ ๊ธฐ๋ฅ์ ์ฌ์ฉํด.
2. ํ
์คํธ ๊ฒฉ๋ฆฌ
๊ฐ ํ
์คํธ๊ฐ ๋
๋ฆฝ์ ์ผ๋ก ์คํ๋๋๋ก ํด. ํ
์คํธ ๊ฐ ์์กด์ฑ์ด ์์ผ๋ฉด ์์์ ๋ฐ๋ผ ๊ฒฐ๊ณผ๊ฐ ๋ฌ๋ผ์ง ์ ์์ด.
3. ๋น ๋ฅธ ํผ๋๋ฐฑ
๋น ๋ฅธ ํ
์คํธ๋ฅผ ๋จผ์ ์คํํ๊ณ , ๋๋ฆฐ ํตํฉ ํ
์คํธ๋ ๋์ค์ ์คํํด. ๊ฐ๋ฐ์๊ฐ ๋น ๋ฅด๊ฒ ํผ๋๋ฐฑ์ ๋ฐ์ ์ ์๋๋ก.
4. ์คํจ ์ ์๋ฆผ
ํ
์คํธ๊ฐ ์คํจํ๋ฉด ์ฆ์ ํ์ ์๋ ค์ผ ํด. ์ฌ๋, ์ด๋ฉ์ผ, MS Teams ๋ฑ์ ํ์ฉํด.
5. ์ ๊ธฐ ์คํ
์ฝ๋ ๋ณ๊ฒฝ์ด ์์ด๋ ์ ๊ธฐ์ ์ผ๋ก ํ
์คํธ๋ฅผ ์คํํด. ์ธ๋ถ API์ ๋ณ๊ฒฝ์ฌํญ์ ์กฐ๊ธฐ์ ๋ฐ๊ฒฌํ ์ ์์ด.
๐ ๋ฆฌํฌํ ๊ณผ ๋ชจ๋ํฐ๋ง
ํ ์คํธ ๊ฒฐ๊ณผ๋ฅผ ์์๊ฒ ์๊ฐํํ๊ณ ๋ชจ๋ํฐ๋งํ๋ ๊ฒ๋ ์ค์ํด! ์ซ์์ ๊ทธ๋ํ๋ก ๋ณด๋ฉด ํจ์ฌ ์ดํดํ๊ธฐ ์ฝ๊ฑฐ๋ . ๐
pytest-html์ ์ฌ์ฉํ๋ฉด ๋ฉ์ง HTML ๋ฆฌํฌํธ๋ฅผ ๋ง๋ค ์ ์์ด:
pip install pytest-html pytest-metadata
๊ทธ๋ฆฌ๊ณ ์ปค์คํ ๋ฆฌํฌํธ๋ฅผ ๋ง๋ค์ด๋ณด์. conftest.py์ ์ถ๊ฐํด:
import pytest
from datetime import datetime
from py.xml import html
def pytest_html_report_title(report):
"""๋ฆฌํฌํธ ์ ๋ชฉ ์ปค์คํฐ๋ง์ด์ง"""
report.title = "API ๊ฒ์ฆ ํ
์คํธ ๋ฆฌํฌํธ"
def pytest_configure(config):
"""๋ฉํ๋ฐ์ดํฐ ์ถ๊ฐ"""
config._metadata['ํ๋ก์ ํธ'] = 'API ์๋ ๊ฒ์ฆ๊ธฐ'
config._metadata['ํ
์คํธ ํ๊ฒฝ'] = 'Staging'
config._metadata['์คํ ์๊ฐ'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def pytest_html_results_table_header(cells):
"""ํ
์ด๋ธ ํค๋ ์ปค์คํฐ๋ง์ด์ง"""
cells.insert(2, html.th('์ค๋ช
'))
cells.insert(3, html.th('์๋ต ์๊ฐ'))
def pytest_html_results_table_row(report, cells):
"""ํ
์ด๋ธ ํ ์ปค์คํฐ๋ง์ด์ง"""
cells.insert(2, html.td(report.description))
cells.insert(3, html.td(f"{report.duration:.3f}s"))
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""ํ
์คํธ ๊ฒฐ๊ณผ์ ์ถ๊ฐ ์ ๋ณด ํฌํจ"""
outcome = yield
report = outcome.get_result()
report.description = str(item.function.__doc__)
์ด์ ๋ฆฌํฌํธ๋ฅผ ์์ฑํ ๋:
pytest --html=report.html --self-contained-html --css=custom.css
Allure๋ ์ ๋ง ์์ ํ ์คํธ ๋ฆฌํฌํธ๋ฅผ ๋ง๋ค์ด์ค. ๋๋ ๊ฐ์ธ์ ์ผ๋ก ์ ๋ง ์ข์ํ๋ ๋๊ตฌ์ผ! ๐
pip install allure-pytest
ํ ์คํธ ์ฝ๋์ Allure ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ถ๊ฐํด:
import allure
import pytest
@allure.feature('์ฌ์ฉ์ ๊ด๋ฆฌ')
@allure.story('์ฌ์ฉ์ ์กฐํ')
class TestUserRetrieval:
@allure.title('๋จ์ผ ์ฌ์ฉ์ ์กฐํ ์ฑ๊ณต')
@allure.description('์กด์ฌํ๋ ์ฌ์ฉ์ ID๋ก ์กฐํ ์ ์ฌ๋ฐ๋ฅธ ๋ฐ์ดํฐ ๋ฐํ')
@allure.severity(allure.severity_level.CRITICAL)
def test_get_user_success(self, user_validator):
with allure.step('์ฌ์ฉ์ ID 1๋ก ์กฐํ ์์ฒญ'):
result = user_validator.validate_user_response(1)
with allure.step('์๋ต ๋ฐ์ดํฐ ๊ฒ์ฆ'):
assert result is not None
assert 'id' in result
with allure.step('์คํฌ๋ฆฐ์ท ์ฒจ๋ถ'):
allure.attach(
str(result),
name='์๋ต ๋ฐ์ดํฐ',
attachment_type=allure.attachment_type.JSON
)
@allure.title('์กด์ฌํ์ง ์๋ ์ฌ์ฉ์ ์กฐํ')
@allure.description('์กด์ฌํ์ง ์๋ ID๋ก ์กฐํ ์ 404 ์๋ฌ ๋ฐํ')
@allure.severity(allure.severity_level.NORMAL)
def test_get_user_not_found(self, user_validator):
with allure.step('์กด์ฌํ์ง ์๋ ID๋ก ์กฐํ'):
response = user_validator.get("/api/users/999999")
with allure.step('404 ์ํ ์ฝ๋ ํ์ธ'):
assert response.status_code == 404
allure.attach(
response.text,
name='์๋ฌ ์๋ต',
attachment_type=allure.attachment_type.TEXT
)
๋ฆฌํฌํธ ์์ฑ:
# ํ
์คํธ ์คํ ๋ฐ ๊ฒฐ๊ณผ ์ ์ฅ
pytest --alluredir=./allure-results
# ๋ฆฌํฌํธ ์์ฑ ๋ฐ ์ด๊ธฐ
allure serve ./allure-results
Allure์ ์ฅ์ :
โจ ์๊ฐ์ - ๊ทธ๋ํ์ ์ฐจํธ๋ก ํ๋์ ํ์
โจ ์์ธํจ - ๊ฐ ๋จ๊ณ๋ณ๋ก ์์ธํ ์ ๋ณด ์ ๊ณต
โจ ํ์คํ ๋ฆฌ - ์ด์ ํ
์คํธ ๊ฒฐ๊ณผ์ ๋น๊ต ๊ฐ๋ฅ
โจ ์ฒจ๋ถ ํ์ผ - ์คํฌ๋ฆฐ์ท, ๋ก๊ทธ ๋ฑ์ ์ฝ๊ฒ ์ฒจ๋ถ
Grafana์ Prometheus๋ฅผ ์ฐ๋ํ๋ฉด ์ค์๊ฐ์ผ๋ก API ์ํ๋ฅผ ๋ชจ๋ํฐ๋งํ ์ ์์ด. ์ฝ๋๋ฅผ ์ข ๋ณด์ฌ์ค๊ฒ:
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import time
class MonitoredValidator(BaseAPIValidator):
"""๋ชจ๋ํฐ๋ง ๊ธฐ๋ฅ์ด ์ถ๊ฐ๋ ๊ฒ์ฆ๊ธฐ"""
def __init__(self, base_url: str):
super().__init__(base_url)
# ๋ฉํธ๋ฆญ ์ ์
self.request_count = Counter(
'api_requests_total',
'Total API requests',
['method', 'endpoint', 'status']
)
self.request_duration = Histogram(
'api_request_duration_seconds',
'API request duration',
['method', 'endpoint']
)
self.success_rate = Gauge(
'api_success_rate',
'API success rate',
['endpoint']
)
self.active_requests = Gauge(
'api_active_requests',
'Number of active requests'
)
def get(self, endpoint: str, **kwargs):
"""๋ชจ๋ํฐ๋ง์ด ์ถ๊ฐ๋ GET ์์ฒญ"""
self.active_requests.inc()
start_time = time.time()
try:
response = super().get(endpoint, **kwargs)
duration = time.time() - start_time
# ๋ฉํธ๋ฆญ ๊ธฐ๋ก
self.request_count.labels(
method='GET',
endpoint=endpoint,
status=response.status_code
).inc()
self.request_duration.labels(
method='GET',
endpoint=endpoint
).observe(duration)
return response
finally:
self.active_requests.dec()
def calculate_success_rate(self, endpoint: str, window: int = 100):
"""์ฑ๊ณต๋ฅ ๊ณ์ฐ"""
# ์ต๊ทผ N๊ฐ ์์ฒญ์ ์ฑ๊ณต๋ฅ ๊ณ์ฐ ๋ก์ง
# ์ค์ ๋ก๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์บ์์์ ๊ฐ์ ธ์์ผ ํจ
pass
# Prometheus ์๋ฒ ์์
start_http_server(8000)
์ด์ Grafana์์ ์ด๋ฐ ์ฟผ๋ฆฌ๋ก ๋์๋ณด๋๋ฅผ ๋ง๋ค ์ ์์ด:
# ์์ฒญ ์ฑ๊ณต๋ฅ
rate(api_requests_total{status="200"}[5m]) / rate(api_requests_total[5m]) * 100
# ํ๊ท ์๋ต ์๊ฐ
rate(api_request_duration_seconds_sum[5m]) / rate(api_request_duration_seconds_count[5m])
# ์ด๋น ์์ฒญ ์
rate(api_requests_total[1m])
๐ฑ ์๋ฆผ ์ค์
์ค์ํ ๋ฉํธ๋ฆญ์ ๋ํด ์๋ฆผ์ ์ค์ ํ๋ ๊ฒ๋ ์์ง ๋ง! ์๋ฅผ ๋ค์ด:
1. ์ฑ๊ณต๋ฅ ํ๋ฝ
์ฑ๊ณต๋ฅ ์ด 95% ์๋๋ก ๋จ์ด์ง๋ฉด ์๋ฆผ
2. ์๋ต ์๊ฐ ์ฆ๊ฐ
ํ๊ท ์๋ต ์๊ฐ์ด 1์ด๋ฅผ ์ด๊ณผํ๋ฉด ์๋ฆผ
3. ์๋ฌ์จ ์ฆ๊ฐ
5xx ์๋ฌ๊ฐ ๋ถ๋น 10๊ฐ ์ด์ ๋ฐ์ํ๋ฉด ์๋ฆผ
4. ํ์์์
ํ์์์์ด ์ฐ์ 3ํ ๋ฐ์ํ๋ฉด ์๋ฆผ
๐ค AI/๋จธ์ ๋ฌ๋ ํ์ฉํ๊ธฐ
์ด์ ์ง์ง ์ฌ๋ฏธ์๋ ๋ถ๋ถ์ด์ผ! AI์ ๋จธ์ ๋ฌ๋์ ํ์ฉํด์ ๊ฒ์ฆ๊ธฐ๋ฅผ ๋ ๋๋ํ๊ฒ ๋ง๋ค์ด๋ณด์. ๐ง
API ์๋ต ํจํด์ ํ์ตํด์ ๋น์ ์์ ์ธ ์๋ต์ ์๋์ผ๋ก ๊ฐ์งํ ์ ์์ด:
from sklearn.ensemble import IsolationForest
import numpy as np
import pandas as pd
from typing import List, Dict
class AnomalyDetector:
"""API ์๋ต ์ด์ ํ์ง๊ธฐ"""
def __init__(self):
self.model = IsolationForest(
contamination=0.1, # 10%๋ฅผ ์ด์์น๋ก ๊ฐ์ฃผ
random_state=42
)
self.is_trained = False
self.feature_names = [
'response_time',
'response_size',
'status_code',
'field_count'
]
def extract_features(self, response_data: Dict) -> List[float]:
"""์๋ต์์ ํน์ง ์ถ์ถ"""
features = [
response_data.get('elapsed', 0), # ์๋ต ์๊ฐ
len(str(response_data.get('content', ''))), # ์๋ต ํฌ๊ธฐ
response_data.get('status_code', 0), # ์ํ ์ฝ๋
len(response_data.get('json', {})) # ํ๋ ๊ฐ์
]
return features
def train(self, historical_data: List[Dict]):
"""๊ณผ๊ฑฐ ๋ฐ์ดํฐ๋ก ๋ชจ๋ธ ํ์ต"""
print(f"๐ {len(historical_data)}๊ฐ์ ๋ฐ์ดํฐ๋ก ํ์ต ์์...")
# ํน์ง ์ถ์ถ
features = [self.extract_features(data) for data in historical_data]
X = np.array(features)
# ๋ชจ๋ธ ํ์ต
self.model.fit(X)
self.is_trained = True
print("โ
ํ์ต ์๋ฃ!")
def detect_anomaly(self, response_data: Dict) -> Dict:
"""์ด์ ์ฌ๋ถ ํ์ง"""
if not self.is_trained:
raise Exception("๋ชจ๋ธ์ด ํ์ต๋์ง ์์์ต๋๋ค")
# ํน์ง ์ถ์ถ
features = self.extract_features(response_data)
X = np.array([features])
# ์์ธก
prediction = self.model.predict(X)[0]
score = self.model.score_samples(X)[0]
is_anomaly = prediction == -1
return {
'is_anomaly': is_anomaly,
'anomaly_score': float(score),
'features': dict(zip(self.feature_names, features)),
'message': '์ด์ ํ์ง!' if is_anomaly else '์ ์'
}
class SmartValidator(BaseAPIValidator):
"""AI ๊ธฐ๋ฐ ์ค๋งํธ ๊ฒ์ฆ๊ธฐ"""
def __init__(self, base_url: str):
super().__init__(base_url)
self.anomaly_detector = AnomalyDetector()
self.response_history = []
def collect_training_data(self, endpoint: str, iterations: int = 100):
"""ํ์ต ๋ฐ์ดํฐ ์์ง"""
print(f"๐ ํ์ต ๋ฐ์ดํฐ ์์ง ์ค... ({iterations}ํ)")
training_data = []
for i in range(iterations):
try:
response = self.get(endpoint)
data = {
'elapsed': response.elapsed.total_seconds(),
'content': response.text,
'status_code': response.status_code,
'json': response.json() if response.headers.get('content-type', '').startswith('application/json') else {}
}
training_data.append(data)
if (i + 1) % 10 == 0:
print(f" ์งํ: {i + 1}/{iterations}")
except Exception as e:
print(f" ์๋ฌ: {str(e)}")
return training_data
def train_anomaly_detector(self, endpoint: str, iterations: int = 100):
"""์ด์ ํ์ง ๋ชจ๋ธ ํ์ต"""
training_data = self.collect_training_data(endpoint, iterations)
self.anomaly_detector.train(training_data)
def validate_with_ai(self, endpoint: str) -> Dict:
"""AI ๊ธฐ๋ฐ ๊ฒ์ฆ"""
print(f"\n๐ค AI ๊ฒ์ฆ ์์: {endpoint}")
# API ํธ์ถ
response = self.get(endpoint)
# ๊ธฐ๋ณธ ๊ฒ์ฆ
basic_valid = response.status_code == 200
# ์๋ต ๋ฐ์ดํฐ ์ค๋น
response_data = {
'elapsed': response.elapsed.total_seconds(),
'content': response.text,
'status_code': response.status_code,
'json': response.json() if response.headers.get('content-type', '').startswith('application/json') else {}
}
# ์ด์ ํ์ง
anomaly_result = self.anomaly_detector.detect_anomaly(response_data)
# ๊ฒฐ๊ณผ ์ถ๋ ฅ
if anomaly_result['is_anomaly']:
print(f"โ ๏ธ ์ด์ ํ์ง!")
print(f" ์ค์ฝ์ด: {anomaly_result['anomaly_score']:.4f}")
print(f" ํน์ง:")
for key, value in anomaly_result['features'].items():
print(f" - {key}: {value}")
else:
print(f"โ
์ ์ ์๋ต")
return {
'basic_validation': basic_valid,
'anomaly_detection': anomaly_result,
'response_data': response_data
}
์ด์ ํ์ง์ ํ์ฉ ์ฌ๋ก:
๐ ์๋ต ์๊ฐ ๊ธ์ฆ - ํ์๋ณด๋ค ํจ์ฌ ๋๋ฆฐ ์๋ต ๊ฐ์ง
๐ ์๋ต ํฌ๊ธฐ ๋ณํ - ๊ฐ์๊ธฐ ์๋ต์ด ๋๋ฌด ํฌ๊ฑฐ๋ ์์์ง
๐ ํ๋ ๊ตฌ์กฐ ๋ณ๊ฒฝ - ์์์น ๋ชปํ ํ๋ ์ถ๊ฐ/์ญ์
๐ ๋ฐ์ดํฐ ๋ถํฌ ์ด์ - ๊ฐ์ ๋ฒ์๊ฐ ํ์์ ๋ค๋ฆ
API ์คํ์ ๋ถ์ํด์ ์๋์ผ๋ก ํ ์คํธ ์ผ์ด์ค๋ฅผ ์์ฑํ ์๋ ์์ด:
from typing import List, Dict, Any
import random
import string
class TestCaseGenerator:
"""์๋ ํ
์คํธ ์ผ์ด์ค ์์ฑ๊ธฐ"""
def __init__(self, schema: Dict):
self.schema = schema
def generate_valid_data(self) -> Dict[str, Any]:
"""์ ํจํ ํ
์คํธ ๋ฐ์ดํฐ ์์ฑ"""
data = {}
for field, rules in self.schema.items():
field_type = rules.get('type')
if field_type == 'string':
if rules.get('format') == 'email':
data[field] = self._generate_email()
elif rules.get('format') == 'url':
data[field] = self._generate_url()
else:
min_len = rules.get('minLength', 1)
max_len = rules.get('maxLength', 50)
data[field] = self._generate_string(min_len, max_len)
elif field_type == 'integer':
minimum = rules.get('minimum', 0)
maximum = rules.get('maximum', 100)
data[field] = random.randint(minimum, maximum)
elif field_type == 'boolean':
data[field] = random.choice([True, False])
elif field_type == 'array':
item_type = rules.get('items', {}).get('type', 'string')
length = random.randint(1, 5)
data[field] = [
self._generate_value(item_type)
for _ in range(length)
]
return data
def generate_invalid_data(self) -> List[Dict[str, Any]]:
"""์ ํจํ์ง ์์ ํ
์คํธ ๋ฐ์ดํฐ ์์ฑ"""
invalid_cases = []
for field, rules in self.schema.items():
# ํ์ ํ๋ ๋๋ฝ
if rules.get('required', False):
case = self.generate_valid_data()
del case[field]
invalid_cases.append({
'data': case,
'reason': f'ํ์ ํ๋ ๋๋ฝ: {field}'
})
# ์๋ชป๋ ํ์
case = self.generate_valid_data()
field_type = rules.get('type')
if field_type == 'string':
case[field] = 12345 # ์ซ์๋ก ๋ณ๊ฒฝ
elif field_type == 'integer':
case[field] = "not a number" # ๋ฌธ์์ด๋ก ๋ณ๊ฒฝ
invalid_cases.append({
'data': case,
'reason': f'์๋ชป๋ ํ์
: {field}'
})
# ๋ฒ์ ์ด๊ณผ
if field_type == 'integer':
maximum = rules.get('maximum')
if maximum:
case = self.generate_valid_data()
case[field] = maximum + 1
invalid_cases.append({
'data': case,
'reason': f'์ต๋๊ฐ ์ด๊ณผ: {field}'
})
return invalid_cases
def _generate_string(self, min_len: int, max_len: int) -> str:
"""๋๋ค ๋ฌธ์์ด ์์ฑ"""
length = random.randint(min_len, max_len)
return ''.join(random.choices(string.ascii_letters, k=length))
def _generate_email(self) -> str:
"""๋๋ค ์ด๋ฉ์ผ ์์ฑ"""
username = self._generate_string(5, 10)
domain = self._generate_string(5, 10)
return f"{username}@{domain}.com"
def _generate_url(self) -> str:
"""๋๋ค URL ์์ฑ"""
path = self._generate_string(5, 10)
return f"https://example.com/{path}"
def _generate_value(self, value_type: str) -> Any:
"""ํ์
์ ๋ง๋ ๋๋ค ๊ฐ ์์ฑ"""
if value_type == 'string':
return self._generate_string(5, 10)
elif value_type == 'integer':
return random.randint(1, 100)
elif value_type == 'boolean':
return random.choice([True, False])
return None
# ์ฌ์ฉ ์์
user_schema = {
'name': {
'type': 'string',
'minLength': 2,
'maxLength': 50,
'required': True
},
'email': {
'type': 'string',
'format': 'email',
'required': True
},
'age': {
'type': 'integer',
'minimum': 0,
'maximum': 150
}
}
generator = TestCaseGenerator(user_schema)
# ์ ํจํ ๋ฐ์ดํฐ 10๊ฐ ์์ฑ
print("โ
์ ํจํ ํ
์คํธ ์ผ์ด์ค:")
for i in range(5):
print(f"{i+1}. {generator.generate_valid_data()}")
# ์ ํจํ์ง ์์ ๋ฐ์ดํฐ ์์ฑ
print("\nโ ์ ํจํ์ง ์์ ํ
์คํธ ์ผ์ด์ค:")
invalid_cases = generator.generate_invalid_data()
for i, case in enumerate(invalid_cases[:5]):
print(f"{i+1}. {case['reason']}")
print(f" ๋ฐ์ดํฐ: {case['data']}")
์๋ ์์ฑ์ ์ฅ์ :
๐ฒ ๋ค์์ฑ - ์ฌ๋์ด ์๊ฐํ์ง ๋ชปํ ์ผ์ด์ค๋ ํ
์คํธ
๐ฒ ํจ์จ์ฑ - ์๋ฐฑ ๊ฐ์ ํ
์คํธ ์ผ์ด์ค๋ฅผ ์์๊ฐ์ ์์ฑ
๐ฒ ์ผ๊ด์ฑ - ์คํค๋ง ๋ณ๊ฒฝ ์ ์๋์ผ๋ก ํ
์คํธ๋ ์
๋ฐ์ดํธ
๐ฒ ๊ฒฝ๊ณ๊ฐ ํ
์คํธ - ์ต์/์ต๋๊ฐ ์๋ ํ
์คํธ
๐ ๋ณด์ ํ ์คํธ ํตํฉ
API ๊ฒ์ฆ์์ ๋ณด์์ ์ ๋ ๋น ์ง ์ ์๋ ๋ถ๋ถ์ด์ผ. ๊ธฐ๋ฅ์ด ์ ์๋ํด๋ ๋ณด์์ ๊ตฌ๋ฉ์ด ์์ผ๋ฉด ํฐ์ผ ๋๊ฑฐ๋ ! ๐ก๏ธ
class SecurityValidator(BaseAPIValidator):
"""๋ณด์ ๊ฒ์ฆ๊ธฐ"""
def __init__(self, base_url: str):
super().__init__(base_url)
self.valid_token = None
self.invalid_token = "invalid_token_12345"
def test_authentication_required(self, endpoint: str):
"""์ธ์ฆ ํ์ ํ์ธ"""
print(f"\n๐ ์ธ์ฆ ํ
์คํธ: {endpoint}")
# ํ ํฐ ์์ด ์์ฒญ
response = self.get(endpoint)
if response.status_code == 401:
self.print_validation_result(
"์ธ์ฆ ํ์", True, "401 Unauthorized"
)
else:
self.print_validation_result(
"์ธ์ฆ ํ์", False,
f"์์: 401, ์ค์ : {response.status_code}"
)
def test_invalid_token(self, endpoint: str):
"""์๋ชป๋ ํ ํฐ ์ฒ๋ฆฌ ํ์ธ"""
print(f"\n๐ซ ์๋ชป๋ ํ ํฐ ํ
์คํธ: {endpoint}")
headers = {'Authorization': f'Bearer {self.invalid_token}'}
response = self.get(endpoint, headers=headers)
if response.status_code in [401, 403]:
self.print_validation_result(
"์๋ชป๋ ํ ํฐ ๊ฑฐ๋ถ", True,
f"{response.status_code}"
)
else:
self.print_validation_result(
"์๋ชป๋ ํ ํฐ ๊ฑฐ๋ถ", False,
"์๋ชป๋ ํ ํฐ์ด ํ์ฉ๋จ!"
)
def test_authorization(self, endpoint: str, required_role: str):
"""๊ถํ ํ์ธ"""
print(f"\n๐ฎ ๊ถํ ํ
์คํธ: {endpoint}")
# ๊ถํ ์๋ ์ฌ์ฉ์๋ก ์์ฒญ
headers = {'Authorization': f'Bearer {self.valid_token}'}
response = self.get(endpoint, headers=headers)
if response.status_code == 403:
self.print_validation_result(
"๊ถํ ์ฒดํฌ", True, "403 Forbidden"
)
else:
self.print_validation_result(
"๊ถํ ์ฒดํฌ", False,
"๊ถํ ์๋ ์ ๊ทผ์ด ํ์ฉ๋จ!"
)
def test_sql_injection(self, endpoint: str, param_name: str):
"""SQL ์ธ์ ์
์ทจ์ฝ์ ํ
์คํธ"""
print(f"\n๐ SQL ์ธ์ ์
ํ
์คํธ: {endpoint}")
# SQL ์ธ์ ์
ํ์ด๋ก๋
payloads = [
"' OR '1'='1",
"1' OR '1' = '1",
"' OR 1=1--",
"admin'--",
"1; DROP TABLE users--"
]
vulnerable = False
for payload in payloads:
params = {param_name: payload}
response = self.get(endpoint, params=params)
# SQL ์๋ฌ ๋ฉ์์ง ํ์ธ
error_keywords = ['sql', 'mysql', 'sqlite', 'postgresql', 'syntax error']
response_text = response.text.lower()
if any(keyword in response_text for keyword in error_keywords):
vulnerable = True
self.print_validation_result(
f"SQL ์ธ์ ์
์ทจ์ฝ์ ", False,
f"ํ์ด๋ก๋: {payload}"
)
break
if not vulnerable:
self.print_validation_result(
"SQL ์ธ์ ์
๋ฐฉ์ด", True, "๋ชจ๋ ํ์ด๋ก๋ ์ฐจ๋จ๋จ"
)
def test_xss(self, endpoint: str, param_name: str):
"""XSS ์ทจ์ฝ์ ํ
์คํธ"""
print(f"\n๐ญ XSS ํ
์คํธ: {endpoint}")
# XSS ํ์ด๋ก๋
payloads = [
"<script>alert('XSS')</script>",
"<img src=x onerror=alert('XSS')>",
"javascript:alert('XSS')",
"<svg onload=alert('XSS')>"
]
vulnerable = False
for payload in payloads:
params = {param_name: payload}
response = self.get(endpoint, params=params)
# ํ์ด๋ก๋๊ฐ ๊ทธ๋๋ก ๋ฐํ๋๋์ง ํ์ธ
if payload in response.text:
vulnerable = True
self.print_validation_result(
"XSS ์ทจ์ฝ์ ", False,
f"ํ์ด๋ก๋๊ฐ ์ด์ค์ผ์ดํ๋์ง ์์: {payload}"
)
break
if not vulnerable:
self.print_validation_result(
"XSS ๋ฐฉ์ด", True, "๋ชจ๋ ์
๋ ฅ์ด ์ ์ ํ ์ด์ค์ผ์ดํ๋จ"
)
def test_rate_limiting(self, endpoint: str, max_requests: int = 100):
"""Rate Limiting ํ
์คํธ"""
print(f"\nโฑ๏ธ Rate Limiting ํ
์คํธ: {endpoint}")
blocked = False
for i in range(max_requests + 10):
response = self.get(endpoint)
if response.status_code == 429: # Too Many Requests
blocked = True
self.print_validation_result(
"Rate Limiting", True,
f"{i+1}๋ฒ์งธ ์์ฒญ์์ ์ฐจ๋จ๋จ"
)
break
if not blocked:
self.print_validation_result(
"Rate Limiting", False,
f"{max_requests + 10}๊ฐ ์์ฒญ์ด ๋ชจ๋ ํ์ฉ๋จ"
)
def test_sensitive_data_exposure(self, endpoint: str):
"""๋ฏผ๊ฐํ ๋ฐ์ดํฐ ๋
ธ์ถ ํ
์คํธ"""
print(f"\n๐ ๋ฏผ๊ฐ ๋ฐ์ดํฐ ๋
ธ์ถ ํ
์คํธ: {endpoint}")
response = self.get(endpoint)
if response.status_code != 200:
return
# ๋ฏผ๊ฐํ ์ ๋ณด ํค์๋
sensitive_keywords = [
'password', 'secret', 'token', 'api_key',
'private_key', 'credit_card', 'ssn'
]
data = response.json() if response.headers.get('content-type', '').startswith('application/json') else {}
exposed = []
for key in data.keys():
if any(keyword in key.lower() for keyword in sensitive_keywords):
exposed.append(key)
if exposed:
self.print_validation_result(
"๋ฏผ๊ฐ ๋ฐ์ดํฐ ๋
ธ์ถ", False,
f"๋
ธ์ถ๋ ํ๋: {', '.join(exposed)}"
)
else:
self.print_validation_result(
"๋ฏผ๊ฐ ๋ฐ์ดํฐ ๋ณดํธ", True, "๋ฏผ๊ฐํ ์ ๋ณด ์์"
)
OWASP Top 10์ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ์ฅ ์ค์ํ ๋ณด์ ์ํ ๋ชฉ๋ก์ด์ผ. API ๊ฒ์ฆ๊ธฐ์ ์ด๋ฐ ํญ๋ชฉ๋ค์ ์ฒดํฌํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ฉด ์ข์:
๐ก๏ธ OWASP API Security Top 10
1. Broken Object Level Authorization
๋ค๋ฅธ ์ฌ์ฉ์์ ๊ฐ์ฒด์ ์ ๊ทผํ ์ ์๋์ง ํ
์คํธ
2. Broken Authentication
์ธ์ฆ ๋ฉ์ปค๋์ฆ์ ์ทจ์ฝ์ ํ์ธ
3. Excessive Data Exposure
ํ์ ์ด์์ ๋ฐ์ดํฐ๊ฐ ๋
ธ์ถ๋๋์ง ํ์ธ
4. Lack of Resources & Rate Limiting
๋ฆฌ์์ค ์ ํ๊ณผ Rate Limiting ํ์ธ
5. Broken Function Level Authorization
๊ธฐ๋ฅ ์์ค์ ๊ถํ ์ฒดํฌ ํ์ธ
6. Mass Assignment
๋๋ ํ ๋น ์ทจ์ฝ์ ํ
์คํธ
7. Security Misconfiguration
๋ณด์ ์ค์ ์ค๋ฅ ํ์ธ
8. Injection
SQL, NoSQL, Command Injection ํ
์คํธ
9. Improper Assets Management
๊ตฌ๋ฒ์ API ์๋ํฌ์ธํธ ๋
ธ์ถ ํ์ธ
10. Insufficient Logging & Monitoring
๋ก๊น
๊ณผ ๋ชจ๋ํฐ๋ง ์ ์ ์ฑ ํ์ธ
๐ ์ค์ ํ๋ก์ ํธ: ์์ ํ ๊ฒ์ฆ ์์คํ
์, ์ด์ ์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ชจ๋ ๊ฑธ ํฉ์ณ์ ์์ ํ API ๊ฒ์ฆ ์์คํ ์ ๋ง๋ค์ด๋ณด์! ๐
api-validator-pro/
โโโ src/
โ โโโ __init__.py
โ โโโ validators/
โ โ โโโ __init__.py
โ โ โโโ base.py
โ โ โโโ functional.py
โ โ โโโ performance.py
โ โ โโโ security.py
โ โ โโโ ai_based.py
โ โโโ reporters/
โ โ โโโ __init__.py
โ โ โโโ html_reporter.py
โ โ โโโ json_reporter.py
โ โ โโโ slack_reporter.py
โ โโโ utils/
โ โ โโโ __init__.py
โ โ โโโ config.py
โ โ โโโ logger.py
โ โ โโโ helpers.py
โ โโโ cli.py
โโโ tests/
โ โโโ __init__.py
โ โโโ unit/
โ โโโ integration/
โ โโโ e2e/
โโโ config/
โ โโโ default.yaml
โ โโโ staging.yaml
โ โโโ production.yaml
โโโ reports/
โโโ .env.example
โโโ requirements.txt
โโโ setup.py
โโโ README.md
from typing import List, Dict, Any
from dataclasses import dataclass
from enum import Enum
import yaml
import logging
class ValidationLevel(Enum):
"""๊ฒ์ฆ ์์ค"""
BASIC = "basic"
STANDARD = "standard"
COMPREHENSIVE = "comprehensive"
SECURITY_FOCUSED = "security_focused"
@dataclass
class ValidationResult:
"""๊ฒ์ฆ ๊ฒฐ๊ณผ"""
endpoint: str
method: str
passed: bool
total_checks: int
passed_checks: int
failed_checks: int
warnings: List[str]
errors: List[str]
duration: float
details: Dict[str, Any]
class APIValidationEngine:
"""ํตํฉ API ๊ฒ์ฆ ์์ง"""
def __init__(self, config_path: str):
"""
Args:
config_path: ์ค์ ํ์ผ ๊ฒฝ๋ก
"""
self.config = self._load_config(config_path)
self.logger = self._setup_logger()
# ๊ฒ์ฆ๊ธฐ ์ด๊ธฐํ
self.functional_validator = None
self.performance_validator = None
self.security_validator = None
self.ai_validator = None
self._initialize_validators()
def _load_config(self, config_path: str) -> Dict:
"""์ค์ ํ์ผ ๋ก๋"""
with open(config_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def _setup_logger(self) -> logging.Logger:
"""๋ก๊ฑฐ ์ค์ """
logger = logging.getLogger('APIValidator')
logger.setLevel(logging.INFO)
# ์ฝ์ ํธ๋ค๋ฌ
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# ํ์ผ ํธ๋ค๋ฌ
file_handler = logging.FileHandler('validation.log')
file_handler.setLevel(logging.DEBUG)
# ํฌ๋งคํฐ
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
return logger
def _initialize_validators(self):
"""๊ฒ์ฆ๊ธฐ ์ด๊ธฐํ"""
base_url = self.config['api']['base_url']
from validators.functional import FunctionalValidator
from validators.performance import PerformanceValidator
from validators.security import SecurityValidator
from validators.ai_based import AIValidator
self.functional_validator = FunctionalValidator(base_url)
self.performance_validator = PerformanceValidator(base_url)
self.security_validator = SecurityValidator(base_url)
self.ai_validator = AIValidator(base_url)
def validate_endpoint(self,
endpoint: str,
method: str = 'GET',
level: ValidationLevel = ValidationLevel.STANDARD) -> ValidationResult:
"""์๋ํฌ์ธํธ ๊ฒ์ฆ"""
self.logger.info(f"๊ฒ์ฆ ์์: {method} {endpoint}")
import time
start_time = time.time()
results = {
'functional': [],
'performance': [],
'security': [],
'ai': []
}
# ๊ธฐ๋ฅ ๊ฒ์ฆ
if level in [ValidationLevel.BASIC, ValidationLevel.STANDARD, ValidationLevel.COMPREHENSIVE]:
self.logger.info("๊ธฐ๋ฅ ๊ฒ์ฆ ์คํ ์ค...")
results['functional'] = self._run_functional_tests(endpoint, method)
# ์ฑ๋ฅ ๊ฒ์ฆ
if level in [ValidationLevel.STANDARD, ValidationLevel.COMPREHENSIVE]:
self.logger.info("์ฑ๋ฅ ๊ฒ์ฆ ์คํ ์ค...")
results['performance'] = self._run_performance_tests(endpoint)
# ๋ณด์ ๊ฒ์ฆ
if level in [ValidationLevel.COMPREHENSIVE, ValidationLevel.SECURITY_FOCUSED]:
self.logger.info("๋ณด์ ๊ฒ์ฆ ์คํ ์ค...")
results['security'] = self._run_security_tests(endpoint)
# AI ๊ธฐ๋ฐ ๊ฒ์ฆ
if level == ValidationLevel.COMPREHENSIVE:
self.logger.info("AI ๊ฒ์ฆ ์คํ ์ค...")
results['ai'] = self._run_ai_tests(endpoint)
duration = time.time() - start_time
# ๊ฒฐ๊ณผ ์ง๊ณ
return self._aggregate_results(endpoint, method, results, duration)
def _run_functional_tests(self, endpoint: str, method: str) -> List[Dict]:
"""๊ธฐ๋ฅ ํ
์คํธ ์คํ"""
tests = []
try:
# ์ํ ์ฝ๋ ๊ฒ์ฆ
response = self.functional_validator.get(endpoint)
tests.append({
'name': '์ํ ์ฝ๋',
'passed': response.status_code == 200,
'message': f'์ํ ์ฝ๋: {response.status_code}'
})
# JSON ํ์ ๊ฒ์ฆ
try:
data = response.json()
tests.append({
'name': 'JSON ํ์',
'passed': True,
'message': '์ ํจํ JSON'
})
# ์คํค๋ง ๊ฒ์ฆ
if 'schema' in self.config.get('endpoints', {}).get(endpoint, {}):
schema = self.config['endpoints'][endpoint]['schema']
is_valid = self.functional_validator.validate_with_schema(data, schema)
tests.append({
'name': '์คํค๋ง ๊ฒ์ฆ',
'passed': is_valid,
'message': '์คํค๋ง ์ผ์น' if is_valid else '์คํค๋ง ๋ถ์ผ์น'
})
except Exception as e:
tests.append({
'name': 'JSON ํ์',
'passed': False,
'message': f'JSON ํ์ฑ ์คํจ: {str(e)}'
})
except Exception as e:
tests.append({
'name': '๊ธฐ๋ฅ ํ
์คํธ',
'passed': False,
'message': f'์๋ฌ: {str(e)}'
})
return tests
def _run_performance_tests(self, endpoint: str) -> List[Dict]:
"""์ฑ๋ฅ ํ
์คํธ ์คํ"""
tests = []
try:
# ์๋ต ์๊ฐ ์ธก์
stats = self.performance_validator.measure_response_time(endpoint, iterations=10)
max_response_time = self.config.get('performance', {}).get('max_response_time', 1.0)
tests.append({
'name': 'ํ๊ท ์๋ต ์๊ฐ',
'passed': stats['avg'] < max_response_time,
'message': f"{stats['avg']:.3f}์ด (๊ธฐ์ค: {max_response_time}์ด)"
})
tests.append({
'name': '์ต๋ ์๋ต ์๊ฐ',
'passed': stats['max'] < max_response_time * 2,
'message': f"{stats['max']:.3f}์ด"
})
except Exception as e:
tests.append({
'name': '์ฑ๋ฅ ํ
์คํธ',
'passed': False,
'message': f'์๋ฌ: {str(e)}'
})
return tests
def _run_security_tests(self, endpoint: str) -> List[Dict]:
"""๋ณด์ ํ
์คํธ ์คํ"""
tests = []
try:
# ์ธ์ฆ ํ
์คํธ
self.security_validator.test_authentication_required(endpoint)
tests.append({
'name': '์ธ์ฆ ํ์',
'passed': True,
'message': '์ธ์ฆ ์ฒดํฌ ์ ์'
})
# SQL ์ธ์ ์
ํ
์คํธ
if 'id' in endpoint or 'search' in endpoint:
self.security_validator.test_sql_injection(endpoint, 'id')
tests.append({
'name': 'SQL ์ธ์ ์
๋ฐฉ์ด',
'passed': True,
'message': 'SQL ์ธ์ ์
์ฐจ๋จ๋จ'
})
except Exception as e:
tests.append({
'name': '๋ณด์ ํ
์คํธ',
'passed': False,
'message': f'์๋ฌ: {str(e)}'
})
return tests
def _run_ai_tests(self, endpoint: str) -> List[Dict]:
"""AI ๊ธฐ๋ฐ ํ
์คํธ ์คํ"""
tests = []
try:
# ์ด์ ํ์ง
result = self.ai_validator.validate_with_ai(endpoint)
tests.append({
'name': '์ด์ ํ์ง',
'passed': not result['anomaly_detection']['is_anomaly'],
'message': result['anomaly_detection']['message']
})
except Exception as e:
tests.append({
'name': 'AI ํ
์คํธ',
'passed': False,
'message': f'์๋ฌ: {str(e)}'
})
return tests
def _aggregate_results(self,
endpoint: str,
method: str,
results: Dict,
duration: float) -> ValidationResult:
"""๊ฒฐ๊ณผ ์ง๊ณ"""
all_tests = []
for category, tests in results.items():
all_tests.extend(tests)
total = len(all_tests)
passed = sum(1 for t in all_tests if t['passed'])
failed = total - passed
errors = [t['message'] for t in all_tests if not t['passed']]
warnings = []
return ValidationResult(
endpoint=endpoint,
method=method,
passed=failed == 0,
total_checks=total,
passed_checks=passed,
failed_checks=failed,
warnings=warnings,
errors=errors,
duration=duration,
details=results
)
def validate_all_endpoints(self, level: ValidationLevel = ValidationLevel.STANDARD) -> List[ValidationResult]:
"""๋ชจ๋ ์๋ํฌ์ธํธ ๊ฒ์ฆ"""
results = []
endpoints = self.config.get('endpoints', {})
for endpoint, config in endpoints.items():
method = config.get('method', 'GET')
self.logger.info(f"\n{'='*60}")
self.logger.info(f"๊ฒ์ฆ ์ค: {method} {endpoint}")
self.logger.info(f"{'='*60}")
result = self.validate_endpoint(endpoint, method, level)
results.append(result)
# ๊ฒฐ๊ณผ ์ถ๋ ฅ
status = "โ
PASS" if result.passed else "โ FAIL"
self.logger.info(f"{status} | {result.passed_checks}/{result.total_checks} ํต๊ณผ")
if result.errors:
self.logger.error("์๋ฌ:")
for error in result.errors:
self.logger.error(f" - {error}")
return results
def generate_report(self, results: List[ValidationResult], format: str = 'html'):
"""๋ฆฌํฌํธ ์์ฑ"""
if format == 'html':
from reporters.html_reporter import HTMLReporter
reporter = HTMLReporter()
elif format == 'json':
from reporters.json_reporter import JSONReporter
reporter = JSONReporter()
else:
raise ValueError(f"์ง์ํ์ง ์๋ ํ์: {format}")
report_path = reporter.generate(results)
self.logger.info(f"๋ฆฌํฌํธ ์์ฑ ์๋ฃ: {report_path}")
return report_path
๋ช ๋ น์ค์์ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋๋ก CLI๋ฅผ ๋ง๋ค์ด๋ณด์:
import click
from src.validators.engine import APIValidationEngine, ValidationLevel
@click.group()
def cli():
"""API ๊ฒ์ฆ ๋๊ตฌ"""
pass
@cli.command()
@click.option('--config', '-c', default='config/default.yaml', help='์ค์ ํ์ผ ๊ฒฝ๋ก')
@click.option('--endpoint', '-e', help='๊ฒ์ฆํ ์๋ํฌ์ธํธ')
@click.option('--level', '-l',
type=click.Choice(['basic', 'standard', 'comprehensive', 'security']),
default='standard',
help='๊ฒ์ฆ ์์ค')
@click.option('--report', '-r',
type=click.Choice(['html', 'json']),
default='html',
help='๋ฆฌํฌํธ ํ์')
def validate(config, endpoint, level, report):
"""API ๊ฒ์ฆ ์คํ"""
engine = APIValidationEngine(config)
level_map = {
'basic': ValidationLevel.BASIC,
'standard': ValidationLevel.STANDARD,
'comprehensive': ValidationLevel.COMPREHENSIVE,
'security': ValidationLevel.SECURITY_FOCUSED
}
if endpoint:
# ๋จ์ผ ์๋ํฌ์ธํธ ๊ฒ์ฆ
result = engine.validate_endpoint(endpoint, level=level_map[level])
results = [result]
else:
# ๋ชจ๋ ์๋ํฌ์ธํธ ๊ฒ์ฆ
results = engine.validate_all_endpoints(level=level_map[level])
# ๋ฆฌํฌํธ ์์ฑ
report_path = engine.generate_report(results, format=report)
click.echo(f"\nโ
๊ฒ์ฆ ์๋ฃ!")
click.echo(f"๐ ๋ฆฌํฌํธ: {report_path}")
# ์คํจํ ํ
์คํธ๊ฐ ์์ผ๋ฉด exit code 1
if any(not r.passed for r in results):
exit(1)
@cli.command()
@click.option('--config', '-c', default='config/default.yaml')
@click.option('--endpoint', '-e', required=True)
@click.option('--iterations', '-i', default=100, help='ํ์ต ๋ฐ๋ณต ํ์')
def train(config, endpoint, iterations):
"""AI ๋ชจ๋ธ ํ์ต"""
engine = APIValidationEngine(config)
click.echo(f"๐ AI ๋ชจ๋ธ ํ์ต ์์...")
click.echo(f"์๋ํฌ์ธํธ: {endpoint}")
click.echo(f"๋ฐ๋ณต ํ์: {iterations}")
engine.ai_validator.train_anomaly_detector(endpoint, iterations)
click.echo("โ
ํ์ต ์๋ฃ!")
@cli.command()
@click.option('--config', '-c', default='config/default.yaml')
@click.option('--watch', '-w', is_flag=True, help='๋ณ๊ฒฝ ๊ฐ์ง ๋ชจ๋')
@click.option('--interval', '-i', default=60, help='์ฒดํฌ ๊ฐ๊ฒฉ (์ด)')
def monitor(config, watch, interval):
"""์ง์์ ๋ชจ๋ํฐ๋ง"""
import time
engine = APIValidationEngine(config)
click.echo("๐ ๋ชจ๋ํฐ๋ง ์์...")
while True:
results = engine.validate_all_endpoints(level=ValidationLevel.STANDARD)
failed = [r for r in results if not r.passed]
if failed:
click.echo(f"โ ๏ธ {len(failed)}๊ฐ ์๋ํฌ์ธํธ ์คํจ!")
for r in failed:
click.echo(f" - {r.endpoint}: {len(r.errors)}๊ฐ ์๋ฌ")
else:
click.echo(f"โ
๋ชจ๋ ์๋ํฌ์ธํธ ์ ์ ({len(results)}๊ฐ)")
if not watch:
break
click.echo(f"๐ค {interval}์ด ๋๊ธฐ ์ค...")
time.sleep(interval)
if __name__ == '__main__':
cli()
์ด์ ์ด๋ ๊ฒ ์ฌ์ฉํ ์ ์์ด:
# ๊ธฐ๋ณธ ๊ฒ์ฆ
python -m src.cli validate
# ํน์ ์๋ํฌ์ธํธ๋ง ๊ฒ์ฆ
python -m src.cli validate -e /api/users/1
# ํฌ๊ด์ ๊ฒ์ฆ
python -m src.cli validate -l comprehensive
# AI ๋ชจ๋ธ ํ์ต
python -m src.cli train -e /api/users/1 -i 200
# ์ง์์ ๋ชจ๋ํฐ๋ง
python -m src.cli monitor --watch --interval 300
๐ฏ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ ๋ง ๊ธด ์ฌ์ ์ด์์ง? ๐ ํ์ง๋ง ์ด์ ๋๋ Python์ผ๋ก ์ ๋ฌธ์ ์ธ API ์๋ต ์๋ ๊ฒ์ฆ๊ธฐ๋ฅผ ๋ง๋ค ์ ์๋ ๋ชจ๋ ์ง์์ ๊ฐ์ถ๊ฒ ๋์ด!
๐ ํต์ฌ ์์ฝ
1. ๊ธฐ๋ณธ ๊ฒ์ฆ
์ํ ์ฝ๋, JSON ํ์, ํ์ ํ๋, ๋ฐ์ดํฐ ํ์
๋ฑ ๊ธฐ๋ณธ์ ์ธ ๊ฒ์ฆ๋ถํฐ ์์ํด.
2. ๊ณ ๊ธ ๊ธฐ๋ฒ
JSON Schema, Pydantic, ์ฑ๋ฅ ํ
์คํธ ๋ฑ์ผ๋ก ๊ฒ์ฆ ์์ค์ ๋์ฌ.
3. ์๋ํ
pytest์ CI/CD๋ฅผ ํตํฉํด์ ์์ ์๋ํ๋ ํ
์คํธ ํ์ดํ๋ผ์ธ์ ๊ตฌ์ถํด.
4. AI ํ์ฉ
๋จธ์ ๋ฌ๋์ผ๋ก ์ด์ ํ์งํ๊ณ ์๋์ผ๋ก ํ
์คํธ ์ผ์ด์ค๋ฅผ ์์ฑํด.
5. ๋ณด์
OWASP Top 10์ ๊ธฐ๋ฐ์ผ๋ก ๋ณด์ ์ทจ์ฝ์ ์ ์ฒดํฌํด.
6. ๋ชจ๋ํฐ๋ง
Grafana, Prometheus๋ก ์ค์๊ฐ ๋ชจ๋ํฐ๋งํ๊ณ ์๋ฆผ์ ๋ฐ์.
์ด์ ๋ญ ํด์ผ ํ ๊น? ๐ค
์ค์ ํ๋ก์ ํธ ์ค์ API์ ์ ์ฉํด๋ณด๊ธฐ
์คํ์์ค GitHub์ ๊ณต๊ฐํ๊ณ ํผ๋๋ฐฑ ๋ฐ๊ธฐ
ํ์ฅ GraphQL, gRPC ์ง์ ์ถ๊ฐํ๊ธฐ
์ต์ ํ ์ฑ๋ฅ ๊ฐ์ ๊ณผ ๋ฆฌํฉํ ๋ง
๋ฌธ์ํ ์ฌ์ฉ ๊ฐ์ด๋์ API ๋ฌธ์ ์์ฑ
API ํ ์คํธ ์๋ํ๋ ์ด๊ธฐ ํฌ์ ์๊ฐ์ด ์ข ๊ฑธ๋ฆฌ์ง๋ง, ์ฅ๊ธฐ์ ์ผ๋ก๋ ์์ฒญ๋ ์๊ฐ๊ณผ ๋น์ฉ์ ์ ์ฝํด์ค. ๋ฒ๊ทธ๋ฅผ ์กฐ๊ธฐ์ ๋ฐ๊ฒฌํ๊ณ , ๊ฐ๋ฐ์์ ์์ ๊ฐ์ ๋์ด๊ณ , ์ฝ๋ ํ์ง์ ํฅ์์ํค๋ ํจ๊ณผ๊ฐ ์๊ฑฐ๋ .
ํน์ ๋งํ๋ ๋ถ๋ถ์ด ์๊ฑฐ๋ ๋ ๊น์ด ๋ฐฐ์ฐ๊ณ ์ถ๋ค๋ฉด, ์ฃผ์ ํ์ง ๋ง๊ณ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๊ฑฐ๋ ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ์. ํผ์ ๊ณ ๋ฏผํ๋ ๊ฒ๋ณด๋ค ํจ์ฌ ๋น ๋ฅด๊ฒ ์ฑ์ฅํ ์ ์์ด! ๐ช
์, ์ด์ ๋๋ง์ ๋ฉ์ง API ๊ฒ์ฆ๊ธฐ๋ฅผ ๋ง๋ค์ด๋ณผ ์๊ฐ์ด์ผ! ํ์ดํ ! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.