Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#12] 콘텐츠 조회기능 레디스 캐싱 적용 #13

Merged
merged 14 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions services/content-service/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,28 @@ WORKDIR /app
# Change ownership of the working directory to the non-root user
RUN chown -R appuser /app

# Set PYTHONPATH to ensure app is included in the path
ENV PYTHONPATH=/app

# Copy only requirements.txt and install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy only the necessary application code
# Copy application code
COPY app/ ./app

# Copy Alembic directory and related files
COPY alembic/ ./alembic
COPY alembic.ini .
COPY alembic.ini ./

# Copy tests directory (optional, if you want to run tests in the container)
COPY tests/ ./tests

# Expose the port for content-service (8000)
EXPOSE 8000

# Switch to the non-root user
USER appuser

# Run the app using uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# Run Alembic migrations before starting the app
CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000"]
31 changes: 0 additions & 31 deletions services/content-service/app/conftest.py

This file was deleted.

26 changes: 22 additions & 4 deletions services/content-service/app/content.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# content.py
import json
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
from app.schemas import ContentCreate, Content as ContentSchema
from app.models import Content
from app.db import get_db
import redis

router = APIRouter()

# 콘텐츠 생성 엔드포인트
redis_client = redis.Redis(host='redis', port=6379, db=0, decode_responses=True)
CACHE_TTL = 3600

@router.post("/", response_model=ContentSchema, status_code=201)
def create_content(content: ContentCreate, db: Session = Depends(get_db)):
new_content = Content(
Expand All @@ -18,26 +23,39 @@ def create_content(content: ContentCreate, db: Session = Depends(get_db)):
db.add(new_content)
db.commit()
db.refresh(new_content)

redis_client.delete("content_list") # 캐시 삭제

return new_content

# 콘텐츠 목록 조회 엔드포인트 (필터링 기능 추가)
@router.get("/", response_model=list[ContentSchema])
def get_contents(title: str = None, category: str = None, db: Session = Depends(get_db)):
query = db.query(Content)
redis_key = f"content_list:{title}:{category}"

cached_data = redis_client.get(redis_key)
if cached_data:
return json.loads(cached_data)

query = db.query(Content)
if title:
query = query.filter(Content.title.ilike(f"%{title}%"))
if category:
query = query.filter(Content.category == category)

contents = query.all()

# 헬퍼 메서드를 사용하여 데이터를 JSON으로 변환
redis_client.setex(redis_key, CACHE_TTL, json.dumps([content.to_dict() for content in contents]))

return contents

# 콘텐츠 삭제 엔드포인트
@router.delete("/{content_id}", status_code=204)
def delete_content(content_id: int, db: Session = Depends(get_db)):
content = db.query(Content).filter(Content.id == content_id).first()
if content is None:
raise HTTPException(status_code=404, detail="Content not found")

db.delete(content)
db.commit()

redis_client.delete("content_list") # 캐시 삭제
4 changes: 4 additions & 0 deletions services/content-service/app/main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from fastapi import FastAPI
from app.content import router as content_router
from app.community import router as community_router
import redis

app = FastAPI()

# Redis 클라이언트 초기화
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)

# 콘텐츠 서비스 라우터 추가
app.include_router(content_router, prefix="/contents", tags=["contents"])

Expand Down
15 changes: 11 additions & 4 deletions services/content-service/app/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# models.py
from sqlalchemy import Column, Integer, String, Text, Index, ForeignKey
from sqlalchemy.orm import relationship
from app.db import Base


# 콘텐츠 정보를 저장할 데이터베이스 모델 정의
class Content(Base):
__tablename__ = "contents"
Expand All @@ -14,6 +12,16 @@ class Content(Base):
category = Column(String, nullable=True, index=True) # 인덱스 추가
creator = Column(String, nullable=True, index=True) # 인덱스 추가

# 콘텐츠를 딕셔너리로 변환하는 메서드 추가
def to_dict(self):
return {
"id": self.id,
"title": self.title,
"description": self.description,
"category": self.category,
"creator": self.creator
}

# 복합 인덱스 추가
Index('idx_title_category', Content.title, Content.category)

Expand All @@ -28,7 +36,6 @@ class Post(Base):
# 댓글 관계 설정
comments = relationship("Comment", back_populates="post", cascade="all, delete-orphan")


class Comment(Base):
__tablename__ = "comments"

Expand All @@ -38,4 +45,4 @@ class Comment(Base):
post_id = Column(Integer, ForeignKey("posts.id"))

# 게시물과의 관계 설정
post = relationship("Post", back_populates="comments")
post = relationship("Post", back_populates="comments")
2 changes: 2 additions & 0 deletions services/content-service/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
alembic==1.13.3
annotated-types==0.7.0
anyio==4.6.0
async-timeout==4.0.3
certifi==2024.8.30
charset-normalizer==3.3.2
click==8.1.7
Expand All @@ -20,6 +21,7 @@ pydantic==2.9.2
pydantic_core==2.23.4
pytest==8.3.3
python-dotenv==1.0.1
redis==5.1.1
requests==2.32.3
sniffio==1.3.1
SQLAlchemy==2.0.32
Expand Down
33 changes: 32 additions & 1 deletion services/content-service/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
import sys
import os
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.db import Base, get_test_db
from fastapi.testclient import TestClient
from app.main import app

# 프로젝트의 루트 경로를 PYTHONPATH에 추가
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))

# 테스트용 데이터베이스 설정
TEST_DATABASE_URL = "postgresql://admin:1234@content-db:5432/test_content_db"
test_engine = create_engine(TEST_DATABASE_URL)
TestSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)

