-
Notifications
You must be signed in to change notification settings - Fork 37
[5주차] 커스텀 에러코드 및 예외 추가 #101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: rmsxo200
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| import com.hanghae.application.port.in.MovieScheduleService; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
|
|
@@ -23,8 +24,7 @@ public class MovieController { | |
| public ResponseEntity<ApiResponse<List<MovieScheduleResponseDto>>> getMovieSchedules() { | ||
| ApiResponse<List<MovieScheduleResponseDto>> response = movieScheduleService.getMovieSchedules(); | ||
|
|
||
| //응답코드 일치시켜서 리턴 | ||
| return ResponseEntity.status(response.status().getCode()).body(response); | ||
| return ResponseEntity.status(HttpStatus.OK).body(response); | ||
| } | ||
|
|
||
| //영화별 상영 시간표 조회 (grouping) | ||
|
|
@@ -37,16 +37,18 @@ public ResponseEntity<ApiResponse<List<ShowingMovieScheduleResponseDto>>> getSho | |
|
|
||
| ApiResponse<List<ShowingMovieScheduleResponseDto>> response = movieScheduleService.getShowingMovieSchedules(requestDto, ip); | ||
|
|
||
| //응답코드 일치시켜서 리턴 | ||
| return ResponseEntity.status(response.status().getCode()).body(response); | ||
| if(response.success()) { | ||
| return ResponseEntity.status(HttpStatus.OK).body(response); //조회 성공 | ||
| } else { | ||
| return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(response); // 조회 실패 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. getShowingMovieSchedules 가 실패하면 모두 TOO_MANY_REQUESTS 로 처리하는게 맞는방법인지, 이렇게 하신 의도가 있었을까요? |
||
| } | ||
| } | ||
|
|
||
| //redis 캐시삭제 (테스트용) | ||
| @GetMapping("/test/evict-cache") | ||
| public ResponseEntity<ApiResponse<Void>> evictCache() { | ||
| ApiResponse<Void> response = movieScheduleService.evictShowingMovieCache(); | ||
|
|
||
| //응답코드 일치시켜서 리턴 | ||
| return ResponseEntity.status(response.status().getCode()).body(response); | ||
| return ResponseEntity.status(HttpStatus.NO_CONTENT).body(response); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,9 @@ | ||
| package com.hanghae.adapter.web.exception; | ||
|
|
||
| import com.hanghae.application.dto.ApiResponse; | ||
| import com.hanghae.application.enums.HttpStatusCode; | ||
| import com.hanghae.application.enums.ErrorCode; | ||
| import com.hanghae.application.exception.CustomRequestException; | ||
| import com.hanghae.application.exception.CustomServerException; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
|
|
@@ -15,22 +17,36 @@ public class GlobalExceptionHandler { | |
| @ExceptionHandler(IllegalArgumentException.class) | ||
| public ResponseEntity<ApiResponse<Void>> handleIllegalArgumentException(IllegalArgumentException e) { | ||
| log.error("IllegalArgumentException occurred: {}", e.getMessage(), e); // 로그 추가 | ||
| ApiResponse<Void> apiResponse = ApiResponse.of(e.getMessage(), HttpStatusCode.BAD_REQUEST); | ||
| return ResponseEntity.status(HttpStatusCode.BAD_REQUEST.getCode()).body(apiResponse); | ||
| ApiResponse<Void> apiResponse = ApiResponse.of(false, e.getMessage()); | ||
| return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(apiResponse); | ||
| } | ||
|
|
||
| @ExceptionHandler(HttpMessageNotReadableException.class) | ||
| public ResponseEntity<ApiResponse<Void>> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { | ||
| log.error("HttpMessageNotReadableException occurred: {}", e.getMessage(), e); // 로그 추가 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. // 로그 추가 등의 의미없는 주석은 제거해도 좋을 것 같습니다. |
||
| ApiResponse<Void> apiResponse = ApiResponse.of(e.getMessage(), HttpStatusCode.BAD_REQUEST); | ||
| return ResponseEntity.status(HttpStatusCode.BAD_REQUEST.getCode()).body(apiResponse); | ||
| ApiResponse<Void> apiResponse = ApiResponse.of(false, e.getMessage()); | ||
| return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(apiResponse); | ||
| } | ||
|
|
||
| @ExceptionHandler(RuntimeException.class) | ||
| public ResponseEntity<ApiResponse<Void>> handleRuntimeException(RuntimeException e) { | ||
| log.error("RuntimeException occurred: {}", e.getMessage(), e); // 로그 추가 | ||
| ApiResponse<Void> apiResponse = ApiResponse.of(e.getMessage(), HttpStatusCode.INTERNAL_SERVER_ERROR); | ||
| return ResponseEntity.status(HttpStatusCode.INTERNAL_SERVER_ERROR.getCode()).body(apiResponse); | ||
| ApiResponse<Void> apiResponse = ApiResponse.of(false, e.getMessage()); | ||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(apiResponse); | ||
| } | ||
|
|
||
| @ExceptionHandler(CustomRequestException.class) | ||
| public ResponseEntity<ApiResponse<Void>> handleCustomRequestException(CustomRequestException e) { | ||
| log.error("CustomRequestException occurred: {}", e.getMessage(), e); // 로그 추가 | ||
| ApiResponse<Void> apiResponse = ApiResponse.of(e.getMessage(), e.getErrorCode()); | ||
| return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(apiResponse); | ||
| } | ||
|
|
||
| @ExceptionHandler(CustomServerException.class) | ||
| public ResponseEntity<ApiResponse<Void>> handleCustomServerException(CustomServerException e) { | ||
| log.error("CustomServerException occurred: {}", e.getMessage(), e); // 로그 추가 | ||
| ApiResponse<Void> apiResponse = ApiResponse.of(e.getMessage(), e.getErrorCode()); | ||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(apiResponse); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -39,7 +55,7 @@ public ResponseEntity<ApiResponse<Void>> handleRuntimeException(RuntimeException | |
| @ExceptionHandler(Exception.class) | ||
| public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception e) { | ||
| log.error("Unhandled exception occurred: {}", e.getMessage(), e); | ||
| ApiResponse<Void> apiResponse = ApiResponse.of("서버 내부에 오류가 발생했습니다.", HttpStatusCode.INTERNAL_SERVER_ERROR); | ||
| return ResponseEntity.status(HttpStatusCode.INTERNAL_SERVER_ERROR.getCode()).body(apiResponse); | ||
| ApiResponse<Void> apiResponse = ApiResponse.of("서버 내부에 오류가 발생했습니다.", ErrorCode.UNDEFINED_ERROR); | ||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(apiResponse); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,33 @@ | ||
| package com.hanghae.application.dto; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonInclude; | ||
| import com.hanghae.application.enums.ErrorCode; | ||
| import com.hanghae.application.enums.HttpStatusCode; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @JsonInclude(JsonInclude.Include.NON_NULL) //null값 JSON에서 제외 | ||
| public record ApiResponse<T> ( | ||
| String message, | ||
| HttpStatusCode status, | ||
| T data | ||
| boolean success, // 성공여부 | ||
| String message, // 응답메시지 | ||
| ErrorCode errorCode, // 오류 발생시 오류 코드 및 메시지 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. errorCode 보다는 HttpStatusCode 그대로 뒀어도 괜찮았을 것 같습니다. |
||
| T data // 응답 데이터 | ||
| ){ | ||
| public static <T> ApiResponse<T> of(String message, HttpStatusCode status) { | ||
| return new ApiResponse<>(message, status, null); | ||
| // 메시지만 응답 (데이터X, 오류코드 X) | ||
| public static <T> ApiResponse<T> of(boolean success, String message) { | ||
| return new ApiResponse<>(success, message, null, null); | ||
| } | ||
|
|
||
| public static <T> ApiResponse<T> of(String message, HttpStatusCode status, T data) { | ||
| // 오류시 응딥 (데이터 X) | ||
| public static <T> ApiResponse<T> of(String message, ErrorCode errorCode) { | ||
| return new ApiResponse<>(false, message, errorCode, null); | ||
| } | ||
|
|
||
| // 데이터 포함된 응답 (오류코드 X) | ||
| public static <T> ApiResponse<T> of(boolean success, String message, T data) { | ||
| if ((data == null || (data instanceof List<?> list && list.isEmpty())) && (message == null || message.isEmpty())) { | ||
| return new ApiResponse<>("조회된 결과가 없습니다.", HttpStatusCode.OK, null); | ||
| return new ApiResponse<>(success, "조회된 결과가 없습니다.", null, null); | ||
| } | ||
| return new ApiResponse<>(message, status, data); | ||
| return new ApiResponse<>(success, message, null, data); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package com.hanghae.application.enums; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonFormat; | ||
| import lombok.Getter; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Getter | ||
| @RequiredArgsConstructor | ||
| @JsonFormat(shape = JsonFormat.Shape.OBJECT) // 객체 형태로 변환 (응답받는 곳에서 코드값, 메시지 같이 보여짐) | ||
| public enum ErrorCode { | ||
| /** | ||
| * 코드 = 도메인 + 상세코드 | ||
| * SYS : 공통 | ||
| * MOV : 영화 | ||
| * RSV : 예매 | ||
| * ERR : 오류 | ||
| */ | ||
| SEARCH_REQUEST_BLOCKED("SYS-001", "너무 많은 요청으로 차단되었습니다."), | ||
| RATE_LIMIT_EXCEEDED("SYS-002", "일정 시간내 재요청이 불가합니다."), | ||
| SEAT_NOT_AVAILABLE("RSV-001", "해당 좌석은 예매할 수 없습니다."), | ||
| RESERVATION_LIMIT_EXCEEDED("RSV-002", "상영시간별 최대 예매 수를 초과 하였습니다."), | ||
| SEAT_ALREADY_RESERVED("RSV-003", "이미 예매된 좌석입니다."), | ||
| SEAT_NOT_FOUND("RSV-004", "선택할 수 없는 좌석입니다."), | ||
| UNDEFINED_ERROR("ERR-999", "서버 내부에 오류가 발생했습니다."); | ||
|
|
||
| private final String code; | ||
| private final String message; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.hanghae.application.exception; | ||
|
|
||
| import com.hanghae.application.enums.ErrorCode; | ||
| import lombok.Getter; | ||
|
|
||
| /** | ||
| * 요청 오류 Exception | ||
| * 400 응답위해 Exception 구분 | ||
| */ | ||
| @Getter | ||
| public class CustomRequestException extends RuntimeException { | ||
| private final String message; | ||
| private final ErrorCode errorCode; | ||
|
|
||
| public CustomRequestException(ErrorCode errorCode) { | ||
| super(errorCode.getMessage()); | ||
| this.errorCode = errorCode; | ||
| this.message = null; | ||
| } | ||
|
|
||
| public CustomRequestException(String message, ErrorCode errorCode) { | ||
| super(message != null ? message : errorCode.getMessage()); | ||
| this.errorCode = errorCode; | ||
| this.message = message; | ||
| } | ||
|
|
||
| public String getErrorMessage() { | ||
| return message != null ? message : errorCode.getMessage(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.hanghae.application.exception; | ||
|
|
||
| import com.hanghae.application.enums.ErrorCode; | ||
| import lombok.Getter; | ||
|
|
||
| /** | ||
| * 서버 내부 오류 Exception | ||
| * 500 응답위해 Exception 구분 | ||
| */ | ||
| @Getter | ||
| public class CustomServerException extends RuntimeException { | ||
| private final String message; | ||
| private final ErrorCode errorCode; | ||
|
|
||
| public CustomServerException(ErrorCode errorCode) { | ||
| super(errorCode.getMessage()); | ||
| this.errorCode = errorCode; | ||
| this.message = null; | ||
| } | ||
|
|
||
| public CustomServerException(String message, ErrorCode errorCode) { | ||
| super(message != null ? message : errorCode.getMessage()); | ||
| this.errorCode = errorCode; | ||
| this.message = message; | ||
| } | ||
|
|
||
| public String getErrorMessage() { | ||
| return message != null ? message : errorCode.getMessage(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,8 @@ | |
|
|
||
| import com.hanghae.application.dto.ApiResponse; | ||
| import com.hanghae.application.dto.request.MovieReservationRequestDto; | ||
| import com.hanghae.application.enums.HttpStatusCode; | ||
| import com.hanghae.application.enums.ErrorCode; | ||
| import com.hanghae.application.exception.CustomRequestException; | ||
| import com.hanghae.application.port.in.MovieReservationService; | ||
| import com.hanghae.application.port.out.message.MessagePort; | ||
| import com.hanghae.application.port.out.redis.RedisRateLimitPort; | ||
|
|
@@ -21,6 +22,7 @@ | |
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| @Service | ||
|
|
@@ -45,49 +47,64 @@ public ApiResponse<Void> saveMovieReservation(MovieReservationRequestDto request | |
|
|
||
| //동 시간대의 영화를 5분에 1번씩 예매 제한 | ||
| if (!redisRateLimitPort.canReserve(scheduleId, memberId)) { | ||
| return ApiResponse.of("동일 시간대 영화 예매는 5분 후 가능합니다.", HttpStatusCode.TOO_MANY_REQUESTS); | ||
| return ApiResponse.of("동일 시간대 영화 예매는 5분 후 가능합니다.", ErrorCode.RATE_LIMIT_EXCEEDED); | ||
| } | ||
|
|
||
| // 예매할 좌석 목록 가져오기 | ||
| List<ScreenSeat> selectedSeats = ScreenSeat.getSelectedConnectedSeats(screenSeat, seatCount); | ||
|
|
||
| try { | ||
| // 여러 좌석에 대해 Redisson 함수형 분산락 획득 | ||
| // 람다식(Supplier<T>)으로 받은 함수의 리턴값을 그대로 리턴 | ||
| return redissonLockPort.executeWithSeatsLocks(scheduleId, selectedSeats, () -> { | ||
| // 영화 스케줄 가져오기 | ||
| ScreeningSchedule screeningSchedule = screeningScheduleRepositoryPort.findById(scheduleId); | ||
| // 회원정보 조회 | ||
| Member member = memberRepositoryPort.findById(memberId); | ||
| // 선택 상영관 좌석 최대값 가져오기 | ||
| ScreenSeatLayout screenSeatLayout = screenSeatLayoutRepositoryPort.findBySeatRowAndScreenId(screenSeat.getSeatRow(), screeningSchedule.getScreen().getScreenId()); | ||
|
|
||
| // 해당 회원의 예매내역 조회 | ||
| int reservedTicketCount = ticketReservationRepositoryPort.countByScreeningScheduleIdAndMemberId(scheduleId, memberId); | ||
| // 여러 좌석에 대해 Redisson 함수형 분산락 획득 | ||
| // 람다식(Supplier<T>)으로 받은 함수의 리턴값을 그대로 리턴 | ||
| return redissonLockPort.executeWithSeatsLocks(scheduleId, selectedSeats, () -> { | ||
| // 영화 스케줄 가져오기 | ||
| ScreeningSchedule screeningSchedule = screeningScheduleRepositoryPort.findById(scheduleId); | ||
| // 회원정보 조회 | ||
| Member member = memberRepositoryPort.findById(memberId); | ||
| // 선택 상영관 좌석 최대값 가져오기 | ||
| ScreenSeatLayout screenSeatLayout = screenSeatLayoutRepositoryPort.findBySeatRowAndScreenId(screenSeat.getSeatRow(), screeningSchedule.getScreen().getScreenId()); | ||
|
|
||
| // 해당 회원의 예매내역 조회 | ||
| int reservedTicketCount = ticketReservationRepositoryPort.countByScreeningScheduleIdAndMemberId(scheduleId, memberId); | ||
|
|
||
| try { | ||
| // 예매 내역 5개 초과 검증 | ||
| reservationService.validateReservationSeatLimit(reservedTicketCount, seatCount); | ||
| } catch (IllegalArgumentException e) { // 도메인 계층에서 발생한 오류 발생시 별도 처리 | ||
| throw new CustomRequestException(e.getMessage(), ErrorCode.RESERVATION_LIMIT_EXCEEDED); | ||
| } | ||
|
|
||
| // 예매하려는 좌석에 대한 예매내역 조회 | ||
| int duplicateReservationCount = ticketReservationRepositoryPort.countByScheduleIdAndScreenSeats(scheduleId, selectedSeats); | ||
| // 예매하려는 좌석에 대한 예매내역 조회 | ||
| int duplicateReservationCount = ticketReservationRepositoryPort.countByScheduleIdAndScreenSeats(scheduleId, selectedSeats); | ||
|
|
||
| try { | ||
| // 예매 좌석 중복 내역 확인하기 | ||
| reservationService.validateSeatAvailability(duplicateReservationCount); | ||
| } catch (IllegalArgumentException e) { // 도메인 계층에서 발생한 오류 발생시 별도 처리 | ||
| throw new CustomRequestException(e.getMessage(), ErrorCode.SEAT_ALREADY_RESERVED); | ||
| } | ||
|
|
||
| List<TicketReservation> ticketReservations = new ArrayList<>(); | ||
| try { | ||
| // 예매 내역 생성 | ||
| List<TicketReservation> ticketReservations = reservationService.createTicketReservations(screeningSchedule, member, screenSeat, screenSeatLayout, seatCount); | ||
| ticketReservations = reservationService.createTicketReservations(screeningSchedule, member, screenSeat, screenSeatLayout, seatCount); | ||
| } catch (IllegalArgumentException e) { // 도메인 계층에서 발생한 오류 발생시 별도 처리 | ||
| throw new CustomRequestException(e.getMessage(), ErrorCode.SEAT_NOT_FOUND); | ||
| } | ||
|
|
||
| if(!ticketReservations.isEmpty()) { | ||
| // 예매 내역 저장 | ||
| ticketReservationRepositoryPort.saveMovieReservations(ticketReservations); | ||
|
|
||
| // 예매 성공 후 Redis 제한 설정 | ||
| redisRateLimitPort.setReservationLimit(scheduleId, memberId); | ||
|
|
||
| //완료 메시지 전송 (비동기) | ||
| messagePort.sendMessage("영화 예매가 완료 되었습니다."); | ||
| messagePort.sendMessage("영화 예매가 완료되었습니다."); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위, 아래의 메세지 포멧이 다른데 통일하고 상수로 빼는게 좋을 것 같습니다 |
||
|
|
||
| return ApiResponse.of("예매가 완료 되었습니다.", HttpStatusCode.CREATED); | ||
| }); | ||
| } catch (IllegalStateException e) { | ||
| return ApiResponse.of("현재 좌석을 다른 사용자가 예매 처리 중입니다.", HttpStatusCode.CONFLICT); | ||
| } | ||
| return ApiResponse.of(true, "예매가 완료되었습니다."); | ||
| } else { | ||
| return ApiResponse.of(false, "예매에 실패하였습니다."); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
domain 에서 customException 을 사용하는데 네이밍 자체도 헷갈리 수 있어서 저는 별도의 DomainException 을 하나 두는것도 괜찮을 것 같아요.
domain 계층에서 domainException , ErrorCode 를 두고, adapter 에서 domainException 을 핸들링 하는 식으로요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
의존성 최소화에만 초점이 되어있지만, 솔직히 불필요한 코드를 추가하지 않는 관점에서 보면 domain 계층에서 아예 사용하지 않는다는 과할 수 있을 것 같습니다.