[프로젝트] ???: 오늘 뭐 하셨나요?

sookyoung.k·2025년 1월 7일
0

🌿 교보DTS TIL

목록 보기
38/39
post-thumbnail

절망이요...

잘 되는 날은 '오~ 희망이 좀 보이는걸~?' 하다가도 바로 다음날이면 주구장창 실패를 겪으며 '이 길이 맞는가...' 고민하며 널뛰기를 하는 프로젝트 기간이다... 오늘은 후자의 경우였다.

딜레이가 되던 프론트엔드 문제는 해결이 되었다. 하지만 내가 나눠놓은 코드는 일단은 사용하지 않게 되었다. 시간이 나면 점진적으로 바꿔나가긴 하겠지만, 시간도 없는데 굳이 기능하는 코드를 건드려야하나 싶긴 하다. 일단 상황을 보기로. 하지만 어제 하루를 썼는데... 괜한 짓이란 건 없지만 속이 좀 쓰리긴 했다. 쨋든, 완성은 한 거니깐 ㄱㅊ.

하지만 더 큰 절망이 터짐...

🚨 상황

원래 우리는 관계형 데이터베이스를 사용할 생각으로 erd를 설계했었다. 하지만 여러 이유 (그중 가장 큰 건 rds 가격이 너무 비싸다는 것이었음. 비용적 측면에서...)로 DynamoDB를 사용하게 되었다. 그러다보니 기존의 스키마가 NoSQL에 그대로 적용하기에는 성능적인 측면에서 유리할 게 하나도 없다는 것을 깨닫고 결국 거기에 맞춰서 테이블 구성을 조금 변경하게 되었다.


나름 고심해서 짜고... 팀원들 의견도 물어봄... ERD는 아직도 수정 안 햇서용

그리고 뭔가 DynamoDB 바로 연결해서 테스트하는게 무서운거임... 왜 그랬을까? 걍 해보지... 왜냐면 일단 NoSQL은 RDS에 비해서 너무나 낯설었고...! 스키마 구조가 이게 맞는지, 여러모로 확신이 들지 않았기 때문이다. 그런데 AWS에 바로 올려...? 너무 무서웠음.

그래서 내가 배운 NoSQL은 뭐다? MongoDB다~ 이거도 NoSQL이잖아요? 이거로 로컬에서 테스트하고 성공하면 DynamoDB에 바로 마이그레이션 하면 되겠지? 헤헿~~ 이렇게 생각을 하고 열심히 설계를 했음.

그런데 말입니다...! NoSQL이라고 해서 다 같은 게 아니었더랍니다... 알면서 몰랐음. 걍 될 줄 알았음... 문득 AI가 그러는 것임. AWS와 MongoDB를 꼭 함께 써야하니? 얜 너무 이상했을 것임. 굳이 왜...? 나는 굳이 왜 스러운 일을 하고 있던 것이다...

같은 NoSQL이라고 같은 데이터베이스가 아닙니다.

AWS DynamoDB와 MongoDB는 서로 다른 특성을 가진 데이터베이스다

DynamoDB는 Key-Value 스토어이고 MongoDB는 문서형 데이터베이스다. 이는 무엇을 의미하냐면...

💡 스키마 설계, 쿼리 패턴이 매우 다르다는 것이다.
💡 성능도 다르다.

  • DynamoDB는 프로비저닝된 처리량 모델을 사용
  • MongoDB는 다른 방식의 인덱싱과 쿼리 최적화를 사용
  • 로컬 테스트 결과가 실제 운영 환경의 성능을 정확히 반영하지 못할 수 있음

그런 것도 모르고 난... 미친 코드 설계를 하고 있었던 것이다. 하... 이래서 사람이 배워야 한다는 것을 깨달았다.

그렇담 테스트 할 때 뭘 쓰면 좋을까?
👉🏻 DynamoDB Local을 사용할 것 ! DynamoDB의 로컬 버전이다. Docker 이미지로 제공되어서 로컬 개발 환경에서 실제 DynamoDB와 거의 동일한 기능을 테스트 할 수 있다고 한다. 이건... 다음에 알아보자

