Skip to content

DDINGJOO/Profile_Server_2025VERSION

Repository files navigation

Profile-Server 가이드 문서

1. 개요

1.1 목적

Profile-Server는 플랫폼 내 사용자 프로필의 생명주기를 전담 관리하는 마이크로서비스이다. 음악 커뮤니티 플랫폼에서 사용자의 장르, 악기 선호도 등 프로필 정보를 관리한다.

1.2 주요 기능

기능 설명
프로필 생성 Kafka 이벤트 수신 시 자동 프로필 초기화
프로필 조회 단일/배치/검색 조회 지원
프로필 수정 닉네임, 소개, 장르, 악기 등 정보 변경
복합 검색 지역, 장르, 악기, 성별 기반 다차원 검색
속성 관리 장르/악기 다중 선택 관리 (최대 3개)
이력 추적 프로필 변경 이력 자동 기록
닉네임 검증 유일성 및 형식 유효성 검증

1.3 기술 스택

구분 기술
Framework Spring Boot 3.5.5
Language Java 21 (Eclipse Temurin)
Database MariaDB 10.6+
Query QueryDSL 5.0.0
Message Broker Apache Kafka
Cache Redis (설정됨, 비활성)
Documentation Swagger/OpenAPI 3.0

2. 시스템 아키텍처

2.1 전체 구조

flowchart TB
    subgraph Client
        APP[Mobile App]
        WEB[Web Client]
    end

    subgraph API_Gateway
        GW[API Gateway]
    end

    subgraph Profile_Service
        PROFILE[Profile Server]
        MARIA[(MariaDB)]
        REDIS[(Redis)]
    end

    subgraph Messaging
        KAFKA[Kafka Cluster]
    end

    subgraph Upstream_Services
        AUTH[Auth Server]
        IMAGE[Image Server]
    end

    subgraph Downstream_Services
        CHAT[Chat Server]
        NOTI[Notification Server]
    end

    APP --> GW
    WEB --> GW
    GW --> PROFILE
    PROFILE --> MARIA
    PROFILE --> REDIS
    PROFILE --> KAFKA
    AUTH -->|user-created| KAFKA
    IMAGE -->|profile-image-changed| KAFKA
    KAFKA -->|user-nickname-changed| CHAT
    KAFKA -->|user-nickname-changed| NOTI
Loading

2.2 레이어 아키텍처 (CQRS 패턴)

flowchart TB
    subgraph Presentation["Presentation Layer"]
        SEARCH_CTRL[ProfileSearchController]
        UPDATE_CTRL[ProfileUpdateController]
        ENUM_CTRL[EnumsController]
        VALID_CTRL[NicknameValidator]
    end

    subgraph Application["Application Layer (CQRS)"]
        subgraph Command["Command Side"]
            CREATE_SVC[UserInfoLifeCycleService]
            UPDATE_SVC[ProfileUpdateService]
        end
        subgraph Query["Query Side"]
            SEARCH_SVC[ProfileSearchService]
        end
    end

    subgraph Domain["Domain Layer"]
        USER[UserInfo - Aggregate Root]
        HIST[History]
        GENRE[UserGenres]
        INST[UserInstruments]
    end

    subgraph Infrastructure["Infrastructure Layer"]
        REPO[JPA Repositories]
        DSL[QueryDSL Implementation]
        KAFKA_PUB[EventPublisher]
        KAFKA_CON[EventConsumer]
    end

    SEARCH_CTRL --> SEARCH_SVC
    UPDATE_CTRL --> UPDATE_SVC
    SEARCH_SVC --> DSL
    UPDATE_SVC --> REPO
    CREATE_SVC --> REPO
    KAFKA_CON --> CREATE_SVC
    UPDATE_SVC --> KAFKA_PUB
Loading

2.3 프로필 생성 흐름

