데이터베이스 구조 리팩토링 및 마이그레이션 경험 공유

2025. 7. 30. 14:16·Backend Programming

기존 데이터베이스의 복잡성과 확장성 한계를 개선하기 위해 데이터베이스 구조를 리팩토링하고, 마이그레이션 및 배포까지 진행한 경험을 공유합니다.


설계의 배경

기존에는 유저 중심의 데이터베이스 설계로 구성되어 있었습니다. 캘린더의 종류는 기본 캘린더, 그룹 캘린더가 있었고, 그룹 캘린더는 group_user 테이블을 통해 그룹 안에 어떤 유저들이 포함되어 있는지를 관리하는 방식이었습니다.
 
그러나 Google Calendar 연동 기능이 추가되면서, 더 이상 유저 중심 구조만으로는 다양한 유형의 캘린더를 명확히 구분하기 어렵다는 판단이 들었습니다. 따라서 구조를 ‘캘린더 중심’으로 재설계하기로 결정했습니다.
 
이 과정에서 group_user는 calendar_member로 변경되었고, 캘린더 색상같은 설정에 관한 것도 캘린더별 설정을 위한 calendar_setting 등으로 분리하여 관심사별 테이블 구조로 나누게 되었습니다.
 
 

테이블 리팩토링 방향

관심사에 따라 다음과 같은 구조 분리를 진행했습니다.

  1. group_user → calendar_member + calendar_setting (그룹 캘린더 멤버, 캘린더 설정 분리)
  2. calendar_info → calendar + calendar_setting (캘린더 정보, 사용자별 설정 분리)

 

마이그레이션 접근 방식

테이블을 정리하려면 테이블의 데이터들을 옮기는 방법을 고민해야 했고, flyway방식이나 Spring JPA 방식을 비교를 해봤지만 기존 테이블의 구조 변경 범위가 크고, 이후에 다시 수정할 가능성이 낮았기 때문에 복잡한 도구보다는 빠르게 적용해줄 수 있는 직접 쿼리 작성이 더 적합했습니다.

 

SQL 쿼리를 활용한 직접 마이그레이션

기존의 calendar_info 테이블을 그대로 수정하는 대신, 새로운 calendar, calendar_setting 테이블을 만들고 데이터를 옮겼습니다.
처음에는 기존 테이블을 직접 수정하는 방식도 고려했지만, 새롭게 테이블을 생성한 후 데이터를 옮기는 방식이 더 안전하다고 판단했습니다.
 
기존 테이블 구조를 직접 수정할 경우, JPA의 엔티티와 DB 테이블 간 매핑 검증 과정에서 오류가 발생할 수 있습니다. 기존 엔티티를 유지한 채 새로운 코드가 먼저 배포되면 스키마 불일치로 인해 서비스 실행 시점에 validation 에러가 발생할 수 있습니다.
 
반대로, 엔티티를 먼저 수정하고 새 코드를 배포하면 이미 배포된 코드에서 사용하는 기존 필드가 사라지거나 변경되기 때문에, 서비스 중단 가능성이 생기게 됩니다.
 
따라서, 배포 서버의 DB에서 새로운 테이블을 미리 추가하고, 기존 데이터를 삽입한 뒤 백엔드 코드를 배포하여 점진적으로 전환하는 방식이 무중단 배포에 유리하다고 판단했습니다. 이러한 판단에 따라, 테이블을 직접 수정하지 않고 새로 만드는 방식을 선택했고, 마이그레이션 후 기존 테이블은 제거하는 순서로 안정적으로 작업을 진행할 수 있었습니다.
 
sql 쿼리
모든 쿼리는 데이터 누락이나 충돌이 없도록 수차례 테스트 환경에서 검증한 뒤, 운영 환경에 적용했습니다.

더보기

테이블 추가

1. calendar_info → calendar

id, user_id, title, category, color → id, user_id, title, category

CREATE TABLE calendar(
	id BIGINT PRIMARY KEY AUTO_INCREMENT,
	user_id BIGINT NOT NULL,
	title VARCHAR(20) NOT NULL,
	category ENUM('USER','GROUP','GOOGLE') NOT NULL,
	FOREIGN KEY (user_id) REFERENCES user(id)
);

 

 

2. group_user → calendar_member

id, group_id, group_title, role, user_id, user_nickname, color → id, calendar_id, user_id, role

CREATE TABLE calendar_member (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    calendar_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    role ENUM('ADMIN','SUB_ADMIN','USER') NOT NULL,
    FOREIGN KEY (calendar_id) REFERENCES calendar(id),
    FOREIGN KEY (user_id) REFERENCES user(id)
);

 

3. calendar_provider 추가

 

CREATE TABLE calendar_provider(
	id BIGINT PRIMARY KEY AUTO_INCREMENT,
	calendar_id BIGINT NOT NULL,
	provider_id VARCHAR(256) NOT NULL,
	provider VARCHAR(10) NOT NULL,
	sync_token VARCHAR(128),
	status VARCHAR(10) NOT NULL,
	FOREIGN KEY (calendar_id) REFERENCES calendar(id)
);

 

