본문 바로가기

N8N - 웹 스크래핑 자동화: JWT 인증 + 병렬 처리 + PostgreSQL 중복 방지

by Tech.Knockknows 2025. 11. 19.

시리즈 요약: 이전 글에서 N8N + Playwright + FastAPI로 뉴스 스크래핑 자동화를 구축하고, Google Sheets 연동까지 완료했어요. 이번 편에서는 보안(JWT), 속도(병렬 처리), 데이터 품질(PostgreSQL 중복 방지) 세 가지를 개선하면서, 동시에 아키텍처를 재설계해서 각 컴포넌트의 역할을 명확하게 분리할 거예요!

이전 글에서 N8N과 Playwright, FastAPI, Google Sheets를 연동해 뉴스 스크래핑 자동화 파이프라인을 구축했어요.

docker-composelifespan을 사용해 이미 고속화된 API 서버를 구축했지만, 시스템에는 여전히 세 가지 아쉬운 점이 있었어요.

  1. 보안성 부재: FastAPI 엔드포인트가 외부에 그대로 노출되어 누구나 호출할 수 있었어요.
  2. 순차 처리: N8N 워크플로우가 제공된 URL을 순차적으로 요청하여, 아무리 lifespan이 빨라도 10개 URL 처리에 20~30초가 걸렸어요.
  3. 데이터 중복: 스크래핑 대상 URL에 대해 중복을 체크하지 않아 같은 기사가 Google Sheets에 계속 쌓일 수 있어요.

이번 글에서는 이 세 가지 문제를 한 번에 해결해볼게요. 그리고 더 나아가, FastAPI와 N8N의 역할을 명확하게 분리해서 유지보수하기 쉬운 시스템으로 개선할 거예요!

📦 GitHub 코드:

이 글의 모든 코드는 GitHub에서 확인할 수 있어요!