하지만 어떡함... DynamoDB로 로컬 테스트를 거의 끝냈는걸. RDS만 알던 난 바보 맹쵕이였던 것이다. 그래도 뭐 우째... 이미 한 거... 삭제하기 전에 아까우니 벨로그에 작성이라도 해보려고 한다.

그래서 오늘의 주제... 크롤러와 DynamoDB 연결하기 ^^...

🗂️ 서비스 구조

server/
├── aws_service/                    # AWS 서비스 관련 모듈
│   ├── __init__.py
│   ├── config.py                  # 환경 변수 및 설정값 관리
│   ├── base.py                    # 기본 클래스 정의 (BaseRepository 등)
│   ├── factory.py                 # Repository 인스턴스 생성 팩토리
│   ├── exceptions.py              # 커스텀 예외 클래스 정의
│   └── services/                  # 각 서비스별 구현체
│       ├── __init__.py
│       ├── common/                # 공통 모듈
│       │   ├── __init__.py
│       │   ├── enums.py          # 열거형 상수 정의
│       │   └── constants.py       # 일반 상수값 정의
│       └── mongodb/              # MongoDB 관련 구현체
│       │   ├── __init__.py
│       │   ├── table_schemas.py  # MongoDB 컬렉션 스키마 정의
│       │   ├── setup.py          # MongoDB 초기 설정 및 인덱스 생성
│       │   ├── repository.py     # MongoDB Repository 구현체
│       │   └── models.py         # MongoDB 모델 클래스 정의
│       └── dynamodb/            # DynamoDB 관련 구현체
│            ├── __init__.py
│            ├── table_schemas.py  # DynamoDB 테이블 스키마 정의
│            ├── setup.py          # DynamoDB 테이블 생성 및 설정
│            ├── repository.py     # DynamoDB Repository 구현체
│            └── models.py         # DynamoDB 모델 클래스 정의 
│
├── crawler/                      # 크롤러 서비스
│   ├── __init__.py
│   ├── main.py                  # 크롤러 메인 애플리케이션
│   ├── base/                    # 크롤러 기본 클래스
│   │   ├── __init__.py
│   │   └── base_crawler.py      # 크롤러 공통 기능 정의
│   ├── crawlers/                # 각 사이트별 크롤러 구현체
│   │   ├── __init__.py
│   │   ├── jobkorea_crawler.py  # 잡코리아 크롤러
│   │   └── saramin_crawler.py   # 사람인 크롤러
│   └── common/                  # 크롤러 공통 모듈
│       ├── __init__.py
│       ├── constants.py         # 크롤러 관련 상수 정의
│       ├── utils.py            # 유틸리티 함수
│       └── driver_setup.py     # Selenium 드라이버 설정
├── Docker
└── docker-compose.yml          # Docker 컨테이너 구성 설정

눈물이 난다 눈물이 나... 굳이 이렇게 복잡한 구조로 짜지 않고 싱글톤 패턴으로 구현할 수 있는 걸 굳이굳이 나눈다고 이지랄 생고생을 했다.

  • aws_service/에서 AWS 서비스와 MongoDB를 추상화해서 데이터 저장소 계층을 구현했다.
  • crawler/에서는 각 취업 사이트의 데이터를 수집하는 크롤러를 구현했다.
  • 각 서비스는 자체적인 common/를 가져 공통 기능을 관리한다.
  • 모든 저장소 구현체는 동일한 인터페이스(BaseRepository)를 따르도록 설계했다.

과한 구조... 과함... 과해요 과해...

그래도 이미 한 걸 어째요.

하지만 뭐 aws_service는 이후에도 더 사용할 게 있기 때문에 폴더 구조를 나누는 것은 의도한 게 맞다. 공고 데이터 수집이 과한거긴 함... 몰라! 일단 해!

🐵 MongoDB 테이블 설계

💃🏻 모델과 테이블 스키마

