Skip to content

Conversation

@seongjunnoh
Copy link
Collaborator

@seongjunnoh seongjunnoh commented Oct 13, 2025

#️⃣ 연관된 이슈

📝 작업 내용

  • [feat] 안읽은 알림 여부 조회 api 추가
  • [chore] 어플리케이션 5xx error 발생 시 디스코드 알림 전송 기능 추가
  • [chore] logback 설정, MDC 추가

📸 스크린샷

💬 리뷰 요구사항

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

📌 PR 진행 시 이러한 점들을 참고해 주세요

* P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
* P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
* P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)

Summary by CodeRabbit

  • New Features
    • 알림 읽지 않음 존재 여부 조회 엔드포인트 추가로 새로운 알림 보유 여부를 빠르게 확인할 수 있습니다.
    • 모든 API 응답에 요청 ID가 포함되어 문제 발생 시 추적이 쉬워졌습니다.
  • Refactor
    • 서버 오류 처리 로직을 통합해 안정성을 높였으며, 심각한 오류 발생 시 운영 채널로 알림을 전송해 대응 속도를 개선했습니다.

seongjunnoh and others added 30 commits September 30, 2025 18:37
…k-unread-one

[feat] 안읽은 알림 여부 조회 api 구현
[Feat] 어플리케이션 단위 500 에러 디스코드로 알림 전송기능 개발
@coderabbitai
Copy link

coderabbitai bot commented Oct 13, 2025

Walkthrough

여러 예외 타입을 InternalServerException/ExternalApiException 중심으로 정리하고, MDC 기반 요청/사용자 ID 로깅과 Discord 웹훅 오류 알림을 도입했다. WebFlux/Logstash 의존성과 Logback 설정을 추가했다. 알림의 미확인 존재 여부 조회 API를 신규 추가하고 관련 포트/어댑터/서비스/테스트를 구현했다.

Changes

Cohort / File(s) Change summary
빌드/로깅 설정
build.gradle, src/main/resources/logback-spring.xml
WebFlux, logstash-encoder 의존성 추가. 콘솔/파일 롤링 로깅과 MDC(request_id,user_id) 출력 설정 추가.
MDC 컨텍스트/보안 필터
src/main/java/konkuk/thip/common/logging/LoggingConstant.java, .../common/logging/MdcLoggingFilter.java, .../config/SecurityConfig.java, .../common/security/filter/JwtAuthenticationFilter.java
MDC 상수 enum과 Request ID 필터 추가, 시큐리티 체인에 필터 삽입. JWT 필터 토큰 검증 로직 메서드화 및 사용자 ID를 MDC에 기록.
전역 예외 처리/Discord 알림
.../common/exception/handler/GlobalExceptionHandler.java, .../common/discord/DiscordClient.java
서버 오류를 단일 핸들러로 통합하고 Discord 웹훅으로 오류 임베드 전송. MDC에서 requestId/userId 추출, 스택트레이스 요약 전송.
예외 타입 도입/정비
.../common/exception/InternalServerException.java, .../common/exception/FirebaseException.java, .../common/exception/code/ErrorCode.java
InternalServerException 신설. FirebaseException이 RuntimeException 상속 및 ErrorCode 포함하도록 변경. ErrorCode 일부 추가/삭제/상태 수정.
기반 응답에 Request ID 추가
.../common/dto/BaseResponse.java, .../common/dto/ErrorResponse.java
requestId 필드와 생성자 확장. MDC에서 요청 ID를 주입하여 응답에 포함.
외부 API 유틸 예외 변경
.../book/adapter/out/api/aladin/AladinApiUtil.java, .../book/adapter/out/api/naver/NaverApiUtil.java
BusinessException 제거, 상황에 따라 ExternalApiException/InternalServerException으로 대체.
Redis/영속 어댑터 예외 정비
.../book/adapter/out/persistence/BookRedisAdapter.java, .../user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java, .../common/aop/StatusFilterAspect.java, .../notification/domain/value/NotificationRedirectSpecConverter.java, .../config/AwsS3ImageUrlInitializer.java, .../common/security/oauth2/CustomSuccessHandler.java, .../common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java, .../user/domain/User.java
JSON 처리/상태 오류 등을 InternalServerException 등으로 변경. 기존 흐름 유지하며 예외 타입만 교체.
알림: 미확인 존재 여부 API 추가
.../notification/adapter/in/web/NotificationQueryController.java, .../notification/adapter/in/web/response/NotificationUncheckedExistsResponse.java, .../notification/application/port/in/NotificationExistsUncheckedUseCase.java, .../notification/application/port/out/NotificationQueryPort.java, .../notification/application/service/NotificationExistsUncheckedService.java, .../notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java, .../notification/adapter/out/persistence/repository/NotificationQueryRepository.java, .../notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java, src/test/java/.../NotificationExistsUncheckedApiTest.java
GET /notifications/exists-unchecked 엔드포인트 및 응답 레코드 추가. 포트/서비스/리포지토리/구현에 existsUnchecked 메서드 도입. 통합 테스트 추가.
Firebase 전송 오류 처리 정비
.../message/adapter/out/firebase/FirebaseAdapter.java
리스트 크기 불일치/배치 실패 시 ErrorCode 기반 처리. FirebaseException 사용 방식 정비.
테스트 컨트롤러 변경
src/main/java/konkuk/thip/TestController.java
클래스명 변경 및 500 에러 테스트 엔드포인트(/api/test/error) 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant Sec as SecurityFilterChain
  participant MDC as MdcLoggingFilter
  participant JWT as JwtAuthenticationFilter
  participant Cn as NotificationQueryController
  participant Svc as NotificationExistsUncheckedService
  participant Port as NotificationQueryPort
  participant Repo as NotificationQueryRepository

  C->>Sec: HTTP GET /notifications/exists-unchecked
  Sec->>MDC: doFilterInternal (set request_id)
  MDC-->>Sec: proceed
  Sec->>JWT: doFilterInternal (validate token, set auth, MDC user_id)
  JWT-->>Sec: proceed
  Sec-->>Cn: dispatch
  Cn->>Svc: existsUnchecked(userId)
  Svc->>Port: existsUnchecked(userId)
  Port->>Repo: existsByUserIdAndIsCheckedFalse(userId)
  Repo-->>Port: boolean
  Port-->>Svc: boolean
  Svc-->>Cn: boolean
  Cn-->>C: 200 BaseResponse{requestId, data.exists}
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant App as App Layer
  participant GEH as GlobalExceptionHandler
  participant MDC as MDC
  participant DC as DiscordClient
  participant DH as Discord Webhook

  C->>App: HTTP Request
  App--x App: Exception thrown
  App->>GEH: handleServerErrors(e)
  GEH->>MDC: get(request_id, user_id)
  GEH->>DC: sendErrorMessage(message, stackTrace, requestId, userId)
  alt env != test
    DC->>DH: POST webhook (embed payload)
    DH-->>DC: 204/2xx
  else env == test
    DC-->>GEH: skipped
  end
  GEH-->>C: 500 ErrorResponse{requestId}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

