본문 바로가기
✨ 프로젝트/EatToFit [F-Lab]

[EatToFit] DB 설계 과정에서의 고민

by dev_writer 2024. 9. 15.

안녕하세요 dev_writer입니다. 이번에는 이전 글에서 말씀드렸듯이 프로젝트의 DB 설계 과정에서 발생한 고민들을 작성해 보겠습니다.
 

1. JPA DDL 자동 생성에만 의존하여 DDL을 까먹지 말자.

가장 크게 느꼈던 점입니다. 그동안 프로젝트를 할 때에는 언제부턴가 JPA의 DDL 옵션을 create로 하면 자동으로 데이터베이스 테이블들을 만들어주니, 크게 신경 쓰지 않았었습니다.
 
하지만 JPA DDL을 자동 (create, create-drop)으로 만들도록 하면 다음 단점이 있습니다.

  • create로 할 경우, 애플리케이션을 재시작할 때마다 자동으로 기존 데이터를 전부 삭제하고 다시 만들게 된다.
  • create-drop으로 할 경우에도 기존 테이블을 삭제하는 것은 동일하다.
  • 위의 둘 중 어떤 것을 하더라도 스키마 정의 (ex: String일 경우 자동으로 varchar(255))를 수동으로 관리하기 어렵다.

결과적으로, 실제 실무에서는 (또는 프로덕션 단계일 경우) 자동으로 만든다면 기존 데이터를 전부 삭제하는 것이 필연적으로 발생하게 된다는 단점이 있기에 가급적 사용하지 않는 것을 알게 되었습니다.
 
또한 과도하게 JPA에 의존하게 될 경우 DDL 작성법을 아예 까먹어버릴 수도 있습니다. 물론 빠른 개발을 위해서는 (로컬 개발 환경에 한해) 자동 생성 옵션을 걸어도 괜찮다고 생각을 하지만, 저는 DDL 작성법을 복습할 겸, 각 테이블의 스키마 (varchar, char, int 등)를 서비스 상황에 최대한 맞추어 적용해보고 싶어 서비스 개발을 하기 전 DB 테이블 설계를 먼저 진행해 보기로 했습니다. (실제 개발을 하고 있는 지금도, 초기 확인을 위해 create로 잠깐 해 본 뒤에는 none으로 설정하였습니다.)
 
이것을 해결하기 위한 방법으로 flyway를 도입하였는데, 관련 내용은 개발 단계에서의 글에서 다루겠습니다.
 

2. 데이터 타입 고민

2-1. MySQL의 char와 varchar는 언제 써야 할까?

JPA에서는 String 타입의 데이터에 대해서는 기본적으로 varchar(255)를 만들어주고 있습니다.

JPA로 DDL 자동 생성을 했을 때 정의되는 email의 타입 (길이는 @Column에서 length가 기본적으로 255 입니다.)

 
그런데 개발을 하지 않고 데이터베이스 설계를 하다 보니, MySQL에서는 varchar 말고도 char 타입이 있다는 사실을 알게 되었습니다.

char

  • 고정형 문자열
  • 경우에 따라 데이터가 낭비될 수 있음
  • 추가적인 연산이 필요하지 않아 검색 속도 및 조회 속도가 varchar에 비해 빠름
  • 글자 수가 고정되는 경우 (주민등록번호 등)에 사용하면 효율적 이용 가능

varchar

  • 가변형 문자열
  • 입력받은 데이터의 크기가 작을 경우 해당 크기만큼 메모리 차지 - 메모리 절약
  • 4글자 이하의 varchar는 char로 자동 변환됨
  • 데이터 삽입 시 데이터 값 + 문자열의 길이를 함께 저장
  • 추가적인 연산 (데이터 길이 확인 연산) 필요, 미세한 속도 발생 가능하나 char와 거의 차이 나지 않음
  • 수정 상황 발생 시 블록을 추가로 생성하여 I/O 시간이 추가로 소모됨

이때 varchar의 수정 상황은, 기존보다 긴 길이를 가진 문자열로 수정하였을 때 약점이 발생합니다. char는 고정 문자열 형태이기에 기존에 할당받은 메모리 공간을 계속 사용하지만, varchar는 이전보다 긴 문자열로 수정할 시 기존에 할당된 공간을 활용하지 못하고 새로운 공간을 할당받는 작업이 발생하기 때문입니다.
 
그래서 값의 길이 편차가 작거나, 잦은 변경 가능성이 있을 경우 char를 선택하는 게 더 나은 판단일 수 있습니다.
 

본 서비스에서는 어떤 경우에 어떤 타입으로 써야 할까?

