BindProject

[Backend] 유저 프로필 모듈 구현 1편. 유저 프로필 도입과 설계 구조: Enum 기반 프로필 시스템의 선택과 대안 비교

dding-shark 2025. 6. 22. 20:55
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_genre

4. 어떤 방식이 있었고, 왜 Enum을 선택했는가?

① 마스터 테이블 방식

구조 interestgenre를 별도 테이블로 만들어 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