Conversation
개요회원 탈퇴 기능을 구현합니다. 컨트롤러 DELETE 엔드포인트, 사용자 엔티티의 삭제 메서드, 서비스 로직, 응답 DTO, 성공 메시지 열거형을 추가하고, 로그아웃 통합을 포함합니다. 변경 사항
시퀀스 다이어그램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
코드 리뷰 추정 난이도🎯 3 (중간 난이도) | ⏱️ ~20-25분 주의가 필요한 영역:
관련 PR
축하 시
💡 피드백 및 제안칭찬: 계층 분리가 깔끔하고, 테스트 커버리지도 탄탄합니다. 세 가지 시나리오(성공, 미존재, 인증 오류)를 모두 다룬 점이 좋습니다. 개선 제안:
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 패턴이라 현재 요구사항에는 잘 맞습니다.다만, 도메인 관점에서 다음 두 가지를 한 번 고민해보면 좋겠습니다.
중복 삭제 호출 처리
- 이미
deletedAt이 존재하는 사용자를 다시delete()하는 경우를 허용할지,- 혹은
if (deletedAt != null) throw ...처럼 한 번만 허용할지 정책을 도메인 레벨에서 명시하면 이후 유지보수 시 의도가 더 분명해집니다.시간 소스 일관성
- 다른 감사 필드(
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)반환요구사항 기준으로는 잘 동작할 구조입니다. 다만, 설계 관점에서 두 가지 정도를 제안드립니다.
서비스 계층의 서블릿 API 의존성 축소 제안
- 현재 시그니처:
DeleteUserResponse deleteUser(Long userId, HttpServletRequest request, HttpServletResponse response)- 이렇게 되면
UserService가 웹 계층 기술(Jakarta Servlet)에 직접 묶이기 때문에,
- 향후 비동기 처리나 다른 인터페이스(배치, 메시지 컨슈머 등)에서 재사용하기 어렵고,
- 단위 테스트에서도 매번
HttpServletRequest/Response를 준비해야 합니다.- 개선 아이디어:
AuthService쪽에 토큰/쿠키 등 필요한 값만 추상화한 파라미터를 넘기도록 리팩터링하고,- 컨트롤러에서
HttpServletRequest/Response를 받아 해당 값만 추출한 뒤 서비스에 전달하는 구조로 분리하는 방법이 있습니다.- 또는, "회원 탈퇴 + 로그아웃"을 담당하는 상위 Application Service/Façade를 두고, 도메인 서비스(
UserService)는 순수하게 유저 도메인 로직만 담당하게 나누는 것도 한 방법입니다.이미 탈퇴된 사용자 처리 여부
- 현재는
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에러가 발생하는지 검증다만, 구조/테스트 전략 관점에서 한 가지 정도만 제안드립니다.
- 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)로 공통 응답 포맷 유지한 가지, 서비스 계층과의 경계에 대해서만 짧게 제안드립니다.
- 컨트롤러에서 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
📒 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 레벨에서 기대하는 동작을 명확하게 검증하고 있어서 좋습니다.
회원_탈퇴에_성공한다
userService.deleteUser(...)를 Stub으로DeleteUserResponse(true)반환하도록 설정DELETE /api/v1/users/me호출 시
- HTTP 200
- 메시지:
SuccessMessage.DELETE_USER_SUCCESS.getMessage()$.data.success == true
→ 정상 플로우가 응답 포맷까지 포함해 잘 검증되어 있습니다.
회원_탈퇴_시_사용자가_존재하지_않으면_예외가_발생한다
- 서비스에서
BaseException(ErrorCode.USER_NOT_FOUND)를 던지는 경우- HTTP 404, 에러 코드의 httpStatus/message,
data미존재 여부까지 확인
→ 글로벌 예외 처리와의 연동이 정확히 테스트됩니다.
회원_탈퇴_중_예외가_발생하면_에러_응답을_반환한다
- 서비스에서
BaseException(ErrorCode.ACCESS_TOKEN_INVALID)를 던지는 경우- HTTP 401, 메시지,
data미존재 검증
→ 토큰/인증 관련 예외가 실제로 어떤 응답으로 매핑되는지 명확히 보장해 줍니다.MockMvc +
jsonPath검증 패턴이 기존 테스트들과도 일관적이고, 경계값/에러 케이스까지 고르게 커버되어 있어 특별히 손볼 부분은 없어 보입니다. 잘 짜인 테스트로 보입니다. 🙂Also applies to: 174-220
✨ 작업 내용
📝 적용 범위
/user📌 참고 사항
Summary by CodeRabbit