String 타입이 사용되는 시점은 크게 다음과 같습니다.

  • 회원 정보
    • 이메일: 수정 불가능, unique
    • 닉네임: 수정 가능, unique, 최대 20자 제한
  • 음식 이름: 수정 가능
  • 플랜 이름: 수정 가능

이때 위에서 파악한 점을 적용하려 하였고, 그 결과 아래 결론을 내렸습니다.

  • 회원 정보
    • 이메일: 절대 변경되지 않고, 그렇기에 수정 상황이 발생하지 않음. 30자 제한 - char
    • 닉네임: 변경 가능성이 있으나, 빈도를 제한할 예정. 20자 제한 - char
  • 음식 이름: 변경 가능성 있음, 빈도 높을 것으로 예상, 길이 제한 없음 - varchar
  • 플랜 이름: 변경 가능성 있음, 빈도 높을 것으로 예상, 길이 제한 없음 - varchar

바로 위에서 잦은 변경 가능성이 있을 경우 char를 선택하는 게 더 나은 판단일 수 있다고는 하였지만, 회원이 먹은 음식 이름, 회원이 진행한 플랜 이름은 회원이 마음대로 길이를 설정할 수 있습니다. (ex: 단순히 '짜장면'에서 '20XX 년 XX월 XX일 졸업식에서 먹은 짜장면' 이런 식으로 수정할 수도 있습니다. 혹은 다시 복구할 수도 있습니다.) 그렇기에 사이즈 고정을 하는 char를 사용한다면 아무리 수정 작업에서 유리하다고 해도, char 길이만큼의 길이를 가지지 않은 문자열로 저장할 일도 빈번할 것이기에 문자열 크기에 유연하게 대처하는 varchar를 통한 이점이 더 클 것이라 판단하였습니다.
 
회원의 이메일과 닉네임은 20~30자 제한으로, 우선 해당 사이즈로 인해 발생할 수 있는 메모리 낭비는 크지 않을 것이라고 판단했습니다. 특히 이메일은 @gmail.com 등 아이디 이후 점유하게 되는 크기가 기본적으로 있다 보니, 더 그런 결론을 내릴 수 있었습니다. 그리고 신규 회원이 가입할 때마다 회원의 이메일과 닉네임 존재 여부를 항상 체크하는데, 이러한 점에서 조회 속도에서 더 유리한 char를 도입하면 좋겠다는 생각이 들었습니다.
 

2-2. 숫자 타입도 종류가 있다.

데이터베이스에 저장할 숫자 타입에 대해서도 고민하게 되었습니다.
 
데이터베이스에 저장되는 id에 대한 타입은 자바에서는 Long이고, 이에 상응하는 MySQL의 최대 타입은 BIGINT입니다. 이는 위의 DDL 예시 사진에서도 보실 수 있습니다. 자바에서의 타입을 Long으로 하면 혹시 모를 데이터 저장의 범위 확장 (Integer는 21억까지만 가능, Long은 그 이상 가능하기에)과 NULL 값을 넣을 수 있다는 점이 있어 그대로 사용하도록 하였습니다.)
 
그러나 아직 아래 부분들에 대한 타입 고민이 남았었습니다.

  • 스포츠 플랜
    • 진행 시간 (time, 초 단위)
    • 남은 진행 시간 (remain_time, 초 단위)
  • 피트니스 (= 헬스) 플랜
    • 세트 횟수 (size)
    • 한 세트에서의 횟수 (repeat)
    • 남은 세트 횟수 (remain_size)
    • 무게 (weight, kg 단위)

MySQL의 정수 유형을 살펴보겠습니다.

데이터 타입 저장 크기 (바이트) 부호 있는 범위 부호 없는 범위 설명
BOOL / BOOLEAN 1 0 ~ 1 0 ~ 1 TINYINT(1) 별칭, 논리형
TINYINT 1 -128 ~ 127 0 ~ 255 작은 정수 저장에 사용
SMALLINT 2 -32,768 ~ 32,767 0 ~ 65,535 중간 크기의 작은 정수
MEDIUMINT 3 -8,388,608 ~ 8,388,607 0 ~ 16,777,215 중간 크기의 정수
INT / INTEGER 4 -2,147,483,648 ~ 2,147,483,647 0 ~ 4,294,967,295 일반적인 정수 타입
BIGINT 8 -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 0 ~ 18,446,744,073,709,551,615 매우 큰 정수

 
이러한 점을 볼 때, 다음과 같은 결론을 내렸습니다.

  • 스포츠 플랜: 모두 초 단위, SMALLINT의 최대 범위인 65,535초는 약 18.2시간, 운동을 (쉬는 시간을 포함하더라도) 18.2시간을 초과하는 경우는 물리적으로 불가능할 것이기에 SMALLINT로 정의
  • 피트니스 플랜
    • 세트 횟수: 자연수, 256세트 이상을 진행하는 경우는 없을 것으로 판단. TINYINT로 정의
    • 무게: kg 단위, 255kg을 초과하는 경우가 많이 있을까? 현재까지는 없을 것으로 판단됨. (뿐만 아니라 세트 횟수도 존재하기 때문) - 평균적인 헬스케어 서비스를 만든다고 가정했을 시 TINYINT로 진행해도 괜찮다는 생각이 듦

