Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0fa3c7d
[feat] Webflux 의존성 추가 (#319)
hd0rable Oct 2, 2025
42bca35
[refactor] 외부api(네이버,알라딘) InternalServerException로 감싸기 (#319)
hd0rable Oct 2, 2025
c99d269
[refactor] 500에러 ExternalApiException로 감싸기 (#319)
hd0rable Oct 2, 2025
3460092
[refactor] FirebaseException이 RuntimeException 상속하도록 수정 (#319)
hd0rable Oct 2, 2025
1b7a147
[feat] 500에러 ExternalApiException 작성 (#319)
hd0rable Oct 2, 2025
591306a
[feat] 서버 내부 오류 예외 (500) 처리 핸들러 통합 및 디스코드 웹훅 트리거 (#319)
hd0rable Oct 2, 2025
c6cd0a5
[feat] 디스코드 웹훅 클라이언트 작성 (#319)
hd0rable Oct 2, 2025
e8a5dfc
[refactor] FirebaseException이 RuntimeException 상속하도록 수정 (#319)
hd0rable Oct 2, 2025
20f3483
[refactor] 관련 에러 코드 수정 (#319)
hd0rable Oct 2, 2025
47f2874
Merge remote-tracking branch 'origin/develop' into feat/#319-discord-…
hd0rable Oct 2, 2025
b89820d
[feat] 디스코드 웹훅 전송 실패시 로깅 (#319)
hd0rable Oct 2, 2025
648f8db
[refactor] 오타 수정 (#319)
hd0rable Oct 2, 2025
a5b8f7b
[chore] logstash 인코더 의존성 주입 (#319)
buzz0331 Oct 3, 2025
6bd3994
[chore] logback 파일 정의 (#319)
buzz0331 Oct 3, 2025
a42a813
[chore] Mdc 로깅 필터 (#319)
buzz0331 Oct 3, 2025
d3cb409
[chore] 응답에서 requestId 반환하도록 수정 (#319)
buzz0331 Oct 3, 2025
4d07e52
[chore] 디스코드 에러 전송시 Mdc에서 추출후 id 전송 (#319)
buzz0331 Oct 3, 2025
c263dec
[refactor] 토큰에서 userId 꺼내서 MDC에 넣도록 수정 (#319)
buzz0331 Oct 3, 2025
ce14b9b
Merge remote-tracking branch 'origin/feat/#319-discord-error' into fe…
buzz0331 Oct 3, 2025
3ee0084
[refactor] 패키지명 오타 수정 (#319)
hd0rable Oct 3, 2025
573d268
[refactor] static 임포트문 추가 (#319)
hd0rable Oct 4, 2025
b1995db
[refactor] static import (#319)
buzz0331 Oct 4, 2025
9cab189
[refactor] 토큰 유효성 검증 순서 변경 (#319)
buzz0331 Oct 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ dependencies {

// Firebase
implementation 'com.google.firebase:firebase-admin:9.3.0'

// Webflux
implementation 'org.springframework.boot:spring-boot-starter-webflux'

// LogStash
implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
}

def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

}

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);
}
}
}

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;
Expand Down Expand Up @@ -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 -66 to +67
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

}
return text;
}
Expand All @@ -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();
}
Expand All @@ -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);
}
}

Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import jakarta.persistence.EntityManager;
import konkuk.thip.common.entity.StatusType;
import konkuk.thip.common.exception.InvalidStateException;
import konkuk.thip.common.exception.InternalServerException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
Expand Down Expand Up @@ -30,7 +30,7 @@ public class StatusFilterAspect {
*/
private Session currentTxSession() {
if (!TransactionSynchronizationManager.isActualTransactionActive()) {
throw new InvalidStateException(PERSISTENCE_TRANSACTION_REQUIRED);
throw new InternalServerException(PERSISTENCE_TRANSACTION_REQUIRED);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

}
return session();
}
Expand Down
64 changes: 64 additions & 0 deletions src/main/java/konkuk/thip/common/discord/DiscordClient.java
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);
Comment thread
hd0rable marked this conversation as resolved.

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 +62
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

웹훅 전송 실패 처리 추가 필요

Discord 웹훅 호출 실패 시 에러 처리가 없습니다. 네트워크 문제나 Discord 서비스 장애 시 로그를 남기는 것이 좋습니다.

에러 핸들링을 추가하세요:

 webClient.post()
         .uri(webhookUrl)
         .contentType(MediaType.APPLICATION_JSON)
         .bodyValue(payload)
         .retrieve()
+        .onStatus(HttpStatusCode::isError, response -> 
+            response.bodyToMono(String.class)
+                .map(body -> new RuntimeException("Discord webhook failed: " + body))
+        )
         .bodyToMono(Void.class)
-        .block();
+        .block();
+        
+// 또는 subscribe 사용 시:
+        .subscribe(
+            result -> log.debug("Discord notification sent"),
+            error -> log.error("Failed to send Discord notification", error)
+        );

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/discord/DiscordClient.java around lines 56
to 62, the WebClient.post() call that sends the Discord webhook has no error
handling; wrap the call to detect HTTP errors and network exceptions, log a
descriptive error with the status code and response body (for non-2xx responses)
and catch runtime exceptions for network/timeouts, e.g., use WebClient's
exchangeToMono/onStatus to map non-successful responses to a Mono error and add
a try/catch (or subscribe with onError) that logs the full error and webhook
payload; ensure the method does not swallow failures silently and include enough
context (webhookUrl, payload summary, exception message) in the logs.

}
}
14 changes: 11 additions & 3 deletions src/main/java/konkuk/thip/common/dto/BaseResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;

import static konkuk.thip.common.logging.LoggingConstant.REQUEST_ID;

@Slf4j
@Getter
@JsonPropertyOrder({"success", "code", "message", "data"})
@JsonPropertyOrder({"success", "code", "message", "requestId", "data"})
public class BaseResponse<T> {

@JsonProperty("isSuccess")
Expand All @@ -15,17 +20,20 @@ public class BaseResponse<T> {

private final String message;

private final String requestId;

private final T data;

private BaseResponse(boolean success, int code, String message, T data) {
private BaseResponse(boolean success, int code, String message, String requestId, T data) {
this.success = success;
this.code = code;
this.message = message;
this.requestId = requestId;
this.data = data;
}

private BaseResponse(ResponseCode response, T data) {
this(response.isSuccess(), response.getCode(), response.getMessage(), data);
this(response.isSuccess(), response.getCode(), response.getMessage(), MDC.get(REQUEST_ID.getValue()), data);
}

public static <T> BaseResponse<T> ok(T data) {
Expand Down
14 changes: 10 additions & 4 deletions src/main/java/konkuk/thip/common/dto/ErrorResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Getter;
import org.slf4j.MDC;

import static konkuk.thip.common.logging.LoggingConstant.REQUEST_ID;

@Getter
@JsonPropertyOrder({"success", "code", "message"})
@JsonPropertyOrder({"success", "code", "message", "requestId"})
public class ErrorResponse {

@JsonProperty("isSuccess")
Expand All @@ -15,14 +18,17 @@ public class ErrorResponse {

private final String message;

private ErrorResponse(boolean success, int code, String message) {
private final String requestId;

private ErrorResponse(boolean success, int code, String message, String requestId) {
this.success = success;
this.code = code;
this.message = message;
this.requestId = requestId;
}

private ErrorResponse(ResponseCode response) {
this(response.isSuccess(), response.getCode(), response.getMessage());
this(response.isSuccess(), response.getCode(), response.getMessage(), MDC.get(REQUEST_ID.getValue()));
}

public static ErrorResponse of(ResponseCode response) {
Expand All @@ -32,6 +38,6 @@ public static ErrorResponse of(ResponseCode response) {
public static ErrorResponse of(ResponseCode response, String message) {
StringBuilder sb = new StringBuilder();
sb.append(response.getMessage()).append(" ").append(message);
return new ErrorResponse(response.isSuccess(), response.getCode(), sb.toString());
return new ErrorResponse(response.isSuccess(), response.getCode(), sb.toString(), MDC.get(REQUEST_ID.getValue()));
}
}
20 changes: 8 additions & 12 deletions src/main/java/konkuk/thip/common/exception/FirebaseException.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,16 @@

import konkuk.thip.common.exception.code.ErrorCode;

public class FirebaseException extends BusinessException {
public FirebaseException(ErrorCode errorCode) {
super(errorCode);
}
public class FirebaseException extends RuntimeException {

public FirebaseException(ErrorCode errorCode, Exception e) {
super(errorCode, e);
}
private final ErrorCode errorCode;

public FirebaseException(Exception e) {
super(ErrorCode.FIREBASE_SEND_ERROR, e);
public FirebaseException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}

public FirebaseException() {
super(ErrorCode.FIREBASE_SEND_ERROR);
public FirebaseException(ErrorCode errorCode, Exception e) {
super(errorCode.getMessage(), e);
this.errorCode = errorCode;
}
}
Comment thread
hd0rable marked this conversation as resolved.
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;
}
}
Comment thread
hd0rable marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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 thread
hd0rable marked this conversation as resolved.

/**
* 90000 : recentSearch error
Expand Down Expand Up @@ -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 리스트의 크기는 같아야 합니다."),


/**
Expand Down
Loading