AWS Cloud School 13기 31일차

Forever 김·2026년 2월 6일

AWS Cloud School

목록 보기
28/97

오늘은 강사 jeff의 마지막 날이자 미니 프로젝트의 최종장이었다.
오늘의 velog는 프로젝트를 기억하는 블로그로 하도록 하자.
먼저 저희가 진행한 프로젝트의 github 주소입니다.
https://github.com/no-easy-days/aws13th-team2-news-scraping.git

저희가 진행한 프로젝트는 테크 과학 뉴스 검색 서비스이다.
여기서 내가 맡은 부분은 검색 기능 구현이다.


1. 검색 유틸리티 (utils/search_utils.py)

  • normalize(text: str) -> str:
    • 목적: 텍스트를 소문자로 변환하고 공백을 표준화하여 비교 준비를 합니다.
    • 구현: 입력 text를 소문자로 변환하고, 정규 표현식을 사용하여 여러 공백 문자를 단일 공백으로 바꾸고 앞뒤 공백을 제거한다.
  • make_ngram(text: str, n=2):
    • 목적: 주어진 텍스트에서 n-gram(n개 문자의 시퀀스)을 생성합니다. 이는 유사도 비교를 위해 텍스트를 나타내는 데 사용됩니다. 기본 n은 2(빅그램)이다.
    • 구현: 먼저 normalize 함수를 사용하여 입력 text를 정규화한다. 그런 다음 정규화된 텍스트를 반복하며 길이 n의 부분 문자열 목록을 만듭니다. 예를 들어, n=2일 때 "apple"은 ['ap', 'pp', 'pl','le']가 된다.
  • get_similarity(query: str, target: str):
    • 목적: n-gram을 사용하여 검색 query와 target 문자열(예: 기사 제목) 간의 자카드 유사도를 계산한다.

여기서 자카드 유사도란?

두 집합 사이의 유사도를 측정하는 지표로, 두 집합의 교집합 크기를 합집합 크기로 나눈 값 입니다. 0과 1 사이의 값을 가지며, 1에 가까울수록 두 집합이 매우 유사함을 의미합니다.

  • 구현: make_ngram을 사용하여 query와 target 모두에 대한 n-gram을 생성합니다. 이 n-gram은 각각 세트(search_bar 및 title)로 변환됩니다. 둘 중 하나의 세트가 비어 있으면 0.0을 반환한다.(유사도 없음). 두 세트의 교집합(intersection = search_bar & title)을 계산하여 공통 n-gram을 나타냅니다. 두 세트의 합집합(union = search_bar | title)을 계산하여 두 텍스트의 모든 고유 n-gram을 나타낸다.
  • 자카드 유사도는 len(intersection) / len(union)으로 계산된다. 이 비율은 공유된 n-gram을 기반으로 두 텍스트가 얼마나 유사한지를 0에서 1 사이의 점수로 나타냅니다.

2. 기사 검색 엔드포인트 (routers/articles.py)

  • @router.get("/articles"):
    • 목적: 기사 검색 요청을 처리하는 GET 엔드포인트 /articles를 정의합니다.
    • 의존성:
      • keyword: str = Query(..., description="검색 키워드"): 클라이언트로부터 keyword 쿼리 매개변수를 예상합니다.
      • db: Session = Depends(get_db): 데이터베이스와 상호 작용하기 위한 데이터베이스 세션을 주입합니다.
  • 입력 유효성 검사:
    • keyword에서 공백을 제거합니다 (clean_keyword).
    • clean_keyword가 비어 있으면 400 Bad Request 오류를 반환합니다.
    • clean_keyword가 2자 미만이면 400 Bad Request 오류를 반환합니다.
  • 기사 검색:
    • 데이터베이스에서 모든 기사를 검색합니다 (db.query(Article).all()).
  • 유사도 계산 및 필터링:
    • 검색된 모든 db_articles를 반복합니다.
    • 각 article에 대해 get_similarity(keyword, article.title)를 호출하여 score를 계산합니다. 이는 검색 키워드와 기사의 제목을 비교합니다.
    • score가 0.01 이상이면 기사의 세부 정보(ID, 제목, URL, 설명, 게시일, 썸네일 URL, 생성일)가 계산된 data_score와 함께 results 목록에 추가됩니다. data_score는 소수점 둘째 자리까지 반올림됩니다.
  • 정렬:
    • results 목록은 data_score를 기준으로 내림차순으로 정렬되어 가장 관련성이 높은 기사가 먼저 나타납니다.
  • 응답 처리:
    • 필터링 후 results가 비어 있으면 결과가 없음을 나타내는 메시지와 함께 NOT_FOUND 상태를 반환합니다.
    • 그렇지 않으면 data(정렬된 기사 목록)와 찾은 기사의 total_count를 포함하는 success 상태를 반환합니다.

