진짜 오늘부턴 코드 작성 및 확인 해보았다.
간단하게 작성해보려고 했는데 생각보다 엄청 길어지고 django 틀에 맞추고 가독성좋은 코드를 작성하기 위해서 길어지기도 하고 redis를 섞어서 사용하니까 많이 코드가 어지럽고 어려웠다.
먼저 RSS로 가져올 뉴스의 모델을 정의하였다.
class NewsSource(models.Model):
name = models.CharField(max_length=100, unique=True)
url = models.URLField(max_length=500)
source_type = models.CharField(max_length=20, choices=[('rss', 'RSS'), ('api', 'API'), ('scraping', '웹 스크래핑')], default='rss')
is_active = models.BooleanField(default=True)
collection_interval = models.IntegerField(default=60)
last_collected_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = '뉴스 소스'
verbose_name_plural = '뉴스 소스'
ordering = ['-created_at']
class NewsArticle(models.Model):
source = models.ForeignKey(NewsSource, on_delete=models.CASCADE, related_name='articles')
title = models.CharField(max_length=500)
url = models.URLField(max_length=1000, unique=True, db_index=True)
description = models.TextField(blank=True, null=True)
content = models.TextField(blank=True, null=True)
author = models.CharField(max_length=200, blank=True, null=True)
category = models.CharField(max_length=100, blank=True, null=True, db_index=True)
published_at = models.DateTimeField(null=True, blank=True, db_index=True)
collected_at = models.DateTimeField(auto_now_add=True, db_index=True)
is_processed = models.BooleanField(default=False)
class Meta:
verbose_name = '뉴스 기사'
verbose_name_plural = '뉴스 기사'
ordering = ['-published_at', '-collected_at']
indexes = [
models.Index(fields=['-published_at', '-collected_at']),
models.Index(fields=['source', '-published_at']),
]
class DataCollectionJob(models.Model):
STATUS_CHOICES = [('pending', '대기 중'), ('running', '실행 중'), ('completed', '완료'), ('failed', '실패')]
source = models.ForeignKey(NewsSource, on_delete=models.SET_NULL, null=True, blank=True, related_name='collection_jobs')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
started_at = models.DateTimeField(auto_now_add=True)
completed_at = models.DateTimeField(null=True, blank=True)
items_collected = models.IntegerField(default=0)
error_message = models.TextField(blank=True, null=True)
class Meta:
verbose_name = '데이터 수집 작업'
verbose_name_plural = '데이터 수집 작업'
ordering = ['-started_at']
기본적은것들은 정리했고 model에서 중요한것은
위에 있는 내용으로 실제코드에는 help_text를 많이 넣어서 나중에 헷갈리지않도록 잘 정리해두었지만 블로그에 다 올리기에는 내용이 너무 길어져 빼고 간단하게 넣어보았다.
models를 작성했으면 serializers도 작성해야하는데
class NewsSourceSerializer(serializers.ModelSerializer):
article_count = serializers.IntegerField(read_only=True)
last_collected_at_display = serializers.DateTimeField(read_only=True, source='last_collected_at', format='%Y-%m-%d %H:%M:%S')
class Meta:
model = NewsSource
fields = '__all__'
read_only_fields = ['id', 'created_at', 'updated_at', 'last_collected_at']
def validate(self, data: Dict[str, Any]) -> Dict[str, Any]:
if 'name' in data:
name = data.get('name', '').strip()
if not name:
raise serializers.ValidationError({'name': '소스 이름은 필수입니다.'})
data['name'] = name
if 'url' in data:
url = data.get('url', '').strip()
if not url:
raise serializers.ValidationError({'url': 'URL은 필수입니다.'})
if not (url.startswith('http://') or url.startswith('https://')):
raise serializers.ValidationError({'url': 'URL은 http:// 또는 https://로 시작해야 합니다.'})
data['url'] = url
return data
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['article_count'] = instance.articles.count()
return representation
class NewsArticleSerializer(serializers.ModelSerializer):
source_detail = NewsSourceSerializer(source='source', read_only=True)
source_name = serializers.CharField(source='source.name', read_only=True)
title_short = serializers.SerializerMethodField()
published_at_display = serializers.DateTimeField(read_only=True, source='published_at', format='%Y-%m-%d %H:%M:%S')
class Meta:
model = NewsArticle
fields = '__all__'
read_only_fields = ['id', 'collected_at', 'url']
def get_title_short(self, obj):
if obj.title and len(obj.title) > 50:
return obj.title[:50] + '...'
return obj.title or ''
def validate(self, data: Dict[str, Any]) -> Dict[str, Any]:
if 'title' in data:
title = data.get('title', '').strip()
if not title:
raise serializers.ValidationError({'title': '제목은 필수입니다.'})
data['title'] = title
if 'url' in data:
url = data.get('url', '').strip()
if url and not (url.startswith('http://') or url.startswith('https://')):
raise serializers.ValidationError({'url': 'URL은 http:// 또는 https://로 시작해야 합니다.'})
data['url'] = url
return data
위에 내용처럼 작성했고
중요한 내용은
위의 내용처럼 validate를 통해 걸러지는 내용과 MethodField를 활용하여 코드를 한줄이라도 줄여서 늘어지지않게끔 조정했다.
또한, 중첩된 객체 표현인 source_detail을 통해 전체내용을 한번 더 가져와 추후 내보낼때 편하도록 작성하였다.
그리고 redis_services에는 redis의 로직을 작성 및 정리했는데 로직을 다 가져오기에는 너무 길어져서 간단요약을 하게되면
기본 클래스인 RedisService를 상속받아 공통 Redis 연결을 하고
각 클래스마다 고유 키 접두사를 사용하여 데이터를 구분하여 수집 및 정리한다.
1. 중복 데이터 수집 방지 (DuplicatePreventionService)
이미 수집한 기사를 다시 수집하지 않도록 방지
동작 흐름
1. 서비스 초기화: Redis 연결 설정 및 키 접두사와 TTL 설정 (7일)
2. 중복 체크: 기사 URL을 키로 변환하여 Redis에 존재 여부 확인
3. 수집 완료 표시: 수집 완료 후 Redis에 키 저장 (TTL 7일 자동 설정)
4. 자동 정리: 7일 후 TTL 만료로 자동 삭제
2.실시간 통계 집계 (RealtimeStatsService)
수집된 기사 수, 이벤트(redis에 저장된) 발생 시간 등을 실시간으로 추적
동작 흐름
1. 서비스 초기화: Redis 연결 설정 및 키 접두사와 TTL 설정 (24시간)
2. 카운터 증가: 전체 카운터와 시간대별 카운터 동시 증가
3. 이벤트 기록: 이벤트 발생 시각을 리스트에 추가 (최신 순서)
4. 리스트 관리: 최근 1000개만 유지하고 나머지 삭제
5. 자동 정리: 24시간 후 TTL 만료로 자동 삭제
3. API Rate Limiting (RateLimitService)
외부 API 호출 시 Rate Limit 관리
동작 흐름
1. 서비스 초기화: Redis 연결 설정 및 기본값 설정 (시간당 100회, 1시간 윈도우)
2. Rate Limit 체크: 현재 요청 수 조회 및 최대치 초과 여부 확인
3. 초과 시: 요청 거부 및 리셋 시간 반환
4. 미만 시: 카운터 증가 (첫 요청은 SETEX, 이후는 INCR)
5. 자동 리셋: TTL이 지나면 키가 자동 삭제되어 리셋
이렇게 작성했다.
마지막으로 오늘 코드 작성하면서 멍청해서 질문했던 내용 중 기억에 남는것들을 정리 한다.
오늘 한거 가지고 계속 살펴보면서 다음 코드도 작성해보고 다음엔 services.py랑 tasks.py를 작성하면서 맨땅에 헤딩해보겠다.
'AI_RSS_트래픽 프로젝트' 카테고리의 다른 글
| 모르는 상태로 하는 RSS&분석&RAG 프로젝트(9) 데이터 수집(3) (0) | 2025.12.03 |
|---|---|
| 모르는 상태로 하는 RSS&분석&RAG 프로젝트(8) 데이터 수집(2) (1) | 2025.12.02 |
| 모르는 상태로 하는 RSS&분석&RAG 프로젝트(6) RSS & DB 선택 (0) | 2025.11.26 |
| 모르는 상태로 하는 RSS&분석&RAG 프로젝트(5) Celery+Redis (0) | 2025.11.25 |
| 모르는 상태로 하는 RSS&분석&RAG 프로젝트(4) Celery (0) | 2025.11.21 |