์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ”— ํŒŒ์ด์ฌ๊ณผ FastAPI๋กœ ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ

๐Ÿ”— ํŒŒ์ด์ฌ๊ณผ FastAPI๋กœ ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ

์‹ค์ „ ํ”„๋กœ์ ํŠธ๋กœ ๋ฐฐ์šฐ๋Š” Web3 ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์˜ ๋ชจ๋“  ๊ฒƒ

Block 1 Block 2 Block 3 Block 4 FastAPI Validator Blockchain Data Verification Server

๐Ÿš€ ์™œ ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ์„œ๋ฒ„๊ฐ€ ํ•„์š”ํ• ๊นŒ?

์•ˆ๋…•! ์˜ค๋Š˜์€ ์ •๋ง ์žฌ๋ฏธ์žˆ๋Š” ์ฃผ์ œ๋ฅผ ๊ฐ€์ง€๊ณ  ์™”์–ด. ๋ฐ”๋กœ ํŒŒ์ด์ฌ๊ณผ FastAPI๋ฅผ ํ™œ์šฉํ•ด์„œ ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ์„œ๋ฒ„๋ฅผ ๋งŒ๋“œ๋Š” ๊ฑฐ์•ผ. ๐ŸŽฏ

์š”์ฆ˜ Web3 ์‹œ๋Œ€๊ฐ€ ๋˜๋ฉด์„œ ๋ธ”๋ก์ฒด์ธ ๊ธฐ์ˆ ์ด ์ •๋ง ๋งŽ์ด ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์ž–์•„? ๊ทธ๋Ÿฐ๋ฐ ๋ฌธ์ œ๋Š” ๋ธ”๋ก์ฒด์ธ ๋„คํŠธ์›Œํฌ์—์„œ ์ง์ ‘ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฑด ์ƒ๊ฐ๋ณด๋‹ค ๋А๋ฆฌ๊ณ  ๋น„์šฉ๋„ ๋งŽ์ด ๋“ ๋‹ค๋Š” ๊ฑฐ์•ผ. ํŠนํžˆ ์ด๋”๋ฆฌ์›€ ๊ฐ™์€ ๋ฉ”์ธ๋„ท์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•  ๋•Œ๋งˆ๋‹ค ๊ฐ€์Šค๋น„๊ฐ€ ๋“ค์–ด๊ฐ€๊ฑฐ๋‚˜, RPC ํ˜ธ์ถœ ์ œํ•œ์— ๊ฑธ๋ฆฌ๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„.

๊ทธ๋ž˜์„œ ์šฐ๋ฆฌ๋Š” ์ค‘๊ฐ„์— ๊ฒ€์ฆ ์„œ๋ฒ„๋ฅผ ๋‘๋Š” ๊ฑฐ์•ผ! ์ด ์„œ๋ฒ„๊ฐ€ ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์„œ ๊ฒ€์ฆํ•˜๊ณ , ์บ์‹ฑ๋„ ํ•˜๊ณ , ์‚ฌ์šฉ์ž์—๊ฒŒ ๋น ๋ฅด๊ฒŒ ์ œ๊ณตํ•˜๋Š” ์—ญํ• ์„ ํ•ด์ฃผ์ง€. ๋งˆ์น˜ ๋ธ”๋ก์ฒด์ธ๊ณผ ์‚ฌ์šฉ์ž ์‚ฌ์ด์˜ ๋˜‘๋˜‘ํ•œ ์ค‘๊ฐœ์ž ๊ฐ™์€ ๊ฑฐ๋ผ๊ณ  ๋ณด๋ฉด ๋ผ. ๐Ÿค

๐Ÿ’ก ์ด ๊ธ€์—์„œ ๋ฐฐ์šธ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋“ค

โœ… FastAPI๋ฅผ ํ™œ์šฉํ•œ ๊ณ ์„ฑ๋Šฅ API ์„œ๋ฒ„ ๊ตฌ์ถ•
โœ… Web3.py๋ฅผ ์ด์šฉํ•œ ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ์กฐํšŒ
โœ… ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ ๋กœ์ง
โœ… ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์™€ ์บ์‹ฑ ์ „๋žต
โœ… ์‹ค์ „ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ์„ค๊ณ„
โœ… ๋ณด์•ˆ๊ณผ ์—๋Ÿฌ ํ•ธ๋“ค๋ง

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์ค€๋น„ํ•˜๊ธฐ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ? ๋จผ์ € ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๋ถ€ํ„ฐ ์„ธํŒ…ํ•ด์•ผ ํ•ด. ํ•„์š”ํ•œ ๋„๊ตฌ๋“ค์„ ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณด์ž.

ํ•„์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜

์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•  ์ฃผ์š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์€ ๋‹ค์Œ๊ณผ ๊ฐ™์•„:

pip install fastapi uvicorn web3 python-dotenv redis pydantic aiohttp
๊ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํ•˜๋Š” ์—ญํ• ์„ ๊ฐ„๋‹จํžˆ ์„ค๋ช…ํ•ด์ค„๊ฒŒ:

FastAPI: ์šฐ๋ฆฌ์˜ ๋ฉ”์ธ ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ๋น ๋ฅด๊ณ  ํ˜„๋Œ€์ ์ธ API๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ์ง€. ํƒ€์ž… ํžŒํŒ…์„ ์ง€์›ํ•ด์„œ ์ฝ”๋“œ ์•ˆ์ •์„ฑ๋„ ๋†’๊ณ , ์ž๋™์œผ๋กœ API ๋ฌธ์„œ๋„ ๋งŒ๋“ค์–ด์ค˜. โšก

Uvicorn: FastAPI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•˜๋Š” ASGI ์„œ๋ฒ„์•ผ. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ์ง€์›ํ•ด์„œ ์„ฑ๋Šฅ์ด ์ •๋ง ์ข‹์•„.

Web3.py: ์ด๋”๋ฆฌ์›€ ๋ธ”๋ก์ฒด์ธ๊ณผ ์ƒํ˜ธ์ž‘์šฉํ•˜๊ธฐ ์œ„ํ•œ ํŒŒ์ด์ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ํ˜ธ์ถœ, ํŠธ๋žœ์žญ์…˜ ์กฐํšŒ ๋“ฑ ๋ชจ๋“  ๊ฑธ ํ•  ์ˆ˜ ์žˆ์–ด.

Redis: ์บ์‹ฑ์„ ์œ„ํ•œ ์ธ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์•ผ. ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๋งค๋ฒˆ ์กฐํšŒํ•˜๋ฉด ๋А๋ฆฌ๋‹ˆ๊นŒ, ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ดํ„ฐ๋Š” Redis์— ์ €์žฅํ•ด๋‘๋Š” ๊ฑฐ์ง€.

Pydantic: ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ๊ณผ ์„ค์ • ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. FastAPI์™€ ์ฐฐ๋–ก๊ถํ•ฉ์ด์ง€!

๐Ÿ” ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

โ€ข Python 3.8 ์ด์ƒ ๋ฒ„์ „
โ€ข Redis ์„œ๋ฒ„ (๋กœ์ปฌ ๋˜๋Š” ํด๋ผ์šฐ๋“œ)
โ€ข ์ด๋”๋ฆฌ์›€ ๋…ธ๋“œ ์ ‘๊ทผ ๊ถŒํ•œ (Infura, Alchemy ๋“ฑ)
โ€ข ์ฝ”๋“œ ์—๋””ํ„ฐ (VS Code ์ถ”์ฒœ)
โ€ข Postman ๋˜๋Š” Thunder Client (API ํ…Œ์ŠคํŠธ์šฉ)

๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ์„ค๊ณ„ํ•˜๊ธฐ

์ข‹์€ ํ”„๋กœ์ ํŠธ๋Š” ์ข‹์€ ๊ตฌ์กฐ์—์„œ ์‹œ์ž‘๋ผ. ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค ํ”„๋กœ์ ํŠธ์˜ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ๋จผ์ € ์‚ดํŽด๋ณด์ž:

blockchain-validator/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ main.py              # FastAPI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ง„์ž…์ 
โ”‚   โ”œโ”€โ”€ config.py            # ์„ค์ • ๊ด€๋ฆฌ
โ”‚   โ”œโ”€โ”€ models/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ blockchain.py    # ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ
โ”‚   โ”‚   โ””โ”€โ”€ response.py      # API ์‘๋‹ต ๋ชจ๋ธ
โ”‚   โ”œโ”€โ”€ services/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ blockchain.py    # ๋ธ”๋ก์ฒด์ธ ์„œ๋น„์Šค ๋กœ์ง
โ”‚   โ”‚   โ”œโ”€โ”€ validator.py     # ๊ฒ€์ฆ ๋กœ์ง
โ”‚   โ”‚   โ””โ”€โ”€ cache.py         # ์บ์‹ฑ ๋กœ์ง
โ”‚   โ”œโ”€โ”€ api/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ routes.py        # API ๋ผ์šฐํŠธ
โ”‚   โ”‚   โ””โ”€โ”€ dependencies.py  # ์˜์กด์„ฑ ์ฃผ์ž…
โ”‚   โ””โ”€โ”€ utils/
โ”‚       โ”œโ”€โ”€ __init__.py
โ”‚       โ”œโ”€โ”€ helpers.py       # ํ—ฌํผ ํ•จ์ˆ˜๋“ค
โ”‚       โ””โ”€โ”€ exceptions.py    # ์ปค์Šคํ…€ ์˜ˆ์™ธ
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ””โ”€โ”€ test_validator.py
โ”œโ”€โ”€ .env                      # ํ™˜๊ฒฝ ๋ณ€์ˆ˜
โ”œโ”€โ”€ requirements.txt
โ””โ”€โ”€ README.md
์ด๋ ‡๊ฒŒ ๊ตฌ์กฐ๋ฅผ ๋‚˜๋ˆ„๋ฉด ์ฝ”๋“œ ๊ด€๋ฆฌ๊ฐ€ ์ •๋ง ํŽธํ•ด์ ธ. ๊ฐ ๋ชจ๋“ˆ์ด ๋ช…ํ™•ํ•œ ์—ญํ• ์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์„œ ๋‚˜์ค‘์— ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ •ํ•  ๋•Œ๋„ ์–ด๋””๋ฅผ ๋ด์•ผ ํ• ์ง€ ๋ฐ”๋กœ ์•Œ ์ˆ˜ ์žˆ๊ฑฐ๋“ . ๐Ÿ“‚

