BindProject

[Backend] 유저 프로필 모듈 구현 3편. 유저 프로필 변경 이력 추적 설계와 구현

dding-shark 2025. 6. 22. 21:00
728x90

-

1. 왜 변경 이력이 필요한가?

실무 시나리오 예시

  • 유저가 닉네임을 공격적인 단어로 바꾸고 신고당함
    이전 닉네임을 알아야 조치 가능
  • 성별/지역 정보를 바꾸고 반복적으로 다른 유저를 속이는 경우
    수정 이력 기반으로 사용자 패턴 확인
  • 운영자가 사용자 지원을 위해 변경 기록을 조회해야 할 때

2. 핵심 설계 구조

필드 추적 기준은 enum으로 정의

우리는 수정 가능한 필드를 코드 상에서 명확하게 열거(enum) 하기로 했다.
이 방식은 추적 가능성과 가독성을 높여주며, 이후 검색 기능에도 도움이 된다.

public enum UpdatableProfileColumn {
    NICKNAME("닉네임"),
    INTRODUCTION("소개글"),
    PROFILE_IMAGE("프로필 이미지"),
    LOCATION("지역"),
    GENDER("성별"),
    PHONE_NUMBER("전화번호"),
    EMAIL("이메일");

    private final String displayName;
    // ...생략
}

변경 이력 엔티티 설계

@Entity
@Table(name = "user_profile_history")
public class UserProfileHistory {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long userId;

    @Enumerated(EnumType.STRING)
    private UpdatableProfileColumn fieldChanged;

    private String oldValue;
    private String newValue;

    private LocalDateTime changedAt;

    private String changedBy; // "USER" or "SYSTEM"

    public static UserProfileHistory of(Long userId, UpdatableProfileColumn column, String oldValue, String newValue, String by) {
        return new UserProfileHistory(null, userId, column, oldValue, newValue, LocalDateTime.now(), by);
    }
}

3. 서비스 구현 흐름

구조 개요

유저가 수정 요청
 → 변경 필드 비교
   → 기존값 ≠ 신규값 → 변경 이력 저장
   → 프로필 엔티티 반영

서비스 코드 (핵심 발췌)

@Transactional
public void updateProfile(Long userId, UserProfileUpdateRequest request) {
    UserProfile profile = findActiveProfile(userId);

    if (request.getNickname() != null && !request.getNickname().equals(profile.getNickname())) {
        validateNickname(request.getNickname());
        saveHistory(userId, NICKNAME, profile.getNickname(), request.getNickname());
        profile.setNickname(request.getNickname());
    }

    // ... 성별, 지역, 이미지, 소개글, 번호도 동일하게 처리

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

이력 저장 로직

private void saveHistory(Long userId, UpdatableProfileColumn column, String oldValue, String newValue) {
    historyRepository.save(UserProfileHistory.of(userId, column, oldValue, newValue, "USER"));
}

실제 사용자는 "USER"로 저장하고, Kafka 기반 등 시스템 자동 갱신은 "SYSTEM"으로 처리 가능


4. 닉네임 유효성 검사 + 욕설 필터

잘못된 닉네임 방지

  • 2~20자 제한
  • 공백 금지
  • 금지어 포함 방지 (욕설, 성적인 단어 등)
private void validateNickname(String nickname) {
    if (nickname.length() < 2) throw new UserProfileException(NICKNAME_TOO_SHORT);
    if (nickname.length() > 20) throw new UserProfileException(NICKNAME_TOO_LONG);
    if (nickname.contains(" ")) throw new UserProfileException(NICKNAME_CONTAINS_SPACES);
    wordFilterService.validate(nickname); // 욕설 감지
}

wordFilterService는 퍼블릭 모듈에 위치한 재사용 가능한 욕설 필터 서비스


5. 변경 이력 조회 API 설계 (예시)

쿼리 조건 예시

  • userId 기준으로 조회
  • 특정 필드(NICKNAME, EMAIL 등)만 조회
  • 변경 시점으로 필터링

DSL 기반 UserProfileQueryRepository로 구현 가능


6. 실무 기준 설계 이점 요약

요소 설계 결정 장점
필드 구분 UpdatableProfileColumn 수정 대상 명확, 추적 용이
이력 저장 방식 DB 엔티티로 명시적 저장 감사(Audit) 용이
수정자 기록 "USER"/"SYSTEM" 명시 자동 수정과 사용자 구분 가능
닉네임 검사 길이, 공백, 금지어 모두 검사 UX + 보안 강화
DSL 조회 기반 이력 분석 및 모니터링 용이 유저 행동 분석 가능

7. 정리 및 다음 편 예고

이번 글에서는 유저 프로필 변경에 대한 정확한 필드 추적 방식,
DB 기반 감사 로그 저장, 닉네임 유효성 및 욕설 필터링 처리까지 다뤘다.

변경 이력 추적은 단순 UX를 넘어서, 보안·운영·데이터 분석을 위한 핵심 시스템이라는 점을 기억하자.


 다음 편 예고:

4편. “유저 탐색 및 필터링 기능 구현 - 닉네임, 지역, 장르, 성별 기반 DSL 설계”

  • 검색 조건 DSL 구성
  • 닉네임/지역/장르/성별 필터링 처리
  • 페이징과 정렬 전략
  • 배치 연동을 위한 DTO 설계

728x90