sequenceDiagram
    participant AUTH as Auth Server
    participant K as Kafka
    participant PS as Profile Server
    participant DB as MariaDB

    AUTH ->> K: user-created 이벤트 발행
    K ->> PS: 이벤트 수신
    PS ->> PS: ProfileCreateRequest 파싱
    PS ->> PS: 닉네임 자동 생성
    PS ->> DB: UserInfo 저장
    PS ->> DB: 기본 속성 초기화
    PS -->> K: 프로필 생성 완료
Loading

2.4 프로필 검색 흐름

sequenceDiagram
    participant C as Client
    participant GW as API Gateway
    participant PS as Profile Server
    participant DB as MariaDB
    participant CACHE as In-Memory Cache

    C ->> GW: GET /api/v1/profiles?city=SEOUL&genres=1,2
    GW ->> PS: 검색 요청 전달
    PS ->> CACHE: 참조 데이터 조회 (장르, 악기, 지역)
    CACHE -->> PS: 캐시된 데이터 반환
    PS ->> DB: QueryDSL 동적 쿼리 실행
    DB -->> PS: 검색 결과 반환
    PS ->> PS: UserResponse 변환
    PS -->> GW: Slice<UserResponse>
    GW -->> C: 검색 결과 (커서 기반 페이징)
Loading

2.5 프로필 수정 흐름

flowchart TD
    A[프로필 수정 요청] --> B{입력값 검증}
    B -->|실패| Z1[400 Bad Request]
    B -->|성공| C{사용자 존재 확인}
    C -->|없음| Z2[404 Not Found]
    C -->|존재| D{닉네임 변경?}
    D -->|Yes| E{닉네임 중복 확인}
    E -->|중복| Z3[409 Conflict]
    E -->|유일| F[프로필 필드 업데이트]
    D -->|No| F
    F --> G[속성 업데이트 - 장르/악기]
    G --> H[변경 이력 저장]
    H --> I{닉네임 변경됨?}
    I -->|Yes| J[Kafka 이벤트 발행]
    I -->|No| K[수정 완료 응답]
    J --> K
Loading

2.6 배치 조회 흐름

sequenceDiagram
    participant C as Client
    participant PS as Profile Server
    participant DB as MariaDB

    C ->> PS: POST /api/v1/profiles/batch
    Note over C,PS: userIds: ["id1", "id2", "id3"]
    PS ->> DB: IN 쿼리로 일괄 조회
    DB -->> PS: List<UserInfo>
    PS ->> DB: 배치 초기화 - 장르 컬렉션
    PS ->> DB: 배치 초기화 - 악기 컬렉션
    Note over PS: N+1 문제 방지
    PS ->> PS: 응답 변환
    alt detail=true
        PS -->> C: List<UserResponse>
    else detail=false
        PS -->> C: List<BatchUserSummaryResponse>
    end
Loading

3. 데이터 모델

3.1 ERD

erDiagram
    UserInfo ||--o{ UserGenres : has
    UserInfo ||--o{ UserInstruments : has
    UserInfo ||--o{ History : tracks
    UserGenres }o--|| GenreNameTable : references
    UserInstruments }o--|| InstrumentNameTable : references
    UserInfo }o--o| LocationNameTable : references

    UserInfo {
        string userId PK "Snowflake ID"
        string nickname UK "2-15자"
        string profileImageUrl
        char sex "M/F/O"
        string city FK
        string introduction
        boolean isPublic
        boolean isChatable
        int version "낙관적 락"
        datetime createdAt
        datetime updatedAt
    }

    UserGenres {
        string userId PK_FK
        int genreId PK_FK
        int version
    }

    UserInstruments {
        string userId PK_FK
        int instrumentId PK_FK
        int version
    }

    History {
        string historyId PK
        string userId FK
        string fieldName
        string oldVal
        string newVal
        datetime updatedAt
    }

    GenreNameTable {
        int genreId PK
        string genreName
        int version
    }

    InstrumentNameTable {
        int instrumentId PK
        string instrumentName
        int version
    }

    LocationNameTable {
        string cityId PK
        string cityName
    }
