Skip to content

Backend Wiki

JayJeong edited this page Sep 4, 2026 · 8 revisions

프로젝트 이름 Backend Wiki

Backend Repository


목차


개발 일정

기간 주요 작업
MM/DD ~ 작업 내용 작성

컨벤션 룰

1. 코드 스타일 가이드

예시

  1. EditorConfig 설정
설정 항목 값 설명
charset utf-8
end_of_line lf
indent_style space
insert_final_newline true
trim_trailing_whitespace true
max_line_length 120

2. CheckStyle 설정

예시

항목 규칙
클래스명
변수명 및 메서드명
상수명
패키지명
탭 사용
줄 바꿈
최대 라인 길이
제어문 블록 {}
연산자 배치
import 순서
주석 스타일

3. 커밋 메시지 컨벤션

예시


** 커밋 메시지는 type: message 형식을 따르며, 아래의 규칙을 따릅니다. **

제목 규칙

  • 제목은 다음 대소문자 스타일 중 하나를 따라야 합니다:
    • sentence-case
    • start-case
    • pascal-case
    • upper-case
    • lower-case
  • 제목 끝에 마침표(.)를 사용하지 않습니다.
  • 제목은 최소 5자 이상이어야 합니다.
  • 전체 헤더는 72자를 초과하지 않아야 합니다.
타입 설명
build
chore
content
docs
feat
fix
refactor
style
test
deploy

커밋 메시지 형식