classDiagram
    class TypedDict {
        <<interface>>
    }
    
    class Company {
        +str company_id
        +str company_name
        +datetime created_at
        +datetime updated_at
    }
    
    class JobPosting {
        +str post_id
        +str post_name
        +str company_id
        +str company_name
        +datetime is_closed
        +str post_url
        +JobStatus status
        +datetime created_at
        +datetime updated_at
        +List[str] tags
    }
    
    class Tag {
        +str tag_id
        +TagCategory category
        +str name
        +Optional[str] parent_id
        +int level
        +int count
        +datetime created_at
        +datetime updated_at
    }
    
    class JobTag {
        +str job_tag_id
        +str job_id
        +str tag_id
        +datetime created_at
    }
    
    TypedDict <|-- Company
    TypedDict <|-- JobPosting
    TypedDict <|-- Tag
    TypedDict <|-- JobTag
    
    JobPosting "1" --> "*" JobTag : has
    Tag "1" --> "*" JobTag : has
    Company "1" --> "*" JobPosting : has

    class TableSchemas {
        +Dict company_schema
        +Dict job_posting_schema
        +Dict tag_schema
        +Dict job_tag_schema
        +List[IndexModel] company_indexes
        +List[IndexModel] job_posting_indexes
        +List[IndexModel] tag_indexes
        +List[IndexModel] job_tag_indexes
    }

    note for Company "기업 정보"
    note for JobPosting "채용 공고 정보"
    note for Tag "태그 정보"
    note for JobTag "공고-태그 매핑"

간단하게 설명하자면, 모델에는 4개의 테이블이 들어가있다.

기업 정보를 담은 Company, 채용 공고 정보를 담은 JobPosting, 태그 정보를 담은 Tag, 공고-태그 매핑 정보를 담을 JobTag 이렇게 4개이다.

