From db560637ffed6cc5432c396eaf3eac59d8d2ab8e Mon Sep 17 00:00:00 2001 From: RosieOh Date: Mon, 5 Oct 2026 02:32:44 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=97=86=EB=8A=94=20=EC=9E=90=EC=9B=90?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=EA=B0=80=20500=20=EC=9D=B4=20=EC=95=84?= =?UTF-8?q?=EB=8B=88=EB=9D=BC=20404=20=EA=B0=80=20=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CareServiceException 의 기본 매핑은 500 이다(핸들러가 아는 코드가 아니면 INTERNAL_SERVER_ERROR). 그런데 서비스들이 '찾을 수 없습니다' 를 이 예외로 던지는 곳이 34군데였다. 없는 시설 ID 로 입소 예측·인기도·대기 통계를 조회하면 500 이 나가고 운영 알림까지 울렸다. 클라이언트는 '내가 잘못 보냈다' 와 '서버가 고장났다' 를 구분할 수 없고, 운영 문서가 경계하는 바로 그 상황이 된다 — 봇이 긁고 가면 알림이 울리고 진짜 장애가 그 소음에 묻힌다. - 34곳(15개 파일)을 ResourceNotFoundException(404 매핑)으로 교체 - 더 이상 쓰지 않게 된 CareServiceException import 정리 - ClientErrorStatusTest 에 '없는 자원은 404, 운영 알림 없음' 추가 ResourceNotFoundException 은 CareCodeException 을 상속하고, HealthService 같은 곳의 catch(Exception) 래핑보다 앞선 catch(CareCodeException) 에서 재던져지므로 래핑에 먹히지 않는다. 그 사실도 테스트가 함께 검증한다(통과 전에는 expected 404 but was 500 이었다). --- .../AdminPolicyVerificationController.java | 6 ++--- .../CareFacilityBookingAdminService.java | 8 +++---- .../chatbot/service/ChatbotService.java | 5 +++-- .../community/service/CommunityService.java | 2 +- .../service/AdmissionForecastService.java | 4 ++-- .../service/FacilityPopularityService.java | 4 ++-- .../service/FacilityWaitlistService.java | 7 +++--- .../domain/health/service/HealthService.java | 9 ++++---- .../notification/app/NotificationFacade.java | 4 ++-- .../NotificationLinkController.java | 4 ++-- .../NotificationPreferenceService.java | 17 +++++++------- .../service/NotificationService.java | 7 +++--- .../controller/BenefitLinkController.java | 3 ++- .../service/BenefitAmountReportService.java | 6 ++--- .../RegionalBenefitComparisonService.java | 3 ++- .../integration/ClientErrorStatusTest.java | 22 +++++++++++++++++++ 16 files changed, 70 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/carecode/domain/admin/controller/AdminPolicyVerificationController.java b/src/main/java/com/carecode/domain/admin/controller/AdminPolicyVerificationController.java index 73d774e4..be07dd99 100644 --- a/src/main/java/com/carecode/domain/admin/controller/AdminPolicyVerificationController.java +++ b/src/main/java/com/carecode/domain/admin/controller/AdminPolicyVerificationController.java @@ -1,6 +1,6 @@ package com.carecode.domain.admin.controller; -import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.core.security.CurrentUserFacade; import com.carecode.domain.policy.entity.Policy; import com.carecode.domain.policy.repository.PolicyRepository; @@ -39,7 +39,7 @@ public ResponseEntity> verify( @Parameter(description = "금액 근거 출처 URL") @RequestParam(required = false) String sourceUrl) { Policy policy = policyRepository.findById(policyId) - .orElseThrow(() -> new CareServiceException("정책을 찾을 수 없습니다: " + policyId)); + .orElseThrow(() -> new ResourceNotFoundException("정책을 찾을 수 없습니다: " + policyId)); policy.setVerifiedAt(LocalDateTime.now()); policy.setVerifiedBy(currentUserFacade.requireCurrentUserEmail()); @@ -59,7 +59,7 @@ public ResponseEntity> verify( @Operation(summary = "검증 표시 해제", description = "추정치로 되돌림") public ResponseEntity unverify(@PathVariable Long policyId) { Policy policy = policyRepository.findById(policyId) - .orElseThrow(() -> new CareServiceException("정책을 찾을 수 없습니다: " + policyId)); + .orElseThrow(() -> new ResourceNotFoundException("정책을 찾을 수 없습니다: " + policyId)); policy.setVerifiedAt(null); policy.setVerifiedBy(null); policyRepository.save(policy); diff --git a/src/main/java/com/carecode/domain/admin/service/CareFacilityBookingAdminService.java b/src/main/java/com/carecode/domain/admin/service/CareFacilityBookingAdminService.java index 074a1f26..5019cb04 100644 --- a/src/main/java/com/carecode/domain/admin/service/CareFacilityBookingAdminService.java +++ b/src/main/java/com/carecode/domain/admin/service/CareFacilityBookingAdminService.java @@ -1,7 +1,7 @@ package com.carecode.domain.admin.service; import com.carecode.core.annotation.LogExecutionTime; -import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.domain.admin.dto.AdminBookingDetailResponse; import com.carecode.domain.admin.dto.AdminBookingListResponse; import com.carecode.domain.facility.dto.response.StatusDistribution; @@ -129,7 +129,7 @@ public AdminBookingSearchResponse searchBookings(AdminBookingSearchRequest reque @LogExecutionTime public AdminBookingDetailResponse getBookingDetail(Long bookingId) { CareFacilityBooking booking = bookingRepository.findById(bookingId) - .orElseThrow(() -> new CareServiceException("예약을 찾을 수 없습니다: " + bookingId)); + .orElseThrow(() -> new ResourceNotFoundException("예약을 찾을 수 없습니다: " + bookingId)); return convertToAdminDetailResponse(booking); } @@ -140,7 +140,7 @@ public AdminBookingDetailResponse getBookingDetail(Long bookingId) { public AdminBookingDetailResponse updateBookingStatus(Long bookingId, AdminStatusUpdateRequest request) { CareFacilityBooking booking = bookingRepository.findById(bookingId) - .orElseThrow(() -> new CareServiceException("예약을 찾을 수 없습니다: " + bookingId)); + .orElseThrow(() -> new ResourceNotFoundException("예약을 찾을 수 없습니다: " + bookingId)); // 없는 상태면 valueOf 가 IllegalArgumentException 을 던져 500 이 됐다. 400 으로 돌려준다. CareFacilityBooking.BookingStatus newStatus; @@ -168,7 +168,7 @@ public AdminBookingDetailResponse updateBookingStatus(Long bookingId, @Transactional public void deleteBooking(Long bookingId) { CareFacilityBooking booking = bookingRepository.findById(bookingId) - .orElseThrow(() -> new CareServiceException("예약을 찾을 수 없습니다: " + bookingId)); + .orElseThrow(() -> new ResourceNotFoundException("예약을 찾을 수 없습니다: " + bookingId)); bookingRepository.delete(booking); } diff --git a/src/main/java/com/carecode/domain/chatbot/service/ChatbotService.java b/src/main/java/com/carecode/domain/chatbot/service/ChatbotService.java index d0c84cea..cd014dfc 100644 --- a/src/main/java/com/carecode/domain/chatbot/service/ChatbotService.java +++ b/src/main/java/com/carecode/domain/chatbot/service/ChatbotService.java @@ -2,6 +2,7 @@ import com.carecode.core.annotation.LogExecutionTime; import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.domain.chatbot.dto.request.ChatbotMessageRequest; import com.carecode.domain.chatbot.dto.response.ChatbotMessageResponse; import com.carecode.domain.chatbot.dto.response.ChatbotChatHistoryDtoResponse; @@ -205,7 +206,7 @@ public void processFeedback(Long messageId, boolean isHelpful) { try { ChatMessage message = chatMessageRepository.findById(messageId) - .orElseThrow(() -> new CareServiceException("메시지를 찾을 수 없습니다.")); + .orElseThrow(() -> new ResourceNotFoundException("메시지를 찾을 수 없습니다.")); message.setIsHelpful(isHelpful); chatMessageRepository.save(message); @@ -379,7 +380,7 @@ private String generateSessionId() { private User resolveUser(String userIdOrEmail) { return userRepository.findByUserId(userIdOrEmail) .or(() -> userRepository.findByEmail(userIdOrEmail)) - .orElseThrow(() -> new CareServiceException("사용자를 찾을 수 없습니다: " + userIdOrEmail)); + .orElseThrow(() -> new ResourceNotFoundException("사용자를 찾을 수 없습니다: " + userIdOrEmail)); } // 의도 분석 diff --git a/src/main/java/com/carecode/domain/community/service/CommunityService.java b/src/main/java/com/carecode/domain/community/service/CommunityService.java index e6dfd3f3..bc55df08 100644 --- a/src/main/java/com/carecode/domain/community/service/CommunityService.java +++ b/src/main/java/com/carecode/domain/community/service/CommunityService.java @@ -357,7 +357,7 @@ private User getCurrentUser() { if (authentication == null || !authentication.isAuthenticated() || "anonymousUser".equals(authentication.getName())) { log.warn("getCurrentUser() - 인증되지 않은 사용자"); - throw new CareServiceException("인증된 사용자 정보를 찾을 수 없습니다."); + throw new ResourceNotFoundException("인증된 사용자 정보를 찾을 수 없습니다."); } String userEmail = authentication.getName(); diff --git a/src/main/java/com/carecode/domain/facility/service/AdmissionForecastService.java b/src/main/java/com/carecode/domain/facility/service/AdmissionForecastService.java index 590cdc98..f32135ef 100644 --- a/src/main/java/com/carecode/domain/facility/service/AdmissionForecastService.java +++ b/src/main/java/com/carecode/domain/facility/service/AdmissionForecastService.java @@ -2,7 +2,7 @@ import com.carecode.core.analytics.EventLogger; import com.carecode.core.analytics.EventType; -import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.domain.facility.dto.response.AdmissionForecastResponse; import com.carecode.domain.facility.entity.CareFacility; import com.carecode.domain.facility.entity.FacilityCapacitySnapshot; @@ -41,7 +41,7 @@ public class AdmissionForecastService { /** 아이 월령 기준으로 목표 시점까지 자리가 날 확률을 추정한다. */ public AdmissionForecastResponse forecast(Long facilityId, Integer childAgeMonths, Integer horizonMonths) { CareFacility facility = careFacilityRepository.findById(facilityId) - .orElseThrow(() -> new CareServiceException("시설을 찾을 수 없습니다: " + facilityId)); + .orElseThrow(() -> new ResourceNotFoundException("시설을 찾을 수 없습니다: " + facilityId)); LocalDate today = LocalDate.now(); int horizon = horizonMonths != null && horizonMonths > 0 ? horizonMonths : DEFAULT_HORIZON_MONTHS; diff --git a/src/main/java/com/carecode/domain/facility/service/FacilityPopularityService.java b/src/main/java/com/carecode/domain/facility/service/FacilityPopularityService.java index cb5350c9..d61fa0d3 100644 --- a/src/main/java/com/carecode/domain/facility/service/FacilityPopularityService.java +++ b/src/main/java/com/carecode/domain/facility/service/FacilityPopularityService.java @@ -2,7 +2,7 @@ import com.carecode.core.analytics.EventLogger; import com.carecode.core.analytics.EventType; -import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.domain.facility.dto.response.FacilityPopularityResponse; import com.carecode.domain.facility.entity.CareFacility; import com.carecode.domain.facility.entity.FacilityCapacitySnapshot; @@ -45,7 +45,7 @@ public class FacilityPopularityService { public FacilityPopularityResponse analyze(Long facilityId) { CareFacility facility = careFacilityRepository.findById(facilityId) - .orElseThrow(() -> new CareServiceException("시설을 찾을 수 없습니다: " + facilityId)); + .orElseThrow(() -> new ResourceNotFoundException("시설을 찾을 수 없습니다: " + facilityId)); eventLogger.log(EventType.FACILITY_POPULARITY_VIEWED, null, String.valueOf(facilityId)); diff --git a/src/main/java/com/carecode/domain/facility/service/FacilityWaitlistService.java b/src/main/java/com/carecode/domain/facility/service/FacilityWaitlistService.java index 0b79a51b..320fe6ff 100644 --- a/src/main/java/com/carecode/domain/facility/service/FacilityWaitlistService.java +++ b/src/main/java/com/carecode/domain/facility/service/FacilityWaitlistService.java @@ -3,6 +3,7 @@ import com.carecode.core.analytics.EventLogger; import com.carecode.core.analytics.EventType; import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.core.security.CurrentUserFacade; import com.carecode.domain.facility.dto.request.WaitlistRequest; import com.carecode.domain.facility.dto.response.WaitlistStatsResponse; @@ -45,7 +46,7 @@ public class FacilityWaitlistService { public Long register(Long facilityId, WaitlistRequest request) { User user = currentUserFacade.requireCurrentUser(); CareFacility facility = facilityRepository.findById(facilityId) - .orElseThrow(() -> new CareServiceException("시설을 찾을 수 없습니다: " + facilityId)); + .orElseThrow(() -> new ResourceNotFoundException("시설을 찾을 수 없습니다: " + facilityId)); Child child = resolveOwnChild(user, request.getChildId()); // 같은 아이·같은 시설의 중복 등록은 통계를 왜곡하므로 기존 기록을 그대로 돌려준다. @@ -73,7 +74,7 @@ public Long register(Long facilityId, WaitlistRequest request) { public void resolve(Long waitlistId, String status, LocalDate resolvedAt, String note) { User user = currentUserFacade.requireCurrentUser(); FacilityWaitlist entry = waitlistRepository.findById(waitlistId) - .orElseThrow(() -> new CareServiceException("대기 기록을 찾을 수 없습니다: " + waitlistId)); + .orElseThrow(() -> new ResourceNotFoundException("대기 기록을 찾을 수 없습니다: " + waitlistId)); if (!entry.getUser().getId().equals(user.getId())) { throw new CareServiceException("본인의 대기 기록만 수정할 수 있습니다."); @@ -90,7 +91,7 @@ public List getMyWaitlists() { @Transactional(readOnly = true) public WaitlistStatsResponse getStats(Long facilityId) { CareFacility facility = facilityRepository.findById(facilityId) - .orElseThrow(() -> new CareServiceException("시설을 찾을 수 없습니다: " + facilityId)); + .orElseThrow(() -> new ResourceNotFoundException("시설을 찾을 수 없습니다: " + facilityId)); List admitted = waitlistRepository.findAdmitted(facilityId); long waiting = waitlistRepository.countWaiting(facilityId); diff --git a/src/main/java/com/carecode/domain/health/service/HealthService.java b/src/main/java/com/carecode/domain/health/service/HealthService.java index d57ef1b7..ebc0dbe8 100644 --- a/src/main/java/com/carecode/domain/health/service/HealthService.java +++ b/src/main/java/com/carecode/domain/health/service/HealthService.java @@ -5,6 +5,7 @@ import com.carecode.core.annotation.LogExecutionTime; import com.carecode.core.exception.CareCodeException; import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.core.exception.UserNotFoundException; import com.carecode.core.exception.HealthRecordNotFoundException; import com.carecode.core.exception.ChildNotFoundException; @@ -173,7 +174,7 @@ public HealthRecordResponse updateHealthRecord(Long recordId, HealthUpdateHealth validateUpdateRequest(request); HealthRecord record = healthRecordRepository.findById(recordId) - .orElseThrow(() -> new CareServiceException("건강 기록을 찾을 수 없습니다: " + recordId)); + .orElseThrow(() -> new ResourceNotFoundException("건강 기록을 찾을 수 없습니다: " + recordId)); assertHealthRecordOwnedByUser(record, actorUserId); // 기록 업데이트 @@ -301,7 +302,7 @@ public Map analyzeHealthStatus(HealthCreateHealthRecordRequest r Long childId = Long.valueOf(request.getChildId()); Child child = childRepository.findById(childId) - .orElseThrow(() -> new CareServiceException("아동을 찾을 수 없습니다: " + request.getChildId())); + .orElseThrow(() -> new ResourceNotFoundException("아동을 찾을 수 없습니다: " + request.getChildId())); assertChildOwnedByUser(child, actorUserId); List records = healthRecordRepository.findByChildOrderByRecordDateDesc(child); @@ -324,7 +325,7 @@ public Map generateHealthReport(HealthCreateHealthRecordRequest Long childId = Long.valueOf(request.getChildId()); Child child = childRepository.findById(childId) - .orElseThrow(() -> new CareServiceException("아동을 찾을 수 없습니다: " + request.getChildId())); + .orElseThrow(() -> new ResourceNotFoundException("아동을 찾을 수 없습니다: " + request.getChildId())); assertChildOwnedByUser(child, actorUserId); List records = healthRecordRepository.findByChildOrderByRecordDateDesc(child); @@ -481,7 +482,7 @@ public List getAttachments(Long recordId, Long a @Transactional public void deleteAttachment(Long attachmentId, Long actorUserId) { HealthRecordAttachment attachment = healthRecordAttachmentRepository.findById(attachmentId) - .orElseThrow(() -> new CareServiceException("첨부파일을 찾을 수 없습니다: " + attachmentId)); + .orElseThrow(() -> new ResourceNotFoundException("첨부파일을 찾을 수 없습니다: " + attachmentId)); assertHealthRecordOwnedByUser(attachment.getHealthRecord(), actorUserId); attachment.deactivate(); healthRecordAttachmentRepository.save(attachment); diff --git a/src/main/java/com/carecode/domain/notification/app/NotificationFacade.java b/src/main/java/com/carecode/domain/notification/app/NotificationFacade.java index 9fce3f9d..c70d9028 100644 --- a/src/main/java/com/carecode/domain/notification/app/NotificationFacade.java +++ b/src/main/java/com/carecode/domain/notification/app/NotificationFacade.java @@ -11,7 +11,7 @@ import com.carecode.domain.notification.dto.response.NotificationStatsResponse; import com.carecode.domain.notification.dto.response.NotificationTemplateResponse; import com.carecode.domain.notification.dto.response.NotificationDeliveryStatusResponse; -import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.domain.notification.entity.Notification; import com.carecode.domain.notification.repository.NotificationPreferenceRepository; import com.carecode.domain.notification.sender.NotificationChannelType; @@ -49,7 +49,7 @@ public class NotificationFacade { @Transactional(readOnly = true) public List getChannelStatuses(String userId) { User user = userRepository.findByUserId(userId) - .orElseThrow(() -> new CareServiceException("사용자를 찾을 수 없습니다: " + userId)); + .orElseThrow(() -> new ResourceNotFoundException("사용자를 찾을 수 없습니다: " + userId)); return Arrays.stream(NotificationChannelType.values()) .map(channel -> toChannelStatus(channel, user)) diff --git a/src/main/java/com/carecode/domain/notification/controller/NotificationLinkController.java b/src/main/java/com/carecode/domain/notification/controller/NotificationLinkController.java index 38685229..3173a071 100644 --- a/src/main/java/com/carecode/domain/notification/controller/NotificationLinkController.java +++ b/src/main/java/com/carecode/domain/notification/controller/NotificationLinkController.java @@ -2,7 +2,7 @@ import com.carecode.core.analytics.EventLogger; import com.carecode.core.analytics.EventType; -import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.core.security.CurrentUserFacade; import com.carecode.domain.notification.entity.Notification; import com.carecode.domain.notification.repository.NotificationRepository; @@ -54,7 +54,7 @@ public ResponseEntity open( @Parameter(description = "대상 식별자") @RequestParam(required = false) String targetId) { Notification notification = notificationRepository.findById(notificationId) - .orElseThrow(() -> new CareServiceException("알림을 찾을 수 없습니다: " + notificationId)); + .orElseThrow(() -> new ResourceNotFoundException("알림을 찾을 수 없습니다: " + notificationId)); Long userId = currentUserIdOrNull(); // 본인 알림만 열람 표시를 남긴다. 남의 알림 ID 로 읽음 처리되면 안 된다. diff --git a/src/main/java/com/carecode/domain/notification/service/NotificationPreferenceService.java b/src/main/java/com/carecode/domain/notification/service/NotificationPreferenceService.java index 4b99cbea..879823d1 100644 --- a/src/main/java/com/carecode/domain/notification/service/NotificationPreferenceService.java +++ b/src/main/java/com/carecode/domain/notification/service/NotificationPreferenceService.java @@ -2,6 +2,7 @@ import com.carecode.core.annotation.LogExecutionTime; import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.domain.notification.dto.request.NotificationRegisterPushTokenRequest; import com.carecode.domain.notification.dto.request.NotificationUpdateSettingsRequest; import com.carecode.domain.notification.dto.response.NotificationSettingsResponse; @@ -41,7 +42,7 @@ public List getUserPreferences(String userId) { try { User user = userRepository.findByUserId(userId) - .orElseThrow(() -> new CareServiceException("사용자를 찾을 수 없습니다: " + userId)); + .orElseThrow(() -> new ResourceNotFoundException("사용자를 찾을 수 없습니다: " + userId)); List preferences = preferenceRepository.findByUserOrderByNotificationType(user); @@ -61,7 +62,7 @@ public NotificationSettingsResponse getPreferenceByType(String userId, Notificat try { User user = userRepository.findByUserId(userId) - .orElseThrow(() -> new CareServiceException("사용자를 찾을 수 없습니다: " + userId)); + .orElseThrow(() -> new ResourceNotFoundException("사용자를 찾을 수 없습니다: " + userId)); Optional preference = preferenceRepository.findByUserAndNotificationType(user, notificationType); @@ -81,7 +82,7 @@ public NotificationSettingsResponse savePreference(String userId, NotificationSe try { User user = userRepository.findByUserId(userId) - .orElseThrow(() -> new CareServiceException("사용자를 찾을 수 없습니다: " + userId)); + .orElseThrow(() -> new ResourceNotFoundException("사용자를 찾을 수 없습니다: " + userId)); NotificationPreference preference = preferenceRepository .findByUserAndNotificationType(user, Notification.NotificationType.valueOf(preferenceDto.getNotificationType())) @@ -106,7 +107,7 @@ public NotificationSettingsResponse updateChannelPreference(String userId, Strin try { User user = userRepository.findByUserId(userId) - .orElseThrow(() -> new CareServiceException("사용자를 찾을 수 없습니다: " + userId)); + .orElseThrow(() -> new ResourceNotFoundException("사용자를 찾을 수 없습니다: " + userId)); NotificationPreference preference = preferenceRepository .findByUserAndNotificationType(user, Notification.NotificationType.valueOf(notificationType)) @@ -136,7 +137,7 @@ public void disableAllNotifications(String userId) { try { User user = userRepository.findByUserId(userId) - .orElseThrow(() -> new CareServiceException("사용자를 찾을 수 없습니다: " + userId)); + .orElseThrow(() -> new ResourceNotFoundException("사용자를 찾을 수 없습니다: " + userId)); Map stored = preferenceRepository.findByUserOrderByNotificationType(user).stream() @@ -167,7 +168,7 @@ public void resetToDefault(String userId) { try { User user = userRepository.findByUserId(userId) - .orElseThrow(() -> new CareServiceException("사용자를 찾을 수 없습니다: " + userId)); + .orElseThrow(() -> new ResourceNotFoundException("사용자를 찾을 수 없습니다: " + userId)); // 기존 설정 삭제 List existingPreferences = preferenceRepository.findByUserOrderByNotificationType(user); @@ -281,7 +282,7 @@ private NotificationSettingsResponse convertToDto(NotificationPreference prefere @Transactional public void registerPushToken(String userId, NotificationRegisterPushTokenRequest request) { User user = userRepository.findByUserId(userId) - .orElseThrow(() -> new CareServiceException("사용자를 찾을 수 없습니다: " + userId)); + .orElseThrow(() -> new ResourceNotFoundException("사용자를 찾을 수 없습니다: " + userId)); // 기존 설정이 있는지 확인 Optional existingPreference = preferenceRepository @@ -342,7 +343,7 @@ private void registerDevice(User user, NotificationRegisterPushTokenRequest requ @Transactional public void updateSettings(String userId, NotificationUpdateSettingsRequest request) { User user = userRepository.findByUserId(userId) - .orElseThrow(() -> new CareServiceException("사용자를 찾을 수 없습니다: " + userId)); + .orElseThrow(() -> new ResourceNotFoundException("사용자를 찾을 수 없습니다: " + userId)); // 각 알림 타입별로 설정 업데이트 for (Notification.NotificationType type : Notification.NotificationType.values()) { diff --git a/src/main/java/com/carecode/domain/notification/service/NotificationService.java b/src/main/java/com/carecode/domain/notification/service/NotificationService.java index 607939a8..a44beb12 100644 --- a/src/main/java/com/carecode/domain/notification/service/NotificationService.java +++ b/src/main/java/com/carecode/domain/notification/service/NotificationService.java @@ -3,6 +3,7 @@ import com.carecode.core.annotation.LogExecutionTime; import com.carecode.core.exception.BusinessException; import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.core.exception.ErrorCode; import com.carecode.domain.notification.dto.request.NotificationCreateRequest; import com.carecode.domain.notification.dto.request.NotificationMarkAsReadRequest; @@ -323,7 +324,7 @@ public void markAsRead(NotificationMarkAsReadRequest request, String actorUserId @Transactional public void sendTestNotification(String userId, NotificationSendTestRequest request) { User user = userRepository.findByUserId(userId) - .orElseThrow(() -> new CareServiceException("사용자를 찾을 수 없습니다: " + userId)); + .orElseThrow(() -> new ResourceNotFoundException("사용자를 찾을 수 없습니다: " + userId)); Notification notification = Notification.builder() .user(user) @@ -380,7 +381,7 @@ public long getNotificationCountByReadStatus(Long userId, boolean isRead) { @Transactional(readOnly = true) public NotificationStatsResponse getNotificationStats(String userId) { User user = userRepository.findByUserId(userId) - .orElseThrow(() -> new CareServiceException("사용자를 찾을 수 없습니다: " + userId)); + .orElseThrow(() -> new ResourceNotFoundException("사용자를 찾을 수 없습니다: " + userId)); long totalCount = notificationRepository.countByUserId(user.getId()); long unreadCount = notificationRepository.countByUserIdAndIsRead(user.getId(), false); @@ -439,7 +440,7 @@ public List getNotificationTemplates(String type) @Transactional(readOnly = true) public NotificationDeliveryStatusResponse getDeliveryStatus(Long notificationId, String actorUserId) { Notification notification = notificationRepository.findById(notificationId) - .orElseThrow(() -> new CareServiceException("알림을 찾을 수 없습니다: " + notificationId)); + .orElseThrow(() -> new ResourceNotFoundException("알림을 찾을 수 없습니다: " + notificationId)); assertNotificationOwnedByUser(notification, actorUserId); return NotificationDeliveryStatusResponse.builder() diff --git a/src/main/java/com/carecode/domain/policy/controller/BenefitLinkController.java b/src/main/java/com/carecode/domain/policy/controller/BenefitLinkController.java index 400251a3..c7f556b1 100644 --- a/src/main/java/com/carecode/domain/policy/controller/BenefitLinkController.java +++ b/src/main/java/com/carecode/domain/policy/controller/BenefitLinkController.java @@ -3,6 +3,7 @@ import com.carecode.core.analytics.EventLogger; import com.carecode.core.analytics.EventType; import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.core.security.CurrentUserFacade; import com.carecode.domain.policy.entity.Policy; import com.carecode.domain.policy.repository.PolicyRepository; @@ -41,7 +42,7 @@ public ResponseEntity apply( @Parameter(description = "정책 ID", required = true) @PathVariable Long policyId) { Policy policy = policyRepository.findById(policyId) - .orElseThrow(() -> new CareServiceException("정책을 찾을 수 없습니다: " + policyId)); + .orElseThrow(() -> new ResourceNotFoundException("정책을 찾을 수 없습니다: " + policyId)); String url = policy.getApplicationUrl(); if (url == null || url.isBlank()) { diff --git a/src/main/java/com/carecode/domain/policy/service/BenefitAmountReportService.java b/src/main/java/com/carecode/domain/policy/service/BenefitAmountReportService.java index e11fc693..6d56d1ae 100644 --- a/src/main/java/com/carecode/domain/policy/service/BenefitAmountReportService.java +++ b/src/main/java/com/carecode/domain/policy/service/BenefitAmountReportService.java @@ -2,7 +2,7 @@ import com.carecode.core.analytics.EventLogger; import com.carecode.core.analytics.EventType; -import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.core.security.CurrentUserFacade; import com.carecode.domain.policy.dto.request.BenefitAmountReportRequest; import com.carecode.domain.policy.dto.response.BenefitAmountConsensusResponse; @@ -41,7 +41,7 @@ public class BenefitAmountReportService { @Transactional public BenefitAmountConsensusResponse report(Long policyId, BenefitAmountReportRequest request) { Policy policy = policyRepository.findById(policyId) - .orElseThrow(() -> new CareServiceException("정책을 찾을 수 없습니다: " + policyId)); + .orElseThrow(() -> new ResourceNotFoundException("정책을 찾을 수 없습니다: " + policyId)); User user = currentUserFacade.requireCurrentUser(); BenefitAmountReport.PaymentType type = @@ -69,7 +69,7 @@ public BenefitAmountConsensusResponse report(Long policyId, BenefitAmountReportR @Transactional(readOnly = true) public BenefitAmountConsensusResponse getConsensus(Long policyId) { Policy policy = policyRepository.findById(policyId) - .orElseThrow(() -> new CareServiceException("정책을 찾을 수 없습니다: " + policyId)); + .orElseThrow(() -> new ResourceNotFoundException("정책을 찾을 수 없습니다: " + policyId)); return buildResponse(policy, findTopReport(policyId)); } diff --git a/src/main/java/com/carecode/domain/policy/service/RegionalBenefitComparisonService.java b/src/main/java/com/carecode/domain/policy/service/RegionalBenefitComparisonService.java index 7b3c9d6f..4ce69d2c 100644 --- a/src/main/java/com/carecode/domain/policy/service/RegionalBenefitComparisonService.java +++ b/src/main/java/com/carecode/domain/policy/service/RegionalBenefitComparisonService.java @@ -3,6 +3,7 @@ import com.carecode.domain.policy.benefit.BenefitPaymentType; import com.carecode.domain.policy.benefit.BenefitProjectionCalculator; import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.core.analytics.EventLogger; import com.carecode.core.analytics.EventType; import com.carecode.core.security.CurrentUserFacade; @@ -228,7 +229,7 @@ private Child resolveChild(List children, Long childId) { } Child child = childId == null ? children.get(0) : children.stream().filter(c -> c.getId().equals(childId)).findFirst() - .orElseThrow(() -> new CareServiceException("자녀를 찾을 수 없습니다: " + childId)); + .orElseThrow(() -> new ResourceNotFoundException("자녀를 찾을 수 없습니다: " + childId)); if (child.getBirthDate() == null) { throw new CareServiceException("자녀의 생년월일이 없어 지원금을 계산할 수 없습니다."); diff --git a/src/test/java/com/carecode/integration/ClientErrorStatusTest.java b/src/test/java/com/carecode/integration/ClientErrorStatusTest.java index 957669c7..59a17e34 100644 --- a/src/test/java/com/carecode/integration/ClientErrorStatusTest.java +++ b/src/test/java/com/carecode/integration/ClientErrorStatusTest.java @@ -71,6 +71,28 @@ class ClientErrorStatusTest { @Autowired MockMvc mockMvc; + /** + * 없는 자원은 404 다. + * + *

서비스들이 "찾을 수 없습니다" 를 {@code CareServiceException} 으로 던지는 곳이 34군데였다. + * 그 예외의 기본 매핑은 **500** 이라, 없는 시설 ID 로 조회하면 서버 오류가 나가고 + * 운영 알림까지 울렸다. 클라이언트는 "내가 잘못 보냈다" 와 "서버가 고장났다" 를 구분할 수 없다. + */ + @Test + @DisplayName("없는 자원 조회는 404 이고 운영 알림을 울리지 않는다") + void unknownResourceIsNotFound() throws Exception { + long missing = 99_999_999L; + + assertThat(status(get("/facilities/{id}/admission-forecast", missing))) + .as("없는 시설의 입소 예측").isEqualTo(404); + assertThat(status(get("/facilities/{id}/popularity", missing))) + .as("없는 시설의 인기도").isEqualTo(404); + assertThat(status(get("/facilities/{id}/waitlist/stats", missing))) + .as("없는 시설의 대기 통계").isEqualTo(404); + + verify(alerter, never()).alert(anyString(), anyString(), anyString()); + } + private int status(RequestBuilder request) throws Exception { MvcResult result = mockMvc.perform(request).andReturn(); return result.getResponse().getStatus();