RDB 설계시 고려해야할 것들

dvnchi·2025년 1월 18일
3

Inhu

목록 보기
1/4
post-thumbnail

Inhu는 인하대 후문의 술집, 밥집, 카페 등 여러 장소를 소개하고 추천해주는 서비스입니다.

안녕하세요. Inhu 프로젝트의 백엔드 개발을 담당하고 있는 팀원입니다.
오늘은 RDB를 설계하는 과정에서 고민했던 점들을 나열해보고자 합니다.

링크 : https://inhu.kr


💁‍♀️ 서비스의 도메인 모델들에 대하여

서비스는 사용자(user)와 장소(place)를 주요 도메인 모델로 정의하고 있습니다.
사용자는 장소에 리뷰(review)를 작성하거나 북마크(bookmark)를 추가할 수 있습니다.
이외에도 서비스 개선을 위한 의견이나 피드백을 관리하는 도메인 모델이 존재합니다.


⭐️ 공통 설계에서의 고민

💡 1. varchar(10) vs varchar

nickname, content, password ... 많은 컬럼에 글자수 제약이 들어갑니다.
DB 레벨에서 글자수 제한을 설정하는 것은 유연성 측면에서 적절하지 않다고 판단했습니다.
향후 정책 변경이나 예상치 못한 요구사항 증가 시 DB까지 가서 제약사항을 변경해줘야하기 때문입니다.
또한 제한을 두더라도 애플리케이션 레벨에서 별도로 검증을 처리하기 때문에, 데이터베이스 레벨에서의 글자 제한은 기술적으로 필수적이지 않다고 판단하였습니다.
따라서 모든 글자수 제약이 필요한 column들은 varchar 형식으로 선언하였습니다.

💡 2. Soft Delete vs Hard Delete

데이터를 삭제하는 방식에는 크게 Hard DeleteSoft Delete가 있습니다.

Hard Delete: 데이터를 영구적으로 삭제하여 복구가 불가능합니다.
Soft Delete: 데이터를 실제로 삭제하지 않고, 삭제된 상태임을 나타내는 플래그(예: deleted_at에 타임스탬프 기록)를 추가하여 논리적으로만 삭제를 처리합니다.

처음에는 Hard Delete를 기본 방식으로 고려했으나 히스토리를 관리하기 어렵고, 실수로 중요한 데이터를 삭제했을 때, 복구할 수 없다는 단점이 있었습니다.

이러한 Hard Delete의 단점을 해결하기 위해 Soft Delete 방식을 떠올렸습니다.

현재 기획 중인 서비스에서는 삭제된 데이터의 양이 많지 않을 것으로 예상되므로, 데이터 복구와 히스토리 관리의 중요성이 더 크다고 판단했습니다. 따라서 주요 테이블에 deleted_at column을 추가하여 Soft Delete 방식을 사용했습니다.

그렇다면 모든 Table에 deleted_at column을 추가해야할까요?

review_image 테이블은 review의 하위 데이터로, review의 삭제 상태에 종속됩니다.
따라서 review 테이블에서 soft delete가 처리되면 review_image는 접근 대상에서 제외되므로, 별도로 review_image 테이블에 deleted_at 컬럼을 추가할 필요가 없습니다.

💡 3. 테이블 이름 짓기 : 범용성 고려

Place 테이블과 연결된 테이블로는 장소 카테고리(place_type), 장소의 메뉴(place_menu), 장소 사진(place_image), 장소 운영시간(place_hours)의 정보를 담고 있는 테이블들이 존재합니다. 해당 테이블들은 모두 Place 테이블 산하에서 이용되기 때문에 Place_XXX형식으로 테이블 이름을 지었습니다.
그러나 review 테이블과 연결된 테이블 중 keyword 테이블은 단순히 리뷰의 키워드를 담는 역할뿐만 아니라, 이후 확장될 수 있는 여러 기능에서 활용될 가능성이 있습니다. 예를 들어 사용자의 관심 키워드를 저장하는 기능이나, 특정 장소에서 진행중인 이벤트 키워드 등 리뷰가 아닌 곳에서도 키워드가 사용될 수 있기 때문에 review_keyword 가 아닌 keyword 로 정의하였습니다.