공고-태그 간의 다대다 관계 관리는 복합키를 사용해서 관리한다. (job_tag_id=job_tag#tag_id)

그리고 table_schemas에서 인덱스를 설정해주었다.

  • Company와 JobPosting: 1:N 관계 (한 회사가 여러 채용공고)
  • JobPosting과 Tag: M:N 관계 (JobTag를 통한 매핑)
  • Tag의 self-referential 관계 (parent_id를 통한 계층구조)

🕺🏻 Repository 구현

classDiagram
    class BaseRepository {
        <<abstract>>
        +__init__(collection_name: str)
    }
    
    class StorageException {
        +__init__(message: str)
    }
    
    class MongoDBRepository {
        -client: MongoClient
        -db: Database
        -collection: Collection
        +__init__(collection_name: str)
        +health_check() bool
        +save(item: Union[Dict, List[Dict]]) bool
        +get(id: Optional[Union[str, List[str]]], query: Optional[Dict]) Union[Dict, List[Dict]]
        +update(id: str, data: Dict, upsert: bool) bool
        +delete(id: Union[str, List[str]]) bool
    }
    
    BaseRepository <|-- MongoDBRepository : inherits
    MongoDBRepository ..> StorageException : throws

MongoDBRepository 크래스는 BaseRepository를 상속 받아서 MongoDB에 특화된 저장소를 구현한다.

  • __init__: MongoDB 연결 설정 및 초기화
  • health_check: MongoDB 연결 상태 확인
  • save: 문서 저장
  • get: ID 기반 또는 쿼리 기반 문서 조회
  • update: ID 기반 문서 업데이트 (upsert 옵션 지원)
  • delete: 삭제 지원

🐵 기본 클래스

classDiagram
    class BaseRepository {
        <<abstract>>
        +collection_name: str
        +__init__(collection_name: str)
        +health_check()* bool
        +save(item: Union[Dict, List[Dict]])* bool
        +get(id: Optional[Union[str, List[str]]], query: Optional[Dict])* Union[Dict, List[Dict]]
        +update(id: str, data: Dict)* bool
        +delete(id: Union[str, List[str]])* bool
    }
    
    class MongoDBRepository {
        +health_check() bool
        +save(item) bool
        +get(id, query) Union[Dict, List[Dict]]
        +update(id, data) bool
        +delete(id) bool
    }
    
    class DynamoDBRepository {
        +health_check() bool
        +save(item) bool
        +get(id, query) Union[Dict, List[Dict]]
        +update(id, data) bool
        +delete(id) bool
    }
    
    class RepositoryFactory {
        +REPOSITORIES: Dict[str, Type[BaseRepository]]
        +create_repository(repo_type: str, collection_name: str) BaseRepository
    }
    
    class StorageException {
        +message: str
    }
    
    BaseRepository <|-- MongoDBRepository : implements
    BaseRepository <|-- DynamoDBRepository : implements
    RepositoryFactory ..> BaseRepository : creates
    RepositoryFactory ..> StorageException : throws

BaseRepository는 추상 클래스로 공통 인터페이스가 정의하며 기본 CRUD 작업 명세를 구현했다.

  • MongoDBRepository: MongoDB 구현체
  • DynamoDBRepository: DynamoDB 구현체

RepositoryFactory는 저장소 타입에 따른 인스턴스를 생성한다. 새로운 저장소를 쉽게 추가 가능하다는 장점이 있다 ^^

🖥️ 시스템 구축

크롤러는... 어떻게 구현했는지 다음번(?) 게시물로 쓰든가 하고...

Docker Compose를 통해서 크롤러 서비스를 구성하고, 데이터베이스를 구성해서 크롤링한 데이터를 MongoDB에 저장할 수 있게 했다.

⌨️ 크롤러 서비스 부분

  crawler:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: crawler-service
    ports:
      - '8000:8000'
    volumes:
      - ./crawler/output:/app/crawler/output
      - ./crawler/logs:/app/crawler/logs
    environment:
      - FLASK_ENV=development
      - DOCKER_CONTAINER=true
      - STORAGE_TYPE=${STORAGE_TYPE:-mongodb}
      - MONGODB_URI=mongodb://admin:password@mongodb:27017/crawler_db?authSource=admin
      - MONGODB_DB=crawler_db

Dockerfile을 통해서 빌드하고 8000포트를 노출시킨다. 아래 환경 변수에 개발 환경 설정에 대한 부분과 MongoDB 연결 정보 설정에 대한 정보를 담았다.

🌿 MongoDB 서비스

  mongodb:
    image: mongo:latest
    container_name: mongodb-service
    ports:
      - '27017:27017'
    volumes:
      - mongodb_data:/data/db
    environment:
      - MONGO_INITDB_ROOT_USERNAME=${MONGO_ROOT_USERNAME:-*****}
      - MONGO_INITDB_ROOT_PASSWORD=${MONGO_ROOT_PASSWORD:-*****}
      - MONGO_INITDB_DATABASE=crawler_db

최신의 MongoDB 이미지를 사용하고 27017 포트를 노출시킨다. 환경 변수에 로그인 정보와 Database를 저장해뒀다.

이렇게 여차여차 하다보니...

MongoDB에 데이터들을 성공적으로 담았다는 기쁜 소식^^

느낀점

아무튼 해결했죠?

...

슬퍼잉... 근데 글 쓰다보니까 아까는 되게 속상했는데 지금은 그냥그냥... 뭐 새로운 거 하나 더 배운셈 칠 수 있을 것 같다. 왜냐면 난 지금 집이니까 ^___^ 눈 감았다 뜨면 다시 학원이겠지만...

암튼 내일은 진짜 DynamoDB 연결해본다. 9시까지 DynamoDB 설정으로 코드 고치다가 결국 개큰 실패로 끝나버림. 하지만 뭔가 오늘은(12시 지남) 가능할 것 같아! 힘내보자고~


본 포스팅은 글로벌소프트웨어캠퍼스와 교보DTS가 함께 진행하는 챌린지입니다.

profile
영차영차 😎

2개의 댓글

comment-user-thumbnail
2025년 1월 7일

전통적인 RDS는 테이블 스키마부터 먼저 정의하는데 dynamoDB는 반대로 내가 어떤 쿼리가 필요한지 먼저 생각해보고 구조를 구성하는게 좋은 것 같아여~

1개의 답글