# pytest fixture: 테스트가 실행되기 전에 DB를 초기화하고, 테스트가 끝나면 데이터를 정리
@pytest.fixture(scope="module")
def test_db():
Base.metadata.create_all(bind=test_engine) # 테이블 생성
yield TestSessionLocal() # 테스트 실행
Base.metadata.drop_all(bind=test_engine) # 테스트가 끝나면 테이블 제거

@pytest.fixture(scope="module")
def client(test_db):
# 테스트용 DB로 연결되도록 의존성을 덮어씌우기
def _get_test_db_override():
try:
yield test_db
finally:
test_db.close()

app.dependency_overrides[get_test_db] = _get_test_db_override
with TestClient(app) as c:
yield c
59 changes: 19 additions & 40 deletions services/content-service/tests/test_community.py
Original file line number Diff line number Diff line change
@@ -1,108 +1,87 @@
import time
import requests
from fastapi.testclient import TestClient
from app.main import app
import requests
import time

client = TestClient(app)

# 유저 서비스 호출 함수
def get_user_info(author_id: int):
try:
response = requests.get(f"https://user-service:8001/api/users/{author_id}")
response.raise_for_status() # 상태 코드가 200이 아닐 경우 HTTPError 발생
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as http_err:
raise requests.exceptions.HTTPError(f"HTTP error occurred while fetching user info: {http_err}")
except requests.exceptions.ConnectionError as conn_err:
raise requests.exceptions.ConnectionError(f"Connection error occurred: {conn_err}")
except requests.exceptions.Timeout as timeout_err:
raise requests.exceptions.Timeout(f"Timeout occurred: {timeout_err}")
except requests.exceptions.RequestException as req_err:
raise requests.exceptions.RequestException(f"Unexpected error occurred: {req_err}")
except requests.exceptions.RequestException as e:
raise e

# 게시물 생성 테스트
def test_create_post():
try:
user_info = get_user_info(1) # 유저 서비스에서 유저 정보 조회
except requests.exceptions.RequestException as e:
assert False, f"유저 정보 조회 실패: {e}"

user_info = get_user_info(1) # 유저 서비스에서 유저 정보 조회
response = client.post("/posts/", json={
"title": "Test Post",
"content": "This is a test post",
"author_id": user_info["id"]
})
assert response.status_code == 201, f"게시물 생성 실패: {response.json()}"
assert response.status_code == 201
assert response.json()["title"] == "Test Post"

# 댓글 추가 테스트
def test_add_comment():
try:
user_info = get_user_info(1)
except requests.exceptions.RequestException as e:
assert False, f"유저 정보 조회 실패: {e}"

user_info = get_user_info(1)
response = client.post("/posts/1/comments", json={
"content": "This is a test comment",
"author_id": user_info["id"]
})
assert response.status_code == 201, f"댓글 생성 실패: {response.json()}"
assert response.status_code == 201
assert response.json()["content"] == "This is a test comment"

# 게시물 삭제 테스트
def test_delete_post():
try:
user_info = get_user_info(1)
except requests.exceptions.RequestException as e:
assert False, f"유저 정보 조회 실패: {e}"
user_info = get_user_info(1)

# 게시물 생성
response = client.post("/posts/", json={
"title": "Test Post for Delete",
"content": "This post will be deleted",
"author_id": user_info["id"]
})
assert response.status_code == 201, f"게시물 생성 실패: {response.json()}"

assert response.status_code == 201
post_id = response.json().get("id")
assert post_id is not None, "post_id가 None 입니다."
assert post_id is not None

# 게시물 존재 여부 확인
time.sleep(1) # 데이터베이스 반영 시간 확보
get_post_response = client.get(f"/posts/{post_id}")
assert get_post_response.status_code == 200, f"게시물이 생성되지 않았습니다: {get_post_response.json()}"
assert get_post_response.status_code == 200

# 게시물 삭제 시도
delete_response = client.delete(f"/posts/{post_id}")
assert delete_response.status_code == 204, f"게시물 삭제 실패: {delete_response.json()}"
assert delete_response.status_code == 204

# 댓글 삭제 테스트
def test_delete_comment():
try:
user_info = get_user_info(1)
except requests.exceptions.RequestException as e:
assert False, f"유저 정보 조회 실패: {e}"
user_info = get_user_info(1)

# 게시물 생성
response = client.post("/posts/", json={
"title": "Test Post with Comment",
"content": "This post has a comment",
"author_id": user_info["id"]
})
assert response.status_code == 201, f"게시물 생성 실패: {response.json()}"
assert response.status_code == 201
post_id = response.json()["id"]

# 댓글 추가
comment_response = client.post(f"/posts/{post_id}/comments", json={
"content": "This comment will be deleted",
"author_id": user_info["id"]
})
assert comment_response.status_code == 201, f"댓글 생성 실패: {comment_response.json()}"
assert comment_response.status_code == 201
comment_id = comment_response.json()["id"]

# 댓글 삭제
delete_comment_response = client.delete(f"/posts/{post_id}/comments/{comment_id}")
assert delete_comment_response.status_code == 204, f"댓글 삭제 실패: {delete_comment_response.json()}"
assert delete_comment_response.status_code == 204

# 존재하지 않는 유저로 게시물 생성 시도 테스트
def test_user_not_found():
Expand All @@ -111,5 +90,5 @@ def test_user_not_found():
"content": "This is a test post",
"author_id": 9999 # 없는 유저 ID
})
assert response.status_code == 404, f"유저가 존재하지 않는 경우 게시물 생성 실패: {response.json()}"
assert response.status_code == 404
assert response.json()["detail"] == "유저 정보를 가져오는 데 실패했습니다."
Loading
Loading