Skip to content

Conversation

@bbbang105
Copy link
Member

@bbbang105 bbbang105 commented Sep 21, 2025

✅ PR 유형

어떤 변경 사항이 있었나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

🚀 작업 내용

이번 PR에서 작업한 내용을 구체적으로 설명해주세요. (이미지 첨부 가능)

기존 문제점 (Problem)

  • 높은 유지 비용 및 비효율성: 월간 약 200~300회의 낮은 요청 빈도에도 불구하고, 크롤링 서버를 위해 24시간 내내 EC2 t2.micro 인스턴스를 유지해야 했습니다. 이는 유휴 시간 대부분에 불필요한 비용이 발생하는 비효율적인 구조였습니다.
  • 수동 배포 프로세스: 코드 변경 사항을 서버에 반영하기 위한 배포 과정이 자동화되어 있지 않아, 직접 서버에 접속하여 수동으로 업데이트해야 했습니다. 이로 인해 배포 시간이 오래 걸리고 사람의 실수(Human Error)가 발생할 위험이 있었습니다.
  • 부족한 확장성: t2.micro 단일 인스턴스 구조는 갑작스러운 트래픽 증가에 대응할 수 없어, 동시 요청이 몰릴 경우 서버가 다운될 수 있는 잠재적인 불안정성을 내포하고 있었습니다.

변경 사항 (Solution)

1. 서버 아키텍처 현대화 (EC2 → Serverless Lambda)

상시 실행되던 EC2 인스턴스 기반의 서버를, 요청이 있을 때만 실행되는 AWS Lambda 기반의 서버리스(Serverless) 아키텍처로 전환했습니다. 배포 단위는 Docker 컨테이너 이미지를 사용하여, 로컬 개발 환경과 실제 운영 환경의 일관성을 확보했습니다.

2. API 프레임워크 업그레이드 (Flask → FastAPI)

기존 Flask로 작성되었던 API 서버를, 현대적인 고성능 웹 프레임워크인 FastAPI로 마이그레이션했습니다. 이를 통해 다음과 같은 개선을 이루었습니다.

  • 비동기 처리: async/await를 활용하여 I/O 병목을 줄일 수 있는 기반을 마련했습니다.
  • 자동 API 문서: Swagger UI가 자동으로 생성되어 API 테스트 및 명세 확인이 용이해졌습니다.
  • 코드 안정성: Pydantic을 활용한 타입 검증, 표준화된 에러 응답 형식 도입으로 코드의 안정성과 예측 가능성을 높였습니다.

3. CI/CD 파이프라인 구축 (배포 자동화)

GitHub Actions, Docker, Amazon ECR, AWS Lambda를 연동하여 CI/CD 파이프라인을 구축했습니다. 이제 develop 브랜치에 코드를 푸시하기만 하면, 아래의 모든 과정이 자동으로 실행됩니다.

  1. Docker 이미지 빌드
  2. Amazon ECR(컨테이너 이미지 저장소)에 이미지 푸시
  3. AWS Lambda 함수의 코드 및 환경 변수 업데이트

4. 성능 최적화 (콜드 스타트 이슈 대응)

배포 초기, Lambda의 콜드 스타트로 인해 응답 속도가 5초 이상 소요되는 문제가 있었습니다. 다음과 같은 최적화를 통해 문제를 해결했습니다.

  • WebDriver Pool 최적화: Lambda 실행 환경에 맞춰 불필요한 동시성 제어 로직을 간소화하고, WebDriver Pool 크기를 5개에서 1개로 축소하여 초기화 및 종료 시간을 단축했습니다.
  • Lambda 메모리 증설: Lambda는 메모리가 커질수록 더 높은 성능의 CPU를 할당받습니다. 메모리를 1024MB에서 3008MB로 증설하여, 콜드 스타트 시에도 응답 속도를 1~2초대로 크게 개선했습니다.

기대 효과 (Expected Effects)

  • 획기적인 비용 절감: 24시간 실행되던 EC2 인스턴스 비용(월 약 15,000원)이 제거되고, 현재 요청량 기준으로는 AWS Lambda의 무료 사용량(Free Tier) 범위 내에 있어 사실상 월간 서버 비용이 0원에 수렴하게 되었습니다.
  • 운영 효율성 증대: git push만으로 배포가 자동화되어 배포에 소요되는 시간이 사라졌으며, OS 업데이트나 보안 패치 등 서버를 직접 관리해야 하는 부담이 완전히 없어졌습니다.
  • 높은 확장성 및 안정성 확보: 사용자가 갑자기 몰리더라도 Lambda가 자동으로 확장하여 요청을 처리하므로, t2.micro 환경에서 발생할 수 있었던 서버 다운의 위험 없이 안정적인 서비스 운영이 가능해졌습니다.
  • 개발 생산성 향상: API 키를 통한 보안 강화, 표준화된 에러 핸들링, 자동 API 문서화 등을 통해 더욱 견고하고 유지보수하기 쉬운 애플리케이션 구조를 갖추게 되었습니다.

📝️ 관련 이슈

본인이 작업한 내용이 어떤 Issue와 관련이 있는지 작성해주세요.


💬 기타 사항 or 추가 코멘트

남기고 싶은 말, 참고 블로그 등이 있다면 기록해주세요.

Summary by CodeRabbit

  • New Features

    • 인증된 GET /api/v1/schedule 엔드포인트(FastAPI)와 AWS Lambda용 핸들러, 통합 로깅·일관된 오류 응답 및 요청 처리 시간 헤더 제공.
    • 예외 발생 시 Discord로 알림 전송 기능 추가(에러 모니터링 강화).
  • Refactor

    • 컨테이너 베이스 및 배포 흐름을 ECR→Lambda 중심으로 전환 및 Flask 기반 서비스 제거.
    • WebDriver 풀 도입으로 동시성·자원 관리 개선.
  • Documentation

    • 시스템 아키텍처 섹션 제거/업데이트.
  • Chores

    • CI/CD 워크플로명·시크릿 처리 정비, 의존성 목록 변경, .gitignore·PR 템플릿 문구 수정.

@bbbang105 bbbang105 self-assigned this Sep 21, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 21, 2025

Walkthrough

Flask 앱을 제거하고 FastAPI(Mangum)로 전환했으며 Selenium WebDriver 풀을 도입하고 Discord 알림·로깅·보안·예외 핸들러 모듈을 추가했습니다. CI/CD는 DockerHub/EC2에서 Amazon ECR→AWS Lambda 이미지 배포로 변경되었습니다. (50단어 이내)

Changes

Cohort / File(s) Summary
CI/CD → ECR → Lambda
.github/workflows/crawler-cicd.yaml
워크플로명·잡 변경, actions/checkout@v4 사용, AWS 자격증명 구성으로 전환, Amazon ECR 로그인·이미지 빌드·푸시(태그: git SHA) 추가, Lambda 코드 및 구성(환경변수) 업데이트. DockerHub/EC2 단계 제거.
레포지토 템플릿·무시·문서
.github/PULL_REQUEST_TEMPLATE.md, \.gitignore, README.md
PR 템플릿의 한글 오타 수정, *.idea Git 무시 추가, README에서 "System Architecture" 섹션(이미지 포함) 삭제.
컨테이너 베이스·런타임
Dockerfile
베이스를 AWS Lambda Python 3.12 이미지로 변경, microdnf로 의존성 설치, Chrome for Testing/Chromedriver를 /opt에 설치, 파일 복사·의존성 설치 순서 조정, CMD를 Lambda 핸들러로 변경.
애플리케이션 진입/구성
app/main.py
FastAPI 앱·lifespan 등록, WebDriverPool 초기화/종료, 로깅 설정 호출, 예외 핸들러 등록, 처리시간 미들웨어·CORS 설정, 라우트 포함, Mangum 핸들러 노출.
API 라우트 추가
app/api/v1/schedule.py
GET /schedule 엔드포인트 추가: X-API-Key 의존성, URL 검증, 요청·풀 전달로 crawl_schedule을 스레드로 실행, CRAWLING-003 → 400, 기타 예외 → 500 처리.
보안 의존성
app/core/security.py
X-API-Key 헤더를 환경변수 API_KEY와 상수시간 비교로 검증; 누락/불일치 시 구조화된 401 반환.
예외 핸들러
app/core/exception_handlers.py
StarletteHTTPException 및 RequestValidationError를 일관된 JSON 응답으로 변환하는 핸들러 추가.
로깅 구성
app/core/logging_config.py
dictConfig 기반 콘솔 로깅 구성과 setup_logging() 제공.
크롤러 리팩터링
app/services/crawler.py
전역 WebDriver 제거, 시그니처 변경 → (url: str, request: Request, pool: WebDriverPool), 풀에서 드라이버 대여·반납, 예외 시 traceback을 Discord로 전송하고 재발생, finally에서 안전하게 반환.
WebDriver 풀 신설
app/services/driver_pool.py
WebDriverPool 클래스 추가: 드라이버 생성(옵션/Service), get_driver/release_driver/close_all, 내부 큐·동기화 구현.
Discord 로거(new)
app/utils/discord_logger.py
웹훅 검사·임베드 구성·타임아웃·오류 처리 포함 예외 전송 함수 추가(요청 컨텍스트 포함).
기존 Flask 삭제
app.py
Flask 인스턴스, /schedule 라우트, CORS 및 실행 블록 전체 제거.
구형 Discord 로거 제거
utils/discord_logger.py
기존 모듈 삭제(새 위치의 Discord 로거로 대체).
의존성 전환
requirements.txt
flask, flask_cors 제거; fastapi, uvicorn[standard], python-multipart, mangum 등 추가.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Client as 클라이언트
    participant APIGW as API Gateway
    participant Lambda as AWS Lambda (Mangum)
    participant FastAPI as FastAPI 앱
    participant Sec as API Key 검증
    participant Pool as WebDriverPool
    participant Crawler as crawl_schedule
    participant Discord as Discord Webhook

    Client->>APIGW: GET /api/v1/schedule?url=...
    APIGW->>Lambda: Invoke
    Lambda->>FastAPI: ASGI 요청
    FastAPI->>Sec: X-API-Key 검증
    Sec-->>FastAPI: OK / 401
    alt 유효한 키
        FastAPI->>Pool: get_driver()
        FastAPI->>Crawler: crawl_schedule(url, request, pool) [스레드]
        Crawler-->>Pool: release_driver()
        alt 성공
            Crawler-->>FastAPI: 결과 (200)
        else 도메인 오류 (CRAWLING-003)
            Crawler-->>FastAPI: 오류 응답 (400)
        end
    else 키 누락/불일치
        FastAPI-->>Client: 401 JSON
    end
    opt 예외 발생
        Crawler->>Discord: send_error_to_discord(...)
        Crawler-->>FastAPI: 예외 -> 500 JSON
    end
    FastAPI-->>Lambda: 응답
    Lambda-->>APIGW: Response
    APIGW-->>Client: HTTP 응답
Loading
sequenceDiagram
    autonumber
    actor GH as GitHub Actions
    participant ECR as Amazon ECR
    participant Lambda as AWS Lambda

    GH->>ECR: Login (amazon-ecr-login)
    GH->>ECR: Build & Push image (:<git-sha>)
    GH->>Lambda: UpdateFunctionCode (imageUri)
    GH->>Lambda: Wait for update
    GH->>Lambda: UpdateFunctionConfiguration (env vars)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

(_/) 토끼가 속삭여요,
(•ᴗ•) FastAPI로 길을 바꿨네,
🐰 드라이버는 풀에 줄지어,
☁️ 이미지 ECR로, Lambda로 훌쩍,
🔔 오류면 디스코드에 톡 보낼게요.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "[feat] : FastAPI로 마이그레이션 & AWS Lambda로 배포한다"은 변경의 핵심인 Flask→FastAPI 마이그레이션과 AWS Lambda 배포를 간결하고 명확하게 요약합니다; 불필요한 파일 목록이나 모호한 표현이 없으므로 팀원이 변경 의도를 빠르게 파악할 수 있습니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#15

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bbbang105 bbbang105 added 🚀 feat 새로운 기능 추가 / 일부 코드 추가 / 일부 코드 수정 (리팩토링과 구분) / 디자인 요소 수정 😵‍💫 sangho 상호 Issue or PR labels Sep 21, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/crawler-cicd.yaml (1)

51-64: 환경변수 업데이트 시 기존 변수 손실 위험

update-function-configuration --environment Variables={...}는 기존 변수를 덮어씁니다.

적용 제안(diff):

-      - name: Update Lambda function configuration
+      - name: Update Lambda function configuration (merge env)
         env:
           API_KEY_SECRET: ${{ secrets.API_KEY }}
           DISCORD_URL_SECRET: ${{ secrets.DISCORD_WEBHOOK_URL }}
         run: |
-          # Lambda가 환경 변수를 업데이트할 때까지 잠시 대기
-          aws lambda wait function-updated --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }}
-
-          # GitHub Secret 값을 사용하여 Lambda 함수의 환경 변수를 설정
-          aws lambda update-function-configuration \
-            --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
-            --environment "Variables={API_KEY=$API_KEY_SECRET,DISCORD_WEBHOOK_URL=$DISCORD_URL_SECRET}"
+          set -euo pipefail
+          aws lambda wait function-updated --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }}
+          EXISTING=$(aws lambda get-function-configuration --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} --query 'Environment.Variables' --output json)
+          echo "$EXISTING" | jq --arg api "$API_KEY_SECRET" --arg wh "$DISCORD_URL_SECRET" '. + {API_KEY:$api, DISCORD_WEBHOOK_URL:$wh}' > merged_env.json
+          aws lambda update-function-configuration \
+            --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
+            --environment "Variables=$(cat merged_env.json)"
+          aws lambda wait function-updated --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }}
app/services/crawler.py (1)

37-43: 오류 응답 형태 일관성: 서비스 레이어에서 dict 반환 대신 예외를 던지세요

전역 예외 핸들러를 도입했으므로 여기서는 400 예외를 던지는 편이 일관적입니다. 현재는 payload 누락으로 스키마도 달라집니다.

적용 제안(diff):

-        if not days:
-            return {
-                "code": "CRAWLING-003",
-                "message": "공개되지 않은 시간표입니다.",
-                "is_success": False
-            }
+        if not days:
+            from fastapi import HTTPException
+            raise HTTPException(
+                status_code=400,
+                detail={"code": "CRAWLING-003", "message": "공개되지 않은 시간표입니다.", "is_success": False}
+            )
🧹 Nitpick comments (19)
.github/PULL_REQUEST_TEMPLATE.md (1)

31-31: LGTM, 표기 명확성 소폭 개선 제안(옵션)

문구 자체는 자연스럽고 의도(연관만 표시, 자동 종료 안 함)도 적절합니다. 혼동 방지를 위해 “이슈를 닫지 않음”을 괄호에 명시하면 좋겠습니다.