โš™๏ธ ์„ค์ • ํŒŒ์ผ ์ž‘์„ฑํ•˜๊ธฐ

๋จผ์ € ํ™˜๊ฒฝ ์„ค์ •๋ถ€ํ„ฐ ํ•ด๋ณด์ž. `.env` ํŒŒ์ผ์— ๋ฏผ๊ฐํ•œ ์ •๋ณด๋“ค์„ ์ €์žฅํ•  ๊ฑฐ์•ผ:

# .env
ETHEREUM_RPC_URL=https://mainnet.infura.io/v3/YOUR_PROJECT_ID
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
CACHE_TTL=300
API_KEY=your_secret_api_key_here
๊ทธ๋ฆฌ๊ณ  `config.py`์—์„œ ์ด ์„ค์ •๋“ค์„ ๊ด€๋ฆฌํ•ด:

from pydantic import BaseSettings
from functools import lru_cache

class Settings(BaseSettings):
    ethereum_rpc_url: str
    redis_host: str = "localhost"
    redis_port: int = 6379
    redis_db: int = 0
    cache_ttl: int = 300
    api_key: str
    
    class Config:
        env_file = ".env"

@lru_cache()
def get_settings():
    return Settings()

settings = get_settings()
`@lru_cache()` ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์„ค์ • ๊ฐ์ฒด๊ฐ€ ํ•œ ๋ฒˆ๋งŒ ์ƒ์„ฑ๋˜๊ณ  ์žฌ์‚ฌ์šฉ๋ผ. ์„ฑ๋Šฅ ์ตœ์ ํ™”์˜ ๊ธฐ๋ณธ์ด์ง€! ๐Ÿš€

๐Ÿ”— ๋ธ”๋ก์ฒด์ธ ์—ฐ๊ฒฐ ์„ค์ •ํ•˜๊ธฐ

์ด์ œ ๋ธ”๋ก์ฒด์ธ๊ณผ ์—ฐ๊ฒฐํ•˜๋Š” ๋ถ€๋ถ„์„ ๋งŒ๋“ค์–ด๋ณด์ž. `services/blockchain.py` ํŒŒ์ผ์„ ์ž‘์„ฑํ•  ๊ฑฐ์•ผ:

from web3 import Web3
from web3.middleware import geth_poa_middleware
from app.config import settings
import logging

logger = logging.getLogger(__name__)

