Skip to content

feat: 회원 탈퇴 기능 구현#23

Merged
JoonKyoLee merged 6 commits intomainfrom
feat/delete-user
Nov 19, 2025
Merged

feat: 회원 탈퇴 기능 구현#23
JoonKyoLee merged 6 commits intomainfrom
feat/delete-user

Conversation

@JoonKyoLee
Copy link
Contributor

@JoonKyoLee JoonKyoLee commented Nov 19, 2025

✨ 작업 내용

  • 회원 탈퇴 기능 구현

📝 적용 범위

  • /user

📌 참고 사항

Summary by CodeRabbit

  • 새로운 기능
    • 사용자가 자신의 계정을 삭제할 수 있는 회원 탈퇴 기능이 추가되었습니다.

@JoonKyoLee JoonKyoLee self-assigned this Nov 19, 2025
@JoonKyoLee JoonKyoLee added the enhancement New feature or request label Nov 19, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 19, 2025

개요

회원 탈퇴 기능을 구현합니다. 컨트롤러 DELETE 엔드포인트, 사용자 엔티티의 삭제 메서드, 서비스 로직, 응답 DTO, 성공 메시지 열거형을 추가하고, 로그아웃 통합을 포함합니다.

변경 사항

결집체 / 파일(들) 변경 요약
성공 메시지 확장
src/main/java/com/almang/inventory/global/api/SuccessMessage.java
DELETE_USER_SUCCESS("회원 탈퇴 성공") 열거형 상수 추가
응답 DTO
src/main/java/com/almang/inventory/user/dto/response/DeleteUserResponse.java
성공 상태를 담는 새로운 공개 레코드 정의: record DeleteUserResponse(boolean success)
도메인 계층
src/main/java/com/almang/inventory/user/domain/User.java
사용자를 삭제로 표시하는 delete() 메서드 추가 (deletedAt을 현재 시간으로 설정)
컨트롤러 계층
src/main/java/com/almang/inventory/user/controller/UserController.java
DELETE /me 엔드포인트 추가: 인증된 사용자가 스스로 계정 탈퇴 (HttpServletRequest, HttpServletResponse 전달)
서비스 계층
src/main/java/com/almang/inventory/user/service/UserService.java
@Transactional deleteUser(userId, request, response) 메서드: 사용자 조회 → 삭제 표시 → AuthService 로그아웃 호출 → 응답 반환
컨트롤러 테스트
src/test/java/com/almang/inventory/user/controller/UserControllerTest.java
세 가지 테스트 케이스 추가: 성공 경로 (true 반환), 사용자 미존재 (404), 인증 토큰 무효 (401)
서비스 테스트
src/test/java/com/almang/inventory/user/service/UserServiceTest.java
두 가지 테스트: 성공적인 삭제 검증 (deletedAt 타임스탬프 확인), 사용자 미존재 시 예외 처리 검증

시퀀스 다이어그램

sequenceDiagram
    participant Client
    participant Controller as UserController
    participant Service as UserService
    participant AuthSvc as AuthService
    participant DB as User Entity

    Client->>Controller: DELETE /me (with auth token)
    Controller->>Service: deleteUser(userId, request, response)
    Service->>DB: findById(userId)
    alt User Found
        Service->>DB: delete() - set deletedAt
        Service->>AuthSvc: logout(request, response)
        AuthSvc-->>Service: ✓ logout complete
        Service-->>Controller: DeleteUserResponse(true)
        Controller-->>Client: 200 OK { success: true }
    else User Not Found
        Service-->>Controller: throw BaseException(USER_NOT_FOUND)
        Controller-->>Client: 404 Not Found { error: "user-not-found" }
    end
Loading

코드 리뷰 추정 난이도

🎯 3 (중간 난이도) | ⏱️ ~20-25분

주의가 필요한 영역:

  • AuthService 통합: AuthService.logout(request, response) 호출이 정확히 쿠키/토큰을 제거하는지 확인. 부분 삭제 시 보안 허점 가능성
  • 트랜잭션 일관성: @Transactional 메서드 내에서 로그아웃 외부 호출 시 롤백 동작 검증 필요 (AuthService 자체가 별도 트랜잭션을 가질 경우)
  • 로깅 및 감사: 회원 탈퇴는 중요 이벤트이므로, 타임스탬프, 사용자 ID, IP 주소 등이 충분히 기록되는지 확인
  • 테스트 커버리지: 정상 흐름은 충분하지만, 로그아웃 실패 시나리오(AuthService 예외)에 대한 테스트도 고려

관련 PR

  • PR #15: SuccessMessage 열거형을 수정하는 다른 PR과 동일 파일 접촉 — 병합 순서와 충돌 가능성 확인
  • PR #16: 이 PR의 AuthService.logout() 호출이 PR #16에서 구현된 로그아웃 로직에 직접 의존 — 두 PR의 호환성 검증 필수