💡 4. 컬럼명 중복으로 인한 혼란 방지

place_hours는 장소의 운영시간을 담고있는 테이블입니다. 원래는 영업 시작 시간을 open, 영업 종료 시간을 close로 정의했었습니다.
그런데 place에서 언제 폐점했는지에 대한 정보를 담고있는 closed_at column을 추가하게 되니, '영업 종료'를 뜻하는 close column과 '폐점'을 뜻하는 closed_at 컬럼이 똑같은 close지만 엄연히 다른 의미를 가지게 되었습니다. 헷갈릴 수 있으니 place_hours 테이블의 open,close 컬럼을 start, end로 바꿔주었습니다.

💡 5. Mapping Table을 활용한 M:N 관계 구현

🔗 M:N 관계란?
서비스에서는 하나의 review가 여러 개의 keyword를 선택할 수 있고, 반대로 하나의 keyword가 여러 개의 review에서 사용될 수 있습니다.
이러한 관계를 M:N 관계라고 합니다.

🔧 Mapping Table
이 M:N 관계를 효율적으로 관리하기 위해 Mapping Table을 구현하였습니다.
Mapping Table은 각 테이블의 Primary Key(PK)를 가져와 Foreign Key(FK)로 설정한 중간 테이블입니다.

위 그림에서 review_keyword_mappingreviewkeyword 간의 관계를 관리하기 위한 Mapping Table입니다.
review_idx: review 테이블의 PK를 참조하는 FK.
keyword_idx: keyword 테이블의 PK를 참조하는 FK.


⭐️ user table에서의 고민

💡 6. 카카오 로그인과 애플 로그인 구분하기

이 서비스에서는 카카오 로그인과 애플 로그인을 지원합니다. 두 로그인 방식에서 제공하는 식별값(sns_id)은 모두 string 타입으로 동일합니다. 따라서 로그인 방식을 구분하기 위한 방안이 필요했습니다.

각 로그인 방식에 대한 식별값은 sns_id column에 저장할 수 있지만, 이를 로그인 방식별로 구분할 수 있는 추가 컬럼이 필요했습니다. 이 column에 smallint 타입을 사용하면 0 또는 1로 간단히 구분할 수 있어 메모리 사용량이 적고, 관리가 용이하다고 판단했습니다.

provider : smallint 타입으로 설정하여 0은 카카오, 1은 애플로 구분합니다.
sns_id : 각 로그인 방식에서 제공하는 고유 식별값을 저장합니다.

이 방식을 통해 로그인 방식과 고유 식별값을 효율적으로 관리할 수 있게 되었습니다.

💡7. local login에서 1:1 관계 보장하기

만약 id, password를 입력하여 로그인하는 local login 기능도 존재하는 경우 어떻게 해야할까요?
먼저 local_login 테이블을 정의해야합니다.

📌 문제점
local_login의 user_idx 컬럼이 user의 idx 컬럼을 참조하도록 FK 제약을 설정했습니다. 그러나 이 방법은 하나의 유저가 여러개의 local_login 데이터를 가질 수 있다는 문제가 있습니다. 이는 데이터 무결성 문제가 발생하게 됩니다.

💡 해결방법
다음은 수정된 local_login 테이블입니다.

local_login 테이블에서 idx를 생략하고 user_idxPK이자 FK로 설정하면 1:1 관계를 보장할 수 있습니다.


⭐️ place table에서의 고민

💡 8. 가격은 항상 int형일까?

place_menu 테이블의 price column은 메뉴의 가격을 저장하기 위해 사용됩니다.
그러나 기획 단계에서, 모든 가격을 int로만 저장할 수 없다는 점을 발견했습니다.

📌 문제점

  • '싯가'와 '변동' 같은 메뉴의 존재

    • 횟집이나 특수한 경우에는 메뉴 가격이 고정되지 않고 '싯가'나 '변동'으로 표시됩니다. 이러한 경우 priceint로 정의하면 표현할 수 없다는 문제가 있었습니다.
  • 그렇다면 int 말고 varchar로 저장하면 되나?

    • 가격을 varchar로 저장하면 'ㅏㅏㅏ' 같은 비정상적인 데이터가 들어올 수 있습니다. 이는 데이터 무결성을 유지하기 어렵게 만듭니다.