(https://github.com/knockknows/Blog/tree/main/code)

 

 

개선 목표

lifespan 아키텍처는 그대로 유지하면서, 보안과 효율성, 데이터 품질을 높이는 것이 목표예요.

개선 항목 기존 방식  개선 방식
기본 속도 lifespan으로 빠름 (요청당 1~2초) FastAPI lifespan (유지)
보안 누구나 API 호출 가능 JWT 토큰 인증 필요 🔒
효율성 순차 처리 (10개 = 20초) 병렬 처리 (10개 = 4초) ⚡
데이터 품질 중복 데이터 저장됨 PostgreSQL로 체크 후 저장 🗄️

📦 필요한 패키지:

GitHub의 requirements.txt에서 전체 목록을 확인할 수 있어요!

bash requirements.txt
# FastAPI & Server
fastapi==0.121.1
uvicorn[standard]==0.38.0

# Browser Automation
playwright==1.56.0

# Data Validation
pydantic==2.9.2

# JWT Authentication
python-jose[cryptography]==3.5.0

# Password Hashing
passlib[bcrypt]==1.7.4
bcrypt==4.0.1

# Form Data
python-multipart==0.0.20

 

1단계: JWT 인증으로 보안 강화 (상세)

JWT(JSON Web Token)는 서버에 세션을 저장하지 않고도 사용자를 인증할 수 있는 토큰 방식이에요. 기존에는 누구나 FastAPI를 호출할 수 있었지만, 이제는 유효한 토큰이 있어야만 스크래핑을 실행할 수 있어요.

JWT가 필요한 이유

인증 방식 기존 방식 (없음) JWT 방식
보안 누구나 호출 가능 ⚠️ 토큰 있어야 호출 ✅
상태 관리 - Stateless (서버 부담 없음)
만료 시간 없음 30분 후 자동 만료
N8N 통합 URL만 호출 토큰 발급 → 헤더에 포함

JWT 토큰의 구조

JWT 토큰은 3개 부분으로 나뉘어져 있고, 점(.)으로 구분돼요:

text JWT Structure
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJuOG5fdXNlciIsImV4cCI6MTczMjAwMDAwMH0.signature

│              Header              │             Payload             │   Signature   │
│ {"alg": "HS256", "typ": "JWT"}   │  {"sub": "n8n_user", "exp":...} │  암호화된 서명    │
  • Header: 토큰 타입과 암호화 알고리즘 정보
  • Payload: 사용자 정보 (username, 만료 시간 등)
  • Signature: Header + Payload를 SECRET_KEY로 암호화한 값

JWT 인증 흐름 (전체 과정)

text Authentication Flow
1. N8N → FastAPI: POST /login
   Body: {"username": "n8n_user", "password": "secure_password_123"}
   ↓
2. FastAPI: 비밀번호 해싱 후 검증
   - passlib.CryptContext로 bcrypt 해싱 비교
   - 일치하면 다음 단계, 불일치하면 401 에러
   ↓
3. FastAPI: JWT 토큰 생성
   - 사용자명(sub)과 만료 시간(exp)을 Payload에 포함
   - SECRET_KEY로 서명 생성
   ↓
4. FastAPI → N8N: 토큰 반환
   {"access_token": "eyJhbGc...", "token_type": "bearer", "expires_in": 1800}
   ↓
5. N8N: 토큰을 변수에 저장 (30분간 유효)
   ↓
6. N8N → FastAPI: POST /scrape/batch
   Header: Authorization: Bearer eyJhbGc...
   ↓
7. FastAPI: 토큰 검증
   - Bearer에서 토큰 추출
   - SECRET_KEY로 서명 검증
   - Payload에서 사용자명 추출
   - 만료 시간 확인
   ↓
8. FastAPI: 스크래핑 실행 ✅

JWT 코드 구현

GitHub의 main_enhanced.py에서 전체 코드를 확인할 수 있어요!

python main_enhanced.py
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta

# JWT 설정
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-this")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))

# 비밀번호 해싱
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# 간단한 사용자 DB (실제로는 PostgreSQL 등 사용)
FAKE_USERS_DB = {
    "n8n_user": {
        "username": "n8n_user",
        "hashed_password": "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYzpLaOALem"
    }
}

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """비밀번호 검증"""
    return pwd_context.verify(plain_password, hashed_password)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """JWT 액세스 토큰 생성"""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

@app.post("/login", response_model=Token)
async def login(request: LoginRequest):
    """JWT 토큰 발급"""
    user = FAKE_USERS_DB.get(request.username)
    
    if not user or not verify_password(request.password, user["hashed_password"]):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="사용자명 또는 비밀번호가 잘못되었습니다."
        )
    
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user["username"]},
        expires_delta=access_token_expires
    )
    
    return Token(
        access_token=access_token,
        token_type="bearer",
        expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60
    )

💡 코드 핵심 포인트:

  • 🛡️ 비밀번호 해싱 (Bcrypt): DB에 비밀번호를 평문으로 저장하는 것은 매우 위험해요. passlib을 사용해 비밀번호를 암호화(해싱)하여 저장하고 검증합니다.
  • ⏱️ 만료 시간 설정 (Expiration): 탈취된 토큰이 악용되는 것을 막기 위해 유효 기간(exp)을 설정해요. 여기선 30분으로 설정되어 있어, 30분이 지나면 토큰이 자동 만료됩니다.
  • 🔑 환경 변수 사용 (Security): SECRET_KEY는 토큰의 위조를 막는 핵심 열쇠예요. 코드에 직접 적지 않고 os.getenv로 환경 변수에서 불러와 보안을 유지합니다.

🚧 참고: 실제 운영 환경에서는?

현재 코드의 FAKE_USERS_DB는 예시를 위해 하드코딩된 딕셔너리 형태입니다. 실제 서비스에서는 이 부분을 PostgreSQL이나 MySQL 같은 데이터베이스와 연동해야 합니다. 사용자가 로그인할 때 DB에서 해당 사용자의 해시된 비밀번호를 조회하여 검증하도록 코드를 수정하면, 동적으로 회원 관리가 가능해집니다.

 

2단계: 병렬 스크래핑으로 속도 개선 (상세)

Python의 asyncio를 활용하면 여러 URL을 동시에 스크래핑할 수 있어요!

병렬 처리가 필요한 이유

처리 방식 순차 처리 병렬 처리 (5개) 개선율
10개 URL 약 20초 약 4초 5배 ⚡
50개 URL 약 100초 약 20초 5배 ⚡
100개 URL 약 200초 약 40초 5배 ⚡