Loading

3.2 테이블 상세

UserInfo (사용자 프로필)

필드 타입 필수 설명
user_id VARCHAR(255) Y Snowflake ID (PK)
nickname VARCHAR(50) Y 닉네임 (UNIQUE, 2-15자)
profile_image_url VARCHAR(500) N 프로필 이미지 URL
sex CHAR(1) N 성별 (M/F/O)
city VARCHAR(50) N 지역 코드
introduction VARCHAR(500) N 자기 소개
is_public BOOLEAN Y 프로필 공개 여부 (기본: true)
is_chatable BOOLEAN Y 채팅 가능 여부 (기본: true)
version INT Y Optimistic Lock
created_at DATETIME(6) Y 생성 시간
last_updated_at DATETIME(6) N 수정 시간

GenreNameTable (장르 마스터)

필드 타입 필수 설명
genre_id INT Y 장르 ID (PK)
genre_name VARCHAR(50) Y 장르명
version INT N 버전

초기 데이터 예시:

ID 장르명
1 ROCK
2 JAZZ
3 CLASSICAL
4 POP
5 BLUES

InstrumentNameTable (악기 마스터)

필드 타입 필수 설명
instrument_id INT Y 악기 ID (PK)
instrument_name VARCHAR(50) Y 악기명
version INT N 버전

초기 데이터 예시:

ID 악기명
1 GUITAR
2 PIANO
3 DRUMS
4 BASS
5 VIOLIN

UserGenres (사용자-장르 연결)

필드 타입 필수 설명
user_id VARCHAR(255) Y FK to UserInfo (복합 PK)
genre_id INT Y FK to GenreNameTable (복합 PK)
version INT N Optimistic Lock

UserInstruments (사용자-악기 연결)

필드 타입 필수 설명
user_id VARCHAR(255) Y FK to UserInfo (복합 PK)
instrument_id INT Y FK to InstrumentNameTable (복합 PK)
version INT N Optimistic Lock

History (변경 이력)

필드 타입 필수 설명
history_id VARCHAR(255) Y Snowflake ID (PK)
user_id VARCHAR(255) Y FK to UserInfo
field_name VARCHAR(50) Y 변경된 필드명
old_val VARCHAR(500) N 변경 전 값
new_val VARCHAR(500) Y 변경 후 값
updated_at DATETIME(6) Y 변경 시각

4. API 명세

4.1 프로필 조회

단일 프로필 조회

GET /api/v1/profiles/{userId}

Response

{
  "userId": "1234567890123456789",
  "nickname": "musiclover",
  "profileImageUrl": "https://cdn.example.com/profiles/user123.jpg",
  "city": "SEOUL",
  "sex": "M",
  "introduction": "기타리스트, 재즈와 블루스를 좋아합니다",
  "genres": [
    {"id": 2, "name": "JAZZ"},
    {"id": 5, "name": "BLUES"}
  ],
  "instruments": [
    {"id": 1, "name": "GUITAR"},
    {"id": 2, "name": "PIANO"}
  ],
  "isPublic": true,
  "isChatable": true,
  "createdAt": "2025-01-01T10:00:00Z",
  "updatedAt": "2025-01-20T15:30:00Z"
}

상태 코드

코드 설명
200 조회 성공
404 사용자를 찾을 수 없음

프로필 검색

GET /api/v1/profiles

Query Parameters

파라미터 타입 필수 기본값 설명
city String N null 지역 코드 필터
nickName String N null 닉네임 검색 (부분 일치)
genres List N null 장르 ID 목록
instruments List N null 악기 ID 목록
sex String N null 성별 필터 (M/F/O)
cursor String N null 페이징 커서 (userId)
size Integer N 10 페이지 크기 (최대 100)

Response