축하 시

🎉 탈퇴의 예술, 우아하게 구현되다
사용자는 떠나고, 토큰은 사라지고
데이터만 조용히 deletedAt 타임스탬프로 기억하네 🕊️


💡 피드백 및 제안

칭찬: 계층 분리가 깔끔하고, 테스트 커버리지도 탄탄합니다. 세 가지 시나리오(성공, 미존재, 인증 오류)를 모두 다룬 점이 좋습니다.

개선 제안:

  1. 트랜잭션 처리 재검토

    • AuthService.logout(request, response)@Transactional deleteUser() 내부에서 호출될 때, 로그아웃 실패 시 사용자 삭제가 롤백되는지 확인하세요.
    • 권장: AuthService 호출을 별도 트랜잭션이나 try-finally로 격리하여, 삭제는 완료되되 로그아웃만 실패하는 엣지 케이스 처리 검토
  2. 감시 로깅 보강

    • 회원 탈퇴는 감시 대상 이벤트입니다. 요청 로그에 사용자 ID, IP 주소, 타임스탬프, 결과(성공/실패)를 기록하세요.
    • 참고: Spring Security Audit Logging
  3. 로그아웃 실패 시나리오 테스트

    • 현재 테스트에서 AuthService.logout() 호출 자체가 예외를 던지는 경우를 다루지 않았습니다.
    • 추천: UserServiceTest에 "로그아웃 중 예외 발생" 케이스 추가 (AuthService 모킹으로 throw 설정)

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 PR의 주요 변경사항인 회원 탈퇴 기능 구현을 명확하고 간결하게 전달하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿의 모든 필수 섹션(작업 내용, 적용 범위, 참고 사항)을 포함하고 있으며, 관련 이슈도 명시되어 있습니다.
Linked Issues check ✅ Passed 구현된 코드가 이슈 #19의 요구사항을 충족합니다: 회원 탈퇴 엔드포인트 구현 및 토큰 삭제 로직(authService.logout) 연결이 모두 완료되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 회원 탈퇴 기능 구현이라는 명확한 범위 내에 있으며, 관련 없는 수정사항이 포함되지 않았습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/delete-user

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
src/main/java/com/almang/inventory/user/domain/User.java (1)

68-71: 엔티티 레벨 soft delete 구현 👍

delete()에서 deletedAt만 세팅하는 단순한 soft delete 패턴이라 현재 요구사항에는 잘 맞습니다.

다만, 도메인 관점에서 다음 두 가지를 한 번 고민해보면 좋겠습니다.

  1. 중복 삭제 호출 처리

    • 이미 deletedAt이 존재하는 사용자를 다시 delete() 하는 경우를 허용할지,
    • 혹은 if (deletedAt != null) throw ... 처럼 한 번만 허용할지 정책을 도메인 레벨에서 명시하면 이후 유지보수 시 의도가 더 분명해집니다.
  2. 시간 소스 일관성

    • 다른 감사 필드(createdAt, updatedAt)가 JPA Auditing 등을 통해 세팅된다면, 같은 방식(예: Auditing, 혹은 별도 Clock 주입)을 사용하는 것도 고려해볼 만합니다.
    • 지금처럼 LocalDateTime.now()를 직접 호출해도 기능상 문제는 없지만, 테스트/타임존 제어가 필요해지면 Clock 주입이 유용해집니다.

현재 구현 자체는 문제 없습니다. 작은 고민 포인트로만 참고해 주세요.

src/main/java/com/almang/inventory/user/dto/response/DeleteUserResponse.java (1)

1-5: 단순한 응답 DTO 설계, 현재 요구에는 적합합니다

DeleteUserResponse(boolean success) 하나로 성공 여부만 내려주는 구조는 지금 스펙에는 충분히 가볍고 명확합니다.

향후에 필요하다면, 예를 들어 아래처럼 추가 정보를 담을 여지도 있습니다(지금 당장 변경이 필요하다는 의미는 아닙니다).

  • Long userId (추가 확인용)
  • LocalDateTime deletedAt (삭제 시각을 바로 응답으로 노출하고 싶을 때)

현재 PR 범위에서는 이대로 두는 것이 가장 깔끔해 보입니다.

src/main/java/com/almang/inventory/user/service/UserService.java (1)

6-60: 회원 탈퇴 플로우는 동작 관점에서 적절하나, 계층 의존성은 한 번 점검해보면 좋겠습니다

구현 흐름 자체는 명확합니다.

  • findUserById로 존재 여부 확인(없으면 USER_NOT_FOUND 예외)
  • soft delete (user.delete())
  • authService.logout(userId, request, response)로 토큰/세션 정리
  • 성공 로그 후 DeleteUserResponse(true) 반환