문자, 숫자형 데이터 타입에 대해 현실적으로 생각해 봤을 때 어떤 타입을 쓰는 게 자연스러울지를 고민할 수 있었던 시간이었습니다. 더불어, 특히 문자 데이터 타입을 결정할 때 완벽하게 적합한 것을 선택해야 한다는 강박에 시달렸는데, 나름의 기준을 가지고 개발한다면 충분히 괜찮은 선택이 될 수 있음을 인정하자는 배움을 얻기도 하였습니다.
 

3. ENUM을 데이터베이스에서 남발하지 말자.

다음으로는 ENUM을 남발했던 상황을 수정하게 된 케이스를 말씀드리겠습니다.

수정 전의 회원 - 선호 운동 관계

 
기존에는 회원과 선호 피트니스 (헬스), 선호 스포츠의 관계가 위와 같았습니다.
회원이 여러 [선호 피트니스, 선호 스포츠]를 가지고, 각각의 prefer_fitness, prefer_sports에 들어갈 수 있는 이름은 enum으로 관리하였습니다. (사진으로 나타내기 위해 줄였고, 실제로는 더 많습니다.)
 
enum을 계속 쓰게 된다면 어떤 문제가 있을까요?
 
간단한 예시로 회원 10명이 선호 피트니스로 '덤벨프레스'를 가진다고 해 보겠습니다. 그러면 조회 시 아래처럼 저장됩니다.

저장된 더미 데이터

 
이때 맨 오른쪽에 name으로 모두 '덤벨프레스'가 저장됨을 보실 수 있습니다. 그리고 한글은 한 글자 당 3바이트를 차지하고, 결국 한 회원 당 15바이트의 '덤벨프레스'를 가지고 있습니다.
 
만약, name을 직접 enum으로 관리하지 않고 FK로써 참조하도록 한다면 어떻게 될까요? enum으로 직접 prefer_fitness, prefer_sports에 넣는 게 아니라 fitness, sports 테이블을 별도로 어드민 전용으로 만들고, 이것을 통해 만든 id를 회원들이 참조하도록 하는 것입니다.
 
자바의 Long에 대치되는 MySQL의 BIGINT로 설계하면 8바이트이니, 약 절반 조금 안 되는 공간을 절약할 수 있습니다.
 
또한, enum을 직접 사용하지 않고 FK로써 관리한다면 데이터 추가 시에도 유리합니다.
 
만약 enum에서 데이터를 수정 (즉, 새로운 운동을 추가/수정할 경우)하는 상황이라면, ALTER TABLE 명령어를 통해 ENUM의 대상이 되는 값을 추가/수정해야 하는데 이 과정에서 속도가 느려질 수 있습니다. 만약 ENUM 대신 참조 방식을 이용한다면, 단순히 INSERT / UPDATE / DELETE를 이용하면 되어 ENUM 방식보다 더 빠를 수 있습니다.
 
따라서 아래와 같이 fitness, sports 테이블을 따로 두도록 하였습니다. (이 부분이 어드민 관점에서도 편리할 것이라 생각하였습니다. 향후 가능한 피트니스, 스포츠에 대한 생성/조회 등을 어드민 전용으로 따로 개발할 예정이기 때문입니다.)

수정 후의 회원 - 선호 운동 관계 (운동 이름은 변하지 않고 고정적일 것이기에 char를 선택)

 
다만 다른 부분 (ex: 회원의 운동 경력 등)에도 전부 FK 및 테이블을 생성하는 방식으로 하는 것은 올바른 접근이 아닌 것 같아, 다른 케이스들에서 발생할 수 있는 ENUM 고민은 추후 다루도록 하겠습니다.
 
다음 글에서는 드디어 프로젝트 개발을 하며 마주쳤던 고민에 대해 다루겠습니다. (그 사이에 DB 고민점이 추가로 발생한다면 DB 고민점 글을 다시 올릴 것 같습니다.)

Reference

 

DB ERD Link

프로젝트 DB ERD 링크를 함께 첨부하겠습니다. 아직 확정은 아니며, 변경 가능성이 있습니다.
https://dbdiagram.io/d/EatToFit-final-66c081d08b4bb5230e5bcc65