{
  "content": [
    {
      "userId": "1234567890123456789",
      "nickname": "musiclover",
      "profileImageUrl": "https://cdn.example.com/...",
      "city": "SEOUL",
      "genres": ["JAZZ", "BLUES"],
      "instruments": ["GUITAR"]
    }
  ],
  "hasNext": true,
  "nextCursor": "1234567890123456788"
}

배치 프로필 조회

POST /api/v1/profiles/batch

Query Parameters

파라미터 타입 필수 기본값 설명
detail Boolean N false 상세 정보 포함 여부

Request Body

["userId1", "userId2", "userId3"]

Response (detail=true)

[
  {
    "userId": "userId1",
    "nickname": "user1",
    "profileImageUrl": "...",
    "genres": [...],
    "instruments": [...]
  }
]

Response (detail=false)

[
  {
    "userId": "userId1",
    "nickname": "user1",
    "profileImageUrl": "..."
  }
]

4.2 프로필 수정

프로필 정보 수정

PUT /api/v1/profiles/{userId}

Request

필드 타입 필수 설명
nickname String N 새 닉네임 (2-15자, 영문/한글/숫자/_)
introduction String N 자기 소개
city String N 지역 코드
sex String N 성별 (M/F/O)
genres List N 장르 ID 목록 (최대 3개)
instruments List N 악기 ID 목록 (최대 3개)
isPublic Boolean N 프로필 공개 여부
isChatable Boolean N 채팅 가능 여부

Request Example

{
  "nickname": "newNickname",
  "introduction": "안녕하세요, 음악을 사랑합니다",
  "city": "BUSAN",
  "genres": [1, 3, 5],
  "instruments": [2, 4],
  "isPublic": true,
  "isChatable": false
}

Response

{
  "success": true
}

상태 코드

코드 설명
200 수정 성공
400 유효성 검증 실패
404 사용자를 찾을 수 없음
409 닉네임 중복

4.3 참조 데이터 조회

장르 목록 조회

GET /api/v1/profiles/genres

Response

{
  "1": "ROCK",
  "2": "JAZZ",
  "3": "CLASSICAL",
  "4": "POP",
  "5": "BLUES"
}

악기 목록 조회

GET /api/v1/profiles/instruments

Response

{
  "1": "GUITAR",
  "2": "PIANO",
  "3": "DRUMS",
  "4": "BASS",
  "5": "VIOLIN"
}

지역 목록 조회

GET /api/v1/profiles/locations

Response

{
  "SEOUL": "서울",
  "BUSAN": "부산",
  "INCHEON": "인천",
  "DAEGU": "대구"
}

4.4 닉네임 검증

닉네임 유효성 검사

GET /api/v1/profiles/validate

Query Parameters

파라미터 타입 필수 설명
type String Y "nickname"
value String Y 검증할 닉네임

Response

{
  "valid": true
}

검증 규칙

  • 길이: 2-15자
  • 허용 문자: 영문, 한글, 숫자, 언더스코어(_)
  • 정규식: ^[a-zA-Z0-9가-힣_]{2,15}$
  • 중복 불가

4.5 헬스 체크

GET /health

Response

OK

5. 이벤트 명세

5.1 Kafka Topics

Topic Producer Consumer 설명
user-created Auth Server Profile Server 회원가입 완료, 프로필 생성 트리거
profile-image-changed Image Server Profile Server 프로필 이미지 변경
user-deleted Auth Server Profile Server 회원 탈퇴, 프로필 삭제
user-nickname-changed Profile Server Chat, Notification 닉네임 변경 브로드캐스트

5.2 Consumer Group

  • Group ID: profile-consumer-group
  • Auto Offset Reset: earliest

5.3 이벤트 페이로드

user-created (수신)

{
  "eventId": "evt-uuid-1234",
  "eventType": "USER_CREATED",
  "timestamp": "2025-01-15T10:00:00Z",
  "payload": {
    "userId": "1234567890123456789",
    "provider": "SYSTEM"
  }
}

profile-image-changed (수신)

{
  "eventId": "evt-uuid-2345",
  "eventType": "PROFILE_IMAGE_CHANGED",
  "timestamp": "2025-01-15T11:00:00Z",
  "payload": {
    "userId": "1234567890123456789",
    "imageUrl": "https://cdn.example.com/new-image.jpg"
  }
}

user-nickname-changed (발행)

{
  "eventId": "evt-uuid-3456",
  "eventType": "USER_NICKNAME_CHANGED",
  "timestamp": "2025-01-15T12:00:00Z",
  "payload": {
    "userId": "1234567890123456789",
    "oldNickname": "oldName",
    "newNickname": "newName"
  }
}

user-deleted (수신)

{
  "eventId": "evt-uuid-4567",
  "eventType": "USER_DELETED",
  "timestamp": "2025-01-15T13:00:00Z",
  "payload": {
    "userId": "1234567890123456789"
  }
}

6. 비즈니스 규칙

6.1 닉네임 규칙

규칙 설명
길이 2자 이상 15자 이하
허용 문자 영문(대소문자), 한글, 숫자, 언더스코어(_)
유일성 전체 시스템에서 유일해야 함
자동 생성 프로필 생성 시 임의 닉네임 자동 부여

6.2 속성 규칙

규칙 장르 악기
최대 개수 3개 3개
최소 개수 0개 0개
중복 선택 불가 불가
유효성 ID가 마스터 테이블에 존재해야 함 ID가 마스터 테이블에 존재해야 함

6.3 프로필 공개 설정

설정 기본값 설명
isPublic true false면 검색 결과에서 제외
isChatable true false면 채팅 요청 불가

6.4 프로필 생성 정책

규칙 설명
트리거 Auth Server의 user-created 이벤트
닉네임 자동 생성 (user_ + 랜덤)
기본값 isPublic=true, isChatable=true
장르/악기 빈 목록으로 초기화

7. 캐싱

7.1 인메모리 캐시 아키텍처

flowchart LR
    subgraph Application
        SEARCH[ProfileSearchService]
        UPDATE[ProfileUpdateService]
        INIT[InitTableMapper]
    end

    subgraph In_Memory_Cache
        GENRE_CACHE[GenreCache]
        INST_CACHE[InstrumentCache]
        LOC_CACHE[LocationCache]
    end

    subgraph Database
        DB[(MariaDB)]
    end

    INIT -->|@PostConstruct| DB
    DB -->|Load| GENRE_CACHE
    DB -->|Load| INST_CACHE
    DB -->|Load| LOC_CACHE
    SEARCH -->|Read| GENRE_CACHE
    SEARCH -->|Read| INST_CACHE
    UPDATE -->|Validate| GENRE_CACHE
    UPDATE -->|Validate| INST_CACHE
Loading

7.2 캐시 갱신 정책

캐시 초기화 시점 갱신 주기
GenreCache 애플리케이션 시작 매일 06:00
InstrumentCache 애플리케이션 시작 매일 06:00
LocationCache 애플리케이션 시작 매일 06:00

7.3 Redis 설정 (비활성)

  • 설정됨: spring.data.redis.repositories.enabled: false
  • 호스트/포트: 환경변수로 설정 가능
  • 용도: 향후 쿼리 결과 캐싱용으로 예약

8. 인덱스 설계

8.1 MariaDB 인덱스

UserInfo 테이블

-- 닉네임 유일성 및 검색
CREATE UNIQUE INDEX idx_user_nickname ON user_info (nickname);

-- 지역 기반 검색
CREATE INDEX idx_user_city ON user_info (city);

-- 공개 프로필 + 지역 복합 검색
CREATE INDEX idx_user_public_city ON user_info (is_public, city);

-- 생성일 기준 정렬
CREATE INDEX idx_user_created ON user_info (created_at DESC);

UserGenres 테이블

-- 사용자별 장르 조회
CREATE INDEX idx_genres_user ON user_genres (user_id);

-- 복합 키 인덱스
CREATE INDEX idx_genres_composite ON user_genres (user_id, genre_id);

UserInstruments 테이블

-- 사용자별 악기 조회
CREATE INDEX idx_instruments_user ON user_instruments (user_id);

-- 복합 키 인덱스
CREATE INDEX idx_instruments_composite ON user_instruments (user_id, instrument_id);

History 테이블

-- 사용자별 이력 조회 (최신순)
CREATE INDEX idx_history_user_date ON profile_update_history (user_id, updated_at DESC);

9. 에러 코드

9.1 프로필 에러

코드 HTTP Status 설명
PROFILE_001 409 닉네임이 이미 존재함
PROFILE_002 500 이력 업데이트 실패
PROFILE_003 400 장르 개수 초과 (최대 3개)
PROFILE_004 400 장르 ID와 이름 동시 지정 불가
PROFILE_005 400 악기 개수 초과 (최대 3개)
PROFILE_006 400 악기 ID와 이름 동시 지정 불가
PROFILE_007 404 사용자를 찾을 수 없음
PROFILE_008 400 닉네임 형식 오류
PROFILE_009 400 장르 ID 또는 이름이 유효하지 않음
PROFILE_010 400 지역 ID와 이름 동시 지정 불가

9.2 에러 응답 형식

{
  "timestamp": "2025-01-15T10:30:00Z",
  "status": 400,
  "code": "PROFILE_003",
  "message": "장르는 최대 3개까지 선택 가능합니다",
  "path": "/api/v1/profiles/1234567890123456789"
}

10. 환경 설정

10.1 환경 변수

# Database
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_NAME=profiles
DATABASE_USER_NAME=profile_user
DATABASE_PASSWORD=your_password

# Kafka
KAFKA_URL1=localhost:9092
KAFKA_URL2=localhost:9093
KAFKA_URL3=localhost:9094

# Redis (선택)
REDIS_HOST=localhost
REDIS_PORT=6379

# Spring Profile
SPRING_PROFILES_ACTIVE=dev

10.2 application-dev.yaml

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mariadb://${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}
    username: ${DATABASE_USER_NAME}
    password: ${DATABASE_PASSWORD}
    driver-class-name: org.mariadb.jdbc.Driver

  jpa:
    hibernate:
      ddl-auto: none
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MariaDBDialect

  kafka:
    bootstrap-servers:
      - ${KAFKA_URL1}
      - ${KAFKA_URL2}
      - ${KAFKA_URL3}
    producer:
      retries: 3
      batch-size: 16384
    consumer:
      group-id: profile-consumer-group
      auto-offset-reset: earliest
    listener:
      ack-mode: record

  data:
    redis:
      host: ${REDIS_HOST}
      port: ${REDIS_PORT}
      repositories:
        enabled: false

  sql:
    init:
      mode: always

validation:
  nickname:
    regex: "^[a-zA-Z0-9가-힣_]{2,15}$"

attribute:
  genre:
    max-size: 3
  instrument:
    max-size: 3

10.3 Docker 배포

Dockerfile

FROM eclipse-temurin:21-jre-jammy

WORKDIR /app