🚀 deploy, 🛠️ feat, 👻 성준

Suggested reviewers

  • buzz0331
  • hd0rable

Poem

귀가 팔랑, 로그는 살랑, 꼬리는 펄럭 펄럭!
요청 ID 품에 안고, 사용자 ID 콩닥콩닥.
알림 있니? 없니? 살짝 들여다보고,
에러 났다! 디스코드로 깡총 전해주고.
오늘도 토끼는 필터 타고, 안전하게 깡총! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.06% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 이 PR 제목은 v1.3.1 버전을 운영 서버로 배포하는 주요 목적을 명확히 나타내며 간결하고 구체적으로 작성되어 있어 변경 내용의 핵심을 잘 요약하고 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

Test Results

481 tests   481 ✅  44s ⏱️
143 suites    0 💤
143 files      0 ❌

Results for commit fc8ff1a.

@seongjunnoh seongjunnoh merged commit 80efd88 into main Oct 13, 2025
7 of 8 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java (2)

57-67: topN=0일 때 전체 반환되는 경계 버그

end 인덱스가 topN-1이므로 topN=0이면 end=-1 → 전체 범위가 반환될 수 있습니다. 사전 가드 필요.

     private List<Map.Entry<String, Double>> getTopNFromZSet(String prefix, LocalDate date, int topN) {
+        if (topN <= 0) return Collections.emptyList();
         String dateStr = date.format(DAILY_KEY_FORMATTER);
         String redisKey = prefix + dateStr;
         Set<ZSetOperations.TypedTuple<String>> topNSet = redisTemplate.opsForZSet()
                 .reverseRangeWithScores(redisKey, 0, topN - 1);

103-112: 상세 랭크 키 TTL 누락 — Redis 메모리/보존 리스크

rank(zset)는 7일 만료지만 detail(value)은 만료가 없습니다. 동일한 보존정책(예: 7일) 적용을 권장합니다.

         redisTemplate.opsForValue().set(redisKey, detailJson);
+        redisTemplate.expire(redisKey, Duration.ofDays(7));
src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java (1)

76-81: MDC에 "null" 문자열 저장 방지

userId가 null일 때 String.valueOf로 "null"이 기록됩니다. null일 때는 넣지 않도록 가드해 주세요. DiscordClient의 "N/A" 처리 로직도 정상 동작합니다.

-            MDC.put(USER_ID.getValue(), String.valueOf(loginUser.userId()));
+            if (loginUser.userId() != null) {
+                MDC.put(USER_ID.getValue(), String.valueOf(loginUser.userId()));
+            }
src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiUtil.java (1)

81-86: HTTP 오류 응답 처리 시 NPE 위험 (errorStream == null)

HTTP 오류에서 getErrorStream이 null일 수 있어 readBody 호출 시 NPE가 납니다. null 처리와 명시적 예외 전환을 권장합니다.

-            int responseCode = con.getResponseCode();
-            if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
-                return readBody(con.getInputStream());
-            } else { // 오류 발생
-                return readBody(con.getErrorStream());
-            }
+            int responseCode = con.getResponseCode();
+            InputStream responseStream = (responseCode == HttpURLConnection.HTTP_OK)
+                    ? con.getInputStream()
+                    : con.getErrorStream();
+            if (responseStream == null) {
+                throw new ExternalApiException(BOOK_NAVER_API_RESPONSE_ERROR);
+            }
+            return readBody(responseStream);
🧹 Nitpick comments (16)
src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java (1)