type: 제목 (#이슈번호)

본문 (선택 사항)

예시

feat: 기능 요약 (#이슈번호)

기능에 대한 간단한 설명 추가

4. 추가 규칙

예시

  • PR 제목은 커밋 메시지와 동일한 형식을 유지합니다.
  • 한 번의 PR은 하나의 목적을 가져야 합니다.
  • 코드 리뷰를 거친 후 병합을 수행합니다.

이 컨벤션을 준수하여 팀의 코드 품질을 유지하고 원활한 협업을 진행합시다!


트러블 슈팅


ERD

ERD 이미지 또는 링크 삽입

1. users

항목 내용
한글 테이블명 사용자
설명 Kakao OAuth 2.0을 통해 가입한 사용자의 식별 정보, 기본 프로필 및 서비스 이용 상태를 관리하는 테이블이다.
기능적 역할 외부 인증 식별자와 서비스 내부 사용자 ID를 연결하고, 프로필/온보딩/계정 상태/마지막 로그인 정보를 관리한다.
서비스 내 사용자 관련 데이터의 기준 엔터티 역할을 한다.
주요 관계 auth_sessions, push_subscriptions, calendar_connections와 1:N, user_taste_profiles와 1:1 관계를 가진다.
추천/클릭/피드백 테이블을 통해 products와 연결된다.
PK id
FK 없음

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
external_id BIGINT NULL -
nickname VARCHAR(50) NOT NULL -
profile_image_url VARCHAR(2048) NULL -
gender ENUM('MALE', 'FEMALE') NOT NULL -
birth_date DATE NULL -
is_onboarding_complete BOOLEAN NOT NULL; DEFAULT FALSE -
status VARCHAR(30) NOT NULL; DEFAULT 'ONBOARDING' -
blocked_at DATETIME(6) NULL -
last_login_at DATETIME(6) NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
deleted_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 서비스 내부 사용자 식별자 사용자 증가와 다른 테이블의 공통 참조를 고려해 BIGINT를 사용한다.
external_id OAuth 제공자가 발급한 외부 사용자 식별자 현재 Kakao 식별자가 숫자라는 전제에 따라 BIGINT를 사용한다. 자체 로그인 사용자는 NULL일 수 있다.
nickname 서비스에서 사용하는 닉네임 가변 길이 문자열이며 서비스 정책상 최대 길이를 제한하기 위해 사용한다.
profile_image_url OAuth 프로필 이미지 URL 긴 외부 URL을 수용하기 위해 최대 2,048자를 허용한다.
gender 사용자 성별 현재 허용되는 값이 고정되어 있으므로 DB에서 유효값을 제한한다.
birth_date 생년월일 시각 정보가 필요하지 않은 날짜 값이므로 DATE를 사용한다.
is_onboarding_complete 온보딩 완료 여부 참·거짓 두 상태를 표현하므로 BOOLEAN을 사용한다.
status 가입 진행, 정상 이용, 차단 등 사용자 상태 향후 상태가 추가될 가능성을 고려해 ENUM 대신 VARCHAR를 사용한다. 허용값은 애플리케이션 정책으로 관리한다.
blocked_at 계정 차단 시각 서비스 시간 정책에 맞춰 날짜와 시각을 함께 저장하며, 차단되지 않은 경우 NULL이다.
last_login_at 마지막 로그인 시각 사용자 활동 및 보안 확인에 필요한 마지막 로그인 시점을 저장한다.
created_at 레코드 생성 시각 생성 시각을 자동 기록하고 다른 시각 컬럼과 정밀도를 통일한다.
updated_at 레코드 최종 수정 시각 사용자 정보의 마지막 변경 시점을 저장한다.
deleted_at 사용자 탈퇴 시각 연관 데이터의 참조를 유지하면서 탈퇴 상태를 표현하기 위해 논리 삭제 시각을 저장한다.

설계 근거

현재는 Kakao OAuth 2.0만 지원하지만 향후 자체 로그인이나 다른 OAuth 공급자가 추가될 가능성을 고려하여 외부 식별자 컬럼을 kakao_id가 아닌 external_id로 정의했다. 자체 가입 사용자는 외부 식별자가 없을 수 있으므로 NULL을 허용한다. 외부 식별자와 별도로 내부 PK인 id를 사용하여 인증 공급자의 식별자 정책이 변경되더라도 다른 업무 테이블의 참조 관계가 영향을 받지 않도록 했다.

사용자 기본 정보만 users에 저장하고 인증 세션, 푸시 구독, 캘린더 연결, 취향 프로필은 별도 테이블로 분리했다. 이를 통해 기능별 데이터의 생명주기와 접근 권한을 독립적으로 관리할 수 있다.

향후 확장성

여러 로그인 방식을 동시에 지원하게 되면 provider, provider_user_id, user_id를 관리하는 별도 외부 인증 계정 테이블로 분리할 수 있다. 이 경우 하나의 사용자가 Kakao와 자체 계정 등 여러 인증 수단을 연결할 수 있다.

현재 status와 is_onboarding_complete가 모두 온보딩 상태에 관여할 수 있으므로 각 컬럼의 책임을 명확히 해야 한다. 온보딩 단계가 복잡해지면 별도의 진행 상태 또는 단계별 이력 테이블로 확장할 수 있다. 해외 사용자를 지원하게 되면 애플리케이션과 DB의 기준 시각을 UTC로 통일하고 화면에서 사용자 시간대로 변환하는 방식을 검토한다.


2. auth_sessions

항목 내용
한글 테이블명 로그인 인증 세션
설명 JWT 인증에서 Refresh Token과 기기별 로그인 상태를 관리하는 테이블이다.
Access JWT 원문은 저장하지 않는다.
기능적 역할 API 요청은 Access JWT의 서명과 만료 시각을 이용해 무상태로 인증하고,
토큰 갱신·로그아웃·회원 탈퇴·보안 조치가 필요한 경우 이 테이블에서 Refresh Token과 로그인 상태를 확인한다.
주요 관계 여러 로그인 세션이 하나의 사용자를 참조하므로 users와 N:1 관계를 가진다.
사용자 한 명이 여러 기기에서 독립적인 로그인 세션을 유지할 수 있다.
PK id
FK user_id → users.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
user_id BIGINT NOT NULL FK → users.id
refresh_token_hash BINARY(32) NOT NULL; UNIQUE UK
device_name VARCHAR(100) NULL -
ip_address VARBINARY(16) NULL -
refresh_expires_at DATETIME(6) NOT NULL -
last_used_at DATETIME(6) NULL -
revoked_at DATETIME(6) NULL -
revoke_reason ENUM('LOGOUT', 'WITHDRAWN', 'SECURITY', 'EXPIRED') NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 기기별 로그인 세션 식별자 사용자별 다중 로그인 세션을 독립적으로 관리하기 위해 대리키를 사용한다.
Refresh JWT의 세션 식별 클레임으로 활용할 수 있다.
user_id 세션 소유 사용자 하나의 사용자가 여러 세션을 가질 수 있도록 사용자 FK를 별도로 둔다.
refresh_token_hash 현재 유효한 Refresh Token의 SHA-256 해시 토큰 원문을 저장하지 않으며, 32바이트 해시를 고정 길이 바이너리로 저장한다.
device_name 사용자가 로그인 기기를 구분하기 위한 표시 이름 브라우저 또는 기기 이름의 가변 길이를 수용한다.
ip_address 세션 생성 또는 마지막 사용 IP 주소 IPv4와 IPv6를 모두 저장할 수 있도록 최대 16바이트 바이너리를 사용한다.
refresh_expires_at Refresh Token의 절대 만료 시각 만료 여부와 정리 대상 세션을 조회할 수 있도록 별도 시각을 저장한다.
last_used_at Refresh Token이 마지막으로 사용된 시각 장기간 사용하지 않은 세션 만료와 이상 사용 탐지에 활용한다.
revoked_at 로그아웃 또는 보안 조치로 세션이 폐기된 시각 활성 세션은 NULL이며 값이 존재하면 더 이상 토큰 갱신을 허용하지 않는다.
revoke_reason 세션 폐기 사유 현재 필요한 폐기 사유를 제한하여 일관된 상태를 유지한다.
Refresh Token 회전은 세션 폐기가 아니므로 ROTATED를 제외한다.
created_at 로그인 세션 생성 시각 로그인 발생 시점을 자동 기록한다.
updated_at 세션 정보 최종 수정 시각 토큰 회전, 마지막 사용 시각 및 폐기 상태 변경 시점을 추적한다.

설계 근거

Access JWT만 사용한 완전한 무상태 구조에서는 발급된 토큰을 만료 전에 강제로 무효화하기 어렵다. 본 서비스에는 익명 채팅 신고와 회원 탈퇴 기능이 있고, 탈퇴 또는 보안 조치 시 다른 기기의 로그인도 종료해야 하므로 Access JWT는 무상태로 검증하되 Refresh Token과 로그인 상태는 DB에서 관리하는 혼합 방식을 사용한다.

Refresh Token은 원문 대신 해시를 저장한다. Refresh JWT에는 세션 식별자인 auth_session_id를 포함하고, 토큰 갱신 시 서명 검증 후 해당 세션의 현재 해시와 비교한다. 정상 갱신 시 새 Refresh Token을 발급하고 refresh_token_hash를 교체한다. 로그아웃이나 회원 탈퇴 시 revoked_at을 설정하여 이후 갱신을 차단한다.

향후 확장성

Refresh Token 회전 이력과 탈취 토큰 재사용 내역을 장기간 감사해야 한다면 개별 토큰을 저장하는 refresh_tokens 테이블을 별도로 둘 수 있다. 사용자별 활성 세션 조회를 위해 (user_id, revoked_at), 만료 세션 정리를 위해 refresh_expires_at 인덱스를 검토한다.

기기 관리 기능이 강화되면 안정적인 기기 식별을 위한 device_id, 플랫폼 구분을 위한 platform, 마지막 사용 IP를 별도로 기록하는 last_ip_address 등을 추가할 수 있다. Access JWT를 즉시 무효화해야 하는 정책이 생기면 짧은 만료 시간과 함께 세션 상태 조회 또는 jti 차단 목록을 사용할 수 있다.


3. push_subscriptions

항목 내용
한글 테이블명 푸시 구독
설명 PWA와 Web Push API를 통해 사용자의 개별 기기에 알림을 발송하기 위한 구독 정보를 관리하는 테이블이다.
기능적 역할 알림 이벤트 발생 시 사용자의 활성 구독을 조회해 모든 등록 기기로 메시지를 발송한다.
페이지가 열려 있으면 React가, 백그라운드 또는 종료 상태에서는 Service Worker가 알림을 처리한다.
주요 관계 여러 기기 구독이 하나의 사용자를 참조하므로 users와 N:1 관계를 가진다.
PK id
FK user_id → users.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
user_id BIGINT NOT NULL FK → users.id
registration_id BIGINT NOT NULL -
status ENUM('ACTIVE', 'REVOKED') NOT NULL; DEFAULT 'ACTIVE' -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
deleted_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 푸시 구독 식별자 사용자별 다중 기기 구독을 구분하기 위해 대리키를 사용한다.
user_id 구독 소유 사용자 사용자별 활성 구독을 조회하기 위한 참조 키다.
registration_id 별도로 관리되는 Web Push 등록 정보의 식별자 숫자형 내부 등록 ID를 참조한다는 전제의 타입이다. 실제 구독 객체 자체를 저장한다면 아래 확장 구조가 필요하다.
status 푸시 구독의 활성 또는 폐기 상태 허용 상태가 명확히 고정되어 있으므로 ENUM으로 무결성을 관리한다.
created_at 구독 생성 시각 기기 등록 시점을 자동 기록한다.
updated_at 구독 정보 최종 수정 시각 상태 또는 구독 정보의 변경 시점을 저장한다.
deleted_at 구독 논리 삭제 시각 구독 이력을 유지하면서 발송 대상에서 제외하기 위해 사용한다.

설계 근거

한 사용자가 스마트폰, 태블릿, PC 등 여러 기기에서 서비스를 사용할 수 있으므로 사용자와 구독을 1:N 관계로 분리했다. 백엔드는 ACTIVE 상태인 구독만 조회하여 발송하고, 만료되거나 사용자가 권한을 철회한 구독은 REVOKED로 전환한다.

향후 확장성

Web Push API의 구독 객체를 DB에 직접 저장한다면 registration_id만으로는 부족할 수 있다. 이 경우 endpoint, p256dh_key, auth_key, expiration_time, user_agent 등을 별도 컬럼 또는 구독 상세 테이블로 관리해야 한다. 동일 구독의 중복 저장을 방지하기 위해 endpoint 또는 등록 정보에 유니크 제약조건을 둘 수 있다.

푸시 공급자가 추가되면 provider, platform을 추가하고, 반복 발송 실패 횟수와 마지막 성공·실패 시각을 저장하여 자동 폐기 정책을 적용할 수 있다.


4. calendar_connections

항목 내용
한글 테이블명 외부 캘린더 연결
설명 사용자의 외부 캘린더 연동 상태와 연결·해제 시각을 관리하는 테이블이다.
기능적 역할 외부 캘린더에서 수집된 일정을 이벤트와 알림 기능에 활용할 수 있도록 사용자별 연결 상태를 제공한다.
주요 관계 여러 캘린더 연결이 하나의 사용자를 참조하므로 users와 N:1 관계를 가진다.
사용자는 캘린더를 연결하지 않거나 여러 캘린더 연결을 가질 수 있다.
PK id
FK user_id → users.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
user_id BIGINT NOT NULL FK → users.id
status ENUM('CONNECTED', 'DISCONNECTED', 'REAUTH_REQUIRED') NOT NULL; DEFAULT 'DISCONNECTED' -
connected_at DATETIME(6) NULL -
disconnected_at DATETIME(6) NULL -
updated_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 캘린더 연결 식별자 사용자별 여러 캘린더 연결을 독립적으로 관리하기 위해 대리키를 사용한다.
user_id 캘린더를 연결한 사용자 하나의 사용자에게 여러 연결을 허용하기 위해 PK가 아닌 FK로 사용한다.
status 연결됨, 연결 해제, 재인증 필요 상태 허용 상태가 정해져 있으므로 ENUM으로 유효값을 제한한다.
connected_at 외부 캘린더가 연결된 시각 연결 전이거나 연결 이력이 없으면 NULL이다.
disconnected_at 외부 캘린더 연결이 해제된 시각 연결 상태라면 NULL이고 연결 해제 시 시각을 기록한다.
updated_at 연결 정보 최종 수정 시각 연결 상태나 인증 상태의 변경 시점을 관리한다.

설계 근거

현재 기획은 Kakao 톡캘린더 단일 연결을 중심으로 하지만, 향후 Google Calendar 등 여러 외부 캘린더에서 이벤트를 가져올 가능성을 고려해 사용자당 하나의 행으로 제한하지 않았다. user_id 대신 별도 id를 PK로 사용하여 사용자와 캘린더 연결을 1:N 구조로 확장했다.

향후 확장성

다중 캘린더 연결을 실제로 구분하려면 provider, external_account_id, calendar_id, created_at 컬럼이 추가로 필요하다. 동일 외부 계정의 중복 연결을 방지하려면 (user_id, provider, external_account_id)에 유니크 제약조건을 적용할 수 있다.

OAuth Access Token이나 Refresh Token을 저장해야 한다면 현재 상태 테이블에 평문으로 추가하지 않고 암호화된 별도 인증 정보 테이블로 분리한다. 동기화 상태를 관리하기 위해 last_synced_at, sync_cursor, sync_error 등을 추가할 수 있다.


5. user_taste_profiles

항목 내용
한글 테이블명 사용자 취향 프로필
설명 AI 대화 분석 결과와 사용자가 온보딩에서 직접 선택한 취향·관심사·제한 정보를 관리하는 테이블이다.
기능적 역할 개인 상품 추천과 선물 추천에서 사용하는 사용자의 최신 취향 정보를 통합해 제공한다.
주요 관계 user_id가 PK이자 FK이므로 users와 1:1 관계를 가진다.
AI 대화 및 분석 결과를 바탕으로 갱신되고 추천 테이블 생성에 활용된다.
PK user_id
FK user_id → users.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
user_id BIGINT NOT NULL PK, FK → users.id
recent_tastes JSON NOT NULL -
recent_interests JSON NOT NULL -
ai_summary TEXT NOT NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
selected_tastes JSON NULL; 최대 5개 -
food_restrictions JSON NULL -
unwanted_product JSON NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
user_id 취향 프로필 소유 사용자 사용자당 하나의 최신 프로필만 유지하기 위해 PK와 FK를 함께 사용한다.
recent_tastes AI가 최근 대화에서 분석한 취향 키워드와 관련 데이터 분석 결과의 항목 수와 구조가 가변적이므로 JSON을 사용한다.
recent_interests AI가 분석한 최근 관심사 관심사 목록과 부가 정보가 유동적이므로 JSON을 사용한다.
ai_summary AI가 생성한 사용자 취향 요약 분석 결과의 문장 길이가 일정하지 않으므로 TEXT를 사용한다.
created_at 취향 프로필 생성 시각 최초 생성 시점을 자동 기록한다.
updated_at 취향 프로필 최종 갱신 시각 최신 AI 분석 결과가 반영된 시점을 확인한다.
selected_tastes 사용자가 온보딩에서 직접 선택한 취향 키워드 선택 항목의 조합이 가변적이므로 JSON을 사용한다. 최대 개수는 애플리케이션에서 검증한다.
food_restrictions 알레르기, 식단 등 음식 관련 제한 키워드 복수의 가변 항목을 저장하기 위해 JSON을 사용한다.
unwanted_product 사용자가 추천받고 싶지 않은 상품 또는 조건 제외 대상의 종류와 개수가 가변적이므로 JSON을 사용한다.

설계 근거

사용자 한 명은 가장 최신 상태의 취향 프로필 하나를 가지므로 user_id를 PK로 사용했다. 분석 결과와 온보딩 선택지는 구조와 길이가 변경될 가능성이 높아 JSON으로 저장하며, 서술형 AI 요약은 가변 길이를 고려해 TEXT로 저장한다.

이 데이터는 개인정보를 바탕으로 생성되므로 회원 탈퇴 시 관련 행을 하드 딜리트하는 정책을 적용한다. 따라서 다른 도메인 테이블과 달리 deleted_at을 두지 않았다.

향후 확장성

취향 키워드별 검색, 통계, 가중치 계산이 중요해지면 JSON 데이터를 취향 코드 및 사용자-취향 연결 테이블로 정규화할 수 있다. JSON 구조 변경에 대비해 schema_version 또는 분석 모델 버전을 추가할 수 있다.

취향 변화 이력이 필요하면 현재 테이블은 최신 상태만 유지하고, 별도의 프로필 스냅샷 또는 분석 이력 테이블을 추가한다. 음식 제한 정보는 민감 정보가 될 수 있으므로 접근 권한, 암호화 및 보존 기간 정책을 별도로 정의한다.


6. products

항목 내용
한글 테이블명 상품
설명 크롤링 또는 외부 API를 통해 수집한 추천 대상 상품의 기본 정보, 가격, 판매처 및 구매 링크를 관리하는 테이블이다.
기능적 역할 개인 추천, 선물 추천, 클릭, 피드백 및 익명 질문 기능에서 공통으로 참조하는 상품 기준 정보를 제공한다.
주요 관계 personal_recommendations, gift_recommendations, product_clicks,
product_feedbacks, anonymous_rooms와 1:N 관계를 가진다.
PK id
FK 없음

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
external_id VARCHAR(255) NULL -
name VARCHAR(300) NOT NULL -
category VARCHAR(100) NULL -
description TEXT NULL -
price DECIMAL(12, 2) NULL -
image_url VARCHAR(2048) NULL -
seller_name VARCHAR(200) NULL -
purchase_url VARCHAR(2048) NULL -
status ENUM('ACTIVE', 'SOLD_OUT', 'UNAVAILABLE') NOT NULL; DEFAULT 'ACTIVE' -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
deleted_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 서비스 내부 상품 식별자 외부 데이터 출처와 독립된 참조 기준을 제공하고 상품 증가를 수용하기 위해 사용한다.
external_id 외부 API가 제공한 상품 식별자 공급자별 식별자 형식이 다를 수 있어 문자열을 사용한다. 크롤링 상품은 외부 ID가 없어 NULL일 수 있다.
name 상품명 비교적 긴 외부 상품명을 수용하도록 최대 길이를 설정한다.
category 상품 카테고리 외부 수집 데이터의 가변적인 카테고리 값을 수용한다.
description 상품 상세 설명 설명 길이가 일정하지 않으므로 TEXT를 사용한다.
price 상품 판매 가격 부동소수점 오차 없이 금액을 저장하기 위해 DECIMAL을 사용한다.
image_url 상품 이미지 URL 긴 외부 URL을 수용한다.
seller_name 판매처 또는 판매자 이름 판매처명의 가변 길이를 수용한다.
purchase_url 상품 구매 페이지 URL 외부 구매 링크의 최대 길이를 고려한다.
status 판매 가능, 품절, 구매 페이지 이용 불가 상태 상태 추가 가능성이 낮고 값의 무결성이 중요하므로 ENUM을 사용한다.
created_at 상품 레코드 생성 시각 상품이 최초 수집된 시점을 기록한다.
updated_at 상품 정보 최종 갱신 시각 재고, 가격, URL 등 외부 정보가 마지막으로 갱신된 시점을 기록한다.
deleted_at 상품 논리 삭제 시각 과거 추천·클릭·피드백 참조를 유지하면서 신규 노출에서 제외한다.

설계 근거

상품 데이터는 API 또는 크롤링으로 수집될 수 있으므로 외부 식별자가 없는 경우를 허용한다. 내부 PK인 id를 사용하여 외부 공급처의 ID 변경이나 수집 방식 변경이 추천·클릭·피드백 관계에 영향을 주지 않도록 했다.

상품 상태는 구매 URL 접근 가능 여부와 재고 상태를 기준으로 ACTIVE, SOLD_OUT, UNAVAILABLE로 제한했다. 현재 상태 종류의 변동 가능성이 낮다고 판단하여 DB에서 ENUM으로 무결성을 관리한다.

향후 확장성

여러 API 또는 쇼핑몰에서 같은 외부 ID가 발생할 수 있으므로 source_type 또는 provider_id를 추가하고 (provider_id, external_id)에 유니크 제약조건을 적용할 수 있다. external_id IS NULL만으로 크롤링 상품을 구분하기보다 데이터 출처를 명시적으로 저장하는 편이 확장에 유리하다.

다중 통화를 지원하면 currency_code를 추가하고, 가격 변동 분석이 필요하면 별도 가격 이력 테이블을 둔다. 카테고리 계층, 다중 이미지, 여러 판매처를 지원하게 되면 각각 별도 테이블로 정규화할 수 있다.


7. personal_recommendations

항목 내용
한글 테이블명 개인 상품 추천
설명 AI 대화를 통해 분석한 사용자의 현재 필요에 적합한 상품 추천 결과를 관리하는 테이블이다.
기능적 역할 선물보다는 사용자가 직접 구매해 사용할 가능성이 높은 상품의 점수와 추천 사유를 제공한다.
주요 관계 users와 products 사이의 연결 테이블로 각각 N:1 관계를 가지며, 사용자와 상품의 N:M 관계를 표현한다.
PK (user_id, product_id)
FK user_id → users.id, product_id → products.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
user_id BIGINT NOT NULL PK(1), FK → users.id
product_id BIGINT NOT NULL PK(2), FK → products.id
score DECIMAL(8, 6) NOT NULL -
reason VARCHAR(500) NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
user_id 추천을 받는 사용자 사용자 중심 조회와 중복 방지를 위해 복합 PK의 첫 번째 컬럼으로 사용한다.
product_id 추천된 상품 동일 사용자에게 같은 상품이 중복 저장되지 않도록 복합 PK에 포함한다.
score 사용자와 상품 간 추천 적합도 점수 정렬 결과의 정밀도와 재현성을 유지하기 위해 고정소수점을 사용한다.
reason 상품 추천 사유 사용자에게 노출하거나 추천 결과를 해석하는 짧은 설명을 저장한다.
created_at 추천 결과 생성 시각 추천이 최초 생성된 시점을 기록한다.
updated_at 추천 결과 최종 갱신 시각 점수 또는 추천 사유가 갱신된 시점을 기록한다.

설계 근거

동일한 상품이 개인 추천과 선물 추천에 모두 포함될 수 있지만 추천 목적과 사용자에게 제공하는 설명이 다르므로 두 테이블을 분리했다. user_id와 product_id를 복합 PK로 사용하여 동일 사용자-상품 조합의 중복을 방지하고, 사용자별 추천 목록을 함께 조회하는 패턴에 맞췄다.

대리키와 복합 UK 조합보다 JPA 복합키 구현이 복잡해지는 단점이 있지만, 추천 결과는 사용자와 상품 조합 자체가 식별자이며 읽기 중심 데이터라는 점을 고려해 복합 PK를 선택했다.

향후 확장성

현재 구조는 사용자별 최신 추천 결과에 적합하다. 추천 이력, 모델 비교, A/B 테스트가 필요해지면 recommendation_batch_id, model_version, generated_at, expires_at을 추가하거나 추천 실행 단위 테이블을 분리할 수 있다.

사용자별 상위 추천을 빠르게 조회하려면 (user_id, score) 인덱스를 검토하고, 점수 범위가 정해져 있다면 CHECK 제약조건을 추가한다.


8. gift_recommendations

항목 내용
한글 테이블명 선물 상품 추천
설명 사용자의 취향을 바탕으로 해당 사용자가 선물로 받고 싶어 할 가능성이 높은 상품 추천 결과를 관리하는 테이블이다.
기능적 역할 친구가 선물을 선택할 때 추천 점수, 추천 사유 및 취향 키워드를 제공한다.
주요 관계 users와 products 사이의 연결 테이블로 각각 N:1 관계를 가지며, 사용자와 상품의 N:M 관계를 표현한다.
PK (user_id, product_id)
FK user_id → users.id, product_id → products.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
user_id BIGINT NOT NULL PK(1), FK → users.id
product_id BIGINT NOT NULL PK(2), FK → products.id
score DECIMAL(8, 6) NOT NULL -
reason VARCHAR(500) NULL -
taste_keywords JSON NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
user_id 선물 추천의 대상 사용자 사용자별 선물 추천 조회와 중복 방지를 위해 복합 PK의 첫 번째 컬럼으로 사용한다.
product_id 선물로 추천된 상품 동일 대상 사용자에게 같은 상품이 중복 저장되지 않도록 복합 PK에 포함한다.
score 취향과 상품 간 선물 적합도 점수 정밀한 추천 순위를 재현하기 위해 고정소수점을 사용한다.
reason 해당 상품을 선물로 추천한 사유 추천 결과의 설명 가능성을 위해 짧은 서술형 내용을 저장한다.
taste_keywords 추천에 활용된 사용자 취향 키워드 키워드 개수와 구조가 가변적이므로 JSON을 사용한다.
created_at 추천 결과 생성 시각 추천이 최초 생성된 시점을 기록한다.
updated_at 추천 결과 최종 갱신 시각 점수, 사유 또는 키워드가 갱신된 시점을 기록한다.

설계 근거

개인 추천은 사용자가 직접 필요로 할 상품을, 선물 추천은 다른 사람이 선물하기 적합한 상품을 표현한다. 두 범주의 경계가 완전히 배타적이지 않아 같은 상품이 양쪽에 존재할 수 있지만, 추천 목적과 노출 맥락이 다르므로 별도 테이블로 관리한다.

user_id와 product_id를 복합 PK로 사용하여 동일 대상 사용자-상품 조합의 중복을 방지하고, 사용자별 추천 목록 조회에 맞는 키 순서를 사용한다. taste_keywords를 함께 저장하여 추천 근거를 사용자에게 설명하거나 결과를 분석할 수 있도록 했다.

향후 확장성

생일, 기념일 등 추천 상황에 따라 결과가 달라지면 event_id 또는 recommendation_context를 추가할 수 있다. 추천 요청자에 따라서도 결과가 달라진다면 requester_user_id와 recipient_user_id를 구분해야 한다.

추천 모델 이력과 A/B 테스트가 필요하면 추천 실행 단위, 모델 버전 및 만료 시각을 관리하는 별도 테이블로 확장할 수 있다. 키워드별 검색이 중요해지면 taste_keywords를 정규화할 수 있다.


9. product_clicks

항목 내용
한글 테이블명 상품 클릭 및 구매 후속 확인
설명 사용자가 추천 상품의 구매 링크를 클릭한 이력과 일정 기간 이후 실제 구매 여부를 확인하는 상태를 관리하는 테이블이다.
기능적 역할 추천 상품 클릭 후 7일이 지나면 구매 여부를 질문하고, 응답 결과를 추천 및 AI 학습 데이터로 제공한다.
주요 관계 여러 클릭 레코드가 하나의 사용자와 상품을 각각 참조하므로 users, products와 N:1 관계를 가진다.
PK id
FK user_id → users.id, product_id → products.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
user_id BIGINT NOT NULL FK → users.id
product_id BIGINT NOT NULL FK → products.id
recommendation_type ENUM('SELF', 'FRIEND') NOT NULL -
clicked_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
follow_up_at DATETIME(6) NOT NULL; clicked_at + 7일 -
follow_up_status ENUM('PENDING', 'ASKED', 'ANSWERED') NOT NULL; DEFAULT 'PENDING' -
purchase_status ENUM('NOT_SELECTED', 'PURCHASED', 'NOT_PURCHASED') NOT NULL; DEFAULT 'NOT_SELECTED' -
answered_at DATETIME(6) NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
deleted_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 상품 클릭 및 후속 확인 레코드 식별자 사용자와 상품 조합의 상태를 독립적으로 관리하기 위해 대리키를 사용한다.
user_id 구매 링크를 클릭한 사용자 사용자별 클릭 및 후속 질문 대상을 조회한다.
product_id 클릭된 상품 상품별 클릭과 구매 전환 데이터를 집계한다.
recommendation_type 본인 구매 추천 또는 친구 선물 추천 구분 클릭 경로가 두 종류로 고정되어 있으므로 ENUM을 사용한다.
clicked_at 가장 최근 구매 링크 클릭 시각 후속 질문 기준 시각으로 사용한다. 대기 중 재클릭하면 최신 시각으로 갱신한다.
follow_up_at 구매 여부 질문 예정 시각 배치 작업이 질문 대상을 직접 조회할 수 있도록 계산 결과를 저장한다.
follow_up_status 질문 대기, 질문 완료, 응답 완료 상태 정해진 후속 처리 단계를 DB에서 제한한다.
purchase_status 미응답, 구매함, 구매하지 않음 상태 구매 여부와 아직 선택하지 않은 상태를 명확히 구분한다.
answered_at 사용자가 구매 여부에 응답한 시각 응답 전에는 NULL이고 응답 완료 시 기록한다.
created_at 레코드 생성 시각 최초 클릭 기반 레코드 생성 시점을 기록한다.
updated_at 레코드 최종 갱신 시각 재클릭, 질문 또는 응답 상태의 변경 시점을 기록한다.
deleted_at 레코드 논리 삭제 시각 분석 데이터의 이력을 유지하면서 업무 조회에서 제외한다.

설계 근거

사용자가 상품 구매 링크를 클릭한 뒤 실제 구매로 이어졌는지 확인하기 위해 클릭 시각과 후속 질문 상태를 한 테이블에서 관리한다. 클릭 목적은 본인 구매와 친구 선물 두 종류로 한정되므로 recommendation_type을 ENUM으로 정의했다.

asked_at은 별도로 두지 않았다. 질문 예정 시각인 follow_up_at과 현재 처리 단계인 follow_up_status를 통해 질문 여부를 판단할 수 있기 때문이다. 후속 질문 대기 중 같은 상품을 다시 클릭하면 clicked_at과 follow_up_at을 최신 값으로 갱신하여 마지막 관심 시점을 기준으로 7일을 다시 계산한다.

향후 확장성

현재 재클릭 갱신 정책을 안정적으로 적용하려면 어떤 행을 갱신할지 결정하는 사용자-상품-추천유형 기준이 필요하다. 애플리케이션 트랜잭션으로 제어하거나 활성 후속 레코드를 식별하는 별도 키 및 유니크 정책을 검토할 수 있다.

후속 질문 배치 조회를 위해 (follow_up_status, follow_up_at) 인덱스가 필요하다. 클릭 데이터가 크게 증가하면 클릭 원본 이벤트와 후속 구매 확인 상태를 분리하고, clicked_at 기준 파티셔닝 또는 아카이빙을 적용할 수 있다. 추천 성과 분석을 위해 recommendation_batch_id, source_screen, campaign_id 등을 추가할 수 있다.


10. product_feedbacks

항목 내용
한글 테이블명 상품 피드백
설명 특정 상품에 대한 사용자의 긍정·부정 평가와 부정 사유를 관리하는 테이블이다.
기능적 역할 사용자별 상품 선호 정보를 수집하고 이후 추천 결과 및 AI 학습 데이터에 활용한다.
주요 관계 여러 피드백이 하나의 사용자와 상품을 각각 참조하므로 users, products와 N:1 관계를 가진다.
(user_id, product_id) UK를 통해 사용자 한 명이 상품 하나에 하나의 현재 피드백만 가지도록 제한한다.
PK id
FK user_id → users.id, product_id → products.id
UK (user_id, product_id) — README 설계 의도이며 실제 DDL에 추가 필요

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
user_id BIGINT NOT NULL; product_id와 복합 UNIQUE FK → users.id
product_id BIGINT NOT NULL; user_id와 복합 UNIQUE FK → products.id
feedback_type ENUM('POSITIVE', 'NEGATIVE') NOT NULL -
dislike_reason VARCHAR(500) NULL -
is_anonymous BOOLEAN NOT NULL; DEFAULT FALSE -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
deleted_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 상품 피드백 식별자 JPA에서 복합키를 처리하는 복잡도를 줄이고 쓰기 중심 테이블의 행을 단순하게 식별하기 위해 대리키를 사용한다.
user_id 피드백 작성 사용자 사용자-상품 조합의 유일성을 보장하면서 사용자 참조를 유지한다.
product_id 피드백 대상 상품 사용자-상품 조합의 유일성을 보장하면서 상품 참조를 유지한다.
feedback_type 상품에 대한 긍정 또는 부정 평가 허용 상태가 두 종류로 고정되어 있으므로 ENUM을 사용한다.
dislike_reason 부정 평가 사유 부정 피드백에서만 선택적으로 입력하므로 NULL을 허용한다.
is_anonymous 피드백 표시 시 작성자 익명 처리 여부 익명 여부를 참·거짓으로 표현한다.
created_at 피드백 생성 시각 피드백이 최초 등록된 시점을 기록한다.
updated_at 피드백 최종 수정 시각 평가나 부정 사유가 수정된 시점을 기록한다.
deleted_at 피드백 논리 삭제 시각 피드백 이력을 보존하면서 일반 조회에서 제외한다.

설계 근거

사용자 한 명이 특정 상품에 대해 하나의 현재 평가만 가지도록 (user_id, product_id) 복합 유니크 제약조건을 사용한다. 처음에는 두 컬럼을 복합 PK로 사용하는 방식을 고려했으나, 이 테이블은 추천 결과보다 쓰기와 수정이 중심이고 JPA 복합키 구현 복잡도에 비해 클러스터드 인덱스의 이점이 크지 않다고 판단하여 대리키 id를 PK로 사용했다.

긍정과 부정 두 상태만 존재하므로 feedback_type을 ENUM으로 제한한다. dislike_reason은 부정 평가일 때만 의미가 있으므로 선택값으로 두며, 해당 조건은 애플리케이션 검증 또는 CHECK 제약조건으로 보완할 수 있다.

향후 확장성

평가 수준이 세분화되면 별점이나 점수 컬럼을 추가할 수 있다. 부정 사유가 표준화되거나 복수 선택이 필요해지면 사유 코드 테이블과 피드백-사유 연결 테이블로 분리할 수 있다.

논리 삭제된 행도 (user_id, product_id) 유니크 제약조건을 점유한다. 사용자가 같은 상품에 다시 피드백을 남길 때 기존 행을 복구·갱신할지, 피드백 이력을 별도 테이블로 분리할지 정책을 정해야 한다. 추천 모델 학습 시 익명 여부와 무관하게 내부 사용자 ID를 사용할 수 있는지 개인정보 및 동의 정책도 함께 정의해야 한다.





11. user_consents

항목 내용
한글 테이블명 사용자 약관 동의
설명 사용자별 약관 및 개인정보·AI 활용 등의 동의 상태와 적용된 정책 버전, 동의 및 철회 시점을 관리하는 테이블
기능적 역할 사용자가 어떤 동의 항목에 대해 어떤 정책 버전을 기준으로 동의했는지 관리하며, 동의 철회 여부 및 시점을 추적한다.
주요 관계 users 1 : N user_consents, consent_types 1 : N user_consents
PK user_id
FK user_id → users.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
user_id BIGINT NOT NULL PK, FK → users.id
consent_id BIGINT NOT NULL PK, FK → consent_types_id
policy_version VARCHAR(50) NOT NULL -
agreed_at DATETIME(6) NULL -
withdrawn_at DATETIME(6) NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
deleted_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
user_id 약관에 동의한 사용자의 식별자 users.id와 동일한 타입을 사용하여 FK 관계를 일관되게 유지한다.
consent_id 사용자가 동의하거나 동의하지 않은 항목의 식별자 동의 종류를 문자열이나 ENUM으로 직접 저장하지 않고 consent_types의 식별자를 참조하여 동의 항목의 추가·수정에 대응한다.
policy_version 해당 동의가 이루어질 당시 적용된 약관 또는 정책의 버전 정책 버전은 숫자뿐 아니라 v1.0, 2026-09-01 등 다양한 표현을 사용할 수 있어 문자열 타입으로 저장한다.
agreed_at 사용자가 해당 정책에 동의한 시각 동의하지 않은 상태를 표현할 수 있도록 NULL을 허용하며, 마이크로초 단위까지 기록하기 위해 DATETIME(6)을 사용한다.
withdrawn_at 사용자가 기존 동의를 철회한 시각 아직 철회하지 않은 경우를 표현하기 위해 NULL을 허용한다.
created_at 사용자 동의 레코드가 최초 생성된 시각 사용자 동의 레코드가 최초 생성된 시각
updated_at 사용자 동의 정보가 마지막으로 변경된 시각 사용자 동의 정보가 마지막으로 변경된 시각
deleted_at 동의 정보를 논리적으로 삭제한 시각 동의 정보를 논리적으로 삭제한 시각

설계 근거

user_consents는 사용자와 동의 항목 간의 관계를 관리하기 위한 연결 테이블로 분리하였다. 하나의 사용자는 개인정보 처리방침, AI 활용 동의, 마케팅 수신 동의 등 여러 종류의 동의 항목을 가질 수 있으며, 동일한 동의 항목 역시 여러 사용자에게 적용되므로 users와 consent_types 사이의 관계를 user_consents가 연결하는 구조로 설계하였다.

새로운 동의 항목이 추가되거나 기존 항목의 이름, 설명, 필수 여부, 활성 여부가 변경되는 경우를 고려해 동의 종류를 ENUM이나 VARCHAR로 user_consents 테이블에 직접 저장하지 않고 별도의 consent_types 테이블로 분리하였다.

⇒ 애플리케이션 코드와 테이블 스키마를 반복적으로 변경하지 않고 동의 종류 데이터를 중심으로 관리할 수 있다.

기본키는 별도의 id 대리키를 추가하지 않고 (user_id, consent_id) 복합 기본키로 구성하였다. 이 테이블에서 하나의 레코드는 본질적으로 특정 사용자가 특정 동의 항목에 대해 가지고 있는 동의 상태를 의미하므로 두 컬럼의 조합 자체가 비즈니스적으로 자연스러운 식별자가 된다. 또한 동일한 사용자와 동일한 동의 항목의 조합이 중복 생성되는 것을 PK 제약조건만으로 방지할 수 있다.

현재 테이블은 다른 테이블에서 참조하지 않고, 사용자와 동의 항목 사이의 관계를 표현하는 연결 테이블이라 별도 대리키를 추가해 얻는 이득이 크지 않다고 판단하여 복합 기본키를 사용하였다.

→ ORM 복합키 구성 위한 추가 작업 필요

향후 확장성

현재 구조에서는 새로운 동의 종류가 필요한 경우 user_consents의 스키마를 변경하지 않고 consent_types 테이블에 새로운 동의 항목을 추가하여 확장할 수 있다. 따라서 개인정보 수집·이용 동의, AI 데이터 활용 동의, 친구 취향 공유 동의, 마케팅 수신 동의 외에 새로운 정책이 추가되는 경우에도 동일한 구조를 사용할 수 있다.

사용자 또는 동의 항목을 기준으로 조회하는 요청이 증가하더라도 복합 PK의 첫 번째 컬럼인 user_id를 활용하여 특정 사용자의 전체 동의 상태를 효율적으로 조회할 수 있다. 반대로 특정 consent_id에 동의한 전체 사용자를 조회하는 패턴이 빈번해지는 경우 consent_id 단독 보조 인덱스를 추가하는 방향으로 확장할 수 있다.


12 notification_types

항목 내용
한글 테이블명 알림 종류
설명 서비스에서 사용하는 알림 종류와 사용자 설정 가능 여부, 활성화 상태를 관리하는 마스터 테이블
기능적 역할 알림 종류를 중앙에서 관리하고, 실제 생성된 알림과 사용자별 알림 설정에서 동일한 알림 종류를 참조할 수 있도록 한다.
주요 관계 notification_types 1 : N notifications, notification_types 1 : N user_notification_settings
PK id
FK 없음

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
type VARCHAR(30) NOT NULL UK
name VARCHAR(50) NOT NULL -
is_configurable BOOLEAN NOT NULL; DEFAULT TRUE -
is_activated BOOLEAN NOT NULL; DEFAULT FALSE -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
deleted_at DATETIME(6) NULL -
updated_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 알림 종류를 식별하는 고유 식별자 알림 종류를 다른 테이블에서 FK로 참조하기 위한 대리키이며, 향후 알림 종류 증가에 충분한 범위를 확보하기 위해 BIGINT를 사용한다.
type 애플리케이션에서 알림 종류를 식별하기 위해 사용하는 코드 CHAT, EVENT, FRIEND_JOIN 등 사람이 읽을 수 있고 코드 레벨에서 활용 가능한 문자열 값을 저장하기 위해 VARCHAR를 사용한다.
name 사용자 또는 관리자 화면에서 사용할 알림 종류의 이름 알림 종류의 표시명을 저장하며 길이가 제한적인 문자열 데이터이므로 VARCHAR(50)을 사용한다.
is_configurable 사용자가 해당 알림의 수신 여부를 직접 설정할 수 있는지 여부 설정 가능/불가능의 두 상태만 필요하므로 BOOLEAN을 사용한다.
is_activated 해당 알림 종류가 현재 서비스에서 활성화되어 있는지 여부 알림 종류의 활성/비활성 두 상태를 표현하기 위해 BOOLEAN을 사용한다.
created_at 알림 종류가 최초 등록된 시각 생성 시점을 자동으로 기록하고 서비스의 시간 컬럼 규칙과 통일하기 위해 DATETIME(6)을 사용한다.
deleted_at 알림 종류가 논리적으로 삭제된 시각 기존 알림 데이터와의 참조 관계를 유지하면서 사용하지 않는 알림 종류를 논리적으로 삭제하기 위해 사용한다.
updated_at 알림 종류 정보가 마지막으로 수정된 시각 수정되지 않은 초기 상태를 구분할 수 있도록 NULL을 허용한다.

설계 근거

notification_types는 서비스에서 사용하는 알림 종류를 별도의 테이블로 관리하기 위해 분리하였다. 알림 종류는 실제 알림을 저장하는 notifications와 사용자의 알림 수신 설정을 저장하는 user_notification_settings에서 공통으로 사용되므로, 각 테이블에서 알림 종류를 ENUM이나 VARCHAR로 중복 관리할 경우 알림 종류가 추가되거나 변경될 때 변경 지점이 다중화 된다.

따라서 알림 종류를 notification_types에서 중앙 관리하고 다른 테이블에서는 notification_type_id를 통해 참조하도록 설계하였다. 이를 통해 새로운 알림 종류 추가 시 notification_types에 데이터를 추가하는 방식으로 대응할 수 있으며, 알림 이름이나 설정 가능 여부와 같은 속성 역시 하나의 관리 지점에서 변경할 수 있다.

notifications와 user_notification_settings에서는 알림 종류의 세부 정보 자체보다 해당 알림 종류를 식별하는 것이 중요하므로 숫자형 식별자를 FK로 사용하는 구조가 적합하다. notifications에서는 notification_type_id가 notification_types.id를 참조하며, user_notification_settings 역시 동일한 알림 종류 식별자를 참조하여 실제 알림과 사용자 설정 간 동일한 분류 체계를 사용할 수 있다.

code는 애플리케이션 로직에서 알림 종류를 식별하는 값이므로 중복된 코드가 등록되지 않도록 UNIQUE 제약조건을 추가했다.

향후 확장성

새로운 알림 종류가 추가되는 경우 notifications나 user_notification_settings의 스키마를 변경하지 않고 notification_types에 새로운 데이터를 추가하는 방식으로 확장할 수 있다. 예를 들어 기존 채팅, 이벤트, 친구 가입 알림 외에 선물 추천, 시스템 공지 등의 알림이 추가 되더라도 동일한 구조를 유지할 수 있다.

알림 종류별 정책이 복잡해지는 경우 notification_types에 추가 속성을 확장할 수 있다. 예를 들어 default_enabled를 추가하여 사용자 알림 설정의 기본값을 종류별로 관리하거나, category, priority, delivery_channel 등의 속성을 추가하여 알림 분류와 전달 정책을 관리할 수 있다.

향후 하나의 알림 종류가 Push, Email, SMS 등 여러 전달 채널을 지원하고 사용자별로 채널마다 수신 여부를 설정해야 한다면 별도의 notification_channels 테이블을 추가하고, notification_types와 채널 간 관계를 별도의 연결 테이블로 분리하는 방식으로 확장할 수 있다.


13. user_notification_settings

항목 내용
한글 테이블명 사용자 알림 설정
설명 사용자별 알림 종류에 대한 수신 활성화 여부를 관리하는 테이블
기능적 역할 사용자가 각 알림 종류별로 알림 수신 여부를 설정할 수 있도록 하며, 알림 발송 시 해당 사용자의 설정 값을 조회하여 발송 여부를 판단하는 데 사용한다.
주요 관계 users 1 : N user_notification_settings, notification_types 1 : N user_notification_settings
PK (user_id, notification_type_id)
FK user_id → users.id, notification_type_id → notification_types.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
user_id BIGINT NOT NULL PK, FK → users.id
notification_type_id BIGINT NOT NULL PK, FK → notification_types.id
is_enabled BOOLEAN NOT NULL; DEFAULT FALSE -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
deleted_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
user_id 알림 설정을 보유한 사용자의 식별자 users.id와 동일한 타입을 사용하여 FK 관계를 일관되게 유지한다.
notification_type_id 사용자가 수신 여부를 설정한 알림 종류의 식별자 알림 종류를 문자열이나 ENUM으로 직접 저장하지 않고 notification_types의 식별자를 참조하여 알림 종류 변경 및 확장에 대응한다.
is_enabled 해당 알림 종류에 대해 사용자가 알림 수신을 허용했는지 여부 수신 허용/비허용의 두 상태만 필요하므로 BOOLEAN을 사용한다.
created_at 사용자 알림 설정이 최초 생성된 시각 생성 시점을 자동 기록하고 다른 시간 컬럼과 타입 및 정밀도를 통일하기 위해 DATETIME(6)을 사용한다.
updated_at 사용자 알림 설정이 마지막으로 변경된 시각 설정 변경이 발생하지 않은 초기 상태와 수정된 상태를 구분할 수 있도록 NULL을 허용한다.
deleted_at 사용자 알림 설정이 논리적으로 삭제된 시각 데이터의 물리적 삭제 없이 비활성 또는 삭제 상태를 표현하기 위해 사용한다.

설계 근거

user_notification_settings는 사용자와 알림 종류 간의 설정 관계를 관리하기 위한 연결 테이블로 분리하였다. 하나의 사용자는 채팅, 이벤트, 친구 가입 등 여러 종류의 알림 설정을 가질 수 있고, 하나의 알림 종류 역시 여러 사용자의 설정 대상으로 사용되므로 users와 notification_types 사이의 N:M 관계를 user_notification_settings가 연결하는 구조로 설계하였다.

기본키는 별도의 id 대리키를 두지 않고 (user_id, notification_type_id) 복합 기본키로 구성하였다.

한 사용자가 동일한 알림 종류에 대해 두 개 이상의 설정 값을 가지는 것은 비즈니스적으로 의미가 없으므로 (user_id, notification_type_id) 조합을 PK로 사용해 동일한 사용자-알림 종류 조합의 중복 생성을 DB 수준에서 방지하도록 하였다.

별도의 id 대리키를 사용할 경우에도 동일한 사용자와 동일한 알림 종류에 대한 중복 설정을 방지하기 위해 (user_id, notification_type_id)에 UNIQUE 제약조건을 추가해야 한다. 따라서 현재처럼 단순한 연결 관계와 설정 값만 관리하는 테이블에서는 별도의 대리키를 추가했을 경우 가져갈 수 있는 이점이 크지 않다고 판단해 복합 기본키를 사용하였다.

다만 JPA 등의 ORM을 사용할 경우 복합키를 표현하기 위해 추가적인 매핑이 필요하다.

⇒ 단일 대리키를 사용하는 엔티티보다 구현 및 테스트가 다소 복잡해질 수 있다는 트레이드오프가 존재한다.

향후 확장성

새로운 알림 종류가 추가되는 경우 user_notification_settings 테이블의 컬럼이나 스키마를 변경하지 않고 notification_types에 새로운 알림 종류를 등록한 뒤 사용자별 설정 데이터를 추가하는 방식으로 확장할 수 있다.

현재는 알림 종류별 활성화 여부만 is_enabled 하나로 관리하지만, 향후 Push, Email, SMS, 카카오톡 등 여러 알림 전달 채널을 사용자가 각각 설정해야 하는 요구사항이 발생할 수 있다. 이 경우 notification_channels와 같은 별도의 채널 테이블을 추가하고 (user_id, notification_type_id, channel_id) 단위로 설정을 관리하는 구조로 확장할 수 있다.

현재 복합 PK의 첫 번째 컬럼이 user_id이므로 특정 사용자의 전체 알림 설정을 조회하는 패턴에서는 PK 인덱스를 활용할 수 있다. 따라서 마이페이지나 알림 설정 화면에서 특정 사용자의 모든 알림 설정을 조회하는 주요 패턴에 적합하다.


14. friends

항목 내용
한글 테이블명 친구 관계
설명 서비스 사용자 간의 친구 관계를 사용자 관점에서 관리하는 테이블
기능적 역할 특정 사용자가 등록한 친구를 관리하고, 친구 즐겨찾기 여부 및 친구를 기준으로 하는 이벤트·추천 등의 기능에서 친구 관계를 식별할 수 있도록 한다.
주요 관계 users 1 : N friends (owner_user_id 기준),
users 1 : N friends (friend_user_id 기준), friends 1 : N events
PK id
FK owner_user_id → users.id, friend_user_id → users.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
owner_user_id BIGINT NOT NULL FK → users.id , UK
friend_user_id BIGINT NOT NULL FK → users.id ,UK
is_favorite BOOLEAN NOT NULL; DEFAULT FALSE -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 친구 관계를 식별하는 고유 식별자 친구 관계 자체를 다른 테이블에서 독립적으로 참조할 수 있도록 대리키를 사용하며, 다른 주요 식별자와 타입을 통일하기 위해 BIGINT를 사용한다.
owner_user_id 친구 관계를 소유하는 사용자의 식별자 users.id를 참조하는 FK이므로 동일한 BIGINT 타입을 사용한다.
friend_user_id owner_user_id 사용자가 친구로 등록한 사용자의 식별자 users.id를 참조하는 FK이므로 동일한 BIGINT 타입을 사용한다.
is_favorite 해당 친구를 즐겨찾기로 지정했는지 여부 즐겨찾기/일반의 두 가지 상태만 필요하므로 BOOLEAN을 사용하며, 친구 등록 시 기본적으로 일반 친구가 되도록 FALSE를 기본값으로 설정한다.

설계 근거

friends는 사용자 간의 친구 관계를 관리하기 위해 users와 별도의 테이블로 분리하였다. 하나의 사용자는 여러 사용자를 친구로 등록할 수 있고, 하나의 사용자 역시 여러 사용자의 친구로 등록될 수 있으므로 사용자 간 자기참조 관계를 별도의 관계 테이블을 통해 표현하였다.

기본키는 id 대리키를 사용한다. 친구 관계 자체가 다른 도메인에서 독립적으로 참조되는 대상이 될 수 있기 때문이다.id 대리키를 사용하면 다른 테이블에서는 friend_id 하나만으로 특정 친구 관계를 참조할 수 있으므로 친구를 대상으로 이벤트, 선물 추천, 알림 등의 경우 관계 식별자를 하나의 값으로 전달하고 참조할 수 있다.

(owner_user_id, friend_user_id)에 복합 UNIQUE 제약조건을 추가하여 중복 친구 관계를 DB 수준에서 방지한다.

동일한 사용자가 여러 사용자의 친구로 등록되어 있더라도 각 사용자가 해당 친구를 즐겨찾기로 지정했는지는 서로 다를 수 있기 때문에 is_favorite는 친구 관계에 종속되는 사용자별 속성이므로 users가 아닌 friends에 저장하였다.

향후 확장성

친구 관계에 추가적인 사용자별 설정이 필요해질 경우 friends 테이블에 속성을 추가하여 확장할 수 있다.

현재 id를 독립적인 식별자로 사용하기 때문에 향후 events뿐 아니라 친구 기반 선물 추천, 친구별 취향 정보, 알림 등의 다른 테이블에서도 friend_id 하나를 통해 특정 친구 관계를 참조할 수 있다. 따라서 친구 관계를 사용하는 도메인이 증가하더라도 복합 FK를 여러 테이블에 반복해서 구성하지 않고 관계를 확장할 수 있다.

특정 사용자의 친구 목록 조회가 주요 조회 패턴이므로 데이터가 증가할 경우 owner_user_id를 클러스터드 인덱스를 활용할 수 있다.

즐겨찾기 친구 조회가 빈번하다면 (owner_user_id, is_favorite) 형태의 복합 인덱스를 추가할 수 있다.


15. events

항목 내용
한글 테이블명 이벤트
설명 사용자 또는 친구와 관련된 생일, 기념일, 일정, 공휴일 등의 이벤트 정보를 관리하는 테이블
기능적 역할 사용자 및 친구와 관련된 일정성 데이터를 저장하고, 이벤트 기반 알림 및 선물 추천 등의 기능에서 기준 데이터로 활용한다.
주요 관계 users 1 : N events, friends 1 : N events, events 1 : N notifications
PK id
FK user_id → users.id, friend_id → friends.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
user_id BIGINT NULL FK → users.id
friend_id BIGINT NULL FK → friends.id
source_type VARCHAR(30) NOT NULL -
event_type VARCHAR(30) NOT NULL -
external_event_id VARCHAR(255) NULL -
title VARCHAR(255) NOT NULL -
event_date DATE NOT NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
deleted_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 이벤트를 식별하는 고유 식별자 이벤트가 알림 등 다른 테이블에서 독립적으로 참조되는 엔티티이므로 단일 대리키를 사용한다.
서비스 내 주요 식별자 타입과 통일하기 위해 BIGINT를 사용한다.
user_id 이벤트와 직접 연관된 사용자의 식별자 사용자 본인의 일정 등 사용자 기반 이벤트를 연결하기 위해 사용하며, 모든 이벤트가 사용자 본인에게 직접 귀속되는 것은 아니므로 NULL을 허용한다.
friend_id 이벤트와 연관된 친구 관계의 식별자 친구 생일이나 친구 관련 기념일 등 친구 기준 이벤트를 식별하기 위해 friends.id를 참조한다.
사용자 본인 이벤트에서는 필요하지 않을 수 있으므로 NULL을 허용한다.
source_type 이벤트가 생성된 출처를 나타내는 코드 이벤트 출처가 서비스 운영 과정에서 추가·변경될 가능성이 있으므로 DB ENUM 대신 VARCHAR에 코드 값을 저장하고 애플리케이션의 Enum 또는 상수로 관리한다.
event_type 이벤트의 종류를 나타내는 코드 생일, 기념일, 일정, 공휴일 등 이벤트 종류가 향후 확장될 가능성을 고려하여 DB ENUM 대신 VARCHAR + 애플리케이션 코드 Enum 방식으로 관리한다.
external_event_id 외부 캘린더 또는 외부 서비스에서 이벤트를 식별하기 위한 ID 외부 서비스별 식별자의 형식과 길이가 다를 수 있으므로 가변 길이 문자열 타입을 사용한다.
서비스 내부에서 직접 생성된 이벤트에는 필요하지 않을 수 있어 NULL을 허용한다.
title 이벤트의 제목 일정 및 이벤트 제목은 비교적 길이가 제한된 문자열 데이터이므로 VARCHAR(255)를 사용한다.
event_date 이벤트가 발생하는 날짜 현재 이벤트 기능에서는 시간보다 날짜 자체가 중요하므로 시간 정보를 포함하지 않는 DATE 타입을 사용한다.
created_at 이벤트 데이터가 최초 생성된 시각 생성 시점을 자동 기록하고 서비스의 시간 컬럼 규칙을 통일하기 위해 DATETIME(6)을 사용한다.
updated_at 이벤트 정보가 마지막으로 수정된 시각 아직 수정되지 않은 초기 상태를 구분할 수 있도록 NULL을 허용한다.
deleted_at 이벤트가 논리적으로 삭제된 시각 기존 알림 및 외부 연동 데이터와의 관계를 보존하면서 이벤트를 비활성화할 수 있도록 Soft Delete 방식으로 관리한다.

설계 근거

events는 사용자 및 친구와 관련된 생일, 기념일, 일정, 공휴일 등의 이벤트를 통합 관리하기 위한 테이블이다. 이벤트 데이터는 이후 알림 생성이나 선물 추천 등의 기능에서 기준 데이터로 활용될 수 있으므로 독립적인 id를 기본키로 사용한다.

user_id와 friend_id는 이벤트의 대상 또는 귀속 주체를 표현하기 위해 사용한다. 사용자 본인의 일정은 user_id를 기준으로 관리하고, 친구의 생일이나 기념일 등 친구 관계에 종속된 이벤트는 friend_id를 통해 관리할 수 있도록 구성하였다. 이벤트의 성격에 따라 두 컬럼 중 하나만 필요할 수 있으므로 NULL을 허용한다.

현재 source_type과 event_type은 별도의 복잡한 메타데이터나 비즈니스 속성을 가지는 독립적인 엔티티라기보다 이벤트 처리 로직을 구분하기 위한 단순한 분류 코드의 성격이 강해 별도 테이블로 분리할 경우 단순한 코드 값 관리에 비해 테이블 구조와 구현 복잡도가 증가한다고 판단하였다.

따라서 현재 설계에서는 source_type과 event_type을 VARCHAR 컬럼에 저장하고 애플리케이션 코드의 Enum으로 허용 값을 관리하는 방식을 선택하였다.

external_event_id는 외부 캘린더 등 외부 서비스에서 제공하는 이벤트와 내부 이벤트를 연결하기 위해 사용한다. 외부 서비스마다 ID 형식이 다를 수 있으므로 숫자 타입으로 제한하지 않고 문자열 타입을 사용한다.

향후 확장성

새로운 이벤트 출처 또는 이벤트 종류가 추가되는 경우 DB 스키마 변경 없이 애플리케이션 코드 Enum에 새로운 값을 추가하는 방식으로 확장할 수 있다.

이 방식은 애플리케이션 배포와 DB Migration의 결합도를 낮추므로 무중단 배포 시에도 상대적으로 유연하게 대응할 수 있다. 신규 이벤트 타입을 먼저 인식할 수 있도록 애플리케이션 코드를 배포한 뒤 해당 타입의 데이터를 생성하는 방식으로 변경 순서를 관리할 수 있다.

이벤트 데이터가 증가하고 특정 사용자의 일정 또는 특정 날짜 범위의 이벤트 조회가 빈번해질 경우 (user_id, event_date), (friend_id, event_date) 등의 복합 인덱스를 실제 조회 패턴과 실행 계획을 기준으로 추가할 수 있다.


16. notification_types

항목 내용
한글 테이블명 알림 종류
설명 서비스에서 사용하는 알림 유형과 각 알림의 설정 가능 여부 및 활성 상태를 관리하는 마스터 테이블
기능적 역할 알림 종류를 중앙에서 관리하고, 실제 알림 데이터와 사용자별 알림 설정에서 동일한 알림 유형을 참조할 수 있도록 한다.
주요 관계 notification_types 1 : N notifications, notification_types 1 : N user_notification_settings
PK id
FK 없음

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
type VARCHAR(30) NOT NULL UK
name VARCHAR(50) NOT NULL -
is_configurable BOOLEAN NOT NULL; DEFAULT TRUE -
is_activated BOOLEAN NOT NULL; DEFAULT FALSE -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
deleted_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 알림 종류를 식별하는 고유 식별자 다른 테이블에서 알림 종류를 안정적으로 참조하기 위한 대리키로 사용하며, 서비스 내 주요 식별자 타입과 통일하기 위해 BIGINT를 사용한다.
type 애플리케이션에서 알림 종류를 식별하기 위한 타입 코드 알림 종류는 운영 중 추가·변경될 가능성이 있으므로 DB ENUM 대신 VARCHAR로 저장하고 애플리케이션 코드 Enum으로 허용 값을 관리한다.
name 화면 또는 관리자 기능에서 표시할 알림 종류의 이름 사용자에게 표시되는 명칭은 내부 식별용 타입과 분리하여 관리하며, 비교적 짧은 문자열이므로 VARCHAR(50)을 사용한다.
is_configurable 사용자가 해당 알림의 수신 여부를 직접 설정할 수 있는지 여부 사용자 설정 가능/불가능 두 상태만 필요하므로 BOOLEAN을 사용한다.
향후 필수 알림 유형을 지원하기 위해 별도 정책 컬럼으로 분리한다.
is_activated 해당 알림 종류가 현재 서비스에서 활성화되어 있는지 여부 활성/비활성 두 상태를 표현하기 위해 BOOLEAN을 사용하며, 알림 종류를 물리적으로 삭제하지 않고 운영 상태를 제어할 수 있도록 한다.
created_at 알림 종류가 최초 생성된 시각 생성 시점을 자동 기록하고 서비스 내 시간 컬럼 규칙과 통일하기 위해 DATETIME(6)을 사용한다.
updated_at 알림 종류 정보가 마지막으로 수정된 시각 수정 이력이 없는 초기 상태를 구분할 수 있도록 NULL을 허용한다.
deleted_at 알림 종류가 논리적으로 삭제된 시각 기존 알림 및 사용자 설정 데이터와의 참조 관계를 유지하면서 신규 사용만 중단할 수 있도록 Soft Delete 방식으로 관리한다.

설계 근거

notification_types는 서비스에서 사용되는 알림 종류를 하나의 기준으로 관리하기 위한 마스터 테이블이다. 알림 종류는 실제 발송된 알림을 저장하는 notifications와 사용자별 알림 수신 설정을 관리하는 user_notification_settings에서 공통으로 사용되므로, 각 테이블에서 하나의 기준 테이블을 참조하도록 구성했다.

알림 종류는 서비스 운영 과정에서 새로운 기능이 추가되면서 함께 증가하거나 변경될 가능성이 있는 값이라고 판단했다.

따라서 알림 종류는 DB 스키마에 강하게 결합시키기보다 VARCHAR로 저장하고 애플리케이션의 코드 Enum을 통해 허용되는 값을 관리하도록 설계했다.

notification_types를 별도 테이블로 둔 이유는 단순히 알림 타입 문자열만 관리하기 위한 목적이 아니다. 알림 종류별로 name, is_configurable, is_activated와 같은 추가 속성을 함께 관리해야 하므로 알림 종류 자체가 별도의 관리 대상이라고 판단했다.

is_configurable은 현재 서비스 기획상 모든 알림에 대해 사용자가 수신 여부를 제어하는 기능이 반드시 필요한 것은 아니지만, 향후 필수 알림이 추가될 가능성을 고려하여 사전에 분리한 컬럼이다.

기본키는 id 대리키를 사용한다. type은 애플리케이션의 비즈니스 의미를 가지는 값으로 향후 명칭이 변경될 가능성이 있으며, 이를 PK로 사용하면 해당 값을 참조하는 모든 FK에 영향을 줄 수 있다. 따라서 변경 가능성이 낮은 BIGINT id를 관계 식별자로 사용하고 type은 비즈니스 식별 코드로 별도로 관리한다.

향후 확장성

새로운 알림 종류가 추가되는 경우 notifications 또는 user_notification_settings의 스키마를 변경하지 않고 notification_types에 새로운 알림 종류를 등록하는 방식으로 확장할 수 있다. 알림 종류를 식별하는 type 역시 DB ENUM이 아니므로 새로운 코드 추가로 인한 테이블 DDL 변경이 필요하지 않는다.

향후 필수 알림이 추가되면 is_configurable = FALSE로 설정하여 사용자 알림 설정 화면에서 제외하거나 비활성화 기능을 제공하지 않는 방식으로 처리할 수 있다. 이를 통해 사용자 설정 가능 여부와 실제 알림 종류를 독립적으로 관리할 수 있다.

알림 정책이 더 복잡해질 경우 notification_types에 알림 우선순위, 기본 활성화 여부, 표시 카테고리 등의 속성을 추가할 수 있다.


17. user_notification_settings

항목 내용
한글 테이블명 사용자 알림 설정
설명 사용자별로 각 알림 종류의 수신 여부를 관리하는 테이블
기능적 역할 사용자가 알림 종류별로 수신 여부를 설정할 수 있도록 하며, 알림 생성 또는 발송 시 해당 사용자의 설정값을 기준으로 수신 가능 여부를 판단한다.
주요 관계 users 1 : N user_notification_settings, notification_types 1 : N user_notification_settings
PK (user_id, notification_type_id)
FK user_id → users.id, notification_type_id → notification_types.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
user_id BIGINT NOT NULL PK, FK → users.id
notification_type_id BIGINT NOT NULL PK, FK → notification_types.id
is_enabled BOOLEAN NOT NULL; DEFAULT FALSE -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
deleted_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
user_id 알림 설정의 대상이 되는 사용자 식별자 users.id를 참조하는 FK이므로 동일한 BIGINT 타입을 사용한다.
notification_type_id 사용자가 설정한 알림 종류의 식별자 notification_types.id를 참조하는 FK이므로 동일한 BIGINT 타입을 사용한다.
is_enabled 해당 알림 종류에 대해 사용자가 수신을 허용했는지 여부 수신/미수신 두 상태만 필요하므로 BOOLEAN을 사용한다.
created_at 사용자 알림 설정이 최초 생성된 시각 생성 시점을 자동 기록하고 서비스 내 시간 컬럼 규칙과 통일하기 위해 DATETIME(6)을 사용한다.
updated_at 사용자 알림 설정이 마지막으로 변경된 시각 설정 변경 이력이 없는 초기 상태를 구분할 수 있도록 NULL을 허용한다.
deleted_at 사용자 알림 설정이 논리적으로 삭제된 시각 설정 이력을 보존하거나 복구 가능성을 고려하여 Soft Delete 용도로 사용한다.

설계 근거

user_notification_settings는 사용자와 알림 종류 사이의 설정 관계를 관리하기 위한 연결 테이블이다. 하나의 사용자는 여러 알림 종류에 대한 설정을 가질 수 있고, 하나의 알림 종류 역시 여러 사용자에게 적용될 수 있으므로 users와 notification_types 사이의 N : M 관계를 해당 테이블을 통해 표현했다.

이 테이블의 하나의 행은 특정 사용자가 특정 알림 종류에 대해 가지고 있는 수신 설정을 의미한다. 따라서 (user_id, notification_type_id) 조합 자체가 하나의 레코드를 자연스럽게 식별할 수 있는 비즈니스 키가 된다.

복합 기본키를 사용하는 경우 MySQL InnoDB의 클러스터드 인덱스 역시 (user_id, notification_type_id) 순서로 구성된다. 따라서 특정 사용자의 전체 알림 설정을 조회하는 주요 패턴에서는 user_id가 PK의 선두 컬럼이므로 별도의 user_id 인덱스를 추가하지 않아도 기본키 인덱스를 활용할 수 있다

향후 확장성

향후 알림 설정에 Push 수신 여부 이외의 추가 속성이 필요해질 경우 해당 사용자-알림 종류 관계에 종속되는 컬럼을 추가할 수 있다. 예를 들어 알림 수신 시간대, 방해 금지 시간, 알림 빈도 등의 설정이 필요해진다면 user_notification_settings에 관련 속성을 추가하여 확장할 수 있다.

향후 user_notification_settings의 개별 레코드를 다른 테이블에서 직접 참조해야 하는 요구사항이 발생하거나, 한 사용자와 알림 종류 조합에 여러 개의 독립적인 설정 레코드가 존재해야 한다면 현재 복합 PK 구조를 재검토할 수 있다. 이 경우 대리키 id를 추가하고 (user_id, notification_type_id) 또는 확장된 비즈니스 키에 UNIQUE 제약조건을 두는 구조가 더 적합할 수 있다.


18. notification_events

항목 내용
한글 테이블명 알림 이벤트
설명 서비스 내 특정 행위나 이벤트로 인해 발생한 알림의 원인 정보와 공통 알림 내용을 관리하는 테이블
기능적 역할 알림 종류, 알림을 발생시킨 사용자, 관련 리소스 및 사용자에게 표시할 제목과 내용을 저장하고, 하나의 알림 이벤트를 여러 사용자의 실제 알림 데이터와 연결할 수 있도록 한다.
주요 관계 notification_types 1 : N notification_events,
users 1 : N notification_events (actor_user_id 기준),
notification_events 1 : N notifications
PK id
FK notification_type_id → notification_types.id,
actor_user_id → users.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
notification_type_id BIGINT NOT NULL FK → notification_types.id
actor_user_id BIGINT NULL FK → users.id
resource_type VARCHAR(30) NULL -
resource_id BIGINT NULL -
title VARCHAR(255) NOT NULL -
content VARCHAR(500) NOT NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
deleted_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 알림 이벤트를 식별하는 고유 식별자 하나의 알림 이벤트로 여러 수신자의 알림이 생성될 수 있고, notifications에서 해당 이벤트를 공통으로 참조해야 하므로 독립적인 대리키를 사용한다. 서비스 내 주요 식별자와 타입을 통일하기 위해 BIGINT를 사용한다.
notification_type_id 발생한 알림의 종류를 식별하는 값 알림 종류를 문자열로 직접 저장하지 않고 notification_types의 식별자를 참조하여 알림 종류의 이름, 설정 가능 여부, 활성 상태 등을 중앙에서 관리할 수 있도록 한다.
actor_user_id 알림 발생의 원인이 된 사용자 식별자 친구 가입, 익명 메시지 등 특정 사용자의 행위로 발생한 알림의 행위자를 식별하기 위해 users.id를 참조한다. 시스템 알림처럼 특정 행위자가 존재하지 않는 경우를 고려하여 NULL을 허용한다.
resource_type 알림과 관련된 원본 리소스의 종류 하나의 알림 이벤트 테이블에서 익명 대화방, 일정, 사용자 등 여러 종류의 리소스를 표현할 수 있도록 리소스 종류를 문자열로 저장한다. 새로운 리소스 종류 추가 가능성을 고려해 DB ENUM 대신 VARCHAR를 사용한다.
resource_id 알림과 관련된 원본 리소스의 식별자 resource_type에서 지정한 리소스의 식별자를 저장하여 사용자가 알림을 선택했을 때 관련 화면이나 데이터로 이동하는 데 활용할 수 있다. 리소스가 필요하지 않은 알림을 고려해 NULL을 허용한다.
title 사용자에게 표시할 알림 제목 알림 생성 당시의 제목을 그대로 보존하고 빠르게 조회할 수 있도록 별도의 문자열 컬럼으로 저장한다.
content 사용자에게 표시할 알림 내용 알림의 간단한 설명을 저장하며 현재 서비스에서 알림 본문의 최대 길이가 제한적인 짧은 문자열이라는 점을 고려해 VARCHAR(500)을 사용한다.
created_at 알림 이벤트가 생성된 시각 실제 알림의 발생 시점을 기록하고 서비스 내 시간 컬럼 규칙과 통일하기 위해 DATETIME(6)을 사용한다.
updated_at 알림 이벤트 정보가 마지막으로 수정된 시각 제목, 내용 또는 관련 정보가 변경되는 경우 최종 수정 시점을 관리하기 위해 사용한다.
deleted_at 알림 이벤트를 논리적으로 삭제한 시각 이미 생성된 notifications와의 참조 관계를 유지하면서 해당 이벤트를 신규 처리 대상에서 제외할 수 있도록 Soft Delete 용도로 사용한다.

설계 근거

notification_events는 알림의 발생 원인 및 공통 정보와 실제 사용자에게 전달되는 개별 알림 상태를 분리하기 위해 구성하였다.

기존처럼 notifications 테이블에서 알림 종류, 발신 사용자, 원본 이벤트, 제목 및 내용을 모두 직접 관리하면 동일한 이벤트로 여러 사용자에게 알림이 생성되는 경우 동일한 데이터가 수신자 수만큼 반복 저장될 수 있다.

따라서 알림의 발생 원인과 공통 정보는 notification_events에 한 번 저장하고, 실제 수신자별 알림은 notifications에서 notification_event_id를 참조하도록 구성하였다.

알림 종류는 notification_type_id를 통해 notification_types를 참조한다. 이를 통해 채팅, 이벤트, 친구 가입 등 알림 종류가 추가되더라도 notification_events의 스키마를 변경하지 않고 마스터 데이터 추가를 통해 확장할 수 있다.

resource_type과 resource_id는 여러 종류의 원본 리소스를 하나의 구조로 표현하기 위해 사용한다. 예를 들어 다음과 같이 사용할 수 있다.

resource_type = 'ANONYMOUS_ROOM'
resource_id   = 100

resource_type = 'EVENT'
resource_id   = 200

resource_type = 'USER'
resource_id   = 300

이 구조를 사용하면 알림 종류마다 anonymous_room_id, event_id, friend_id와 같은 Nullable FK 컬럼을 계속 추가하지 않고 새로운 리소스 종류를 수용할 수 있다.

다만 resource_type, resource_id 조합은 실제 DB FK가 아닌 논리적인 참조 관계이다. 따라서 해당 리소스가 실제 존재하는지에 대한 무결성은 애플리케이션 레벨에서 검증해야 한다는 트레이드오프가 있다.

향후 확장성

알림이 참조할 수 있는 원본 리소스 종류가 증가하더라도 resource_type, resource_id 구조를 사용하면 새로운 FK 컬럼을 추가하지 않고 기존 테이블 구조를 유지할 수 있다.

반면 리소스별 참조 무결성이 중요해지거나 특정 리소스의 추가 속성을 알림 이벤트에서 직접 사용해야 하는 요구사항이 커질 경우 알림 유형별 상세 테이블을 분리하거나 별도의 알림 리소스 매핑 테이블을 구성하는 방법을 고려할 수 있다.

알림 이벤트가 대량으로 증가할 경우 알림 종류별 조회를 위해 (notification_type_id, created_at), 특정 사용자에 의해 발생한 이벤트 조회를 위해 (actor_user_id, created_at) 등의 복합 인덱스를 실제 조회 패턴과 실행 계획에 따라 검토할 수 있다.

19. notifications

항목 내용
한글 테이블명 알림
설명 사용자에게 전달되는 실제 알림 데이터를 저장하는 테이블
기능적 역할 이벤트, 사용자 활동, 익명 대화 등 서비스 내 다양한 행위로 인해 생성된 알림을 수신자 기준으로 저장하고, 알림 종류·읽음 여부·유효기간 등을 관리한다.
주요 관계 events 1 : N notifications, users 1 : N notifications (sender_user_id 기준),
users 1 : N notifications (receiver_user_id 기준),
anonymous_rooms 1 : N notifications, notification_types 1 : N notifications
PK id
FK event_id → events.id, sender_user_id → users.id,
receiver_user_id → users.id, anonymous_room_id → anonymous_rooms.id,
notification_type_id → notification_types.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
event_id BIGINT NULL FK → events.id
sender_user_id BIGINT NULL FK → users.id
receiver_user_id BIGINT NOT NULL FK → users.id
anonymous_room_id BIGINT NULL FK → anonymous_rooms.id
notification_type_id BIGINT NOT NULL FK → notification_types.id
title VARCHAR(255) NOT NULL -
content VARCHAR(500) NULL -
read_at DATETIME(6) NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
expires_at DATETIME(6) NOT NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 알림을 식별하는 고유 식별자 각 알림은 독립적인 데이터로 생성되고 조회·읽음 처리 등의 대상이 되므로 단일 대리키를 사용한다.
서비스 내 주요 식별자 타입과 통일하기 위해 BIGINT를 사용한다.
event_id 해당 알림을 발생시킨 이벤트의 식별자 생일, 기념일, 일정 등의 이벤트 기반 알림을 원본 이벤트와 연결하기 위해 사용한다.
모든 알림이 이벤트에서 발생하는 것은 아니므로 NULL을 허용한다.
sender_user_id 알림을 발생시킨 사용자 또는 발신 사용자의 식별자 친구 가입, 콕 찌르기, 메시지 등 사용자 행동에 의해 생성되는 알림에서 발신자를 식별하기 위해 사용한다.
시스템 또는 이벤트 기반 알림에서는 발신 사용자가 없을 수 있으므로 NULL을 허용한다.
receiver_user_id 알림을 수신하는 사용자의 식별자 모든 알림은 특정 사용자에게 전달되므로 반드시 존재해야 하며 users.id를 참조한다.
anonymous_room_id 익명 대화와 관련된 알림의 대화방 식별자 익명 질문이나 답변 등 익명 대화에서 발생한 알림을 해당 대화방과 연결하기 위해 사용하며, 일반 알림에서는 필요하지 않아 NULL을 허용한다.
notification_type_id 알림의 종류를 식별하는 식별자 알림 종류 자체의 속성과 정책은 notification_types에서 관리하고, 실제 알림에서는 해당 마스터 데이터의 식별자만 참조하도록 한다.
title 알림 목록에 표시되는 제목 사용자에게 표시되는 비교적 짧은 문자열이므로 VARCHAR(255)를 사용한다.
content 알림의 간단한 상세 내용 알림 목록에서 제목과 함께 표시할 보조 설명을 저장하며, 짧은 안내 문구 수준으로 제한하기 위해 VARCHAR(500)을 사용한다.
내용이 필요 없는 유형을 고려하여 NULL을 허용한다.
read_at 사용자가 해당 알림을 읽은 시각 NULL이면 읽지 않은 알림, 값이 존재하면 읽은 알림으로 판단할 수 있으며 읽은 정확한 시점까지 함께 저장할 수 있다.
created_at 알림이 생성된 시각 알림 목록의 최신순 정렬 및 생성 시점 기록에 사용하며, 서비스 내 시간 타입과 통일하기 위해 DATETIME(6)을 사용한다.
expires_at 해당 알림의 유효기간이 종료되는 시각 일정 기간 이후 노출 또는 처리 대상에서 제외해야 하는 알림의 만료 시점을 명시적으로 관리하기 위해 사용한다.

설계 근거

notifications는 실제 사용자에게 생성된 개별 알림 데이터를 저장하는 테이블이다. 하나의 알림 종류에서 여러 개의 실제 알림이 생성될 수 있으므로 notification_types와 분리하고 notification_type_id를 통해 참조하도록 구성했다.

기본키는 id 단일 대리키를 사용하였다. notifications의 각 행은 사용자에게 전달되는 하나의 독립적인 알림을 의미하며, 동일한 이벤트나 동일한 사용자 간 행위에서도 여러 알림이 생성될 수 있다. 따라서 receiver_user_id, notification_type_id 등의 조합만으로는 각 알림을 유일하게 식별할 수 없으며 독립적인 식별자가 필요하다.

또한 알림 읽음 처리와 알림 상세 조회 등의 API에서 특정 알림 한 건을 직접 식별하는 경우가 많으므로 단일 id를 사용하면 요청 경로 및 ORM 매핑을 단순하게 구성할 수 있다.

event_id, sender_user_id, anonymous_room_id는 모두 NULL을 허용한다. 이는 하나의 알림이 모든 원본 엔티티를 동시에 필요로 하지 않기 때문이다. 예를 들어 이벤트 알림은 event_id를 사용할 수 있고, 사용자 행동 기반 알림은 sender_user_id를 사용할 수 있으며, 익명 대화 관련 알림은 anonymous_room_id를 사용할 수 있다.

receiver_user_id는 NOT NULL로 구성하였다. 알림은 최종적으로 특정 사용자의 알림 목록에 노출되는 데이터이므로 어떤 사용자가 알림을 받는지 반드시 식별할 수 있어야 한다.

향후 확장성

알림 데이터가 증가할 경우 사용자의 알림 목록 조회가 주요 조회 패턴이 될 가능성이 높으므로 (receiver_user_id, created_at) 복합 인덱스를 검토할 수 있다.

읽지 않은 알림 조회가 빈번할 경우 receiver_user_id, read_at, created_at을 활용하는 복합 인덱스를 실제 쿼리와 실행 계획을 기준으로 검토할 수 있다.

expires_at 기준으로 만료 알림을 정리하는 배치 작업이 수행되는 경우 만료 데이터 탐색 성능을 위해 expires_at 인덱스 추가를 검토할 수 있다.

알림 생성의 중복 방지가 필요한 경우 원본 이벤트의 식별자와 알림 종류 등을 기반으로 별도의 deduplication_key를 추가하는 방식도 검토할 수 있다.


20. anonymous_rooms

항목 내용
한글 테이블명 익명 대화방
설명 특정 상품에 대해 질문자와 수신자가 익명으로 대화할 수 있는 대화방 정보를 관리하는 테이블
기능적 역할 익명 질문 기능에서 대화의 참여자와 대상 상품을 연결하고, 익명 메시지 및 관련 알림이 동일한 대화 단위를 참조할 수 있도록 한다.
주요 관계 products 1 : N anonymous_rooms, users 1 : N anonymous_rooms (questioner_user_id 기준),
users 1 : N anonymous_rooms (recipient_user_id 기준),
anonymous_rooms 1 : N anonymous_messages, anonymous_rooms 1 : N notifications
PK id
FK product_id → products.id, questioner_user_id → users.id, recipient_user_id → users.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
product_id BIGINT NOT NULL FK → products.id , UK
questioner_user_id BIGINT NOT NULL FK → users.id , UK
recipient_user_id BIGINT NOT NULL FK → users.id
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
opened_at DATETIME(6) NULL -
expires_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 익명 대화방을 식별하는 고유 식별자 익명 대화방 자체가 메시지와 알림 등 다른 테이블에서 독립적으로 참조되는 엔티티이므로 단일 대리키를 사용한다.
서비스 내 주요 식별자 타입과 통일하기 위해 BIGINT를 사용한다.
product_id 익명 질문의 대상이 되는 상품 식별자 익명 대화가 특정 상품에 대한 질문을 중심으로 생성되므로 해당 상품을 식별하기 위해 products.id를 참조한다.
questioner_user_id 익명 질문을 시작한 사용자의 식별자 질문자의 실제 식별 정보는 서비스 내부에서 메시지 처리 및 권한 검증을 위해 필요하므로 users.id를 참조한다.
recipient_user_id 익명 질문을 전달받는 사용자의 식별자 질문을 수신하고 답변할 사용자를 식별하기 위해 users.id를 참조한다.
created_at 익명 대화방이 생성된 시각 대화방 생성 시점을 기록하고 서비스 내 시간 컬럼 규칙과 통일하기 위해 DATETIME(6)을 사용한다.
opened_at 수신자가 익명 대화방을 최초로 확인한 시각 아직 대화방을 확인하지 않은 상태와 확인한 상태를 구분하면서 최초 확인 시점도 함께 기록할 수 있도록 NULL을 허용한다.
expires_at 익명 대화방의 유효기간이 종료되는 시각 익명 질문의 이용 가능 기간을 명시적으로 관리하기 위해 사용하며, 만료 정책이 적용되지 않는 경우를 고려하여 NULL을 허용한다.

설계 근거

하나의 대화방에는 여러 개의 anonymous_messages가 존재할 수 있으며, 익명 질문 또는 답변으로 발생한 알림 역시 특정 익명 대화방과 연결되어야 하므로 메시지 데이터와 별도로 대화방 자체를 관리한다.

기본키는 대리키를 사용했다. 질문자, 수신자, 상품의 조합만으로도 어떤 관계의 대화인지 표현할 수 있지만, 동일한 질문자가 동일한 사용자에게 동일한 상품을 대상으로 서로 다른 시점에 여러 익명 대화를 생성할 가능성이 있다. 따라서 해당 컬럼들의 조합을 하나의 대화방을 유일하게 식별하는 자연키로 사용하기에는 적합하지 않다.

현재 구조에서는 anonymous_messages.room_id와 notifications.anonymous_room_id가 anonymous_rooms.id를 참조한다. 따라서 대리키를 사용하면 room_id 또는 anonymous_room_id 하나만으로 특정 대화방을 참조할 수 있다.

questioner_user_id와 recipient_user_id는 모두 users.id를 참조한다. 익명성은 DB에서 질문자의 신원을 제거하는 방식이 아니라 수신자에게 질문자의 실제 식별 정보를 노출하지 않는 애플리케이션 정책으로 보장한다.

opened_at은 최초 확인 시각 자체를 저장한다. NULL인 경우 아직 확인하지 않은 상태로 판단할 수 있고, 값이 존재하면 확인 여부뿐 아니라 언제 대화방을 확인했는지까지 함께 파악할 수 있으므로 중복 상태 컬럼을 추가할 필요가 없다.

expires_at은 익명 질문의 유효기간을 관리하기 위해 사용한다. 대화방을 생성할 때 유효기간 정책에 따라 만료 시각을 계산하여 저장하면 조회 시 현재 시각과 비교해 활성화된 대화방만 노출할 수 있다.

향후 확장성

향후 익명 대화방 자체의 상태 관리가 필요해질 경우 status 컬럼을 추가하여 ACTIVE, EXPIRED, CLOSED, BLOCKED 등의 상태를 명시적으로 관리할 수 있다. 현재는 expires_at 등의 시각 정보를 통해 상태를 판단할 수 있으므로 별도의 상태 컬럼이 반드시 필요하지는 않다.

대화방이 만료되더라도 기존 메시지나 알림 기록을 유지해야 한다면 물리적으로 대화방을 삭제하지 않고 expires_at을 기준으로 사용자에게만 노출되지 않도록 처리할 수 있다. 향후 명시적인 삭제 기능까지 필요한 경우 deleted_at을 추가하여 Soft Delete 구조를 적용할 수 있다.

익명 대화방 목록 조회가 증가하면 사용자 역할에 따라 questioner_user_id 또는 recipient_user_id를 기준으로 조회할 가능성이 높다. 따라서 실제 조회 패턴에 따라 (questioner_user_id, created_at) 또는 (recipient_user_id, created_at) 복합 인덱스 추가를 검토할 수 있다.

만료된 대화방을 주기적으로 조회하거나 배치 처리하는 경우에는 expires_at 인덱스를 추가하여 만료 대상 탐색 성능을 개선할 수 있다.


21. anonymous_messages

항목 내용
한글 테이블명 익명 메시지
설명 익명 대화방에서 질문자와 수신자가 주고받은 개별 메시지를 저장하는 테이블
기능적 역할 익명 대화방별 메시지 내용을 시간 순서대로 저장하고, 메시지 작성자·읽음 여부 등을 관리한다.
향후 메시지 단위 신고 및 관리 기능에서 개별 메시지를 식별할 수 있도록 한다.
주요 관계 anonymous_rooms 1 : N anonymous_messages, users 1 : N anonymous_messages
PK id
FK room_id → anonymous_rooms.id, user_id → users.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id VARCHAR(255) NOT NULL PK
room_id BIGINT NOT NULL FK → anonymous_rooms.id
user_id BIGINT NOT NULL FK → users.id
content VARCHAR(1000) NOT NULL -
read_at DATETIME(6) NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
deleted_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 개별 익명 메시지를 식별하는 고유 식별자 메시지를 대화방 내부의 순서만으로 식별하지 않고 독립적인 엔티티로 식별하기 위한 대리키이다.
향후 메시지 단위 신고·제재·조회 등의 기능에서 특정 메시지를 직접 참조할 수 있도록 한다.
room_id 메시지가 속한 익명 대화방의 식별자 하나의 익명 대화방에 여러 메시지가 포함되므로 각 메시지가 어떤 대화에 속하는지 식별하기 위해 anonymous_rooms.id를 참조한다.
user_id 메시지를 작성한 사용자의 식별자 메시지 작성 권한 검증 및 질문자·수신자 구분을 위해 실제 작성자의 사용자 식별자를 저장한다.
content 사용자가 전송한 메시지의 본문 익명 대화 메시지는 일정 길이 이하의 문자열로 제한할 수 있으므로 VARCHAR(1000)을 사용한다.
read_at 상대 사용자가 메시지를 읽은 시각 NULL 여부를 통해 읽음 여부를 판단하면서 실제 읽은 시점도 함께 기록할 수 있도록 한다.
created_at 메시지가 생성된 시각 대화 메시지를 시간 순서대로 조회하고 전송 시점을 기록하기 위해 사용한다.
서비스 내 시간 컬럼 타입과 통일하기 위해 DATETIME(6)을 사용한다.
updated_at 메시지가 마지막으로 수정된 시각 향후 메시지 수정 기능 또는 관리 작업이 필요한 경우 변경 시점을 기록할 수 있도록 하며, 수정되지 않은 초기 상태를 구분하기 위해 NULL을 허용한다.
deleted_at 메시지가 논리적으로 삭제된 시각 메시지 삭제 후에도 신고·운영 이력 등의 필요에 따라 데이터를 유지할 수 있도록 Soft Delete 방식으로 관리한다.

설계 근거

하나의 익명 대화방에서는 질문자와 수신자가 여러 메시지를 주고받을 수 있으므로 대화방 정보와 실제 메시지 데이터를 분리하여 1 : N 관계로 구성하였다.

기본키는 별도의 id 대리키를 사용하였다.

현재 서비스 기획에서는 신고 기능이 개별 메시지가 아닌 익명 대화방 단위로 이루어지기 때문에 현재 요구사항만 고려하면 메시지를 다른 테이블에서 직접 참조할 필요가 크지 않다. 하지만 서비스 운영 과정에서 특정 메시지만을 신고하거나 제재해야 하는 요구사항이 추가될 가능성을 고려하였다.

향후 확장성

향후 신고 기능이 현재의 대화방 단위에서 메시지 단위까지 확장될 경우 anonymous_messages.id를 신고 대상의 식별자로 활용할 수 있다. 현재부터 메시지별대리키를 부여하고 있기 때문에 기존 테이블의 PK 구조를 변경하지 않고 신고 테이블과 FK 관계를 추가할 수 있다.

익명 대화의 메시지 수가 증가할 경우 가장 주요한 조회 패턴은 특정 대화방의 메시지를 시간 순서로 조회하는 형태가 될 가능성이 높다. 따라서 (room_id, created_at) 복합 인덱스 추가를 검토할 수 있다.

동일한 created_at을 가지는 메시지가 존재할 가능성까지 고려하여 안정적인 정렬 또는 커서 기반 페이지네이션이 필요하다면 (room_id, created_at, id) 형태의 인덱스를 구성하고 created_at과 id를 함께 커서로 사용하는 방식도 검토할 수 있다.

향후 메시지 전송 재시도로 인한 중복 메시지 생성을 방지해야 할 경우 클라이언트에서 생성한 client_message_id를 별도로 저장하고 UNIQUE 제약조건을 적용하는 방식도 고려할 수 있다. 이는 서버가 동일 요청을 여러 번 전달받더라도 동일 메시지가 중복 저장되는 것을 방지하는 용도로 활용할 수 있다.


22. reports

항목 내용
한글 테이블명 신고
설명 익명 대화에서 발생한 사용자 신고 내역과 신고 처리 상태를 관리하는 테이블
기능적 역할 신고자, 신고 대상 사용자, 신고 대상 대화방과 신고 사유를 저장하고 운영자의 신고 처리 상태 및 승인 시점을 관리한다.
주요 관계 users 1 : N reports (reporter_user_id 기준),
users 1 : N reports (reported_user_id 기준), anonymous_rooms 1 : N reports
PK id
FK reporter_user_id → users.id, reported_user_id → users.id, room_id → anonymous_rooms.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
reporter_user_id BIGINT NOT NULL FK → users.id
reported_user_id BIGINT NOT NULL FK → users.id UK
room_id BIGINT NOT NULL FK → anonymous_rooms.id UK
type VARCHAR(30) NOT NULL
reason VARCHAR(500) NULL -
status ENUM('PENDING', 'APPROVED', 'REJECTED') NOT NULL; DEFAULT 'PENDING' -
approved_at DATETIME(6) NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 신고 내역을 식별하는 고유 식별자 동일한 사용자나 대화방에서 여러 신고가 발생할 수 있고 개별 신고 건을 독립적으로 처리해야 하므로 대리키를 사용한다.
reporter_user_id 신고를 요청한 사용자 식별자 신고 주체를 식별하고 중복 신고 검증 및 운영 처리에 활용하기 위해 users.id를 참조한다.
reported_user_id 신고 대상 사용자의 식별자 신고된 사용자를 식별하고 신고 누적 및 제재 판단 등에 활용하기 위해 users.id를 참조한다.
room_id 신고 대상이 되는 익명 대화방 식별자 현재 서비스에서는 개별 메시지가 아닌 대화방 단위로 신고하므로 anonymous_rooms.id를 참조한다.
type 신고 유형 신고 유형은 서비스 운영 과정에서 추가·변경될 가능성이 있으므로 DB ENUM 대신 VARCHAR(30)으로 저장하고 애플리케이션 코드 Enum으로 허용 값을 관리한다.
reason 사용자가 입력한 신고 상세 사유 정형화된 신고 유형 외에 추가 설명이 필요한 경우를 지원하기 위해 문자열로 저장하며, 선택 입력을 고려해 NULL을 허용한다.
status 신고 처리 상태 신고 처리 흐름을 구성하는 상태 값은 변경 가능성이 낮고 허용되지 않은 상태가 저장되지 않도록 DB 수준에서 강제할 필요가 있다고 판단하여 ENUM으로 구성한다.
approved_at 신고가 승인된 시각 승인되지 않은 신고에서는 값이 존재하지 않으므로 NULL을 허용하며, 승인 여부뿐 아니라 실제 승인 시점까지 기록한다.
created_at 신고가 접수된 시각 신고 생성 시점을 기록하고 서비스 내 시간 컬럼 타입을 통일하기 위해 DATETIME(6)을 사용한다.

설계 근거

하나의 대화방이나 동일한 사용자에 대해 여러 번의 신고가 발생할 수 있고 각각의 신고는 독립적인 처리 상태를 가질 수 있으므로 별도의 id 대리키를 기본키로 사용하였다.

현재 신고 기능은 개별 메시지가 아니라 익명 대화방 단위로 신고하는 구조이므로 room_id를 통해 anonymous_rooms를 참조한다. 신고 처리 과정에서는 신고를 요청한 사용자와 신고 대상 사용자를 모두 식별해야 하므로 reporter_user_id, reported_user_id를 각각 별도의 FK로 구성하였다. 현재 DDL에서도 두 사용자 FK와 anonymous_rooms FK가 정의되어 있다.

type과 status는 모두 제한된 문자열 값이라는 점에서는 유사하지만, 값의 변경 가능성과 비즈니스 의미가 다르기 때문에 서로 다른 방식으로 관리한다.

신고 유형은 운영환경 확장성 고려해 DB ENUM으로 고정하지 않고 VARCHAR(30)으로 저장하며 애플리케이션의 코드 Enum에서 허용되는 값을 관리하도록 구성했다.

status는 신고 유형과 달리 신고 처리 프로세스 자체를 나타낸다. 현재 DDL에서는 PENDING, APPROVED, REJECTED 세 상태를 사용하며 신고가 접수되면 기본적으로 PENDING 상태로 생성된다.

향후 확장성

향후 새로운 신고 유형이 추가되더라도 type이 VARCHAR(30)으로 구성되어 있으므로 DB ENUM 변경 없이 애플리케이션 Enum에 새로운 값을 추가하여 대응할 수 있다.

신고 유형별 이름이나 설명, 신고 정책, 자동 제재 기준 등 추가적인 메타데이터가 필요해지는 경우에는 현재 코드 Enum 관리 방식을 유지하기보다 report_types와 같은 마스터 테이블을 별도로 분리하는 방안을 검토할 수 있다.

현재는 대화방 단위 신고만 지원하지만 향후 특정 메시지를 신고하는 기능이 추가될 경우 앞서 anonymous_messages에 부여한 대리키를 활용할 수 있다. 이 경우 anonymous_message_id를 추가하여 특정 메시지를 직접 참조하거나 신고 대상의 모델링 방식 자체를 재검토할 수 있다.


23. ai_chat_rooms

항목 내용
한글 테이블명 AI 대화방
설명 사용자와 AI가 주고받는 대화를 하나의 대화 단위로 관리하는 테이블
기능적 역할 사용자별 AI 대화 세션을 생성하고 대화 상태와 원문 삭제 예정 시점을 관리하며, AI 메시지와 취향 분석 데이터를 동일한 대화방 단위로 연결한다.
주요 관계 users 1 : N ai_chat_rooms, ai_chat_rooms 1 : N ai_messages, ai_chat_rooms 1 : 1 taste_analyses
PK id
FK user_id → users.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
user_id BIGINT NOT NULL FK → users.id
status ENUM('ACTIVE', 'ANALYZING', 'COMPLETED') NOT NULL; DEFAULT 'ACTIVE' -
purge_at DATETIME(6) NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
deleted_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id AI 대화방을 식별하는 고유 식별자 하나의 사용자가 여러 AI 대화를 생성할 수 있고 메시지와 취향 분석에서 특정 대화방을 참조해야 하므로 독립적인 대리키를 사용한다.
user_id AI 대화를 진행하는 사용자 식별자 어떤 사용자의 AI 대화인지 식별하기 위해 users.id를 참조한다.
status AI 대화방의 현재 진행 상태 대화 진행 상태가 ACTIVE → ANALYZING → COMPLETED와 같이 제한된 상태 집합으로 관리되므로 DB ENUM을 통해 허용 가능한 값을 제한한다.
purge_at AI 대화 원문을 삭제할 예정 시각 대화 원문 보관 정책에 따라 삭제 예정 시점을 관리하기 위해 사용한다.
삭제 예정 시점이 아직 결정되지 않은 상태를 고려하여 NULL을 허용한다.
created_at AI 대화방이 생성된 시각 대화 생성 시점을 기록하고 서비스 내 시간 컬럼 타입과 통일하기 위해 DATETIME(6)을 사용한다.
updated_at AI 대화방 정보가 마지막으로 변경된 시각 상태 변경이나 원문 삭제 예정 시점 변경 등의 수정 시점을 기록하기 위해 사용한다.
deleted_at AI 대화방이 논리적으로 삭제된 시각 대화방을 즉시 물리 삭제하지 않고 필요 시 복구 또는 운영 기록을 유지할 수 있도록 Soft Delete 용도로 사용한다.

설계 근거

하나의 사용자가 여러 번 AI와 대화를 시작할 수 있으므로 user_id 자체를 기본키로 사용하지 않고 각각의 대화를 독립적으로 식별할 수 있도록 별도의 id 대리키를 사용하였다.

AI 대화방은 단순히 사용자와 AI 사이의 연결 관계만을 나타내는 테이블이 아니라 대화 상태, 생성 시점, 원문 삭제 예정 시점 등 자체적인 생명주기와 상태를 가지는 엔티티이다. 따라서 (user_id, ...)와 같은 자연키를 구성하기보다 독립적인 id를 통해 대화방 자체를 식별하도록 구성하였다.

취향 분석의 경우 개별 메시지가 아니라 하나의 AI 대화방을 기준으로 수행되므로 taste_analyses.room_id가 ai_chat_rooms.id를 참조하도록 구성하였다. 현재 taste_analyses는 room_id 자체를 PK로 사용하고 있기 때문에 하나의 AI 대화방에 하나의 취향 분석 데이터만 존재하는 구조이다.

user_id는 대화방의 소유자를 나타낸다. 하나의 사용자가 여러 대화방을 가질 수 있으므로 users와 ai_chat_rooms는 1 : N 관계로 구성하였다. 현재 DDL에서도 user_id가 users.id를 FK로 참조한다.

향후 확장성

대화방 제목이나 마지막 메시지 시각이 필요한 경우 title, last_message_at 등의 컬럼 추가를 검토할 수 있다. 특히 AI 대화 목록 화면이 추가되어 최근 대화방을 자주 조회하게 된다면 last_message_at을 별도로 관리하는 것이 조회 시마다 ai_messages에서 MAX(created_at)을 계산하는 것보다 효율적일 수 있다.

대화 원문 삭제 정책이 실제 운영에 적용되는 경우 purge_at을 기준으로 삭제 대상 대화방을 주기적으로 조회할 가능성이 있으므로 purge_at 인덱스 추가를 검토할 수 있다.

향후 AI 대화 상태가 ACTIVE, ANALYZING, COMPLETED 이외에도 FAILED, CANCELLED, EXPIRED 등으로 자주 확장되고 배포 과정에서 상태 변경이 빈번해진다면 DB ENUM 유지 여부를 재검토할 수 있다. 그 시점에는 VARCHAR(30) + 코드 Enum 방식이 운영 유연성 측면에서 더 적합할 수 있다.


24. ai_messages

항목 내용
한글 테이블명 AI 대화 메시지
설명 사용자와 AI가 대화방에서 주고받은 메시지를 저장하는 테이블
기능적 역할 AI 대화방에서 발생한 사용자 메시지와 AI 응답을 저장하고, 메시지 발신 주체와 생성 시점을 기준으로 대화 내역을 관리한다.
주요 관계 ai_chat_rooms 1 : N ai_messages
PK id
FK room_id → ai_chat_rooms.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
id BIGINT NOT NULL PK
room_id BIGINT NOT NULL FK → ai_chat_rooms.id
sender_type ENUM('USER', 'AI') NOT NULL -
content TEXT NOT NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
컬럼명 컬럼 설명 데이터 타입 선택 근거
id 메시지를 식별하는 고유 식별자 하나의 대화방에서 여러 메시지가 생성되고 각 메시지를 독립적으로 식별해야 하므로 대리키를 사용한다.
서비스 내 주요 식별자 타입과 통일하기 위해 BIGINT를 사용한다.
room_id 메시지가 포함된 AI 대화방의 식별자 하나의 대화방에 여러 메시지가 존재하는 1:N 관계를 표현하기 위해 ai_chat_rooms.id를 참조한다.
sender_type 메시지를 생성한 주체 메시지 발신 주체는 사용자와 AI 두 종류로 명확하게 제한되며 값의 변경 및 확장 가능성이 낮으므로 DB ENUM을 사용한다.
content 사용자 또는 AI가 전송한 메시지 내용 AI 응답은 메시지 길이가 가변적이고 비교적 긴 텍스트가 저장될 수 있으므로 고정된 길이를 지정하는 VARCHAR보다 TEXT를 사용한다.
created_at 메시지가 생성된 시각 대화 메시지의 생성 시점 기록 및 메시지 순서 판단에 사용하며 서비스 내 시간 타입과 통일하기 위해 DATETIME(6)을 사용한다.

설계 근거

하나의 ai_chat_rooms에 여러 개의 메시지가 생성될 수 있으므로 room_id를 FK로 두어 ai_chat_rooms와 1:N 관계로 구성하였다.

각 메시지는 독립적으로 식별될 필요가 있으므로 id 대리키를 기본키로 사용하였다. (room_id, created_at) 등의 조합을 통해 메시지를 구분할 수도 있지만 동일 시각에 여러 메시지가 생성될 가능성을 완전히 배제하기 어렵고, 특정 메시지 조회 및 향후 메시지 단위 기능을 추가할 때 단일 식별자를 사용하는 것이 유리하므로 별도의 id를 사용하였다.

별도의 sender_user_id를 두지 않은 이유는 AI 대화방의 사용자를 ai_chat_rooms를 통해 식별할 수 있고, sender_type = 'USER'인 경우 해당 대화방의 사용자가 발신자라는 관계가 명확하기 때문이다. 현재처럼 하나의 사용자와 AI가 대화하는 구조에서는 메시지마다 사용자 ID를 중복 저장할 필요성이 낮다.

향후 확장성

도대화 내 메시지가 증가할 경우 특정 대화방의 메시지를 시간순으로 조회하는 것이 주요 조회 패턴이 되므로 (room_id, created_at, id) 복합 인덱스를 검토할 수 있다. 특히 메시지 목록을 커서 기반 페이지네이션으로 조회하는 경우 created_at과 id를 함께 사용하면 동일한 생성 시각을 가진 메시지에 대해서 조회 순서를 결정할 수 있다.

AI 응답 생성 실패나 재생성 기능이 추가될 경우 status, error_code, regenerated_from_message_id 등의 정보를 추가하여 메시지 생성 상태 및 메시지 간 관계를 관리하는 구조로 확장할 수 있다.


25. taste_analyses

항목 내용
한글 테이블명 취향 분석
설명 AI 대화 내용을 기반으로 생성된 사용자의 취향 분석 결과와 분석 진행 상태를 저장하는 테이블
기능적 역할 AI 대화방에서 수행되는 취향 분석의 진행 상태, 임시 분석 결과 및 사용자의 분석 결과 승인 여부를 관리한다.
주요 관계 ai_chat_rooms 1 : 1 taste_analyses
PK room_id
FK room_id → ai_chat_rooms.id

컬럼 목록

컬럼명 데이터 타입 제약조건 / 기본값 PK/FK
room_id BIGINT NOT NULL PK, FK → ai_chat_rooms.id
status ENUM('QUEUED', 'RUNNING', 'SUCCEEDED', 'FAILED') NOT NULL; DEFAULT 'QUEUED' -
progress TINYINT NOT NULL; DEFAULT 0 -
progress_message VARCHAR(255) NULL -
candidate_result JSON NULL -
decision ENUM('PENDING', 'ACCEPTED', 'REJECTED') NOT NULL; DEFAULT 'PENDING' -
completed_at DATETIME(6) NULL -
decided_at DATETIME(6) NULL -
created_at DATETIME(6) NOT NULL; DEFAULT CURRENT_TIMESTAMP(6) -
updated_at DATETIME(6) NULL -
컬럼명 컬럼 설명 데이터 타입 선택 근거
room_id 취향 분석이 수행된 AI 대화방의 식별자 하나의 AI 대화방에는 하나의 취향 분석 결과만 존재하므로 별도의 대리키를 생성하지 않고 부모 테이블의 PK를 그대로 PK 및 FK로 사용하여 1:1 관계를 표현한다.
status 취향 분석 작업의 현재 진행 상태 분석 작업의 상태는 대기, 실행 중, 성공, 실패라는 제한된 상태 전이를 표현하며 운영 중 임의의 값이 추가되는 성격이 아니므로 DB ENUM으로 허용 값을 제한한다.
progress 취향 분석 진행률 분석 진행률을 0~100 수준의 작은 정수 값으로 표현하므로 TINYINT를 사용한다.
progress_message 현재 분석 진행 상태를 사용자에게 안내하기 위한 메시지 진행 단계에 따른 짧은 안내 문구를 저장하는 용도이므로 VARCHAR(255)를 사용하며 필요하지 않은 경우를 고려해 NULL을 허용한다.
candidate_result 사용자 승인 전 생성된 임시 취향 분석 결과 취향 분석 결과는 선호 항목, 비선호 항목, 관심사 등 여러 구조의 데이터를 포함할 수 있어 구조적 데이터를 하나의 결과로 저장할 수 있도록 JSON을 사용한다.
decision 생성된 취향 분석 결과에 대한 사용자의 결정 상태 사용자의 결정은 대기, 승인, 거절이라는 명확하고 제한된 상태만 가지며 값의 변경 가능성이 낮으므로 DB ENUM을 사용한다.
completed_at 취향 분석 작업이 완료된 시각 분석이 완료되기 전에는 완료 시각이 존재하지 않으므로 NULL을 허용하며 서비스 내 시간 컬럼과 통일하기 위해 DATETIME(6)을 사용한다.
decided_at 사용자가 분석 결과에 대해 승인 또는 거절을 결정한 시각 사용자의 결정이 이루어지기 전에는 값이 존재하지 않으므로 NULL을 허용한다.
created_at 취향 분석 데이터가 생성된 시각 분석 작업의 생성 시점을 기록하기 위해 사용하며 서비스의 시간 타입과 통일하기 위해 DATETIME(6)을 사용한다.
updated_at 취향 분석 데이터가 마지막으로 수정된 시각 분석 진행률, 상태, 분석 결과 및 사용자 결정 등이 변경된 시점을 기록하기 위해 사용한다.

설계 근거

taste_analyses는 AI 대화방의 대화 내용을 기반으로 수행되는 취향 분석 작업과 그 결과를 관리하는 테이블이다. 분석 작업의 진행 상태뿐 아니라 분석 완료 후 생성된 임시 결과와 사용자의 승인 여부까지 하나의 분석 생명주기로 관리한다.

하나의 ai_chat_rooms에 대해 하나의 취향 분석 결과만 존재하는 것이 현재 비즈니스 규칙이므로 room_id를 taste_analyses의 기본키이자 외래키로 사용하였다.

별도의 id 대리키를 두는 경우에도 하나의 대화방에 하나의 분석 결과만 존재한다는 규칙을 보장하기 위해 결국 room_id에 UNIQUE 제약조건이 추가로 필요하다. 또한 현재 taste_analyses 자체를 다른 테이블에서 독립적으로 참조해야 하는 요구사항이 없기 때문에 별도의 식별자를 추가했을 경우 이득이 크지 않다.

따라서 부모인 ai_chat_rooms.id를 자식의 PK로 그대로 사용하는 식별 관계로 설계하여 ai_chat_rooms와 taste_analyses의 관계를 1:1로 명확하게 표현하였다.

향후 확장성

현재는 하나의 AI 대화방에 하나의 취향 분석 결과만 존재한다는 비즈니스 규칙을 기준으로 room_id를 PK로 사용하고 있다.

향후 동일한 대화방에 대해 취향 분석을 여러 번 재수행하고 각각의 분석 결과를 이력으로 보존해야 하는 요구사항이 생긴다면 현재의 1:1 구조를 그대로 사용할 수 없다. 이 경우 taste_analyses.id와 같은 별도의 대리키를 추가하고 room_id를 일반 FK로 변경하여 ai_chat_rooms 1 : N taste_analyses 구조로 변경하는 것을 검토할 수 있다.

분석 재시도가 단순히 실패한 작업을 다시 실행하는 수준이고 이전 분석 결과를 별도의 이력으로 보존할 필요가 없다면 현재 레코드의 status, progress, candidate_result 등을 갱신하는 방식으로 현재 구조를 유지할 수도 있다.

candidate_result 내부 특정 값을 기준으로 검색이나 집계가 빈번하게 발생할 경우 JSON 내부 데이터 중 주요 조회 대상은 별도 컬럼 또는 정규화된 테이블로 분리하는 것을 검토할 수 있다.



API

  1. [API 인증 흐름](API 인증 흐름 링크)
  2. [API 응답 규격](응답 규격 링크)
  3. [에러 메세지 목록](에러 목록 링크)

배포 환경 및 CI/CD 파이프라인

관련 구성 및 파이프라인 설명 작성


코드 품질 관리 도구

예시

도구 역할 목적
.editorconfig
Checkstyle
JaCoCo
Java Test Fixtures
SonarQube

Clone this wiki locally