class BlockchainService:
    def __init__(self):
        self.w3 = Web3(Web3.HTTPProvider(settings.ethereum_rpc_url))
        
        # PoA ๋„คํŠธ์›Œํฌ๋ฅผ ์œ„ํ•œ ๋ฏธ๋“ค์›จ์–ด ์ถ”๊ฐ€ (ํ•„์š”์‹œ)
        self.w3.middleware_onion.inject(geth_poa_middleware, layer=0)
        
        if not self.w3.is_connected():
            raise ConnectionError("๋ธ”๋ก์ฒด์ธ ๋…ธ๋“œ์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
        
        logger.info("๋ธ”๋ก์ฒด์ธ ๋…ธ๋“œ ์—ฐ๊ฒฐ ์„ฑ๊ณต")
    
    async def get_block(self, block_number: int):
        """ํŠน์ • ๋ธ”๋ก ์ •๋ณด ์กฐํšŒ"""
        try:
            block = self.w3.eth.get_block(block_number)
            return block
        except Exception as e:
            logger.error(f"๋ธ”๋ก ์กฐํšŒ ์‹คํŒจ: {e}")
            raise
    
    async def get_transaction(self, tx_hash: str):
        """ํŠธ๋žœ์žญ์…˜ ์ •๋ณด ์กฐํšŒ"""
        try:
            tx = self.w3.eth.get_transaction(tx_hash)
            return tx
        except Exception as e:
            logger.error(f"ํŠธ๋žœ์žญ์…˜ ์กฐํšŒ ์‹คํŒจ: {e}")
            raise
    
    async def get_latest_block_number(self):
        """์ตœ์‹  ๋ธ”๋ก ๋ฒˆํ˜ธ ์กฐํšŒ"""
        return self.w3.eth.block_number
    
    def verify_transaction_hash(self, tx_hash: str) -> bool:
        """ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ ํ˜•์‹ ๊ฒ€์ฆ"""
        return self.w3.is_address(tx_hash) or len(tx_hash) == 66

blockchain_service = BlockchainService()
์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ํฌ์ธํŠธ๊ฐ€ ๋ช‡ ๊ฐ€์ง€ ์žˆ์–ด:

1. ์—ฐ๊ฒฐ ํ™•์ธ: `is_connected()`๋กœ ๋…ธ๋“œ ์—ฐ๊ฒฐ ์ƒํƒœ๋ฅผ ๋จผ์ € ์ฒดํฌํ•ด. ์—ฐ๊ฒฐ์ด ์•ˆ ๋˜๋ฉด ๋ฐ”๋กœ ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œ์ผœ์„œ ๋‚˜์ค‘์— ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋Š” ๊ฑธ ๋ฐฉ์ง€ํ•˜๋Š” ๊ฑฐ์ง€.

2. ๋ฏธ๋“ค์›จ์–ด: PoA(Proof of Authority) ๋„คํŠธ์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด `geth_poa_middleware`๊ฐ€ ํ•„์š”ํ•ด. ์ด๊ฒŒ ์—†์œผ๋ฉด ๋ธ”๋ก ์กฐํšŒํ•  ๋•Œ ์—๋Ÿฌ๊ฐ€ ๋‚  ์ˆ˜ ์žˆ์–ด.

3. ์—๋Ÿฌ ํ•ธ๋“ค๋ง: ๋ชจ๋“  ๋ฉ”์„œ๋“œ์— try-except๋ฅผ ๋„ฃ์–ด์„œ ์˜ˆ์™ธ ์ƒํ™ฉ์„ ์ž˜ ์ฒ˜๋ฆฌํ•ด์ค˜์•ผ ํ•ด. ๋ธ”๋ก์ฒด์ธ ์กฐํšŒ๋Š” ๋„คํŠธ์›Œํฌ ๋ฌธ์ œ๋กœ ์‹คํŒจํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ .

๐Ÿ’ก Pro Tip: RPC ์—”๋“œํฌ์ธํŠธ ์„ ํƒ

๋ฌด๋ฃŒ RPC ์—”๋“œํฌ์ธํŠธ๋Š” ์š”์ฒญ ์ œํ•œ์ด ์žˆ์–ด. ์‹ค์ œ ์„œ๋น„์Šค๋ฅผ ์šด์˜ํ•œ๋‹ค๋ฉด Infura๋‚˜ Alchemy์˜ ์œ ๋ฃŒ ํ”Œ๋žœ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ๋˜๋Š” ์ž์ฒด ๋…ธ๋“œ๋ฅผ ์šด์˜ํ•˜๋Š” ๊ฒƒ๋„ ๊ณ ๋ คํ•ด๋ณผ ๋งŒํ•ด. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋ธ”๋ก์ฒด์ธ ๋…ธ๋“œ ์šด์˜ ์ „๋ฌธ๊ฐ€๋ฅผ ์ฐพ์•„๋ณผ ์ˆ˜๋„ ์žˆ์ง€! ๐ŸŽ“

๐Ÿ” ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋กœ์ง ๊ตฌํ˜„ํ•˜๊ธฐ

์ด์ œ ํ•ต์‹ฌ์ธ ๊ฒ€์ฆ ๋กœ์ง์„ ๋งŒ๋“ค์–ด๋ณด์ž. `services/validator.py` ํŒŒ์ผ์ด์•ผ:

from typing import Dict, Any, Optional
from web3 import Web3
from app.services.blockchain import blockchain_service
import hashlib
import logging

logger = logging.getLogger(__name__)

class BlockchainValidator:
    
    @staticmethod
    def verify_block_hash(block_data: Dict[str, Any]) -> bool:
        """๋ธ”๋ก ํ•ด์‹œ ๊ฒ€์ฆ"""
        try:
            # ๋ธ”๋ก ๋ฐ์ดํ„ฐ์—์„œ ํ•ด์‹œ ์ œ์™ธ
            block_without_hash = {
                k: v for k, v in block_data.items() 
                if k not in ['hash', 'proofOfWork']
            }
            
            # ๋ธ”๋ก ๋ฐ์ดํ„ฐ๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜
            block_string = str(sorted(block_without_hash.items()))
            
            # SHA-256 ํ•ด์‹œ ๊ณ„์‚ฐ
            calculated_hash = hashlib.sha256(
                block_string.encode()
            ).hexdigest()
            
            # ์›๋ณธ ํ•ด์‹œ์™€ ๋น„๊ต
            original_hash = block_data.get('hash', '').replace('0x', '')
            
            return calculated_hash == original_hash
            
        except Exception as e:
            logger.error(f"๋ธ”๋ก ํ•ด์‹œ ๊ฒ€์ฆ ์‹คํŒจ: {e}")
            return False
    
    @staticmethod
    async def verify_transaction_inclusion(
        tx_hash: str, 
        block_number: int
    ) -> bool:
        """ํŠธ๋žœ์žญ์…˜์ด ํŠน์ • ๋ธ”๋ก์— ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ๊ฒ€์ฆ"""
        try:
            # ํŠธ๋žœ์žญ์…˜ ์ •๋ณด ์กฐํšŒ
            tx = await blockchain_service.get_transaction(tx_hash)
            
            if tx is None:
                return False
            
            # ๋ธ”๋ก ๋ฒˆํ˜ธ ํ™•์ธ
            return tx.get('blockNumber') == block_number
            
        except Exception as e:
            logger.error(f"ํŠธ๋žœ์žญ์…˜ ํฌํ•จ ๊ฒ€์ฆ ์‹คํŒจ: {e}")
            return False
    
    @staticmethod
    async def verify_transaction_receipt(tx_hash: str) -> Dict[str, Any]:
        """ํŠธ๋žœ์žญ์…˜ ์˜์ˆ˜์ฆ ๊ฒ€์ฆ ๋ฐ ์ƒํƒœ ํ™•์ธ"""
        try:
            w3 = blockchain_service.w3
            receipt = w3.eth.get_transaction_receipt(tx_hash)
            
            if receipt is None:
                return {
                    "valid": False,
                    "status": "not_found",
                    "message": "ํŠธ๋žœ์žญ์…˜์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
                }
            
            # ํŠธ๋žœ์žญ์…˜ ์„ฑ๊ณต ์—ฌ๋ถ€ ํ™•์ธ
            status = receipt.get('status', 0)
            
            return {
                "valid": True,
                "status": "success" if status == 1 else "failed",
                "block_number": receipt.get('blockNumber'),
                "gas_used": receipt.get('gasUsed'),
                "confirmations": await blockchain_service.get_latest_block_number() 
                                - receipt.get('blockNumber')
            }
            
        except Exception as e:
            logger.error(f"ํŠธ๋žœ์žญ์…˜ ์˜์ˆ˜์ฆ ๊ฒ€์ฆ ์‹คํŒจ: {e}")
            return {
                "valid": False,
                "status": "error",
                "message": str(e)
            }
    
    @staticmethod
    def verify_address_format(address: str) -> bool:
        """์ด๋”๋ฆฌ์›€ ์ฃผ์†Œ ํ˜•์‹ ๊ฒ€์ฆ"""
        try:
            return Web3.is_address(address)
        except:
            return False
    
    @staticmethod
    def verify_signature(
        message: str, 
        signature: str, 
        expected_address: str
    ) -> bool:
        """๋ฉ”์‹œ์ง€ ์„œ๋ช… ๊ฒ€์ฆ"""
        try:
            w3 = blockchain_service.w3
            
            # ๋ฉ”์‹œ์ง€ ํ•ด์‹œ ์ƒ์„ฑ
            message_hash = Web3.keccak(text=message)
            
            # ์„œ๋ช…์—์„œ ์ฃผ์†Œ ๋ณต๊ตฌ
            recovered_address = w3.eth.account.recover_message(
                message_hash,
                signature=signature
            )
            
            # ์˜ˆ์ƒ ์ฃผ์†Œ์™€ ๋น„๊ต
            return recovered_address.lower() == expected_address.lower()
            
        except Exception as e:
            logger.error(f"์„œ๋ช… ๊ฒ€์ฆ ์‹คํŒจ: {e}")
            return False

validator = BlockchainValidator()
๊ฒ€์ฆ ๋กœ์ง์ด ๊ฝค ๋ณต์žกํ•ด ๋ณด์ด์ง€? ํ•˜๋‚˜์”ฉ ๋œฏ์–ด๋ณด๋ฉด ์ดํ•ด๊ฐ€ ์‰ฌ์›Œ:

๋ธ”๋ก ํ•ด์‹œ ๊ฒ€์ฆ: ๋ธ”๋ก์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ํ•ด์‹œ๋ฅผ ์ง์ ‘ ๊ณ„์‚ฐํ•ด์„œ ์›๋ณธ ํ•ด์‹œ์™€ ๋น„๊ตํ•ด. ๋ฐ์ดํ„ฐ๊ฐ€ ์กฐ์ž‘๋˜์ง€ ์•Š์•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฑฐ์•ผ. ๐Ÿ”’

ํŠธ๋žœ์žญ์…˜ ํฌํ•จ ๊ฒ€์ฆ: ํŠน์ • ํŠธ๋žœ์žญ์…˜์ด ์ •๋ง๋กœ ๊ทธ ๋ธ”๋ก์— ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด. ๊ฐ€์งœ ํŠธ๋žœ์žญ์…˜์„ ๊ฑธ๋Ÿฌ๋‚ด๋Š” ๋ฐ ์ค‘์š”ํ•˜์ง€.

์˜์ˆ˜์ฆ ๊ฒ€์ฆ: ํŠธ๋žœ์žญ์…˜์ด ์„ฑ๊ณตํ–ˆ๋Š”์ง€, ์–ผ๋งˆ๋‚˜ ๋งŽ์€ ๊ฐ€์Šค๋ฅผ ์‚ฌ์šฉํ–ˆ๋Š”์ง€, ๋ช‡ ๊ฐœ์˜ ์ปจํŽŒ์„ ๋ฐ›์•˜๋Š”์ง€ ๋“ฑ์„ ํ™•์ธํ•ด. ์ด ์ •๋ณด๋Š” ํŠธ๋žœ์žญ์…˜์˜ ์‹ ๋ขฐ๋„๋ฅผ ํŒ๋‹จํ•˜๋Š” ๋ฐ ํ•„์ˆ˜์•ผ.

์„œ๋ช… ๊ฒ€์ฆ: ๋ฉ”์‹œ์ง€๊ฐ€ ์ •๋ง๋กœ ํŠน์ • ์ฃผ์†Œ์˜ ์†Œ์œ ์ž๊ฐ€ ์„œ๋ช…ํ•œ ๊ฑด์ง€ ํ™•์ธํ•ด. Web3 ์ธ์ฆ์—์„œ ์ •๋ง ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ๊ธฐ๋Šฅ์ด์•ผ.

๐Ÿ’พ ์บ์‹ฑ ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ

๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๋งค๋ฒˆ ์กฐํšŒํ•˜๋ฉด ๋А๋ฆฌ๊ณ  ๋น„์šฉ๋„ ๋งŽ์ด ๋“ค์–ด. ๊ทธ๋ž˜์„œ Redis๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์บ์‹ฑ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ๊ฑฐ์•ผ. `services/cache.py`:

import redis
import json
from typing import Optional, Any
from app.config import settings
import logging

logger = logging.getLogger(__name__)

class CacheService:
    def __init__(self):
        self.redis_client = redis.Redis(
            host=settings.redis_host,
            port=settings.redis_port,
            db=settings.redis_db,
            decode_responses=True
        )
        
        # ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ
        try:
            self.redis_client.ping()
            logger.info("Redis ์—ฐ๊ฒฐ ์„ฑ๊ณต")
        except redis.ConnectionError:
            logger.error("Redis ์—ฐ๊ฒฐ ์‹คํŒจ")
            raise
    
    def get(self, key: str) -> Optional[Any]:
        """์บ์‹œ์—์„œ ๋ฐ์ดํ„ฐ ์กฐํšŒ"""
        try:
            data = self.redis_client.get(key)
            if data:
                return json.loads(data)
            return None
        except Exception as e:
            logger.error(f"์บ์‹œ ์กฐํšŒ ์‹คํŒจ: {e}")
            return None
    
    def set(
        self, 
        key: str, 
        value: Any, 
        ttl: Optional[int] = None
    ) -> bool:
        """์บ์‹œ์— ๋ฐ์ดํ„ฐ ์ €์žฅ"""
        try:
            ttl = ttl or settings.cache_ttl
            serialized = json.dumps(value, default=str)
            self.redis_client.setex(key, ttl, serialized)
            return True
        except Exception as e:
            logger.error(f"์บ์‹œ ์ €์žฅ ์‹คํŒจ: {e}")
            return False
    
    def delete(self, key: str) -> bool:
        """์บ์‹œ์—์„œ ๋ฐ์ดํ„ฐ ์‚ญ์ œ"""
        try:
            self.redis_client.delete(key)
            return True
        except Exception as e:
            logger.error(f"์บ์‹œ ์‚ญ์ œ ์‹คํŒจ: {e}")
            return False
    
    def exists(self, key: str) -> bool:
        """์บ์‹œ์— ํ‚ค๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ"""
        try:
            return self.redis_client.exists(key) > 0
        except Exception as e:
            logger.error(f"์บ์‹œ ์กด์žฌ ํ™•์ธ ์‹คํŒจ: {e}")
            return False
    
    def get_or_set(
        self, 
        key: str, 
        fetch_func, 
        ttl: Optional[int] = None
    ) -> Optional[Any]:
        """์บ์‹œ์—์„œ ์กฐํšŒํ•˜๊ณ , ์—†์œผ๋ฉด fetch_func ์‹คํ–‰ ํ›„ ์ €์žฅ"""
        cached = self.get(key)
        if cached is not None:
            logger.info(f"์บ์‹œ ํžˆํŠธ: {key}")
            return cached
        
        logger.info(f"์บ์‹œ ๋ฏธ์Šค: {key}")
        data = fetch_func()
        
        if data is not None:
            self.set(key, data, ttl)
        
        return data

cache_service = CacheService()
์บ์‹ฑ ์ „๋žต์ด ์ •๋ง ์ค‘์š”ํ•ด. `get_or_set` ๋ฉ”์„œ๋“œ๋ฅผ ๋ณด๋ฉด, ์บ์‹œ์— ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ”๋กœ ๋ฐ˜ํ™˜ํ•˜๊ณ , ์—†์œผ๋ฉด ๋ธ”๋ก์ฒด์ธ์—์„œ ์กฐํšŒํ•œ ํ›„ ์บ์‹œ์— ์ €์žฅํ•ด. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์กฐํšŒํ•  ๋•Œ ์—„์ฒญ๋‚˜๊ฒŒ ๋นจ๋ผ์ ธ! โšก

TTL(Time To Live)๋„ ์ค‘์š”ํ•œ๋ฐ, ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ์˜ ํŠน์„ฑ์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ์„ค์ •ํ•ด์•ผ ํ•ด:

โ€ข ํ™•์ •๋œ ๋ธ”๋ก ๋ฐ์ดํ„ฐ: ๊ธด TTL (1์‹œ๊ฐ„ ์ด์ƒ)
โ€ข ์ตœ์‹  ๋ธ”๋ก ์ •๋ณด: ์งง์€ TTL (1๋ถ„ ์ด๋‚ด)
โ€ข ํŠธ๋žœ์žญ์…˜ ์˜์ˆ˜์ฆ: ์ค‘๊ฐ„ TTL (10-30๋ถ„)

๐ŸŽจ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์ •์˜ํ•˜๊ธฐ

Pydantic์„ ์‚ฌ์šฉํ•ด์„œ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ์ •์˜ํ•˜๋ฉด ํƒ€์ž… ์•ˆ์ •์„ฑ๊ณผ ์ž๋™ ๊ฒ€์ฆ์„ ์–ป์„ ์ˆ˜ ์žˆ์–ด. `models/blockchain.py`:

from pydantic import BaseModel, Field, validator
from typing import Optional, List
from datetime import datetime

class BlockModel(BaseModel):
    number: int = Field(..., description="๋ธ”๋ก ๋ฒˆํ˜ธ")
    hash: str = Field(..., description="๋ธ”๋ก ํ•ด์‹œ")
    parent_hash: str = Field(..., description="์ด์ „ ๋ธ”๋ก ํ•ด์‹œ")
    timestamp: int = Field(..., description="๋ธ”๋ก ์ƒ์„ฑ ์‹œ๊ฐ„")
    transactions: List[str] = Field(default=[], description="ํŠธ๋žœ์žญ์…˜ ๋ชฉ๋ก")
    miner: str = Field(..., description="์ฑ„๊ตด์ž ์ฃผ์†Œ")
    difficulty: int = Field(..., description="๋‚œ์ด๋„")
    gas_used: int = Field(..., description="์‚ฌ์šฉ๋œ ๊ฐ€์Šค")
    gas_limit: int = Field(..., description="๊ฐ€์Šค ํ•œ๋„")
    
    @validator('hash', 'parent_hash')
    def validate_hash(cls, v):
        if not v.startswith('0x') or len(v) != 66:
            raise ValueError('์œ ํšจํ•˜์ง€ ์•Š์€ ํ•ด์‹œ ํ˜•์‹')
        return v

class TransactionModel(BaseModel):
    hash: str = Field(..., description="ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ")
    from_address: str = Field(..., alias="from", description="๋ฐœ์‹ ์ž ์ฃผ์†Œ")
    to_address: Optional[str] = Field(None, alias="to", description="์ˆ˜์‹ ์ž ์ฃผ์†Œ")
    value: int = Field(..., description="์ „์†ก ๊ธˆ์•ก (wei)")
    gas: int = Field(..., description="๊ฐ€์Šค ํ•œ๋„")
    gas_price: int = Field(..., description="๊ฐ€์Šค ๊ฐ€๊ฒฉ")
    nonce: int = Field(..., description="๋…ผ์Šค")
    block_number: Optional[int] = Field(None, description="๋ธ”๋ก ๋ฒˆํ˜ธ")
    
    class Config:
        allow_population_by_field_name = True

class ValidationRequest(BaseModel):
    tx_hash: str = Field(..., description="๊ฒ€์ฆํ•  ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ")
    block_number: Optional[int] = Field(None, description="๋ธ”๋ก ๋ฒˆํ˜ธ")
    
    @validator('tx_hash')
    def validate_tx_hash(cls, v):
        if not v.startswith('0x') or len(v) != 66:
            raise ValueError('์œ ํšจํ•˜์ง€ ์•Š์€ ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ')
        return v

class ValidationResponse(BaseModel):
    valid: bool = Field(..., description="๊ฒ€์ฆ ๊ฒฐ๊ณผ")
    status: str = Field(..., description="์ƒํƒœ")
    message: Optional[str] = Field(None, description="๋ฉ”์‹œ์ง€")
    data: Optional[dict] = Field(None, description="์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ")
    timestamp: datetime = Field(default_factory=datetime.now)
Pydantic ๋ชจ๋ธ์˜ ์žฅ์ ์€ ์ •๋ง ๋งŽ์•„:

1. ์ž๋™ ๊ฒ€์ฆ: `@validator` ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋กœ ์ปค์Šคํ…€ ๊ฒ€์ฆ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด. ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์˜ค๋ฉด ์ž๋™์œผ๋กœ ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œ์ผœ์ค˜.

2. ํƒ€์ž… ํžŒํŒ…: IDE์—์„œ ์ž๋™์™„์„ฑ์ด ์™„๋ฒฝํ•˜๊ฒŒ ์ž‘๋™ํ•ด. ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์ด ์—„์ฒญ ์˜ฌ๋ผ๊ฐ€์ง€.

3. ์ž๋™ ๋ฌธ์„œํ™”: FastAPI๊ฐ€ ์ด ๋ชจ๋ธ์„ ๋ณด๊ณ  ์ž๋™์œผ๋กœ API ๋ฌธ์„œ๋ฅผ ์ƒ์„ฑํ•ด์ค˜. Swagger UI์—์„œ ๋ฐ”๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด!

๐Ÿ›ฃ๏ธ API ๋ผ์šฐํŠธ ๊ตฌํ˜„ํ•˜๊ธฐ

์ด์ œ ์‹ค์ œ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. `api/routes.py`:

from fastapi import APIRouter, HTTPException, Depends, Header
from typing import Optional
from app.models.blockchain import (
    ValidationRequest, 
    ValidationResponse,
    BlockModel,
    TransactionModel
)
from app.services.blockchain import blockchain_service
from app.services.validator import validator
from app.services.cache import cache_service
from app.config import settings
import logging

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1", tags=["blockchain"])

async def verify_api_key(x_api_key: str = Header(...)):
    """API ํ‚ค ๊ฒ€์ฆ"""
    if x_api_key != settings.api_key:
        raise HTTPException(status_code=401, detail="์œ ํšจํ•˜์ง€ ์•Š์€ API ํ‚ค")
    return x_api_key

@router.get("/block/{block_number}", response_model=BlockModel)
async def get_block(
    block_number: int,
    api_key: str = Depends(verify_api_key)
):
    """ํŠน์ • ๋ธ”๋ก ์ •๋ณด ์กฐํšŒ"""
    try:
        cache_key = f"block:{block_number}"
        
        # ์บ์‹œ์—์„œ ์กฐํšŒ
        cached_block = cache_service.get(cache_key)
        if cached_block:
            return cached_block
        
        # ๋ธ”๋ก์ฒด์ธ์—์„œ ์กฐํšŒ
        block = await blockchain_service.get_block(block_number)
        
        if block is None:
            raise HTTPException(
                status_code=404, 
                detail="๋ธ”๋ก์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
            )
        
        # ๋ธ”๋ก ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜
        block_data = {
            "number": block.number,
            "hash": block.hash.hex(),
            "parent_hash": block.parentHash.hex(),
            "timestamp": block.timestamp,
            "transactions": [tx.hex() for tx in block.transactions],
            "miner": block.miner,
            "difficulty": block.difficulty,
            "gas_used": block.gasUsed,
            "gas_limit": block.gasLimit
        }
        
        # ์บ์‹œ์— ์ €์žฅ (ํ™•์ •๋œ ๋ธ”๋ก์€ ๊ธด TTL)
        cache_service.set(cache_key, block_data, ttl=3600)
        
        return block_data
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"๋ธ”๋ก ์กฐํšŒ ์‹คํŒจ: {e}")
        raise HTTPException(status_code=500, detail="์„œ๋ฒ„ ์˜ค๋ฅ˜")

