From d7d48404ff74b2eb2eb48abe4161d7039440f817 Mon Sep 17 00:00:00 2001 From: kim keun tae Date: Sun, 9 Feb 2025 18:35:09 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EC=BD=94=EB=93=9C=20=EB=B0=8F=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +- .../hanghae/adapter/web/MovieController.java | 14 ++-- .../adapter/web/ReservationController.java | 11 +++- .../web/exception/GlobalExceptionHandler.java | 34 +++++++--- .../web/ReservationControllerTest.java | 2 +- .../hanghae/application/dto/ApiResponse.java | 25 ++++--- .../hanghae/application/enums/ErrorCode.java | 28 ++++++++ .../exception/CustomRequestException.java | 30 +++++++++ .../exception/CustomServerException.java | 30 +++++++++ .../service/MovieReservationServiceImpl.java | 65 ++++++++++++------- .../service/MovieScheduleServiceImpl.java | 9 +-- .../MovieReservationServiceImplTest.java | 24 +++---- .../adapter/redis/RedissonLockAdapter.java | 6 +- 13 files changed, 215 insertions(+), 68 deletions(-) create mode 100644 cinema-application/src/main/java/com/hanghae/application/enums/ErrorCode.java create mode 100644 cinema-application/src/main/java/com/hanghae/application/exception/CustomRequestException.java create mode 100644 cinema-application/src/main/java/com/hanghae/application/exception/CustomServerException.java diff --git a/README.md b/README.md index 60ab06801..3bfb2bf05 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,10 @@ > java 21 > Mysql > Redis - + +> ⚠ `domain`계층에서는 `CustomException`를 사용하지 않는다. ⚠ +> * 의존성 최소화를 위해 `domain`계층에서 발생한 오류는 `CustomException`를 사용하지 않고 `application`계층에서 처리 + ### Junit5 테스트 환경 * DB : H2 * Redis : Testcontainers diff --git a/cinema-adapter/src/main/java/com/hanghae/adapter/web/MovieController.java b/cinema-adapter/src/main/java/com/hanghae/adapter/web/MovieController.java index 0343baf61..5c7363dac 100644 --- a/cinema-adapter/src/main/java/com/hanghae/adapter/web/MovieController.java +++ b/cinema-adapter/src/main/java/com/hanghae/adapter/web/MovieController.java @@ -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>> getMovieSchedules() { ApiResponse> response = movieScheduleService.getMovieSchedules(); - //응답코드 일치시켜서 리턴 - return ResponseEntity.status(response.status().getCode()).body(response); + return ResponseEntity.status(HttpStatus.OK).body(response); } //영화별 상영 시간표 조회 (grouping) @@ -37,8 +37,11 @@ public ResponseEntity>> getSho ApiResponse> 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); // 조회 실패 + } } //redis 캐시삭제 (테스트용) @@ -46,7 +49,6 @@ public ResponseEntity>> getSho public ResponseEntity> evictCache() { ApiResponse response = movieScheduleService.evictShowingMovieCache(); - //응답코드 일치시켜서 리턴 - return ResponseEntity.status(response.status().getCode()).body(response); + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(response); } } diff --git a/cinema-adapter/src/main/java/com/hanghae/adapter/web/ReservationController.java b/cinema-adapter/src/main/java/com/hanghae/adapter/web/ReservationController.java index 332580890..a11be9b83 100644 --- a/cinema-adapter/src/main/java/com/hanghae/adapter/web/ReservationController.java +++ b/cinema-adapter/src/main/java/com/hanghae/adapter/web/ReservationController.java @@ -2,9 +2,11 @@ import com.hanghae.application.dto.ApiResponse; import com.hanghae.application.dto.request.MovieReservationRequestDto; +import com.hanghae.application.enums.ErrorCode; import com.hanghae.application.enums.HttpStatusCode; import com.hanghae.application.port.in.MovieReservationService; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -23,7 +25,12 @@ public class ReservationController { public ResponseEntity> saveMovieReservation(@RequestBody MovieReservationRequestDto requestDto) { ApiResponse response = movieReservationService.saveMovieReservation(requestDto); - //응답코드 일치시켜서 리턴 - return ResponseEntity.status(response.status().getCode()).body(response); + if(response.success()) { + return ResponseEntity.status(HttpStatus.CREATED).body(response); //조회 성공 + } else if(response.errorCode() == ErrorCode.RATE_LIMIT_EXCEEDED) { + return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(response); // 조회 실패 + } else { + return ResponseEntity.status(HttpStatus.CONFLICT).body(response); // 조회 실패 + } } } diff --git a/cinema-adapter/src/main/java/com/hanghae/adapter/web/exception/GlobalExceptionHandler.java b/cinema-adapter/src/main/java/com/hanghae/adapter/web/exception/GlobalExceptionHandler.java index baf5428d4..0cd1264e3 100644 --- a/cinema-adapter/src/main/java/com/hanghae/adapter/web/exception/GlobalExceptionHandler.java +++ b/cinema-adapter/src/main/java/com/hanghae/adapter/web/exception/GlobalExceptionHandler.java @@ -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> handleIllegalArgumentException(IllegalArgumentException e) { log.error("IllegalArgumentException occurred: {}", e.getMessage(), e); // 로그 추가 - ApiResponse apiResponse = ApiResponse.of(e.getMessage(), HttpStatusCode.BAD_REQUEST); - return ResponseEntity.status(HttpStatusCode.BAD_REQUEST.getCode()).body(apiResponse); + ApiResponse apiResponse = ApiResponse.of(false, e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(apiResponse); } @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { log.error("HttpMessageNotReadableException occurred: {}", e.getMessage(), e); // 로그 추가 - ApiResponse apiResponse = ApiResponse.of(e.getMessage(), HttpStatusCode.BAD_REQUEST); - return ResponseEntity.status(HttpStatusCode.BAD_REQUEST.getCode()).body(apiResponse); + ApiResponse apiResponse = ApiResponse.of(false, e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(apiResponse); } @ExceptionHandler(RuntimeException.class) public ResponseEntity> handleRuntimeException(RuntimeException e) { log.error("RuntimeException occurred: {}", e.getMessage(), e); // 로그 추가 - ApiResponse apiResponse = ApiResponse.of(e.getMessage(), HttpStatusCode.INTERNAL_SERVER_ERROR); - return ResponseEntity.status(HttpStatusCode.INTERNAL_SERVER_ERROR.getCode()).body(apiResponse); + ApiResponse apiResponse = ApiResponse.of(false, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(apiResponse); + } + + @ExceptionHandler(CustomRequestException.class) + public ResponseEntity> handleCustomRequestException(CustomRequestException e) { + log.error("CustomRequestException occurred: {}", e.getMessage(), e); // 로그 추가 + ApiResponse apiResponse = ApiResponse.of(e.getMessage(), e.getErrorCode()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(apiResponse); + } + + @ExceptionHandler(CustomServerException.class) + public ResponseEntity> handleCustomServerException(CustomServerException e) { + log.error("CustomServerException occurred: {}", e.getMessage(), e); // 로그 추가 + ApiResponse apiResponse = ApiResponse.of(e.getMessage(), e.getErrorCode()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(apiResponse); } /** @@ -39,7 +55,7 @@ public ResponseEntity> handleRuntimeException(RuntimeException @ExceptionHandler(Exception.class) public ResponseEntity> handleGenericException(Exception e) { log.error("Unhandled exception occurred: {}", e.getMessage(), e); - ApiResponse apiResponse = ApiResponse.of("서버 내부에 오류가 발생했습니다.", HttpStatusCode.INTERNAL_SERVER_ERROR); - return ResponseEntity.status(HttpStatusCode.INTERNAL_SERVER_ERROR.getCode()).body(apiResponse); + ApiResponse apiResponse = ApiResponse.of("서버 내부에 오류가 발생했습니다.", ErrorCode.UNDEFINED_ERROR); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(apiResponse); } } diff --git a/cinema-adapter/src/test/java/com/hanghae/adapter/web/ReservationControllerTest.java b/cinema-adapter/src/test/java/com/hanghae/adapter/web/ReservationControllerTest.java index 4bd4143f1..ce05b507e 100644 --- a/cinema-adapter/src/test/java/com/hanghae/adapter/web/ReservationControllerTest.java +++ b/cinema-adapter/src/test/java/com/hanghae/adapter/web/ReservationControllerTest.java @@ -82,7 +82,7 @@ void saveMovieReservationSuccess() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) .andExpect(status().isCreated()) - .andExpect(jsonPath("$.message").value("예매가 완료 되었습니다.")); + .andExpect(jsonPath("$.message").value("예매가 완료되었습니다.")); } @Test diff --git a/cinema-application/src/main/java/com/hanghae/application/dto/ApiResponse.java b/cinema-application/src/main/java/com/hanghae/application/dto/ApiResponse.java index d1bcaca66..0c62a6cb7 100644 --- a/cinema-application/src/main/java/com/hanghae/application/dto/ApiResponse.java +++ b/cinema-application/src/main/java/com/hanghae/application/dto/ApiResponse.java @@ -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 ( - String message, - HttpStatusCode status, - T data + boolean success, // 성공여부 + String message, // 응답메시지 + ErrorCode errorCode, // 오류 발생시 오류 코드 및 메시지 + T data // 응답 데이터 ){ - public static ApiResponse of(String message, HttpStatusCode status) { - return new ApiResponse<>(message, status, null); + // 메시지만 응답 (데이터X, 오류코드 X) + public static ApiResponse of(boolean success, String message) { + return new ApiResponse<>(success, message, null, null); } - public static ApiResponse of(String message, HttpStatusCode status, T data) { + // 오류시 응딥 (데이터 X) + public static ApiResponse of(String message, ErrorCode errorCode) { + return new ApiResponse<>(false, message, errorCode, null); + } + + // 데이터 포함된 응답 (오류코드 X) + public static ApiResponse 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); } } diff --git a/cinema-application/src/main/java/com/hanghae/application/enums/ErrorCode.java b/cinema-application/src/main/java/com/hanghae/application/enums/ErrorCode.java new file mode 100644 index 000000000..c254b61ec --- /dev/null +++ b/cinema-application/src/main/java/com/hanghae/application/enums/ErrorCode.java @@ -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; +} diff --git a/cinema-application/src/main/java/com/hanghae/application/exception/CustomRequestException.java b/cinema-application/src/main/java/com/hanghae/application/exception/CustomRequestException.java new file mode 100644 index 000000000..0cdf67947 --- /dev/null +++ b/cinema-application/src/main/java/com/hanghae/application/exception/CustomRequestException.java @@ -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(); + } +} diff --git a/cinema-application/src/main/java/com/hanghae/application/exception/CustomServerException.java b/cinema-application/src/main/java/com/hanghae/application/exception/CustomServerException.java new file mode 100644 index 000000000..3a9cbf49d --- /dev/null +++ b/cinema-application/src/main/java/com/hanghae/application/exception/CustomServerException.java @@ -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(); + } +} diff --git a/cinema-application/src/main/java/com/hanghae/application/service/MovieReservationServiceImpl.java b/cinema-application/src/main/java/com/hanghae/application/service/MovieReservationServiceImpl.java index 27d45d858..33930b323 100644 --- a/cinema-application/src/main/java/com/hanghae/application/service/MovieReservationServiceImpl.java +++ b/cinema-application/src/main/java/com/hanghae/application/service/MovieReservationServiceImpl.java @@ -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,36 +47,51 @@ public ApiResponse 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 selectedSeats = ScreenSeat.getSelectedConnectedSeats(screenSeat, seatCount); - try { - // 여러 좌석에 대해 Redisson 함수형 분산락 획득 - // 람다식(Supplier)으로 받은 함수의 리턴값을 그대로 리턴 - 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)으로 받은 함수의 리턴값을 그대로 리턴 + 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 ticketReservations = new ArrayList<>(); + try { // 예매 내역 생성 - List 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); @@ -82,12 +99,12 @@ public ApiResponse saveMovieReservation(MovieReservationRequestDto request redisRateLimitPort.setReservationLimit(scheduleId, memberId); //완료 메시지 전송 (비동기) - messagePort.sendMessage("영화 예매가 완료 되었습니다."); + messagePort.sendMessage("영화 예매가 완료되었습니다."); - return ApiResponse.of("예매가 완료 되었습니다.", HttpStatusCode.CREATED); - }); - } catch (IllegalStateException e) { - return ApiResponse.of("현재 좌석을 다른 사용자가 예매 처리 중입니다.", HttpStatusCode.CONFLICT); - } + return ApiResponse.of(true, "예매가 완료되었습니다."); + } else { + return ApiResponse.of(false, "예매에 실패하였습니다."); + } + }); } } diff --git a/cinema-application/src/main/java/com/hanghae/application/service/MovieScheduleServiceImpl.java b/cinema-application/src/main/java/com/hanghae/application/service/MovieScheduleServiceImpl.java index 34dadbffa..5856c9651 100644 --- a/cinema-application/src/main/java/com/hanghae/application/service/MovieScheduleServiceImpl.java +++ b/cinema-application/src/main/java/com/hanghae/application/service/MovieScheduleServiceImpl.java @@ -4,6 +4,7 @@ import com.hanghae.application.dto.request.MovieScheduleRequestDto; import com.hanghae.application.dto.response.MovieScheduleResponseDto; import com.hanghae.application.dto.response.ShowingMovieScheduleResponseDto; +import com.hanghae.application.enums.ErrorCode; import com.hanghae.application.enums.HttpStatusCode; import com.hanghae.application.port.in.MovieScheduleService; import com.hanghae.application.port.out.redis.RedisRateLimitPort; @@ -34,7 +35,7 @@ public ApiResponse> getMovieSchedules() { List schedules = screeningScheduleRepositoryPort.findAll(); List responseDtos = schedules.stream().map(this::convertToDto).collect(Collectors.toList()); - return ApiResponse.of("Success", HttpStatusCode.OK, responseDtos); + return ApiResponse.of(true, "Success", responseDtos); } @Override @@ -42,7 +43,7 @@ public ApiResponse> getMovieSchedules() { public ApiResponse> getShowingMovieSchedules(MovieScheduleRequestDto requestDto, String ip) { //1분에 50회 이상 조회시 조회 제한 if (!redisRateLimitPort.isAllowed(ip)) { - return ApiResponse.of("너무 많은 요청으로 조회가 차단되었습니다. ", HttpStatusCode.TOO_MANY_REQUESTS); + return ApiResponse.of("너무 많은 요청으로 조회가 차단되었습니다. ", ErrorCode.SEARCH_REQUEST_BLOCKED); } List projections = movieRepositoryPort.findShowingMovieSchedules(requestDto); @@ -80,13 +81,13 @@ public ApiResponse> getShowingMovieSchedul .build()); } - return ApiResponse.of("Success", HttpStatusCode.OK, new ArrayList<>(movieMap.values())); + return ApiResponse.of(true, "Success", new ArrayList<>(movieMap.values())); } @Override public ApiResponse evictShowingMovieCache() { movieRepositoryPort.evictShowingMovieCache(); - return ApiResponse.of("Success", HttpStatusCode.NO_CONTENT); + return ApiResponse.of(true, "캐시 삭제 완료."); } private MovieScheduleResponseDto convertToDto(ScreeningSchedule schedule) { diff --git a/cinema-application/src/test/java/com/hanghae/application/service/MovieReservationServiceImplTest.java b/cinema-application/src/test/java/com/hanghae/application/service/MovieReservationServiceImplTest.java index c9a201d88..a490c8d38 100644 --- a/cinema-application/src/test/java/com/hanghae/application/service/MovieReservationServiceImplTest.java +++ b/cinema-application/src/test/java/com/hanghae/application/service/MovieReservationServiceImplTest.java @@ -3,7 +3,8 @@ import com.hanghae.application.TestDataFactory; 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.out.message.MessagePort; import com.hanghae.application.port.out.redis.RedisRateLimitPort; import com.hanghae.application.port.out.redis.RedissonLockPort; @@ -92,8 +93,8 @@ void setUp() { void saveMovieReservationSuccess() { ApiResponse response = movieReservationService.saveMovieReservation(requestDto); - // 응답 코드 비교 - assertEquals(HttpStatusCode.CREATED, response.status()); + // 응답 성공 확인 + assertTrue(response.success()); //ticketReservationRepositoryPort.saveMovieReservations 메서드가 1회 호출 되었는지 확인 verify(ticketReservationRepositoryPort, times(1)).saveMovieReservations(anyList()); @@ -109,8 +110,8 @@ void saveMovieReservationTooManyRequests() { ApiResponse response = movieReservationService.saveMovieReservation(requestDto); - // 응답 코드 확인 - assertEquals(HttpStatusCode.TOO_MANY_REQUESTS, response.status()); + // 응답 실패 확인 + assertFalse(response.success()); //ticketReservationRepositoryPort.saveMovieReservations 메서드가 호출 되지 않았는지 확인 verify(ticketReservationRepositoryPort, never()).saveMovieReservations(anyList()); @@ -130,7 +131,7 @@ void saveMovieReservationSeatAlreadyReserved() { return null; }).when(reservationService).validateSeatAvailability(anyInt()); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> + CustomRequestException exception = assertThrows(CustomRequestException.class, () -> movieReservationService.saveMovieReservation(requestDto)); // 응답 메시지 비교 @@ -155,7 +156,7 @@ void saveMovieReservationSeatLimitExceeded() { }).when(reservationService).validateReservationSeatLimit(anyInt(), anyInt()); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> + CustomRequestException exception = assertThrows(CustomRequestException.class, () -> movieReservationService.saveMovieReservation(requestDto)); // 응답 메시지 확인 @@ -170,11 +171,12 @@ void saveMovieReservationLockFailed() { // executeWithSeatsLocks 예상 동작 다시 정의 when(redissonLockPort.executeWithSeatsLocks(anyLong(), anyList(), any())) - .thenThrow(new IllegalStateException("현재 좌석을 다른 사용자가 예매 처리 중입니다.")); + .thenThrow(new CustomRequestException("현재 좌석을 다른 사용자가 예매 처리 중입니다.", ErrorCode.SEAT_NOT_AVAILABLE)); - ApiResponse response = movieReservationService.saveMovieReservation(requestDto); + CustomRequestException exception = assertThrows(CustomRequestException.class, () -> + movieReservationService.saveMovieReservation(requestDto)); - // 응답 코드 확인 - assertEquals(HttpStatusCode.CONFLICT, response.status()); + // 응답 메시지 확인 + assertEquals("현재 좌석을 다른 사용자가 예매 처리 중입니다.", exception.getMessage()); } } diff --git a/cinema-infrastructure/src/main/java/com/hanghae/infrastructure/adapter/redis/RedissonLockAdapter.java b/cinema-infrastructure/src/main/java/com/hanghae/infrastructure/adapter/redis/RedissonLockAdapter.java index 11666f144..acc940450 100644 --- a/cinema-infrastructure/src/main/java/com/hanghae/infrastructure/adapter/redis/RedissonLockAdapter.java +++ b/cinema-infrastructure/src/main/java/com/hanghae/infrastructure/adapter/redis/RedissonLockAdapter.java @@ -1,5 +1,7 @@ package com.hanghae.infrastructure.adapter.redis; +import com.hanghae.application.enums.ErrorCode; +import com.hanghae.application.exception.CustomRequestException; import com.hanghae.application.port.out.redis.RedissonLockPort; import com.hanghae.domain.model.enums.ScreenSeat; import lombok.RequiredArgsConstructor; @@ -37,7 +39,7 @@ public T executeWithSeatLock(Long scheduleId, ScreenSeat seat, Supplier a releaseLock(lock); } } else { - throw new IllegalStateException("좌석에 대한 락을 획득할 수 없습니다: " + seat); + throw new CustomRequestException("현재 좌석을 다른 사용자가 예매 처리 중입니다." + seat, ErrorCode.SEAT_NOT_AVAILABLE); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -62,7 +64,7 @@ public T executeWithSeatsLocks(Long scheduleId, List seats, Supp releaseLocks(locks); } } else { - throw new IllegalStateException("현재 좌석을 다른 사용자가 예매 처리 중입니다." + seats); + throw new CustomRequestException("현재 좌석을 다른 사용자가 예매 처리 중입니다." + seats, ErrorCode.SEAT_NOT_AVAILABLE); } } catch (InterruptedException e) { Thread.currentThread().interrupt();