올바른 데이터베이스 설계란?

2025. 11. 1. 20:07·TIL

0. 좋은 DB 설계하기

 

좋은 DB 를 설꼐하기 위해서 제가 고민한 부분은 다음 3가지였습니다.

 

  1. 무결성 : 저장된 값은 제약사항을 어기지 않는 정확한 값을 유지함을 보장
  2. 유연성 : 요구사항 변화에 대해 수정이 용이
  3. 확장성 : 기능 확장에 대한 수정이 용이

다만, 이 세가지를 모두 완벽하게 만족할 수 있는 설계는 현실적으로 드물다. 

결국 DB 설계는 트레이드오프를 이해하고, 서비스 상황에 맞게 선택하는 과정에 가깝다고 느꼈다.

 


 

1. 예비 테이블 만들기

가장 먼저 화면설계서, 기능정의서를 바탕으로 DB 에 저장되어야 할 필드가 무엇이 있을지 목록을 뽑아본다.

 

이번 서비스에서 저장해야 할 필드들의 목록은 다음과 같이 정리했다.

 

저장해야 할 데이터 목록
  • 프로필 이미지
  • 이메일
  • 비밀번호
  • 닉네임
  • 첫 로그인 여부
  • 건강 상태 설문조사에 대해 각 문항에서 선택한 답변
  • 알림 설정
    • 알림 간격
    • 활동 시간
    • 제외 시간
    • 요일 반복
  • 선택된 캐릭터 종류
  • 캐릭터 이름
  • 레벨
  • exp
  • 연속 학습일수
  • 사용자 활동 기록
  • 내 운동 플랜
    • 설문 기반 분석 결과
    • 현재 설정되어 있는 운동 루틴
  • 방해 금지 설정
  • 운동 별 정보
    • 운동 이름
    • 운동 내용
    • 운동 효과
  • 운동 진행 정보
    • 진행한 루틴 정보
    • 알림 응답 성공 여부
    • 각 루틴별 성공 여부
    • 각 루틴별 정확도
    • 스트레칭 진행 중 회원 정보
  • 퀘스트 정보
    • 퀘스트명
    • 퀘스트 이미지
    • 퀘스트 종료 시점
    • 퀘스트 태그
    • 현재 사용자의 퀘스트 진행 상황
    • 퀘스트 성공 여부
    • 퀘스트 성공 시점
  • 게시글
    • 제목
    • 본문
    • 태그
    • 이미지들
    • 작성자 정보
    • 좋아요 개수

 

이제 이 목록을 기반으로 데이터들을 적절히 묶어서 예비 테이블을 생성한다.

1. 예비 테이블 생성

 

 


 

2. 이상적인 테이블 구조를 기준으로 다시 나누기

2.1 기본키는 비즈니스 값과 분리한다.

기본키의 경우 비즈니스 로직과 무관한 값으로 별도 컬럼으로 추가하는 것이 유연성을 줄 수 있다고 판단했습니다.

 

가령 이메일이나 닉네임과 같이 정책에 따라서 수정될 수 있는 값을 PK 로 사용하게 되면 추후 변경 비용이 커질 수 있습니다.

2.1 기본키 추가

 

2.2 테이블이 단일 주제를 표현하는지 확인한다

단일 주제라 함은 테이블에 존재하는 컬럼들이

 

  • 함께 수정되는가 ?
  • 함께 생성되는가 ?
  • 함께 검증되는가 ?
  • 함께 삭제되는가 ?

 

즉, 같은 생명주기와 규칙을 공유하는 데이터인가를 보는 것입니다.

 

이 기준으로 구조를 다시 보면 한 테이블 안에 여러 주제가 섞여 있는 부분들이 많았습니다.

 

  • 회원 테이블
  • 생명주기가 일치 하지 않는 주제들이 함께 존재
  • 기존 회원 테이블에는 식별 / 인증과 관련한 내용만 남기고 나머지는 분리합니다.
  • 좋아요
  • 좋아요는 게시글의 단순 속성이 아니라 사용자가 게시글에 대해 수행한 행위이다.
  • 게시글의 내용과는 생명 주기가 다르므로 별도로 분리합니다.

 

  • 퀘스트
  • 진행 쇳수 / 성공 시점의 경우 유저별로 별개의 값을 가져야 합니다.

 

  • 알림 설정 / 설문 응답
  • 알림 설정은 여러 속성으로 구성되는 독립 주제이고, 설문 응답 역시 문항 / 선택지 / 응답이라는 구조를 가지므로 분리합니다.