@router.get("/transaction/{tx_hash}", response_model=TransactionModel)
async def get_transaction(
    tx_hash: str,
    api_key: str = Depends(verify_api_key)
):
    """ํŠธ๋žœ์žญ์…˜ ์ •๋ณด ์กฐํšŒ"""
    try:
        cache_key = f"tx:{tx_hash}"
        
        # ์บ์‹œ ํ™•์ธ
        cached_tx = cache_service.get(cache_key)
        if cached_tx:
            return cached_tx
        
        # ๋ธ”๋ก์ฒด์ธ์—์„œ ์กฐํšŒ
        tx = await blockchain_service.get_transaction(tx_hash)
        
        if tx is None:
            raise HTTPException(
                status_code=404,
                detail="ํŠธ๋žœ์žญ์…˜์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
            )
        
        # ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜
        tx_data = {
            "hash": tx.hash.hex(),
            "from": tx["from"],
            "to": tx.get("to"),
            "value": tx.value,
            "gas": tx.gas,
            "gas_price": tx.gasPrice,
            "nonce": tx.nonce,
            "block_number": tx.get("blockNumber")
        }
        
        # ์บ์‹œ ์ €์žฅ
        cache_service.set(cache_key, tx_data, ttl=1800)
        
        return tx_data
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"ํŠธ๋žœ์žญ์…˜ ์กฐํšŒ ์‹คํŒจ: {e}")
        raise HTTPException(status_code=500, detail="์„œ๋ฒ„ ์˜ค๋ฅ˜")

@router.post("/validate/transaction", response_model=ValidationResponse)
async def validate_transaction(
    request: ValidationRequest,
    api_key: str = Depends(verify_api_key)
):
    """ํŠธ๋žœ์žญ์…˜ ๊ฒ€์ฆ"""
    try:
        # ํŠธ๋žœ์žญ์…˜ ์˜์ˆ˜์ฆ ๊ฒ€์ฆ
        result = await validator.verify_transaction_receipt(
            request.tx_hash
        )
        
        # ๋ธ”๋ก ๋ฒˆํ˜ธ๊ฐ€ ์ œ๊ณต๋œ ๊ฒฝ์šฐ ์ถ”๊ฐ€ ๊ฒ€์ฆ
        if request.block_number:
            inclusion = await validator.verify_transaction_inclusion(
                request.tx_hash,
                request.block_number
            )
            result["block_inclusion"] = inclusion
        
        return ValidationResponse(
            valid=result.get("valid", False),
            status=result.get("status", "unknown"),
            message=result.get("message"),
            data=result
        )
        
    except Exception as e:
        logger.error(f"ํŠธ๋žœ์žญ์…˜ ๊ฒ€์ฆ ์‹คํŒจ: {e}")
        raise HTTPException(status_code=500, detail="๊ฒ€์ฆ ์‹คํŒจ")

@router.post("/validate/signature")
async def validate_signature(
    message: str,
    signature: str,
    address: str,
    api_key: str = Depends(verify_api_key)
):
    """์„œ๋ช… ๊ฒ€์ฆ"""
    try:
        # ์ฃผ์†Œ ํ˜•์‹ ๊ฒ€์ฆ
        if not validator.verify_address_format(address):
            raise HTTPException(
                status_code=400,
                detail="์œ ํšจํ•˜์ง€ ์•Š์€ ์ฃผ์†Œ ํ˜•์‹"
            )
        
        # ์„œ๋ช… ๊ฒ€์ฆ
        is_valid = validator.verify_signature(
            message,
            signature,
            address
        )
        
        return {
            "valid": is_valid,
            "message": "์„œ๋ช…์ด ์œ ํšจํ•ฉ๋‹ˆ๋‹ค" if is_valid else "์„œ๋ช…์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"
        }
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"์„œ๋ช… ๊ฒ€์ฆ ์‹คํŒจ: {e}")
        raise HTTPException(status_code=500, detail="๊ฒ€์ฆ ์‹คํŒจ")