4. calendar_setting 추가

CREATE TABLE calendar_setting (
	id BIGINT PRIMARY KEY AUTO_INCREMENT,
	calendar_id BIGINT NOT NULL,
	user_id BIGINT NOT NULL,
	color VARCHAR(10) NOT NULL,
	checked BOOLEAN NOT NULL,
	FOREIGN KEY (calendar_id) REFERENCES calendar(id),
  FOREIGN KEY (user_id) REFERENCES user(id)
);

 

5. user_provider 추가

-- calendar.user_provider definition

CREATE TABLE `user_provider` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL,
  `provider` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_user` (`user_id`),
  CONSTRAINT `fk_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
);

 데이터 삽입

  1. calendar 정보 저장하기
INSERT INTO calendar (id, user_id, title, category)
SELECT
	ci.id,
	ci.admin_id,
	ci.title,
	ci.category
FROM
	calendar_info ci;
    2.1 기존 캘린더에 저장된 color값 가져오기
INSERT INTO calendar_setting (calendar_id, user_id, color, checked)
SELECT
    ci.id,
    ci.admin_id,
    ci.color,
    TRUE
FROM
    calendar_info ci
WHERE
    ci.category != 'GROUP';

 

2.2 그룹 캘린더에 저장된 color값 가져오기

INSERT INTO calendar_setting (calendar_id, user_id, color, checked)
SELECT
	gu.group_id,
	gu.user_id,
	gu.color,
	TRUE
FROM
	group_user gu;

 

3. 그룹 캘린더에서 role값 가져오기

INSERT INTO calendar_member (calendar_id, user_id, role)
SELECT
	gu.group_id,
	gu.user_id,
	gu.role
FROM
	group_user gu;

 

4. 기존 유저들의 provider 적용

INSERT INTO user_provider (user_id, provider)
SELECT u.id, 'local'
FROM user u
WHERE NOT EXISTS (
    SELECT 1
    FROM user_provider up
    WHERE up.user_id = u.id AND up.provider = 'local'
);

 

 

배포 과정

1. 로컬 환경에서의 쿼리 검증
2. 배포 환경에서의 데이터 베이스 마이그레이션
3. github에 코드 최신화
4. jenkins를 통한 코드 배포
5. 대상 테이블 정상 작동 여부 재확인
6. 불필요한 기존 테이블 제거
 
운영 환경에 적용되기 전까지 전체 마이그레이션 흐름을 수차례 테스트했기 때문에, 실제 배포 시에는 문제없이 빠르게 진행할 수 있었습니다.
 

마무리

이번 작업을 통해 단순히 테이블 구조만 개선된 것이 아니라 구글 캘린더 연동과 같은 확장 요구사항에 유연하게 대응할 수 있는 구조로 개선됐고, 향후 기능 추가 및 유지보수의 복잡도도 감소했습니다. 앞으로도 구조적인 한계를 미리 인식하고, 마이그레이션까지 고려한 설계를 통해 안정적이고 확장 가능한 시스템을 유지해 나갈 예정입니다.

저작자표시 (새창열림)

'Backend Programming' 카테고리의 다른 글

정말 HTTPS에 대해서 알고 있는가?  (1) 2025.10.28
JPA에서 ID는 언제 만들어지고, 언제 DB에 저장될까?  (0) 2025.10.24
빌드 실패 디버깅  (1) 2025.03.07
CI/CD? 배포 자동화를 해보자  (0) 2024.12.13
EC2를 활용한 HTTPS 및 도메인 설정  (3) 2024.12.12
'Backend Programming' 카테고리의 다른 글
  • 정말 HTTPS에 대해서 알고 있는가?
  • JPA에서 ID는 언제 만들어지고, 언제 DB에 저장될까?
  • 빌드 실패 디버깅
  • CI/CD? 배포 자동화를 해보자
chanheess
chanheess
'왜' 그렇게 했는가?에 대한 생각으로 공부 및 작업의 저장관리
  • chanheess
    왜 그렇게 생각했는가?
    chanheess
  • 전체
    오늘
    어제
    • 분류 전체보기
      • Backend Programming
      • Game Programming
        • Unreal
        • DirectX
      • C++
        • Memo
        • Basic
        • Effective Modern
      • Algorithm
        • Memo
        • Baekjoon
        • Programmers
        • HackerRank, LeetCode
      • Data Structure
      • Design Pattern
      • Etc
        • Memo
        • Daily Log
        • Book
  • 최근 글

  • 최근 댓글

  • 태그

    Java
    spring
    JWT
    알고리즘
    JPA
    티스토리챌린지
    위클리 챌린지
    백준
    오블완
    SpringSecurity
    프로그래머스
    dp
    dfs
    c++ 기초 플러스
  • hELLO· Designed By정상우.v4.10.0
chanheess
데이터베이스 구조 리팩토링 및 마이그레이션 경험 공유
상단으로

티스토리툴바