50-59: 효율적인 존재 여부 확인 구현입니다.

selectOne() + fetchFirst() 패턴을 사용하여 하나의 레코드만 조회하므로 성능상 효율적입니다.

참고로, QueryDSL의 다른 존재 확인 패턴도 고려할 수 있습니다:

대안 1: 명시적 필드 선택

Long result = queryFactory
    .select(notification.notificationId)
    .from(notification)
    .where(notification.userJpaEntity.userId.eq(userId)
            .and(notification.isChecked.eq(false)))
    .fetchFirst();
return result != null;

대안 2: count 사용 (단, 성능상 현재 방식이 더 효율적일 수 있음)

long count = queryFactory
    .selectFrom(notification)
    .where(notification.userJpaEntity.userId.eq(userId)
            .and(notification.isChecked.eq(false)))
    .fetchCount();
return count > 0;

다만 현재 구현이 이미 충분히 효율적이므로 변경이 필수적이지는 않습니다.

src/test/java/konkuk/thip/notification/adapter/in/web/NotificationExistsUncheckedApiTest.java (2)

53-71: JdbcTemplate 직접 업데이트보다 엔티티 메서드 사용을 권장합니다.

현재 JdbcTemplate을 사용해 직접 DB를 업데이트하고 있는데, 이는 JPA 엔티티 관리를 우회하여 잠재적으로 캐시 불일치를 유발할 수 있습니다. 테스트 컨텍스트에서는 동작하지만, 더 명시적인 접근을 위해 NotificationJpaEntity의 메서드나 리포지토리를 통한 업데이트를 고려해보세요.

예시:

-        jdbcTemplate.update(
-                "UPDATE notifications SET is_checked = TRUE WHERE notification_id = ?",
-                n1.getNotificationId()
-        );
+        n1.markAsChecked(); // 또는 적절한 엔티티 메서드
+        notificationJpaRepository.save(n1);

또는 전용 체크 처리 메서드가 있다면:

notificationJpaRepository.markAsChecked(n1.getNotificationId());

37-71: 추가 엣지 케이스 테스트를 고려해보세요.

현재 테스트는 기본 시나리오를 잘 커버하고 있습니다. 아래 시나리오들도 추가하면 더 견고한 테스트가 될 수 있습니다:

  • 알림이 전혀 없는 사용자의 경우
  • 여러 알림 중 일부만 읽은 경우 (읽지 않은 알림이 여전히 존재)
  • 모든 알림을 읽은 경우
src/main/resources/logback-spring.xml (1)

36-40: 롤링 정책 설정을 재검토하세요.

maxHistory가 10으로 설정되어 있어 최대 10일치 로그만 보관됩니다. 운영 환경에서 장애 분석이나 규정 준수를 위해 더 긴 보관 기간이 필요한지 확인하시기 바랍니다. 또한 totalSizeCap을 추가하여 전체 로그 디스크 사용량을 제한하는 것을 권장합니다.

         <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
             <fileNamePattern>${LOG_PATH}/info/info.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
             <maxFileSize>10MB</maxFileSize>
-            <maxHistory>10</maxHistory>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>1GB</totalSizeCap>
         </rollingPolicy>
src/main/java/konkuk/thip/config/AwsS3ImageUrlInitializer.java (1)

21-24: 빈 값 체크 강화 및 메시지 개선 제안

null/empty 대신 hasText로 공백만 있는 값도 차단하세요. 또한 환경키를 로그/메시지에 포함하면 원인 파악이 빨라집니다.