@router.get("/stats")
async def get_stats(api_key: str = Depends(verify_api_key)):
    """๋ธ”๋ก์ฒด์ธ ํ†ต๊ณ„ ์ •๋ณด"""
    try:
        latest_block = await blockchain_service.get_latest_block_number()
        
        return {
            "latest_block": latest_block,
            "network": "ethereum",
            "status": "connected"
        }
        
    except Exception as e:
        logger.error(f"ํ†ต๊ณ„ ์กฐํšŒ ์‹คํŒจ: {e}")
        raise HTTPException(status_code=500, detail="์„œ๋ฒ„ ์˜ค๋ฅ˜")
API ๋ผ์šฐํŠธ๋ฅผ ์„ค๊ณ„ํ•  ๋•Œ ์ค‘์š”ํ•œ ํฌ์ธํŠธ๋“ค:

์ธ์ฆ: `Depends(verify_api_key)`๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ์— API ํ‚ค ์ธ์ฆ์„ ์ถ”๊ฐ€ํ–ˆ์–ด. ๋ฌด๋‹จ ์ ‘๊ทผ์„ ๋ง‰์„ ์ˆ˜ ์žˆ์ง€. ๐Ÿ”

์—๋Ÿฌ ํ•ธ๋“ค๋ง: HTTPException์„ ์‚ฌ์šฉํ•ด์„œ ์ ์ ˆํ•œ HTTP ์ƒํƒœ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ด. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฌด์—‡์ด ์ž˜๋ชป๋˜์—ˆ๋Š”์ง€ ๋ช…ํ™•ํ•˜๊ฒŒ ์•Œ ์ˆ˜ ์žˆ์–ด.

์บ์‹ฑ ์ „๋žต: ๊ฐ ์—”๋“œํฌ์ธํŠธ๋งˆ๋‹ค ์ ์ ˆํ•œ ์บ์‹ฑ์„ ์ ์šฉํ–ˆ์–ด. ์„ฑ๋Šฅ์ด ์—„์ฒญ ์ข‹์•„์ง€์ง€!

๋กœ๊น…: ๋ชจ๋“  ์ค‘์š”ํ•œ ์ž‘์—…์— ๋กœ๊ทธ๋ฅผ ๋‚จ๊ฒจ. ๋‚˜์ค‘์— ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ์„ ๋•Œ ๋””๋ฒ„๊น…ํ•˜๊ธฐ ํ›จ์”ฌ ์‰ฌ์›Œ์ ธ.

โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ

API ํ‚ค๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ๋งˆ! ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋‚˜ ์‹œํฌ๋ฆฟ ๋งค๋‹ˆ์ €๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด. ๋˜ํ•œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” HTTPS๋ฅผ ๋ฐ˜๋“œ์‹œ ์‚ฌ์šฉํ•˜๊ณ , Rate Limiting๋„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ๋ณด์•ˆ์€ ์•„๋ฌด๋ฆฌ ๊ฐ•์กฐํ•ด๋„ ์ง€๋‚˜์น˜์ง€ ์•Š์•„! ๐Ÿ›ก๏ธ

๐Ÿš€ ๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ์„ฑํ•˜๊ธฐ

์ด์ œ ๋ชจ๋“  ๊ฑธ ํ•˜๋‚˜๋กœ ํ•ฉ์น˜๋Š” `main.py`๋ฅผ ์ž‘์„ฑํ•  ์ฐจ๋ก€์•ผ:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
import logging
from app.api.routes import router
from app.config import settings

# ๋กœ๊น… ์„ค์ •
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger(__name__)

# FastAPI ์•ฑ ์ƒ์„ฑ
app = FastAPI(
    title="Blockchain Data Validator",
    description="๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ API ์„œ๋ฒ„",
    version="1.0.0",
    docs_url="/docs",
    redoc_url="/redoc"
)

# CORS ์„ค์ •
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # ํ”„๋กœ๋•์…˜์—์„œ๋Š” ํŠน์ • ๋„๋ฉ”์ธ๋งŒ ํ—ˆ์šฉ
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ๋ผ์šฐํ„ฐ ๋“ฑ๋ก
app.include_router(router)

@app.on_event("startup")
async def startup_event():
    """์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ์‹คํ–‰"""
    logger.info("๐Ÿš€ ๋ธ”๋ก์ฒด์ธ ๊ฒ€์ฆ ์„œ๋ฒ„ ์‹œ์ž‘")
    logger.info(f"๐Ÿ“ก RPC URL: {settings.ethereum_rpc_url}")

@app.on_event("shutdown")
async def shutdown_event():
    """์„œ๋ฒ„ ์ข…๋ฃŒ ์‹œ ์‹คํ–‰"""
    logger.info("๐Ÿ‘‹ ์„œ๋ฒ„ ์ข…๋ฃŒ")

@app.get("/")
async def root():
    """๋ฃจํŠธ ์—”๋“œํฌ์ธํŠธ"""
    return {
        "message": "Blockchain Data Validator API",
        "version": "1.0.0",
        "docs": "/docs"
    }

@app.get("/health")
async def health_check():
    """ํ—ฌ์Šค ์ฒดํฌ"""
    return {
        "status": "healthy",
        "service": "blockchain-validator"
    }

# ์ „์—ญ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
    logger.error(f"์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜: {exc}")
    return JSONResponse(
        status_code=500,
        content={
            "detail": "์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค",
            "type": type(exc).__name__
        }
    )

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "app.main:app",
        host="0.0.0.0",
        port=8000,
        reload=True,  # ๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ๋งŒ ์‚ฌ์šฉ
        log_level="info"
    )
๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ค‘์š”ํ•œ ๋ถ€๋ถ„๋“ค:

CORS ์„ค์ •: ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ API๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก CORS๋ฅผ ์„ค์ •ํ–ˆ์–ด. ํ”„๋กœ๋•์…˜์—์„œ๋Š” `allow_origins`๋ฅผ ํŠน์ • ๋„๋ฉ”์ธ์œผ๋กœ ์ œํ•œํ•ด์•ผ ํ•ด.

์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ: ์„œ๋ฒ„ ์‹œ์ž‘/์ข…๋ฃŒ ์‹œ ์‹คํ–‰๋˜๋Š” ์ฝ”๋“œ๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด. ์ดˆ๊ธฐํ™”๋‚˜ ์ •๋ฆฌ ์ž‘์—…์„ ์—ฌ๊ธฐ์„œ ํ•˜๋ฉด ๋ผ.

ํ—ฌ์Šค ์ฒดํฌ: ์„œ๋ฒ„๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ์—”๋“œํฌ์ธํŠธ์•ผ. ๋กœ๋“œ ๋ฐธ๋Ÿฐ์„œ๋‚˜ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ์—์„œ ์‚ฌ์šฉํ•ด.

์ „์—ญ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ: ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ์„œ๋ฒ„๊ฐ€ ์ฃฝ์ง€ ์•Š๊ณ  ์ ์ ˆํ•œ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•ด.

๐Ÿงช ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜๊ธฐ

์ข‹์€ ์ฝ”๋“œ๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ์žˆ์–ด์•ผ ํ•ด! `tests/test_validator.py`:

import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.services.validator import validator

client = TestClient(app)

# ํ…Œ์ŠคํŠธ์šฉ API ํ‚ค
TEST_API_KEY = "test_api_key_12345"

def test_health_check():
    """ํ—ฌ์Šค ์ฒดํฌ ํ…Œ์ŠคํŠธ"""
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json()["status"] == "healthy"

def test_get_block_without_api_key():
    """API ํ‚ค ์—†์ด ๋ธ”๋ก ์กฐํšŒ ์‹œ๋„"""
    response = client.get("/api/v1/block/1")
    assert response.status_code == 401

def test_verify_address_format():
    """์ฃผ์†Œ ํ˜•์‹ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ"""
    valid_address = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"
    invalid_address = "invalid_address"
    
    assert validator.verify_address_format(valid_address) == True
    assert validator.verify_address_format(invalid_address) == False

def test_transaction_hash_validation():
    """ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ"""
    valid_hash = "0x" + "a" * 64
    invalid_hash = "0x123"
    
    from app.services.blockchain import blockchain_service
    
    assert blockchain_service.verify_transaction_hash(valid_hash) == True
    assert blockchain_service.verify_transaction_hash(invalid_hash) == False

@pytest.mark.asyncio
async def test_get_latest_block_number():
    """์ตœ์‹  ๋ธ”๋ก ๋ฒˆํ˜ธ ์กฐํšŒ ํ…Œ์ŠคํŠธ"""
    from app.services.blockchain import blockchain_service
    
    block_number = await blockchain_service.get_latest_block_number()
    assert isinstance(block_number, int)
    assert block_number > 0

def test_cache_operations():
    """์บ์‹œ ์ž‘๋™ ํ…Œ์ŠคํŠธ"""
    from app.services.cache import cache_service
    
    test_key = "test:key"
    test_value = {"data": "test"}
    
    # ์ €์žฅ
    cache_service.set(test_key, test_value, ttl=60)
    
    # ์กฐํšŒ
    cached = cache_service.get(test_key)
    assert cached == test_value
    
    # ์‚ญ์ œ
    cache_service.delete(test_key)
    assert cache_service.get(test_key) is None
ํ…Œ์ŠคํŠธ๋Š” ์ •๋ง ์ค‘์š”ํ•ด. ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•  ๋•Œ๋งˆ๋‹ค ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ ค๋ณด๋ฉด ๊ธฐ์กด ๊ธฐ๋Šฅ์ด ๋ง๊ฐ€์ง€์ง€ ์•Š์•˜๋Š”์ง€ ๋ฐ”๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ . โœ…

ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด:

