-
Notifications
You must be signed in to change notification settings - Fork 0
[deploy] v1.3.1 운영서버로 배포 #324
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
Changes from all commits
73a16e3
2377a42
d28b95e
2bc5f6e
c2dee7b
c6b5e7b
0fa3c7d
42bca35
c99d269
3460092
1b7a147
591306a
c6cd0a5
e8a5dfc
20f3483
af7709e
47f2874
b89820d
648f8db
a5b8f7b
6bd3994
a42a813
d3cb409
4d07e52
c263dec
ce14b9b
3ee0084
573d268
b1995db
9cab189
fc8ff1a
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 | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |||||||||||
|
|
||||||||||||
| import com.fasterxml.jackson.databind.JsonNode; | ||||||||||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||||||||||
| import konkuk.thip.common.exception.BusinessException; | ||||||||||||
| import konkuk.thip.common.exception.ExternalApiException; | ||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||
| import lombok.extern.slf4j.Slf4j; | ||||||||||||
| import org.springframework.beans.factory.annotation.Value; | ||||||||||||
|
|
@@ -54,14 +54,15 @@ public Integer getPageCount(String isbn) { | |||||||||||
| // TODO : 알라딘으로부터 page 정보가 없으면 ?? | ||||||||||||
| // 보상 시나리오 : 유저에게 "page 정보를 찾을 수 없는 책입니다. 직접 page 정보를 입력하세요" 라고 안내 | ||||||||||||
| // 일단 지금은 exception throw 만 진행 | ||||||||||||
| throw new BusinessException(BOOK_ALADIN_API_ISBN_NOT_FOUND); | ||||||||||||
| throw new ExternalApiException(BOOK_ALADIN_API_ISBN_NOT_FOUND); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| JsonNode subInfo = items.get(0).path(SUB_INFO_PARSING_KEY.getValue()); | ||||||||||||
|
|
||||||||||||
| return subInfo.path(PAGE_COUNT_PARSING_KEY.getValue()).asInt(); | ||||||||||||
| } catch (IOException e) { | ||||||||||||
| throw new BusinessException(BOOK_ALADIN_API_PARSING_ERROR); | ||||||||||||
| throw new ExternalApiException(BOOK_ALADIN_API_PARSING_ERROR); | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+64
to
65
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. 🛠️ Refactor suggestion | 🟠 Major 파싱 예외의 원인(cause) 보존 스택트레이스를 유지하려면 cause를 포함해 던지세요. - } catch (IOException e) {
- throw new ExternalApiException(BOOK_ALADIN_API_PARSING_ERROR);
+ } catch (IOException e) {
+ throw new ExternalApiException(BOOK_ALADIN_API_PARSING_ERROR, e);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package konkuk.thip.book.adapter.out.api.naver; | ||
|
|
||
| import konkuk.thip.common.exception.BusinessException; | ||
| import konkuk.thip.common.exception.ExternalApiException; | ||
| import konkuk.thip.common.exception.InternalServerException; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.stereotype.Component; | ||
|
|
@@ -63,7 +64,7 @@ private String keywordToEncoding(String keyword) { | |
| try { | ||
| text = URLEncoder.encode(keyword, "UTF-8"); | ||
| } catch (UnsupportedEncodingException e) { | ||
| throw new BusinessException(BOOK_KEYWORD_ENCODING_FAILED); | ||
| throw new InternalServerException(BOOK_KEYWORD_ENCODING_FAILED); | ||
| } | ||
|
Comment on lines
+67
to
68
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. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chain원인 예외(cause) 보존하여 진단력 향상 재래핑 시 원인 예외를 포함하지 않아 스택트레이스 손실이 있습니다. 두 번째 생성자 사용으로 cause를 보존해 주세요. - throw new InternalServerException(BOOK_KEYWORD_ENCODING_FAILED);
+ throw new InternalServerException(BOOK_KEYWORD_ENCODING_FAILED, e);
@@
- throw new ExternalApiException(BOOK_NAVER_API_REQUEST_ERROR);
+ throw new ExternalApiException(BOOK_NAVER_API_REQUEST_ERROR, e);
@@
- throw new InternalServerException(BOOK_NAVER_API_URL_ERROR);
+ throw new InternalServerException(BOOK_NAVER_API_URL_ERROR, e);
@@
- throw new InternalServerException(BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED);
+ throw new InternalServerException(BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED, e);
@@
- throw new ExternalApiException(BOOK_NAVER_API_RESPONSE_ERROR);
+ throw new ExternalApiException(BOOK_NAVER_API_RESPONSE_ERROR, e);ExternalApiException에 (ErrorCode, Throwable) 생성자가 없다면 동일 시그니처를 추가하는 것도 고려해 주세요. Based on learnings Also applies to: 88-89, 100-103, 120-121 🏁 Script executed: #!/bin/bash
# InternalServerException 생성자 시그니처 확인
rg -nP "class InternalServerException\b" -g "src/main/java/**/*.java" -C5
rg -nP "InternalServerException\s*\(" -g "src/main/java/**/*.java" -C3
# ExternalApiException 생성자 시그니처 확인
rg -nP "class ExternalApiException\b" -g "src/main/java/**/*.java" -C5
rg -nP "ExternalApiException\s*\(" -g "src/main/java/**/*.java" -C3Length of output: 19126 예외 래핑 시 원인 예외 보존 🤖 Prompt for AI Agents |
||
| return text; | ||
| } | ||
|
|
@@ -84,7 +85,7 @@ String get(String apiUrl, Map<String, String> requestHeaders){ | |
| return readBody(con.getErrorStream()); | ||
| } | ||
| } catch (IOException e) { | ||
| throw new BusinessException(BOOK_NAVER_API_REQUEST_ERROR); | ||
| throw new ExternalApiException(BOOK_NAVER_API_REQUEST_ERROR); | ||
| } finally { | ||
| con.disconnect(); | ||
| } | ||
|
|
@@ -96,9 +97,9 @@ private HttpURLConnection connect(String apiUrl){ | |
| URL url = new URL(apiUrl); | ||
| return (HttpURLConnection)url.openConnection(); | ||
| } catch (MalformedURLException e) { | ||
| throw new BusinessException(BOOK_NAVER_API_URL_ERROR); | ||
| throw new InternalServerException(BOOK_NAVER_API_URL_ERROR); | ||
| } catch (IOException e) { | ||
| throw new BusinessException(BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED); | ||
| throw new InternalServerException(BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -116,7 +117,7 @@ private String readBody(InputStream body){ | |
|
|
||
| return responseBody.toString(); | ||
| } catch (IOException e) { | ||
| throw new BusinessException(BOOK_NAVER_API_RESPONSE_ERROR); | ||
| throw new ExternalApiException(BOOK_NAVER_API_RESPONSE_ERROR); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ | |
| import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; | ||
| import konkuk.thip.book.application.port.out.BookRedisCommandPort; | ||
| import konkuk.thip.book.application.port.out.BookRedisQueryPort; | ||
| import konkuk.thip.common.exception.ExternalApiException; | ||
| import konkuk.thip.common.exception.InternalServerException; | ||
| import konkuk.thip.common.exception.code.ErrorCode; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
|
|
@@ -80,7 +80,7 @@ public List<BookMostSearchResult.BookRankInfo> getYesterdayBookRankInfos(LocalDa | |
| new TypeReference<List<BookMostSearchResult.BookRankInfo>>() {} | ||
| ); | ||
| } catch (JsonProcessingException e) { | ||
| throw new ExternalApiException(ErrorCode.JSON_PROCESSING_ERROR); | ||
| throw new InternalServerException(ErrorCode.JSON_PROCESSING_ERROR); | ||
| } | ||
|
Comment on lines
+83
to
84
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. 🛠️ Refactor suggestion | 🟠 Major JSON 처리 예외의 원인(cause) 보존 스택 추적을 유지하세요. - throw new InternalServerException(ErrorCode.JSON_PROCESSING_ERROR);
+ throw new InternalServerException(ErrorCode.JSON_PROCESSING_ERROR, e);- throw new InternalServerException(JSON_PROCESSING_ERROR);
+ throw new InternalServerException(JSON_PROCESSING_ERROR, e);Also applies to: 109-110 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
|
|
@@ -106,7 +106,7 @@ public void saveBookSearchRankDetail(List<BookMostSearchResult.BookRankInfo> boo | |
| try { | ||
| detailJson = objectMapper.writeValueAsString(bookRankDetails); | ||
| } catch (JsonProcessingException e) { | ||
| throw new ExternalApiException(JSON_PROCESSING_ERROR); | ||
| throw new InternalServerException(JSON_PROCESSING_ERROR); | ||
| } | ||
| redisTemplate.opsForValue().set(redisKey, detailJson); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,64 @@ | ||||||||||||||||||||||||||||||||||||||
| package konkuk.thip.common.discord; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import org.springframework.beans.factory.annotation.Value; | ||||||||||||||||||||||||||||||||||||||
| import org.springframework.http.MediaType; | ||||||||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Component; | ||||||||||||||||||||||||||||||||||||||
| import org.springframework.web.reactive.function.client.WebClient; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import java.time.LocalDateTime; | ||||||||||||||||||||||||||||||||||||||
| import java.time.format.DateTimeFormatter; | ||||||||||||||||||||||||||||||||||||||
| import java.util.HashMap; | ||||||||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| @Component | ||||||||||||||||||||||||||||||||||||||
| public class DiscordClient { | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| @Value("${discord.env}") | ||||||||||||||||||||||||||||||||||||||
| private String env; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| @Value("${discord.webhook-url}") | ||||||||||||||||||||||||||||||||||||||
| private String webhookUrl; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| public void sendErrorMessage(String message, String stackTrace, String requestId, String userId) { | ||||||||||||||||||||||||||||||||||||||
| if("test".equals(env)) return; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| WebClient webClient = WebClient.create(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Map<String, Object> embedData = new HashMap<>(); | ||||||||||||||||||||||||||||||||||||||
| embedData.put("title", "THIP 서버 500 에러 발생"); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Map<String, String> field1 = new HashMap<>(); | ||||||||||||||||||||||||||||||||||||||
| field1.put("name", "발생시각"); | ||||||||||||||||||||||||||||||||||||||
| field1.put("value", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Map<String, String> field2 = new HashMap<>(); | ||||||||||||||||||||||||||||||||||||||
| field2.put("name", "에러 명"); | ||||||||||||||||||||||||||||||||||||||
| field2.put("value", message); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Map<String, String> field3 = new HashMap<>(); | ||||||||||||||||||||||||||||||||||||||
| field3.put("name", "스택 트레이스"); | ||||||||||||||||||||||||||||||||||||||
| field3.put("value", stackTrace); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Map<String, String> field4 = new HashMap<>(); | ||||||||||||||||||||||||||||||||||||||
| field4.put("name", "Request ID"); | ||||||||||||||||||||||||||||||||||||||
| field4.put("value", requestId != null ? requestId : "N/A"); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Map<String, String> field5 = new HashMap<>(); | ||||||||||||||||||||||||||||||||||||||
| field5.put("name", "User ID"); | ||||||||||||||||||||||||||||||||||||||
| field5.put("value", userId != null ? userId : "N/A"); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| embedData.put("fields", List.of(field1, field2, field3, field4, field5)); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Map<String, Object> payload = new HashMap<>(); | ||||||||||||||||||||||||||||||||||||||
| payload.put("embeds", new Object[]{embedData}); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| webClient.post() | ||||||||||||||||||||||||||||||||||||||
| .uri(webhookUrl) | ||||||||||||||||||||||||||||||||||||||
| .contentType(MediaType.APPLICATION_JSON) | ||||||||||||||||||||||||||||||||||||||
| .bodyValue(payload) | ||||||||||||||||||||||||||||||||||||||
| .retrieve() | ||||||||||||||||||||||||||||||||||||||
| .bodyToMono(Void.class) | ||||||||||||||||||||||||||||||||||||||
| .block(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+63
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. 요청 스레드에서 block() 호출 및 타임아웃 부재 → 신뢰성 저하 예외 처리 중 Discord 호출이 무기한 블록될 수 있습니다. 타임아웃 + 비동기 fire-and-forget로 전환을 권장합니다. - webClient.post()
- .uri(webhookUrl)
- .contentType(MediaType.APPLICATION_JSON)
- .bodyValue(payload)
- .retrieve()
- .bodyToMono(Void.class)
- .block();
+ webClient.post()
+ .uri(webhookUrl)
+ .contentType(MediaType.APPLICATION_JSON)
+ .bodyValue(payload)
+ .retrieve()
+ .toBodilessEntity()
+ .timeout(java.time.Duration.ofSeconds(3))
+ .onErrorResume(ex -> reactor.core.publisher.Mono.empty())
+ .subscribe();추가로, WebClient는 매 호출 생성 대신 빈으로 주입해 재사용하면 연결 풀/성능 측면에서 유리합니다. Based on learnings 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package konkuk.thip.common.exception; | ||
|
|
||
| import konkuk.thip.common.exception.code.ErrorCode; | ||
|
|
||
| public class InternalServerException extends RuntimeException { | ||
|
|
||
| private final ErrorCode errorCode; | ||
|
|
||
| public InternalServerException(ErrorCode errorCode) { | ||
| super(errorCode.getMessage()); | ||
| this.errorCode = errorCode; | ||
| } | ||
| public InternalServerException(ErrorCode errorCode, Exception e) { | ||
| super(errorCode.getMessage(), e); | ||
| this.errorCode = errorCode; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -78,11 +78,10 @@ public enum ErrorCode implements ResponseCode { | |||||
| BOOK_NAVER_API_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80009, "네이버 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."), | ||||||
| BOOK_NOT_FOUND(HttpStatus.NOT_FOUND, 80010, "존재하지 않는 BOOK 입니다."), | ||||||
| BOOK_ALREADY_SAVED(HttpStatus.BAD_REQUEST, 80011, "사용자가 이미 저장한 책입니다."), | ||||||
| DUPLICATED_BOOKS_IN_COLLECTION(HttpStatus.INTERNAL_SERVER_ERROR, 80012, "중복된 책이 존재합니다."), | ||||||
| BOOK_NOT_SAVED_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80013, "사용자가 저장하지 않은 책은 저장삭제 할 수 없습니다."), | ||||||
| BOOK_NOT_SAVED_DB_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80014, "DB에 존재하지 않은 책은 저장삭제 할 수 없습니다."), | ||||||
| BOOK_ALADIN_API_PARSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80015, "알라딘 API 응답 파싱에 실패하였습니다."), | ||||||
| BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."), | ||||||
| BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."), | ||||||
|
|
||||||
|
Comment on lines
+84
to
85
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. 🧩 Analysis chain외부 API ‘ISBN 미존재’의 500 매핑 재검토 알라딘 ISBN 미존재를 500으로 올린 것은 사용자/클라이언트 오류 성격과 맞지 않습니다. 네이버(80009)는 BAD_REQUEST입니다. 일관성 및 UX를 위해 400(또는 404)로 되돌리는 것을 권장합니다. - BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."),
+ BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."),도메인 의도상 404가 더 적절하다면 404도 검토해 주세요. Based on learnings 🏁 Script executed: #!/bin/bash
rg -nP --type=java 'NAVER.*ISBN_NOT_FOUND' -C3 src/main/java/konkuk/thip/common/exception/code/ErrorCode.javaLength of output: 625 외부 API ISBN 미존재의 HTTP 상태코드 500 → 400(또는 404)로 변경 - BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."),
+ BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| /** | ||||||
| * 90000 : recentSearch error | ||||||
|
|
@@ -225,6 +224,7 @@ public enum ErrorCode implements ResponseCode { | |||||
| FCM_TOKEN_ENABLED_STATE_ALREADY(HttpStatus.BAD_REQUEST, 200001, "요청한 상태로 이미 푸쉬 알림 여부가 설정되어 있습니다."), | ||||||
| FCM_TOKEN_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, 200002, "토큰을 소유하고 있는 계정이 아닙니다."), | ||||||
| FIREBASE_SEND_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 200003, "FCM 푸쉬 알림 전송에 실패했습니다."), | ||||||
| FCM_TOKEN_DEVICE_ARRAY_MISMATCH(HttpStatus.INTERNAL_SERVER_ERROR, 200004, "메시지, FCM 토큰, 디바이스 ID 리스트의 크기는 같아야 합니다."), | ||||||
|
|
||||||
|
|
||||||
| /** | ||||||
|
|
||||||
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.
페이지 수 미존재(0) 반환은 모호합니다 — 명시적으로 예외 처리 권장
item/subInfo가 존재하더라도 pageCount 필드가 없으면 asInt()가 0을 반환해 성공처럼 보입니다. 사용자 안내 시나리오와 일관되게 0인 경우도 ExternalApiException으로 처리하는 편이 안전합니다.
적용 예:
📝 Committable suggestion