์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ Python์œผ๋กœ ์„ธ์ƒ๊ณผ ์—ฐ๊ฒฐํ•˜๊ธฐ: requests์™€ ์›น API ํด๋ผ์ด์–ธํŠธ๋กœ ์™ธ๋ถ€ ์„œ๋น„์Šค ์™„์ „ ์ •๋ณต!

๐Ÿ Python์œผ๋กœ ์„ธ์ƒ๊ณผ ์—ฐ๊ฒฐํ•˜๊ธฐ: requests์™€ ์›น API ํด๋ผ์ด์–ธํŠธ๋กœ ์™ธ๋ถ€ ์„œ๋น„์Šค ์™„์ „ ์ •๋ณต!

ํ”„๋กœ๊ทธ๋žจ๊ฐœ๋ฐœ โ€บ Python | ์‹ค์ „ API ์—ฐ๋™ ๊ฐ€์ด๋“œ ๐Ÿš€

์•ผ, ์†”์งํžˆ ๋งํ•ด๋ด. ํŒŒ์ด์ฌ ๋ฐฐ์šฐ๋ฉด์„œ "์ด๊ฑธ๋กœ ๋ญ˜ ๋งŒ๋“ค ์ˆ˜ ์žˆ์ง€?" ๋ผ๋Š” ์ƒ๊ฐ ํ•œ ๋ฒˆ์ฏค ํ•ด๋ดค์ง€? ๐Ÿค”
๊ทธ ๋‹ต ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ ์™ธ๋ถ€ ์„œ๋น„์Šค ์—ฐ๋™์ด์•ผ. ๋‚ ์”จ ์•ฑ, ํ™˜์œจ ๊ณ„์‚ฐ๊ธฐ, ์œ ํŠœ๋ธŒ ๋ฐ์ดํ„ฐ ๋ถ„์„, ์นด์นด์˜ค ๋กœ๊ทธ์ธ... ์ด ๋ชจ๋“  ๊ฒŒ ๋‹ค ์›น API๋ฅผ ํ†ตํ•ด์„œ ์ด๋ฃจ์–ด์ง€๊ฑฐ๋“ .

์˜ค๋Š˜์€ ํŒŒ์ด์ฌ์˜ ๊ตญ๋ฏผ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ requests๋ฅผ ๋“ค๊ณ , ์‹ค์ œ๋กœ ์™ธ๋ถ€ ์„œ๋น„์Šค๋“ค๊ณผ ์–ด๋–ป๊ฒŒ ๋Œ€ํ™”ํ•˜๋Š”์ง€ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ํŒŒํ—ค์ณ๋ณผ ๊ฑฐ์•ผ. ์ฝ”๋“œ ํ•œ ์ค„ ํ•œ ์ค„ ๊ฐ™์ด ๋œฏ์–ด๋ณด๋ฉด์„œ, ์ง„์งœ ์‹ค๋ฌด์—์„œ ์“ฐ์ด๋Š” ํŒจํ„ด๊นŒ์ง€ ๋‹ค ์•Œ๋ ค์ค„๊ฒŒ. ์ค€๋น„๋์–ด? ๊ทธ๋Ÿผ ๊ณ ๊ณ ! ๐Ÿ”ฅ

๐Ÿ Python requests ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ HTTP ์š”์ฒญ JSON ์‘๋‹ต ๐ŸŒ ์›น API ์„œ๋ฒ„ GET / POST / PUT DELETE / PATCH REST API ์—”๋“œํฌ์ธํŠธ โ˜€๏ธ ๋‚ ์”จ API OpenWeather ๐Ÿ—บ๏ธ ์ง€๋„ API ์นด์นด์˜ค๋งต ๋“ฑ ๐Ÿ’ณ ๊ฒฐ์ œ API ํ† ์ŠคํŽ˜์ด ๋“ฑ ๐Ÿ“ฑ SNS API ์นด์นด์˜คยท๋„ค์ด๋ฒ„ Python requests โ†’ HTTP ํ†ต์‹  โ†’ ๋‹ค์–‘ํ•œ ์™ธ๋ถ€ ์„œ๋น„์Šค ์—ฐ๋™

๐ŸŒ 1. ์›น API๊ฐ€ ๋ญ”์ง€๋ถ€ํ„ฐ ์ œ๋Œ€๋กœ ์•Œ๊ณ  ๊ฐ€์ž

API๋Š” Application Programming Interface์˜ ์•ฝ์ž์•ผ. ์–ด๋ ต๊ฒŒ ๋“ค๋ฆฌ์ง€? ๊ทธ๋ƒฅ ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด "ํ”„๋กœ๊ทธ๋žจ๋ผ๋ฆฌ ๋Œ€ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•"์ด์•ผ.

์‹๋‹น์œผ๋กœ ๋น„์œ ํ•˜๋ฉด ์ด๋ž˜. ๋„ˆ(ํด๋ผ์ด์–ธํŠธ)๊ฐ€ ์‹๋‹น(์„œ๋ฒ„)์— ๊ฐ€์„œ ๋ฉ”๋‰ดํŒ(API ๋ฌธ์„œ)์„ ๋ณด๊ณ , ์›จ์ดํ„ฐ(API)ํ•œํ…Œ "๊น€์น˜์ฐŒ๊ฐœ ์ฃผ์„ธ์š”"(์š”์ฒญ)๋ผ๊ณ  ํ•˜๋ฉด, ์ฃผ๋ฐฉ์—์„œ ์š”๋ฆฌํ•ด์„œ ์Œ์‹(์‘๋‹ต)์„ ๊ฐ€์ ธ๋‹ค์ฃผ๋Š” ๊ฑฐ์•ผ. ๋„ˆ๋Š” ์ฃผ๋ฐฉ์—์„œ ์–ด๋–ป๊ฒŒ ์š”๋ฆฌํ•˜๋Š”์ง€ ๋ชฐ๋ผ๋„ ๋ผ. ๊ทธ๋ƒฅ ์ฃผ๋ฌธ๋งŒ ํ•˜๋ฉด ๋˜๋Š” ๊ฑฐ์ง€! ๐Ÿฒ

๐Ÿ“ก REST API์˜ ํ•ต์‹ฌ ๊ฐœ๋…

์š”์ฆ˜ ๋Œ€๋ถ€๋ถ„์˜ ์›น API๋Š” REST(Representational State Transfer) ๋ฐฉ์‹์„ ์จ. REST API๋Š” HTTP ํ”„๋กœํ† ์ฝœ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๊ณ , ์•„๋ž˜ 4๊ฐ€์ง€ ๋ฉ”์„œ๋“œ๋ฅผ ์ฃผ๋กœ ์‚ฌ์šฉํ•ด:

HTTP ๋ฉ”์„œ๋“œ ์—ญํ•  ์˜ˆ์‹œ
GET ๋ฐ์ดํ„ฐ ์กฐํšŒ ๋‚ ์”จ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
POST ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์ƒˆ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ
PUT/PATCH ๋ฐ์ดํ„ฐ ์ˆ˜์ • ํ”„๋กœํ•„ ์ •๋ณด ์—…๋ฐ์ดํŠธ
DELETE ๋ฐ์ดํ„ฐ ์‚ญ์ œ ๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ

๊ทธ๋ฆฌ๊ณ  ์„œ๋ฒ„๋Š” ์‘๋‹ตํ•  ๋•Œ HTTP ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ๊ฐ™์ด ๋ณด๋‚ด์ค˜. ์ด๊ฒŒ ๋˜๊ฒŒ ์ค‘์š”ํ•ด:

์ƒํƒœ ์ฝ”๋“œ ์˜๋ฏธ
200 OK ์„ฑ๊ณต! ์š”์ฒญํ•œ ๋Œ€๋กœ ์ž˜ ๋์–ด โœ…
201 Created ์ƒ์„ฑ ์„ฑ๊ณต! ์ƒˆ ๋ฆฌ์†Œ์Šค๊ฐ€ ๋งŒ๋“ค์–ด์กŒ์–ด ๐Ÿ†•
400 Bad Request ์š”์ฒญ์ด ์ž˜๋ชป๋์–ด. ํŒŒ๋ผ๋ฏธํ„ฐ ํ™•์ธํ•ด๋ด ๐Ÿค”
401 Unauthorized ์ธ์ฆ์ด ํ•„์š”ํ•ด. API ํ‚ค ํ™•์ธํ•ด๋ด ๐Ÿ”‘
403 Forbidden ๊ถŒํ•œ ์—†์Œ. ์ ‘๊ทผ ๋ถˆ๊ฐ€ ๐Ÿšซ
404 Not Found ๊ทธ๋Ÿฐ ๊ฑฐ ์—†์–ด. URL ํ™•์ธํ•ด๋ด ๐Ÿ”
429 Too Many Requests ์š”์ฒญ์ด ๋„ˆ๋ฌด ๋งŽ์•„. ์ž ๊น ์‰ฌ์–ด โณ
500 Internal Server Error ์„œ๋ฒ„ ๋ฌธ์ œ์•ผ. ๋‚ด ์ž˜๋ชป ์•„๋‹˜ ๐Ÿ˜…

๐Ÿ“ฆ 2. requests ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜์™€ ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

ํŒŒ์ด์ฌ ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—๋„ urllib์ด ์žˆ๊ธด ํ•œ๋ฐ, ์†”์งํžˆ ์“ฐ๊ธฐ ๋ถˆํŽธํ•ด. requests๋Š” "HTTP for Humans"๋ผ๋Š” ์Šฌ๋กœ๊ฑด์ฒ˜๋Ÿผ ์‚ฌ๋žŒ์ด ์“ฐ๊ธฐ ํŽธํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. ํŒŒ์ด์ฌ ์ƒํƒœ๊ณ„์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ๋‹ค์šด๋กœ๋“œ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ค‘ ํ•˜๋‚˜๊ณ , ์‹ค๋ฌด์—์„œ๋„ ๊ฑฐ์˜ ํ‘œ์ค€์ฒ˜๋Ÿผ ์“ฐ์—ฌ.