요구사항 기준으로는 잘 동작할 구조입니다. 다만, 설계 관점에서 두 가지 정도를 제안드립니다.

  1. 서비스 계층의 서블릿 API 의존성 축소 제안

    • 현재 시그니처:
      DeleteUserResponse deleteUser(Long userId, HttpServletRequest request, HttpServletResponse response)
    • 이렇게 되면 UserService가 웹 계층 기술(Jakarta Servlet)에 직접 묶이기 때문에,
      • 향후 비동기 처리나 다른 인터페이스(배치, 메시지 컨슈머 등)에서 재사용하기 어렵고,
      • 단위 테스트에서도 매번 HttpServletRequest/Response를 준비해야 합니다.
    • 개선 아이디어:
      • AuthService 쪽에 토큰/쿠키 등 필요한 값만 추상화한 파라미터를 넘기도록 리팩터링하고,
      • 컨트롤러에서 HttpServletRequest/Response를 받아 해당 값만 추출한 뒤 서비스에 전달하는 구조로 분리하는 방법이 있습니다.
      • 또는, "회원 탈퇴 + 로그아웃"을 담당하는 상위 Application Service/Façade를 두고, 도메인 서비스(UserService)는 순수하게 유저 도메인 로직만 담당하게 나누는 것도 한 방법입니다.
  2. 이미 탈퇴된 사용자 처리 여부

    • 현재는 deletedAt이 이미 설정된 사용자에 대해서도 다시 deleteUser를 호출하면 그대로 deletedAt을 덮어쓰게 됩니다.
    • 정책에 따라,
      • 재호출을 허용(멱등하게 처리)하려면 if (user.getDeletedAt() != null) return new DeleteUserResponse(true);
      • 혹은 예외를 던져 "이미 탈퇴 처리된 사용자"를 구분할 수도 있습니다.
    • 어떤 쪽이든 도메인 정책이 명시되면 이후 유지보수 시 혼란을 줄일 수 있습니다.

지금 PR의 범위에서는 기능적으로 문제는 없어 보이고, 위 내용은 추후 리팩터링 시 고려해 볼 만한 포인트로 보입니다.

src/test/java/com/almang/inventory/user/service/UserServiceTest.java (1)

13-18: 서비스 레벨 회원 탈퇴 테스트는 핵심 경로를 잘 커버하고 있습니다

회원_탈퇴에_성공한다 / 회원_탈퇴_시_사용자가_존재하지_않으면_예외가_발생한다 두 테스트가 다음을 잘 검증하고 있어 좋습니다.

  • 정상 케이스

    • DeleteUserResponse.success()true인지 확인
    • 실제 DB에서 다시 조회한 deletedUser.getDeletedAt()null이 아닌지 확인 → soft delete 동작 확인
  • 비정상 케이스

    • 존재하지 않는 ID에 대해 USER_NOT_FOUND 에러가 발생하는지 검증

다만, 구조/테스트 전략 관점에서 한 가지 정도만 제안드립니다.

  1. AuthService 의존성에 대한 테스트 민감도 완화 제안
    • 현재 @SpringBootTest에서 실제 UserService 빈을 사용하고 있고, UserService.deleteUser 내부에서 authService.logout(...)을 호출합니다.
    • 만약 AuthService.logout 구현이 요청 헤더/쿠키에 강하게 의존하거나 토큰 유효성 검사 실패 시 예외를 던지는 구조라면,
      • 서비스 테스트가 "토큰 파싱 로직" 변화에까지 영향을 받아 불필요하게 깨질 수 있는 구조가 될 수 있습니다.
    • 완전히 필수는 아니지만, 다음과 같은 방식도 고려해볼 수 있습니다.
      • @MockBean AuthService authService;를 추가하고,
      • doNothing().when(authService).logout(anyLong(), any(HttpServletRequest.class), any(HttpServletResponse.class));
      • 이렇게 하면 이 테스트는 **"회원 soft delete 도메인 동작"**에만 집중하고,
        토큰/로그아웃 로직은 별도 테스트(또는 상위 계층)에서 검증할 수 있습니다.

현재 추가하신 테스트만으로도 이번 PR의 핵심 플로우는 잘 커버되고 있습니다. 위 내용은 테스트 분리도를 더 높이고 싶을 때 고려해 볼 선택지 정도로 봐주시면 될 것 같습니다.

Also applies to: 131-162

src/main/java/com/almang/inventory/user/controller/UserController.java (1)

7-20: 회원 탈퇴 엔드포인트 설계가 명확하고 기존 패턴과 잘 맞습니다

