지난 API 분석 글에서 작성했던 내용들을 바탕으로 DB를 설계해보았다.
예전에 UML을 그릴 때 자주 사용했던 툴로는 StarUML이나 draw.io(현재는 app.diagrams.net)을 사용했었는데, 둘 모두 GUI 기반이라 그런지 사용법은 쉬웠지만 수정할 내용이 많을 때는 번거로운 경우가 있었다. 그러다 코드 기반으로 UML을 그릴 수 있는 온라인 툴을 알게 되었다.
팀원 공유, 서버 백업, sql 및 이미지 파일로 import/export 등 필요한 기능들은 웬만하면 다 있고 사용 후 결과물의 비주얼도 맘에 들어 돌고돌아 결국 이 툴을 사용하게 되었다. 아쉬운 점이라면 테이블 위치 자동정렬 기능이 없느니만도 못하고 테이블 간 간격 및 위치를 미세하게 조정하는 점이 좀 불편하다는 것인데, 핵심적인건 아니라 괜찮은 것 같다.
사실 프로젝트 시작하면서 대략적으로 만들어두긴 했었지만 막상 본격적으로 조사 및 설계를 해보니 맞지 않는 부분이 많아 달라진 부분이 많다. 당시에는 소셜 로그인에 대해서 염두해두지 않았었고, 랭킹 기능도 적용되어 있지 않은 상태였다.
막상 개발하다보면 생각하지 못했던 부분들이 나오고 놓친 것이 있을 수 있기 때문에 감히 최종이라는 말은 못 쓰겠지만, 나름 깔끔하게 설계한 것 같다. 생각했던 것보다 Contents(영화, TV 프로그램 등의 정보) 부분의 덩치가 커져서 이 부분은 나중에 칼질이 들어갈 수도 있을 것 같다.
위에서 언급했듯이 내가 UML을 그릴 때 사용했던 툴은 코드로 작성하고, 후에 테이블 위치들만 GUI에서 조정해주었다. 맨 위부터 테이블들의 상세 스펙을, 아래쪽에는 테이블 간의 관계를 명시해주었다.
Table User {
user_id integer [primary key]
email varchar
user_nickname varchar
gender varchar
age_range integer
profile_image varchar
role varchar
auth_provider varchar
social_id varchar
refresh_token varchar
}
/*
* Movie / TV Program 상위 테이블
* ===============================================
*/
Table Contents {
contents_id integer [primary key]
contents_type integer [primary key]
translated_title varchar
origianl_title varchar
origianl_language varchar
tagline varchar
overview varchar
poster_path varchar
production_countries varchar
certifications varchar [note: "시청 등급"]
review_cnt integer [note: "작품 리뷰 수"]
article_cnt integer [note: "작품 게시글 수"]
imad_score float [note: "IMAD 자체 평점"]
}
/*
* Movie 관련 테이블
* ===============================================
*/
Table MovieData {
movie_id integer [primary key]
release_date date [note: "국내 개봉일 기준"]
status integer [note: "개봉 여부"]
runtime integer
}
/*
* TV Program 관련 테이블
* ===============================================
*/
Table TvProgramData {
tv_program_id integer [primary key]
first_air_date date
last_air_date date
number_of_episodes integer
number_of_seasons integer
}
Table Networks [note: "방송사, OTT 등"] {
networks_id integer [primary key]
networks_name varchar
logo_path varchar
origin_country varchar
}
Table Broadcaster {
tv_program_id integer [primary key]
networks_id integer [primary key]
}
Table Season {
season_id integer [primary key]
name varchar
air_date date
episode_count integer
overview varchar
poster_path varchar
season_number integer
}
Table SeasonCollection {
tv_program_id integer [primary key]
season_id integer [primary key]
}
/*
* 배우 및 스태프 관련 테이블
* ===============================================
*/
Table Crew {
crew_id integer [primary key]
original_name varchar
translated_name varchar
gender integer
known_for_department varchar [note: "유명분야"]
profile_path varchar
}
Table Actor {
actor_id integer [primary key]
original_name varchar
translated_name varchar
gender integer
known_for_department varchar [note: "유명분야"]
profile_path varchar
}
Table ContentsCrew {
contents_id integer [primary key]
contents_type integer [primary key]
crew_id integer [primary key]
department varchar [note: "Ex) Writing"]
job varchar [note: "Ex) Story"]
character varchar [note: "스태프가 연기도 겸한 경우"]
}
Table ContentsActor {
contents_id integer [primary key]
contents_type integer [primary key]
actor_id integer [primary key]
character varchar [note: "배역명"]
}
/*
* 장르 관련 테이블
* ===============================================
*/
Table Genre {
genre_id integer [primary key]
name varchar
}
Table UserPreferredGenre {
genre_id integer [primary key]
user_id integer [primary key]
rate integer [note: "장르 선호점수"]
}
Table ContentsGenre {
genre_id integer [primary key]
contents_id integer [primary key]
contents_type integer [primary key]
}
/*
* 리뷰 관련 테이블
* ===============================================
*/
Table Review {
review_id integer [primary key]
user_id integer
contents_id integer
contents_type integer
title varchar
content varchar
rate float [note: "작품 평점"]
is_spoiler boolean
like integer
dislike integer
created_at datetime
modified_at datetime
}
/*
* 게시글 관련 테이블
* ===============================================
*/
Table Article {
article_id integer [primary key]
user_id integer
contents_id integer
contents_type integer
category integer
title varchar
content varchar
is_spoiler boolean
view_cnt integer
comment_cnt integer
like integer
dislike integer
created_at datetime
modified_at datetime
}
Table Comment {
comment_id integer [primary key]
user_id integer
article_id integer
content text
comment_order integer
created_at datetime
modified_at datetime
}
/*
* 리뷰 및 게시글 추천/비추천 관련 테이블
* ===============================================
* 추천이면 +1, 비추천이면 -1, 아무 상태도 아니면 0이므로 해당 데이터 삭제
*/
Table ReviewLike {
user_id integer [primary key]
review_id integer [primary key]
like_status integer [note: "추천이면 +1, 비추천이면 -1, 아무 상태도 아니면 해당 데이터 삭제"]
}
Table ArticleLike {
user_id integer [primary key]
article_id integer [primary key]
like_status integer [note: "추천이면 +1, 비추천이면 -1, 아무 상태도 아니면 해당 데이터 삭제"]
}
/*
* 북마크 관련 테이블
* ===============================================
* 추천이면 +1, 비추천이면 -1, 아무 상태도 아니면 0이므로 해당 데이터 삭제
*/
Table ArticleBookmark {
user_id integer [primary key]
article_id integer [primary key]
bookmark_status integer
}
Table ContentsBookmark {
user_id integer [primary key]
contents_id integer [primary key]
bookmark_status integer
}
/*
* 랭킹 관련 테이블
* ===============================================
*/
Table AllTimeScore {
contents_id integer [primary key]
contents_type integer [primary key]
score integer
}
Table AllTimeRanking {
contents_id integer [primary key]
contents_type integer [primary key]
rank integer
rank_changed integer
}
Table WeeklyScore {
contents_id integer [primary key]
contents_type integer [primary key]
score integer
}
Table WeeklyRanking {
contents_id integer [primary key]
contents_type integer [primary key]
rank integer
rank_changed integer
}
/*
* 참조 관계
* ===============================================
*/
// Contents : Movie / TV Program
Ref: Contents.contents_id < MovieData.movie_id
Ref: Contents.contents_id < TvProgramData.tv_program_id
Ref: TvProgramData.tv_program_id < Broadcaster.tv_program_id
Ref: Networks.networks_id < Broadcaster.networks_id
Ref: TvProgramData.tv_program_id < SeasonCollection.tv_program_id
Ref: Season.season_id < SeasonCollection.season_id
// Contents : Crew / Actor
Ref: Contents.contents_id < ContentsCrew.contents_id
Ref: Contents.contents_type < ContentsCrew.contents_type
Ref: Crew.crew_id < ContentsCrew.crew_id
Ref: Contents.contents_id < ContentsActor.contents_id
Ref: Contents.contents_type < ContentsActor.contents_type
Ref: Actor.actor_id < ContentsActor.actor_id
// Genre : User / Contents
Ref: Genre.genre_id < UserPreferredGenre.genre_id
Ref: User.user_id < UserPreferredGenre.user_id
Ref: Genre.genre_id < ContentsGenre.genre_id
Ref: Contents.contents_id < ContentsGenre.contents_id
Ref: Contents.contents_type < ContentsGenre.contents_type
// Review : User / Contents
Ref: User.user_id < Review.user_id
Ref: Contents.contents_id < Review.contents_id
Ref: Contents.contents_type < Review.contents_type
// Article : Comment
Ref: User.user_id < Article.user_id
Ref: Contents.contents_id < Article.contents_id
Ref: Contents.contents_type < Article.contents_type
Ref: User.user_id < Comment.user_id
Ref: Article.article_id < Comment.article_id
// Contetns : Score / Ranking
Ref: Contents.contents_id < AllTimeScore.contents_id
Ref: Contents.contents_type < AllTimeScore.contents_type
Ref: Contents.contents_id < AllTimeRanking.contents_id
Ref: Contents.contents_type < AllTimeRanking.contents_type
Ref: Contents.contents_id < WeeklyScore.contents_id
Ref: Contents.contents_type < WeeklyScore.contents_type
Ref: Contents.contents_id < WeeklyRanking.contents_id
Ref: Contents.contents_type < WeeklyRanking.contents_type
// Like : Review / Article
Ref: User.user_id < ReviewLike.user_id
Ref: Review.review_id < ReviewLike.review_id
Ref: User.user_id < ArticleLike.user_id
Ref: Article.article_id < ArticleLike.article_id
// Bookmark : Aritlce / Contents / User
Ref: User.user_id < ArticleBookmark.user_id
Ref: Article.article_id < ArticleBookmark.article_id
Ref: User.user_id < ContentsBookmark.user_id
Ref: Contents.contents_id < ContentsBookmark.contents_id
사실 이 부분은 설계보다 구현이 먼저 되어있던 파트다. 클라이언트 측 테스트를 위해서 본격적인 설계 및 구현 전에 선행적으로 수행되었어야 했던 부분이다 보니 이렇게 된 것 같다. 그렇지만 결과적으로는 많은 시행착오를 겪으면서 초기 구상했던 내용과 많이 달라져서, 설계를 먼저 했다고 해도 많이 달려졌을 것 같긴하다...
어쨌든간에 User 테이블은 말 그대로 회원정보를 담고 있으며 이메일, 닉네임, 비밀번호, 성별, 연령대 등 기본적인 정보들을 가지고 있다. 그 외에 눈에 띄는 칼럼들이 있는데, 각 역할은 다음과 같다.
설계할 때 가장 많이 고민했고, 덩치가 제일 커진 파트이다. TMDB에서는 컨텐츠(여기서는 TV, 드라마, 다큐, 애니메이션 등 모든 영상 작품들을 컨텐츠라고 칭하겠다)를 크게 Movie
와 TV Program
으로 분류한다.
IMAD의 DB 스키마 관점에서 봤을 때 이 둘은 동일한 계층에 있기에 하나의 테이블에 담고 싶었다. 그렇지만 서로 중복되지 않는 고유한 데이터의 종류가 많다보니 하나의 테이블에서 관리하려면 비어있는 칼럼이 많게 되었다. 그래서 공통된 부분만 묶어서 Contents
에 정보를 담고, 나머지 고유한 것들은 따로 분리하게 되었다.
프로젝트의 규모와 예상 사용자 수에 비하면 호들갑을 떤 느낌도 없지 않아 있지만 추후 확장성이나 데이터 관리 측면에서 봤을 때는 좋을 것 같다.
참고로 Movie
와 TV Program
은 ID 값이 겹치는 경우가 있어 기본키로 contents_id
와 contents_type
을 함께 지정해주었다.
배우와 스태프(감독, 작가, 연출, PD 등)과 관련된 정보들을 담고 있다. Contents와 Actor/Crew는 M:N 관계를 가지기 때문에 중간에 연결 테이블을 만들어줘서 1:N 관계로 풀어주었다. JPA 단에서 다대다 관계를 매핑할 수 있기는 하지만 중간에 연결 테이블이 단순 연결 뿐만 아니라 다른 값이 추가로 들어갈 수 있기 때문에 연결 테이블을 엔티티로 승격시켜주는게 좋다. 자세한 내용은 아래 링크를 참고하자.
[JPA] @ManyToMany, 다대다[N:M] 관계
character
는 배역명을 뜻하며, 한 사람이 배우/감독/작가 등을 다 소화하는 경우가 있어 ContentsCrew
테이블에도 넣게 되었다. ContentsCrew
에서 department
는 "Writing" 같은 정보를 담고 있는 컬럼이고 job
은 "Writing" 부서 내의 "Story" 역할 같은 값을 가진다.
한 작품에는 장르가 여러 개이고, 유저가 선호하는 장르 또한 두 개 이상일 수 있기 때문에 다대가 관계를 가정했다. 그래서 각각 연결테이블을 만들어주었는데, UserPreferredGenere
의 경우 추후 선호 장르를 기반으로 작품을 추천해주기 위해 rate 칼럼을 넣어주었다. 이 rate는 작품 찜이나 게시글, 작품 추천 등을 합산할까 생각 중이다.
IMAD에서 작성할 수 있는 글은 리뷰
와 게시글
두 가지가 있다. 리뷰
는 작품에 대한 감상을 적고 평점을 매길 수 있다. 게시글
은 작품의 질문, 토론 등 원하는 주제로 자유롭게 글을 작성할 수 있으며 댓글이 달릴 수 있다. 둘 모두 추천 / 비추천을 받을 수 있고 spoiler 플래그가 true이면 preview에서 모자이크 처리되어 보여진다.
Like와 Bookmark 정보 모두 위에서처럼 다대다 관계를 일대다 관계로 풀어내기 위해 연결 테이블을 두었다.
도메인 / DB / 유즈 케이스 등의 과정을 분리해서 단계별로 수행해나가고 싶었다. 하지만 막상 팀 프로젝트를 진행하다보니 이전 과정으로 되돌아가 내용을 갈아엎고 작성해두었던 문서가 충돌하는 등 여러 문제가 있어서 결국엔 한 번에 뭉뜽그려서 하게 되었다. 지금이야 규모가 그렇게 크지 않으니 크게 문제가 와닿지는 않았지만 팀과 프로젝트의 덩치가 커지면 분명 이런 방식에는 문제가 사방에서 터질 것이다. 개인 수준에서는 그런 경험을 하기 쉽지 않아서 아쉽지만 지금이라도 조금씩 실천해 나가보자. 취업도 얼른 하고...