๐Ÿ”ง ์„ค์น˜
pip install requests
๐Ÿš€ ์ฒซ ๋ฒˆ์งธ API ํ˜ธ์ถœ - GET ์š”์ฒญ

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ GET ์š”์ฒญ๋ถ€ํ„ฐ ํ•ด๋ณด์ž. JSONPlaceholder๋ผ๋Š” ๋ฌด๋ฃŒ ํ…Œ์ŠคํŠธ API๋ฅผ ์จ๋ณผ๊ฒŒ:

import requests

# GET ์š”์ฒญ ๋ณด๋‚ด๊ธฐ
response = requests.get('https://jsonplaceholder.typicode.com/posts/1')

# ์ƒํƒœ ์ฝ”๋“œ ํ™•์ธ
print(response.status_code)  # 200

# JSON ์‘๋‹ต ํŒŒ์‹ฑ
data = response.json()
print(data)
# {'userId': 1, 'id': 1, 'title': '...', 'body': '...'}

๋”ฑ 3์ค„์ด์•ผ! requests.get()์œผ๋กœ ์š”์ฒญ ๋ณด๋‚ด๊ณ , .json()์œผ๋กœ ์‘๋‹ต ํŒŒ์‹ฑ. ์ด๊ฒŒ ๋์ด์•ผ. ์ง„์งœ ๊ฐ„๋‹จํ•˜์ง€? ๐Ÿ˜„

๐Ÿ” Response ๊ฐ์ฒด ํŒŒํ—ค์น˜๊ธฐ

์‘๋‹ต ๊ฐ์ฒด์—๋Š” ๋‹ค์–‘ํ•œ ์ •๋ณด๊ฐ€ ๋‹ด๊ฒจ ์žˆ์–ด:

response = requests.get('https://jsonplaceholder.typicode.com/posts/1')

print(response.status_code)    # 200 (์ƒํƒœ ์ฝ”๋“œ)
print(response.headers)        # ์‘๋‹ต ํ—ค๋” ๋”•์…”๋„ˆ๋ฆฌ
print(response.text)           # ์‘๋‹ต ๋ณธ๋ฌธ (๋ฌธ์ž์—ด)
print(response.json())         # JSON ํŒŒ์‹ฑ ๊ฒฐ๊ณผ (๋”•์…”๋„ˆ๋ฆฌ)
print(response.content)        # ์‘๋‹ต ๋ณธ๋ฌธ (๋ฐ”์ดํŠธ)
print(response.url)            # ์ตœ์ข… ์š”์ฒญ URL
print(response.elapsed)        # ์‘๋‹ต ์‹œ๊ฐ„
print(response.encoding)       # ์ธ์ฝ”๋”ฉ ๋ฐฉ์‹

# ์„ฑ๊ณต ์—ฌ๋ถ€ ํ™•์ธ (200~299๋ฉด True)
print(response.ok)             # True
๐Ÿ’ก ํŒ! response.raise_for_status()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด 4xx, 5xx ์—๋Ÿฌ ์‹œ ์ž๋™์œผ๋กœ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œ์ผœ์ค˜. ์—๋Ÿฌ ์ฒ˜๋ฆฌํ•  ๋•Œ ์ง„์งœ ์œ ์šฉํ•ด!

๐ŸŽฏ 3. ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ, ํ—ค๋”, ์ธ์ฆ - ์‹ค์ „ ์š”์ฒญ ๋งŒ๋“ค๊ธฐ
๐Ÿ“‹ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ (Query Parameters)

URL์— ?key=value&key2=value2 ํ˜•ํƒœ๋กœ ๋ถ™์ด๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ์•ผ. ๊ฒ€์ƒ‰ ์กฐ๊ฑด์ด๋‚˜ ํ•„ํ„ฐ๋ง์— ๋งŽ์ด ์จ. requests์—์„œ๋Š” ๋”•์…”๋„ˆ๋ฆฌ๋กœ ๊น”๋”ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด:

import requests

# ๋ฐฉ๋ฒ• 1: URL์— ์ง์ ‘ ๋ถ™์ด๊ธฐ (๋น„์ถ”์ฒœ)
response = requests.get('https://api.example.com/search?q=python&page=1&limit=10')

# ๋ฐฉ๋ฒ• 2: params ๋”•์…”๋„ˆ๋ฆฌ ์‚ฌ์šฉ (์ถ”์ฒœ! โœ…)
params = {
    'q': 'python',
    'page': 1,
    'limit': 10,
    'sort': 'latest'
}
response = requests.get('https://api.example.com/search', params=params)

# ์‹ค์ œ ์š”์ฒญ URL ํ™•์ธ
print(response.url)
# https://api.example.com/search?q=python&page=1&limit=10&sort=latest

params ๋”•์…”๋„ˆ๋ฆฌ๋ฅผ ์“ฐ๋ฉด URL ์ธ์ฝ”๋”ฉ๋„ ์ž๋™์œผ๋กœ ํ•ด์ค˜์„œ ํŠน์ˆ˜๋ฌธ์ž ๊ฑฑ์ • ์—†์–ด. ์ง„์งœ ํŽธ๋ฆฌํ•˜์ง€! ๐Ÿ™Œ

๐Ÿ“จ ์š”์ฒญ ํ—ค๋” (Request Headers)

ํ—ค๋”๋Š” ์š”์ฒญ์— ๋Œ€ํ•œ ๋ฉ”ํƒ€ ์ •๋ณด๋ฅผ ๋‹ด์•„. API ํ‚ค, ์ธ์ฆ ํ† ํฐ, ์ฝ˜ํ…์ธ  ํƒ€์ž… ๋“ฑ์„ ์—ฌ๊ธฐ์— ๋„ฃ์–ด:

import requests

headers = {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'User-Agent': 'MyApp/1.0',
    'Authorization': 'Bearer your_token_here'
}

response = requests.get(
    'https://api.example.com/data',
    headers=headers
)

print(response.json())
๐Ÿ”‘ ์ธ์ฆ ๋ฐฉ์‹๋“ค

API ์ธ์ฆ์€ ํฌ๊ฒŒ 3๊ฐ€์ง€ ๋ฐฉ์‹์ด ์žˆ์–ด:

import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth

# 1. Basic ์ธ์ฆ (์•„์ด๋””/๋น„๋ฐ€๋ฒˆํ˜ธ)
response = requests.get(
    'https://api.example.com/data',
    auth=HTTPBasicAuth('username', 'password')
)
# ๋˜๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ
response = requests.get(
    'https://api.example.com/data',
    auth=('username', 'password')
)

# 2. API Key ์ธ์ฆ (ํ—ค๋”์— ๋„ฃ๊ธฐ)
headers = {'X-API-Key': 'your_api_key_here'}
response = requests.get('https://api.example.com/data', headers=headers)

# 3. Bearer Token ์ธ์ฆ (OAuth 2.0)
headers = {'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...'}
response = requests.get('https://api.example.com/data', headers=headers)
โš ๏ธ ์ฃผ์˜! API ํ‚ค๋‚˜ ํ† ํฐ์„ ์ฝ”๋“œ์— ์ง์ ‘ ํ•˜๋“œ์ฝ”๋”ฉํ•˜๋ฉด ์ ˆ๋Œ€ ์•ˆ ๋ผ! ๊นƒํ—ˆ๋ธŒ์— ์˜ฌ๋ ธ๋‹ค๊ฐ€ ํ‚ค ์œ ์ถœ๋˜๋ฉด ์ง„์งœ ํฐ์ผ ๋‚˜. ๋ฐ˜๋“œ์‹œ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋‚˜ ๋ณ„๋„ ์„ค์ • ํŒŒ์ผ๋กœ ๊ด€๋ฆฌํ•ด์•ผ ํ•ด.
import os
import requests

# ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ API ํ‚ค ๊ฐ€์ ธ์˜ค๊ธฐ (์•ˆ์ „ํ•œ ๋ฐฉ๋ฒ• โœ…)
api_key = os.environ.get('MY_API_KEY')

headers = {'Authorization': f'Bearer {api_key}'}
response = requests.get('https://api.example.com/data', headers=headers)

๐Ÿ“ค 4. POST, PUT, DELETE - ๋ฐ์ดํ„ฐ ๋ณด๋‚ด๊ธฐ
โœ๏ธ POST ์š”์ฒญ - ๋ฐ์ดํ„ฐ ์ƒ์„ฑ

POST ์š”์ฒญ์€ ์„œ๋ฒ„์— ์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ ๋งŒ๋“ค ๋•Œ ์จ. JSON ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด๋Š” ๊ฒŒ ์š”์ฆ˜ ํ‘œ์ค€์ด์•ผ:

import requests

# JSON ๋ฐ์ดํ„ฐ ๋ณด๋‚ด๊ธฐ
url = 'https://jsonplaceholder.typicode.com/posts'

payload = {
    'title': 'ํŒŒ์ด์ฌ์œผ๋กœ API ์—ฐ๋™ํ•˜๊ธฐ',
    'body': '์˜ค๋Š˜ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ดค์–ด์š”!',
    'userId': 1
}

# json ํŒŒ๋ผ๋ฏธํ„ฐ ์‚ฌ์šฉ (Content-Type ์ž๋™ ์„ค์ •)
response = requests.post(url, json=payload)

print(response.status_code)  # 201 Created
print(response.json())
# {'id': 101, 'title': 'ํŒŒ์ด์ฌ์œผ๋กœ API ์—ฐ๋™ํ•˜๊ธฐ', ...}
๐Ÿ’ก json=payload๋ฅผ ์“ฐ๋ฉด ์ž๋™์œผ๋กœ JSON ์ง๋ ฌํ™” + Content-Type: application/json ํ—ค๋” ์„ค์ •๊นŒ์ง€ ํ•ด์ค˜. data=json.dumps(payload)๋ณด๋‹ค ํ›จ์”ฌ ํŽธํ•ด!
๐Ÿ“ ํผ ๋ฐ์ดํ„ฐ ๋ณด๋‚ด๊ธฐ
import requests

# application/x-www-form-urlencoded ํ˜•์‹
form_data = {
    'username': 'pythonlover',
    'password': 'secret123'
}
response = requests.post('https://api.example.com/login', data=form_data)