- - Related to : #00 #01 (해당 커밋과 관련됨)
+ - Related to : #00 #01 (연관 — 이슈를 닫지 않음)
README.md (1)

8-8: 아키텍처 이미지: HTML 대신 마크다운 이미지로 전환 + 대체 텍스트 구체화 권장

GitHub 렌더링/반응형 대응을 위해 <img width="983"> 고정보다 ![설명](url) 형식을 권장합니다. 대체 텍스트도 “FastAPI→API Gateway→Lambda(ECR 이미지) 아키텍처”처럼 구체화해 주세요.

app/core/logging_config.py (1)

20-23: 로그 레벨/포맷 환경변수화로 운영 편의성 개선

CloudWatch에서 단계별 레벨 제어가 필요합니다. LOG_LEVEL(기본 INFO), JSON 포맷 선택 토글을 지원해 주세요.

적용 예시(diff):

+import os
 LOGGING_CONFIG = {
@@
-    "root": {  # 모든 로거의 기본 설정
-        "level": "INFO",  # INFO 레벨 이상의 로그만 출력
+    "root": {
+        "level": os.getenv("LOG_LEVEL", "INFO"),
         "handlers": ["console"],
     },
 }
app/utils/discord_logger.py (2)

57-63: 예외 로깅은 logging.exception 사용

스택트레이스 자동 포함을 위해 error 대신 exception 사용.

적용 예시(diff):

-    except requests.exceptions.RequestException as err:
-        logger.error(f"❌ Discord 전송 실패 (네트워크/HTTP 오류): {err}")
+    except requests.exceptions.RequestException as err:
+        logger.exception("❌ Discord 전송 실패 (네트워크/HTTP 오류)")
         if err.response:
-            logger.error(f"응답 코드: {err.response.status_code}, 응답 내용: {err.response.text}")
+            logger.exception(f"응답 코드: {err.response.status_code}, 응답 내용: {err.response.text}")
 
-    except Exception as err:
-        logger.error(f"❌ Discord 알림 처리 중 알 수 없는 오류 발생: {err}", exc_info=True)
+    except Exception:
+        logger.exception("❌ Discord 알림 처리 중 알 수 없는 오류 발생")

11-13: 환경변수 지연 로드 또는 재평가 권장

모듈 임포트 시점에 .env를 로드하고 값을 고정합니다. 런타임에 변경될 수 있으니 함수 내부에서 os.getenv 재조회 또는 초기화 함수를 두는 방식을 권장합니다.

app/api/v1/schedule.py (2)

17-26: 파라미터 검증을 Pydantic/Query로 위임하고 도메인 검사를 보완

  • url은 필수로 선언해 422를 활용하세요.
  • www. 서브도메인 허용(현재는 거부).

적용 예시(diff):

-from fastapi import APIRouter, Request, HTTPException, Query, Depends
+from fastapi import APIRouter, Request, HTTPException, Query, Depends
+import re
@@
-async def get_schedule(request: Request, url: str = Query(None)):
+async def get_schedule(
+    request: Request,
+    url: str = Query(..., min_length=10, regex=r"^https://(www\.)?everytime\.kr/.*"),
+):
@@
-    if not url:
-        raise HTTPException(status_code=400,
-                            detail={"code": "CRAWLING-001", "message": "URL이 제공되지 않았습니다.", "is_success": False})
-    if not url.startswith("https://everytime.kr/"):
-        raise HTTPException(status_code=400,
-                            detail={"code": "CRAWLING-002", "message": "잘못된 URL 형식입니다.", "is_success": False})
+    # 위의 Query 검증으로 미/오입력은 422 처리됨

39-45: 예외 재던지기/연쇄 개선

raise e 대신 raise 사용, 미지정 예외는 원인 체인과 함께 래핑하세요.

적용 예시(diff):

-    except HTTPException as e:
-        # 핸들링된 HTTP 예외는 그대로 다시 발생시킴
-        raise e
-    except Exception:
-        # 핸들링되지 않은 모든 예외는 500 에러로 처리
-        raise HTTPException(status_code=500,
-                            detail={"code": "INTERNAL_SERVER_ERROR", "message": "서버 내부 오류가 발생했습니다.", "is_success": False})
+    except HTTPException:
+        raise
+    except Exception as err:
+        raise HTTPException(
+            status_code=500,
+            detail={"code": "INTERNAL_SERVER_ERROR", "message": "서버 내부 오류가 발생했습니다.", "is_success": False},
+        ) from err
app/main.py (1)

44-59: 미들웨어에서 예외 시에도 처리시간 헤더 기록하려면 finally 사용

현재 call_next에서 예외가 나면 이후 코드가 실행되지 않습니다.

적용 예시(diff):

 @app.middleware("http")
 async def add_process_time_header(request: Request, call_next):
-    start_time = time.time()
-    response = await call_next(request)
-    process_time = time.time() - start_time
-    response.headers["X-Process-Time"] = str(process_time)
-    logger.info(f'"{request.method} {request.url.path}" {response.status_code} | Process Time: {process_time:.4f}s')
-    return response
+    start_time = time.time()
+    try:
+        response = await call_next(request)
+        return response
+    finally:
+        process_time = time.time() - start_time
+        try:
+            response.headers["X-Process-Time"] = str(process_time)  # 실패 응답에도 가능하면 부여
+        except Exception:
+            pass
+        logger.info(f'"{request.method} {request.url.path}" | Process Time: {process_time:.4f}s')
app/core/exception_handlers.py (1)

28-44: 검증 예외: 빈 에러 리스트/필드 경로 처리 및 ARG001 제거

exc.errors()가 빈 경우 IndexError 가능. 또한 request 미사용 경고(ruff ARG001).

적용 제안(diff):

-async def validation_exception_handler(request: Request, exc: RequestValidationError):
+async def validation_exception_handler(_request: Request, exc: RequestValidationError):
     """RequestValidationError를 공통 포맷으로 변환합니다."""
-
-    # 유효성 검사 에러에 대한 상세 내용을 메시지로 조합할 수 있습니다.
-    # 여기서는 간단하게 첫 번째 에러 메시지만 사용합니다.
-    first_error = exc.errors()[0]
-    error_message = f"Validation Error: '{first_error['loc'][-1]}' - {first_error['msg']}"
+    errors = exc.errors()
+    if errors:
+        first = errors[0]
+        loc = first.get("loc", [])
+        # 'body' 등 루트 토큰 제거하고 '.'로 연결
+        path = ".".join(str(p) for p in loc if p not in {"body"})
+        msg = first.get("msg", "Invalid input")
+        error_message = f"Validation Error: '{path or 'unknown'}' - {msg}"
+    else:
+        error_message = "Validation Error"
 
     return JSONResponse(
         status_code=422,
         content={
             "code": "VALIDATION_ERROR",
             "message": error_message,
             "is_success": False,
             "payload": None,
         },
     )
app/services/driver_pool.py (4)

12-22: Chrome 옵션 Lambda 친화성 개선 필요

--headless 구버전 플래그, /tmp 사용자 데이터 미사용, zygote 비활성화 누락 등으로 런타임 이슈 가능.

적용 제안(diff):

     def _create_driver(self):
         """새로운 WebDriver 인스턴스를 생성합니다."""
         chrome_options = Options()
-        chrome_options.add_argument("--disable-gpu")
+        chrome_options.add_argument("--disable-gpu")
         chrome_options.add_argument("--window-size=1920x1080")
         chrome_options.add_argument("--no-sandbox")
-        chrome_options.add_argument("--disable-dev-shm-usage")
-        chrome_options.add_argument("--headless")
+        chrome_options.add_argument("--disable-dev-shm-usage")
+        chrome_options.add_argument("--headless=new")
+        chrome_options.add_argument("--no-zygote")
+        chrome_options.add_argument("--single-process")
+        chrome_options.add_argument("--disable-software-rasterizer")
+        chrome_options.add_argument("--user-data-dir=/tmp/chrome-user-data")
+        chrome_options.add_argument("--data-path=/tmp/chrome-data")
+        chrome_options.add_argument("--disk-cache-dir=/tmp/chrome-cache")
         chrome_options.add_argument("user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
 
         return webdriver.Chrome(options=chrome_options)

최종 이미지의 Chrome/Chromedriver 버전에 따라 --headless=new 지원 여부 확인이 필요합니다.


24-27: 대여 시 무한 대기 방지 및 게으른 생성

현재 Queue.get()은 드라이버 고갈 시 영구 블록. 타임아웃/즉시 생성으로 전환 권장.

적용 제안(diff):

-    def get_driver(self):
-        """Pool에서 WebDriver를 가져옵니다."""
-        return self._pool.get()
+    def get_driver(self, timeout: float | None = 30):
+        """Pool에서 WebDriver를 가져오며, 없으면 새로 생성합니다."""
+        try:
+            return self._pool.get(timeout=timeout) if timeout else self._pool.get_nowait()
+        except queue.Empty:
+            return self._create_driver()

28-31: 반납 시 데드락 방지 및 과잉 드라이버 정리

풀 가득 찬 상태에서 put()은 블록합니다. 즉시 실패 시 quit() 권장.

적용 제안(diff):

-    def release_driver(self, driver):
-        """사용한 WebDriver를 Pool에 반납합니다."""
-        self._pool.put(driver)
+    def release_driver(self, driver):
+        """사용한 WebDriver를 Pool에 반납합니다. 가득 차면 종료."""
+        try:
+            self._pool.put_nowait(driver)
+        except queue.Full:
+            try:
+                driver.quit()
+            except Exception:
+                pass

32-36: 종료 루틴 강화

큐에 없는 ‘사용 중’ 드라이버는 종료되지 않습니다. 추적/강제 종료 보완을 고려하세요(예: 약한참조 집합으로 all_drivers 추적).

원하시면 최소 침습으로 all_drivers 추적과 close_all() 보강 패치를 제안 드리겠습니다.

.github/workflows/crawler-cicd.yaml (2)

32-41: 아키텍처/플랫폼 불일치 가능성 및 안정성 개선

  • --platform linux/amd64 고정은 Lambda 함수가 arm64일 경우 실패합니다.
  • 스크립트에 set -euo pipefail 미설정.

적용 제안(diff):

-        run: |
-          docker build --platform linux/amd64 -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
+        run: |
+          set -euo pipefail
+          ARCH="${{ secrets.LAMBDA_ARCHITECTURE || 'x86_64' }}"
+          PLATFORM=$([ "$ARCH" = "arm64" ] && echo "linux/arm64" || echo "linux/amd64")
+          docker build --platform "$PLATFORM" -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
           docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
           echo "image_uri=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

함수의 실제 아키텍처 확인이 필요합니다(콘솔/CLI로 확인 부탁).


11-14: 동시 실행 배포 충돌 방지

동일 브랜치에서 중복 실행 시 레이스가 발생할 수 있습니다. concurrency 추가 권장.

적용 제안(diff):

 jobs:
+  concurrency:
+    group: deploy-develop
+    cancel-in-progress: true
   deploy:
app/services/crawler.py (4)

66-75: 매직 넘버 상수화 및 경계값 검증

450, 25, 30, 시작 시각 9 등이 하드코딩되어 유지보수/이식성이 낮습니다. 음수(이른 top) 처리도 필요.

적용 제안(diff):

-            start_total_minutes = ((top - 450) // 25) * 30
-            start_hour = 9 + (start_total_minutes // 60)
+            PIXELS_PER_30M = 25
+            START_OFFSET_PX = 450
+            START_HOUR_BASE = 9
+            start_total_minutes = max(0, ((top - START_OFFSET_PX) // PIXELS_PER_30M) * 30)
+            start_hour = START_HOUR_BASE + (start_total_minutes // 60)
@@
-            duration_total_minutes = math.ceil((height - 1) / 25) * 30
+            duration_total_minutes = math.ceil((height - 1) / PIXELS_PER_30M) * 30
-            end_hour = 9 + (end_total_minutes // 60)
+            end_hour = START_HOUR_BASE + (end_total_minutes // 60)

122-127: 원래 traceback 유지: raise e 대신 맨몸 raise

raise e는 traceback을 재설정합니다. 린트(TRY201) 지적과 동일.

적용 제안(diff):

-        raise e
+        raise

129-132: 드라이버 반납 로깅 안전성 및 정리 보강

driver.session_id 접근이 실패할 수 있습니다(세션 종료/크래시). 반납 전 가벼운 헬스체크/쿠키 정리도 권장.

적용 제안(diff):

-        logger.info(f"Driver 반납: {driver.session_id}")
-        pool.release_driver(driver)
+        sid = getattr(driver, "session_id", "unknown")
+        logger.info(f"Driver 반납: {sid}")
+        try:
+            driver.delete_all_cookies()
+        except Exception:
+            pass
+        pool.release_driver(driver)

1-20: 불필요한 import 정리 및 페이지 로드 타임아웃 고려

webdriver, Options, Service는 이 파일에서 미사용입니다. 또한 driver.get()의 무한 대기를 방지하려면 생성 시 page_load_timeout을 설정하는 편이 안전합니다(풀 생성부에서 처리 추천).

원하시면 driver_pool._create_driver()driver.set_page_load_timeout(15) 또는 page_load_strategy="none" 적용 패치를 함께 제안하겠습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c05e7c and 2976b30.

📒 Files selected for processing (16)
  • .github/PULL_REQUEST_TEMPLATE.md (1 hunks)
  • .github/workflows/crawler-cicd.yaml (1 hunks)
  • .gitignore (1 hunks)
  • Dockerfile (1 hunks)
  • README.md (1 hunks)
  • app.py (0 hunks)
  • app/api/v1/schedule.py (1 hunks)
  • app/core/exception_handlers.py (1 hunks)
  • app/core/logging_config.py (1 hunks)
  • app/core/security.py (1 hunks)
  • app/main.py (1 hunks)
  • app/services/crawler.py (3 hunks)
  • app/services/driver_pool.py (1 hunks)
  • app/utils/discord_logger.py (1 hunks)
  • requirements.txt (1 hunks)
  • utils/discord_logger.py (0 hunks)
💤 Files with no reviewable changes (2)
  • utils/discord_logger.py
  • app.py
🧰 Additional context used
🧬 Code graph analysis (3)
app/api/v1/schedule.py (2)
app/services/crawler.py (1)
  • crawl_schedule (21-131)
app/core/security.py (1)
  • get_api_key (11-34)
app/services/crawler.py (2)
app/services/driver_pool.py (3)
  • WebDriverPool (5-36)
  • get_driver (24-26)
  • release_driver (28-30)
app/utils/discord_logger.py (1)
  • send_error_to_discord (15-63)
app/main.py (3)
app/core/exception_handlers.py (2)
  • http_exception_handler (7-25)
  • validation_exception_handler (28-44)
app/core/logging_config.py (1)
  • setup_logging (26-28)
app/services/driver_pool.py (2)
  • WebDriverPool (5-36)
  • close_all (32-36)
🪛 Ruff (0.13.1)
app/core/exception_handlers.py

7-7: Unused function argument: request

(ARG001)


28-28: Unused function argument: request

(ARG001)

app/api/v1/schedule.py

35-35: Abstract raise to an inner function

(TRY301)


37-37: Consider moving this statement to an else block

(TRY300)


41-41: Use raise without specifying exception name

Remove exception name

(TRY201)


42-42: Do not catch blind exception: Exception

(BLE001)


44-45: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

app/utils/discord_logger.py

58-58: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


60-60: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

app/services/crawler.py

126-126: Use raise without specifying exception name

Remove exception name

(TRY201)

🔇 Additional comments (2)
.gitignore (1)

7-7: .gitignore JetBrains IDE 무시 패턴 수정 (*.idea.idea/, *.iml 추가`)

  • *.idea를 폴더 무시용 .idea/로 변경
  • 모듈 파일 무시용 *.iml 추가
    현재 커밋된 .idea 또는 .iml 파일이 없으므로 추가 제거 작업은 불필요합니다.
app/main.py (1)

83-83: Mangum lifespan 처리 확인 필요 (app/main.py:83)

requirements.txt에 mangum가 있으나 버전이 고정되어 있지 않아(로컬/sandbox엔 설치돼 있지 않음) 실제 설치된 버전을 확인할 수 없습니다.

최신 Mangum은 문자열 파라미터 lifespan("auto" | "on" | "off")를 사용하며 기본값은 "auto"입니다. (mangum.fastapiexpert.com)
과거 버전은 enable_lifespan(bool)을 사용했고 해당 옵션은 제거(0.11.0)되어 버전 차이로 에러가 발생할 수 있습니다. (newreleases.io)

권장 조치: requirements.txt에 Mangum 버전을 명시(pin)하거나, 최신 API 사용 시 명시적으로 handler = Mangum(app, lifespan="auto")로 변경하십시오. 레거시 호환성이 필요하면 버전 고정 또는 try/exceptlifespanenable_lifespan 폴백을 구현하세요.

Comment on lines +1 to 6
name: Deploy Onetime Crawler to AWS Lambda

on:
pull_request:
branches: [ "develop" ]

Copy link

@coderabbitai coderabbitai bot Sep 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

PR 이벤트에서 실배포 수행은 위험합니다

pull_request 트리거로 Lambda를 업데이트하면 미승인 변경이 배포될 수 있고, 포크 PR은 시크릿 접근도 불가하여 실패합니다.

적용 제안(diff):

-on:
-  pull_request:
-    branches: [ "develop" ]
+on:
+  push:
+    branches: [ "develop" ]
+  workflow_dispatch:

또는 if: github.event_name == 'push'로 job 레벨 가드 추가.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
name: Deploy Onetime Crawler to AWS Lambda
on:
pull_request:
branches: [ "develop" ]
name: Deploy Onetime Crawler to AWS Lambda
on:
push:
branches: [ "develop" ]
workflow_dispatch:
🤖 Prompt for AI Agents
.github/workflows/crawler-cicd.yaml lines 1-6: the workflow currently triggers
on pull_request which can unintentionally deploy unapproved changes and fails
for forked PRs due to secret access; change the workflow trigger to a safer
event (e.g., use on: push with branches: [ "develop" ] or keep pull_request for
checks only) and/or add a job-level guard so deploy jobs run only on pushes (add
if: github.event_name == 'push' or a condition checking github.ref ==
'refs/heads/develop'), ensuring Lambda-update jobs never run on pull_request
events from forks.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

궁금한 게 그러면 무조건 머지를 해야지만 배포 후 테스트를 할 수 있는 거 아니야?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bbbang105 좋은 지적이에요! 머지 전에 테스트할 수 있는 몇 가지 방법이 있습니다:

권장 방법들:

1. Staging 환경 분리

on:
  pull_request:
    branches: [ "develop" ]
  push:
    branches: [ "develop" ]

jobs:
  deploy-staging:
    if: github.event_name == 'pull_request'
    # staging Lambda 함수로 배포
    
  deploy-production:
    if: github.event_name == 'push'
    # production Lambda 함수로 배포

2. 수동 배포 트리거

on:
  pull_request:
    branches: [ "develop" ]
  workflow_dispatch:  # 수동 실행 버튼
    inputs:
      environment:
        description: 'Deploy environment'
        required: true
        default: 'staging'
        type: choice
        options:
        - staging
        - production

3. Preview 배포

  • PR마다 임시 Lambda 함수 생성 후 테스트 완료시 삭제
  • AWS Lambda Alias/Version 기능 활용

4. 로컬 테스트 강화

# Docker로 Lambda 환경 시뮬레이션
docker run --platform linux/amd64 -p 9000:8080 your-image
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'

가장 실용적인 해결책은 staging 환경을 만드는 것입니다. PR에서는 staging으로, merge 후에는 production으로 배포하면 안전하게 테스트할 수 있어요.

🤔 어떤 방식이 가장 적합할 것 같으신가요?

Comment on lines 7 to 9
permissions:
id-token: write
contents: read
Copy link

@coderabbitai coderabbitai bot Sep 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

OIDC 권한 선언과 정적 키 혼용

permissions.id-token: write를 선언했지만 정적 액세스 키를 사용 중입니다. OIDC 역할 연동으로 전환 권장.

적용 제안(diff):

-      - name: Configure AWS Credentials
-        uses: aws-actions/configure-aws-credentials@v4
-        with:
-          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
-          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-          aws-region: ${{ secrets.AWS_REGION }}
+      - name: Configure AWS Credentials (OIDC)
+        uses: aws-actions/configure-aws-credentials@v4
+        with:
+          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
+          aws-region: ${{ secrets.AWS_REGION }}

AWS IAM에 GitHub OIDC 신뢰 정책/role 구성 필요.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
.github/workflows/crawler-cicd.yaml lines 7-9: 현재 워크플로우에 permissions.id-token:
write를 선언해 놓고도 정적 AWS 액세스 키(Secrets)를 병행 사용하고 있어 혼용 상태입니다; AWS OIDC로 전환하려면 정적 키를
제거하고 GitHub OIDC 신뢰 정책 및 역할(Role)을 AWS IAM에 설정한 뒤 워크플로우에서 OIDC를 통해 임시 자격증명을
획득하도록 수정하세요; 구체적으로는(1) AWS에 GitHub Actions용 신뢰 정책과 역할을 생성하고 필요한 최소 권한을 부여하고, (2)
워크플로우에서 aws-actions/configure-aws-credentials 같은 액션을 사용해 role-to-assume 설정으로
OIDC를 통한 인증을 하며, (3) 기존 정적 AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY 시크릿을 제거하거나 더
이상 사용하지 않도록 업데이트하세요.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정적으로 갈거라서 이 부분은 제거할게

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bbbang105 알겠습니다! 정적 자격증명 방식으로 가시는군요. permissions 섹션을 제거하시면 일관성 있게 구성되겠습니다.

🐰✨
(\ /)
( ..)
o
(")(")
thoughtful


✏️ Learnings added
Learnt from: bbbang105
PR: onetime-with-members/crawling-server#16
File: .github/workflows/crawler-cicd.yaml:7-9
Timestamp: 2025-09-21T20:00:59.927Z
Learning: User bbbang105 prefers using static AWS credentials over OIDC for GitHub Actions workflows in the crawling-server project.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +29 to +33
pool = request.app.state.driver_pool

# 서비스 계층의 크롤링 함수 호출
result = await asyncio.to_thread(crawl_schedule, url, request, pool)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

driver_pool 존재 보장 필요(수명주기 미호출/초기화 실패 대비)

Lambda/Mangum 환경에서 lifespan 미호출 시 app.state.driver_pool 부재 가능. 안전가드 추가하세요.

적용 예시(diff):

-        pool = request.app.state.driver_pool
+        pool = getattr(request.app.state, "driver_pool", None)
+        if pool is None:
+            # 지연 초기화(최소 1개). 대안: 503 반환.
+            from app.services.driver_pool import WebDriverPool
+            pool = request.app.state.driver_pool = WebDriverPool(size=1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pool = request.app.state.driver_pool
# 서비스 계층의 크롤링 함수 호출
result = await asyncio.to_thread(crawl_schedule, url, request, pool)
pool = getattr(request.app.state, "driver_pool", None)
if pool is None:
# 지연 초기화(최소 1개). 대안: 503 반환.
from app.services.driver_pool import WebDriverPool
pool = request.app.state.driver_pool = WebDriverPool(size=1)
# 서비스 계층의 크롤링 함수 호출
result = await asyncio.to_thread(crawl_schedule, url, request, pool)
🤖 Prompt for AI Agents
In app/api/v1/schedule.py around lines 29 to 33, there is no guarantee that
request.app.state.driver_pool exists (lifespan may not have run in
Lambda/Mangum), so add a safety check before using it: verify driver_pool is
present and valid, and if missing either return/raise an HTTP 503/500 with a
clear message or lazily initialize a new pool (ensuring async-safety) depending
on your app policy; log the missing pool for diagnostics and avoid calling
crawl_schedule with a None pool.

Comment on lines +7 to +25
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
"""HTTPException을 공통 포맷으로 변환합니다."""

# 기본 HTTPException의 detail이 딕셔너리 형태일 경우, 그 내용을 사용합니다.
if isinstance(exc.detail, dict):
content = {**exc.detail, "payload": None}
else:
# 아닐 경우, 표준 에러 포맷을 생성합니다.
content = {
"code": f"HTTP_{exc.status_code}",
"message": exc.detail,
"is_success": False,
"payload": None,
}

return JSONResponse(
status_code=exc.status_code,
content=content,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

HTTP 예외 응답 포맷 불일치 및 인증 헤더 손실 위험

  • exc.detail이 dict일 때 is_success/code가 누락될 수 있어 응답 스키마가 흔들립니다.
  • WWW-Authenticateexc.headers가 빠져 401/429 등에서 클라이언트 호환성 문제가 납니다.

적용 제안(diff):

-async def http_exception_handler(request: Request, exc: StarletteHTTPException):
+async def http_exception_handler(_request: Request, exc: StarletteHTTPException):
     """HTTPException을 공통 포맷으로 변환합니다."""
-
-    # 기본 HTTPException의 detail이 딕셔너리 형태일 경우, 그 내용을 사용합니다.
-    if isinstance(exc.detail, dict):
-        content = {**exc.detail, "payload": None}
-    else:
-        # 아닐 경우, 표준 에러 포맷을 생성합니다.
-        content = {
-            "code": f"HTTP_{exc.status_code}",
-            "message": exc.detail,
-            "is_success": False,
-            "payload": None,
-        }
+    # 표준 에러 포맷을 기본으로, dict detail이면 overlay
+    detail = exc.detail
+    base = {
+        "code": f"HTTP_{exc.status_code}",
+        "message": str(detail) if not isinstance(detail, dict) else str(detail.get("message", "")) or f"HTTP_{exc.status_code}",
+        "is_success": False,
+        "payload": None,
+    }
+    content = {**base, **detail} if isinstance(detail, dict) else base
-    return JSONResponse(
-        status_code=exc.status_code,
-        content=content,
-    )
+    return JSONResponse(
+        status_code=exc.status_code,
+        content=content,
+        headers=getattr(exc, "headers", None) or {}
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
"""HTTPException을 공통 포맷으로 변환합니다."""
# 기본 HTTPException의 detail이 딕셔너리 형태일 경우, 그 내용을 사용합니다.
if isinstance(exc.detail, dict):
content = {**exc.detail, "payload": None}
else:
# 아닐 경우, 표준 에러 포맷을 생성합니다.
content = {
"code": f"HTTP_{exc.status_code}",
"message": exc.detail,
"is_success": False,
"payload": None,
}
return JSONResponse(
status_code=exc.status_code,
content=content,
)
async def http_exception_handler(_request: Request, exc: StarletteHTTPException):
"""HTTPException을 공통 포맷으로 변환합니다."""
# 표준 에러 포맷을 기본으로, dict detail이면 overlay
detail = exc.detail
base = {
"code": f"HTTP_{exc.status_code}",
"message": str(detail) if not isinstance(detail, dict) else str(detail.get("message", "")) or f"HTTP_{exc.status_code}",
"is_success": False,
"payload": None,
}
content = {**base, **detail} if isinstance(detail, dict) else base
return JSONResponse(
status_code=exc.status_code,
content=content,
headers=getattr(exc, "headers", None) or {},
)
🧰 Tools
🪛 Ruff (0.13.1)

7-7: Unused function argument: request

(ARG001)

🤖 Prompt for AI Agents
In app/core/exception_handlers.py around lines 7 to 25, the handler currently
returns inconsistent response shapes when exc.detail is a dict and drops
exception headers (e.g., WWW-Authenticate), so change the logic to normalize and
merge defaults into the detail dict (ensure keys: "code" defaulting to
f"HTTP_{exc.status_code}", "message" using exc.detail.get("message") or
str(exc.detail), "is_success": False, and "payload": None) without deleting
existing fields, and pass exc.headers (if any) into the JSONResponse via the
headers parameter so authentication/rate-limit headers are preserved.

Comment on lines +6 to +12
API_KEY = os.getenv("API_KEY")
API_KEY_NAME = "X-API-Key"

api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)

async def get_api_key(api_key_header: str = Security(api_key_header)):
"""헤더에서 API 키를 추출하고 유효성을 검사하는 의존성 함수"""
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

API_KEY 미설정 시 TypeError 가능성 → 500으로 튀는 버그

secrets.compare_digest(api_key_header, API_KEY)에서 API_KEY가 None이면 TypeError가 발생합니다. 미설정 시 명시적으로 500/설정오류를 반환하거나 안전 실패하도록 처리하세요.

적용 예시(diff):

-import os
+import os
+import logging
@@
 API_KEY = os.getenv("API_KEY")
@@
 async def get_api_key(api_key_header: str = Security(api_key_header)):
@@
-    if secrets.compare_digest(api_key_header, API_KEY):
+    if API_KEY is None:
+        logging.getLogger(__name__).critical("API_KEY 환경변수가 설정되어 있지 않습니다.")
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail={"code": "AUTH-000", "message": "서버 보안 설정 오류(API_KEY 미설정).", "is_success": False},
+        )
+
+    if secrets.compare_digest(api_key_header, API_KEY):
         return api_key_header
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
API_KEY = os.getenv("API_KEY")
API_KEY_NAME = "X-API-Key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
async def get_api_key(api_key_header: str = Security(api_key_header)):
"""헤더에서 API 키를 추출하고 유효성을 검사하는 의존성 함수"""
import os
import logging
API_KEY = os.getenv("API_KEY")
API_KEY_NAME = "X-API-Key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
async def get_api_key(api_key_header: str = Security(api_key_header)):
"""헤더에서 API 키를 추출하고 유효성을 검사하는 의존성 함수"""
if API_KEY is None:
logging.getLogger(__name__).critical("API_KEY 환경변수가 설정되어 있지 않습니다.")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={"code": "AUTH-000", "message": "서버 보안 설정 오류(API_KEY 미설정).", "is_success": False},
)
if secrets.compare_digest(api_key_header, API_KEY):
return api_key_header
🤖 Prompt for AI Agents
In app/core/security.py around lines 6 to 12, the dependency uses
secrets.compare_digest(api_key_header, API_KEY) but API_KEY can be None which
causes a TypeError; update get_api_key to first check if API_KEY is None and, if
so, raise an HTTPException with a 500 status (e.g., "API key not configured") or
otherwise fail-safe by rejecting the request; only call secrets.compare_digest
when API_KEY is a non-None string (and ensure api_key_header is a string) and
return/raise a 401 Unauthorized when the comparison fails.

Comment on lines 23 to 31
@asynccontextmanager
async def lifespan(app: FastAPI):
# 앱 시작 시 실행
logger.info("🚀 WebDriver Pool을 생성합니다...")
app.state.driver_pool = WebDriverPool(size=5)
yield
# 앱 종료 시 실행
logger.info("💀 WebDriver Pool을 종료합니다...")
app.state.driver_pool.close_all()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Lambda에서 WebDriverPool 사전 생성은 콜드스타트/실행 실패 유발(브라우저 미존재 문제와 결합)

  • Lambda 1-동시성 인스턴스 특성상 풀(5개) 사전 생성은 비효율/메모리 증가.
  • 현재 이미지엔 Chromium이 없어 초기화 단계에서 즉시 실패 가능.
    풀 크기 축소 및 지연 초기화/환경기반 토글이 필요합니다.

적용 예시(diff):

+import os
@@
 async def lifespan(app: FastAPI):
-    logger.info("🚀 WebDriver Pool을 생성합니다...")
-    app.state.driver_pool = WebDriverPool(size=5)
+    pool_size = int(os.getenv("POOL_SIZE", "0"))  # 기본 지연초기화
+    if pool_size > 0:
+        logger.info(f"🚀 WebDriver Pool 생성(size={pool_size})")
+        app.state.driver_pool = WebDriverPool(size=pool_size)
+    else:
+        logger.info("🚀 WebDriver Pool 지연초기화 모드")
+        app.state.driver_pool = None
     yield
     # 앱 종료 시 실행
-    logger.info("💀 WebDriver Pool을 종료합니다...")
-    app.state.driver_pool.close_all()
+    if getattr(app.state, "driver_pool", None):
+        logger.info("💀 WebDriver Pool을 종료합니다...")
+        app.state.driver_pool.close_all()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@asynccontextmanager
async def lifespan(app: FastAPI):
# 앱 시작 시 실행
logger.info("🚀 WebDriver Pool을 생성합니다...")
app.state.driver_pool = WebDriverPool(size=5)
yield
# 앱 종료 시 실행
logger.info("💀 WebDriver Pool을 종료합니다...")
app.state.driver_pool.close_all()
import os
@asynccontextmanager
async def lifespan(app: FastAPI):
# 앱 시작 시 실행
pool_size = int(os.getenv("POOL_SIZE", "0")) # 기본 지연초기화
if pool_size > 0:
logger.info(f"🚀 WebDriver Pool 생성(size={pool_size})")
app.state.driver_pool = WebDriverPool(size=pool_size)
else:
logger.info("🚀 WebDriver Pool 지연초기화 모드")
app.state.driver_pool = None
yield
# 앱 종료 시 실행
if getattr(app.state, "driver_pool", None):
logger.info("💀 WebDriver Pool을 종료합니다...")
app.state.driver_pool.close_all()
🤖 Prompt for AI Agents
In app/main.py around lines 23 to 31, the lifespan currently eagerly constructs
a WebDriverPool of size 5 at app startup which can cause cold-start failures and
increased memory usage in AWS Lambda (and will fail if Chromium is not present);
change this to create a much smaller default pool (e.g., 1) or defer creation
entirely by storing a lazy factory on app.state and instantiating the pool on
first use, add an environment-based toggle (e.g., DISABLE_DRIVER_POOL or
DRIVER_POOL_LAZY=true) to skip pre-creation in Lambda, and ensure the shutdown
path still closes the pool if it exists and handles absence of a browser by
catching initialization errors and logging them without crashing the app.

Comment on lines 6 to 11
def __init__(self, size=5):
self._pool = queue.Queue(maxsize=size)
for _ in range(size):
driver = self._create_driver()
self._pool.put(driver)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Lambda에서의 초기 드라이버 일괄 생성은 메모리/콜드스타트 리스크가 큽니다

Lambda 컨테이너에서 Chrome 5개 선생성은 OOM/콜드스타트 지연을 유발할 수 있습니다. 실패 시 앱 초기화 전체가 터집니다.

게으른 생성+비동기 대여로 전환 권장(diff):

 class WebDriverPool:
-    def __init__(self, size=5):
-        self._pool = queue.Queue(maxsize=size)
-        for _ in range(size):
-            driver = self._create_driver()
-            self._pool.put(driver)
+    def __init__(self, size=2):
+        # Lambda 기본 메모리(예: 512~1024MB) 고려해 기본값 축소
+        self._pool = queue.Queue(maxsize=size)

이후 get_driver/release_driver를 lazy/논블로킹으로 조정(아래 코멘트 참고).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def __init__(self, size=5):
self._pool = queue.Queue(maxsize=size)
for _ in range(size):
driver = self._create_driver()
self._pool.put(driver)
def __init__(self, size=2):
# Lambda 기본 메모리(예: 512~1024MB) 고려해 기본값 축소
self._pool = queue.Queue(maxsize=size)

Comment on lines +5 to +8
fastapi
uvicorn[standard]
python-multipart
mangum
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

프로덕션 재현성과 보안 위해 버전 고정 및 런타임/개발 분리 필요

현재 전부 미핀(최신 부수 변경 위험). Lambda 컨테이너는 재현성 중요합니다. 운영 필수 deps만 고정하고, 로컬 전용(uvicorn 등)은 dev로 분리해 주세요.

적용 예시(diff):

-selenium
-psutil
-python-dotenv
-requests
-fastapi
-uvicorn[standard]
-python-multipart
-mangum
+selenium>=4.21,<5.0
+psutil>=5.9,<6.0
+python-dotenv>=1.0,<2.0
+requests>=2.32,<3.0
+fastapi>=0.110,<1.0
+python-multipart>=0.0.9,<1.0
+mangum>=0.17,<1.0
+# 로컬 개발 전용은 별도 requirements-dev.txt로 분리:
+# uvicorn[standard]>=0.30,<1.0

또는 constraints/lock 파일(pip-tools) 사용을 권장합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fastapi
uvicorn[standard]
python-multipart
mangum
fastapi>=0.110,<1.0
# 로컬 개발 전용은 별도 requirements-dev.txt로 분리:
# uvicorn[standard]>=0.30,<1.0
python-multipart>=0.0.9,<1.0
mangum>=0.17,<1.0
🤖 Prompt for AI Agents
requirements.txt lines 5-8: currently all deps are unpinned and include runtime
and local-only packages; pin production runtime packages and split dev/runtime
deps. Update by locking exact versions for production-required packages (e.g.,
fastapi==X.Y.Z, mangum==A.B.C, python-multipart==M.N.O if used at runtime) in
requirements.txt, move local/dev-only packages such as uvicorn[standard] into a
requirements-dev.txt (also pinned), and optionally adopt a constraints or
lockfile workflow (pip-tools/requirements.txt + requirements-dev.txt generated
from requirements.in) to ensure reproducible Lambda containers.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
app/services/driver_pool.py (4)

39-43: close_all는 race에 취약하고 블로킹 가능 → get_nowait 루프와 예외 방어

empty() 체크 후 get()은 경합 시 블로킹될 수 있습니다. 논블로킹으로 소거하고 종료 예외는 무시하세요.

 def close_all(self):
     """애플리케이션 종료 시 모든 WebDriver를 종료합니다."""
-    while not self._pool.empty():
-        driver = self._pool.get()
-        driver.quit()
+    while True:
+        try:
+            driver = self._pool.get_nowait()
+        except queue.Empty:
+            break
+        try:
+            driver.quit()
+        except Exception:
+            pass

1-1: 락/ENV 사용을 위한 import 보강

위 제안(diff) 적용 시 필요합니다.

-import queue
+import os
+import queue
+import threading

5-11: 운영 가이드: 풀 메트릭/로깅/백오프

  • 생성/종료/반납/풀고갈 이벤트에 카운터·게이지(예: Prometheus)와 구조화 로그 추가.
  • _create_driver 실패 시 지수 백오프+재시도(최대 N회) 및 Discord/Slack 경보 연동.

31-37: 사용성 개선: 컨텍스트 매니저 제공

with pool.borrow() as driver: 형태로 누수·이중반납 방지에 도움됩니다.

아래 보조 API를 동일 파일 하단에 추가 제안(참고용):

from contextlib import contextmanager

@contextmanager
def borrow(self, timeout: float = 10.0):
    driver = self.get_driver(timeout=timeout)
    try:
        yield driver
    finally:
        self.release_driver(driver)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2976b30 and 9f1c960.

📒 Files selected for processing (1)
  • app/services/driver_pool.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.13.1)
app/services/driver_pool.py

23-23: f-string without any placeholders

Remove extraneous f prefix

(F541)


24-24: f-string without any placeholders

Remove extraneous f prefix

(F541)


25-25: f-string without any placeholders

Remove extraneous f prefix

(F541)

🔇 Additional comments (2)
app/services/driver_pool.py (2)

6-10: Lambda에서 드라이버 ‘선생성’은 콜드스타트·메모리 리스크 큼 → 게으른 생성으로 전환 권장

컨테이너 기동 시 Chrome를 size개 동시 생성하면 콜드스타트 지연·OOM·초기화 전체 실패 전파 위험이 큽니다. 요청 시 부족분만 생성하고, 최대 개수만 추적하세요.

아래처럼 초기 선생성을 제거하고, 최대치/생성수/락을 두세요:

-def __init__(self, size=5):
-    self._pool = queue.Queue(maxsize=size)
-    for _ in range(size):
-        driver = self._create_driver()
-        self._pool.put(driver)
+def __init__(self, size: int = int(os.getenv("WEBDRIVER_POOL_SIZE", "2"))):
+    self._pool = queue.Queue(maxsize=size)
+    self._size = size
+    self._created = 0
+    self._lock = threading.Lock()

(상단 import 추가는 별도 코멘트 참조)


12-29: 드라이버 배포 방식 확인 필요 — 현재 Selenium Manager 의존 가능성, 번들링 권장

  • 검증 결과: Dockerfile에서 Chromium/ChromeDriver 번들 관련 참조 없음.
  • 코드: app/services/driver_pool.py (약 라인 28–29)에서 _create_driver가 webdriver.Chrome(options=...)만 호출하여 실행 바이너리/드라이버 경로(ENV)를 사용하지 않음 → 런타임에 Selenium Manager가 드라이버를 다운로드할 가능성 높음.
  • 반납 보장: app/services/crawler.py (라인 128–131)에서 finally로 드라이버 반납이 안전하게 처리되고 있음.
  • 권장 조치: Lambda 이미지에 Chromium + ChromeDriver를 고정 번들링하고 CHROME_BINARY_PATH / CHROMEDRIVER_PATH 환경변수로 경로를 주입하세요. 드라이버 생성 시 options.binary_location 또는 selenium.webdriver.chrome.service.Service(executable_path=...)로 바이너리/드라이버 경로를 명시해 Selenium Manager 다운로드(콜드스타트)를 회피하세요. 변경 후 Dockerfile·ENV·driver_pool 코드 재검증 필요.

Comment on lines 12 to 29
def _create_driver(self):
"""새로운 WebDriver 인스턴스를 생성합니다."""
chrome_options = Options()
chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--window-size=1280x1696")
chrome_options.add_argument("--single-process")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--disable-dev-tools")
chrome_options.add_argument("--no-zygote")
chrome_options.add_argument(f"--user-data-dir=/tmp/user-data")
chrome_options.add_argument(f"--data-path=/tmp/data-path")
chrome_options.add_argument(f"--disk-cache-dir=/tmp/cache-dir")
chrome_options.add_argument("--remote-debugging-port=9222")
chrome_options.add_argument("user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")

return webdriver.Chrome(options=chrome_options)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Chrome 플래그/바이너리 설정 개선: --single-process 제거, 환경변수 기반 경로/UA, F541 정리

  • --single-process는 크래시·불안정 원인으로 알려져 제거 권장.
  • 바이너리/드라이버 경로는 Lambda 이미지/레이어에 포함 후 ENV로 주입 권장.
  • 하드코딩 UA는 유지보수 부담. ENV로 옵셔널 처리.
  • f-string 남용(F541)도 함께 정리.
-    def _create_driver(self):
-        """새로운 WebDriver 인스턴스를 생성합니다."""
-        chrome_options = Options()
-        chrome_options.add_argument("--headless=new")
-        chrome_options.add_argument("--no-sandbox")
-        chrome_options.add_argument("--disable-gpu")
-        chrome_options.add_argument("--window-size=1280x1696")
-        chrome_options.add_argument("--single-process")
-        chrome_options.add_argument("--disable-dev-shm-usage")
-        chrome_options.add_argument("--disable-dev-tools")
-        chrome_options.add_argument("--no-zygote")
-        chrome_options.add_argument(f"--user-data-dir=/tmp/user-data")
-        chrome_options.add_argument(f"--data-path=/tmp/data-path")
-        chrome_options.add_argument(f"--disk-cache-dir=/tmp/cache-dir")
-        chrome_options.add_argument("--remote-debugging-port=9222")
-        chrome_options.add_argument("user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
-
-        return webdriver.Chrome(options=chrome_options)
+    def _create_driver(self):
+        """새로운 WebDriver 인스턴스를 생성합니다."""
+        chrome_options = Options()
+        chrome_options.add_argument("--headless=new")
+        chrome_options.add_argument("--no-sandbox")
+        chrome_options.add_argument("--disable-setuid-sandbox")
+        chrome_options.add_argument("--disable-gpu")
+        chrome_options.add_argument("--disable-extensions")
+        chrome_options.add_argument("--disable-dev-shm-usage")
+        chrome_options.add_argument("--no-zygote")
+        chrome_options.add_argument("--window-size=1280x1696")
+        chrome_options.add_argument("--remote-debugging-port=9222")
+        chrome_options.add_argument("--user-data-dir=/tmp/user-data")
+        chrome_options.add_argument("--data-path=/tmp/data-path")
+        chrome_options.add_argument("--disk-cache-dir=/tmp/cache-dir")
+        ua = os.getenv("CHROME_USER_AGENT")
+        if ua:
+            chrome_options.add_argument(f"user-agent={ua}")
+
+        binary = os.getenv("CHROME_BINARY_PATH")
+        if binary:
+            chrome_options.binary_location = binary
+        driver_path = os.getenv("CHROMEDRIVER_PATH")
+        if driver_path:
+            from selenium.webdriver.chrome.service import Service
+            service = Service(executable_path=driver_path)
+            return webdriver.Chrome(options=chrome_options, service=service)
+        return webdriver.Chrome(options=chrome_options)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.13.1)

23-23: f-string without any placeholders

Remove extraneous f prefix

(F541)


24-24: f-string without any placeholders

Remove extraneous f prefix

(F541)


25-25: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
In app/services/driver_pool.py around lines 12 to 29, remove the unstable
"--single-process" flag, stop hardcoding binary/driver paths and
user-data/data/cache dirs and user-agent; instead read Chrome binary path,
chromedriver path, user-data-dir, data-path, disk-cache-dir and optional
USER_AGENT from environment variables and pass them to Options/binary_location
and add_argument only when set, and replace unnecessary f-strings used with
constant strings to plain literals to satisfy F541; ensure any missing env vars
fall back to sensible defaults or are omitted.

Comment on lines 31 to 34
def get_driver(self):
"""Pool에서 WebDriver를 가져옵니다."""
return self._pool.get()

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

풀 고갈 시 무기한 대기 위험 → timeout 부여 + 부족분 즉시 생성

현재 get()은 끝없이 블로킹합니다. 요청 스레드가 고립되지 않도록 timeout과 lazy-create를 적용하세요.

-def get_driver(self):
-    """Pool에서 WebDriver를 가져옵니다."""
-    return self._pool.get()
+def get_driver(self, timeout: float = 10.0):
+    """Pool에서 WebDriver를 가져오되, 비어있으면 부족분을 생성하고 마지막엔 timeout으로 대기합니다."""
+    try:
+        return self._pool.get_nowait()
+    except queue.Empty:
+        pass
+    with self._lock:
+        if self._created < self._size:
+            driver = self._create_driver()
+            self._created += 1
+            return driver
+    return self._pool.get(timeout=timeout)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/services/driver_pool.py around lines 31-34, get_driver currently blocks
forever on self._pool.get(); change it to call
self._pool.get(timeout=SHORT_TIMEOUT) and catch queue.Empty, then under a lock
check if current total drivers < max_allowed and if so lazily create a new
driver (e.g. self._create_driver()), increment the internal counter and return
it; if at max, raise a clear TimeoutError or custom exception to the caller.
Ensure the creation path is thread-safe (use the same lock used for counters)
and choose a sensible SHORT_TIMEOUT value (seconds) configurable from the pool
settings.

Comment on lines +35 to +37
def release_driver(self, driver):
"""사용한 WebDriver를 Pool에 반납합니다."""
self._pool.put(driver)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

release 시 큐가 가득 차면 영구 블로킹 가능 → nowait/timeout + 초과분/불량 드라이버 폐기

이중 반납 등 실수 시 put()이 무기한 대기할 수 있습니다. 초과분은 종료하고, 카운터를 줄여 다음 요청에서 재생성되게 하세요.

-def release_driver(self, driver):
-    """사용한 WebDriver를 Pool에 반납합니다."""
-    self._pool.put(driver)
+def release_driver(self, driver):
+    """사용한 WebDriver를 Pool에 반납합니다. 가득 차면 드라이버를 종료해 데드락/누수를 방지합니다."""
+    # 간단한 생존 체크(세션이 없으면 버림)
+    if getattr(driver, "session_id", None) is None:
+        try:
+            driver.quit()
+        finally:
+            with self._lock:
+                self._created = max(0, self._created - 1)
+        return
+    try:
+        self._pool.put(driver, timeout=1)
+    except queue.Full:
+        try:
+            driver.quit()
+        finally:
+            with self._lock:
+                self._created = max(0, self._created - 1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def release_driver(self, driver):
"""사용한 WebDriver를 Pool에 반납합니다."""
self._pool.put(driver)
def release_driver(self, driver):
"""사용한 WebDriver를 Pool에 반납합니다. 가득 차면 드라이버를 종료해 데드락/누수를 방지합니다."""
# 간단한 생존 체크(세션이 없으면 버림)
if getattr(driver, "session_id", None) is None:
try:
driver.quit()
finally:
with self._lock:
self._created = max(0, self._created - 1)
return
try:
self._pool.put(driver, timeout=1)
except queue.Full:
try:
driver.quit()
finally:
with self._lock:
self._created = max(0, self._created - 1)
🤖 Prompt for AI Agents
In app/services/driver_pool.py around lines 35 to 37, release_driver currently
calls self._pool.put(driver) which can block forever if the queue is full;
change it to use a non-blocking or timed put (e.g., put_nowait or put with
timeout) and handle queue.Full by disposing/closing the incoming driver and
updating the pool's active/available counter so the slot is considered free and
a new driver can be created on demand; also catch exceptions from driver
disposal and ensure the counter is decremented only once to avoid leaking state
on double-release.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
app/services/driver_pool.py (3)

1-6: 환경설정/동시성 대비: os·logging·threading import 및 로거 추가 권장

ENV 기반 설정, 락, 로깅 적용을 위한 선행 준비가 필요합니다.

 import queue
+import os
+import logging
+import threading
 from selenium import webdriver
 from selenium.webdriver.chrome.options import Options
 from selenium.webdriver.chrome.service import Service
+
+logger = logging.getLogger(__name__)

53-58: close_all는 대여 중 드라이버를 놓칩니다 → 카운터 리셋 및 non-blocking drain
대여 중인 드라이버는 남고 카운터도 초기화되지 않습니다. 최소한 큐 drain 후 카운터를 리셋하세요.

     def close_all(self):
         """애플리케이션 종료 시 모든 WebDriver를 종료합니다."""
-        while not self._pool.empty():
-            driver = self._pool.get()
-            driver.quit()
+        try:
+            while True:
+                driver = self._pool.get_nowait()
+                try:
+                    driver.quit()
+                except Exception:
+                    logger.debug("driver.quit() 실패 무시", exc_info=True)
+        except queue.Empty:
+            pass
+        finally:
+            if hasattr(self, "_created"):
+                self._created = 0

40-43: broad exception + print 사용 금지(BLE001)
로깅으로 대체하고 예외를 좁히세요. 위 제안(diff)에서 함께 수정되었습니다.

Dockerfile (3)

44-44: Chrome/Driver 경로 환경변수 추가 설정 권장

PATH 추가만으로도 동작할 수 있으나, 드라이버가 바이너리를 확실히 찾도록 아래 환경변수도 설정하세요.

-ENV PATH="/opt/chrome-linux64:/opt/chromedriver-linux64:${PATH}"
+ENV CHROME_BIN=/opt/chrome-linux64/chrome
+ENV CHROMEDRIVER=/opt/chromedriver-linux64/chromedriver
+ENV PATH="/opt/chrome-linux64:/opt/chromedriver-linux64:${PATH}"

48-52: 레이어 캐시 최적화는 적절함 · 추가 제안(선택)

requirements.txt만 먼저 복사하는 순서는 좋습니다. 이미지 크기/재현성을 위해 pip install 후 pip 캐시 제거를 추가하는 소소한 최적화 제안을 남깁니다.

-RUN pip install --no-cache-dir -r requirements.txt
+RUN pip install --no-cache-dir -r requirements.txt && rm -rf ~/.cache/pip

6-41: 대안(선택): OS 패키지 사용 또는 레이어로 브라우저 분리

  • 대안 1: AL2023 리포에 chromium/chromedriver가 제공된다면 dnf로 직접 설치하여 외부 다운로드를 제거(재현성↑, 속도↑). 버전 조합은 반드시 일치 검증 필요.
  • 대안 2: Lambda Layer에 브라우저/드라이버를 올리고 함수 설정에서 레이어로 주입(이미지 슬림화, 보안/업데이트 관리 용이).
    운영 요구사항에 따라 선택하세요.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f1c960 and bdb3a8b.

📒 Files selected for processing (2)
  • Dockerfile (1 hunks)
  • app/services/driver_pool.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.13.1)
app/services/driver_pool.py

40-40: Do not catch blind exception: Exception

(BLE001)

🪛 GitHub Actions: Deploy Onetime Crawler to AWS Lambda
Dockerfile

[error] 6-6: Docker build failed during RUN yum update -y ...: 'yum' command not found in the base image.

🔇 Additional comments (6)
app/services/driver_pool.py (4)

8-14: Lambda에서 드라이버 선(先)생성(size=5) → 콜드스타트/메모리 리스크. Lazy 생성으로 전환하세요.
초기 일괄 생성은 OOM/콜드스타트 지연과 실패 시 전체 초기화 실패를 유발합니다. Lazy-create(+락)로 변경하세요. 과거 코멘트와 동일 맥락입니다.

-class WebDriverPool:
-    def __init__(self, size=5):
-        self._pool = queue.Queue(maxsize=size)
-        for _ in range(size):
-            driver = self._create_driver()
-            if driver:
-                self._pool.put(driver)
+class WebDriverPool:
+    def __init__(self, size: int = 2):
+        self._size = max(1, size)
+        self._pool = queue.Queue(maxsize=self._size)
+        self._lock = threading.Lock()
+        self._created = 0  # 현재 생성된 드라이버 수 (대여/반납과 무관)

15-44: 하드코딩 경로/UA 제거 및 예외 처리 개선(ENV 기반, 안전 플래그, 로깅)
경로/UA 하드코딩은 이식성과 운영 편의성을 저해합니다. broad exception+print도 지양하세요. 과거 코멘트와 동일 맥락입니다.

     def _create_driver(self):
         """
         새로운 WebDriver 인스턴스를 생성합니다.
         """
-        try:
-            chrome_options = Options()
-
-            # Chrome 바이너리 위치를 지정합니다.
-            chrome_options.binary_location = "/opt/chrome-linux64/chrome"
-
-            # Headless 실행을 위한 표준 옵션들을 설정합니다.
-            chrome_options.add_argument("--headless=new")
-            chrome_options.add_argument("--no-sandbox")
-            chrome_options.add_argument("--disable-dev-shm-usage")
-            chrome_options.add_argument("--disable-gpu")
-            chrome_options.add_argument("--window-size=1280x1696")
-
-            # User-Agent 설정
-            chrome_options.add_argument(
-                "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
-
-            # Chromedriver 경로를 지정하여 Service 객체를 생성합니다.
-            service = Service(executable_path="/opt/chromedriver-linux64/chromedriver")
-
-            return webdriver.Chrome(service=service, options=chrome_options)
-        except Exception as e:
-            # 드라이버 생성 실패 시 로그를 남기고 None을 반환하여 Pool에 추가되지 않도록 함
-            print(f"WebDriver 생성 실패: {e}")
-            return None
+        try:
+            chrome_options = Options()
+            # 안전 플래그
+            chrome_options.add_argument("--headless=new")
+            chrome_options.add_argument("--no-sandbox")
+            chrome_options.add_argument("--disable-setuid-sandbox")
+            chrome_options.add_argument("--disable-dev-shm-usage")
+            chrome_options.add_argument("--disable-gpu")
+            chrome_options.add_argument("--disable-extensions")
+            chrome_options.add_argument("--no-zygote")
+            chrome_options.add_argument("--window-size=1280x1696")
+
+            # 바이너리/경로/UA/임시 디렉터리: ENV 우선, 없으면 합리적 기본값(/tmp)
+            binary = os.getenv("CHROME_BINARY_PATH")
+            if binary:
+                chrome_options.binary_location = binary
+
+            ua = os.getenv("CHROME_USER_AGENT")
+            if ua:
+                chrome_options.add_argument(f"user-agent={ua}")
+
+            user_data_dir = os.getenv("CHROME_USER_DATA_DIR", "/tmp/user-data")
+            data_path = os.getenv("CHROME_DATA_PATH", "/tmp/data-path")
+            cache_dir = os.getenv("CHROME_CACHE_DIR", "/tmp/cache-dir")
+            for d in (user_data_dir, data_path, cache_dir):
+                try:
+                    os.makedirs(d, exist_ok=True)
+                except OSError:
+                    pass
+            chrome_options.add_argument(f"--user-data-dir={user_data_dir}")
+            chrome_options.add_argument(f"--data-path={data_path}")
+            chrome_options.add_argument(f"--disk-cache-dir={cache_dir}")
+
+            # chromedriver 경로: ENV 설정 시에만 지정
+            driver_path = os.getenv("CHROMEDRIVER_PATH")
+            if driver_path:
+                service = Service(executable_path=driver_path)
+                return webdriver.Chrome(service=service, options=chrome_options)
+            return webdriver.Chrome(options=chrome_options)
+        except Exception:  # noqa: BLE001
+            # 구체 예외로 좁히는 것을 권장: selenium.common.exceptions.WebDriverException 등
+            logger.exception("WebDriver 생성 실패")
+            return None

45-47: 무기한 블로킹 위험(get) → timeout + lazy-create
풀 고갈 시 요청 스레드가 영구 대기합니다. 과거 코멘트와 동일 맥락입니다.

-    def get_driver(self):
-        """Pool에서 WebDriver를 가져옵니다."""
-        return self._pool.get()
+    def get_driver(self, timeout: float = 10.0):
+        """Pool에서 WebDriver를 가져오되, 부족하면 즉시 생성 후 반환하고, 끝에는 timeout으로 대기합니다."""
+        try:
+            return self._pool.get_nowait()
+        except queue.Empty:
+            pass
+        with self._lock:
+            if self._created < self._size:
+                driver = self._create_driver()
+                if driver:
+                    self._created += 1
+                    return driver
+        # 여기까지 왔다면 생성 불가 또는 이미 최대치
+        try:
+            return self._pool.get(timeout=timeout)
+        except queue.Empty:
+            raise TimeoutError("WebDriver 풀에서 드라이버를 획득하지 못했습니다(timeout).")

49-51: release 시 큐 가득/불량 드라이버 처리 → nowait/timeout + 폐기
이중 반납/불량 세션에서 영구 대기/누수 위험. 과거 코멘트와 동일 맥락입니다.

-    def release_driver(self, driver):
-        """사용한 WebDriver를 Pool에 반납합니다."""
-        self._pool.put(driver)
+    def release_driver(self, driver):
+        """사용한 WebDriver를 Pool에 반납합니다. 가득 차면 종료하여 데드락/누수를 방지합니다."""
+        # 간단한 생존 체크(세션이 없거나 크래시 시 폐기)
+        if getattr(driver, "session_id", None) is None:
+            try:
+                driver.quit()
+            finally:
+                with self._lock:
+                    self._created = max(0, self._created - 1)
+            return
+        try:
+            self._pool.put(driver, timeout=1)
+        except queue.Full:
+            try:
+                driver.quit()
+            finally:
+                with self._lock:
+                    self._created = max(0, self._created - 1)
Dockerfile (2)

55-55: Lambda 핸들러 지정은 적합

CMD ["app.main.handler"] 구성은 Lambda Python 베이스 이미지와 호환됩니다. app/main.pyhandler 심볼이 실제로 존재하는지만 한번 더 확인 부탁드립니다.


32-45: 과거 리뷰 대응 현황: 크롬/드라이버 번들링 시도는 좋으나 아직 불안정

이전 지적 사항(브라우저/드라이버 미포함) 대비, 이번 커밋에서 번들링을 시도한 점은 긍정적입니다. 다만 AL2023 패키지/도구 호환성과 다운로드 출처/권한/무결성 이슈가 남아 있어 런타임 실패 가능성이 큽니다. 위 수정안 반영을 권고합니다.

Dockerfile Outdated
Comment on lines 6 to 30
RUN yum update -y && \
yum install -y \
gzip \
tar \
unzip \
alsa-lib \
atk \
cups-libs \
gtk3 \
ipa-gothic-fonts \
libXcomposite \
libXcursor \
libXdamage \
libXext \
libXi \
libXrandr \
libXScrnSaver \
libXtst \
pango \
xorg-x11-fonts-100dpi \
xorg-x11-fonts-75dpi \
xorg-x11-fonts-cyrillic \
xorg-x11-fonts-misc \
xorg-x11-fonts-Type1 \
&& rm -rf /var/cache/yum
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

AL2023 기반 Lambda 이미지에서 yum 미존재 → 빌드 실패. dnf로 교체하고 패키지 세트 수정 필요

public.ecr.aws/lambda/python:3.12는 Amazon Linux 2023 기반이라 yum이 없고, 현재 패키지 목록 중 다수(예: xorg-x11-fonts-*)는 AL2023 리포에 존재하지 않습니다. 또한 curl을 설치하지 않아 이후 단계에서 curl 호출도 실패합니다. 아래처럼 dnf로 교체하고, AL2023에서 사용 가능한 최소 의존성 세트 및 클린업을 적용하세요.

-RUN yum update -y && \
-    yum install -y \
-    gzip \
-    tar \
-    unzip \
-    alsa-lib \
-    atk \
-    cups-libs \
-    gtk3 \
-    ipa-gothic-fonts \
-    libXcomposite \
-    libXcursor \
-    libXdamage \
-    libXext \
-    libXi \
-    libXrandr \
-    libXScrnSaver \
-    libXtst \
-    pango \
-    xorg-x11-fonts-100dpi \
-    xorg-x11-fonts-75dpi \
-    xorg-x11-fonts-cyrillic \
-    xorg-x11-fonts-misc \
-    xorg-x11-fonts-Type1 \
-    && rm -rf /var/cache/yum
+RUN set -eux; \
+    dnf -y update && \
+    dnf -y install \
+      curl \
+      unzip \
+      tar \
+      gzip \
+      ca-certificates \
+      nss \
+      alsa-lib \
+      atk \
+      at-spi2-atk \
+      at-spi2-core \
+      cups-libs \
+      dbus-libs \
+      gtk3 \
+      libX11 \
+      libXcomposite \
+      libXcursor \
+      libXdamage \
+      libXext \
+      libXi \
+      libXrandr \
+      libXScrnSaver \
+      libXtst \
+      pango \
+      freetype \
+      libdrm \
+      mesa-libgbm \
+    --setopt=install_weak_deps=False && \
+    dnf clean all && rm -rf /var/cache/dnf
🧰 Tools
🪛 GitHub Actions: Deploy Onetime Crawler to AWS Lambda

[error] 6-6: Docker build failed during RUN yum update -y ...: 'yum' command not found in the base image.

🤖 Prompt for AI Agents
In Dockerfile around lines 6–30, the Dockerfile uses yum and installs packages
unavailable on Amazon Linux 2023 causing build failures; replace yum with dnf,
update the package list to only include AL2023-available dependencies (remove
xorg-x11-fonts-* and any other unavailable font packages), add curl to the
install list, use non-interactive flags (e.g., -y) and minimize weak deps if
desired, and perform proper cleanup with dnf clean all and removing
metadata/cache directories to keep the image small.

Comment on lines +34 to +41
ENV CHROME_VERSION=125.0.6422.78
RUN curl -Lo /tmp/chrome-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chrome-linux64.zip" && \
unzip /tmp/chrome-linux64.zip -d /opt/ && \
rm /tmp/chrome-linux64.zip

# 4️⃣ 필요한 패키지 설치
RUN apt-get update && apt-get install -y \
curl unzip \
chromium chromium-driver \
&& rm -rf /var/lib/apt/lists/*
RUN curl -Lo /tmp/chromedriver-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" && \
unzip /tmp/chromedriver-linux64.zip -d /opt/ && \
rm /tmp/chromedriver-linux64.zip
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Chrome/Driver 다운로드 소스/무결성/실행권한 보강

  • edgedl.me.gvt1.com 대신 공식 GCS 경로(storage.googleapis.com/chrome-for-testing-public/...) 사용을 권장.
  • curl-fSL 추가, unzip에 -q로 로그 최소화.
  • chromedriver에 실행권한 부여.
  • 가능하면 SHA256 검증을 추가(아래는 기본형).
-ENV CHROME_VERSION=125.0.6422.78
-RUN curl -Lo /tmp/chrome-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chrome-linux64.zip" && \
-    unzip /tmp/chrome-linux64.zip -d /opt/ && \
-    rm /tmp/chrome-linux64.zip
-
-RUN curl -Lo /tmp/chromedriver-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" && \
-    unzip /tmp/chromedriver-linux64.zip -d /opt/ && \
-    rm /tmp/chromedriver-linux64.zip
+ARG CHROME_VERSION=125.0.6422.78
+ENV CHROME_VERSION=${CHROME_VERSION}
+RUN set -eux; \
+    curl -fSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chrome-linux64.zip" -o /tmp/chrome-linux64.zip && \
+    unzip -q /tmp/chrome-linux64.zip -d /opt/ && rm -f /tmp/chrome-linux64.zip
+RUN set -eux; \
+    curl -fSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" -o /tmp/chromedriver-linux64.zip && \
+    unzip -q /tmp/chromedriver-linux64.zip -d /opt/ && rm -f /tmp/chromedriver-linux64.zip && \
+    chmod +x /opt/chromedriver-linux64/chromedriver
+# 선택: 무결성 검증 (SHA256SUMS 파일 활용)도 추가 가능
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ENV CHROME_VERSION=125.0.6422.78
RUN curl -Lo /tmp/chrome-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chrome-linux64.zip" && \
unzip /tmp/chrome-linux64.zip -d /opt/ && \
rm /tmp/chrome-linux64.zip
# 4️⃣ 필요한 패키지 설치
RUN apt-get update && apt-get install -y \
curl unzip \
chromium chromium-driver \
&& rm -rf /var/lib/apt/lists/*
RUN curl -Lo /tmp/chromedriver-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" && \
unzip /tmp/chromedriver-linux64.zip -d /opt/ && \
rm /tmp/chromedriver-linux64.zip
ARG CHROME_VERSION=125.0.6422.78
ENV CHROME_VERSION=${CHROME_VERSION}
RUN set -eux; \
curl -fSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chrome-linux64.zip" -o /tmp/chrome-linux64.zip && \
unzip -q /tmp/chrome-linux64.zip -d /opt/ && rm -f /tmp/chrome-linux64.zip
RUN set -eux; \
curl -fSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" -o /tmp/chromedriver-linux64.zip && \
unzip -q /tmp/chromedriver-linux64.zip -d /opt/ && rm -f /tmp/chromedriver-linux64.zip && \
chmod +x /opt/chromedriver-linux64/chromedriver
# Optional: add SHA256 checksum verification for downloads
🤖 Prompt for AI Agents
In Dockerfile around lines 34 to 41, the download/unpack steps should be
hardened: switch the download URLs from edgedl.me.gvt1.com to the official GCS
path (storage.googleapis.com/chrome-for-testing-public/.../linux64/...), add
curl flags -fSL to fail on HTTP errors and follow redirects, use unzip -q to
quiet output, and after extracting give the chromedriver binary executable
permission (chmod +x /opt/chromedriver*/chromedriver). Also add an optional
SHA256 verification step: download the .sha256 or compute sha256sum of the zip,
compare it against a trusted checksum before extracting and fail the build if it
doesn’t match.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (8)
.github/workflows/crawler-cicd.yaml (6)

8-9: 동시 실행 취소로 레이스/중복 배포 방지

같은 브랜치에 여러 실행이 겹치면 최신만 남기고 이전 실행은 취소하세요.

 jobs:
   deploy:
+    concurrency:
+      group: deploy-${{ github.ref }}
+      cancel-in-progress: true

16-21: 정적 자격증명 사용 시 최소권한·회전 정책 확인

사용자 선호에 따라 OIDC 미사용은 OK. 대신 IAM 사용자에 ECR push, Lambda UpdateFunctionCode/Configuration 최소 권한만 부여하고 키 주기적 회전(예: 90일), 필요 시 IP 조건을 적용하세요. Region은 시크릿이 아니므로 상수/env로 빼도 됩니다.


29-38: 빌드 캐시/보안 강화를 위해 build-push 액션 사용 고려

로컬 docker 명령은 캐시 미활용·로그인 상태 의존이 큽니다. buildx+캐시로 시간/요금 절감하고 액션 버전을 핀으로 고정하세요.

-      - name: Build, tag, and push image to Amazon ECR
-        id: build-image
-        env:
-          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
-          ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY_NAME }}
-          IMAGE_TAG: ${{ github.sha }} # Git 커밋 ID를 이미지 태그로 사용하여 버전 관리
-        run: |
-          docker build --platform linux/amd64 -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
-          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
-          echo "image_uri=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
+      - name: Set image meta
+        id: meta
+        run: echo "tag=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
+
+      - name: Build and push (with cache)
+        id: build-image
+        uses: docker/build-push-action@v6
+        with:
+          context: .
+          platforms: linux/amd64
+          push: true
+          tags: ${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPOSITORY_NAME }}:${{ steps.meta.outputs.tag }}
+          cache-from: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPOSITORY_NAME }}:buildcache
+          cache-to: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPOSITORY_NAME }}:buildcache,mode=max
+
+      - name: Export image uri
+        run: echo "image_uri=${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPOSITORY_NAME }}:${{ steps.meta.outputs.tag }}" >> "$GITHUB_OUTPUT"

41-46: $LATEST 직접 갱신 대신 버전+별칭(alias) 배포로 롤백/안정성 확보

버전 발행(--publish) 후 prod/staging 별칭을 이동시키면 점진적 전환·즉시 롤백이 가능합니다.

-          aws lambda update-function-code \
-            --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
-            --image-uri ${{ steps.build-image.outputs.image_uri }}
+          VERSION=$(aws lambda update-function-code \
+            --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
+            --image-uri ${{ steps.build-image.outputs.image_uri }} \
+            --publish \
+            --query 'Version' --output text)
+          # prod/staging 중 운영 별칭을 이동 (사전 생성 가정)
+          aws lambda update-alias \
+            --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
+            --name prod \
+            --function-version "$VERSION"

11-21: 액션 버전 고정(pinning) 및 최소 GITHUB_TOKEN 권한 설정

서드파티/공식 액션은 커밋 SHA로 핀 고정, GITHUB_TOKEN 권한은 최소화(contents: read 등) 권장.

-      - name: Checkout repository
-        uses: actions/checkout@v4
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        # 예: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

+permissions:
+  contents: read

28-34: ECR 리포지토리 존재 보장(첫 배포 실패 방지)

리포지토리가 없으면 빌드 후 push에서 실패합니다. 사전 생성 또는 조건부 생성 스텝을 추가하세요.

       - name: Login to Amazon ECR
         id: login-ecr
         uses: aws-actions/amazon-ecr-login@v2
+
+      - name: Ensure ECR repository exists
+        run: |
+          aws ecr describe-repositories --repository-names "${{ secrets.ECR_REPOSITORY_NAME }}" >/dev/null 2>&1 || \
+          aws ecr create-repository --repository-name "${{ secrets.ECR_REPOSITORY_NAME }}"
Dockerfile (2)

11-18: OS 패키지 설치 슬림화 및 재현성 개선 제안

chromium/chromium-driver 설치는 OK이나, 이미지 비대화/콜드스타트 비용이 큽니다. 다음을 권장합니다.

  • --no-install-recommends로 추천 패키지 제외.
  • 가능한 경우 버전 고정(동일 메이저 매칭 유지) 또는 Chrome-for-Testing 번들로 명시 버전 관리.
  • 불필요한 도구 미설치 유지.

패치 예시:

-RUN apt-get update && apt-get install -y \
-    chromium chromium-driver \
-    && rm -rf /var/lib/apt/lists/*
+RUN apt-get update && apt-get install -y --no-install-recommends \
+    chromium chromium-driver \
+    && rm -rf /var/lib/apt/lists/*

22-23: pip --user + PATH 구성은 동작하나, 운영 근거지로는 venv가 더 안전

현재 --user 설치와 PATH 추가로 런타임 해상은 됩니다. 다만, 재현성과 의존성 격리를 위해 프로젝트 고정 venv 경로(예: /opt/venv + ENV PATH="/opt/venv/bin:$PATH" + pip install -t 또는 python -m venv ) 사용을 고려하세요.

Also applies to: 29-31

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bdb3a8b and 451d561.

📒 Files selected for processing (2)
  • .github/workflows/crawler-cicd.yaml (1 hunks)
  • Dockerfile (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-21T20:00:59.927Z
Learnt from: bbbang105
PR: onetime-with-members/crawling-server#16
File: .github/workflows/crawler-cicd.yaml:7-9
Timestamp: 2025-09-21T20:00:59.927Z
Learning: User bbbang105 prefers using static AWS credentials over OIDC for GitHub Actions workflows in the crawling-server project.

Applied to files:

  • .github/workflows/crawler-cicd.yaml
🔇 Additional comments (1)
.github/workflows/crawler-cicd.yaml (1)

3-6: PR 이벤트에서 실배포 방지 + 수동/푸시 배포로 분리하세요

pull_request 트리거에서 바로 Lambda 배포가 일어나면 미승인 코드가 배포될 수 있고, 포크 PR에선 시크릿 미제공으로 실패합니다. push/workflow_dispatch만 배포로 가드하세요.

적용 예시(둘 다 적용 권장):

 on:
-  pull_request:
-    branches: [ "develop" ]
+  pull_request:
+    branches: [ "develop" ]  # PR에서는 빌드/테스트까지만
+  push:
+    branches: [ "develop" ]  # 머지 시 자동 배포
+  workflow_dispatch:         # 필요 시 수동 배포

 jobs:
   deploy:
+    if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}

Comment on lines +48 to +59
- name: Update Lambda function configuration
env:
API_KEY_SECRET: ${{ secrets.API_KEY }}
DISCORD_URL_SECRET: ${{ secrets.DISCORD_WEBHOOK_URL }}
run: |
# Lambda가 환경 변수를 업데이트할 때까지 잠시 대기
aws lambda wait function-updated --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }}
# GitHub Secret 값을 사용하여 Lambda 함수의 환경 변수를 설정
aws lambda update-function-configuration \
--function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
--environment "Variables={API_KEY=$API_KEY_SECRET,DISCORD_WEBHOOK_URL=$DISCORD_URL_SECRET}"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

환경 변수 전체 덮어쓰기 위험(기존 키 소실 가능)

현재 Variables=…는 기존 변수를 모두 대체합니다. get-function-configuration로 병합 후 업데이트하세요.

-          aws lambda update-function-configuration \
-            --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
-            --environment "Variables={API_KEY=$API_KEY_SECRET,DISCORD_WEBHOOK_URL=$DISCORD_URL_SECRET}"
+          # 기존 변수와 병합
+          EXISTING=$(aws lambda get-function-configuration \
+            --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
+            --query 'Environment.Variables' --output json)
+          MERGED=$(jq -n --argjson a "$EXISTING" \
+                        --arg API_KEY "$API_KEY_SECRET" \
+                        --arg DISCORD "$DISCORD_URL_SECRET" \
+                        '$a + {API_KEY:$API_KEY, DISCORD_WEBHOOK_URL:$DISCORD}')
+          aws lambda update-function-configuration \
+            --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
+            --environment "Variables=${MERGED}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Update Lambda function configuration
env:
API_KEY_SECRET: ${{ secrets.API_KEY }}
DISCORD_URL_SECRET: ${{ secrets.DISCORD_WEBHOOK_URL }}
run: |
# Lambda가 환경 변수를 업데이트할 때까지 잠시 대기
aws lambda wait function-updated --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }}
# GitHub Secret 값을 사용하여 Lambda 함수의 환경 변수를 설정
aws lambda update-function-configuration \
--function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
--environment "Variables={API_KEY=$API_KEY_SECRET,DISCORD_WEBHOOK_URL=$DISCORD_URL_SECRET}"
- name: Update Lambda function configuration
env:
API_KEY_SECRET: ${{ secrets.API_KEY }}
DISCORD_URL_SECRET: ${{ secrets.DISCORD_WEBHOOK_URL }}
run: |
# Lambda가 환경 변수를 업데이트할 때까지 잠시 대기
aws lambda wait function-updated --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }}
# 기존 변수와 병합 (존재하지 않으면 빈 객체로 처리)
EXISTING=$(aws lambda get-function-configuration \
--function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
--query 'Environment.Variables' --output json || echo 'null')
MERGED=$(jq -c -n --argjson a "$EXISTING" \
--arg API_KEY "$API_KEY_SECRET" \
--arg DISCORD "$DISCORD_URL_SECRET" \
'($a // {}) + {API_KEY:$API_KEY, DISCORD_WEBHOOK_URL:$DISCORD}')
ENV_JSON=$(jq -c -n --argjson vars "$MERGED" '{"Variables": $vars}')
aws lambda update-function-configuration \
--function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} \
--environment "$ENV_JSON"
🤖 Prompt for AI Agents
In .github/workflows/crawler-cicd.yaml around lines 48 to 59, the workflow
currently overwrites the Lambda function’s entire environment (Variables=...)
which can erase existing keys; change it to fetch the current environment with
aws lambda get-function-configuration, parse/merge the existing Variables with
the new API_KEY and DISCORD_WEBHOOK_URL entries, then call aws lambda
update-function-configuration with the merged Variables so existing keys are
preserved; ensure you handle empty/missing Variables, proper quoting/escaping of
values, and fail fast if merging or parsing the JSON configuration fails.

Dockerfile Outdated

# 8️⃣ 실행 명령어 설정
CMD ["python", "app.py"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

비루트 사용자 + 포트 80 바인딩 실패

현재 USER가 appuser이므로 80/tcp 바인딩은 권한 오류로 실패합니다. 8000/8080 등 비특권 포트로 변경하세요.

적용 패치 예시:

-CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
🤖 Prompt for AI Agents
In Dockerfile around line 36, the container command binds to privileged port 80
while the image switches to non-root user (appuser), causing permission errors;
change the uvicorn bind port to a non-privileged port (e.g., 8000 or 8080) in
the CMD, update any EXPOSE line to match the new port, and ensure any external
documentation or docker-compose ports mapping is updated accordingly so the app
runs without root privileges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
Dockerfile (2)

22-23: PATH만으로는 애플리케이션 설정과 불일치 가능 — 표준 ENV도 함께 노출 권장

코드가 CHROME_BIN/CHROMEDRIVER_PATH를 참조한다면 현재 Dockerfile에는 해당 ENV가 없습니다. PATH 탐색에만 의존하면 런타임 설정 변경 시 디버깅이 어려워집니다. 아래처럼 명시적 ENV를 추가하는 것을 권장합니다.

-ENV PATH="/opt/chrome-linux64:/opt/chromedriver-linux64:${PATH}"
+ENV CHROME_BIN=/opt/chrome-linux64/chrome
+ENV CHROMEDRIVER_PATH=/opt/chromedriver-linux64/chromedriver
+ENV PATH="/opt/chrome-linux64:/opt/chromedriver-linux64:${PATH}"

추가로, 애플리케이션에서 위 ENV를 실제로 읽는지 확인 부탁드립니다.


31-31: pip 설치 단계 무난함 (추가 최적화 여지 소소)

--no-cache-dir 사용은 적절합니다. 필요 시 안전한 범위에서 pip 버전 고정 또는 constraints 파일 도입을 고려할 수 있습니다(선택).

app/services/driver_pool.py (2)

42-44: 예외 처리를 구체적으로 개선하세요

현재 모든 예외를 포괄적으로 잡고 있어 디버깅이 어렵습니다.

구체적인 예외 처리로 개선하세요:

-    except Exception as e:
-        print(f"WebDriver 생성 실패: {e}")
+    except (WebDriverException, OSError) as e:
+        print(f"WebDriver 생성 실패: {e}")
     return None

selenium의 예외를 import하는 것도 잊지 마세요:

 from selenium import webdriver
+from selenium.common.exceptions import WebDriverException
 from selenium.webdriver.chrome.options import Options

54-58: 종료 시 드라이버 정리 로직에 예외 처리 추가

드라이버 종료 중 예외가 발생하면 나머지 드라이버들이 정리되지 않을 수 있습니다.

안전한 종료를 위해 예외 처리를 추가하세요:

 def close_all(self):
     """애플리케이션 종료 시 모든 WebDriver를 종료합니다."""
     while not self._pool.empty():
-        driver = self._pool.get()
-        driver.quit()
+        try:
+            driver = self._pool.get_nowait()
+            driver.quit()
+        except queue.Empty:
+            break
+        except Exception as e:
+            print(f"드라이버 종료 중 오류 발생: {e}")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 451d561 and 058a508.

📒 Files selected for processing (2)
  • Dockerfile (1 hunks)
  • app/services/driver_pool.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.13.1)
app/services/driver_pool.py

42-42: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: deploy
🔇 Additional comments (9)
Dockerfile (5)

1-2: Lambda 베이스 이미지 전환 잘했습니다

AWS Lambda Python 3.12 공식 이미지를 사용하고 CMD를 핸들러로 지정하는 방향과 일치합니다. CI/CD와 함수 핸들러 설정도 이 전제에 맞춰 유지해 주세요.


26-28: 레이어 캐싱 활용 구조 적절

requirements → pip 설치 → 앱 코드 복사 순서가 적절해 빌드 캐시 재활용에 유리합니다.


33-34: Lambda 핸들러 진입점 지정 OK

Mangum 핸들러 노출(CMD ["app.main.handler"])이 베이스 이미지와 일치합니다.

Lambda 함수 설정(이미지 기반)에서도 핸들러가 빈 값이어야 하고(이미지 엔트리포인트 사용), 필요한 환경변수(API 키 등)가 세팅되어 있는지 확인해 주세요.


13-20: Chrome/Driver 다운로드 소스/무결성/실행권한 강화 필요

이전 리뷰와 동일한 이슈입니다: 공식 GCS 경로 사용, curl 실패 시 빌드 중단, unzip 조용 모드, chromedriver 실행권한, 버전 ARG화, 선택적 SHA256 검증을 권장합니다.

-ENV CHROME_VERSION=125.0.6422.78
-RUN curl -Lo /tmp/chrome-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chrome-linux64.zip" && \
-    unzip /tmp/chrome-linux64.zip -d /opt/ && \
-    rm /tmp/chrome-linux64.zip
-
-RUN curl -Lo /tmp/chromedriver-linux64.zip "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" && \
-    unzip /tmp/chromedriver-linux64.zip -d /opt/ && \
-    rm /tmp/chromedriver-linux64.zip
+ARG CHROME_VERSION=125.0.6422.78
+ENV CHROME_VERSION=${CHROME_VERSION}
+RUN set -eux; \
+    curl -fSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chrome-linux64.zip" -o /tmp/chrome-linux64.zip && \
+    unzip -q /tmp/chrome-linux64.zip -d /opt/ && rm -f /tmp/chrome-linux64.zip
+RUN set -eux; \
+    curl -fSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" -o /tmp/chromedriver-linux64.zip && \
+    unzip -q /tmp/chromedriver-linux64.zip -d /opt/ && rm -f /tmp/chromedriver-linux64.zip && \
+    chmod +x /opt/chromedriver-linux64/chromedriver
+# (선택) SHA256 검증 추가 가능
+# RUN curl -fSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/sha256sum.txt" -o /tmp/SHA256SUMS && \
+#     (cd /tmp && sha256sum -c SHA256SUMS --ignore-missing)

5-10: 필수 패키지 누락으로 빌드/런타임 실패 위험 (curl, libgbm 계열 등 추가 필요)

  • curl 미설치 상태에서 아래 단계에서 curl 호출(라인 14, 18)로 인해 빌드가 즉시 실패합니다.
  • Headless Chromium 구동에 자주 필요한 mesa-libgbm/libdrm, at-spi2-core, dbus-libs, freetype가 빠져 크롬 실행이 실패할 수 있습니다(“error while loading shared libraries: libgbm.so.1” 등).

아래 패치를 적용해 주세요.

-RUN microdnf install -y \
-    unzip gzip tar \
-    glib2 nss atk at-spi2-atk cups-libs gtk3 pango \
-    libX11 libXcomposite libXcursor libXdamage libXext libXi \
-    libXrandr libXScrnSaver libXtst alsa-lib fontconfig \
-    && microdnf clean all
+RUN microdnf install -y \
+    unzip gzip tar curl ca-certificates \
+    glib2 nss atk at-spi2-atk at-spi2-core cups-libs gtk3 pango freetype \
+    dbus-libs \
+    libX11 libXcomposite libXcursor libXdamage libXext libXi \
+    libXrandr libXScrnSaver libXtst alsa-lib fontconfig \
+    libdrm mesa-libgbm \
+  && microdnf clean all
app/services/driver_pool.py (4)

8-14: Lambda에서의 초기 드라이버 일괄 생성은 메모리/콜드스타트 리스크가 큽니다

Lambda 컨테이너에서 Chrome 5개 선생성은 OOM/콜드스타트 지연을 유발할 수 있습니다. 실패 시 앱 초기화 전체가 터집니다.

게으른 생성+비동기 대여로 전환을 권장합니다.


46-48: 풀 고갈 시 무기한 대기 위험 → timeout 부여 필요

현재 get()은 끝없이 블로킹하여 요청 스레드가 고립될 위험이 있습니다.


50-52: release 시 큐가 가득 차면 영구 블로킹 위험

이중 반납 등 실수 시 put()이 무기한 대기할 수 있습니다.


19-41: Chrome 설정을 환경변수 기반으로 개선하고 불안정한 플래그를 제거하세요

현재 구현에는 여러 개선점이 필요합니다:

  • --single-process 플래그는 크래시와 불안정의 원인이므로 제거 권장
  • 바이너리 경로와 드라이버 경로가 하드코딩되어 있어 환경별 배포에 취약
  • User-Agent가 하드코딩되어 유지보수가 어려움

환경변수 기반 설정으로 개선하세요:

 def _create_driver(self):
     try:
         chrome_options = Options()
-
-        chrome_options.binary_location = "/opt/chrome-linux64/chrome"
+        
+        binary = os.getenv("CHROME_BINARY_PATH", "/opt/chrome-linux64/chrome")
+        chrome_options.binary_location = binary
 
         # Headless & Lambda 옵션 추가
         chrome_options.add_argument("--headless")
         chrome_options.add_argument("--no-sandbox")
+        chrome_options.add_argument("--disable-setuid-sandbox")
         chrome_options.add_argument("--disable-dev-shm-usage")
         chrome_options.add_argument("--disable-gpu")
+        chrome_options.add_argument("--disable-extensions")
         chrome_options.add_argument("--window-size=1280x1696")
-        chrome_options.add_argument("--single-process")
         chrome_options.add_argument("--disable-software-rasterizer")
         chrome_options.add_argument("--no-zygote")
         chrome_options.add_argument("--remote-debugging-port=9222")
-        chrome_options.add_argument('--user-data-dir=/tmp/user-data')
-        chrome_options.add_argument('--data-path=/tmp/data-path')
-        chrome_options.add_argument('--disk-cache-dir=/tmp/cache-dir')
+        
+        user_data_dir = os.getenv("CHROME_USER_DATA_DIR", "/tmp/user-data")
+        data_path = os.getenv("CHROME_DATA_PATH", "/tmp/data-path")
+        cache_dir = os.getenv("CHROME_CACHE_DIR", "/tmp/cache-dir")
+        chrome_options.add_argument(f'--user-data-dir={user_data_dir}')
+        chrome_options.add_argument(f'--data-path={data_path}')
+        chrome_options.add_argument(f'--disk-cache-dir={cache_dir}')
 
         # User-Agent 설정
-        chrome_options.add_argument(
-            "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
+        ua = os.getenv("CHROME_USER_AGENT")
+        if ua:
+            chrome_options.add_argument(f"user-agent={ua}")
 
-        service = Service(executable_path="/opt/chromedriver-linux64/chromedriver")
+        driver_path = os.getenv("CHROMEDRIVER_PATH", "/opt/chromedriver-linux64/chromedriver")
+        service = Service(executable_path=driver_path)
 
         return webdriver.Chrome(service=service, options=chrome_options)

이 변경사항을 위해 파일 상단에 import os를 추가해야 합니다:

 import queue
+import os
 from selenium import webdriver

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
app/main.py (2)

44-59: 프로세스 시간 측정에 time.perf_counter 사용 권장

고해상도 단조 타이머로 전환하면 더 정확합니다. 헤더 값은 소수점 고정으로 전달하세요.

 async def add_process_time_header(request: Request, call_next):
-    # 1. 요청이 들어올 때 시작 시간 기록
-    start_time = time.time()
+    # 1. 요청이 들어올 때 시작 시간 기록
+    start_time = time.perf_counter()
@@
-    process_time = time.time() - start_time
+    process_time = time.perf_counter() - start_time
@@
-    response.headers["X-Process-Time"] = str(process_time)
+    response.headers["X-Process-Time"] = f"{process_time:.4f}"

62-79: CORS: 테스트 도메인 정리 및 와일드카드 대체로 정규식 허용 추가

  • vercel.app는 www 서브도메인을 사용하지 않습니다. 목록에서 제거하세요.
  • 제품 도메인은 allow_origin_regex로 서브도메인 전체 허용이 깔끔합니다.
 origins = [
-    "http://localhost:5173",
-    "http://localhost:3000",
-    "https://onetime-test.vercel.app",
-    "https://www.onetime-test.vercel.app",
-    "https://onetime-with-members.com",
-    "https://www.onetime-with-members.com",
-    "https://1-ti.me",
+    "http://localhost:5173",
+    "http://localhost:3000",
+    "https://onetime-test.vercel.app",
+    "https://onetime-with-members.com",
+    "https://www.onetime-with-members.com",
+    "https://1-ti.me",
 ]
 
 app.add_middleware(
     CORSMiddleware,
     allow_origins=origins,
+    allow_origin_regex=r"^https://([a-z0-9-]+\.)?onetime-with-members\.com$",
     allow_credentials=True,
     allow_methods=["*"],
     allow_headers=["*"],
 )
app/services/driver_pool.py (4)

1-4: 환경 변수 사용을 위한 os 임포트 추가

하드코드된 경로/플래그를 ENV로 치환할 준비가 필요합니다.

 import logging
 import queue
 import threading
+import os

5-8: 구체 예외 임포트로 블라인드 캐치 제거 준비

WebDriverException을 명시적으로 다루기 위해 임포트합니다.

 from selenium import webdriver
 from selenium.webdriver.chrome.options import Options
 from selenium.webdriver.chrome.service import Service
+from selenium.common.exceptions import WebDriverException

70-75: close_all: None 가드 및 카운터 정합성 회복

큐에서 나온 항목이 None일 수 있으므로 가드하고, 종료 시 created_count를 감소시켜 정합성을 맞춥니다.

     def close_all(self):
         """애플리케이션 종료 시 모든 WebDriver를 종료합니다."""
-        while not self._pool.empty():
-            driver = self._pool.get()
-            driver.quit()
+        while not self._pool.empty():
+            driver = self._pool.get()
+            try:
+                if driver:
+                    driver.quit()
+            finally:
+                with self.lock:
+                    self.created_count = max(0, self.created_count - 1)

11-17: 문맥 관리자 제공으로 대여/반납 보일러플레이트 제거 제안

with pool.borrow(): 형태를 제공하면 누수 방지에 효과적입니다.

# (추가 제안) 클래스 내부에 컨텍스트 매니저 제공
from contextlib import contextmanager

@contextmanager
def borrow(self, timeout: float = 10.0):
    driver = self.get_driver(timeout=timeout)
    try:
        yield driver
    finally:
        self.release_driver(driver)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 058a508 and fa91dea.

📒 Files selected for processing (2)
  • app/main.py (1 hunks)
  • app/services/driver_pool.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/main.py (3)
app/core/exception_handlers.py (2)
  • http_exception_handler (7-25)
  • validation_exception_handler (28-44)
app/core/logging_config.py (1)
  • setup_logging (26-28)
app/services/driver_pool.py (2)
  • WebDriverPool (11-74)
  • close_all (70-74)
🪛 Ruff (0.13.1)
app/services/driver_pool.py

45-45: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: deploy
🔇 Additional comments (3)
app/main.py (1)

23-31: Lambda에서 드라이버 풀 초기화 지연·환경 토글 적용 필요 (이전 코멘트와 동일 이슈)

콜드스타트/의존 바이너리 부재 시 실패를 피하려면 POOL_SIZE(기본 0)로 제어하고, 0이면 생성 생략+지연 초기화가 안전합니다. 종료 시 존재 여부를 확인해 안전 종료하세요.

+import os
@@
 @asynccontextmanager
 async def lifespan(app: FastAPI):
-    # 앱 시작 시 실행
-    logger.info("🚀 WebDriver Pool을 생성합니다...")
-    app.state.driver_pool = WebDriverPool(size=1)
+    # 앱 시작 시 실행
+    pool_size = int(os.getenv("POOL_SIZE", "0"))
+    if pool_size > 0:
+        logger.info(f"🚀 WebDriver Pool 생성(size={pool_size})")
+        app.state.driver_pool = WebDriverPool(size=pool_size)
+    else:
+        logger.info("🚀 WebDriver Pool 지연초기화 모드")
+        app.state.driver_pool = None
     yield
     # 앱 종료 시 실행
-    logger.info("💀 WebDriver Pool을 종료합니다...")
-    app.state.driver_pool.close_all()
+    if getattr(app.state, "driver_pool", None):
+        logger.info("💀 WebDriver Pool을 종료합니다...")
+        app.state.driver_pool.close_all()
app/services/driver_pool.py (2)

66-69: release_driver: None/불량 드라이버 처리 및 put 무한대기 방지

  • None 또는 세션 없는 드라이버는 폐기하고 카운터 감소.
  • 큐가 가득 차면 timeout 후 폐기.
     def release_driver(self, driver):
         """사용한 WebDriver를 Pool에 반납합니다."""
-        self._pool.put(driver)
+        # None 또는 비정상 드라이버는 폐기
+        if driver is None or getattr(driver, "session_id", None) is None:
+            try:
+                if driver:
+                    driver.quit()
+            finally:
+                with self.lock:
+                    self.created_count = max(0, self.created_count - 1)
+            return
+        try:
+            self._pool.put(driver, timeout=1)
+        except queue.Full:
+            try:
+                driver.quit()
+            finally:
+                with self.lock:
+                    self.created_count = max(0, self.created_count - 1)

49-65: get_driver: 실패 시 None 반환·무기한 대기·카운터 불일치 가능성 → 타임아웃·성공 시에만 카운트 증가

  • 생성 실패 시 None 반환은 호출측 NPE 유발.
  • 무한 대기 대신 timeout 후 명확히 예외.
  • created_count는 성공 생성 후에만 증가.
-    def get_driver(self):
-        """Pool에서 드라이버를 가져오거나, 없으면 새로 생성합니다."""
-        try:
-            # 1. 일단 Pool에 드라이버가 있는지 확인 (기다리지 않음)
-            return self._pool.get_nowait()
-        except queue.Empty:
-            # 2. Pool이 비어있으면, 새로 만들어도 되는지 확인
-            with self.lock:
-                if self.created_count < self.max_size:
-                    self.created_count += 1
-                    logger.info(f"새로운 WebDriver 생성 (현재 {self.created_count}/{self.max_size}개)")
-                    return self._create_driver()
-
-            # 3. 만들 수 없다면, 다른 요청이 드라이버를 반납할 때까지 기다림
-            logger.info("사용 가능한 WebDriver가 없어 대기합니다...")
-            return self._pool.get()
+    def get_driver(self, timeout: float = 10.0):
+        """Pool에서 드라이버를 가져오거나, 없으면 새로 생성합니다."""
+        try:
+            return self._pool.get_nowait()
+        except queue.Empty:
+            pass
+        # 2) 여유가 있으면 생성 시도(성공 시에만 카운트 증가)
+        with self.lock:
+            if self.created_count < self.max_size:
+                driver = self._create_driver()
+                if driver is not None:
+                    self.created_count += 1
+                    logger.info(f"새로운 WebDriver 생성 (현재 {self.created_count}/{self.max_size}개)")
+                    return driver
+                logger.error("WebDriver 생성 실패로 풀 확장을 건너뜁니다.")
+        # 3) 반납 대기 (타임아웃)
+        logger.info("사용 가능한 WebDriver가 없어 대기합니다...")
+        try:
+            return self._pool.get(timeout=timeout)
+        except queue.Empty:
+            raise TimeoutError("WebDriverPool: 드라이버 대여가 제한 시간 내에 불가했습니다.")

Comment on lines +18 to +44
def _create_driver(self):
try:
chrome_options = Options()

chrome_options.binary_location = "/opt/chrome-linux64/chrome"

# Headless & Lambda 옵션 추가
chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--window-size=1280x1696")
chrome_options.add_argument("--single-process")
chrome_options.add_argument("--disable-software-rasterizer")
chrome_options.add_argument("--no-zygote")
chrome_options.add_argument("--remote-debugging-port=9222")
chrome_options.add_argument('--user-data-dir=/tmp/user-data')
chrome_options.add_argument('--data-path=/tmp/data-path')
chrome_options.add_argument('--disk-cache-dir=/tmp/cache-dir')

# User-Agent 설정
chrome_options.add_argument(
"user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")

service = Service(executable_path="/opt/chromedriver-linux64/chromedriver")

return webdriver.Chrome(service=service, options=chrome_options)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

드라이버 생성: 불안정 플래그 제거, 경로/UA 하드코드 제거, ENV 기반 설정으로 전환

  • --single-process 제거(불안정).
  • 바이너리/드라이버/UA/캐시 경로를 ENV로 지정 가능하게.
  • Lambda에 맞게 headless/new·옵션 정리.
     def _create_driver(self):
         try:
             chrome_options = Options()
-
-            chrome_options.binary_location = "/opt/chrome-linux64/chrome"
+            # ENV 기반 경로/설정
+            binary = os.getenv("CHROME_BINARY_PATH", "/opt/chrome-linux64/chrome")
+            driver_path = os.getenv("CHROMEDRIVER_PATH", "/opt/chromedriver-linux64/chromedriver")
+            user_data_dir = os.getenv("CHROME_USER_DATA_DIR", "/tmp/user-data")
+            data_path = os.getenv("CHROME_DATA_PATH", "/tmp/data-path")
+            cache_dir = os.getenv("CHROME_CACHE_DIR", "/tmp/cache-dir")
+            user_agent = os.getenv("CHROME_USER_AGENT")
+            if binary:
+                chrome_options.binary_location = binary
 
             # Headless & Lambda 옵션 추가
-            chrome_options.add_argument("--headless")
+            chrome_options.add_argument("--headless=new")
             chrome_options.add_argument("--no-sandbox")
             chrome_options.add_argument("--disable-dev-shm-usage")
             chrome_options.add_argument("--disable-gpu")
             chrome_options.add_argument("--window-size=1280x1696")
-            chrome_options.add_argument("--single-process")
             chrome_options.add_argument("--disable-software-rasterizer")
             chrome_options.add_argument("--no-zygote")
             chrome_options.add_argument("--remote-debugging-port=9222")
-            chrome_options.add_argument('--user-data-dir=/tmp/user-data')
-            chrome_options.add_argument('--data-path=/tmp/data-path')
-            chrome_options.add_argument('--disk-cache-dir=/tmp/cache-dir')
+            chrome_options.add_argument(f'--user-data-dir={user_data_dir}')
+            chrome_options.add_argument(f'--data-path={data_path}')
+            chrome_options.add_argument(f'--disk-cache-dir={cache_dir}')
 
             # User-Agent 설정
-            chrome_options.add_argument(
-                "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
+            if user_agent:
+                chrome_options.add_argument(f"user-agent={user_agent}")
 
-            service = Service(executable_path="/opt/chromedriver-linux64/chromedriver")
+            service = Service(executable_path=driver_path)
 
             return webdriver.Chrome(service=service, options=chrome_options)
-        except Exception as e:
-            logger.info(f"WebDriver 생성 실패: {e}")
+        except (WebDriverException, OSError) as e:
+            logger.error(f"WebDriver 생성 실패: {e}")
             return None

🏁 Script executed:

#!/bin/bash
# driver_pool.py 의 상단 import 및 예외 처리용 타입 확인
sed -n '1,50p' app/services/driver_pool.py
rg -n "import os" -g "app/services/driver_pool.py"
rg -n "WebDriverException" -g "app/services/driver_pool.py"
rg -n "Service" -g "app/services/driver_pool.py"
rg -n "Options" -g "app/services/driver_pool.py"

Length of output: 2318


드라이버 생성: ENV 기반 설정 적용 및 불안정 플래그 제거 (필수 리팩터)

  • 위치: app/services/driver_pool.py (약 18–44행)
  • 변경 요구:
    • CHROME_BINARY_PATH, CHROMEDRIVER_PATH, CHROME_USER_DATA_DIR, CHROME_DATA_PATH, CHROME_CACHE_DIR, CHROME_USER_AGENT 등으로 하드코드된 바이너리/드라이버/디렉토리/UA 대체(기본값은 기존 값 유지).
    • chrome 옵션에서 --single-process 제거, --headless → --headless=new로 교체; 나머지 Lambda 친화 옵션은 필요 시 유지 검토.
    • 파일 상단에 import os 및 from selenium.common.exceptions import WebDriverException 추가. except Exception → except (WebDriverException, OSError)로 제한하고 logger.info → logger.error로 변경.
    • Service(executable_path=...)를 환경변수(CHROMEDRIVER_PATH) 사용으로 대체.
  • 검증 요구: 변경 후 Lambda/컨테이너 환경에서 Chrome/Chromedriver 경로·권한과 headless 옵션 호환성 확인.
🤖 Prompt for AI Agents
In app/services/driver_pool.py around lines 18 to 44, the driver creation
currently uses many hardcoded paths and unstable flags; update it to read
CHROME_BINARY_PATH, CHROMEDRIVER_PATH, CHROME_USER_DATA_DIR, CHROME_DATA_PATH,
CHROME_CACHE_DIR, and CHROME_USER_AGENT from environment variables with the
current hardcoded values as defaults, replace
chrome_options.add_argument("--headless") with "--headless=new", remove the
"--single-process" flag, keep or review other Lambda-friendly flags as needed,
use os.environ.get(...) for each path/UA, replace Service(executable_path="...")
with Service(executable_path=CHROMEDRIVER_PATH), add imports "import os" and
"from selenium.common.exceptions import WebDriverException" at the top, change
the bare except Exception to except (WebDriverException, OSError) and switch the
logger.info call in the except block to logger.error, and then validate
Chrome/Chromedriver paths, permissions and headless compatibility in
Lambda/container environments after making these changes.

@bbbang105 bbbang105 added 📄 docs 문서 추가 및 수정 🚨 fix 버그 수정 / 에러 해결 labels Sep 22, 2025
@bbbang105 bbbang105 merged commit 097f94b into develop Sep 22, 2025
2 checks passed
@bbbang105 bbbang105 deleted the feature/#15 branch September 22, 2025 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📄 docs 문서 추가 및 수정 🚀 feat 새로운 기능 추가 / 일부 코드 추가 / 일부 코드 수정 (리팩토링과 구분) / 디자인 요소 수정 🚨 fix 버그 수정 / 에러 해결 😵‍💫 sangho 상호 Issue or PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] : FastAPI로 마이그레이션한다

2 participants