병렬 스크래핑 핵심 코드

python main_enhanced.py
@app.post("/scrape/batch", response_model=List[ScrapeResponse])
async def batch_scrape(
    request: BatchScrapeRequest,
    current_user: str = Depends(get_current_user)
):
    """병렬 스크래핑 (여러 URL 동시 처리)"""
    logger.info(f"📥 병렬 스크래핑 요청: {len(request.urls)}개 URL")
    
    # Semaphore로 동시 실행 제한
    semaphore = asyncio.Semaphore(request.max_concurrent)
    
    async def scrape_with_semaphore(url: HttpUrl):
        async with semaphore:
            return await scrape_single_url(
                url=str(url),
                wait_for=request.wait_for,
                timeout=request.timeout,
                stealth_mode=request.stealth_mode
            )
    
    # 병렬 실행
    tasks = [scrape_with_semaphore(url) for url in request.urls]
    results = await asyncio.gather(*tasks)
    
    success_count = sum(1 for r in results if r.success)
    logger.info(f"✅ 병렬 스크래핑 완료: {success_count}/{len(results)} 성공")
    
    return results

💡 코드 핵심 포인트:

  • 🚦 세마포어 (Semaphore): asyncio.Semaphore(request.max_concurrent)는 동시에 실행될 수 있는 작업의 수를 제한합니다. 예를 들어 URL이 100개라도 세마포어가 5라면, 동시에 5개의 브라우저 탭만 열리고 나머지는 대기합니다. 이는 서버 리소스 고갈을 막는 핵심 장치예요.
  • ⚡ Gather (동시 실행): asyncio.gather(*tasks)는 준비된 모든 스크래핑 작업(Task)을 한 번에 실행하고, 모든 작업이 끝날 때까지 기다린 후 결과를 리스트로 반환합니다. 순차적으로 하나씩 기다리는 것보다 훨씬 효율적입니다.

 

완성된 FastAPI 코드

전체 코드는 GitHub에서 확인하세요: main_enhanced.py

💡 핵심 특징:

  • JWT 인증: 보안 강화
  • 병렬 처리: asyncio.gather() + Semaphore
  • Lifespan: 브라우저 연결 재사용
  • Stealth 모드: 봇 탐지 우회
  • 에러 처리: 타임아웃 및 예외 핸들링

 

3단계: Docker Compose 설정

GitHub의 docker-compose.yml을 확인하세요.

예전에 작성했던 .yml 파일과 조금 달라졌어요. 설정 값을 이제는 .env 라는 환경변수 파일에서 읽어와요. 그래서 fastapi의 설정을 보면 { ... } 사이에 있는 값을 .env 파일에서 읽어오는 것을 볼 수 있어요.

1. docker-compose.yml

yaml docker-compose.yml
version: '3.8'