# multipart/form-data (ํŒŒ์ผ ์—…๋กœ๋“œ)
files = {
    'profile_image': ('avatar.png', open('avatar.png', 'rb'), 'image/png')
}
response = requests.post('https://api.example.com/upload', files=files)
๐Ÿ”„ PUT/PATCH ์š”์ฒญ - ๋ฐ์ดํ„ฐ ์ˆ˜์ •
import requests

# PUT: ์ „์ฒด ๋ฆฌ์†Œ์Šค ๊ต์ฒด
response = requests.put(
    'https://jsonplaceholder.typicode.com/posts/1',
    json={
        'id': 1,
        'title': '์ˆ˜์ •๋œ ์ œ๋ชฉ',
        'body': '์ˆ˜์ •๋œ ๋‚ด์šฉ',
        'userId': 1
    }
)

# PATCH: ์ผ๋ถ€ ํ•„๋“œ๋งŒ ์ˆ˜์ •
response = requests.patch(
    'https://jsonplaceholder.typicode.com/posts/1',
    json={'title': '์ œ๋ชฉ๋งŒ ์ˆ˜์ •'}
)

print(response.status_code)  # 200
print(response.json())
๐Ÿ—‘๏ธ DELETE ์š”์ฒญ
import requests

response = requests.delete('https://jsonplaceholder.typicode.com/posts/1')
print(response.status_code)  # 200 ๋˜๋Š” 204 No Content

๐Ÿ›ก๏ธ 5. ์—๋Ÿฌ ์ฒ˜๋ฆฌ์™€ ์˜ˆ์™ธ ๊ด€๋ฆฌ - ์‹ค๋ฌด์—์„œ ํ•„์ˆ˜!

API ํ˜ธ์ถœ์ด ํ•ญ์ƒ ์„ฑ๊ณตํ•˜๋ฉด ์–ผ๋งˆ๋‚˜ ์ข‹๊ฒ ์–ด. ๊ทผ๋ฐ ํ˜„์‹ค์€ ๋‹ฌ๋ผ. ๋„คํŠธ์›Œํฌ ๋Š๊ธฐ๊ณ , ์„œ๋ฒ„ ๋‹ค์šด๋˜๊ณ , ์ธ์ฆ ๋งŒ๋ฃŒ๋˜๊ณ ... ์ด๋Ÿฐ ์ƒํ™ฉ์„ ์šฐ์•„ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ์ง„์งœ ์‹ค๋ ฅ์ด์•ผ! ๐Ÿ’ช

๐ŸŽฏ ๊ธฐ๋ณธ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํŒจํ„ด
import requests
from requests.exceptions import (
    ConnectionError,
    Timeout,
    HTTPError,
    RequestException
)

def fetch_data(url, params=None):
    try:
        response = requests.get(
            url,
            params=params,
            timeout=10  # 10์ดˆ ํƒ€์ž„์•„์›ƒ ์„ค์ •
        )
        
        # HTTP ์—๋Ÿฌ (4xx, 5xx) ๋ฐœ์ƒ ์‹œ ์˜ˆ์™ธ raise
        response.raise_for_status()
        
        return response.json()
    
    except Timeout:
        print("โฐ ์š”์ฒญ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋์–ด์š”. ์„œ๋ฒ„๊ฐ€ ๋А๋ฆฐ๊ฐ€๋ด์š”.")
        return None
    
    except ConnectionError:
        print("๐Ÿ”Œ ์—ฐ๊ฒฐ ์‹คํŒจ! ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.")
        return None
    
    except HTTPError as e:
        status_code = e.response.status_code
        if status_code == 401:
            print("๐Ÿ”‘ ์ธ์ฆ ์‹คํŒจ! API ํ‚ค๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.")
        elif status_code == 403:
            print("๐Ÿšซ ์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์–ด์š”.")
        elif status_code == 404:
            print("๐Ÿ” ์š”์ฒญํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์–ด์š”.")
        elif status_code == 429:
            print("โณ ์š”์ฒญ์ด ๋„ˆ๋ฌด ๋งŽ์•„์š”. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.")
        elif status_code >= 500:
            print(f"๐Ÿ’ฅ ์„œ๋ฒ„ ์—๋Ÿฌ ๋ฐœ์ƒ! ์ƒํƒœ ์ฝ”๋“œ: {status_code}")
        return None
    
    except RequestException as e:
        print(f"โŒ ์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜: {e}")
        return None

# ์‚ฌ์šฉ
data = fetch_data('https://jsonplaceholder.typicode.com/posts/1')
if data:
    print(data)
๐Ÿ” ์žฌ์‹œ๋„ ๋กœ์ง (Retry)

์ผ์‹œ์ ์ธ ์˜ค๋ฅ˜๋Š” ์žฌ์‹œ๋„ํ•˜๋ฉด ํ•ด๊ฒฐ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„. requests์˜ Session๊ณผ HTTPAdapter๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์ž๋™ ์žฌ์‹œ๋„๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด:

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def create_session_with_retry():
    session = requests.Session()
    
    retry_strategy = Retry(
        total=3,              # ์ตœ๋Œ€ 3๋ฒˆ ์žฌ์‹œ๋„
        backoff_factor=1,     # ์žฌ์‹œ๋„ ๊ฐ„๊ฒฉ: 1์ดˆ, 2์ดˆ, 4์ดˆ...
        status_forcelist=[429, 500, 502, 503, 504],  # ์žฌ์‹œ๋„ํ•  ์ƒํƒœ ์ฝ”๋“œ
        allowed_methods=["GET", "POST"]  # ์žฌ์‹œ๋„ํ•  ๋ฉ”์„œ๋“œ
    )
    
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    
    return session

# ์‚ฌ์šฉ
session = create_session_with_retry()
response = session.get('https://api.example.com/data', timeout=10)
print(response.json())
๐Ÿ’ก backoff_factor๋Š” ์žฌ์‹œ๋„ ๊ฐ„๊ฒฉ์„ ์ง€์ˆ˜์ ์œผ๋กœ ๋Š˜๋ ค์ค˜. 1๋กœ ์„ค์ •ํ•˜๋ฉด 1์ดˆ, 2์ดˆ, 4์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ์žฌ์‹œ๋„ํ•ด. ์„œ๋ฒ„์— ๋ถ€๋‹ด์„ ์ค„์ด๋Š” ์ข‹์€ ํŒจํ„ด์ด์•ผ!

โšก 6. Session ํ™œ์šฉ - ์„ฑ๋Šฅ๊ณผ ํŽธ์˜์„ฑ UP!

๋งค๋ฒˆ requests.get()์„ ํ˜ธ์ถœํ•˜๋ฉด ๋งค ์š”์ฒญ๋งˆ๋‹ค ์ƒˆ TCP ์—ฐ๊ฒฐ์„ ๋งบ์–ด. ๊ฐ™์€ ์„œ๋ฒ„์— ์—ฌ๋Ÿฌ ๋ฒˆ ์š”์ฒญํ•  ๋•Œ๋Š” Session์„ ์“ฐ๋ฉด ์—ฐ๊ฒฐ์„ ์žฌ์‚ฌ์šฉํ•ด์„œ ํ›จ์”ฌ ๋น ๋ฅด๊ณ  ํšจ์œจ์ ์ด์•ผ!

import requests

# Session ์ƒ์„ฑ
session = requests.Session()

# ๊ณตํ†ต ํ—ค๋” ์„ค์ • (๋ชจ๋“  ์š”์ฒญ์— ์ž๋™ ์ ์šฉ)
session.headers.update({
    'Authorization': 'Bearer your_token',
    'Content-Type': 'application/json',
    'User-Agent': 'MyApp/1.0'
})

# ๊ณตํ†ต ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •
session.params = {'api_version': 'v2'}

# ์ด์ œ ๋ชจ๋“  ์š”์ฒญ์— ์œ„ ์„ค์ •์ด ์ž๋™ ์ ์šฉ๋ผ!
response1 = session.get('https://api.example.com/users')
response2 = session.get('https://api.example.com/posts')
response3 = session.post('https://api.example.com/comments', json={'text': '์ข‹์•„์š”!'})

# Session ์ข…๋ฃŒ (with ๋ฌธ ์‚ฌ์šฉ ๊ถŒ์žฅ)
session.close()

# ๋” ์ข‹์€ ๋ฐฉ๋ฒ•: with ๋ฌธ์œผ๋กœ ์ž๋™ ์ข…๋ฃŒ
with requests.Session() as session:
    session.headers.update({'Authorization': 'Bearer your_token'})
    
    users = session.get('https://api.example.com/users').json()
    posts = session.get('https://api.example.com/posts').json()
    
    print(f"์œ ์ € ์ˆ˜: {len(users)}")
    print(f"ํฌ์ŠคํŠธ ์ˆ˜: {len(posts)}")
๐Ÿช ์ฟ ํ‚ค ๊ด€๋ฆฌ

Session์€ ์ฟ ํ‚ค๋„ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌํ•ด์ค˜. ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ ์„œ๋น„์Šค๋ฅผ ์Šคํฌ๋ž˜ํ•‘ํ•  ๋•Œ ํŠนํžˆ ์œ ์šฉํ•ด:

with requests.Session() as session:
    # ๋กœ๊ทธ์ธ
    login_response = session.post(
        'https://example.com/login',
        data={'username': 'user', 'password': 'pass'}
    )
    
    # ๋กœ๊ทธ์ธ ํ›„ ์ฟ ํ‚ค๊ฐ€ ์ž๋™์œผ๋กœ ์„ธ์…˜์— ์ €์žฅ๋จ
    # ์ดํ›„ ์š”์ฒญ์— ์ž๋™์œผ๋กœ ์ฟ ํ‚ค๊ฐ€ ํฌํ•จ๋จ
    profile = session.get('https://example.com/profile')
    print(profile.json())

