KR_BestPractices - somaz94/python-study GitHub Wiki
๋ช
ํํ๊ณ ์ผ๊ด๋ ์ฝ๋ ์์ฑ ๋ฐฉ๋ฒ์ ์ค๋ช
ํ๋ค.
# 1. ๋ช
ํํ ๋ณ์๋ช
๊ณผ ํจ์๋ช
์ฌ์ฉ
# ๋์ ์
def f(x):
return x * 2
# ์ข์ ์
def double_number(number: int) -> int:
return number * 2
# 2. ํด๋์ค ๊ตฌ์กฐํ
class User:
"""์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ด๋ฆฌํ๋ ํด๋์ค"""
def __init__(self, name: str, email: str):
self.name = name
self.email = email
@property
def display_name(self) -> str:
return f"{self.name} <{self.email}>"
โ
ํน์ง:
- ๋ช ํํ ์ด๋ฆ ์ฌ์ฉ
- ํ์ ํํธ ํ์ฉ
- ๋ฌธ์ํ ์ฃผ์ ์ถ๊ฐ
ํจ๊ณผ์ ์ธ ์์ธ ์ฒ๋ฆฌ ๋ฐ ์๋ฌ ๊ด๋ฆฌ ๋ฐฉ๋ฒ์ ๋ค๋ฃฌ๋ค.
from typing import Any, Dict, Optional
import logging
class CustomError(Exception):
"""์ฌ์ฉ์ ์ ์ ์์ธ"""
def __init__(self, message: str, error_code: int):
self.message = message
self.error_code = error_code
super().__init__(self.message)
def safe_operation(func):
"""์๋ฌ ์ฒ๋ฆฌ ๋ฐ์ฝ๋ ์ดํฐ"""
def wrapper(*args, **kwargs) -> Dict[str, Any]:
try:
result = func(*args, **kwargs)
return {'success': True, 'data': result}
except CustomError as e:
logging.error(f"Custom error: {e.message}")
return {
'success': False,
'error': e.message,
'error_code': e.error_code
}
except Exception as e:
logging.exception("Unexpected error occurred")
return {
'success': False,
'error': str(e),
'error_code': 500
}
return wrapper
โ
ํน์ง:
- ์ปค์คํ ์์ธ ์ ์
- ๋ก๊น ํ์ฉ
- ๋ฐ์ฝ๋ ์ดํฐ ํจํด ์ฌ์ฉ
- ๊ตฌ์กฐํ๋ ์๋ฌ ์๋ต
- ์์ธํ ์๋ฌ ์ ๋ณด ์ ๊ณต
์ ํ๋ฆฌ์ผ์ด์
์ค์ ์ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ด๋ค.
from dataclasses import dataclass
from typing import Optional
import yaml
import os
@dataclass
class DatabaseConfig:
host: str
port: int
username: str
password: str
database: str
@dataclass
class AppConfig:
debug: bool
secret_key: str
db: DatabaseConfig
class ConfigManager:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
self.config = self.load_config()
def load_config(self) -> AppConfig:
env = os.getenv('APP_ENV', 'development')
config_path = f'config/{env}.yml'
with open(config_path) as f:
config_data = yaml.safe_load(f)
return AppConfig(
debug=config_data['debug'],
secret_key=config_data['secret_key'],
db=DatabaseConfig(**config_data['database'])
)
โ
ํน์ง:
- ๋ฐ์ดํฐํด๋์ค ํ์ฉ
- ์ฑ๊ธํค ํจํด ๊ตฌํ
- ํ๊ฒฝ๋ณ ์ค์ ๋ถ๋ฆฌ
- ํ์ ์์ ์ฑ ๋ณด์ฅ
- ๊ณ์ธต์ ๊ตฌ์กฐํ
ํ์ง ๋์ ์ฝ๋๋ฅผ ์ํ ํ
์คํธ ์์ฑ ๋ฐฉ๋ฒ์ ์ค๋ช
ํ๋ค.
import pytest
from typing import List, Dict
class Calculator:
def add(self, x: int, y: int) -> int:
return x + y
def divide(self, x: int, y: int) -> float:
if y == 0:
raise ValueError("Cannot divide by zero")
return x / y
class TestCalculator:
@pytest.fixture
def calculator(self):
return Calculator()
def test_add(self, calculator):
assert calculator.add(2, 3) == 5
def test_divide(self, calculator):
assert calculator.divide(6, 2) == 3.0
def test_divide_by_zero(self, calculator):
with pytest.raises(ValueError):
calculator.divide(1, 0)
โ
ํน์ง:
- pytest ํ์ฉ
- ํฝ์ค์ฒ ์ฌ์ฉ
- ์์ธ ํ ์คํธ ํฌํจ
- ๋ช ํํ ํ ์คํธ ๊ตฌ์กฐ
- ๋จ์ ํ ์คํธ ๋ถ๋ฆฌ
ํจ๊ณผ์ ์ธ ์ฝ๋ ๋ฌธ์ํ ๋ฐฉ๋ฒ๊ณผ ๋๊ตฌ๋ฅผ ์ค๋ช
ํ๋ค.
from typing import List, Optional
class DocumentationExample:
"""
๋ฌธ์ํ ์์ ํด๋์ค
์ด ํด๋์ค๋ ํ์ด์ฌ ๋ฌธ์ํ์ ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ๋ณด์ฌ์ค๋ค.
Attributes:
name (str): ๊ฐ์ฒด์ ์ด๋ฆ
value (int): ๊ฐ์ฒด์ ๊ฐ
"""
def __init__(self, name: str, value: int):
self.name = name
self.value = value
def process_data(self, data: List[int]) -> Optional[float]:
"""
๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ๋ค.
Args:
data (List[int]): ์ฒ๋ฆฌํ ์ ์ ๋ฆฌ์คํธ
Returns:
Optional[float]: ์ฒ๋ฆฌ๋ ๊ฒฐ๊ณผ๊ฐ, ์คํจ์ None
Raises:
ValueError: ๋น ๋ฆฌ์คํธ๊ฐ ์
๋ ฅ๋ ๊ฒฝ์ฐ
"""
if not data:
raise ValueError("Empty data list")
try:
return sum(data) / len(data)
except Exception:
return None
โ
ํน์ง:
- ๋ํ๋ฉํธ ์คํธ๋ง ํ์ฉ
- ํ์ ํํธ ํฌํจ
- ์์ธ ๋ช ์ธ ๊ธฐ๋ก
- ์ผ๊ด๋ ๋ฌธ์ํ ์คํ์ผ
- ์์ฑ ๋ฐ ๋ฉ์๋ ์ค๋ช
์ค์ ์ ํ๋ฆฌ์ผ์ด์
์์ ์ฌ์ฉํ ์ ์๋ ๋ชจ๋ฒ ์ฌ๋ก ์์ ๋ฅผ ์ดํด๋ณธ๋ค.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
class UserBase(BaseModel):
name: str
email: str
class UserCreate(UserBase):
password: str
class User(UserBase):
id: int
is_active: bool
class UserAPI:
def __init__(self):
self.app = FastAPI()
self.setup_routes()
def setup_routes(self):
@self.app.post("/users/", response_model=User)
async def create_user(user: UserCreate):
return await self.create_user_handler(user)
async def create_user_handler(self, user: UserCreate) -> User:
# ์ฌ์ฉ์ ์์ฑ ๋ก์ง
pass
โ
ํน์ง:
- FastAPI ํ์ฉ
- Pydantic ๋ชจ๋ธ ์ฌ์ฉ
- ์ปจํ ์คํธ ๋งค๋์ ํ์ฉ
from contextlib import contextmanager
from typing import Generator
from sqlalchemy.orm import Session
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
class DatabaseManager:
def __init__(self, connection_string: str):
self.engine = create_engine(connection_string)
self.SessionLocal = sessionmaker(bind=self.engine)
@contextmanager
def get_session(self) -> Generator[Session, None, None]:
session = self.SessionLocal()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
def execute_transaction(self, operations):
with self.get_session() as session:
return operations(session)
โ
ํน์ง:
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ๊ทผ ํจํด ๋ณด์ฌ์ค
import asyncio
from typing import List, Dict, Any
import aiohttp
class AsyncDataFetcher:
def __init__(self, base_url: str):
self.base_url = base_url
self.session = None
async def __aenter__(self):
self.session = aiohttp.ClientSession()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self.session:
await self.session.close()
async def fetch_data(self, endpoint: str) -> Dict[str, Any]:
if not self.session:
raise RuntimeError("์ธ์
์ด ์ด๊ธฐํ๋์ง ์์์ต๋๋ค. ์ปจํ
์คํธ ๋งค๋์ ๋ก ์ฌ์ฉํ์ธ์.")
url = f"{self.base_url}/{endpoint}"
async with self.session.get(url) as response:
if response.status != 200:
raise ValueError(f"๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ ์คํจ: {response.status}")
return await response.json()
async def fetch_multiple(self, endpoints: List[str]) -> List[Dict[str, Any]]:
tasks = [self.fetch_data(endpoint) for endpoint in endpoints]
return await asyncio.gather(*tasks, return_exceptions=True)
# ์ฌ์ฉ ์:
async def main():
async with AsyncDataFetcher("https://api.example.com") as fetcher:
data = await fetcher.fetch_multiple(["users", "products", "orders"])
print(data)
# ์คํ
# asyncio.run(main())
โ
ํน์ง:
- ๋น๋๊ธฐ ์ฝ๋ ๊ตฌ์ฑ
- ์ปจํ ์คํธ ๋งค๋์ ํ์ฉ
- ํ์ ์์ ์ฑ ๋ณด์ฅ
- ์๋ฌ ์ฒ๋ฆฌ ๊ณ ๋ ค
import logging
import time
from functools import wraps
from typing import Any, Callable, Dict
# ๋ก๊ฑฐ ์ค์
def setup_logger(name: str, level=logging.INFO) -> logging.Logger:
logger = logging.getLogger(name)
logger.setLevel(level)
if not logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
return logger
# ์ฑ๋ฅ ์ธก์ ๋ฐ์ฝ๋ ์ดํฐ
def measure_performance(logger: logging.Logger):
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs) -> Any:
start_time = time.time()
result = func(*args, **kwargs)
execution_time = time.time() - start_time
logger.info(f"ํจ์ {func.__name__} ์คํ ์๊ฐ: {execution_time:.4f}์ด")
return result
return wrapper
return decorator
# ์ฌ์ฉ ์์
logger = setup_logger("app_logger")
@measure_performance(logger)
def process_data(data: Dict[str, Any]) -> Dict[str, Any]:
# ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๋ก์ง
time.sleep(0.5) # ์ฒ๋ฆฌ ์๊ฐ ์๋ฎฌ๋ ์ด์
return {"processed": True, "original": data}
# ์คํ
# result = process_data({"id": 1, "value": "test"})
โ
ํน์ง:
- ๋ก๊น ๋ฐ ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง
- ์ปจํ ์คํธ ๋งค๋์ ํ์ฉ
- ํ์ ์์ ์ฑ ๋ณด์ฅ
- ์๋ฌ ์ฒ๋ฆฌ ๊ณ ๋ ค
โ
ํน์ง:
- API ๋์์ธ ํจํด
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ํธ๋์ญ์ ๊ด๋ฆฌ
- ๋น๋๊ธฐ ์ฝ๋ ๊ตฌ์ฑ
- ๋ก๊น ๋ฐ ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง
- ์ปจํ ์คํธ ๋งค๋์ ํ์ฉ
- ํ์ ์์ ์ฑ ๋ณด์ฅ
- ์๋ฌ ์ฒ๋ฆฌ ๊ณ ๋ ค
ํ์ด์ฌ ์ฝ๋์ ์ฑ๋ฅ์ ๊ฐ์ ํ๋ ๋ฐฉ๋ฒ์ ์ค๋ช
ํ๋ค.
from collections import defaultdict, deque
from typing import List, Dict, Set, Deque
# ๋ฆฌ์คํธ๋ณด๋ค ํจ์จ์ ์ธ ์์
์ ์ํ ๋ฐํฌ ํ์ฉ
def sliding_window(data: List[int], window_size: int) -> List[List[int]]:
"""
๋ฐํฌ๋ฅผ ์ฌ์ฉํ ํจ์จ์ ์ธ ์ฌ๋ผ์ด๋ฉ ์๋์ฐ ๊ตฌํ
"""
result = []
window: Deque[int] = deque(maxlen=window_size)
for item in data:
window.append(item)
if len(window) == window_size:
result.append(list(window))
return result
# ๋ฃจํ ๋์ ๋์
๋๋ฆฌ ํ์ฉ
def count_occurrences(items: List[str]) -> Dict[str, int]:
"""
๊ฐ ํญ๋ชฉ์ ๋ฐ์ ํ์๋ฅผ ๊ณ์ฐ
"""
counter = defaultdict(int)
for item in items:
counter[item] += 1
return dict(counter)
# ์งํฉ์ ์ฌ์ฉํ ํจ์จ์ ์ธ ์ค๋ณต ์ ๊ฑฐ
def find_unique(items: List[str]) -> List[str]:
"""
์งํฉ์ ์ฌ์ฉํ์ฌ ์ค๋ณต ์ ๊ฑฐ ๋ฐ ์์ ์ ์ง
"""
seen: Set[str] = set()
return [item for item in items if not (item in seen or seen.add(item))]
import functools
import time
from typing import Dict, Any, Callable, TypeVar
T = TypeVar('T')
# ๋ฉ๋ชจ์ด์ ์ด์
์ ํตํ ๋น์ฉ์ด ๋ง์ด ๋๋ ๊ณ์ฐ ์ต์ ํ
@functools.lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
"""
ํผ๋ณด๋์น ์์ด ๊ณ์ฐ (์บ์ฑ ์ฌ์ฉ)
"""
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# ์ ๋๋ ์ดํฐ๋ฅผ ์ฌ์ฉํ ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ ์ธ ์ฒ๋ฆฌ
def process_large_file(file_path: str, chunk_size: int = 1024):
"""
๋์ฉ๋ ํ์ผ์ ์ฒญํฌ ๋จ์๋ก ์ฒ๋ฆฌํ๋ ์ ๋๋ ์ดํฐ
"""
with open(file_path, 'r') as f:
while True:
data = f.read(chunk_size)
if not data:
break
yield data
# ๋ฆฌ์คํธ ์ปดํ๋ฆฌํจ์
์ฌ์ฉ (๋ฃจํ๋ณด๋ค ํจ์จ์ )
def transform_data(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
๋ฆฌ์คํธ ์ปดํ๋ฆฌํจ์
์ ์ฌ์ฉํ ๋ฐ์ดํฐ ๋ณํ
"""
return [{
'id': item['id'],
'name': item['name'].upper(),
'value': item['value'] * 2
} for item in data if item['active']]
โ
ํน์ง:
- ์ ์ ํ ์๋ฃ๊ตฌ์กฐ ์ ํ
- ๋ฉ๋ชจ์ด์ ์ด์ ํ์ฉ
- ์ ๋๋ ์ดํฐ์ ์ดํฐ๋ ์ดํฐ ํ์ฉ
- ๋ฆฌ์คํธ ์ปดํ๋ฆฌํจ์ ์ต์ ํ
- ๋ถํ์ํ ๊ณ์ฐ ๋ฐฉ์ง
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๊ณ ๋ ค
ํ์ด์ฌ ์ ํ๋ฆฌ์ผ์ด์
์ ๋ณด์์ ๊ฐํํ๋ ๋ฐฉ๋ฒ์ ์ค๋ช
ํ๋ค.
import hashlib
import os
import hmac
import binascii
from typing import Tuple
def hash_password(password: str) -> Tuple[str, str]:
"""
๋น๋ฐ๋ฒํธ๋ฅผ ์์ ํ๊ฒ ํด์ฑํ๊ณ ์ํธ์ ํด์๋ฅผ ๋ฐํํ๋ค
Args:
password: ํด์ฑํ ์๋ณธ ๋น๋ฐ๋ฒํธ
Returns:
(salt, hash) ํํ
"""
# ๋๋ค ์ํธ ์์ฑ
salt = os.urandom(32)
# ๋น๋ฐ๋ฒํธ ํด์ฑ
pw_hash = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
100000 # ๋ฐ๋ณต ํ์
)
# 16์ง์ ๋ฌธ์์ด๋ก ๋ณํ
salt_hex = binascii.hexlify(salt).decode('utf-8')
hash_hex = binascii.hexlify(pw_hash).decode('utf-8')
return salt_hex, hash_hex
def verify_password(password: str, salt_hex: str, stored_hash: str) -> bool:
"""
์
๋ ฅ๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ ์ฅ๋ ํด์์ ์ผ์นํ๋์ง ํ์ธํ๋ค
Args:
password: ํ์ธํ ๋น๋ฐ๋ฒํธ
salt_hex: ์ ์ฅ๋ ์ํธ(16์ง์ ๋ฌธ์์ด)
stored_hash: ์ ์ฅ๋ ํด์(16์ง์ ๋ฌธ์์ด)
Returns:
๋น๋ฐ๋ฒํธ ์ผ์น ์ฌ๋ถ
"""
# ์ํธ๋ฅผ ๋ฐ์ดํธ๋ก ๋ณํ
salt = binascii.unhexlify(salt_hex)
# ์
๋ ฅ๋ ๋น๋ฐ๋ฒํธ ํด์ฑ
pw_hash = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
100000 # ๋ฐ๋ณต ํ์ (์ ์ฅ ์์ ๋์ผ)
)
# ํด์ ๋น๊ต (ํ์ด๋ฐ ๊ณต๊ฒฉ ๋ฐฉ์ง๋ฅผ ์ํด hmac.compare_digest ์ฌ์ฉ)
new_hash = binascii.hexlify(pw_hash).decode('utf-8')
return hmac.compare_digest(new_hash, stored_hash)
import sqlite3
from typing import List, Dict, Any, Tuple
class SafeDatabase:
"""์์ ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์
์ ์ํ ํด๋์ค"""
def __init__(self, db_path: str):
self.db_path = db_path
self.connection = None
def connect(self):
self.connection = sqlite3.connect(self.db_path)
self.connection.row_factory = sqlite3.Row
def close(self):
if self.connection:
self.connection.close()
def __enter__(self):
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
def safe_query(self, query: str, params: Tuple = ()) -> List[Dict[str, Any]]:
"""
๋งค๊ฐ๋ณ์ํ๋ ์ฟผ๋ฆฌ๋ฅผ ์์ ํ๊ฒ ์คํํ๋ค
Args:
query: SQL ์ฟผ๋ฆฌ(๋งค๊ฐ๋ณ์๋ ? ์ฌ์ฉ)
params: ์ฟผ๋ฆฌ ๋งค๊ฐ๋ณ์
Returns:
์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ๋ฆฌ์คํธ
"""
cursor = self.connection.cursor()
cursor.execute(query, params)
results = [dict(row) for row in cursor.fetchall()]
cursor.close()
return results
# ์๋ชป๋ ๋ฐฉ๋ฒ (์ฐธ๊ณ ์ฉ์ผ๋ก๋ง ํ์)
def unsafe_query(self, user_input: str) -> List[Dict[str, Any]]:
"""
์ ๋ ์ฌ์ฉํ๋ฉด ์ ๋๋ ๋ฐฉ๋ฒ
SQL ์ธ์ ์
์ ์ทจ์ฝํจ
"""
query = f"SELECT * FROM users WHERE username = '{user_input}'"
cursor = self.connection.cursor()
cursor.execute(query)
results = [dict(row) for row in cursor.fetchall()]
cursor.close()
return results
โ
ํน์ง:
- ์์ ํ ๋น๋ฐ๋ฒํธ ํด์ฑ
- ์ํธ ์ฌ์ฉ
- ๋งค๊ฐ๋ณ์ํ๋ SQL ์ฟผ๋ฆฌ
- ๋ณด์ ์ทจ์ฝ์ ๋ฐฉ์ง
- ํ์ด๋ฐ ๊ณต๊ฒฉ ๋ฐฉ์ด
- ์ ์ ํ ์ํธํ ๊ธฐ๋ฒ ์ฌ์ฉ
โ
๋ชจ๋ฒ ์ฌ๋ก:
- ์ผ๊ด๋ ์ฝ๋ ์คํ์ผ: PEP 8 ์ค์, ์๋ ํฌ๋งทํฐ ์ฌ์ฉ(Black, YAPF)
- ๋ช ํํ ๋ช ๋ช ๊ท์น: ์๋ฏธ ์๋ ๋ณ์๋ช , ํจ์๋ช ์ฌ์ฉ
- ํ์ ํํธ ํ์ฉ: ์ฝ๋์ ๊ฐ๋ ์ฑ๊ณผ ์์ ์ฑ ํฅ์
- ์ ์ ํ ์๋ฌ ์ฒ๋ฆฌ: ์์ธ ๊ณ์ธต ๊ตฌ์กฐํ, ์์ธํ ์๋ฌ ๋ฉ์์ง
- ํ๊ฒฝ ๋ณ์ ๋ถ๋ฆฌ: ๊ฐ๋ฐ, ํ ์คํธ, ํ๋ก๋์ ์ค์ ๊ตฌ๋ถ
- ์ฒ ์ ํ ํ ์คํธ: ๋จ์ ํ ์คํธ, ํตํฉ ํ ์คํธ ์์ฑ
- ๋ฌธ์ํ ์ต๊ดํ: ํจ์, ํด๋์ค, ๋ชจ๋ ์์ค์ ๋ฌธ์ํ
- ๋ณด์ ๊ณ ๋ ค: ์์ ํ ์ํธํ, ์ ๋ ฅ ๊ฒ์ฆ
- ์ฑ๋ฅ ์ต์ ํ: ์ ์ ํ ์๋ฃ๊ตฌ์กฐ, ์๊ณ ๋ฆฌ์ฆ ์ ํ
- ์ฝ๋ ๋ฆฌ๋ทฐ ํ์ฑํ: ํผ์ด ๋ฆฌ๋ทฐ๋ฅผ ํตํ ํ์ง ๊ฐ์
- ๋ํ๋์ ๊ด๋ฆฌ: ์์กด์ฑ ์ต์ํ, ๋ฒ์ ๊ณ ์
- ๋ก๊น ์ ๋ต ์๋ฆฝ: ๊ตฌ์กฐํ๋ ๋ก๊น , ์ ์ ํ ๋ก๊ทธ ๋ ๋ฒจ
- ๋ฆฌํฉํ ๋ง ์ ๊ธฐํ: ๊ธฐ์ ๋ถ์ฑ ๋์ ๋ฐฉ์ง
- ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ: DRY ์์น, ์ ์ ํ ์ถ์ํ
- ๋ง์ดํฌ๋ก์๋น์ค ๊ณ ๋ ค: ๊ธฐ๋ฅ๋ณ ๋ถ๋ฆฌ, ํ์ฅ์ฑ ํ๋ณด