2.2 테이블을 단일 주제로 분리

 

2.3 다중값, 다중 부분 필드를 제거한다

한 컬럼 안에 여러 값이 들어가거나, 하나의 개념 안에 다시 여러 요소가 섞여 있는 부분도 분리 대상으로 보았습니다.

 

대표적으로는 다음과 같습니다.

 

루틴

루틴 자체의 메타 정보와, 루틴을 구성하는 운동 목록이 섞여 있었습니다.

따라서

  • 루틴
  • 루틴 단계

를 나누고, 단계마다 어떤 운동이 포함되는지 별도 테이블로 표현했습니다.

 

루틴 진행 정보

운동별 결과를 한 곳에 몰아 넣는 대신,

  • 세션 단위 정보
  • 각 운동 단계별 수행 결과

를 분리했습니다.

 

태그

태그는 하나의 게시글이 여러 개를 가질 수 있으므로, 별도 tag 테이블과 매핑 테이블로 분리했습니다.

 

설문 응답

설문은 여러 문항과 선택지, 응답이 존재하므로 이를 각각 분리해서 표현했습니다.

2.3 다중값, 다중 부분 필드 분리

 

2.4 계산된 필드는 저장하지 않는다

현재 계산된 값이 저장되어 있는 필드는 존재하지 않았습니다.

 

2.5 최소한의 중복 데이터를 포함한다

현재 각 테이블에 중복되어 데이터를 저장하고 있지는 않습니다.

 


 

3. 필드 명세 정하기

테이블 구조를 어느 정도 정리한 뒤에는 각 컬럼의 이름과 타입, 길이, null 허용 여부, 기본 값, 유니크 여부 등을 결정합니다.

 

TEXT vs VARCHAR

둘 다 가변 길이 문자열을 저장할 수 있지만,
VARCHAR는 길이 제한을 명시할 수 있다는 점이 중요합니다.

그래서 다음과 같이 구분했습니다.

  • 닉네임, 이메일
    • 사용자가 입력하고 정책상 최대 길이가 정해져 있으므로 VARCHAR
  • 설문 문항 내용, 운동 설명
    • 길이를 미리 엄격히 제한할 필요가 적으므로 initially TEXT 고려

다만 이후 피드백과 추가 조사 과정에서, TEXT는 성능과 의미 표현 측면에서 남용하지 않는 것이 낫다고 판단하여 짧고 길이를 예측 가능한 데이터는 VARCHAR로 다시 조정했습니다.

 

숫자 타입의 선택

 

모두 BIGINT 를 사용하면 편리하지 않을까 ? 생각할 수 있지만, 숫자 타입은 단순히 저장 가능한 범위 이상의 의미를 갖습니다.

 

- 1. 도메인 의미 표현

  •   예를 들어 레벨을 BIGINT로 저장하면, 이 값은 엄청 큰 범위까지 갈 수 있는 값인가?라는 오해를 줄 수 있습니다.
  •   DB 스키마는 단순한 저장 구조가 아니라, 도메인 의미를 드러내기도 합니다.

 

- 2. 인덱스 / 캐시 / 메모리 비용

  •   행 수가 적을 때는 체감이 작아보여도, 데이터가 많아지고 인덱스가 붙으면 크기 차이가 누적됩니다.
  •   타입이 커질수록 인덱스의 크기가 커지고, 캐시 효율이 저하될 수 있습니다.

 

- 3. 작은 타입에서 큰 타입으로 늘리는 비용은 비교적 낮다

  •   - 반대로 처음부터 큰 타입을 쓰고, 나중에 줄이는 것은 어렵습니다.

 

DATETIME vs TIMESTAMP

 

시간 타입도 고민이 필요했습니다.

  • TIMESTAMP는 timezone과 더 밀접하게 동작하지만, 2038년 문제 등 제약이 존재합니다.
  • DATETIME은 특정 로컬 시간값을 그대로 저장합니다.

우리 서비스는 일단 국내 시장을 기준으로 시작하므로, 이번 설계에서는 DATETIME을 기준으로 사용하기로 했습니다.

3. 필드 명세 정하기 - 1
3. 필드 명세 정하기 -2

 


 

 

4. 관계 설정

식별 관계 vs 비식별 관계

ERD 설계 중 가장 고민했던 부분 중 하나는 부모 - 자식 관계를 식별 관계로 보아야 하는지, 비식별 관계로 보아야 하는지였습니다.

 

식별 관계

- 자식 테이블의 PK 에 부모 PK 가 포함된다.

