728x90
0. TL;DR
유저 프로필 시스템은 카프카 기반의 유저 데이터 반영, 다대일 관심사/장르 관계,
그리고 검색/추천/변경이력 추적을 전제로 enum 중심의 유연한 구조로 설계했다.
이 글에서는 "왜 그렇게 설계했는지", "대안은 무엇이었는지", "어떤 장점이 있는지"를 정리한다.
1. 유저 프로필 시스템이 필요한 이유
우리의 서비스는 음악 취향을 중심으로 사람을 매칭하는 플랫폼이다.
그렇다면 단순한 회원가입 정보만으로는 사람을 탐색하거나 추천하기 어렵다.
기본 정보만으로는 부족한 이유
| 항목 | 회원가입 정보 | 프로필에서 필요한 정보 |
|---|---|---|
| 닉네임 | ❌ 없음 | ✅ 있어야 탐색 가능 |
| 관심사 | ❌ 없음 | ✅ 드럼, 기타, React 등 |
| 장르 | ❌ 없음 | ✅ Jazz, Hip-hop, Rock 등 |
| 지역 | ❌ 없음 | ✅ 매칭을 위한 필수 조건 |
| 이미지 | ❌ 없음 | ✅ 이미지 있어야 프로필 이쁨 |
2. 설계 요구사항 정리
우리는 유저 프로필을 단순한 테이블이 아니라 다음과 같은 도메인 요구사항을 충족시켜야 했다:
도메인 요구
- Kafka 기반 유저 등록 이벤트 수신
- 필드별 수정 가능 및 변경 이력 저장
- 관심사/장르 다대일 관계
- 페이징/필터 기반 검색
- Soft Delete & Hard Delete 지원
- 부적절 닉네임 필터링 로직 포함
- 배치 처리 및 외부 분석 시스템 연동 고려
3. 전체 설계 구조
주요 테이블 구성
| 테이블 | 설명 |
|---|---|
user_profile |
유저의 핵심 정보 |
user_interest |
관심사 목록 (ex: React, 기타) |
user_genre |
선호 장르 목록 (ex: Jazz, Rock) |
user_profile_history |
변경 이력 (필드, 값, 시점 등 저장) |
설계 구조 흐름도
UserCreatedEvent (Kafka)
↓
+--------------------+
| UserProfile |
+--------------------+
| nickname |
| location |
| gender |
| deletedAt |
+--------------------+
↓ ↓
Interest Genre
(enum) (enum)
↓ ↓
user_interest user_genre4. 어떤 방식이 있었고, 왜 Enum을 선택했는가?
① 마스터 테이블 방식
| 구조 | interest나 genre를 별도 테이블로 만들어 FK로 참조 |
|---|---|
| 장점 | 동적으로 관리 가능, 어드민 추가 용이 |
| 단점 | 실제 서비스에서는 거의 고정됨, JOIN 성능 손해 있음 |
| 고려 | 어드민이 필드 추가/수정할 수 있는 SaaS가 아니라면 과한 일반화 |
② 태그 기반 문자열 저장
| 구조 | user_profile.interests = "jazz,rock" 처럼 문자열 저장 |
| 장점 | 간단하다 |
| 단점 | 인덱싱, 정규화, 조건 검색, 변경 이력 모두 비효율적 |
| 보완 | Lucene/Elasticsearch 같은 도구가 붙어야 쓸 수 있음 |
③ Enum 기반 설계 (우리가 선택한 방식)
public enum Genre {
JAZZ, ROCK, HIPHOP, EDM, CLASSIC
}
@Entity
public class UserGenre {
@Enumerated(EnumType.STRING)
private Genre genre;
}
| 장점 |
|---|
- 타입 안정성 (컴파일 시점 오류 방지)
- 단일 테이블, JOIN 최소화
- 프론트와 연동할 때 타입 명시적 (ex:
Genre.JAZZ) - 변경 이력 추적이 쉬움 (
Genre.valueOf(...)) - DB에서 문자열 비교가 간단함
| 단점 |
- enum 값 추가 시 배포 필요 (코드 변경)
- 어드민이 직접 관리하는 시스템에서는 불리함
우리는 고정된 도메인에 대한 빠르고 안정적인 조회를 원했기에 enum 방식이 압도적이었다.
5. 프로필 생성 흐름 (Kafka 기반)
@Transactional
public void createProfile(UserCreatedEvent event) {
if (userProfileRepository.existsById(event.getUserId())) return;
UserProfile profile = UserProfile.builder()
.userId(event.getUserId())
.nickname("user_" + event.getUserId().toString().substring(0, 6))
.email(event.getEmail())
.profileImageUrl(event.getProfileImageUrl())
.createdAt(LocalDateTime.now())
.build();
userProfileRepository.save(profile);
}
흐름 정리
회원 가입 시
→ Auth 모듈에서 Kafka 발행
→ Profile 서비스에서 수신 후 DB에 저장
→ 닉네임은 UUID 기반 임시 발급
6. 실전 설계 기준 요약
| 기준 | 우리가 고려한 점 | 선택한 이유 |
|---|---|---|
| 데이터 타입 | Enum vs 문자열 vs FK | Enum은 타입 안정성과 관리 효율 측면에서 우위 |
| 데이터 소스 | Kafka 기반 수신 | 이벤트 드리븐 시스템이므로 일관성 보장 필요 |
| 확장성 | 관심사/장르 필드 동적 확장 필요 없음 | 초기 MVP이므로 코드 중심 관리 우선 |
| 검색 최적화 | 조인 최소화, 단일 필드 검색 | Enum + 다대일 구조가 가장 효율적 |
| 변경 추적 | 변경 이력 저장 고려 | enum이 이력 로깅에 유리 |
7. 정리 및 다음 편 예고
이번 글에서는 유저 프로필의 도입 배경, 다양한 대안 설계, 우리가 선택한 enum 기반 구조의 장점과 이유를 모두 짚어봤다.
다음 글에서는 본격적인 프로필 생성/삭제 처리, 관심사/장르와의 관계 구성, 삭제 방식의 선택 기준(soft/hard) 등에 대해 다룬다.
다음 글 예고:
2편. "프로필 생성/삭제 흐름과 관심사/장르 연관 구조 설계"
- Kafka 수신 기반 생성 처리
- soft delete vs hard delete 선택과 구현
- 연관 관심사/장르 테이블 설계와 삭제 처리
- 프로필 초기화 전략
728x90
'BindProject' 카테고리의 다른 글
| [Backend] 유저 프로필 모듈 구현 3편. 유저 프로필 변경 이력 추적 설계와 구현 (0) | 2025.06.22 |
|---|---|
| [Backend] 유저 프로필 모듈 구현 2편. 유저 프로필 도입과 설계 구조: 유저 프로필 생성/삭제 처리와 관심사/장르 연관 구조 설계 (1) | 2025.06.22 |
| [Backend] 유저 활동 로그 수집 모듈 개발기 5편 : 마무리 회고 (0) | 2025.06.22 |
| [Backend] 유저 활동 로그 수집 모듈 개발기 4편 : 로그 수집 서비스 구현 (0) | 2025.06.22 |
| [Backend] 유저 활동 로그 수집 모듈 개발기 3편 : 로그 메타데이터 구현 (0) | 2025.06.22 |