๐ŸŒ 7. ์‹ค์ „ ์˜ˆ์ œ - ์‹ค์ œ API ์—ฐ๋™ํ•ด๋ณด๊ธฐ
๐Ÿ”ฅ ์‹ค์ „ API ์—ฐ๋™ ์˜ˆ์ œ ๋ชจ์Œ โ˜€๏ธ ๋‚ ์”จ API OpenWeatherMap ํ˜„์žฌ ๋‚ ์”จ ์กฐํšŒ 5์ผ ์˜ˆ๋ณด ์กฐํšŒ ๋ฌด๋ฃŒ ํ”Œ๋žœ ์ œ๊ณต ๐Ÿ’ฑ ํ™˜์œจ API ExchangeRate-API ์‹ค์‹œ๊ฐ„ ํ™˜์œจ ์กฐํšŒ ํ†ตํ™” ๋ณ€ํ™˜ ๊ณ„์‚ฐ ๋ฌด๋ฃŒ ํ”Œ๋žœ ์ œ๊ณต ๐Ÿ™ GitHub API GitHub REST API ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์กฐํšŒ ์ด์Šˆ ๊ด€๋ฆฌ ํ† ํฐ ์ธ์ฆ ๐Ÿ—บ๏ธ ์นด์นด์˜ค API Kakao Developers ์ฃผ์†Œ ๊ฒ€์ƒ‰ ์žฅ์†Œ ๊ฒ€์ƒ‰ REST API ํ‚ค ๊ณตํ†ต ํŒจํ„ด: API ํ‚ค ๋ฐœ๊ธ‰ โ†’ ์š”์ฒญ ๊ตฌ์„ฑ โ†’ ์‘๋‹ต ํŒŒ์‹ฑ โ†’ ๋ฐ์ดํ„ฐ ํ™œ์šฉ API ํ‚ค ๋ฐœ๊ธ‰ ์š”์ฒญ ๊ตฌ์„ฑ ์‘๋‹ต ํŒŒ์‹ฑ ๋ฐ์ดํ„ฐ ํ™œ์šฉ ๋ชจ๋“  API ์—ฐ๋™์˜ ๊ธฐ๋ณธ ํ๋ฆ„์€ ๋™์ผํ•ด!
โ˜€๏ธ ์˜ˆ์ œ 1: OpenWeatherMap ๋‚ ์”จ API

OpenWeatherMap์—์„œ ๋ฌด๋ฃŒ๋กœ API ํ‚ค๋ฅผ ๋ฐœ๊ธ‰๋ฐ›์•„์„œ ํ˜„์žฌ ๋‚ ์”จ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์˜ˆ์ œ์•ผ:

import requests
import os

def get_weather(city: str, api_key: str) -> dict:
    """๋„์‹œ ์ด๋ฆ„์œผ๋กœ ํ˜„์žฌ ๋‚ ์”จ ์กฐํšŒ"""
    
    url = "https://api.openweathermap.org/data/2.5/weather"
    params = {
        'q': city,
        'appid': api_key,
        'units': 'metric',  # ์„ญ์”จ ์˜จ๋„
        'lang': 'kr'        # ํ•œ๊ตญ์–ด ์„ค๋ช…
    }
    
    try:
        response = requests.get(url, params=params, timeout=10)
        response.raise_for_status()
        
        data = response.json()
        
        return {
            '๋„์‹œ': data['name'],
            '๋‚ ์”จ': data['weather'][0]['description'],
            '์˜จ๋„': f"{data['main']['temp']}ยฐC",
            '์ฒด๊ฐ์˜จ๋„': f"{data['main']['feels_like']}ยฐC",
            '์Šต๋„': f"{data['main']['humidity']}%",
            'ํ’์†': f"{data['wind']['speed']}m/s"
        }
    
    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 401:
            return {'์—๋Ÿฌ': 'API ํ‚ค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์•„์š”!'}
        elif e.response.status_code == 404:
            return {'์—๋Ÿฌ': f'"{city}" ๋„์‹œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์–ด์š”!'}
        return {'์—๋Ÿฌ': str(e)}
    
    except Exception as e:
        return {'์—๋Ÿฌ': str(e)}

# ์‚ฌ์šฉ ์˜ˆ์‹œ
api_key = os.environ.get('OPENWEATHER_API_KEY')
weather = get_weather('Seoul', api_key)

for key, value in weather.items():
    print(f"{key}: {value}")

# ์ถœ๋ ฅ ์˜ˆ์‹œ:
# ๋„์‹œ: Seoul
# ๋‚ ์”จ: ๋ง‘์Œ
# ์˜จ๋„: 22.5ยฐC
# ์ฒด๊ฐ์˜จ๋„: 21.8ยฐC
# ์Šต๋„: 65%
# ํ’์†: 3.2m/s
๐Ÿ™ ์˜ˆ์ œ 2: GitHub API ์—ฐ๋™

GitHub API๋Š” ์ธ์ฆ ์—†์ด๋„ ๊ธฐ๋ณธ ์กฐํšŒ๊ฐ€ ๊ฐ€๋Šฅํ•ด. ํŠน์ • ์‚ฌ์šฉ์ž์˜ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์™€๋ณด์ž:

import requests

class GitHubClient:
    """GitHub API ํด๋ผ์ด์–ธํŠธ"""
    
    BASE_URL = "https://api.github.com"
    
    def __init__(self, token: str = None):
        self.session = requests.Session()
        self.session.headers.update({
            'Accept': 'application/vnd.github.v3+json',
            'User-Agent': 'MyGitHubApp/1.0'
        })
        
        # ํ† ํฐ์ด ์žˆ์œผ๋ฉด ์ธ์ฆ ํ—ค๋” ์ถ”๊ฐ€
        if token:
            self.session.headers.update({
                'Authorization': f'token {token}'
            })
    
    def get_user(self, username: str) -> dict:
        """์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ"""
        response = self.session.get(f"{self.BASE_URL}/users/{username}")
        response.raise_for_status()
        return response.json()
    
    def get_repos(self, username: str, sort: str = 'updated') -> list:
        """์‚ฌ์šฉ์ž ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๋ชฉ๋ก ์กฐํšŒ"""
        params = {
            'sort': sort,
            'per_page': 10
        }
        response = self.session.get(
            f"{self.BASE_URL}/users/{username}/repos",
            params=params
        )
        response.raise_for_status()
        return response.json()
    
    def get_repo_info(self, owner: str, repo: str) -> dict:
        """ํŠน์ • ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ •๋ณด ์กฐํšŒ"""
        response = self.session.get(f"{self.BASE_URL}/repos/{owner}/{repo}")
        response.raise_for_status()
        return response.json()
    
    def create_issue(self, owner: str, repo: str, title: str, body: str) -> dict:
        """์ด์Šˆ ์ƒ์„ฑ (ํ† ํฐ ํ•„์š”)"""
        payload = {'title': title, 'body': body}
        response = self.session.post(
            f"{self.BASE_URL}/repos/{owner}/{repo}/issues",
            json=payload
        )
        response.raise_for_status()
        return response.json()
    
    def __del__(self):
        self.session.close()

# ์‚ฌ์šฉ ์˜ˆ์‹œ
client = GitHubClient()  # ๊ณต๊ฐœ API๋Š” ํ† ํฐ ์—†์ด๋„ ๊ฐ€๋Šฅ

# ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ
user = client.get_user('torvalds')
print(f"์ด๋ฆ„: {user['name']}")
print(f"ํŒ”๋กœ์›Œ: {user['followers']:,}๋ช…")
print(f"๊ณต๊ฐœ ๋ ˆํฌ: {user['public_repos']}๊ฐœ")

# ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๋ชฉ๋ก
repos = client.get_repos('torvalds')
print("\n์ตœ๊ทผ ์—…๋ฐ์ดํŠธ๋œ ๋ ˆํฌ์ง€ํ† ๋ฆฌ:")
for repo in repos[:5]:
    stars = repo['stargazers_count']
    print(f"  โญ {repo['name']} ({stars:,} stars)")
๐Ÿ—บ๏ธ ์˜ˆ์ œ 3: ์นด์นด์˜ค ๋กœ์ปฌ API - ์ฃผ์†Œ ๊ฒ€์ƒ‰

์นด์นด์˜ค Developers์—์„œ ์•ฑ์„ ๋งŒ๋“ค๊ณ  REST API ํ‚ค๋ฅผ ๋ฐœ๊ธ‰๋ฐ›์œผ๋ฉด ๋ผ. ์ฃผ์†Œ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด๋ณด์ž:

import requests
import os

def search_address(query: str, api_key: str) -> list:
    """์นด์นด์˜ค ์ฃผ์†Œ ๊ฒ€์ƒ‰ API"""
    
    url = "https://dapi.kakao.com/v2/local/search/address.json"
    headers = {
        'Authorization': f'KakaoAK {api_key}'
    }
    params = {
        'query': query,
        'size': 5  # ์ตœ๋Œ€ 5๊ฐœ ๊ฒฐ๊ณผ
    }
    
    response = requests.get(url, headers=headers, params=params, timeout=10)
    response.raise_for_status()
    
    data = response.json()
    results = []
    
    for doc in data['documents']:
        results.append({
            '์ฃผ์†Œ': doc['address_name'],
            '์œ„๋„': doc['y'],
            '๊ฒฝ๋„': doc['x'],
            '์ฃผ์†Œํƒ€์ž…': doc['address_type']
        })
    
    return results

def search_keyword(keyword: str, api_key: str, category: str = None) -> list:
    """์นด์นด์˜ค ํ‚ค์›Œ๋“œ ์žฅ์†Œ ๊ฒ€์ƒ‰ API"""
    
    url = "https://dapi.kakao.com/v2/local/search/keyword.json"
    headers = {'Authorization': f'KakaoAK {api_key}'}
    params = {
        'query': keyword,
        'size': 5
    }
    
    if category:
        params['category_group_code'] = category
    
    response = requests.get(url, headers=headers, params=params, timeout=10)
    response.raise_for_status()
    
    data = response.json()
    results = []
    
    for place in data['documents']:
        results.append({
            '์žฅ์†Œ๋ช…': place['place_name'],
            '์ฃผ์†Œ': place['road_address_name'],
            '์ „ํ™”๋ฒˆํ˜ธ': place.get('phone', '์—†์Œ'),
            '์นดํ…Œ๊ณ ๋ฆฌ': place['category_name']
        })
    
    return results

# ์‚ฌ์šฉ ์˜ˆ์‹œ
kakao_key = os.environ.get('KAKAO_REST_API_KEY')