요약

  1. 사용자가 검색 keyword를 /articles 엔드포인트로 보냅니다.
  2. 엔드포인트는 키워드를 유효성 검사합니다(비어 있지 않고 최소 2자여야 함).
  3. 데이터베이스에서 모든 기사를 검색합니다.
  4. 각 기사에 대해 빅그램 기반 자카드 유사도 알고리즘을 사용하여 사용자 keyword와 기사 title 간의 유사도 점수를 계산합니다.
  5. 유사도 점수가 0.01 이상인 기사는 일치하는 것으로 간주됩니다.
  6. 일치하는 기사는 유사도 점수 내림차순으로 정렬됩니다.

데이터베이스 연결 (database.py)

database.py 파일은 SQLAlchemy를 사용하여 데이터베이스 연결을 설정하는 역할을 합니다.

  1. 설정 로딩:
    • from settings import settings: settings.py에서 settings를 가져옵니다. 이 settings 객체는 DATABASE_URL 및 기타 구성 매개변수를 포함할 것으로 예상됩니다. 이는 구성을 중앙 집중화하고 다른
      환경(예: 개발, 프로덕션)을 쉽게 관리할 수 있도록 합니다.
    • DATABASE_URL = settings.database_url: 로드된 설정에서 데이터베이스 연결 문자열을 검색합니다. 이 문자열에는 일반적으로 데이터베이스 유형, 자격 증명, 호스트, 포트 및 데이터베이스 이름이
      포함됩니다(예: SQLite의 경우 sqlite:///./sql_app.db, PostgreSQL의 경우 postgresql://user:password@host:port/dbname).
  1. 엔진 생성:
    • engine = create_engine(DATABASE_URL): 이 줄은 SQLAlchemy Engine 인스턴스를 생성합니다. Engine은 모든 SQLAlchemy 애플리케이션의 시작점이며, 데이터베이스에 대한 연결을 처리하는 역할을 합니다.
      DATABASE_URL을 해석하여 적절한 데이터베이스에 연결합니다.
  1. 세션 로컬 및 기본 선언:
    • SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine): SessionLocal 클래스를 생성합니다. SessionLocal의 인스턴스는 실제 데이터베이스 "세션"이 됩니다.
      • autocommit=False: 변경 사항이 데이터베이스에 자동으로 커밋되지 않도록 합니다. 변경 사항을 저장하려면 명시적으로 db.commit()을 호출해야 합니다.
      • autoflush=False: 쿼리 실행 전에 변경 사항이 데이터베이스에 자동으로 플러시되는 것을 비활성화합니다.
      • bind=engine: 이 세션 생성기를 이전에 생성된 engine에 바인딩하여 SessionLocal에서 생성된 모든 세션이 이 데이터베이스 연결을 사용하도록 합니다.
    • Base = declarative_base(): ORM 모델의 기본 클래스를 생성합니다. 애플리케이션의 모든 SQLAlchemy 모델(테이블)은 이 Base를 상속합니다. 이를 통해 SQLAlchemy는 모델을 알고 데이터베이스 테이블에
      매핑할 수 있습니다.
  1. 데이터베이스 세션 종속성 (get_db):
    • def get_db():: 이 함수는 FastAPI 종속성으로 사용되도록 설계되었습니다.
    • db = SessionLocal(): 새 SQLAlchemy 세션을 생성합니다.
    • try...finally: 요청 처리 중에 오류가 발생하더라도 데이터베이스 세션(db)이 항상 닫히도록 try...finally 블록을 사용합니다. 이는 데이터베이스 연결을 연결 풀로 해제하고 리소스 누수를 방지하는 데
      중요합니다.
    • yield db: yield 키워드는 이 함수를 제너레이터 함수로 만듭니다. FastAPI는 이를 사용하여 경로 핸들러에 db 세션을 제공하며, yield 이후의 코드는 응답이 전달되거나(예외가 발생하더라도)
      db.close()가 호출되도록 실행됩니다.

요약하자면, database.py는 핵심 SQLAlchemy 구성 요소를 설정합니다: 연결을 위한 Engine, 데이터베이스 상호 작용 세션 생성을 위한 SessionLocal 클래스, ORM 모델 정의를 위한 Base. get_db 함수는 FastAPI
경로가 데이터베이스 세션을 얻고 관리하는 깔끔한 종속성 주입 방식을 제공합니다.


웹 크롤러 (crawler/mk_news.py)

이 스크립트는 뉴스 기사를 크롤링하도록 설계되었습니다. HTTP 요청을 위해 requests를 사용하고 HTML 콘텐츠 파싱을 위해 BeautifulSoup를 사용합니다. 또한 데이터 유효성 검사를 위해
schemas.article.ArticleCreate를 활용하고 데이터베이스 작업을 위해 repositories.article_repository.ArticleRepository를 활용합니다.

주요 구성 요소 및 기능:

  1. crawl_mk_news(days: int = 365) -> list[ArticleCreate]:
    • 목적: 크롤링 프로세스를 총괄하는 메인 함수입니다. 뉴스 섹션에서 기사를 가져옵니다.
    • 인수:
      • days: 오늘로부터 몇 일 전까지의 기사를 크롤링할지 지정합니다(기본값은 365일, 즉 1년).
    • 헤더 및 쿠키:
      • 웹 브라우저 요청을 모방하기 위해 User-Agent 헤더를 사용하며, 이는 차단을 피하는 데 도움이 됩니다.
      • 특정 PCID 및 SCOUTER 쿠키를 사용합니다.
    • 페이지네이션 및 루프:
      • URL의 page 매개변수를 증가시키면서 while 루프를 사용하여 뉴스 기사 페이지를 반복합니다.
      • 루프는 페이지에서 더 이상 기사를 찾을 수 없거나 cutoff_date보다 오래된 기사를 발견할 때까지 계속됩니다(stop_crawling이 True가 됨).
    • 오류 처리 및 재시도:
      • 잠재적인 네트워크 문제 또는 임시 서버 오류를 처리하기 위해 requests.get 호출에 대한 재시도 메커니즘(최대 3회)을 포함합니다.
      • time.sleep() 및 random.uniform()을 사용하여 요청 사이에 지연을 도입하여 공격적인 크롤링을 방지하고 차단될 가능성을 줄입니다.
    • HTML 파싱:
      • BeautifulSoup(response.text, "html.parser")는 각 페이지의 HTML 콘텐츠를 파싱하는 데 사용됩니다.
      • soup.select("li.article_list")를 사용하여 기사 목록 항목을 선택합니다.
    • 기사 파싱 및 필터링:
      • 각 기사 HTML 요소에 대해 _parse_article을 호출하여 관련 데이터를 추출합니다.
      • published_at 날짜를 기준으로 기사를 필터링하며, cutoff_date보다 오래된 기사가 발견되면 크롤링을 중단합니다. 이는 지정된 days 범위 내의 기사만 수집하도록 보장합니다.
    • 로깅:
      • logging을 사용하여 크롤링 진행 상황 및 발생한 오류를 보고합니다.
  1. _parse_article(article) -> ArticleCreate | None:
    • 목적: 단일 HTML 기사 요소에서 특정 데이터 포인트를 추출하는 도우미 함수입니다.
    • 추출:
      • BeautifulSoup 셀렉터(select_one)를 사용하여 링크(a.news_item), 제목(h4), 설명(p.art_desc), 게시 시간(div.time_area span), 썸네일 이미지(div.list_thumb img)를 찾습니다.
    • 날짜 파싱:
      • time_area의 date_text를 datetime.strptime을 사용하여 datetime 객체로 파싱하려고 시도합니다.
    • 데이터 유효성 검사 및 객체 생성:
      • 필수 요소(링크, 제목, 게시 날짜)가 있는지 확인하기 위한 기본적인 검사를 수행합니다.
      • 추출된 데이터로 ArticleCreate 객체(schemas.article에서)를 구성합니다. 이 단계는 ArticleCreate Pydantic 모델에 대해 데이터를 암시적으로 유효성 검사합니다.
      • 필수 데이터가 없거나 Pydantic 유효성 검사가 실패하면 오류를 로깅하고 None을 반환합니다.
  1. save_to_db(articles: list[ArticleCreate]) -> tuple[int, int]:
    • 목적: ArticleCreate 객체 목록을 받아 데이터베이스에 저장합니다.
    • 데이터베이스 상호 작용:
      • SessionLocal()을 사용하여 새 데이터베이스 세션을 생성합니다.
      • 세션으로 ArticleRepository를 인스턴스화합니다.
      • repo.bulk_create(articles)를 호출하여 여러 기사를 데이터베이스에 효율적으로 삽입합니다.
      • finally 블록을 사용하여 데이터베이스 세션이 닫히도록 합니다.

요약하자면, 웹 크롤러는 다음과 같습니다.

  • 웹 페이지를 가져오고 파싱하기 위해 requests 및 BeautifulSoup를 사용합니다.
  • 지정된 days보다 오래된 기사에 도달하면 중지하면서 시간을 거슬러 페이지를 크롤링합니다.
  • 견고하고 정중하게 작동하기 위해 재시도 로직과 지연을 포함합니다.
  • 개별 기사 세부 정보를 구조화된 ArticleCreate Pydantic 모델로 파싱합니다.
  • 유효성 검사를 거친 기사를 대량 삽입을 위해 ArticleRepository를 사용하여 데이터베이스에 저장합니다.

전반적인 아키텍처, 데이터베이스 연결 및 애플리케이션 초기화 (main.py)

main.py 파일은 FastAPI 애플리케이션의 진입점 역할을 하며, 웹 서버 설정, 데이터베이스 통합, 웹 크롤러 스케줄링 및 미들웨어 구성을 담당합니다.

  1. FastAPI 애플리케이션 인스턴스:
    • app = FastAPI(lifespan=lifespan): FastAPI 애플리케이션을 초기화합니다. lifespan 컨텍스트 관리자는 시작 및 종료 이벤트를 관리하기 위해 여기에 전달됩니다.
  2. CORS 미들웨어:
    • app.add_middleware(CORSMiddleware, ...): CORS(Cross-Origin Resource Sharing)를 구성합니다. 이는 프런트엔드(예: localhost:5173에서 실행)가 백엔드 API에 요청해야 하는 웹 애플리케이션에
      필수적입니다.
      • allow_origins: 요청을 보낼 수 있는 허용된 출처를 지정합니다(이 경우 프런트엔드의 http://localhost:5173http://127.0.0.1:5173).
      • allow_credentials=True: 자격 증명(예: 쿠키 또는 HTTP 인증)이 교차 출처 요청과 함께 전송될 수 있도록 허용합니다.
      • allow_methods=["*"], allow_headers=["*"]: 모든 HTTP 메서드 및 헤더를 허용하며, 이는 개발 시 일반적이지만 프로덕션에서는 제한될 수 있습니다.
  3. 데이터베이스 테이블 생성:
    • Base.metadata.create_all(bind=engine): 데이터베이스 설정에 중요한 줄입니다.
      • Base(database.py에서)는 자신을 상속하는 모든 ORM 모델(Article, Bookmark, User)에 대해 알고 있습니다.
      • metadata는 테이블 객체의 컬렉션입니다.
      • create_all(bind=engine)은 SQLAlchemy에 engine이 연결된 데이터베이스에 정의된 모든 테이블을 아직 존재하지 않는 경우 생성하도록 지시합니다. 이는 일반적으로 애플리케이션 시작 또는
        마이그레이션 중에 한 번 실행됩니다.
  4. 라우터 포함:
    • app.include_router(articles.router): routers/articles.py에 정의된 API 경로를 포함합니다. 이는 /articles(검색용)와 같은 엔드포인트가 주요 FastAPI 애플리케이션의 일부가 됨을 의미합니다.
    • app.include_router(bookmark_router): routers/bookmark.py에 정의된 API 경로를 포함합니다. 이는 북마크 관리와 관련된 엔드포인트를 활성화합니다.
  1. 웹 크롤러 스케줄링 (Lifespan 컨텍스트 관리자):
    • @asynccontextmanager async def lifespan(app: FastAPI):: FastAPI가 애플리케이션 시작 및 종료 시 코드를 실행하는 데 사용하는 비동기 컨텍스트 관리자를 정의합니다.
    • 시작 (scheduler.add_job(...), scheduler.start()):
      • apscheduler의 BackgroundScheduler가 초기화됩니다.
      • scheduler.add_job(run_crawler, "interval", minutes=30, next_run_time=datetime.now()): run_crawler 함수를 30분마다 실행하도록 스케줄링합니다. next_run_time=datetime.now()는 애플리케이션이
        처음 시작될 때 즉시 실행되도록 합니다.
      • scheduler.start(): 백그라운드 스케줄러 스레드를 시작합니다.
    • 종료 (scheduler.shutdown()):
      • yield: 스케줄러가 시작된 후 애플리케이션이 진행됩니다.
      • 애플리케이션이 종료되면 yield 이후의 코드가 실행됩니다: scheduler.shutdown()은 백그라운드 크롤링 프로세스를 정상적으로 중지합니다.
    • run_crawler() 함수:
      • 이 함수는 스케줄러에 의해 호출됩니다.
      • crawl_mk_news(days=1)을 호출하여 지난 하루 동안의 기사를 가져옵니다.
      • 그런 다음 save_to_db(articles)를 호출하여 크롤링된 기사를 데이터베이스에 저장합니다.
  • 로깅은 성공 및 중복 횟수를 추적하는 데 사용됩니다.

    데이터베이스 상호 작용 흐름:

  • 연결 설정: database.py는 settings.py의 DATABASE_URL을 사용하여 engine 및 SessionLocal을 설정합니다.
  • 스키마 생성: main.py는 시작 시 Base.metadata.create_all(bind=engine)을 호출하여 ORM 모델을 기반으로 데이터베이스 테이블(articles, bookmarks, users)이 생성되었는지 확인합니다.
  • 종속성 주입: 데이터베이스 액세스가 필요한 FastAPI 경로(예: routers/articles.py 및 routers/bookmark.py의)는 get_db(database.py에서)에 대한 종속성을 선언합니다.
  • 세션 관리: get_db는 경로 핸들러에 SQLAlchemy Session 객체(db)를 제공합니다. 이 세션은 요청 시작 시 자동으로 열리고 끝날 때 닫히므로 적절한 리소스 관리가 보장됩니다.
  • 리포지토리 사용: 경로 핸들러 내에서 제공된 db 세션을 사용하여 ArticleRepository 또는 BookmarkRepository 인스턴스가 생성되어 구조화되고 추상화된 데이터베이스 작업을 수행할 수 있습니다.
  • 크롤러 데이터베이스 상호 작용: run_crawler 함수도 save_to_db를 사용하며, 이는 다시 ArticleRepository 및 SessionLocal을 사용하여 새로 크롤링된 기사를 저장합니다.

전반적인 아키텍처 다이어그램 :

    +-------------------+       +-------------------+
    |     프런트엔드      | <---> |   FastAPI 앱    |
    | (예: React/Vite)  |       |    (main.py)    |
    +-------------------+       +--------^----------+
                                         |
    +------------------------------------+------------------------------------+
    | API 경로 (routers/)              | 스케줄러 (apscheduler)          |
    | - /articles (articles.py)          | - crawl_mk_news 실행 (30분마다)  |
    | - /bookmarks (bookmark.py)         | - save_to_db 호출                 |
    +------------------------------------+------------------------------------+
           |                                       |
           v                                       v
    +-------------------+                  +-------------------+
    |   리포지토리      |                  |  웹 크롤러        |
    | (article_repo.py, |                  | (mk_news.py)      |
    | bookmark_repo.py) | <--------------> |                   |
    +-------------------+                  +-------------------+
           ^                                       ^
           | 종속성 주입 (get_db)                     |
           v                                       v
    +-------------------+       +-------------------+
    | SQLAlchemy ORM    | <---> |   데이터베이스    |
    | (models/, Base)   |       | (SQLite/Postgres) |
    +-------------------+       +-------------------+
           ^
           | 구성
           v
    +-------------------+
    |     설정          |
    |    (settings.py)  |
    +-------------------+
profile
나를 한줄로

0개의 댓글