services:
  # Playwright 브라우저 서버
  playwright:
    image: [mcr.microsoft.com/playwright:v1.56.0-jammy](https://mcr.microsoft.com/playwright:v1.56.0-jammy)
    container_name: playwright_server
    command: npx -y playwright@1.56.0 run-server --host 0.0.0.0 --port 3000
    ports:
      - "3000:3000"
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - n8n_network
    restart: unless-stopped

  # FastAPI 애플리케이션 (순수 스크래핑만)
  fastapi:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: fastapi_scraper
    env_file:
      - .env
    environment:
      - PLAYWRIGHT_SERVER_URL=${PLAYWRIGHT_SERVER_URL:-ws://playwright:3000}
      - SECRET_KEY=${SECRET_KEY}
      - ACCESS_TOKEN_EXPIRE_MINUTES=${ACCESS_TOKEN_EXPIRE_MINUTES:-30}
    ports:
      - "8000:8000"
    depends_on:
      playwright:
        condition: service_healthy
    volumes:
      - ./main_enhanced.py:/app/main.py
    networks:
      - n8n_network
    restart: unless-stopped

# N8N과 같은 네트워크 사용 (외부 네트워크)
networks:
  n8n_network:
    external: true
    name: ${NETWORK_NAME:-n8n_network}

💡 주요 변경점 설명:

  • Playwright 서비스 분리: 브라우저 엔진만 별도 컨테이너로 띄워 관리해요. 이렇게 하면 FastAPI가 가벼워지고, 브라우저 충돌 시 브라우저 컨테이너만 재시작하면 됩니다.
  • depends_on: FastAPI 컨테이너가 시작될 때 Playwright 브라우저가 준비될 때까지 기다리도록 설정했어요. (condition: service_healthy)
  • n8n_network: N8N과 FastAPI가 서로 통신할 수 있도록 같은 Docker 네트워크로 묶어주었습니다. 이 설정이 없으면 N8N에서 FastAPI를 호출할 수 없어요!

 

2. Dockerfile

docker 파일은 이전에 비해 시스템 의존성으로 gcc 패키지를 설치해요.

GitHub: Dockerfile

dockerfile Dockerfile
FROM python:3.11-slim

WORKDIR /app

# 시스템 의존성 설치 (최소화)
RUN apt-get update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# Python 패키지 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 애플리케이션 코드 복사
COPY main_enhanced.py main.py

# 포트 노출
EXPOSE 8000

# 애플리케이션 실행
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

💡 왜 'slim'과 'gcc'를 쓰나요?

  • python:3.11-slim: 기본 파이썬 이미지는 용량이 크기 때문에, 불필요한 도구가 제거된 'slim' 버전을 사용하여 이미지 크기를 1/5 수준으로 줄였습니다.
  • gcc 설치: python-jose(암호화)나 일부 파이썬 패키지는 설치 과정에서 C 컴파일러(gcc)를 필요로 합니다. slim 버전에는 이게 없어서 수동으로 설치해줘야 해요.

 

3. 환경 변수 설정 (.env)

GitHub의 env.example을 복사해서 사용해요.

env.example -> .env 로 변경해야해요.

properties .env
# JWT 인증 설정
SECRET_KEY=your-secret-key-change-this-in-production-2024-min-32-characters
ACCESS_TOKEN_EXPIRE_MINUTES=30

# Playwright 설정
PLAYWRIGHT_SERVER_URL=ws://playwright:3000

# Docker 네트워크 설정
NETWORK_NAME=n8n_network

🛡️ 보안 팁:

SECRET_KEY는 JWT 토큰의 서명을 만드는 중요한 열쇠예요. 절대 GitHub에 그대로 올리지 말고, 로컬의 .env 파일에서만 관리하세요. 실제 운영 시에는 길고 복잡한 무작위 문자열을 사용해야 안전합니다.

4. 실행 방법

bash Terminal
# 1. N8N 네트워크 확인
docker network ls | grep n8n

# 2. 환경 파일 생성 및 수정
cp env.example .env
nano .env  # SECRET_KEY, NETWORK_NAME 수정

# 3. 서비스 시작
docker compose up -d

# 4. 확인
curl http://localhost:8000/health

 

4단계: N8N이 PostgreSQL 관리

중복 체크와 데이터 저장은 모두 N8N에서 PostgreSQL을 통해 관리해요!

⚠️ 중요: FastAPI에는 PostgreSQL 코드가 없어요! 모든 DB 작업은 N8N에서 처리합니다.

1. 테이블 생성 

shell Terminal
## Docker로 동작중인 PostgreSQL 컨테이너에 Shell 연결
knockknows@instance:~/n8n_domain$ docker compose exec postgres /bin/bash

## PSQL로 PostgreSQL 접속
root@7c1b0c0ed470:/# psql -U <PostgreSQL 관리자 계정> -d <DB 이름>

## 새로운 Database 및 DB 생성
n8n=# create database <생성할 DB 이름>;
CREATE DATABASE

## N8N PostgreSQL 노드 (Execute Query)
n8n_workflow_db=# CREATE TABLE IF NOT EXISTS processed_urls (
    id SERIAL PRIMARY KEY,
    url TEXT UNIQUE NOT NULL,
    title TEXT,
    success BOOLEAN DEFAULT true,
    processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

## 인덱스 생성 (검색 속도 10~100배 향상)
n8n_workflow_db=# CREATE INDEX IF NOT EXISTS idx_url ON processed_urls(url);

💡 SQL 포인트: UNIQUE NOT NULL 제약 조건을 걸어두면 DB 레벨에서 중복 저장을 원천 차단할 수 있습니다. 나중에 같은 URL이 들어오면 DB가 알아서 거부하거나 업데이트해요.

2. URL 추출 및 배열 변환 (Code 노드)

RSS 피드는 여러 개의 아이템(객체)을 배열 형태로 제공해요. 하지만 PostgreSQL에서 중복 체크를 하려면 URL로만 구성된 배열이 필요하죠. 그래서 Code 노드를 추가해서 RSS 아이템에서 link만 쏙 뽑아내야 합니다.

javascript Extract Links Code
// 이전 노드(RSS Read)의 모든 아이템에서 'link' 속성만 추출
const urls = $input.all().map(item => item.json.link);

// 배열을 하나의 긴 문자열로 합칩니다. 구분자는 URL에 절대 쓰이지 않는 문자열(예: '|||')을 사용합니다.
// 예: "url1|||url2|||url3"
const urlString = urls.join('|||');

// 다음 노드(PostgreSQL)로 전송
return { json: { urls: urlString } };

3. 중복 URL 체크 (PostgreSQL)

이제 DB에서 중복을 확인할 차례예요. 여기서 중요한 점은 "성공적으로 처리된(success=true)" URL만 중복으로 간주해야 한다는 점이에요. 만약 이전에 실패했다면 다시 시도해야 하니까요!

sql SQL Query
-- 성공 이력이 있는 URL만 중복으로 체크
SELECT url FROM processed_urls 
WHERE url = ANY($1::text[])
AND success = true;

-- 파라미터: $1 = {{ $json.urls }} (위의 Code 노드에서 만든 배열)

4. 중복 제외 필터링 (Code 노드)

javascript N8N Code Node
// 1. PostgreSQL에서 조회한 '이미 처리된(성공한)' URL 목록
const processedUrls = $('중복 체크').all()
  .map(item => item.json.url);

// 2. 원본 RSS 데이터 (모든 정보 포함)
const allItems = $('RSS Read').all();

// 3. 중복이 아닌(DB에 없는) URL만 필터링하여 배열로 생성
const newUrls = allItems
  .filter(item => !processedUrls.includes(item.json.link)) // DB에 없는 것만 통과
  .map(item => item.json.link); // 통과된 아이템에서 'link'만 뽑아서 문자열 배열 생성

// 4. HTTP Request (Batch) 노드가 한 번에 받을 수 있는 형태로 반환
// 결과 예시: [{ json: { urls: ["http://...", "http://..."] } }]
return {
  json: {
    urls: newUrls
  }
};

5. 처리 완료 URL 저장

sql SQL Query
-- N8N PostgreSQL 노드 (Execute Query)
INSERT INTO processed_urls (url, title, success)
VALUES ($1, $2, true) -- 여기서 success를 true로 명시적 지정
ON CONFLICT (url) DO UPDATE SET
  title = EXCLUDED.title,
  success = true, -- 업데이트 시에도 true로 설정
  processed_at = CURRENT_TIMESTAMP;

💡 UPSERT (ON CONFLICT) 란?

이미 존재하는 URL이 들어오면 에러를 내는 대신(INSERT 실패), 기존 정보를 최신 정보로 업데이트(UPDATE)하는 방식이에요. 이를 통해 스크래핑 재시도 시 정보를 갱신할 수 있어 데이터 무결성이 보장됩니다.

 

5단계: 완전한 N8N 워크플로우 설정

이제 모든 것을 통합한 완전한 워크플로우예요! 각 단계별 핵심 설정값을 확인해보세요.

💡 워크플로우 논리 구조:

  • 선(先) 필터링 (단계 3~6): 무거운 스크래핑 작업을 하기 전에 DB에서 중복 여부를 먼저 확인합니다. URL을 문자열로 변환하여 PostgreSQL에서 효율적으로 조회하고, 중복이 아닌 것만 배열로 만들어 새 URL이 있을 때만 다음 단계로 진행합니다. 이렇게 해야 불필요한 API 호출을 줄이고 속도를 높일 수 있어요.
  • 병렬 스크래핑 (단계 7~9): JWT 토큰을 발급받고, FastAPI에 URL 배열을 전송하여 병렬로 스크래핑합니다. 실패한 요청을 제외하기 위해 Filter로 success=true인 것만 통과시킵니다.
  • 데이터 추출 및 정규화 (단계 10~12): Loop Over Items로 각 아이템을 순회하면서, WebpageContentExtractor로 HTML에서 텍스트를 추출하고(단계 11), Google Sheets 헤더 형식에 맞게 데이터를 정규화합니다.
  • 후(後) 저장 (단계 13~14): 스크래핑과 구글 시트 저장이 모두 성공했을 때만 DB에 '처리 완료'라고 기록합니다. 만약 중간에 에러가 나면 DB에 기록되지 않으므로, 다음 실행 때 다시 시도하게 되어 누락을 방지합니다.

1. HTTP Request (JWT 토큰 발급)

FastAPI에 로그인하여 인증 토큰을 받아오는 단계입니다.

설정 항목 값 (Value)
Method POST
URL http://fastapi:8000/login (Docker 내부망 주소)
Body Content Type JSON
Body Parameters
{
  "username": "n8n_user",
  "password": "secure_password_123"
}

2. HTTP Request (Batch 스크래핑)

발급받은 토큰을 헤더에 싣고, 필터링된 URL 목록을 전송하여 병렬 스크래핑을 요청합니다.

설정 항목 값 (Value)
Method POST
URL http://fastapi:8000/scrape/batch
Authentication Header Auth
Header Parameters Name: Authorization
Value: Bearer {{ $json.access_token }}
Body Parameters
{
  "urls": {{ $json.urls }},
  "max_concurrent": 5,
  "wait_for": "load"
}
(이전 Code 노드에서 만든 URL 배열을 넣습니다)

3. Filter (성공 여부 필터링)

FastAPI는 실패한 요청도 에러를 내지 않고 success: false 응답을 줍니다. 따라서 저장하기 전에 성공한 건들만 걸러내야 합니다.

설정 항목 값 (Value)
Condition Boolean
Value 1 {{ $json.success }}
Operation is true


⚠️ 중요: 컨테이너명 사용!

N8N에서 FastAPI를 호출할 때는 localhost가 아닌 컨테이너명을 사용해야 해요!

  • ❌ 잘못된 예: http://localhost:8000/scrape
  • ✅ 올바른 예: http://fastapi:8000/scrape 또는 http://fastapi_scraper:8000/scrape

 

트러블슈팅 (Q&A)

Q1: JWT 토큰이 계속 만료돼요

A: JWT 토큰은 기본 30분 동안만 유효해요. 워크플로우 맨 앞에 JWT 토큰 발급 노드를 배치하면 매번 새 토큰을 받아요.

Q2: N8N에서 FastAPI 연결이 안 돼요

A: localhost 대신 컨테이너명을 사용하세요! N8N과 FastAPI가 같은 Docker 네트워크에 있어야 해요.

Q3: 병렬 처리가 느려요

A: max_concurrent 값을 늘려보세요. 하지만 너무 많이 늘리면 메모리가 부족하거나 봇으로 감지될 수 있어요.

 

정리

이전 글에서 만든 시스템을 이번 글에서 완전히 개선했어요!

항목 이전 글 현재 글 (완성) 개선
보안 없음 JWT 토큰
속도 20초 (10개 URL) 4초 (병렬 처리) 5배 ⚡
중복 방지 없음 PostgreSQL
아키텍처 혼재 역할 분리

🎉 최종 시스템의 특징:

  • 보안: JWT 토큰으로 인증된 요청만 처리
  • 속도: 병렬 처리로 5배 빠른 스크래핑
  • 품질: PostgreSQL로 중복 데이터 완벽 차단
  • 구조: FastAPI는 스크래핑, N8N은 데이터 관리
  • 확장성: FastAPI를 여러 개 띄워도 문제없음

📦 전체 코드 다운로드:

🔗 GitHub: https://github.com/knockknows/Blog/tree/main/code

상세한 설정 가이드는 README.md를 참고하세요!