DELETE /api/v1/users/me 엔드포인트가 기존 컨트롤러 스타일과 잘 맞고, 로그/응답 메시지도 일관적이라 사용성 측면에서 이해하기 쉬운 구조입니다. 👍

  • 인증 정보에서 userId만 추출
  • 서비스 호출 후
  • ApiResponse.success(SuccessMessage.DELETE_USER_SUCCESS.getMessage(), response)로 공통 응답 포맷 유지

한 가지, 서비스 계층과의 경계에 대해서만 짧게 제안드립니다.

  1. 컨트롤러에서 HTTP 의존성을 소화하는 구조 고려
    • 현재는 HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse를 그대로 서비스로 전달하고 있습니다.
    • 이상적인 계층 분리 관점에서는,
      • 컨트롤러에서 HTTP 요청/응답 객체를 다루고,
      • 서비스에는 "도메인에 필요한 최소한의 정보"만 넘겨주는 식으로 역할을 나누면 재사용성이 더 좋아집니다.
    • 예를 들면:
      • 컨트롤러: 토큰/쿠키 등 필요한 값을 추출
      • 서비스: 추상화된 값(예: accessToken 문자열, refreshToken 식별자 등)만 인자로 받아 처리

지금 PR 범위에선 크게 문제 될 수준은 아니지만, 이후 도메인 서비스 재사용이나 테스트 분리도를 더 올리고 싶을 때 한 번쯤 고려해 보면 좋을 것 같습니다.

Also applies to: 65-78

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 935d90f and 00f8d14.

📒 Files selected for processing (7)
  • src/main/java/com/almang/inventory/global/api/SuccessMessage.java (1 hunks)
  • src/main/java/com/almang/inventory/user/controller/UserController.java (2 hunks)
  • src/main/java/com/almang/inventory/user/domain/User.java (1 hunks)
  • src/main/java/com/almang/inventory/user/dto/response/DeleteUserResponse.java (1 hunks)
  • src/main/java/com/almang/inventory/user/service/UserService.java (3 hunks)
  • src/test/java/com/almang/inventory/user/controller/UserControllerTest.java (3 hunks)
  • src/test/java/com/almang/inventory/user/service/UserServiceTest.java (2 hunks)
🔇 Additional comments (2)
src/main/java/com/almang/inventory/global/api/SuccessMessage.java (1)

17-18: 성공 메시지 확장 잘 연결되었습니다

DELETE_USER_SUCCESS("회원 탈퇴 성공")가 기존 메시지 패턴과 일관되고, 컨트롤러/테스트에서도 그대로 재사용되고 있어 응답 포맷 유지에 좋아 보입니다. 따로 손볼 부분은 없어 보입니다.

src/test/java/com/almang/inventory/user/controller/UserControllerTest.java (1)

7-25: 삭제 엔드포인트에 대한 컨트롤러 테스트 구성이 아주 깔끔합니다

추가된 세 가지 테스트가 API 레벨에서 기대하는 동작을 명확하게 검증하고 있어서 좋습니다.

  1. 회원_탈퇴에_성공한다

    • userService.deleteUser(...)를 Stub으로 DeleteUserResponse(true) 반환하도록 설정
    • DELETE /api/v1/users/me 호출 시
      • HTTP 200
      • 메시지: SuccessMessage.DELETE_USER_SUCCESS.getMessage()
      • $.data.success == true
        → 정상 플로우가 응답 포맷까지 포함해 잘 검증되어 있습니다.
  2. 회원_탈퇴_시_사용자가_존재하지_않으면_예외가_발생한다

    • 서비스에서 BaseException(ErrorCode.USER_NOT_FOUND)를 던지는 경우
    • HTTP 404, 에러 코드의 httpStatus/message, data 미존재 여부까지 확인
      → 글로벌 예외 처리와의 연동이 정확히 테스트됩니다.
  3. 회원_탈퇴_중_예외가_발생하면_에러_응답을_반환한다

    • 서비스에서 BaseException(ErrorCode.ACCESS_TOKEN_INVALID)를 던지는 경우
    • HTTP 401, 메시지, data 미존재 검증
      → 토큰/인증 관련 예외가 실제로 어떤 응답으로 매핑되는지 명확히 보장해 줍니다.

MockMvc + jsonPath 검증 패턴이 기존 테스트들과도 일관적이고, 경계값/에러 케이스까지 고르게 커버되어 있어 특별히 손볼 부분은 없어 보입니다. 잘 짜인 테스트로 보입니다. 🙂

Also applies to: 174-220

@JoonKyoLee JoonKyoLee merged commit db18f1c into main Nov 19, 2025
1 check passed
@JoonKyoLee JoonKyoLee deleted the feat/delete-user branch November 20, 2025 05:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 회원 탈퇴 기능 구현

1 participant