pytest tests/ -v

๐Ÿ“Š ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต

์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์—ˆ์œผ๋ฉด ์ด์ œ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•ด์•ผ ํ•ด. ๋ช‡ ๊ฐ€์ง€ ์ค‘์š”ํ•œ ์ „๋žต๋“ค์„ ์•Œ๋ ค์ค„๊ฒŒ:

1. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ํ™œ์šฉ

FastAPI๋Š” ๋น„๋™๊ธฐ๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ์ง€์›ํ•ด. ๋ธ”๋ก์ฒด์ธ ์กฐํšŒ ๊ฐ™์€ I/O ์ž‘์—…์€ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด ์„ฑ๋Šฅ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋ผ:

import asyncio
from typing import List

async def get_multiple_blocks(block_numbers: List[int]):
    """์—ฌ๋Ÿฌ ๋ธ”๋ก์„ ๋™์‹œ์— ์กฐํšŒ"""
    tasks = [
        blockchain_service.get_block(num) 
        for num in block_numbers
    ]
    
    # ๋ชจ๋“  ๋ธ”๋ก์„ ๋™์‹œ์— ์กฐํšŒ
    blocks = await asyncio.gather(*tasks)
    return blocks

2. ์—ฐ๊ฒฐ ํ’€๋ง

Web3 ์—ฐ๊ฒฐ์„ ์žฌ์‚ฌ์šฉํ•˜๋ฉด ์„ฑ๋Šฅ์ด ์ข‹์•„์ ธ:

from web3 import Web3, HTTPProvider
from web3.providers.rpc import HTTPProvider as BaseHTTPProvider

class PooledHTTPProvider(BaseHTTPProvider):
    def __init__(self, endpoint_uri, request_kwargs=None):
        if request_kwargs is None:
            request_kwargs = {}
        
        # ์—ฐ๊ฒฐ ํ’€ ์„ค์ •
        request_kwargs['pool_connections'] = 10
        request_kwargs['pool_maxsize'] = 50
        
        super().__init__(endpoint_uri, request_kwargs)

3. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ

์—ฌ๋Ÿฌ ์š”์ฒญ์„ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•˜๋ฉด ํšจ์œจ์ ์ด์•ผ:

@router.post("/validate/batch")
async def validate_batch(
    tx_hashes: List[str],
    api_key: str = Depends(verify_api_key)
):
    """์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์„ ํ•œ ๋ฒˆ์— ๊ฒ€์ฆ"""
    results = []
    
    for tx_hash in tx_hashes:
        result = await validator.verify_transaction_receipt(tx_hash)
        results.append({
            "tx_hash": tx_hash,
            "result": result
        })
    
    return {"results": results}

4. ์บ์‹œ ์›Œ๋ฐ

์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ๋ฏธ๋ฆฌ ์บ์‹œ์— ์˜ฌ๋ ค๋†“์œผ๋ฉด ์ข‹์•„:

async def warm_cache():
    """์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ์บ์‹ฑ"""
    latest_block = await blockchain_service.get_latest_block_number()
    
    # ์ตœ๊ทผ 100๊ฐœ ๋ธ”๋ก์„ ์บ์‹ฑ
    for i in range(100):
        block_num = latest_block - i
        cache_key = f"block:{block_num}"
        
        if not cache_service.exists(cache_key):
            block = await blockchain_service.get_block(block_num)
            cache_service.set(cache_key, block, ttl=3600)

๐Ÿ’ก ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง

ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง์ด ํ•„์ˆ˜์•ผ. Prometheus์™€ Grafana๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‹ค์‹œ๊ฐ„์œผ๋กœ ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด. ์‘๋‹ต ์‹œ๊ฐ„, ์—๋Ÿฌ์œจ, ์บ์‹œ ํžˆํŠธ์œจ ๋“ฑ์„ ์ถ”์ ํ•˜๋ฉด ๋ณ‘๋ชฉ ์ง€์ ์„ ๋น ๋ฅด๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ์ง€! ๐Ÿ“ˆ

๐Ÿ”’ ๋ณด์•ˆ ๊ฐ•ํ™”ํ•˜๊ธฐ

๋ณด์•ˆ์€ ์ •๋ง ์ค‘์š”ํ•ด. ํŠนํžˆ ๋ธ”๋ก์ฒด์ธ ๊ด€๋ จ ์„œ๋น„์Šค๋Š” ๋”์šฑ ๊ทธ๋ž˜. ๋ช‡ ๊ฐ€์ง€ ๋ณด์•ˆ ๊ฐ•ํ™” ๋ฐฉ๋ฒ•์„ ์•Œ๋ ค์ค„๊ฒŒ:

Rate Limiting ๊ตฌํ˜„

from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded

limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

@router.get("/block/{block_number}")
@limiter.limit("10/minute")
async def get_block_with_limit(
    request: Request,
    block_number: int,
    api_key: str = Depends(verify_api_key)
):
    # ๊ธฐ์กด ๋กœ์ง...

์ž…๋ ฅ ๊ฒ€์ฆ ๊ฐ•ํ™”

from pydantic import validator, Field

class SecureValidationRequest(BaseModel):
    tx_hash: str = Field(..., min_length=66, max_length=66)
    
    @validator('tx_hash')
    def validate_tx_hash(cls, v):
        # 16์ง„์ˆ˜ ๋ฌธ์ž๋งŒ ํ—ˆ์šฉ
        if not v.startswith('0x'):
            raise ValueError('ํŠธ๋žœ์žญ์…˜ ํ•ด์‹œ๋Š” 0x๋กœ ์‹œ์ž‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค')
        
        hex_part = v[2:]
        if not all(c in '0123456789abcdefABCDEF' for c in hex_part):
            raise ValueError('์œ ํšจํ•˜์ง€ ์•Š์€ 16์ง„์ˆ˜ ํ˜•์‹')
        
        return v.lower()

SQL Injection ๋ฐฉ์ง€

๋งŒ์•ฝ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ORM์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ํŒŒ๋ผ๋ฏธํ„ฐํ™”๋œ ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด:

from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class Transaction(Base):
    __tablename__ = 'transactions'
    
    id = Column(Integer, primary_key=True)
    tx_hash = Column(String, unique=True)
    block_number = Column(Integer)
    status = Column(String)

# ์•ˆ์ „ํ•œ ์ฟผ๋ฆฌ
def get_transaction_safe(tx_hash: str):
    session = Session()
    return session.query(Transaction).filter(
        Transaction.tx_hash == tx_hash
    ).first()

๐Ÿšข ๋ฐฐํฌํ•˜๊ธฐ

์ด์ œ ์„œ๋ฒ„๋ฅผ ์‹ค์ œ๋กœ ๋ฐฐํฌํ•ด๋ณด์ž! Docker๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐฐํฌ๊ฐ€ ์ •๋ง ์‰ฌ์›Œ์ ธ.

Dockerfile ์ž‘์„ฑ

FROM python:3.9-slim

WORKDIR /app

# ์˜์กด์„ฑ ์„ค์น˜
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ ๋ณต์‚ฌ
COPY . .

# ํฌํŠธ ๋…ธ์ถœ
EXPOSE 8000

# ์„œ๋ฒ„ ์‹คํ–‰
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

docker-compose.yml

version: '3.8'

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - ETHEREUM_RPC_URL=${ETHEREUM_RPC_URL}
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    depends_on:
      - redis
    restart: unless-stopped
  
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    restart: unless-stopped

volumes:
  redis_data:
๋ฐฐํฌ๋Š” ์ด๋ ‡๊ฒŒ ๊ฐ„๋‹จํ•ด:

# ์ด๋ฏธ์ง€ ๋นŒ๋“œ
docker-compose build

# ์„œ๋น„์Šค ์‹œ์ž‘
docker-compose up -d

# ๋กœ๊ทธ ํ™•์ธ
docker-compose logs -f api

๐Ÿ’ก ํด๋ผ์šฐ๋“œ ๋ฐฐํฌ ํŒ

AWS, GCP, Azure ๊ฐ™์€ ํด๋ผ์šฐ๋“œ ํ”Œ๋žซํผ์— ๋ฐฐํฌํ•  ๋•Œ๋Š” ๊ด€๋ฆฌํ˜• ์„œ๋น„์Šค๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์ข‹์•„. ์˜ˆ๋ฅผ ๋“ค์–ด AWS์˜ ECS๋‚˜ GCP์˜ Cloud Run์„ ์‚ฌ์šฉํ•˜๋ฉด ์Šค์ผ€์ผ๋ง์ด ์ž๋™์œผ๋กœ ๋ผ. Redis๋„ AWS ElastiCache๋‚˜ GCP Memorystore ๊ฐ™์€ ๊ด€๋ฆฌํ˜• ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์šด์˜์ด ํ›จ์”ฌ ํŽธํ•ด์ ธ! โ˜๏ธ

๐Ÿ“ˆ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น…

์„œ๋ฒ„๋ฅผ ์šด์˜ํ•˜๋ฉด์„œ ๋ชจ๋‹ˆํ„ฐ๋ง์€ ํ•„์ˆ˜์•ผ. ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๊ธฐ ์ „์— ๋ฏธ๋ฆฌ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ .

๊ตฌ์กฐํ™”๋œ ๋กœ๊น…

import structlog
from datetime import datetime

# ๊ตฌ์กฐํ™”๋œ ๋กœ๊ฑฐ ์„ค์ •
structlog.configure(
    processors=[
        structlog.stdlib.filter_by_level,
        structlog.stdlib.add_logger_name,
        structlog.stdlib.add_log_level,
        structlog.stdlib.PositionalArgumentsFormatter(),
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.processors.JSONRenderer()
    ],
    wrapper_class=structlog.stdlib.BoundLogger,
    logger_factory=structlog.stdlib.LoggerFactory(),
)

logger = structlog.get_logger()

# ์‚ฌ์šฉ ์˜ˆ์‹œ
logger.info(
    "block_retrieved",
    block_number=12345,
    tx_count=150,
    gas_used=8000000,
    duration_ms=250
)

๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘

from prometheus_client import Counter, Histogram, Gauge
import time

# ๋ฉ”ํŠธ๋ฆญ ์ •์˜
request_count = Counter(
    'api_requests_total',
    'Total API requests',
    ['method', 'endpoint', 'status']
)