# ์ฃผ์†Œ ๊ฒ€์ƒ‰
addresses = search_address('๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 152', kakao_key)
for addr in addresses:
    print(f"๐Ÿ“ {addr['์ฃผ์†Œ']} (์œ„๋„: {addr['์œ„๋„']}, ๊ฒฝ๋„: {addr['๊ฒฝ๋„']})")

# ์นดํŽ˜ ๊ฒ€์ƒ‰
cafes = search_keyword('๊ฐ•๋‚จ์—ญ ์นดํŽ˜', kakao_key, 'CE7')
for cafe in cafes:
    print(f"โ˜• {cafe['์žฅ์†Œ๋ช…']} - {cafe['์ฃผ์†Œ']}")

๐Ÿ—๏ธ 8. API ํด๋ผ์ด์–ธํŠธ ํด๋ž˜์Šค ์„ค๊ณ„ - ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ ๋งŒ๋“ค๊ธฐ

์‹ค๋ฌด์—์„œ๋Š” API ํ˜ธ์ถœ ์ฝ”๋“œ๋ฅผ ์—ฌ๊ธฐ์ €๊ธฐ ํฉ์–ด๋†“์œผ๋ฉด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์ง€์˜ฅ์ด ๋ผ. ํด๋ž˜์Šค๋กœ ๊น”๋”ํ•˜๊ฒŒ ์บก์Аํ™”ํ•˜๋Š” ๊ฒŒ ์ •์„์ด์•ผ. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์™ธ๋ถ€ ๊ฒฐ์ œ API๋‚˜ ์†Œ์…œ ๋กœ๊ทธ์ธ API๋ฅผ ์—ฐ๋™ํ•  ๋•Œ ์ด๋Ÿฐ ํŒจํ„ด์„ ๋งŽ์ด ์จ!

import requests
import time
import logging
from typing import Optional, Dict, Any
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# ๋กœ๊น… ์„ค์ •
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class BaseAPIClient:
    """์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ API ํด๋ผ์ด์–ธํŠธ ๋ฒ ์ด์Šค ํด๋ž˜์Šค"""
    
    def __init__(
        self,
        base_url: str,
        api_key: str = None,
        timeout: int = 10,
        max_retries: int = 3
    ):
        self.base_url = base_url.rstrip('/')
        self.timeout = timeout
        self.session = self._create_session(api_key, max_retries)
    
    def _create_session(self, api_key: str, max_retries: int) -> requests.Session:
        """์žฌ์‹œ๋„ ๋กœ์ง์ด ํฌํ•จ๋œ ์„ธ์…˜ ์ƒ์„ฑ"""
        session = requests.Session()
        
        # ๊ธฐ๋ณธ ํ—ค๋” ์„ค์ •
        session.headers.update({
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        })
        
        # API ํ‚ค ์„ค์ •
        if api_key:
            session.headers.update({'Authorization': f'Bearer {api_key}'})
        
        # ์žฌ์‹œ๋„ ์ „๋žต ์„ค์ •
        retry = Retry(
            total=max_retries,
            backoff_factor=0.5,
            status_forcelist=[429, 500, 502, 503, 504]
        )
        adapter = HTTPAdapter(max_retries=retry)
        session.mount('https://', adapter)
        session.mount('http://', adapter)
        
        return session
    
    def _request(
        self,
        method: str,
        endpoint: str,
        **kwargs
    ) -> Optional[Dict[str, Any]]:
        """๊ณตํ†ต ์š”์ฒญ ๋ฉ”์„œ๋“œ"""
        url = f"{self.base_url}/{endpoint.lstrip('/')}"
        
        try:
            logger.info(f"[{method.upper()}] {url}")
            start_time = time.time()
            
            response = self.session.request(
                method,
                url,
                timeout=self.timeout,
                **kwargs
            )
            
            elapsed = time.time() - start_time
            logger.info(f"์‘๋‹ต: {response.status_code} ({elapsed:.2f}์ดˆ)")
            
            response.raise_for_status()
            
            # ์‘๋‹ต ๋ณธ๋ฌธ์ด ์—†๋Š” ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ (204 No Content ๋“ฑ)
            if response.status_code == 204 or not response.content:
                return {}
            
            return response.json()
        
        except requests.exceptions.HTTPError as e:
            logger.error(f"HTTP ์—๋Ÿฌ: {e.response.status_code} - {e.response.text}")
            raise
        
        except requests.exceptions.Timeout:
            logger.error(f"ํƒ€์ž„์•„์›ƒ: {url}")
            raise
        
        except requests.exceptions.RequestException as e:
            logger.error(f"์š”์ฒญ ์‹คํŒจ: {e}")
            raise
    
    def get(self, endpoint: str, params: dict = None) -> dict:
        return self._request('GET', endpoint, params=params)
    
    def post(self, endpoint: str, data: dict = None) -> dict:
        return self._request('POST', endpoint, json=data)
    
    def put(self, endpoint: str, data: dict = None) -> dict:
        return self._request('PUT', endpoint, json=data)
    
    def patch(self, endpoint: str, data: dict = None) -> dict:
        return self._request('PATCH', endpoint, json=data)
    
    def delete(self, endpoint: str) -> dict:
        return self._request('DELETE', endpoint)
    
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.session.close()


# ์‹ค์ œ ์„œ๋น„์Šค ํด๋ผ์ด์–ธํŠธ ๊ตฌํ˜„ ์˜ˆ์‹œ
class TodoAPIClient(BaseAPIClient):
    """ํ•  ์ผ ๊ด€๋ฆฌ API ํด๋ผ์ด์–ธํŠธ"""
    
    def __init__(self, api_key: str = None):
        super().__init__(
            base_url='https://jsonplaceholder.typicode.com',
            api_key=api_key
        )
    
    def get_todos(self, user_id: int = None) -> list:
        """ํ•  ์ผ ๋ชฉ๋ก ์กฐํšŒ"""
        params = {'userId': user_id} if user_id else None
        return self.get('/todos', params=params)
    
    def get_todo(self, todo_id: int) -> dict:
        """ํŠน์ • ํ•  ์ผ ์กฐํšŒ"""
        return self.get(f'/todos/{todo_id}')
    
    def create_todo(self, title: str, user_id: int) -> dict:
        """ํ•  ์ผ ์ƒ์„ฑ"""
        return self.post('/todos', data={
            'title': title,
            'userId': user_id,
            'completed': False
        })
    
    def complete_todo(self, todo_id: int) -> dict:
        """ํ•  ์ผ ์™„๋ฃŒ ์ฒ˜๋ฆฌ"""
        return self.patch(f'/todos/{todo_id}', data={'completed': True})
    
    def delete_todo(self, todo_id: int) -> dict:
        """ํ•  ์ผ ์‚ญ์ œ"""
        return self.delete(f'/todos/{todo_id}')


# ์‚ฌ์šฉ ์˜ˆ์‹œ
with TodoAPIClient() as client:
    # ํ•  ์ผ ๋ชฉ๋ก ์กฐํšŒ
    todos = client.get_todos(user_id=1)
    print(f"์ด {len(todos)}๊ฐœ์˜ ํ•  ์ผ")
    
    # ์ƒˆ ํ•  ์ผ ์ƒ์„ฑ
    new_todo = client.create_todo('ํŒŒ์ด์ฌ API ๊ณต๋ถ€ํ•˜๊ธฐ', user_id=1)
    print(f"์ƒ์„ฑ๋จ: {new_todo}")
    
    # ์™„๋ฃŒ ์ฒ˜๋ฆฌ
    completed = client.complete_todo(1)
    print(f"์™„๋ฃŒ: {completed}")
๐Ÿ’ก ํด๋ž˜์Šค ์„ค๊ณ„ ํฌ์ธํŠธ!
BaseAPIClient์— ๊ณตํ†ต ๋กœ์ง(์žฌ์‹œ๋„, ๋กœ๊น…, ์—๋Ÿฌ์ฒ˜๋ฆฌ)์„ ๋„ฃ๊ณ , ์‹ค์ œ ์„œ๋น„์Šค ํด๋ผ์ด์–ธํŠธ๋Š” ์ƒ์†๋ฐ›์•„์„œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋งŒ ๊ตฌํ˜„ํ•ด. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ƒˆ๋กœ์šด API๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ ๊ณตํ†ต ์ฝ”๋“œ๋ฅผ ๋ฐ˜๋ณต ์ž‘์„ฑํ•  ํ•„์š”๊ฐ€ ์—†์–ด!

๐Ÿ“„ 9. ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ฒ˜๋ฆฌ - ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ

API๋Š” ํ•œ ๋ฒˆ์— ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ์ง€ ์•Š์•„. ๋ณดํ†ต ํŽ˜์ด์ง€ ๋‹จ์œ„๋กœ ๋‚˜๋ˆ ์„œ ์ค˜. ์ด๊ฑธ ํŽ˜์ด์ง€๋„ค์ด์…˜์ด๋ผ๊ณ  ํ•ด. ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž:

import requests
import time

def fetch_all_pages(base_url: str, params: dict = None, max_pages: int = 10) -> list:
    """๋ชจ๋“  ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜"""
    
    all_data = []
    params = params or {}
    params['page'] = 1
    params['per_page'] = 100  # ํŽ˜์ด์ง€๋‹น ์ตœ๋Œ€ ํ•ญ๋ชฉ ์ˆ˜
    
    for page_num in range(1, max_pages + 1):
        params['page'] = page_num
        
        response = requests.get(base_url, params=params, timeout=10)
        response.raise_for_status()
        
        data = response.json()
        
        # ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ์ข…๋ฃŒ
        if not data:
            print(f"โœ… ์ด {page_num - 1}ํŽ˜์ด์ง€ ์™„๋ฃŒ!")
            break
        
        all_data.extend(data)
        print(f"๐Ÿ“„ {page_num}ํŽ˜์ด์ง€ ๋กœ๋“œ: {len(data)}๊ฐœ ํ•ญ๋ชฉ")
        
        # API ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋ฐฉ์ง€: ์š”์ฒญ ๊ฐ„ ๋”œ๋ ˆ์ด
        time.sleep(0.5)
        
        # ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€ ๊ฐ์ง€ (Link ํ—ค๋” ๋ฐฉ์‹)
        link_header = response.headers.get('Link', '')
        if 'rel="next"' not in link_header:
            print(f"โœ… ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€ ๋„๋‹ฌ!")
            break
    
    return all_data


