Skip to content

Commit

Permalink
#81 [feat] : 유효성 검증 및 에러 처리를 추가한다
Browse files Browse the repository at this point in the history
  • Loading branch information
bbbang105 committed Oct 21, 2024
1 parent b564774 commit 0aecfd8
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 67 deletions.
3 changes: 2 additions & 1 deletion src/main/java/side/onetime/controller/EventController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package side.onetime.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand All @@ -20,7 +21,7 @@ public class EventController {
// 이벤트 생성 API
@PostMapping
public ResponseEntity<ApiResponse<CreateEventResponse>> createEvent(
@RequestBody CreateEventRequest createEventRequest,
@Valid @RequestBody CreateEventRequest createEventRequest,
@RequestHeader(value = "Authorization", required = false) String authorizationHeader) {

CreateEventResponse createEventResponse;
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/side/onetime/controller/MemberController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package side.onetime.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -25,7 +26,7 @@ public class MemberController {
// 멤버 등록 API
@PostMapping("/action-register")
public ResponseEntity<ApiResponse<RegisterMemberResponse>> registerMember(
@RequestBody RegisterMemberRequest registerMemberRequest) {
@Valid @RequestBody RegisterMemberRequest registerMemberRequest) {

RegisterMemberResponse registerMemberResponse = memberService.registerMember(registerMemberRequest);
return ApiResponse.onSuccess(SuccessStatus._REGISTER_MEMBER, registerMemberResponse);
Expand All @@ -34,7 +35,7 @@ public ResponseEntity<ApiResponse<RegisterMemberResponse>> registerMember(
// 멤버 로그인 API
@PostMapping("/action-login")
public ResponseEntity<ApiResponse<LoginMemberResponse>> loginMember(
@RequestBody LoginMemberRequest loginMemberRequest) {
@Valid @RequestBody LoginMemberRequest loginMemberRequest) {

LoginMemberResponse loginMemberResponse = memberService.loginMember(loginMemberRequest);
return ApiResponse.onSuccess(SuccessStatus._LOGIN_MEMBER, loginMemberResponse);
Expand All @@ -43,7 +44,7 @@ public ResponseEntity<ApiResponse<LoginMemberResponse>> loginMember(
// 이름 중복 확인 API
@PostMapping("/name/action-check")
public ResponseEntity<ApiResponse<IsDuplicateResponse>> isDuplicate(
@RequestBody IsDuplicateRequest isDuplicateRequest) {
@Valid @RequestBody IsDuplicateRequest isDuplicateRequest) {

IsDuplicateResponse isDuplicateResponse = memberService.isDuplicate(isDuplicateRequest);
return ApiResponse.onSuccess(SuccessStatus._IS_POSSIBLE_NAME, isDuplicateResponse);
Expand Down
9 changes: 5 additions & 4 deletions src/main/java/side/onetime/controller/ScheduleController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package side.onetime.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand All @@ -23,7 +24,7 @@ public class ScheduleController {
// 요일 스케줄 등록 API
@PostMapping("/day")
public ResponseEntity<ApiResponse<SuccessStatus>> createDaySchedules(
@RequestBody CreateDayScheduleRequest createDayScheduleRequest,
@Valid @RequestBody CreateDayScheduleRequest createDayScheduleRequest,
@RequestHeader(value = "Authorization", required = false) String authorizationHeader) {

if (authorizationHeader != null) {
Expand All @@ -37,7 +38,7 @@ public ResponseEntity<ApiResponse<SuccessStatus>> createDaySchedules(
// 날짜 스케줄 등록 API
@PostMapping("/date")
public ResponseEntity<ApiResponse<SuccessStatus>> createDateSchedules(
@RequestBody CreateDateScheduleRequest createDateScheduleRequest,
@Valid @RequestBody CreateDateScheduleRequest createDateScheduleRequest,
@RequestHeader(value = "Authorization", required = false) String authorizationHeader) {

if (authorizationHeader != null) {
Expand Down Expand Up @@ -80,7 +81,7 @@ public ResponseEntity<ApiResponse<PerDaySchedulesResponse>> getUserDaySchedules(
// 멤버 필터링 요일 스케줄 조회 API
@GetMapping("/day/action-filtering")
public ResponseEntity<ApiResponse<List<PerDaySchedulesResponse>>> getFilteredDaySchedules(
@RequestBody GetFilteredSchedulesRequest getFilteredSchedulesRequest) {
@Valid @RequestBody GetFilteredSchedulesRequest getFilteredSchedulesRequest) {

List<PerDaySchedulesResponse> perDaySchedulesResponses = scheduleService.getFilteredDaySchedules(getFilteredSchedulesRequest);
return ApiResponse.onSuccess(SuccessStatus._GET_FILTERED_DAY_SCHEDULES, perDaySchedulesResponses);
Expand Down Expand Up @@ -118,7 +119,7 @@ public ResponseEntity<ApiResponse<PerDateSchedulesResponse>> getUserDateSchedule
// 멤버 필터링 날짜 스케줄 조회 API
@GetMapping("/date/action-filtering")
public ResponseEntity<ApiResponse<List<PerDateSchedulesResponse>>> getFilteredDateSchedules(
@RequestBody GetFilteredSchedulesRequest getFilteredSchedulesRequest) {
@Valid @RequestBody GetFilteredSchedulesRequest getFilteredSchedulesRequest) {

List<PerDateSchedulesResponse> perDateSchedulesResponses = scheduleService.getFilteredDateSchedules(getFilteredSchedulesRequest);
return ApiResponse.onSuccess(SuccessStatus._GET_FILTERED_DATE_SCHEDULES, perDateSchedulesResponses);
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/side/onetime/controller/TokenController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package side.onetime.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -21,7 +22,7 @@ public class TokenController {
// 액세스 토큰 재발행 API
@PostMapping("/action-reissue")
public ResponseEntity<ApiResponse<ReissueTokenResponse>> reissueToken(
@RequestBody ReissueTokenRequest reissueAccessTokenRequest) {
@Valid @RequestBody ReissueTokenRequest reissueAccessTokenRequest) {

ReissueTokenResponse reissueTokenResponse = tokenService.reissueToken(reissueAccessTokenRequest);
return ApiResponse.onSuccess(SuccessStatus._REISSUE_TOKENS, reissueTokenResponse);
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/side/onetime/controller/UrlController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package side.onetime.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -23,7 +24,7 @@ public class UrlController {
// 원본 -> 단축 URL API
@PostMapping("/action-shorten")
public ResponseEntity<ApiResponse<ConvertToShortenUrlResponse>> convertToShortenUrl(
@RequestBody ConvertToShortenUrlRequest covertToShortenUrlRequest) {
@Valid @RequestBody ConvertToShortenUrlRequest covertToShortenUrlRequest) {

ConvertToShortenUrlResponse convertToShortenUrlResponse = urlService.convertToShortenUrl(covertToShortenUrlRequest);
return ApiResponse.onSuccess(SuccessStatus._CONVERT_TO_SHORTEN_URL, convertToShortenUrlResponse);
Expand All @@ -32,7 +33,7 @@ public ResponseEntity<ApiResponse<ConvertToShortenUrlResponse>> convertToShorten
// 단축 -> 원본 URL API
@PostMapping("/action-original")
public ResponseEntity<ApiResponse<ConvertToOriginalUrlResponse>> convertToOriginalUrl(
@RequestBody ConvertToOriginalUrlRequest convertToOriginalUrlRequest) {
@Valid @RequestBody ConvertToOriginalUrlRequest convertToOriginalUrlRequest) {

ConvertToOriginalUrlResponse convertToOriginalUrlResponse = urlService.convertToOriginalUrl(convertToOriginalUrlRequest);
return ApiResponse.onSuccess(SuccessStatus._CONVERT_TO_ORIGINAL_URL, convertToOriginalUrlResponse);
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/side/onetime/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package side.onetime.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand All @@ -20,7 +21,7 @@ public class UserController {
// 유저 온보딩 API
@PostMapping("/onboarding")
public ResponseEntity<ApiResponse<OnboardUserResponse>> onboardUser(
@RequestBody OnboardUserRequest onboardUserRequest) {
@Valid @RequestBody OnboardUserRequest onboardUserRequest) {

OnboardUserResponse onboardUserResponse = userService.onboardUser(onboardUserRequest);
return ApiResponse.onSuccess(SuccessStatus._ONBOARD_USER, onboardUserResponse);
Expand All @@ -39,7 +40,7 @@ public ResponseEntity<ApiResponse<GetUserProfileResponse>> getUserProfile(
@PatchMapping("/profile/action-update")
public ResponseEntity<ApiResponse<SuccessStatus>> updateUserProfile(
@RequestHeader("Authorization") String authorizationHeader,
@RequestBody UpdateUserProfileRequest updateUserProfileRequest) {
@Valid @RequestBody UpdateUserProfileRequest updateUserProfileRequest) {

userService.updateUserProfile(authorizationHeader, updateUserProfileRequest);
return ApiResponse.onSuccess(SuccessStatus._UPDATE_USER_PROFILE);
Expand Down
154 changes: 100 additions & 54 deletions src/main/java/side/onetime/exception/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -1,88 +1,134 @@
package side.onetime.exception;

import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import side.onetime.global.common.ApiResponse;
import side.onetime.global.common.code.BaseErrorCode;
import side.onetime.global.common.status.ErrorStatus;
import side.onetime.global.common.dto.ErrorReasonDto;
import side.onetime.global.common.status.ErrorStatus;

import java.util.List;
import java.util.stream.Collectors;

@RestControllerAdvice
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

// Event
@ExceptionHandler(EventException.class)
public ResponseEntity<ApiResponse<BaseErrorCode>> handleTokenException(EventException e) {
EventErrorResult errorResult = e.getEventErrorResult();
return ApiResponse.onFailure(errorResult);
// 커스텀 예외 처리
@ExceptionHandler(CustomException.class)
public ResponseEntity<ApiResponse<ErrorReasonDto>> handleCustomException(CustomException e) {
logError(e.getMessage(), e);
return ApiResponse.onFailure(e.getErrorCode());
}

// Member
@ExceptionHandler(MemberException.class)
public ResponseEntity<ApiResponse<BaseErrorCode>> handleMemberException(MemberException e) {
MemberErrorResult errorResult = e.getMemberErrorResult();
return ApiResponse.onFailure(errorResult);
// Security 인증 관련 처리
@ExceptionHandler(SecurityException.class)
public ResponseEntity<ApiResponse<ErrorReasonDto>> handleSecurityException(SecurityException e) {
logError(e.getMessage(), e);
return ApiResponse.onFailure(ErrorStatus._UNAUTHORIZED);
}

// Schedule
@ExceptionHandler(ScheduleException.class)
public ResponseEntity<ApiResponse<BaseErrorCode>> handleScheduleException(ScheduleException e) {
ScheduleErrorResult errorResult = e.getScheduleErrorResult();
return ApiResponse.onFailure(errorResult);
// IllegalArgumentException 처리 (잘못된 인자가 전달된 경우)
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Object> handleIllegalArgumentException(IllegalArgumentException e) {
String errorMessage = "잘못된 요청입니다: " + e.getMessage();
logError("IllegalArgumentException", errorMessage);
return ApiResponse.onFailure(ErrorStatus._BAD_REQUEST, errorMessage);
}

// Selection
@ExceptionHandler(SelectionException.class)
public ResponseEntity<ApiResponse<BaseErrorCode>> handleSelectionException(SelectionException e) {
SelectionErrorResult errorResult = e.getSelectionErrorResult();
return ApiResponse.onFailure(errorResult);
// ConstraintViolationException 처리 (쿼리 파라미터에 올바른 값이 들어오지 않은 경우)
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Object> handleValidationParameterError(ConstraintViolationException ex) {
String errorMessage = ex.getMessage();
logError("ConstraintViolationException", errorMessage);
return ApiResponse.onFailure(ErrorStatus._BAD_REQUEST, errorMessage);
}

// User
@ExceptionHandler(UserException.class)
public ResponseEntity<ApiResponse<BaseErrorCode>> handleUserException(UserException e) {
UserErrorResult errorResult = e.getUserErrorResult();
return ApiResponse.onFailure(errorResult);
// MissingServletRequestParameterException 처리 (필수 쿼리 파라미터가 입력되지 않은 경우)
@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
String errorMessage = "필수 파라미터 '" + ex.getParameterName() + "'가 없습니다.";
logError("MissingServletRequestParameterException", errorMessage);
return ApiResponse.onFailure(ErrorStatus._BAD_REQUEST, errorMessage);
}

// Token
@ExceptionHandler(TokenException.class)
public ResponseEntity<ApiResponse<BaseErrorCode>> handleTokenException(TokenException e) {
TokenErrorResult errorResult = e.getTokenErrorResult();
return ApiResponse.onFailure(errorResult);
// MethodArgumentNotValidException 처리 (RequestBody로 들어온 필드들의 유효성 검증에 실패한 경우)
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
String combinedErrors = extractFieldErrors(ex.getBindingResult().getFieldErrors());
logError("Validation error", combinedErrors);
return ApiResponse.onFailure(ErrorStatus._BAD_REQUEST, combinedErrors);
}

// EventParticipation
@ExceptionHandler(EventParticipationException.class)
public ResponseEntity<ApiResponse<BaseErrorCode>> handleEventParticipationException(EventParticipationException e) {
EventParticipationErrorResult errorResult = e.getEventParticipationErrorResult();
return ApiResponse.onFailure(errorResult);
// NoHandlerFoundException 처리 (요청 경로에 매핑된 핸들러가 없는 경우)
@Override
protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
String errorMessage = "해당 경로에 대한 핸들러를 찾을 수 없습니다: " + ex.getRequestURL();
logError("NoHandlerFoundException", errorMessage);
return ApiResponse.onFailure(ErrorStatus._NOT_FOUND_HANDLER, errorMessage);
}

// AccessDeniedException 등 보안 관련 에러 처리
@ExceptionHandler(SecurityException.class)
public ResponseEntity<ErrorReasonDto> handleSecurityException(SecurityException e) {
log.error("SecurityException: {}", e.getMessage());
return ResponseEntity.status(ErrorStatus._UNAUTHORIZED.getHttpStatus())
.body(ErrorStatus._UNAUTHORIZED.getReasonHttpStatus());
// HttpRequestMethodNotSupportedException 처리 (지원하지 않는 HTTP 메소드 요청이 들어온 경우)
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
String errorMessage = "지원하지 않는 HTTP 메소드 요청입니다: " + ex.getMethod();
logError("HttpRequestMethodNotSupportedException", errorMessage);
return ApiResponse.onFailure(ErrorStatus._METHOD_NOT_ALLOWED, errorMessage);
}

// HttpMediaTypeNotSupportedException 처리 (지원하지 않는 미디어 타입 요청이 들어온 경우)
@Override
protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
String errorMessage = "지원하지 않는 미디어 타입입니다: " + ex.getContentType();
logError("HttpMediaTypeNotSupportedException", errorMessage);
return ApiResponse.onFailure(ErrorStatus._UNSUPPORTED_MEDIA_TYPE, errorMessage);
}

// 기타 Exception 처리
// 내부 서버 에러 처리 (500)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorReasonDto> handleException(Exception e) {
log.error("Exception: {}", e.getMessage());
public ResponseEntity<ApiResponse<ErrorReasonDto>> handleException(Exception e) {
// 서버 내부 에러 발생 시 로그에 예외 내용 기록
logError(e.getMessage(), e);
return ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR);
}

if (e instanceof IllegalArgumentException) {
return ResponseEntity.status(ErrorStatus._BAD_REQUEST.getHttpStatus())
.body(ErrorStatus._BAD_REQUEST.getReasonHttpStatus());
}
// 유효성 검증 오류 메시지 추출 메서드 (FieldErrors)
private String extractFieldErrors(List<FieldError> fieldErrors) {
return fieldErrors.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
}

// 그 외 내부 서버 오류로 처리
return ResponseEntity.status(ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus())
.body(ErrorStatus._INTERNAL_SERVER_ERROR.getReasonHttpStatus());
// 로그 기록 메서드
private void logError(String message, Object errorDetails) {
log.error("{}: {}", message, errorDetails);
}
}

0 comments on commit 0aecfd8

Please sign in to comment.