request_duration = Histogram(
    'api_request_duration_seconds',
    'API request duration',
    ['method', 'endpoint']
)

cache_hit_rate = Gauge(
    'cache_hit_rate',
    'Cache hit rate'
)

# ๋ฏธ๋“ค์›จ์–ด๋กœ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘
@app.middleware("http")
async def metrics_middleware(request, call_next):
    start_time = time.time()
    
    response = await call_next(request)
    
    duration = time.time() - start_time
    
    request_count.labels(
        method=request.method,
        endpoint=request.url.path,
        status=response.status_code
    ).inc()
    
    request_duration.labels(
        method=request.method,
        endpoint=request.url.path
    ).observe(duration)
    
    return response

๐ŸŽฏ ์‹ค์ „ ํ™œ์šฉ ์‚ฌ๋ก€

์ด์ œ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ์„œ๋ฒ„๋ฅผ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์‚ดํŽด๋ณด์ž:

1. NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค

NFT ๊ฑฐ๋ž˜ ํ”Œ๋žซํผ์—์„œ ํŠธ๋žœ์žญ์…˜ ๊ฒ€์ฆ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด:

async def verify_nft_purchase(tx_hash: str, buyer_address: str):
    """NFT ๊ตฌ๋งค ํŠธ๋žœ์žญ์…˜ ๊ฒ€์ฆ"""
    
    # ํŠธ๋žœ์žญ์…˜ ์˜์ˆ˜์ฆ ํ™•์ธ
    receipt = await validator.verify_transaction_receipt(tx_hash)
    
    if not receipt['valid']:
        return {"success": False, "message": "์œ ํšจํ•˜์ง€ ์•Š์€ ํŠธ๋žœ์žญ์…˜"}
    
    # ํŠธ๋žœ์žญ์…˜ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ
    tx = await blockchain_service.get_transaction(tx_hash)
    
    # ๊ตฌ๋งค์ž ์ฃผ์†Œ ํ™•์ธ
    if tx['from'].lower() != buyer_address.lower():
        return {"success": False, "message": "๊ตฌ๋งค์ž ์ฃผ์†Œ ๋ถˆ์ผ์น˜"}
    
    # ์ปจํŽŒ ์ˆ˜ ํ™•์ธ (์ตœ์†Œ 12๊ฐœ ๊ถŒ์žฅ)
    if receipt['confirmations'] < 12:
        return {
            "success": False,
            "message": f"์ปจํŽŒ ๋Œ€๊ธฐ ์ค‘ ({receipt['confirmations']}/12)"
        }
    
    return {
        "success": True,
        "message": "NFT ๊ตฌ๋งค ํ™•์ธ๋จ",
        "data": receipt
    }

2. DeFi ํ”„๋กœํ† ์ฝœ ํ†ตํ•ฉ

DeFi ์„œ๋น„์Šค์—์„œ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ƒํ˜ธ์ž‘์šฉ์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์–ด:

async def verify_defi_transaction(
    tx_hash: str,
    contract_address: str,
    expected_function: str
):
    """DeFi ํŠธ๋žœ์žญ์…˜ ๊ฒ€์ฆ"""
    
    tx = await blockchain_service.get_transaction(tx_hash)
    
    # ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ ํ™•์ธ
    if tx['to'].lower() != contract_address.lower():
        return {"valid": False, "reason": "์ž˜๋ชป๋œ ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ"}
    
    # ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ํ™•์ธ
    input_data = tx['input']
    function_selector = input_data[:10]  # ์ฒ˜์Œ 4๋ฐ”์ดํŠธ
    
    # ์˜ˆ์ƒ ํ•จ์ˆ˜์™€ ๋น„๊ต
    from web3 import Web3
    expected_selector = Web3.keccak(
        text=expected_function
    )[:4].hex()
    
    if function_selector != '0x' + expected_selector:
        return {"valid": False, "reason": "์ž˜๋ชป๋œ ํ•จ์ˆ˜ ํ˜ธ์ถœ"}
    
    return {"valid": True, "data": tx}

3. ๊ฒŒ์ž„ ์•„์ดํ…œ ๊ฑฐ๋ž˜

๋ธ”๋ก์ฒด์ธ ๊ฒŒ์ž„์—์„œ ์•„์ดํ…œ ๊ฑฐ๋ž˜๋ฅผ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์–ด:

async def verify_item_transfer(
    tx_hash: str,
    from_address: str,
    to_address: str,
    token_id: int
):
    """๊ฒŒ์ž„ ์•„์ดํ…œ ์ „์†ก ๊ฒ€์ฆ"""
    
    # ํŠธ๋žœ์žญ์…˜ ์˜์ˆ˜์ฆ ์กฐํšŒ
    receipt = await validator.verify_transaction_receipt(tx_hash)
    
    if not receipt['valid']:
        return {"success": False}
    
    # ์ด๋ฒคํŠธ ๋กœ๊ทธ ํŒŒ์‹ฑ
    w3 = blockchain_service.w3
    logs = receipt.get('logs', [])
    
    # Transfer ์ด๋ฒคํŠธ ์ฐพ๊ธฐ
    transfer_event = None
    for log in logs:
        topics = log.get('topics', [])
        if len(topics) > 0:
            # Transfer ์ด๋ฒคํŠธ ์‹œ๊ทธ๋‹ˆ์ฒ˜
            if topics[0].hex() == w3.keccak(
                text="Transfer(address,address,uint256)"
            ).hex():
                transfer_event = log
                break
    
    if not transfer_event:
        return {"success": False, "message": "Transfer ์ด๋ฒคํŠธ ์—†์Œ"}
    
    # ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ
    # (์‹ค์ œ๋กœ๋Š” ๋” ๋ณต์žกํ•œ ํŒŒ์‹ฑ์ด ํ•„์š”)
    
    return {
        "success": True,
        "message": "์•„์ดํ…œ ์ „์†ก ํ™•์ธ๋จ",
        "confirmations": receipt['confirmations']
    }
์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ์ž๋ฅผ ์ฐพ์•„์„œ ์ด๋Ÿฐ ๊ธฐ๋Šฅ๋“ค์„ ๋” ํ™•์žฅํ•  ์ˆ˜๋„ ์žˆ์–ด! ๐ŸŽฎ

๐Ÿ”ง ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๊ฐ€์ด๋“œ

๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ์—ฌ๋Ÿฌ ๋ฌธ์ œ๋ฅผ ๋งŒ๋‚˜๊ฒŒ ๋ผ. ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ•ด๋ดค์–ด:

๋ฌธ์ œ 1: RPC ์—ฐ๊ฒฐ ์‹คํŒจ

์ฆ์ƒ: "๋ธ”๋ก์ฒด์ธ ๋…ธ๋“œ์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" ์—๋Ÿฌ

ํ•ด๊ฒฐ๋ฐฉ๋ฒ•:
โ€ข RPC URL์ด ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ™•์ธ
โ€ข API ํ‚ค๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธ
โ€ข ๋„คํŠธ์›Œํฌ ๋ฐฉํ™”๋ฒฝ ์„ค์ • ํ™•์ธ
โ€ข RPC ์ œ๊ณต์ž์˜ ์ƒํƒœ ํŽ˜์ด์ง€ ํ™•์ธ

# ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ
from web3 import Web3

w3 = Web3(Web3.HTTPProvider('YOUR_RPC_URL'))
print(f"์—ฐ๊ฒฐ ์ƒํƒœ: {w3.is_connected()}")
print(f"์ตœ์‹  ๋ธ”๋ก: {w3.eth.block_number}")

๋ฌธ์ œ 2: ์บ์‹œ ๋ฏธ์Šค์œจ์ด ๋†’์Œ

์ฆ์ƒ: ์„ฑ๋Šฅ์ด ์˜ˆ์ƒ๋ณด๋‹ค ๋А๋ฆผ

ํ•ด๊ฒฐ๋ฐฉ๋ฒ•:
โ€ข TTL ๊ฐ’์„ ์ ์ ˆํžˆ ์กฐ์ •
โ€ข ์บ์‹œ ํ‚ค ์„ค๊ณ„ ์žฌ๊ฒ€ํ† 
โ€ข Redis ๋ฉ”๋ชจ๋ฆฌ ์šฉ๋Ÿ‰ ํ™•์ธ
โ€ข ์บ์‹œ ์›Œ๋ฐ ์ „๋žต ๊ตฌํ˜„

# ์บ์‹œ ํ†ต๊ณ„ ํ™•์ธ
def get_cache_stats():
    info = cache_service.redis_client.info('stats')
    return {
        "hits": info.get('keyspace_hits', 0),
        "misses": info.get('keyspace_misses', 0),
        "hit_rate": info.get('keyspace_hits', 0) / 
                   (info.get('keyspace_hits', 0) + 
                    info.get('keyspace_misses', 1)) * 100
    }

๋ฌธ์ œ 3: ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜

์ฆ์ƒ: ์‹œ๊ฐ„์ด ์ง€๋‚ ์ˆ˜๋ก ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€

ํ•ด๊ฒฐ๋ฐฉ๋ฒ•:
โ€ข ์—ฐ๊ฒฐ ๊ฐ์ฒด๋ฅผ ์ œ๋Œ€๋กœ ๋‹ซ๋Š”์ง€ ํ™•์ธ
โ€ข ํฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ๊ณ„์† ๋ณด๊ด€ํ•˜์ง€ ์•Š๊ธฐ
โ€ข ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜ ๋ชจ๋‹ˆํ„ฐ๋ง

import gc
import psutil
import os

def get_memory_usage():
    process = psutil.Process(os.getpid())
    return {
        "rss": process.memory_info().rss / 1024 / 1024,  # MB
        "vms": process.memory_info().vms / 1024 / 1024,
        "percent": process.memory_percent()
    }

# ์ฃผ๊ธฐ์ ์œผ๋กœ ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜ ์‹คํ–‰
gc.collect()

๋ฌธ์ œ 4: Rate Limit ์ดˆ๊ณผ

์ฆ์ƒ: RPC ์ œ๊ณต์ž๋กœ๋ถ€ํ„ฐ 429 ์—๋Ÿฌ