# GitHub API ํŽ˜์ด์ง€๋„ค์ด์…˜ ์˜ˆ์‹œ
def get_all_repos(username: str, token: str = None) -> list:
    """์‚ฌ์šฉ์ž์˜ ๋ชจ๋“  ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ"""
    
    headers = {'Accept': 'application/vnd.github.v3+json'}
    if token:
        headers['Authorization'] = f'token {token}'
    
    all_repos = []
    page = 1
    
    while True:
        response = requests.get(
            f'https://api.github.com/users/{username}/repos',
            headers=headers,
            params={'page': page, 'per_page': 30, 'sort': 'updated'},
            timeout=10
        )
        response.raise_for_status()
        
        repos = response.json()
        
        if not repos:
            break
        
        all_repos.extend(repos)
        
        # Link ํ—ค๋”๋กœ ๋‹ค์Œ ํŽ˜์ด์ง€ ํ™•์ธ
        if 'rel="next"' not in response.headers.get('Link', ''):
            break
        
        page += 1
        time.sleep(0.3)  # ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋ฐฉ์ง€
    
    return all_repos

# ์‚ฌ์šฉ
repos = get_all_repos('octocat')
print(f"์ด ๋ ˆํฌ์ง€ํ† ๋ฆฌ: {len(repos)}๊ฐœ")
for repo in repos:
    print(f"  ๐Ÿ“ {repo['name']} โญ{repo['stargazers_count']}")

๐Ÿ” 10. OAuth 2.0 ์ธ์ฆ ํ๋ฆ„ ์ดํ•ดํ•˜๊ธฐ

์นด์นด์˜ค ๋กœ๊ทธ์ธ, ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ๊ฐ™์€ ์†Œ์…œ ๋กœ๊ทธ์ธ์€ OAuth 2.0์„ ์จ. ์ด๊ฒŒ ์ฒ˜์Œ์—” ๋ณต์žกํ•ด ๋ณด์ด๋Š”๋ฐ, ํ๋ฆ„์„ ์ดํ•ดํ•˜๋ฉด ๋ณ„๊ฑฐ ์•„๋‹ˆ์•ผ!

๐Ÿ” OAuth 2.0 ์ธ์ฆ ์ฝ”๋“œ ํ๋ฆ„ ๐Ÿ‘ค ์‚ฌ์šฉ์ž ๐Ÿ ๋‚ด ์•ฑ (Python) ํด๋ผ์ด์–ธํŠธ ๐Ÿ”‘ ์ธ์ฆ ์„œ๋ฒ„ (์นด์นด์˜ค ๋“ฑ) Auth Server ๐Ÿ“ฆ ๋ฆฌ์†Œ์Šค ์„œ๋ฒ„ API ์„œ๋ฒ„ Resource Server โ‘  ๋กœ๊ทธ์ธ ์š”์ฒญ โ‘ก ์ธ์ฆ ์š”์ฒญ โ‘ข ๋กœ๊ทธ์ธ ํ™”๋ฉด ํ‘œ์‹œ โ‘ฃ ๋กœ๊ทธ์ธ ์™„๋ฃŒ (์ธ์ฆ ์ฝ”๋“œ ๋ฐœ๊ธ‰) โ‘ค ์ธ์ฆ ์ฝ”๋“œ ์ „๋‹ฌ โ‘ฅ ์•ก์„ธ์Šค ํ† ํฐ ์š”์ฒญ โ‘ฆ ์•ก์„ธ์Šค ํ† ํฐ ๋ฐœ๊ธ‰ โ‘ง ํ† ํฐ์œผ๋กœ API ํ˜ธ์ถœ โ‘จ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ OAuth 2.0 Authorization Code Flow
import requests
import os

# ์นด์นด์˜ค OAuth 2.0 ์˜ˆ์‹œ

# 1๋‹จ๊ณ„: ์ธ์ฆ URL ์ƒ์„ฑ (์‚ฌ์šฉ์ž๋ฅผ ์ด URL๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ)
def get_kakao_auth_url():
    client_id = os.environ.get('KAKAO_CLIENT_ID')
    redirect_uri = 'https://myapp.com/callback'
    
    return (
        f"https://kauth.kakao.com/oauth/authorize"
        f"?client_id={client_id}"
        f"&redirect_uri={redirect_uri}"
        f"&response_type=code"
    )

# 2๋‹จ๊ณ„: ์ธ์ฆ ์ฝ”๋“œ๋กœ ์•ก์„ธ์Šค ํ† ํฐ ๋ฐœ๊ธ‰
def get_kakao_token(auth_code: str) -> dict:
    client_id = os.environ.get('KAKAO_CLIENT_ID')
    client_secret = os.environ.get('KAKAO_CLIENT_SECRET')
    redirect_uri = 'https://myapp.com/callback'
    
    response = requests.post(
        'https://kauth.kakao.com/oauth/token',
        data={
            'grant_type': 'authorization_code',
            'client_id': client_id,
            'client_secret': client_secret,
            'redirect_uri': redirect_uri,
            'code': auth_code
        }
    )
    response.raise_for_status()
    return response.json()
    # {'access_token': '...', 'token_type': 'bearer', 'expires_in': 21599, ...}

# 3๋‹จ๊ณ„: ์•ก์„ธ์Šค ํ† ํฐ์œผ๋กœ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ
def get_kakao_user_info(access_token: str) -> dict:
    response = requests.get(
        'https://kapi.kakao.com/v2/user/me',
        headers={'Authorization': f'Bearer {access_token}'}
    )
    response.raise_for_status()
    return response.json()

# ์‚ฌ์šฉ ํ๋ฆ„
auth_url = get_kakao_auth_url()
print(f"์ด URL๋กœ ์‚ฌ์šฉ์ž๋ฅผ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ: {auth_url}")

# ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ ํ›„ ์ฝœ๋ฐฑ์œผ๋กœ code ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์˜ด
auth_code = "๋ฐ›์€_์ธ์ฆ_์ฝ”๋“œ"
token_data = get_kakao_token(auth_code)
access_token = token_data['access_token']

user_info = get_kakao_user_info(access_token)
print(f"์‚ฌ์šฉ์ž ID: {user_info['id']}")
print(f"๋‹‰๋„ค์ž„: {user_info['kakao_account']['profile']['nickname']}")

โš™๏ธ 11. ๊ณ ๊ธ‰ ๊ธฐ๋ฒ• - ํƒ€์ž„์•„์›ƒ, ํ”„๋ก์‹œ, SSL, ์ŠคํŠธ๋ฆฌ๋ฐ
โฑ๏ธ ํƒ€์ž„์•„์›ƒ ์„ค์ •
import requests

# ์—ฐ๊ฒฐ ํƒ€์ž„์•„์›ƒ + ์ฝ๊ธฐ ํƒ€์ž„์•„์›ƒ ๋ถ„๋ฆฌ ์„ค์ •
response = requests.get(
    'https://api.example.com/data',
    timeout=(3.05, 27)  # (์—ฐ๊ฒฐ ํƒ€์ž„์•„์›ƒ, ์ฝ๊ธฐ ํƒ€์ž„์•„์›ƒ) ์ดˆ
)

# ๋‹จ์ผ ํƒ€์ž„์•„์›ƒ (์—ฐ๊ฒฐ+์ฝ๊ธฐ ํ•ฉ์‚ฐ)
response = requests.get('https://api.example.com/data', timeout=10)
๐ŸŒ ํ”„๋ก์‹œ ์„ค์ •
import requests

proxies = {
    'http': 'http://proxy.example.com:8080',
    'https': 'http://proxy.example.com:8080'
}

response = requests.get(
    'https://api.example.com/data',
    proxies=proxies
)
๐Ÿ”’ SSL ์ธ์ฆ์„œ ์ฒ˜๋ฆฌ
import requests

# SSL ์ธ์ฆ์„œ ๊ฒ€์ฆ ๋น„ํ™œ์„ฑํ™” (๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ! ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์ ˆ๋Œ€ ๊ธˆ์ง€)
response = requests.get('https://api.example.com/data', verify=False)

# ์ปค์Šคํ…€ CA ์ธ์ฆ์„œ ์‚ฌ์šฉ
response = requests.get(
    'https://api.example.com/data',
    verify='/path/to/ca-bundle.crt'
)
๐Ÿ“ก ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต ์ฒ˜๋ฆฌ (๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ)
import requests

def download_file(url: str, filename: str):
    """๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ์„ ์ฒญํฌ ๋‹จ์œ„๋กœ ๋‹ค์šด๋กœ๋“œ"""
    
    with requests.get(url, stream=True, timeout=30) as response:
        response.raise_for_status()
        
        total_size = int(response.headers.get('content-length', 0))
        downloaded = 0
        
        with open(filename, 'wb') as f:
            for chunk in response.iter_content(chunk_size=8192):
                if chunk:  # ๋นˆ ์ฒญํฌ ํ•„ํ„ฐ๋ง
                    f.write(chunk)
                    downloaded += len(chunk)
                    
                    if total_size:
                        progress = (downloaded / total_size) * 100
                        print(f"\r๋‹ค์šด๋กœ๋“œ: {progress:.1f}%", end='', flush=True)
        
        print(f"\nโœ… ๋‹ค์šด๋กœ๋“œ ์™„๋ฃŒ: {filename}")

# ์‚ฌ์šฉ
download_file(
    'https://example.com/large-file.zip',
    'downloaded-file.zip'
)

๐Ÿงช 12. API ํด๋ผ์ด์–ธํŠธ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

์‹ค๋ฌด์—์„œ API ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ๋Š” ์‹ค์ œ API๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ ๋ผ. ๋น„์šฉ์ด ๋ฐœ์ƒํ•˜๊ฑฐ๋‚˜ ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹์— ๊ฑธ๋ฆด ์ˆ˜ ์žˆ๊ฑฐ๋“ . Mock์„ ์‚ฌ์šฉํ•ด์„œ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒŒ ์ •์„์ด์•ผ!

