BindProject

[Backend] 유저 프로필 모듈 구현 2편. 유저 프로필 도입과 설계 구조: 유저 프로필 생성/삭제 처리와 관심사/장르 연관 구조 설계

dding-shark 2025. 6. 22. 20:58
728x90

1. 도입: 유저 생성은 단순하지 않다

우리 서비스는 회원가입과 동시에 유저 프로필이 생성되어야 한다.
그런데 이 프로필은 Auth 모듈에서 만들어지는 게 아니라, Kafka 이벤트를 수신해서 생성된다.

또한 프로필 삭제에도 두 가지 방식이 필요했다:

  • Soft Delete → 유저가 탈퇴했지만 이력은 남겨야 할 때
  • Hard Delete → 유저 데이터를 완전히 삭제해야 할 때 (예: 개인정보 삭제 요청)

2. 유저 프로필 생성 흐름 (Kafka 기반)

흐름도

회원가입 완료
 → Kafka: UserCreatedEvent 발행
   → user-profile-service: 이벤트 수신
     → UserProfile 생성
       → 관심사/장르 기본 세팅 (optional)

이벤트 예시: UserCreatedEvent

public class UserCreatedEvent {
    private Long userId;
    private String email;
    private String profileImageUrl;
}

수신 후 생성 처리

@Transactional
public void createProfile(UserCreatedEvent event) {
    if (userProfileRepository.existsById(event.getUserId())) return;

    UserProfile profile = UserProfile.builder()
        .userId(event.getUserId())
        .nickname("user_" + UUID.randomUUID().toString().substring(0, 6))
        .email(event.getEmail())
        .profileImageUrl(event.getProfileImageUrl())
        .createdAt(LocalDateTime.now())
        .build();

    userProfileRepository.save(profile);
}

 참고: nickname은 최초 생성 시 임시 UUID 기반 자동 발급


3. 삭제 정책: Soft vs Hard

Soft Delete가 필요한 이유

  • 유저가 일시적으로 탈퇴한 경우
  • 신고/정지에 따른 비공개 처리
  • 변경 이력이나 활동 기록을 보존하고 싶을 때
@Transactional
public void softDelete(Long userId) {
    UserProfile profile = findActiveProfile(userId);
    if (profile.isDeleted()) throw new UserProfileException(ALREADY_DELETED);

    profile.setDeletedAt(LocalDateTime.now());
}

Hard Delete가 필요한 이유

  • 개인정보 삭제 요청 (GDPR/ISMS 대응)
  • 테스트 계정 등 완전한 제거 필요
  • 연관 테이블까지 완전 삭제 필요
@Transactional
public void hardDelete(Long userId) {
    userInterestRepository.deleteByUserId(userId);
    userGenreRepository.deleteByUserId(userId);
    userProfileRepository.deleteById(userId);
}

4. 관심사 / 장르 구조 설계

연관 구조 흐름

UserProfile
   ├── UserInterest (관심사 enum)
   └── UserGenre (장르 enum)

각 유저는 다수의 관심사와 장르를 가질 수 있다.
이를 enum 기반으로 설계함으로써 고정 도메인에 대한 빠른 필터링과 조회를 가능하게 했다.


UserInterest

@Entity
public class UserInterest {
    @Id
    @GeneratedValue
    private Long id;

    private Long userId;

    @Enumerated(EnumType.STRING)
    private Interest interest; // ex: DRUM, VOCAL, REACT

    private LocalDateTime createdAt;
}

UserGenreGenre enum만 바꿔서 동일하게 구성


5. 삭제 시 연관 데이터도 함께 처리

Hard delete를 수행할 경우에는 반드시 user_interest, user_genre도 함께 삭제되어야 한다.

이때, 두 가지 방식 중 하나를 선택해야 한다:

① Cascade 설정 (JPA 연관 자동 삭제)

@OneToMany(mappedBy = "userId", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserInterest> interests;

장점: 자동 처리
단점: mappedBy가 Long userId라면 OneToMany 매핑 불가 → 객체 참조로 바꿔야 함


② 명시적 삭제 처리 (우리가 선택한 방식)

@Transactional
public void hardDelete(Long userId) {
    userInterestRepository.deleteByUserId(userId);
    userGenreRepository.deleteByUserId(userId);
    userProfileRepository.deleteById(userId);
}

장점: 쿼리 명확, 실제 삭제되는 대상 예측 가능
단점: 중복 삭제 로직 필요


6. 실무 기준 삭제 정책 비교표

항목 Soft Delete Hard Delete
이력 보존 적합 불가능
GDPR 대응 불가능 적합
테스트 계정 삭제 불가능 삭제됨
관심사/장르 삭제 처리 불필요 명시적 처리 필요

우리는 유저 요청/운영 정책에 따라 둘 다 제공하기로 했다.


7. 정리 및 다음 편 예고

이번 글에서는 유저 프로필의 생성/삭제 흐름과 연관 테이블 관리 방식을 다뤘다.
Kafka 기반 이벤트 처리, soft/hard delete 정책 차이, 관심사/장르 연관 처리까지
실전 서비스 수준의 설계/구현 기준으로 정리했다.


다음 편 예고:

3편. "프로필 수정 기능과 변경 이력 추적 전략"

  • 어떤 필드를 수정 가능하게 할 것인가
  • UpdatableProfileColumn enum 설계
  • 변경 이력(UserProfileHistory) 저장 전략
  • 감사(Audit) 기반 설계 고민

 

 


 

728x90