💡 해결 방법
위 문제를 해결하기 위해 price 외에 is_flexible이라는 boolean column을 추가하기로 결정했습니다.

  • is_flexibletrue인 경우: '변동'이라는 값을 표시
  • is_flexiblefalse인 경우: 고정된 가격 표시

💡 9. 장소 폐업과 장소 삭제는 다르다

inhu 프로젝트는 인하대 후문의 모든 장소를 소개하고 추천하는 서비스입니다.
현재 서비스에는 없지만, 지금은 운영을 종료했지만 과거에는 많은 인하대 학생들의 사랑을 받았던 '추억의 장소'를 소개하는 서비스를 추후에 고려중입니다.
따라서 해당 장소가 폐업했는지에 대한 정보도 필요했습니다.

처음에는 place 테이블의 deleted_at column을 활용해, deleted_atNULL이 아니면 장소가 폐업된 것으로 간주하는 방안을 고려했습니다. 그러나 예를 들어, 가게 사장님이 "우리 가게를 이 서비스에서 삭제해주세요"라고 요청하면, 가게가 여전히 정상적으로 운영 중임에도 불구하고 이 가게는 '폐업' 처리됩니다.

그래서 저희는 장소가 폐업된 시점을 저장하는 closed_at column 을 추가하여 폐업과 삭제를 분리하여 관리하고자 하였습니다.


💡 자잘하게 고민했던 것들

❗️ image_url vs image_path

이 서비스에서는 S3 BUCKET을 사용하여 이미지를 저장할 계획입니다.
저장시 image_url(ex: https://bucket-name.s3.amazonaws.com/folder/image.jpg)과 image_path(ex: folder/image.jpg) 방식 중 어떤 것을 사용할지 고민했습니다.
서비스의 확장성과 유지 보수를 고려했을 때, image_path를 사용하는 것이 더 적합하다고 판단했습니다.
버킷 이름이나 도메인 변경이 발생할 가능성을 대비하여, 상대 경로를 저장하고 이를 통해 유연하게 URL을 생성할 수 있도록 설계했습니다.

❗️ is_deleted(bool) vs deleted_at(timestamp)

Soft Delete를 구현할 때, 데이터를 삭제된 상태로 관리하는 방법으로 2가지를 떠올렸습니다.

is_deleted : 삭제여부만을 저장하는 column (bool)

deleted_at : 삭제시기를 저장하는 column (timestamp)

bool과 timestamp의 저장 공간 차이가 크지 않기 때문에 데이터 복구와 히스토리 관리의 장점이 더 크다고 판단하여 deleted_at 방식을 선택했습니다.

❗️ 전화번호는 항상 Unique일까?

전화번호를 저장하는 tel column은 장소 정보를 관리할 때 중요한 역할을 합니다. 초기에는 전화번호를 고유하게 관리하기 위해 unique 제약 조건을 설정하는 방안을 고려했지만, 법인 전화번호는 여러 장소에서 공유될 수 있다는 것을 알게되었습니다.
따라서 tel column에 unique 제약조건을 설정하지 않게 되었습니다.


💡 가장 어려웠던 부분

M:N 관계를 구현하기 위해 Mapping Table을 설계하는 것이 가장 어려웠습니다.
각 테이블의 Primary Key를 조합하여 관계를 정의하는 방식이 처음에는 혼란스러웠고, 중복 데이터를 방지하면서 관계를 효율적으로 관리하는 방법을 고민하는 데 시간이 걸렸습니다.


💡 결론

테이블 설계는 단순히 데이터 저장을 넘어서 다양한 비즈니스적, 기술적 요구를 고려해야 하므로 쉽지 않았습니다.
혼자 했다면 놓쳤을 세부사항들을 팀원들과 논의하면서 더 촘촘하게 설계할 수 있어 큰 도움이 되었습니다.
글 읽어주셔서 감사합니다.

0개의 댓글