import unittest
from unittest.mock import patch, MagicMock
import requests

# ํ…Œ์ŠคํŠธํ•  ํ•จ์ˆ˜
def get_user_info(user_id: int) -> dict:
    response = requests.get(f'https://api.example.com/users/{user_id}')
    response.raise_for_status()
    return response.json()

# ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
class TestAPIClient(unittest.TestCase):
    
    @patch('requests.get')
    def test_get_user_success(self, mock_get):
        """์„ฑ๊ณต ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ"""
        # Mock ์‘๋‹ต ์„ค์ •
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            'id': 1,
            'name': 'ํ™๊ธธ๋™',
            'email': 'hong@example.com'
        }
        mock_get.return_value = mock_response
        
        # ํ•จ์ˆ˜ ํ˜ธ์ถœ
        result = get_user_info(1)
        
        # ๊ฒ€์ฆ
        self.assertEqual(result['name'], 'ํ™๊ธธ๋™')
        mock_get.assert_called_once_with('https://api.example.com/users/1')
    
    @patch('requests.get')
    def test_get_user_not_found(self, mock_get):
        """404 ์—๋Ÿฌ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ"""
        mock_response = MagicMock()
        mock_response.status_code = 404
        mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
            response=mock_response
        )
        mock_get.return_value = mock_response
        
        # ์˜ˆ์™ธ ๋ฐœ์ƒ ํ™•์ธ
        with self.assertRaises(requests.exceptions.HTTPError):
            get_user_info(999)
    
    @patch('requests.get')
    def test_timeout(self, mock_get):
        """ํƒ€์ž„์•„์›ƒ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ"""
        mock_get.side_effect = requests.exceptions.Timeout()
        
        with self.assertRaises(requests.exceptions.Timeout):
            get_user_info(1)

if __name__ == '__main__':
    unittest.main()
๐Ÿ’ก responses ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋„ ์ถ”์ฒœํ•ด! pip install responses๋กœ ์„ค์น˜ํ•˜๋ฉด ๋” ์ง๊ด€์ ์œผ๋กœ HTTP ์‘๋‹ต์„ ๋ชจํ‚นํ•  ์ˆ˜ ์žˆ์–ด. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์™ธ๋ถ€ ๊ฒฐ์ œ API๋‚˜ ์•Œ๋ฆผ API๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ ์ •๋ง ์œ ์šฉํ•˜๊ฒŒ ์“ฐ์—ฌ!

๐Ÿ“Š 13. ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹(Rate Limit) ์ฒ˜๋ฆฌ - ์˜ˆ์˜ ๋ฐ”๋ฅธ API ํ˜ธ์ถœ

๋Œ€๋ถ€๋ถ„์˜ API๋Š” ์ผ์ • ์‹œ๊ฐ„ ๋‚ด ์š”์ฒญ ํšŸ์ˆ˜๋ฅผ ์ œํ•œํ•ด. ์ด๊ฑธ ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹์ด๋ผ๊ณ  ํ•ด. ์ด๊ฑธ ๋ฌด์‹œํ•˜๋ฉด 429 ์—๋Ÿฌ ๋งž๊ณ  IP ์ฐจ๋‹จ๋‹นํ•  ์ˆ˜๋„ ์žˆ์–ด! ๐Ÿ˜ฑ

import requests
import time
from functools import wraps

def rate_limited(max_calls: int, period: float):
    """๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ"""
    calls = []
    
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            
            # ๊ธฐ๊ฐ„ ๋‚ด ํ˜ธ์ถœ ๊ธฐ๋ก ํ•„ํ„ฐ๋ง
            calls[:] = [c for c in calls if now - c < period]
            
            if len(calls) >= max_calls:
                # ๋Œ€๊ธฐ ์‹œ๊ฐ„ ๊ณ„์‚ฐ
                sleep_time = period - (now - calls[0])
                if sleep_time > 0:
                    print(f"โณ ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋Œ€๊ธฐ: {sleep_time:.2f}์ดˆ")
                    time.sleep(sleep_time)
            
            calls.append(time.time())
            return func(*args, **kwargs)
        
        return wrapper
    return decorator

# ์ดˆ๋‹น ์ตœ๋Œ€ 5๋ฒˆ ํ˜ธ์ถœ ์ œํ•œ
@rate_limited(max_calls=5, period=1.0)
def call_api(endpoint: str) -> dict:
    response = requests.get(endpoint, timeout=10)
    response.raise_for_status()
    return response.json()

# ์‘๋‹ต ํ—ค๋”์—์„œ ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ์ •๋ณด ์ฝ๊ธฐ
def smart_request(url: str) -> dict:
    response = requests.get(url, timeout=10)
    
    # GitHub API ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ํ—ค๋” ์˜ˆ์‹œ
    remaining = int(response.headers.get('X-RateLimit-Remaining', 999))
    reset_time = int(response.headers.get('X-RateLimit-Reset', 0))
    
    if response.status_code == 429:
        # ๋ฆฌ์…‹ ์‹œ๊ฐ„๊นŒ์ง€ ๋Œ€๊ธฐ
        wait_time = reset_time - time.time() + 1
        print(f"โณ ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ์ดˆ๊ณผ! {wait_time:.0f}์ดˆ ๋Œ€๊ธฐ...")
        time.sleep(max(wait_time, 0))
        return smart_request(url)  # ์žฌ์‹œ๋„
    
    if remaining < 10:
        print(f"โš ๏ธ ๋‚จ์€ ์š”์ฒญ ํšŸ์ˆ˜: {remaining}ํšŒ")
    
    response.raise_for_status()
    return response.json()

๐ŸŽ 14. ์‹ค์ „ ๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ - ํ™˜์œจ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ด‡

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๊ฑธ ๋‹ค ํ•ฉ์ณ์„œ ์‹ค์šฉ์ ์ธ ํ™˜์œจ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ด‡์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๋ชฉํ‘œ ํ™˜์œจ์— ๋„๋‹ฌํ•˜๋ฉด ์•Œ๋ฆผ์„ ๋ณด๋‚ด๋Š” ํ”„๋กœ๊ทธ๋žจ์ด์•ผ:

import requests
import time
import os
from datetime import datetime
from typing import Optional

class ExchangeRateMonitor:
    """ํ™˜์œจ ๋ชจ๋‹ˆํ„ฐ๋ง ํด๋ผ์ด์–ธํŠธ"""
    
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = f"https://v6.exchangerate-api.com/v6/{api_key}"
        self.session = requests.Session()
        self.session.headers.update({'Accept': 'application/json'})
    
    def get_rate(self, from_currency: str, to_currency: str) -> Optional[float]:
        """ํ˜„์žฌ ํ™˜์œจ ์กฐํšŒ"""
        try:
            response = self.session.get(
                f"{self.base_url}/pair/{from_currency}/{to_currency}",
                timeout=10
            )
            response.raise_for_status()
            data = response.json()
            
            if data['result'] == 'success':
                return data['conversion_rate']
            return None
        
        except Exception as e:
            print(f"โŒ ํ™˜์œจ ์กฐํšŒ ์‹คํŒจ: {e}")
            return None
    
    def get_all_rates(self, base_currency: str = 'USD') -> dict:
        """๊ธฐ์ค€ ํ†ตํ™” ๋Œ€๋น„ ๋ชจ๋“  ํ™˜์œจ ์กฐํšŒ"""
        try:
            response = self.session.get(
                f"{self.base_url}/latest/{base_currency}",
                timeout=10
            )
            response.raise_for_status()
            data = response.json()
            
            if data['result'] == 'success':
                return data['conversion_rates']
            return {}
        
        except Exception as e:
            print(f"โŒ ํ™˜์œจ ์กฐํšŒ ์‹คํŒจ: {e}")
            return {}
    
    def send_slack_alert(self, webhook_url: str, message: str):
        """์Šฌ๋ž™ ์›นํ›…์œผ๋กœ ์•Œ๋ฆผ ์ „์†ก"""
        try:
            response = requests.post(
                webhook_url,
                json={'text': message},
                timeout=10
            )
            response.raise_for_status()
            print("โœ… ์Šฌ๋ž™ ์•Œ๋ฆผ ์ „์†ก ์™„๋ฃŒ!")
        except Exception as e:
            print(f"โŒ ์Šฌ๋ž™ ์•Œ๋ฆผ ์‹คํŒจ: {e}")
    
    def monitor(
        self,
        from_currency: str,
        to_currency: str,
        target_rate: float,
        condition: str = 'below',  # 'below' ๋˜๋Š” 'above'
        check_interval: int = 60,  # ์ดˆ
        webhook_url: str = None
    ):
        """ํ™˜์œจ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์ž‘"""
        print(f"๐Ÿ” ํ™˜์œจ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์ž‘!")
        print(f"   {from_currency} โ†’ {to_currency}")
        print(f"   ๋ชฉํ‘œ: {condition} {target_rate}")
        print(f"   ์ฒดํฌ ๊ฐ„๊ฒฉ: {check_interval}์ดˆ")
        print("-" * 40)
        
        while True:
            rate = self.get_rate(from_currency, to_currency)
            
            if rate is None:
                print("โš ๏ธ ํ™˜์œจ ์กฐํšŒ ์‹คํŒจ, ์žฌ์‹œ๋„...")
                time.sleep(check_interval)
                continue
            
            now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            print(f"[{now}] {from_currency}/{to_currency} = {rate:.4f}")
            
            # ๋ชฉํ‘œ ํ™˜์œจ ๋„๋‹ฌ ํ™•์ธ
            triggered = False
            if condition == 'below' and rate <= target_rate:
                triggered = True
                msg = f"๐ŸŽฏ ๋ชฉํ‘œ ํ™˜์œจ ๋„๋‹ฌ!\n{from_currency}/{to_currency} = {rate:.4f}\n๋ชฉํ‘œ: {target_rate} ์ดํ•˜"
            elif condition == 'above' and rate >= target_rate:
                triggered = True
                msg = f"๐ŸŽฏ ๋ชฉํ‘œ ํ™˜์œจ ๋„๋‹ฌ!\n{from_currency}/{to_currency} = {rate:.4f}\n๋ชฉํ‘œ: {target_rate} ์ด์ƒ"
            
            if triggered:
                print(f"\n๐Ÿšจ {msg}")
                if webhook_url:
                    self.send_slack_alert(webhook_url, msg)
                break
            
            time.sleep(check_interval)
    
    def __del__(self):
        self.session.close()