- 부모 없이 자식이 존재할 수 없고, 생명 주기가 강하게 결합된다.

 

비식별 관계

- 자식은 독립적인 PK 를 갖는다.

- 부모 PK 를 FK 로만 참조한다.

 

이론상 식별 관계는 무결성 측면에서 엄격합니다. 

하지만 실무에서는 주로 연관관계 매핑을 위해 JPA 를 사용하기 때문에, 식별 관계가 많아질수록 매핑 난이도가 올라가고 PK 가 비대해진다는 문제가 있습니다.

 

그래서 처음에는 논리 모델(ERD)은 식별 관계, 물리 모델(DB / ORM)은 비식별 관계 로 나누어 매핑하게 되었습니다.

 

하지만 이후 피드백을 받고 다시 생각해보니, ERD는 팀 전체가 공유하는 명세서이기 때문에 실제 구현과 달라지면 오히려 오해를 만들 수 있었습니다.

 

결국 최종적으로는 ERD와 실제 구현을 일치시키는 방향으로 수정했고, 복합키 대신 비식별 기본키를 도입하는 방식으로 정리했습니다.

 

4. 연관관계 매핑 ( 해당 이미지는 식별 관계를 포함했을 때의 ERD 입니다 )

 


 

5. 실무와 관련한 문제들 반영하기

실제 운영을 생각하면 반드시 추가로 고려되어야 하는 문제들이 있습니다.

 

생성일시 / 수정일시

생성일시는 거의 모든 테이블에서 필요하다고 판단했습니다.

  • 정렬
  • 분석
  • 로그 추적

등에서 매우 자주 사용되기 때문입니다.

 

반면 수정일시는 모든 테이블에 무조건 필요하지는 않았습니다.

 

로그나 이력성 데이터처럼 수정되지 않는 테이블도 있기 때문입니다. 그래서 수정일시는 테이블 특성에 따라 선택적으로 추가했습니다.

 

Soft Delete

실무에서는 데이터를 실제로 테이블에서 삭제하는 경우보다, soft delete 를 적용하는 경우가 대부분입니다.

 

이유는 다음과 같습니다.

  • 운영상 복구 필요
  • 감사 / 추적
  • 과거 기록 활용

 

다만 모든 테이블에 soft delete 를 넣는 것은 과하다고 판단하여 복구나 감사가 필요한 테이블에만 선택적으로 적용했습니다.

 

예약어와 enum

예약어 문제

order 같은 이름은 예약어 충돌 가능성이 있어 다른 이름으로 조정했습니다.

 

enum 사용 문제

처음에는 enum을 DB 컬럼에 그대로 반영하는 방식을 고려했습니다.
하지만 운영 관점에서 다음과 같은 문제가 있었습니다.

  • 값 변경 시 테이블 변경 비용 발생
  • 서버 배포와 DB 변경 순서 문제
  • 운영 중 확장 유연성 부족

처음에는 tinyint + 애플리케이션 매핑 방식을 선택하려 했지만, 피드백 이후 다시 고민해보니 현재 사용 중인 enum 성격상
조회 성능상 이점이 크지 않았고, 오히려 가독성이 더 중요하다고 판단했습니다.

 

그래서 최종적으로는 일부 enum 성격 컬럼을 varchar 기반으로 두는 방향으로 조정했습니다.

5. 실무와 관련한 문제들을 반영 ( 식별 관계 적용되어 있는 상태 )

 


 

6. 정규화 검토

테이블 구조를 정규화 한 뒤에는 정규화 관점에서 다시 점검했습니다.

 

1NF

다중값과 반복 속성은 앞 단계에서 이미 분리했기 때문에 충족한다고 보았습니다.

2NF

복합키를 갖는 테이블 중 부분 종속이 있는지 확인했습니다.

 

예를 들어 survey_response에서 created_at이 submission 단위로만 의미가 있다면, 이는 사실 survey_submission에 종속되는 값이므로 2NF를 위반할 수 있습니다.

 

그래서 survey_response에서는 별도의 created_at을 제거했습니다.

3NF

기본키가 아닌 컬럼이 다른 비기본 컬럼에 종속되는 부분을 점검했습니다.

 

예를 들어 캐릭터 이미지 URL이 상태와 레벨로부터 결정되는 값이라면, 이를 DB에 그대로 저장하는 것은 3NF 위반이 될 수 있습니다.

그래서 최종적으로는 결정에 필요한 값만 저장하고, 이미지는 애플리케이션 레벨에서 계산 / 매핑하도록 수정했습니다.

 