COPY build/libs/*.jar /app/app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Docker Compose

version: '3.8'

services:
  profile-server:
    image: profile-server:latest
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - DATABASE_HOST=mariadb
      - DATABASE_PORT=3306
      - DATABASE_NAME=profiles
      - DATABASE_USER_NAME=${DB_USER}
      - DATABASE_PASSWORD=${DB_PASSWORD}
      - KAFKA_URL1=kafka:9092
    depends_on:
      - mariadb
      - kafka

  mariadb:
    image: mariadb:10.6
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: profiles
      MYSQL_USER: ${DB_USER}
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes:
      - mariadb_data:/var/lib/mysql

volumes:
  mariadb_data:

11. 성능 최적화

11.1 N+1 문제 해결

flowchart LR
    subgraph Before["Before - N+1 Problem"]
        Q1[1. UserInfo 조회]
        Q2[2. UserGenres 조회 x N]
        Q3[3. UserInstruments 조회 x N]
    end

    subgraph After["After - Batch Initialization"]
        B1[1. UserInfo 조회]
        B2[2. UserGenres IN 쿼리]
        B3[3. UserInstruments IN 쿼리]
    end

    Before --> |"N+1 쿼리"| SLOW[느림]
    After --> |"3개 쿼리"| FAST[빠름]
Loading

11.2 커서 기반 페이징

방식 Offset 기반 Cursor 기반
성능 O(n) - 데이터 증가시 느려짐 O(1) - 일정한 성능
일관성 삽입/삭제 시 데이터 누락 가능 일관된 결과 보장
사용 작은 데이터셋 대용량 데이터셋

11.3 인메모리 캐시

  • 참조 데이터(장르, 악기, 지역)를 애플리케이션 시작 시 메모리에 로드
  • 매일 06:00에 자동 갱신 (@Scheduled)
  • DB 조회 없이 빠른 유효성 검증 가능

12. 구현 우선순위

Phase 1 - 핵심 기능 (완료)

  • 프로필 CRUD
  • Kafka 이벤트 수신 (user-created)
  • 닉네임 유효성 검증
  • 장르/악기 속성 관리

Phase 2 - 검색 기능 (완료)

  • QueryDSL 기반 복합 검색
  • 커서 기반 페이징
  • 배치 조회 API

Phase 3 - 이벤트 발행 (완료)

  • 닉네임 변경 이벤트 발행
  • 프로필 이미지 변경 수신

Phase 4 - 고도화 (진행 중)

  • Redis 쿼리 결과 캐싱
  • 프로필 삭제 이벤트 처리
  • 성능 모니터링

13. 참고 사항

13.1 QueryDSL 동적 쿼리

복합 검색 조건을 타입 안전하게 처리:

BooleanBuilder builder = new BooleanBuilder();

if (criteria.getCity() != null) {
    builder.and(userInfo.city.eq(criteria.getCity()));
}
if (criteria.getGenres() != null && !criteria.getGenres().isEmpty()) {
    builder.and(userInfo.userId.in(
        JPAExpressions.select(userGenres.id.userId)
            .from(userGenres)
            .where(userGenres.id.genreId.in(criteria.getGenres()))
    ));
}

13.2 Optimistic Locking

동시 수정 충돌 방지:

@Version
private int version;

// 충돌 시 OptimisticLockException 발생
// 클라이언트는 재시도 필요

13.3 배치 컬렉션 초기화

N+1 문제 해결을 위한 별도 쿼리 실행:

private void batchInitializeCollections(List<UserInfo> users) {
    Set<String> userIds = users.stream()
        .map(UserInfo::getUserId)
        .collect(Collectors.toSet());

    // 장르 일괄 조회
    List<UserGenres> genres = queryFactory
        .selectFrom(userGenres)
        .where(userGenres.id.userId.in(userIds))
        .fetch();

    // 악기 일괄 조회
    List<UserInstruments> instruments = queryFactory
        .selectFrom(userInstruments)
        .where(userInstruments.id.userId.in(userIds))
        .fetch();

    // 매핑
    Map<String, List<UserGenres>> genreMap = genres.stream()
        .collect(Collectors.groupingBy(g -> g.getId().getUserId()));
    // ...
}

14. API 문서

Swagger UI

  • 개발: http://localhost:8080/swagger-ui.html

OpenAPI JSON

  • http://localhost:8080/v3/api-docs

버전: 1.0.0 최종 업데이트: 2025-01-25 : Teambind Backend Development Team

About

프로필 서비스 2025버전

Resources

Stars

Watchers

Forks

Packages

No packages published