# ์‚ฌ์šฉ ์˜ˆ์‹œ
api_key = os.environ.get('EXCHANGE_RATE_API_KEY')
monitor = ExchangeRateMonitor(api_key)

# ํ˜„์žฌ ํ™˜์œจ ํ™•์ธ
usd_krw = monitor.get_rate('USD', 'KRW')
print(f"ํ˜„์žฌ USD/KRW: {usd_krw:.2f}์›")

# ์ฃผ์š” ํ†ตํ™” ํ™˜์œจ ์กฐํšŒ
rates = monitor.get_all_rates('USD')
currencies = ['KRW', 'JPY', 'EUR', 'GBP', 'CNY']
print("\n๐Ÿ“Š ์ฃผ์š” ํ†ตํ™” ํ™˜์œจ (USD ๊ธฐ์ค€):")
for currency in currencies:
    if currency in rates:
        print(f"  {currency}: {rates[currency]:.4f}")

# ํ™˜์œจ ๋ชจ๋‹ˆํ„ฐ๋ง (1๋‹ฌ๋Ÿฌ = 1300์› ์ดํ•˜๊ฐ€ ๋˜๋ฉด ์•Œ๋ฆผ)
# monitor.monitor(
#     from_currency='USD',
#     to_currency='KRW',
#     target_rate=1300,
#     condition='below',
#     check_interval=300,  # 5๋ถ„๋งˆ๋‹ค ์ฒดํฌ
#     webhook_url=os.environ.get('SLACK_WEBHOOK_URL')
# )

๐Ÿ’ก 15. ์‹ค๋ฌด ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค ์ด์ •๋ฆฌ
โœ… ๊ผญ ์ง€์ผœ์•ผ ํ•  ๊ฒƒ๋“ค

1. ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ API ํ‚ค ๊ด€๋ฆฌ ๐Ÿ”‘
์ ˆ๋Œ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ๋งˆ. python-dotenv ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ .env ํŒŒ์ผ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒŒ ํŽธํ•ด.

2. ํ•ญ์ƒ ํƒ€์ž„์•„์›ƒ ์„ค์ • โฑ๏ธ
timeout ์—†์œผ๋ฉด ์‘๋‹ต ์•ˆ ์˜ค๋Š” ์„œ๋ฒ„์— ์˜์›ํžˆ ๊ธฐ๋‹ค๋ฆด ์ˆ˜ ์žˆ์–ด. ๋ฐ˜๋“œ์‹œ ์„ค์ •ํ•ด!

3. ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋Š” ๊ตฌ์ฒด์ ์œผ๋กœ ๐Ÿ›ก๏ธ
except Exception์œผ๋กœ ๋‹ค ์žก์ง€ ๋ง๊ณ , ๊ตฌ์ฒด์ ์ธ ์˜ˆ์™ธ ํƒ€์ž…๋ณ„๋กœ ์ฒ˜๋ฆฌํ•ด.

4. Session ์žฌ์‚ฌ์šฉ โšก
๊ฐ™์€ ์„œ๋ฒ„์— ์—ฌ๋Ÿฌ ๋ฒˆ ์š”์ฒญํ•  ๋•Œ๋Š” Session ์จ์„œ ์—ฐ๊ฒฐ ์žฌ์‚ฌ์šฉํ•ด.

5. ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ์กด์ค‘ ๐Ÿค
API ์ œ๊ณต์ž์˜ ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹์„ ์ง€์ผœ. ์š”์ฒญ ๊ฐ„ ์ ์ ˆํ•œ ๋”œ๋ ˆ์ด ์ถ”๊ฐ€ํ•ด.

6. ์‘๋‹ต ์บ์‹ฑ ํ™œ์šฉ ๐Ÿ’พ
์ž์ฃผ ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ๋ฐ์ดํ„ฐ๋Š” ์บ์‹ฑํ•ด์„œ ๋ถˆํ•„์š”ํ•œ API ํ˜ธ์ถœ ์ค„์—ฌ.

7. ๋กœ๊น… ์ถ”๊ฐ€ ๐Ÿ“
์š”์ฒญ/์‘๋‹ต ๋กœ๊ทธ๋ฅผ ๋‚จ๊ฒจ๋‘๋ฉด ๋””๋ฒ„๊น…ํ•  ๋•Œ ์—„์ฒญ ๋„์›€๋ผ.

8. ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ๐Ÿงช
Mock์„ ํ™œ์šฉํ•ด์„œ ์‹ค์ œ API ํ˜ธ์ถœ ์—†์ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑํ•ด.

โš ๏ธ ์ ˆ๋Œ€ ํ•˜๋ฉด ์•ˆ ๋˜๋Š” ๊ฒƒ๋“ค!
- API ํ‚ค๋ฅผ ๊นƒํ—ˆ๋ธŒ์— ์˜ฌ๋ฆฌ๊ธฐ (์ง„์งœ ํฐ์ผ ๋‚จ!)
- ํ”„๋กœ๋•์…˜์—์„œ verify=False SSL ๊ฒ€์ฆ ๋น„ํ™œ์„ฑํ™”
- ํƒ€์ž„์•„์›ƒ ์—†์ด ๋ฌดํ•œ ๋Œ€๊ธฐ
- ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋ฌด์‹œํ•˜๊ณ  ๋ฌดํ•œ ๋ฃจํ”„ ์š”์ฒญ
- ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์—†์ด .json() ๋ฐ”๋กœ ํ˜ธ์ถœ

๐Ÿš€ ๋งˆ๋ฌด๋ฆฌ - ์ด์ œ ์„ธ์ƒ๊ณผ ์—ฐ๊ฒฐํ•  ์ค€๋น„ ์™„๋ฃŒ!

์™€, ์—ฌ๊ธฐ๊นŒ์ง€ ์™”์–ด? ์ง„์งœ ๋Œ€๋‹จํ•ด! ๐Ÿ‘

์˜ค๋Š˜ ์šฐ๋ฆฌ๊ฐ€ ๋‹ค๋ฃฌ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณด๋ฉด:

๐ŸŒ ์›น API ๊ธฐ๋ณธ ๊ฐœ๋… - REST API, HTTP ๋ฉ”์„œ๋“œ, ์ƒํƒœ ์ฝ”๋“œ
๐Ÿ“ฆ requests ์„ค์น˜์™€ ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ• - GET/POST/PUT/DELETE
๐ŸŽฏ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ, ํ—ค๋”, ์ธ์ฆ - API Key, Bearer Token, OAuth
๐Ÿ›ก๏ธ ์—๋Ÿฌ ์ฒ˜๋ฆฌ์™€ ์žฌ์‹œ๋„ ๋กœ์ง - ์‹ค๋ฌด ํ•„์ˆ˜ ํŒจํ„ด
โšก Session ํ™œ์šฉ - ์„ฑ๋Šฅ ์ตœ์ ํ™”
๐ŸŒ ์‹ค์ „ API ์—ฐ๋™ - ๋‚ ์”จ, GitHub, ์นด์นด์˜ค
๐Ÿ—๏ธ API ํด๋ผ์ด์–ธํŠธ ํด๋ž˜์Šค ์„ค๊ณ„ - ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ
๐Ÿ“„ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ฒ˜๋ฆฌ - ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
๐Ÿ” OAuth 2.0 ํ๋ฆ„ - ์†Œ์…œ ๋กœ๊ทธ์ธ ์ดํ•ด
โš™๏ธ ๊ณ ๊ธ‰ ๊ธฐ๋ฒ• - ํƒ€์ž„์•„์›ƒ, ์ŠคํŠธ๋ฆฌ๋ฐ, ํ”„๋ก์‹œ
๐Ÿงช ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ• - Mock์„ ํ™œ์šฉํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ
๐Ÿ“Š ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ์ฒ˜๋ฆฌ - ์˜ˆ์˜ ๋ฐ”๋ฅธ API ํ˜ธ์ถœ
๐ŸŽ ์‹ค์ „ ๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ - ํ™˜์œจ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ด‡


์ด์ œ ํŒŒ์ด์ฌ์œผ๋กœ ๋‚ ์”จ ์•ฑ๋„ ๋งŒ๋“ค๊ณ , ํ™˜์œจ ๋ชจ๋‹ˆํ„ฐ๋ง๋„ ํ•˜๊ณ , ๊นƒํ—ˆ๋ธŒ ์ž๋™ํ™”๋„ ํ•˜๊ณ , ์นด์นด์˜ค ๋กœ๊ทธ์ธ๋„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด! ๐ŸŽ‰

API ์—ฐ๋™ ์‹ค๋ ฅ์„ ๋” ํ‚ค์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ ํŒŒ์ด์ฌ ์ „๋ฌธ๊ฐ€๋“ค์˜ ๋„์›€์„ ๋ฐ›์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ๋‹ค์–‘ํ•œ ์‹ค๋ฌด ๊ฒฝํ—˜์„ ๊ฐ€์ง„ ๊ฐœ๋ฐœ์ž๋“ค์ด API ์—ฐ๋™๋ถ€ํ„ฐ ์‹ค์ œ ์„œ๋น„์Šค ๊ฐœ๋ฐœ๊นŒ์ง€ ๋„์™€์ค„ ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿ’ช

์ž, ์ด์ œ ์ฝ”๋“œ ์—๋””ํ„ฐ ์—ด๊ณ  ์ง์ ‘ ๋งŒ๋“ค์–ด๋ด. ์ฒ˜์Œ์—” JSONPlaceholder ๊ฐ™์€ ๋ฌด๋ฃŒ ํ…Œ์ŠคํŠธ API๋กœ ์‹œ์ž‘ํ•ด์„œ, ์ ์  ์‹ค์ œ ์„œ๋น„์Šค API๋กœ ํ™•์žฅํ•ด๋‚˜๊ฐ€๋ฉด ๋ผ. ํ™”์ดํŒ…! ๐Ÿ”ฅ๐Ÿ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0