반면 퀘스트 완료 시점처럼 성능과 조회 편의성 때문에 넣은 값은, 의도적인 비정규화로 보고 유지했습니다.

6. 정규화

 


 

7.  이후 추가 보완

DDL을 만든 뒤에도 끝은 아닙니다.

 

특히 인덱스와 유니크 제약, CHECK 제약은 별도로 보완할 필요가 있습니다.

인덱스

주요 조회 패턴을 기준으로 인덱스를 추가했습니다.

 

예를 들면:

  • 회원별 루틴 조회
  • 루틴 단계 순서 조회
  • 최신 설문 제출 조회
  • 게시글 최신순 조회
  • 게시글 이미지 순서 조회
  • 좋아요 수 조회

즉, 인덱스는 단순히 FK라고 다 넣는 것이 아니라, 실제로 어떤 조회가 자주 발생하는가를 기준으로 설계했습니다.

유니크 제약

정책상 반드시 중복되면 안 되는 값들에도 제약을 추가했습니다.

 

예를 들어:

  • users.email
  • tags.name
  • 사용자별 단일 설정 / 프로필 / 알림 설정
  • 루틴 내 단계 순서

 


 

8. 추가 피드백 및 운영에서의 수정

이후 추가적인 피드백과 운영 상황에서의 수정이 여러 차례 이루어지며 최종적으로는 다음과 같은 형태의 ERD 가 되었습니다.

 

https://www.erdcloud.com/d/TKS6KJq2CFgF6HkSB

마무리

DB 설계는 단순히 정규화 규칙만 적용한다고 끝나는 작업이 아니라는 점이었습니다.

 

실제로는

  • 요구사항 변화 가능성
  • 운영 정책
  • ORM 매핑의 복잡도
  • 인덱스와 조회 패턴
  • 팀원과 공유되는 명세로서의 가독성
  • 등등

다양한 부분에 대한 고려가 필요했습니다.

 

처음에는 “정답이 있는 작업”처럼 느껴졌지만, 설계를 계속 수정해가면서 오히려 중요한 것은 정답보다 근거라는 생각이 들었습니다.

 

왜 이 테이블을 분리했는지, 왜 이 타입을 골랐는지, 왜 이 관계를 유지했는지,

 

이 질문에 답할 수 있는 설계가 결국 좋은 설계에 더 가깝다고 느꼈습니다.

'TIL' 카테고리의 다른 글

SAGA 패턴으로 분산 환경에서 안전하게 환전하기 - 토스뱅크  (0) 2026.04.21
RESTFul 한 API 를 설계했다구요 ?  (0) 2026.01.30
API 명세서 작성 방법에 대하여  (0) 2026.01.12
테스트 코드 작성  (0) 2025.12.09
'TIL' 카테고리의 다른 글
  • SAGA 패턴으로 분산 환경에서 안전하게 환전하기 - 토스뱅크
  • RESTFul 한 API 를 설계했다구요 ?
  • API 명세서 작성 방법에 대하여
  • 테스트 코드 작성
Charlie.han
Charlie.han
백엔드 개발자가 되기 위해 공부한 것들(Java, Spring, SQL, 네트워크, AWS 등)을 기록하고 공유하는 블로그입니다.
  • Charlie.han
    Chalie's Devlog
    Charlie.han
  • 전체
    오늘
    어제
    • 분류 전체보기 (43)
      • TIL (5)
        • Java (22)
        • Spring (4)
        • SQL (5)
        • 알고리즘 (4)
        • 트러블슈팅 (1)
      • WIL (0)
        • 카카오테크 부트캠프 (0)
  • 태그

    인스턴스
    컬렉션프레임워크
    object
    static 필드
    string
    자바
    데이터베이스
    unsupported characters for the charset 'iso-8859-1'
    유효한 테스트 코드
    SQL
    다형성
    프로그래머스 소수찾기 자바
    인텔리제이 한글 깨짐
    JVM
    FIRST원칙
    Java
    프로그래머스
    타입안전열거형
    Spring
    인터페이스
    인텔리제이한글
    DispatcherServlet
    코드트리 #코딩테스트 #코테공부 #코테준비 #알고리즘공부 #갭체크
    참조공유
    doDispatch()
    SAGA패턴
    REST 원칙
    기술세션 분석
    프로그래머스 베스트앨범 자바
    jdk란?
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Charlie.han
올바른 데이터베이스 설계란?
상단으로

티스토리툴바