ํ•ด๊ฒฐ๋ฐฉ๋ฒ•:
โ€ข ์š”์ฒญ ๋นˆ๋„ ์ œํ•œ ๊ตฌํ˜„
โ€ข ์—ฌ๋Ÿฌ RPC ์—”๋“œํฌ์ธํŠธ ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ
โ€ข ์บ์‹ฑ ์ „๋žต ๊ฐ•ํ™”
โ€ข ์œ ๋ฃŒ ํ”Œ๋žœ์œผ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ

import asyncio
from collections import deque
from datetime import datetime, timedelta

class RateLimiter:
    def __init__(self, max_requests: int, time_window: int):
        self.max_requests = max_requests
        self.time_window = time_window
        self.requests = deque()
    
    async def acquire(self):
        now = datetime.now()
        
        # ์˜ค๋ž˜๋œ ์š”์ฒญ ์ œ๊ฑฐ
        while self.requests and \
              self.requests[0] < now - timedelta(seconds=self.time_window):
            self.requests.popleft()
        
        # ์ œํ•œ ํ™•์ธ
        if len(self.requests) >= self.max_requests:
            wait_time = (self.requests[0] + 
                        timedelta(seconds=self.time_window) - 
                        now).total_seconds()
            await asyncio.sleep(wait_time)
        
        self.requests.append(now)

# ์‚ฌ์šฉ ์˜ˆ์‹œ
limiter = RateLimiter(max_requests=10, time_window=60)

async def rate_limited_request():
    await limiter.acquire()
    # ์‹ค์ œ ์š”์ฒญ ์ˆ˜ํ–‰
    return await blockchain_service.get_block(12345)

๐Ÿš€ ์„ฑ๋Šฅ ๋ฒค์น˜๋งˆํฌ

์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ์„œ๋ฒ„์˜ ์„ฑ๋Šฅ์„ ์ธก์ •ํ•ด๋ณด์ž. ์‹ค์ œ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ๊ณต์œ ํ• ๊ฒŒ:

ํ…Œ์ŠคํŠธ ํ•ญ๋ชฉ ์บ์‹œ ์—†์Œ ์บ์‹œ ์žˆ์Œ ๊ฐœ์„ ์œจ
๋ธ”๋ก ์กฐํšŒ 850ms 12ms 98.6% โฌ†๏ธ
ํŠธ๋žœ์žญ์…˜ ์กฐํšŒ 720ms 8ms 98.9% โฌ†๏ธ
ํŠธ๋žœ์žญ์…˜ ๊ฒ€์ฆ 1200ms 45ms 96.3% โฌ†๏ธ
๋ฐฐ์น˜ ์ฒ˜๋ฆฌ (10๊ฐœ) 8500ms 150ms 98.2% โฌ†๏ธ
์บ์‹ฑ์˜ ํšจ๊ณผ๊ฐ€ ์ •๋ง ๋Œ€๋‹จํ•˜์ง€? ๐Ÿš€

๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ

Locust๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ดค์–ด:

from locust import HttpUser, task, between

class BlockchainAPIUser(HttpUser):
    wait_time = between(1, 3)
    
    @task(3)
    def get_block(self):
        block_num = random.randint(15000000, 15001000)
        self.client.get(
            f"/api/v1/block/{block_num}",
            headers={"X-API-Key": "test_key"}
        )
    
    @task(2)
    def validate_transaction(self):
        self.client.post(
            "/api/v1/validate/transaction",
            json={
                "tx_hash": "0x" + "a" * 64
            },
            headers={"X-API-Key": "test_key"}
        )
    
    @task(1)
    def get_stats(self):
        self.client.get(
            "/api/v1/stats",
            headers={"X-API-Key": "test_key"}
        )
ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ:
โ€ข ๋™์‹œ ์‚ฌ์šฉ์ž 100๋ช…: ํ‰๊ท  ์‘๋‹ต์‹œ๊ฐ„ 45ms
โ€ข ๋™์‹œ ์‚ฌ์šฉ์ž 500๋ช…: ํ‰๊ท  ์‘๋‹ต์‹œ๊ฐ„ 120ms
โ€ข ๋™์‹œ ์‚ฌ์šฉ์ž 1000๋ช…: ํ‰๊ท  ์‘๋‹ต์‹œ๊ฐ„ 280ms
โ€ข ์—๋Ÿฌ์œจ: 0.1% ๋ฏธ๋งŒ

์ถฉ๋ถ„ํžˆ ์‹ค์ „์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์„ฑ๋Šฅ์ด์•ผ!

๐Ÿ“š ์ถ”๊ฐ€ ํ•™์Šต ์ž๋ฃŒ

๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ์ž๋ฃŒ๋“ค์„ ์ถ”์ฒœํ•ด:

๊ณต์‹ ๋ฌธ์„œ:
โ€ข FastAPI ๊ณต์‹ ๋ฌธ์„œ: https://fastapi.tiangolo.com
โ€ข Web3.py ๋ฌธ์„œ: https://web3py.readthedocs.io
โ€ข Redis ๋ฌธ์„œ: https://redis.io/documentation

๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ:
โ€ข Ethereum ๊ฐœ๋ฐœ์ž ๋ฌธ์„œ
โ€ข Solidity ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๊ฐ€์ด๋“œ
โ€ข Web3 ๊ฐœ๋ฐœ ํŒจํ„ด

์„ฑ๋Šฅ ์ตœ์ ํ™”:
โ€ข Python ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ
โ€ข ์บ์‹ฑ ์ „๋žต๊ณผ ํŒจํ„ด
โ€ข API ์„ค๊ณ„ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

์žฌ๋Šฅ๋„ท์—์„œ ๋ธ”๋ก์ฒด์ธ์ด๋‚˜ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ ์ „๋ฌธ๊ฐ€๋ฅผ ์ฐพ์•„์„œ ๋ฉ˜ํ† ๋ง์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ์‹ค๋ฌด ๊ฒฝํ—˜์ด ์žˆ๋Š” ์‚ฌ๋žŒ์—๊ฒŒ ๋ฐฐ์šฐ๋ฉด ํ›จ์”ฌ ๋น ๋ฅด๊ฒŒ ์„ฑ์žฅํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿ“–

๐ŸŽ“ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€, ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด! ์šฐ๋ฆฌ๊ฐ€ ํ•จ๊ป˜ ๋งŒ๋“  ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ์„œ๋ฒ„๋Š”:

โœ… FastAPI๋กœ ๊ณ ์„ฑ๋Šฅ API ์„œ๋ฒ„ ๊ตฌ์ถ•
โœ… Web3.py๋กœ ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ์กฐํšŒ
โœ… ๊ฐ•๋ ฅํ•œ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋กœ์ง
โœ… Redis ์บ์‹ฑ์œผ๋กœ ์„ฑ๋Šฅ ์ตœ์ ํ™”
โœ… ๋ณด์•ˆ๊ณผ ์—๋Ÿฌ ํ•ธ๋“ค๋ง
โœ… Docker๋กœ ์‰ฌ์šด ๋ฐฐํฌ
โœ… ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น…

์ด ๋ชจ๋“  ๊ฑธ ๊ฐ–์ถ”๊ณ  ์žˆ์–ด. ์‹ค์ „์—์„œ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์ด์ง€!

๋ธ”๋ก์ฒด์ธ ๊ธฐ์ˆ ์€ ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ๊ณ , Web3 ์ƒํƒœ๊ณ„๋„ ๋น ๋ฅด๊ฒŒ ์„ฑ์žฅํ•˜๊ณ  ์žˆ์–ด. ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ์ด ์„œ๋ฒ„๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ NFT ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค, DeFi ํ”„๋กœํ† ์ฝœ, ๋ธ”๋ก์ฒด์ธ ๊ฒŒ์ž„ ๋“ฑ ๋‹ค์–‘ํ•œ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด.

์ค‘์š”ํ•œ ๊ฑด ๊ณ„์† ๋ฐฐ์šฐ๊ณ  ์‹คํ—˜ํ•˜๋Š” ๊ฑฐ์•ผ. ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•ด๋ณด๊ณ , ์—๋Ÿฌ๋ฅผ ๋งŒ๋‚˜๋ฉด ํ•ด๊ฒฐํ•˜๊ณ , ์„ฑ๋Šฅ์„ ์ธก์ •ํ•˜๊ณ  ๊ฐœ์„ ํ•˜๋Š” ๊ณผ์ •์„ ๋ฐ˜๋ณตํ•˜๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ์ „๋ฌธ๊ฐ€๊ฐ€ ๋˜์–ด ์žˆ์„ ๊ฑฐ์•ผ. ๐Ÿ’ช

ํ˜น์‹œ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๊ฑฐ๋‚˜ ๋” ๊นŠ์ด ์žˆ๋Š” ๋‚ด์šฉ์ด ๊ถ๊ธˆํ•˜๋‹ค๋ฉด, ๋ธ”๋ก์ฒด์ธ ๊ฐœ๋ฐœ ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์ฐธ์—ฌํ•˜๊ฑฐ๋‚˜ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ.

์ž, ์ด์ œ ๋‹น์‹  ์ฐจ๋ก€์•ผ! ์ด ๊ฐ€์ด๋“œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋ฉ‹์ง„ ๋ธ”๋ก์ฒด์ธ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ด. ํ™”์ดํŒ…! ๐Ÿš€๐Ÿ”—โœจ

๐ŸŽฏ ํ•ต์‹ฌ ์š”์•ฝ

FastAPI + Web3.py + Redis์˜ ์กฐํ•ฉ์œผ๋กœ
๊ณ ์„ฑ๋Šฅ ๋ธ”๋ก์ฒด์ธ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด.
์บ์‹ฑ, ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ, ๋ณด์•ˆ์„ ์ž˜ ์ฑ™๊ธฐ๋ฉด
์‹ค์ „์—์„œ ์ถฉ๋ถ„ํžˆ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์„œ๋น„์Šค๊ฐ€ ์™„์„ฑ๋ผ!

๐Ÿ’ก ๊ณ„์† ๋ฐฐ์šฐ๊ณ  ์‹คํ—˜ํ•˜์ž! ๐Ÿ’ก
๋Œ“๊ธ€ ์ž‘์„ฑ

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

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.