뭔가 FM 방식으로 한다면 기능명세를 보고 분석하여 DB 모델링을 하겠지만 지금은 제가 기획자이자 설계자이니(^_^) 화면 설계 기준으로 보면 필요한 것을 대략적으로 알 수 있으므로 최종 필요한 테이블은 아래와 같이 되겠네요.
회원 계정 관련 테이블
- 회원 / User
- 회원 휴대폰번호 / UserPhone
- 프로필 / Profile
- 프로필 사진 / ProfilePicture
채팅방 및 채팅메시지 테이블
- 채팅방 / ChatRoom
- 채팅방 멤버 / ChatRoomMember
- 채팅 메시지 / ChatMessage
기타
- (초기 회원가입 시) 휴대폰 인증 이력 / PhoneVerificationHistory
- 매칭 / Match
- (비밀번호 찾기를 위한) 임시 비밀번호 발급 이력 / TemporaryPasswordIssueHistory
위의 테이블들을 가지고 ERD 다이어그램을 그리면 다음과 같은 그림이 됩니다.
(VSCode 확장프로그램인 ERD Editor 사용)
Django ORM을 사용하니 순수 데이터베이스 스키마 기반으로 설명하기보다 ORM 코드 기준으로 설명하는게 좋을 것 같네요.
참고로 기본적으로 존재하는 각 테이블의 created_datetime(생성일자) 및 updated_datetime(수정일자) 필드들은 설명에서 제외하였습니다.
휴대폰 인증 이력 / PhoneVerificationHistory
- phone_number : CharField
- verification_code : CharField
- is_verified : BooleanField
- uuid : UUIDField
휴대폰 인증 시도 시, 휴대폰 번호와 발급된 인증번호 저장 필드가 기본적으로 들어가구요.
그리고 인증 여부를 확인하기 위해 is_verified 필드를 넣어주었습니다.
uuid 필드는 휴대폰 인증 완료 시, 나중에 회원가입할 때 인증키로 활용하는 필드입니다.
인증 구조에 따라 uuid 값 없이 진행할 수도 있겠지만 회원가입할 때 확실하게 인증된 사람을 구분하기 좋을 것 같아서 넣었습니다.
회원 / User (Django)
Django를 사용하기에 프레임워크에서 제공되는 기본 User 테이블을 사용하였습니다.
별도로 만들지 않아서 좋네요.
(참고로 위 테이블 그림은 표기된 필드들 외에 다른 기본 필드도 있으나 프로젝트 내에서 사용하지 않아서 표기하지 않았습니다.)
옵션코드그룹, 옵션코드 / OptionCodeGroup, OptionCode
OptionCodeGroup
- name : CharField
OptionCode
- group : ForeignKey(OptionCodeGroup)
- sub_id : PositiveSmallIntegerField
- value : CharField
각종 옵션값들을 저장하는 테이블입니다. 현재는 프로필에 들어가는 데이터들 중에 선택할 수 있는 옵션 값들이 들어가 있습니다.
다른 기능을 추가할 때도 사용할 수 있으며 예를 들어, 신고 기능을 넣는다면 신고 종류(욕설, 광고, 허위 사실 유포 등)를 여기에 저장할 수 있겠네요.
현재는 취미 리스트와 MBTI 리스트를 넣어서 사용하고 있습니다.
프로필 / Profile
- user : OneToOneField(User)
- uuid : UUIDField
- nickname : CharField / 닉네임
- birthdate : DateField / 생년월일
- gender : CharField / 내 성별
- preferred_gender : CharField / 나에게 보여줄 성별
- mbti : CharField / MBTI
- passions : JSONField / 취미
- height : CharField / 신장
- religion : CharField / 종교
- smoking_status : BooleanField / 흡연 여부
- drinking_status : CharField / 음주 여부, 빈도
- location : CharField / 사는 지역
- bio : CharField / 자기 소개
- is_banned : BooleanField / 차단 여부
프로필 테이블을 만들 때, 제일 많이 변경했던 것 같네요.
종류가 많으니 필드별로 설명드리겠습니다.
- user : User와 1:1 매핑을 위해 OneToOneField로 연결하였습니다.
- uuid : Frontend에서 각 사용자를 구분하기 위한 값입니다. Profile 또는 User의 (Auto Increment가 설정된) 기본키 값으로 사용자 조회 및 식별을 하게 되면 타인의 프로필 정보를 쉽게 접근하고 조회할 수 있는 문제가 있습니다.
Auto Increment가 설정된 기본키 값으로 API 조회 예시
GET users/1
GET users/2
GET users/3
…
이러한 경우, 대량 조회도 문제지만 매칭 서비스의 경우, 하루 최대 소개하는 횟수를 제한하는데 이 때 원하는 유저만 골라서 좋아요를 보낼 수 있어 문제가 될 수 있습니다.
추가적으로 각 테이블에 자동으로 들어가는 기본키 id 필드를 제거하고 UUID로 기본키를 지정하는 방법도 있지만 문제는 나중에 테스트 또는 다른 목적으로 테이블을 단 건 조회할 때, UUID의 긴 키 값을 일일이 복사 붙여넣기 작업을 하면서 조회해야합니다. 너무 불편할 수 있기에 그대로 두기로 하였습니다.
다른 방법으로는 id 필드를 그대로 두고 UUID를 기본키로 지정하는 것도 좋은 방법으로 보입니다. 근데 그렇게 하려면 id 필드의 자동 증가(auto increment)를 위해 AutoField를 사용해야하는데 AutoField 사용 시, 무조건 primary key로 두어야합니다. stackoverflow에는 AutoField를 상속받아서 오버라이드하는 방법이 있는 것 같은데 이건 테스트를 해봐야겠네요.
(관련 질의)
- gender : 'M', 'F' (남성, 여성)
- preferred_gender : 'M', 'F', 'A' (남성, 여성, 모두)
- MBTI : 'ENFJ', 'ENFP', 'ENTJ', …
- religion : '없음', '기독교', '불교', '천주교'
- drinking_status : '안함', '가끔', '자주'
- location : '서울', '부산', '대구', …
각각 본인 성별, 보여주길 원하는 성별, MBTI, 종교, 음주 상태(?), 거주 지역 필드입니다.
위 필드들의 경우, 공통적으로 선택할 수 있는 옵션 값들이 있습니다. 초기 설계하고 코딩할 때는 대부분의 프로필 옵션 값들을 OptionCode 테이블에 전부 넣어서 관리했었습니다. 그렇게한 이유는 하드코딩(Django Model의 Choices 사용을 의미)하는 것보다는 아무래도 유연하게 옵션 값들을 조정할 수 있는 장점을 더 중요하게 생각했었습니다.
하지만 개발하면서 언급된 각 필드들은 선택할 수 있는 옵션 값들이 많은 편도 아니고 프레임워크(DRF)에서 주는 이점(예를 들어, validate을 신경쓸 필요가 없는 점)과 DB 조회 빈도가 줄어들기 때문에 Choices 사용하는 것으로 변경하였습니다.
(변경해보니 이전보다 코드 양도 줄고 좀(?) 심플해진 것 같다.)
- passions : 취미 정보를 저장하는 필드입니다. 복수의 데이터 저장이 가능합니다.
처음 Passions 필드를 만들었을 때는 ManyToManyField(Passion 테이블 별도 생성)를 사용했었습니다. 취미 부분은 아무래도 한 사람 당 여러 선택이 가능한 부분이기에 자연스럽게 ManyToManyField를 사용했었습니다. 사용하면서 드는 느낌 점은
매칭 프로젝트 특성 상, 프로필 정보를 자주 가져옵니다. 그래서 프로필 정보를 가져올 때, 자연스럽게 'prefetch_related'를 사용하여 passions 정보를 가져오는데 그러다보니 쿼리 호출 수가 자연스럽게 N+1이 된다는 점입니다.
(거기다가 프로필 사진도 동시에 가져오면 N+2인가..)
그리고 취미는 프로필의 아주 작은 일부분의 정보일 뿐인데 짧은 문자열 몇 개 매핑하는 것이 너무 리소스 낭비가 아닌가라는 생각이 들었습니다.
(반대로 얼마나 취미를 사람들이 수정한다고.. 어차피 메인은 프로필, 채팅이 메이저일텐데..)
그 예 중 하나로 ModelSerializer를 그대로 사용한다는 가정 하에 (즉, 마개조 제외)
Passions 필드에 여러 취미들을 신규로 넣거나 수정하게 되면, 해당 데이터가 있는지 각각 1개씩 DB를 조회해서 데이터를 넣어줍니다.
그래서 고민하다가 그냥 JSON을 통째로 넣어주는게 더 낫겠다라는 생각이 들어 JSONField를 사용하였습니다. 솔직히 이 때, 그냥 프로필 정보를 통째로 JSONField 하나에 때려넣고 싶..
(ArrayField를 가장 쓰고 싶었으나 PostgreSQL 한정이라 고민 대상에서 일단 제외시켰습니다)
CharField를 사용하여 넣어줘도 되지만 그러면 데이터 형태부터 검사(regex 사용)까지 제가 직접 모두 정의해주긴 귀찮기 때문에 패쓰. 굳이 형태를 정의한다면
형태 1 : 하키 / 축구 / 농구
형태 2 : 하키, 축구, 농구
형태 3 : 하키 축구 농구
등등 여러 종류의 형태로 저장 가능하겠네요. 꺼내서 보낼 땐, split하여 리스트로 만들어서 보내주면 될 것 같습니다.
JSONField를 쓰면 프레임워크에서 JSON 구조는 알아서 검사해주니 딱히 신경 안써도될 것 같네요.
물론 뒤에 가면 나오지만 JSON 구조를 구체적으로 어떤 식으로 사용할 것인지는 정하긴 해야합니다. -_-
논제에서 벗어나지만 예를 들어,
(1) ["축구", "야구", "농구"]
(2) { "passions": {"data": ["축구", "야구", "농구"]} }
(3) { "passions": {"passion_1": "축구", "passion_2": "야구", "passion_3": "농구"]} }
1번은 JSON 구조가 아니니 알아서 에러를 발생시키겠지만 2, 3번은 JSON 구조는 맞으나 둘 중 하나의 형태를 정해서 해당 형태가 아니면 에러를 발생시켜야합니다.
(당장 서비스의 빠른 런칭 및 시장반응 또는 클라이언트의 빠른 피드백 목적이 중요하다면 이런 효율성은 다 버리고 ManyToManyField 그냥 쓸 것 같네요.)
UserPhone / 회원 휴대폰번호
- user : OneToOneField(User)
- phone_number : CharField
사용자 핸드폰 번호를 저장하는 테이블입니다. Profile에 함께 넣어도되긴 하지만 실수로라도 ModelSerializer 등을 사용할 때 휴대폰 번호가 같이 노출 될 수 있기에 민감 정보는 별도 테이블에 저장하였습니다.
ProfilePicture / 프로필 사진
- user : ForeignKey(User)
- uuid : UUIDField
- main : BooleanField
- image : ImageField
프로필 사진 정보를 저장하는 테이블입니다. main은 메인 프로필 사진을 구분하는 용도로 사용됩니다. uuid 필드는 Profile uuid처럼 동일하게 외부에서 프로필 사진을 구분할 때 사용되는 필드입니다.
여기서는 API를 통해 타인의 Profile 정보를 가져올 때, 사진(이미지 URL)도 같이 가져오고 있기 때문에 uuid가 필요없을 수도 있겠네요. 그래도 외부에서 예측 불가능한 것이 좋으니 uuid 필드를 사용했습니다.
Match / 매칭
- sender : ForeignKey(User)
- receiver : ForeignKey(User)
- is_liked : BooleanField
- is_matched : BooleanField
서로 호감있는 사람들끼리 매칭시켜주기 위한 매칭 테이블입니다. sender는 좋아요를 보낸 사람, receiver는 좋아요를 받는 사람에 대한 외래키입니다. is_liked는 좋아요, 싫어요를 구분하는 필드이고 is_matched는 서로 매칭된 경우를 확인하는 필드입니다.
ChatRoom / 채팅방
- uuid : UUIDField
- users : ManyToManyField(User)
- is_active : BooleanField
uuid는 Profile의 uuid와 동일하게 외부에서 채팅방을 구분하는 용도로 사용되며 users는 채팅방에 참가한 인원들입니다. is_active는 채팅방 활성 상태를 뜻합니다. 이 프로젝트에서는 채팅방 인원이 최대 2명이므로 둘 중 한 명만 나가도 채팅방이 불활성 상태로 변하여 더이상 메시지를 보낼 수 없도록 하였습니다.
ChatRoomMember / 채팅방 멤버
- room : ForeignKey(ChatRoom)
- user : ForeignKey(User)
- is_active : BooleanField
채팅방에 소속된 인원에 대한 정보를 나타냅니다. is_active 값에 따라 멤버가 채팅방을 나갔는지 안나갔는지 구분하는 요소로 판단합니다.
ChatMessage / 채팅 메시지
- uuid : UUIDField
- room : ForeignKey(ChatRoom)
- user : ForeignKey(Django User)
- message : CharField
채팅방 메시지를 저장하는 테이블입니다. uuid는 역시 다른 테이블과 동일하게 외부에서 각 메시지를 구분하는 용도로 사용됩니다. 특히 이 uuid 부분은 Frontend에서 중복된 메시지를 잡아주는데 도움이 됩니다.
예를 들어, 소켓 통신으로 실시간 채팅 중에 상대방이 메시지를 보냄과 동시에 받는 사람이 페이지 Refresh를 하게 되면, 소켓 통신으로 들어오는 신규 메시지와 DB에 저장된 같은 메시지를 가지고와서 중복으로 메시지가 표시될 수 있습니다.
TemporaryPasswordIssueHistory / 임시 비밀번호 발급 이력
- user : ForeignKey(User)
임시 비밀번호 발급 시, 발급 이력을 저장하는 테이블입니다. 생성날짜 created_datetime 필드를 이용하여 임시 비밀번호 발급 횟수를 조절합니다. (예를 들어, 15분당 1번씩 임시 비밀번호 발급) 보통 유료로 사용되는 서비스인 SMS의 무제한 사용을 방지하기 위한 목적입니다.
'Project > [Project] Red Horse' 카테고리의 다른 글
[Backend] env 환경변수 (0) | 2023.06.12 |
---|---|
[Backend] 프로젝트 환경셋팅 (0) | 2023.06.12 |
[Backend] API 설계 (0) | 2023.06.09 |
[Backend] 화면 설계 (0) | 2023.06.06 |
Intro. (0) | 2023.06.06 |