-        if (baseUrl == null || baseUrl.isEmpty()) {
-            throw new InternalServerException(AWS_BUCKET_BASE_URL_NOT_CONFIGURED);
+        if (!org.springframework.util.StringUtils.hasText(baseUrl)) {
+            throw new InternalServerException(AWS_BUCKET_BASE_URL_NOT_CONFIGURED);
         }
src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpecConverter.java (1)

17-18: ObjectMapper 정합성 유지 제안

전역 모듈(예: JavaTime, Kotlin, custom mixin) 설정을 공유하려면 static ObjectMapper 대신 Spring 빈을 주입받는 방식으로 전환을 고려해 주세요.

src/main/java/konkuk/thip/common/logging/MdcLoggingFilter.java (1)

24-33: X-Request-ID를 응답 헤더로도 전파하세요

클라이언트/로그 상관관계를 위해 response 헤더 설정을 권장합니다.

             MDC.put(REQUEST_ID.getValue(), requestId);
+            response.setHeader("X-Request-ID", requestId);
             filterChain.doFilter(request, response);
src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java (1)

86-88: 프로파일 판별 로직 개선 제안

server.profile 문자열 대신 Spring Environment의 활성 프로파일을 활용하면 오탑재를 줄일 수 있습니다(예: Arrays.asList(env.getActiveProfiles()).contains("dev")).

src/main/java/konkuk/thip/common/exception/FirebaseException.java (1)

5-16: errorCode 게터 추가 제안

전역 예외 핸들러/디스코드 알림에서 코드 접근이 필요할 수 있습니다. 간단한 게터를 추가해 두면 활용성이 높습니다.

 public class FirebaseException extends RuntimeException {
 
     private final ErrorCode errorCode;
 
     public FirebaseException(ErrorCode errorCode) {
         super(errorCode.getMessage());
         this.errorCode = errorCode;
     }
     public FirebaseException(ErrorCode errorCode, Exception e) {
         super(errorCode.getMessage(), e);
         this.errorCode = errorCode;
     }
+
+    public ErrorCode getErrorCode() {
+        return errorCode;
+    }
 }
src/main/java/konkuk/thip/common/dto/BaseResponse.java (2)

11-14: requestId 직렬화 최적화 제안 (null 생략)

requestId가 없는 경우 응답 JSON에서 필드를 생략하면 노이즈가 줄어듭니다. 클래스에 NON_NULL 적용을 권장합니다.

 import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.annotation.JsonInclude;
@@
-@JsonPropertyOrder({"success", "code", "message", "requestId", "data"})
+@JsonPropertyOrder({"success", "code", "message", "requestId", "data"})
+@JsonInclude(JsonInclude.Include.NON_NULL)
 public class BaseResponse<T> {

Also applies to: 23-27


6-11: 사용되지 않는 로거 제거 제안

이 클래스에서 log를 사용하지 않습니다. @slf4j 어노테이션과 import 제거로 바이트코드/필드 생성 최소화가 가능합니다.

src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java (1)

116-133: Bearer 토큰 파싱 안전성 개선 및 로그 레벨 조정

"Bearer"만 온 경우 split 인덱스 오류 가능성이 있습니다. 접두사 길이 기반으로 안전하게 파싱하고 토큰 부재 로그는 debug로 낮추는 것을 권장합니다(필터 미적용 엔드포인트 외 401 요청에서 로그 오염 방지).

-        String authorization = request.getHeader(JWT_HEADER_KEY.getValue());
-        if (authorization != null && authorization.startsWith(JWT_PREFIX.getValue())) {
-            return authorization.split(" ")[1];
-        }
+        String authorization = request.getHeader(JWT_HEADER_KEY.getValue());
+        String prefix = JWT_PREFIX.getValue() + " ";
+        if (authorization != null && authorization.startsWith(prefix) && authorization.length() > prefix.length()) {
+            return authorization.substring(prefix.length()).trim();
+        }
@@
-        log.info("토큰이 없습니다.");
+        log.debug("토큰이 없습니다.");
         return null;
src/main/java/konkuk/thip/common/exception/InternalServerException.java (1)

5-17: errorCode 접근자 추가 제안

핸들러/로그에서 errorCode 조회가 필요할 수 있습니다. Getter 추가를 권장합니다.

 package konkuk.thip.common.exception;
 
 import konkuk.thip.common.exception.code.ErrorCode;
+import lombok.Getter;
 
+@Getter
 public class InternalServerException extends RuntimeException {
 
     private final ErrorCode errorCode;
src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiUtil.java (1)

95-104: 외부 호출 안정성: 연결/읽기 타임아웃 설정

기본 타임아웃이 무제한일 수 있어 장애 전파 위험이 큽니다. 적절한 타임아웃을 설정해 주세요(예: 2~5초).

-            URL url = new URL(apiUrl);
-            return (HttpURLConnection)url.openConnection();
+            URL url = new URL(apiUrl);
+            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+            conn.setConnectTimeout(3000);
+            conn.setReadTimeout(5000);
+            return conn;

Based on learnings

src/main/java/konkuk/thip/common/dto/ErrorResponse.java (1)

10-12: requestId null 생략으로 응답 간결화

requestId가 없을 때 필드 생략을 권장합니다. NON_NULL을 추가해 주세요.

 import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.annotation.JsonInclude;
@@
-@JsonPropertyOrder({"success", "code", "message", "requestId"})
+@JsonPropertyOrder({"success", "code, "message", "requestId"})
+@JsonInclude(JsonInclude.Include.NON_NULL)
 public class ErrorResponse {

Also applies to: 21-28

src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java (1)

134-144: 500 알림 운영 측면: 샘플링/레이트 리밋 고려

트래픽 급증 시 동일 오류가 Discord로 폭주할 수 있습니다. 동일 예외/엔드포인트 기준 샘플링(예: 1분당 N건) 또는 버퍼링을 고려해 주세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7619564 and fc8ff1a.

📒 Files selected for processing (34)
  • build.gradle (1 hunks)
  • src/main/java/konkuk/thip/TestController.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java (2 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiUtil.java (5 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java (3 hunks)
  • src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java (2 hunks)
  • src/main/java/konkuk/thip/common/discord/DiscordClient.java (1 hunks)
  • src/main/java/konkuk/thip/common/dto/BaseResponse.java (2 hunks)
  • src/main/java/konkuk/thip/common/dto/ErrorResponse.java (3 hunks)
  • src/main/java/konkuk/thip/common/exception/FirebaseException.java (1 hunks)
  • src/main/java/konkuk/thip/common/exception/InternalServerException.java (1 hunks)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (2 hunks)
  • src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java (3 hunks)
  • src/main/java/konkuk/thip/common/logging/LoggingConstant.java (1 hunks)
  • src/main/java/konkuk/thip/common/logging/MdcLoggingFilter.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java (4 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java (2 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java (2 hunks)
  • src/main/java/konkuk/thip/config/AwsS3ImageUrlInitializer.java (2 hunks)
  • src/main/java/konkuk/thip/config/SecurityConfig.java (3 hunks)
  • src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java (3 hunks)
  • src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java (3 hunks)
  • src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationUncheckedExistsResponse.java (1 hunks)
  • src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepository.java (1 hunks)
  • src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java (1 hunks)
  • src/main/java/konkuk/thip/notification/application/port/in/NotificationExistsUncheckedUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/notification/application/service/NotificationExistsUncheckedService.java (1 hunks)
  • src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpecConverter.java (3 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java (2 hunks)
  • src/main/java/konkuk/thip/user/domain/User.java (2 hunks)
  • src/main/resources/logback-spring.xml (1 hunks)
  • src/test/java/konkuk/thip/notification/adapter/in/web/NotificationExistsUncheckedApiTest.java (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#285
File: src/main/java/konkuk/thip/room/adapter/out/jpa/RoomStatus.java:1-7
Timestamp: 2025-08-31T05:25:14.845Z
Learning: seongjunnoh는 enum 의존성에 대해 유연한 접근을 선호하며, 도메인→어댑터 레이어 참조와 같은 아키텍처 layering 원칙보다 실용적인 구현을 우선시한다.
🧬 Code graph analysis (11)
src/main/java/konkuk/thip/config/AwsS3ImageUrlInitializer.java (1)
src/main/java/konkuk/thip/common/exception/InternalServerException.java (1)
  • InternalServerException (5-17)
src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java (1)
src/main/java/konkuk/thip/common/exception/InternalServerException.java (1)
  • InternalServerException (5-17)
src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java (1)
src/main/java/konkuk/thip/common/exception/InternalServerException.java (1)
  • InternalServerException (5-17)
src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpecConverter.java (1)
src/main/java/konkuk/thip/common/exception/InternalServerException.java (1)
  • InternalServerException (5-17)
src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiUtil.java (1)
src/main/java/konkuk/thip/common/exception/InternalServerException.java (1)
  • InternalServerException (5-17)
src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java (1)
src/main/java/konkuk/thip/common/exception/InternalServerException.java (1)
  • InternalServerException (5-17)
src/test/java/konkuk/thip/notification/adapter/in/web/NotificationExistsUncheckedApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (35-403)
src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java (1)
src/main/java/konkuk/thip/common/exception/InternalServerException.java (1)
  • InternalServerException (5-17)
src/main/java/konkuk/thip/common/dto/BaseResponse.java (1)
src/main/java/konkuk/thip/common/dto/ErrorResponse.java (1)
  • Getter (10-43)
src/main/java/konkuk/thip/user/domain/User.java (1)
src/main/java/konkuk/thip/common/exception/InternalServerException.java (1)
  • InternalServerException (5-17)
src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java (1)
src/main/java/konkuk/thip/common/exception/InternalServerException.java (1)
  • InternalServerException (5-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (24)
src/main/java/konkuk/thip/TestController.java (2)

13-13: 클래스명 변경이 적절합니다.

TestTokenController에서 TestController로의 이름 변경은 컨트롤러의 책임 범위가 확장된 것을 반영하여 더 일반적인 이름을 사용한 것으로 보입니다.


22-25: 테스트용 500 에러 엔드포인트 구현 및 설정 확인

  • 운영 환경 설정에 thip.test-api.enabled가 명시되지 않아 기본값(false)로 비활성화됨을 확인했습니다.
  • 해당 엔드포인트가 GlobalExceptionHandler를 통해 Discord 알림을 정상적으로 트리거하는지 검증 부탁드립니다.
src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationUncheckedExistsResponse.java (1)

1-9: LGTM!

불변 응답 DTO로 record를 사용한 것이 적절하며, 정적 팩토리 메서드를 통한 인스턴스 생성 방식도 명확합니다.

src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java (1)

43-46: LGTM!

포트 인터페이스 구현이 올바르며, 리포지토리로의 위임 로직이 명확합니다.

src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepository.java (1)

15-15: LGTM!

메서드 시그니처가 명확하며, Spring Data 네이밍 컨벤션을 잘 따르고 있습니다.

src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java (1)

15-15: LGTM!

포트 인터페이스 정의가 명확하며, 헥사고날 아키텍처 패턴을 잘 준수하고 있습니다.

src/main/java/konkuk/thip/notification/application/service/NotificationExistsUncheckedService.java (1)

1-20: LGTM!

조회 전용 트랜잭션 설정(@Transactional(readOnly = true))이 적절하게 적용되었으며, 유스케이스 구현이 깔끔합니다.

src/main/java/konkuk/thip/notification/application/port/in/NotificationExistsUncheckedUseCase.java (1)

1-6: LGTM!

단일 책임 원칙을 잘 준수한 유스케이스 인터페이스입니다.

src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java (1)

63-71: LGTM!

새로운 API 엔드포인트가 잘 구현되었습니다:

  • @UserId 어노테이션을 통한 인증 처리가 적절합니다
  • Swagger 문서화가 명확합니다
  • 응답 래핑 패턴이 기존 엔드포인트들과 일관성 있게 유지되고 있습니다
src/test/java/konkuk/thip/notification/adapter/in/web/NotificationExistsUncheckedApiTest.java (2)

25-35: 통합 테스트 구성이 적절합니다.

@AutoConfigureMockMvc(addFilters = false)를 통해 보안 필터를 비활성화하고 있는데, 이는 통합 테스트에서 인증 흐름을 우회하는 일반적인 패턴입니다. 다만, 실제 프로덕션 환경의 인증 메커니즘과 requestAttr("userId", ...)를 통한 사용자 컨텍스트 전달 방식이 일치하는지 확인해주세요.


37-51: 기본 시나리오 테스트 LGTM!

읽지 않은 알림이 존재할 때 true를 반환하는 핵심 시나리오를 정확하게 검증하고 있습니다.

build.gradle (1)

96-100: 의존성 추가가 적절합니다.

WebFlux는 Discord 웹훅 클라이언트 구현에 필요하고, Logstash 인코더는 MDC 기반 구조화된 로깅을 지원합니다. Spring Boot 3.5.0과 호환되는 버전들입니다.

src/main/resources/logback-spring.xml (2)

8-8: 로그 디렉토리 생성을 확인하세요.

LOG_PATH로 지정된 logs 디렉토리가 애플리케이션 시작 시 자동으로 생성되는지 확인하시기 바랍니다. Logback은 자동으로 생성하지만, 권한 문제가 발생할 수 있습니다.


79-84: 로그 레벨별 파일 분리가 적절합니다.

INFO, WARN, ERROR를 별도 파일로 분리하여 로그 분석이 용이하고, LevelFilter로 중복을 방지한 구성이 좋습니다.

src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java (1)

54-54: 예외 타입 변경이 적절합니다.

웹 도메인 origin이 비어있는 것은 서버 설정 오류이므로 InternalServerException을 사용하는 것이 AuthException보다 의미적으로 더 정확합니다.

src/main/java/konkuk/thip/user/domain/User.java (1)

84-84: 예외 타입 변경이 적절합니다.

oauth2Id가 null인 상태에서 markAsDeleted가 호출되는 것은 내부 불변성 위반이므로 InternalServerExceptionInvalidStateException보다 적합합니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java (1)

55-55: 예외 타입 변경이 적절합니다.

내부 데이터 구조를 JSON으로 직렬화하는 과정에서 발생하는 오류는 외부 API 오류가 아닌 내부 서버 오류이므로 InternalServerExceptionExternalApiException보다 정확합니다.

src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java (1)

42-42: 예외 타입 변경이 적절합니다.

Redis에서 예상과 다른 타입을 반환하는 것은 인증 오류가 아닌 내부 데이터 일관성 문제이므로 InternalServerExceptionAuthException보다 적합합니다.

src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java (1)

33-33: 예외 타입 변경이 적절합니다.

트랜잭션이 활성화되지 않은 상태에서 Hibernate 세션에 접근하는 것은 프로그래밍 오류이므로 InternalServerExceptionInvalidStateException보다 적합합니다. 이는 개발 단계에서 발견되어야 하는 내부 불변성 위반입니다.

src/main/java/konkuk/thip/config/SecurityConfig.java (1)

50-50: MDC 클린업 로직 확인됨

MdcLoggingFilterdoFilterInternal에서 MDC.clear()를 호출해 필터 완료 후 MDC를 적절히 정리하고 있습니다. 추가 조치 불필요합니다.

src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java (1)

44-47: 네트워크/HTTP 예외 매핑 누락 가능성

RestTemplate.getForObject(...)의 RestClientException 계열을 ExternalApiException으로 매핑하지 않으면 외부 API 장애가 적절히 분류되지 않을 수 있습니다. 전역 핸들러 정책과 맞춰 RestClientException 캐치 → ExternalApiException 변환을 검토해 주세요.

src/main/java/konkuk/thip/common/logging/MdcLoggingFilter.java (1)

16-35: 필터 실행 순서 정상 확인
SecurityConfig.filterChain에서 이미 addFilterBefore(mdcLoggingFilter, JwtAuthenticationFilter.class)로 MDC 로깅 필터가 JWT 인증 필터 이전에 등록되어 있습니다.

src/main/java/konkuk/thip/common/logging/LoggingConstant.java (1)

1-16: MDC 키 사용 일치 확인 및 설정 지점 검증 요청
logback-spring.xml의 file.format 패턴에서 request_id·user_id 키가 일관되게 반영됨을 확인했습니다. USER_ID를 MDC에 설정하는 인증 필터 또는 인터셉터 구현부도 올바르게 동작하는지 함께 검증해 주세요.

src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java (1)

90-95: 예외 전파 전략 확인 요청

catch에서 request 속성 "exception"만 설정하고 체인을 계속 진행합니다. 이후(AuthenticationEntryPoint/HandlerExceptionResolver 등)에서 이 속성을 사용해 401/500으로 매핑되는지 확인해 주세요. 미처리 시 컨트롤러까지 진행될 수 있습니다.

Comment on lines 60 to 63
JsonNode subInfo = items.get(0).path(SUB_INFO_PARSING_KEY.getValue());

return subInfo.path(PAGE_COUNT_PARSING_KEY.getValue()).asInt();
} catch (IOException e) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

페이지 수 미존재(0) 반환은 모호합니다 — 명시적으로 예외 처리 권장

item/subInfo가 존재하더라도 pageCount 필드가 없으면 asInt()가 0을 반환해 성공처럼 보입니다. 사용자 안내 시나리오와 일관되게 0인 경우도 ExternalApiException으로 처리하는 편이 안전합니다.

적용 예:

-            return subInfo.path(PAGE_COUNT_PARSING_KEY.getValue()).asInt();
+            int pageCount = subInfo.path(PAGE_COUNT_PARSING_KEY.getValue()).asInt();
+            if (pageCount <= 0) {
+                throw new ExternalApiException(BOOK_ALADIN_API_ISBN_NOT_FOUND);
+            }
+            return pageCount;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
JsonNode subInfo = items.get(0).path(SUB_INFO_PARSING_KEY.getValue());
return subInfo.path(PAGE_COUNT_PARSING_KEY.getValue()).asInt();
} catch (IOException e) {
JsonNode subInfo = items.get(0).path(SUB_INFO_PARSING_KEY.getValue());
int pageCount = subInfo.path(PAGE_COUNT_PARSING_KEY.getValue()).asInt();
if (pageCount <= 0) {
throw new ExternalApiException(BOOK_ALADIN_API_ISBN_NOT_FOUND);
}
return pageCount;
} catch (IOException e) {

Comment on lines +64 to 65
throw new ExternalApiException(BOOK_ALADIN_API_PARSING_ERROR);
}
Copy link

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
throw new ExternalApiException(BOOK_ALADIN_API_PARSING_ERROR);
}
} catch (IOException e) {
throw new ExternalApiException(BOOK_ALADIN_API_PARSING_ERROR, e);
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java
around lines 64-65, the code throws ExternalApiException without preserving the
original parsing exception; modify the throw to include the caught exception as
the cause (pass the caught Throwable/Exception into ExternalApiException via its
constructor or a factory method) so the stack trace and root cause are preserved
when rethrowing.

Comment on lines +67 to 68
throw new InternalServerException(BOOK_KEYWORD_ENCODING_FAILED);
}
Copy link

Choose a reason for hiding this comment

The 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" -C3

Length of output: 19126


예외 래핑 시 원인 예외 보존
src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiUtil.java의 catch 블록(67-68, 88-89, 100-103, 120-121)에서 throw 시 두 번째 인자로 e를 전달해 스택 트레이스를 보존하세요.

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiUtil.java around
lines 67-68, 88-89, 100-103 and 120-121, the catch blocks throw
InternalServerException without preserving the original exception; update each
throw to pass the caught exception (e) as the cause/second argument when
constructing InternalServerException so the original stack trace is retained
(e.g., throw new InternalServerException(BOOK_KEYWORD_ENCODING_FAILED, e));
ensure all four catch sites are changed similarly.

Comment on lines +83 to 84
throw new InternalServerException(ErrorCode.JSON_PROCESSING_ERROR);
}
Copy link

Choose a reason for hiding this comment

The 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
In src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java
around lines 83-84 and 109-110, the catch blocks that throw new
InternalServerException(ErrorCode.JSON_PROCESSING_ERROR) discard the original
exception and its stack trace; update those throws to preserve the cause by
passing the caught exception as the cause (e.g., throw new
InternalServerException(ErrorCode.JSON_PROCESSING_ERROR, e)) or otherwise attach
the original throwable so the stack trace and root cause are retained.

Comment on lines +56 to +63
webClient.post()
.uri(webhookUrl)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(payload)
.retrieve()
.bodyToMono(Void.class)
.block();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

요청 스레드에서 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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();
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/discord/DiscordClient.java around lines
56-63, the current code blocks the calling thread with .block() and has no
timeout; change to use a reusable injected WebClient bean (do not create
per-call), send the request asynchronously and fire-and-forget by using the
reactive chain without .block(), add a timeout (e.g., Duration.ofSeconds(5)) on
the Mono, and subscribe with error handling to log failures (or
.doOnError(...).subscribe()). Ensure WebClient is injected as a @Bean and reused
to benefit from connection pooling.

Comment on lines +84 to 85
BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."),

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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.java

Length of output: 625


외부 API ISBN 미존재의 HTTP 상태코드 500 → 400(또는 404)로 변경
알라딘 API에서 ISBN 미존재는 클라이언트 입력 오류이므로 HttpStatus.INTERNAL_SERVER_ERROR 대신 HttpStatus.BAD_REQUEST(또는 도메인 의도에 따라 HttpStatus.NOT_FOUND)로 수정하세요.

-    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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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으로 검색한 결과가 존재하지 않습니다."),
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/exception/code/ErrorCode.java around lines
84-85, the BOOK_ALADIN_API_ISBN_NOT_FOUND entry uses
HttpStatus.INTERNAL_SERVER_ERROR but should represent a client-side error;
change the HttpStatus to HttpStatus.BAD_REQUEST (or HttpStatus.NOT_FOUND if you
prefer domain semantics) while keeping the error code and message intact so the
enum reflects a 4xx status for ISBN-not-found cases.

Comment on lines 33 to 36
} catch (FirebaseMessagingException e) {
log.warn("[FCM:SEND] fail token={} device={} code={} msg={}", maskDependingProfile(fcmToken), maskDependingProfile(deviceId), e.getMessagingErrorCode(), e.getMessage());
throw new FirebaseException(e);
throw new FirebaseException(FIREBASE_SEND_ERROR);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

원인(cause) 없이 예외 래핑 → 추적성 저하

catch 블록에서 원인을 포함해 던지세요.

-        } catch (FirebaseMessagingException e) {
+        } catch (FirebaseMessagingException e) {
             log.warn("[FCM:SEND] fail token={} device={} code={} msg={}", maskDependingProfile(fcmToken), maskDependingProfile(deviceId), e.getMessagingErrorCode(), e.getMessage());
-            throw new FirebaseException(FIREBASE_SEND_ERROR);
+            throw new FirebaseException(FIREBASE_SEND_ERROR, e);
         }
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java
around lines 33 to 36, the catch block wraps FirebaseMessagingException without
preserving the original cause; change the throw to include the caught exception
as the cause (e.g., throw new FirebaseException(FIREBASE_SEND_ERROR, e)); if
FirebaseException lacks a constructor that accepts a cause, add one (or call
initCause(e)) so the original stacktrace is preserved for traceability.

Comment on lines 70 to 73
} catch (FirebaseMessagingException e) {
log.warn("[FCM:BATCH] 메시지 전송 실패: code={} msg={}", e.getMessagingErrorCode(), e.getMessage());
throw new FirebaseException(e);
throw new FirebaseException(FIREBASE_SEND_ERROR);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

배치 전송 실패 예외도 cause 포함

루트 원인 보존을 권장합니다.

-        } catch (FirebaseMessagingException e) {
+        } catch (FirebaseMessagingException e) {
             log.warn("[FCM:BATCH] 메시지 전송 실패: code={} msg={}", e.getMessagingErrorCode(), e.getMessage());
-            throw new FirebaseException(FIREBASE_SEND_ERROR);
+            throw new FirebaseException(FIREBASE_SEND_ERROR, e);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (FirebaseMessagingException e) {
log.warn("[FCM:BATCH] 메시지 전송 실패: code={} msg={}", e.getMessagingErrorCode(), e.getMessage());
throw new FirebaseException(e);
throw new FirebaseException(FIREBASE_SEND_ERROR);
}
} catch (FirebaseMessagingException e) {
log.warn("[FCM:BATCH] 메시지 전송 실패: code={} msg={}", e.getMessagingErrorCode(), e.getMessage());
throw new FirebaseException(FIREBASE_SEND_ERROR, e);
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java
around lines 70 to 73, the catch block for FirebaseMessagingException logs the
error but throws a new FirebaseException without preserving the original
exception; update the throw to include the caught exception as the cause (use
the FirebaseException constructor that accepts a cause or otherwise
wrap/preserve e) so the root cause is retained when rethrowing.

Comment on lines +25 to 26
throw new InternalServerException(NOTIFICATION_REDIRECT_DATA_SERIALIZE_FAILED);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

직렬화 실패 시 원인(cause) 보존

원인 예외를 포함해 던지면 추적이 쉬워집니다.

-        } catch (JsonProcessingException e) {
-            throw new InternalServerException(NOTIFICATION_REDIRECT_DATA_SERIALIZE_FAILED);
+        } catch (JsonProcessingException e) {
+            throw new InternalServerException(NOTIFICATION_REDIRECT_DATA_SERIALIZE_FAILED, e);
         }
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpecConverter.java
around lines 25-26, the catch block throws InternalServerException without
preserving the original exception; modify the code to capture the serialization
exception (e.g., catch Exception e) and rethrow InternalServerException with the
caught exception as the cause by passing it into the InternalServerException
constructor (or calling initCause) so the original stacktrace and message are
preserved for debugging.

Comment on lines +35 to 36
throw new InternalServerException(NOTIFICATION_REDIRECT_DATA_DESERIALIZE_FAILED);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

역직렬화 실패 시 원인(cause) 보존

동일하게 cause 포함 권장.

-        } catch (IOException e) {
-            throw new InternalServerException(NOTIFICATION_REDIRECT_DATA_DESERIALIZE_FAILED);
+        } catch (IOException e) {
+            throw new InternalServerException(NOTIFICATION_REDIRECT_DATA_DESERIALIZE_FAILED, e);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
throw new InternalServerException(NOTIFICATION_REDIRECT_DATA_DESERIALIZE_FAILED);
}
} catch (IOException e) {
throw new InternalServerException(NOTIFICATION_REDIRECT_DATA_DESERIALIZE_FAILED, e);
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpecConverter.java
around lines 35-36, the InternalServerException thrown on deserialization
failure currently omits the original exception; modify the throw to preserve the
cause by passing the caught exception as the cause (e.g., use the
InternalServerException constructor or builder variant that accepts a Throwable)
so the original exception is attached to the new InternalServerException for
debugging and logging.

hd0rable added a commit that referenced this pull request Dec 6, 2025
- 게시글 좋아요 로직이 레디스 원자적 연산으로 수정되면서 게시글 엔티티의 좋아요 정보(likeCount)는 더이상 실시간정보가 아니므로 db 정합성을 확인하는 코드 주석 처리
(#324)
hd0rable added a commit that referenced this pull request Dec 6, 2025
- updateLikeCount()의newLikeCount 매개변수가 추가되면서 테스트코드 수정
(#324)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants