Post

데이터 수집 파이프라인 구축

데이터 수집 파이프라인 구축

졸업 프로젝트로 AI기반 평생교육 커리큘럼 추천 서비스 큐릭(Kuriq)을 개발하고 있다.

간단하게 서비스를 소개하자면, 국내 주요 공공 교육 플랫폼에 흩어져 있는 강좌를 하나의 인터페이스에서 통합 탐색할 수 있게 하고, 사용자가 자연어로 입력한 관심사/학습 목표/현재 수준등을 바탕으로 RAG 기반으로 개인에게 최적화된 주간 학습 로드맵을 자동으로 생성해주는게 주 기능이다.

그렇기에 서비스 구현을 위해서는 강좌 데이터를 벡터 DB에 적재하는 파이프라인이 필요하다!! 그래서 내가 구현한 공공 교육 데이터를 수집하고 전처리한 뒤 DB에 적재하기까지의 과정을 정리해봤다.

1. 환경 설정

의존성 충돌 방지, 환경의 동일성 유지, 뭉서보다 시스템의 깔끔한 유지등을 위해… 가상환경을 세팅하고 그 위에서 작업했다.

  • 가상환경이란? 컴퓨터 전체 환경에 라이브러리를 설치하는 게 아니라, 특정 프로젝트만을 위한 독립적인 실행 환경을 구축하는 것이다.

가상환경 설치 및 의존성 설치

1
2
3
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

requirements.txt

1
2
3
4
requests==2.31.0
chromadb>=0.5.0
sentence-transformers==2.6.1
python-dotenv==1.0.0
  • requirements.txt 란, 프로젝트를 실행하기 위해서 필요한 패키지의 리스트이다. (의존성 관리를 위한 문서)

사용 라이브러리는 네 가지로 최소화했다. 임베딩 모델로 현재는 OpenAI API 대신 sentence-transformers의 로컬 모델을 사용하기로 했는데, 후에 제공신청을 한 API에 대해서 제공여부가 결정된 후 여러 모델(OpenAI API, Qwen 등…)을 사용해본 후 가장 성능이 좋은 모델을 사용할 예정이다.

환경변수 (.env)

1
2
3
4
5
6
7
DATA_GO_KR_API_KEY=공공데이터포털_API_키
KOCW_API_KEY=KOCW_API_키

CHROMA_MODE=local        # local | server
CHROMA_PATH=./chroma_db  # 로컬 모드일 때 저장 경로
CHROMA_HOST=localhost    # 서버 모드일 때
CHROMA_PORT=8000

공공데이터 API를 크롤링하여 정보를 가져오므로 공공데이터 포털 API키와 KOCW API키를 넣어야한다.

아직 서버를 배포하지 않아서 로컬에 저장해야하는데, 나중에 서버를 띄운 후 코드의 일부분을 수정해야하는 것을 막기 위해 CHROMA_MODE 변수로 local모드와 server모드를 나눴다. 나중에 서버 배포 후에는 CHROMA_MODE를 server로 바꾸고 서버 주소만 입력하면 DB에 연결할 수 있다.

2. 데이터 모델 설정

우리 서비스는 우선적으로 강좌 정보 API를 제공하는 국가평생교육 K-MOOC, 서울시 평생학습포털, KOCW의 강좌 정보만을 가져온다. (온국민평생배움터 강좌 정보 API 제공신청을 한 상태.) 이후, 서비스를 확장하게 되면 Gseek등의 좀 더 다양한 국가 평생교육 사이트의 강좌를 추가할 예정이다.

따라서 API 크롤링을 진행해야한다. 이에 앞서 모든 수집기가 공통으로 사용하는 Course 데이터클래스를 정의했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@dataclass
class Course:
    id: str                          # 플랫폼 내 고유 ID
    title: str                       # 강의명
    institution: str                 # 운영 기관
    platform: str                    # 플랫폼명 (K-MOOC, KOCW 등)
    category: str                    # 원본 카테고리
    std_category: str = "기타"        # 큐릭 표준 카테고리 (전처리 후 채워짐)
    description: str = ""            # 강의 소개
    duration: str = ""               # 학습 기간 / 시간
    url: str = ""                    # 수강 신청 URL
    is_free: bool = True             # 무료 여부
    level: str = ""                  # 난이도 (있는 경우)

    def embed_text(self) -> str:
        """임베딩용 텍스트 생성: 강의명 + 표준카테고리 + 기관 + 소개 200자"""
        snippet = self.description[:200] if self.description else ""
        return f"{self.title} [{self.std_category}] {self.institution} {snippet}".strip()

    def to_metadata(self) -> dict:
        """ChromaDB metadata 딕셔너리"""
        return {
            "title": self.title,
            "institution": self.institution,
            "platform": self.platform,
            "category": self.std_category,
            "duration": self.duration,
            "url": self.url,
            "is_free": self.is_free,
            "level": self.level,
        }

    def chroma_id(self) -> str:
        """ChromaDB document ID — 플랫폼_ID"""
        return f"{self.platform}_{self.id}"
  • 데이터클래스(@dataclass)란, 데이터를 저장하는 클래스를 만들 때 보일러플레이트 코드를 자동으로 생성해주는 데코레이터이다. 데이터 구조를 정의하는 간단한 경우에는 일반 클래스보다 가독성이 높고 유지보수가 편리하다.
  • embed_text(self)메서드로 벡터 DB에 저장하기 위해 임베딩용 텍스트를 생성한다. (강의명, 카테고리, 기관, 소개글을 합쳐서 하나의 문장으로 만듦) 강의 전체 내용을 전부 넣으면 검색 품질이 저하될 수 있기에 소개 앞 200자만 사용하여 토큰 수를 줄인다.
  • ChromaDB는 실제 텍스트 외에 메타데이터를 딕셔너리 형태로 저장하기에, to_metadata(self)메서드로 메타데이터를 가공한다.
  • 강의를 구별하기 위해 플랫폼 이름과 ID를 조합하여 DB에서 각 강의를 식별할 고유 키를 만든다.

3. 수집기 설계

This post is licensed under CC BY 4.0 by the author.