Skip to content

[feat] 책 검색 api 개발#33

Merged
hd0rable merged 25 commits intodevelopfrom
feat/#32-get-book-search
Jun 29, 2025
Merged

[feat] 책 검색 api 개발#33
hd0rable merged 25 commits intodevelopfrom
feat/#32-get-book-search

Conversation

@hd0rable
Copy link
Member

@hd0rable hd0rable commented Jun 27, 2025

#️⃣ 연관된 이슈

closes #32

📝 작업 내용

  • 책 검색 api를 개발하였습니다.
  • 네이버 검색 api를 사용할때 응답형식은 책 상세조회에서 xml만 제공하기 때문에 통일성을 위해 xml로 선택했습니다
  • 페이징 처리도 같이 진행하였습니다.
  • 관련 예외처리도 하였습니다.
  • THIP 서비스에서 페이지를 보여주는 size는 10으로 고정될 것 같아서 상수로 선언해서 사용했습니다.
  • 아직 회원가입/로그인 기능이 개발되지않아 최근검색어 로직은 빠진상태입니다. 추후에 기능 추가하도록 하겠습니다!
  • 또한 , Exception관련해서 리펙할 것들이 보여서 수정해주었습니다.
  • BookQueryControllerTestNaverApiUtilTest 테스트도 진행하였습니다.
  • 추가로 네이머 api의 시크릿키는 수정된버전으로 노션에 업데이트 해두었습니다.

📸 스크린샷

image

💬 리뷰 요구사항

헥사고날 아키텍쳐와 외부 api연동을 같이하다보니 오래걸렸던것같습니다.. ㅜㅜ 아직 헥사고날에 익숙하지않아서 어색한 부분이있다면 알려주세용..!

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

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

Summary by CodeRabbit

  • 신규 기능

    • 도서 검색 API 엔드포인트(/books) 추가 및 검색 결과 페이징 지원.
    • 네이버 도서 검색 API 연동 및 XML 파싱 기능 도입.
    • 검색 결과 및 페이징 정보를 담은 응답 구조 제공.
  • 버그 수정

    • 오류 응답의 JSON 필드명 오타 수정(콜론 제거).
    • 비즈니스 예외 메시지 전달 개선.
  • 테스트

    • 도서 검색 API 및 네이버 API 유틸리티에 대한 단위 테스트 추가.
  • 기타

    • 불필요한 더미 클래스 및 인터페이스 제거.
    • 도서 관련 에러 코드 다수 추가.

@coderabbitai
Copy link

coderabbitai bot commented Jun 27, 2025

Walkthrough

책 검색 결과 조회 API가 네이버 검색 API와 연동되어 새롭게 구현되었습니다. 주요 변경사항으로는 검색 요청을 처리하는 컨트롤러, 서비스, 포트, 어댑터, 외부 API 유틸리티 및 XML 파서가 추가되었고, 관련 예외 및 응답 DTO, 테스트 코드가 도입되었습니다. 불필요한 더미 클래스들은 삭제되었습니다.

Changes

파일/그룹 변경 요약
.../BookQueryController.java BookSearchUseCase 의존성 및 /books GET 엔드포인트 추가
.../request/DummyRequest.java
.../application/port/in/DummyUseCase.java
.../service/BookService.java
불필요한 더미 클래스 및 인터페이스 삭제
.../response/GetBookSearchListResponse.java 검색 결과 응답용 record 및 BookDto, of 팩토리 메서드 추가
.../out/api/BookApiAdapter.java SearchBookQueryPort 구현체로 네이버 API 연동 어댑터 추가
.../out/api/dto/NaverBookParseResult.java 네이버 API 파싱 결과 및 NaverBook record, of 팩토리 메서드 추가
.../port/in/BookSearchUseCase.java 책 검색 유스케이스 인터페이스 추가
.../port/out/SearchBookQueryPort.java 검색 포트 인터페이스 추가
.../service/BookSearchService.java BookSearchUseCase 구현 서비스 및 검색 비즈니스 로직 추가
.../out/api/NaverApiUtil.java 네이버 API 호출 및 예외처리 유틸리티 컴포넌트 추가
.../out/api/NaverBookXmlParser.java 네이버 API XML 파싱 클래스 및 파싱 로직 추가
.../exception/code/ErrorCode.java 책 검색 관련 신규 에러코드 9종 추가
.../exception/BusinessException.java 예외 메시지 전달 방식 개선
.../dto/ErrorResponse.java JSON 프로퍼티명 오타 수정
.../BookQueryControllerTest.java 컨트롤러 단위 테스트(성공/실패/유효성) 추가
.../NaverApiUtilTest.java NaverApiUtil 유닛 테스트(성공/예외) 추가

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant BookQueryController
    participant BookSearchService
    participant BookApiAdapter
    participant NaverApiUtil
    participant NaverBookXmlParser

    Client->>BookQueryController: GET /books?keyword=...&page=...
    BookQueryController->>BookSearchService: searchBooks(keyword, page)
    BookSearchService->>BookApiAdapter: findBooksByKeyword(keyword, start)
    BookApiAdapter->>NaverApiUtil: searchBook(keyword, start)
    NaverApiUtil->>NaverBookXmlParser: parse(xml)
    NaverBookXmlParser-->>NaverApiUtil: NaverBookParseResult
    NaverApiUtil-->>BookApiAdapter: XML String
    BookApiAdapter-->>BookSearchService: NaverBookParseResult
    BookSearchService-->>BookQueryController: NaverBookParseResult
    BookQueryController-->>Client: BaseResponse<GetBookSearchListResponse>
Loading

Assessment against linked issues

Objective Addressed Explanation
네이버 검색 API를 이용한 책 검색 결과 조회 API 개발 (#32)
검색 결과 응답 구조 및 페이징 처리 구현 (#32)
검색 파라미터 유효성 검증 및 예외처리 (#32)
API 단위/유닛 테스트 작성 (#32)

Suggested labels

🧸 현준

Poem

🐇
책을 찾아 hop hop hop,
네이버 API로 점프하며 멈추지 않죠!
예외도 척척, 테스트도 착착,
새 엔드포인트에 꿈을 담아
토끼는 오늘도 코드를 달려요.
📚✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 7

🧹 Nitpick comments (5)
src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverBookParseResult.java (1)

12-14: 정적 팩토리 메서드의 필요성을 재검토해보세요.

현재 of() 메서드는 단순히 생성자를 호출하는 것 외에 추가 로직이 없습니다. Record 클래스는 이미 간결한 생성자를 제공하므로, 별도의 팩토리 메서드가 필요한 특별한 이유가 있는지 확인해보세요.

만약 유지한다면 매개변수 검증을 추가하는 것을 고려해보세요:

public static NaverBookParseResult of(List<Book> books, int total, int start) {
+   if (books == null) {
+       throw new IllegalArgumentException("books 목록은 null일 수 없습니다.");
+   }
+   if (total < 0 || start < 0) {
+       throw new IllegalArgumentException("total과 start는 음수일 수 없습니다.");
+   }
    return new NaverBookParseResult(books, total, start);
}
src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookSearchListResponse.java (1)

7-15: @builder와 Record 조합을 재검토해보세요.

Record 클래스는 이미 간결한 생성자와 정적 팩토리 메서드를 제공합니다. @builder 어노테이션이 정말 필요한지 확인해보세요.

만약 Builder 패턴이 꼭 필요하다면 다음과 같이 명시적으로 구현하는 것을 고려해보세요:

-@Builder
public record GetBookSearchListResponse(
        List<BookDto> searchResult, // 책 목록
        int page,                   // 현재 페이지 (1부터 시작)
        long totalElements,         // 전체 데이터 개수
        int totalPages,             // 전체 페이지 수
        boolean last,               // 마지막 페이지 여부
        boolean first               // 첫 페이지 여부
) {
+   public static Builder builder() {
+       return new Builder();
+   }
+   
+   public static class Builder {
+       // builder implementation
+   }
}

또는 단순히 Record의 기본 생성자를 사용하는 것도 고려해보세요.

src/main/java/konkuk/thip/util/NaverApiUtil.java (3)

56-56: 메소드 접근 제어자를 명시적으로 설정해주세요.

get 메소드가 package-private으로 되어 있습니다. 테스트에서만 사용할 목적이라면 @VisibleForTesting 어노테이션을 추가하거나, 외부에서 사용하지 않을 예정이라면 private으로 변경하는 것을 고려해보세요.


48-48: 표준 상수 사용을 권장합니다.

"UTF-8" 문자열 대신 StandardCharsets.UTF_8을 사용하는 것이 좋습니다.

-text = URLEncoder.encode(keyword, "UTF-8");
+text = URLEncoder.encode(keyword, StandardCharsets.UTF_8);

27-27: 상수를 별도로 관리하는 것을 고려해보세요.

API URL과 같은 상수들을 configuration properties나 별도의 상수 클래스에서 관리하는 것이 유지보수에 도움이 될 것 같습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a49e155 and 02a8d41.

📒 Files selected for processing (18)
  • src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/in/web/request/DummyRequest.java (0 hunks)
  • src/main/java/konkuk/thip/book/adapter/in/web/response/BookDto.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookSearchListResponse.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/api/BookApiAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverBookParseResult.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/port/in/BookSearchUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/port/in/DummyUseCase.java (0 hunks)
  • src/main/java/konkuk/thip/book/application/port/out/SearchBookQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/service/BookSearchService.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/service/BookService.java (0 hunks)
  • src/main/java/konkuk/thip/common/dto/ErrorResponse.java (1 hunks)
  • src/main/java/konkuk/thip/common/exception/BusinessException.java (1 hunks)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1 hunks)
  • src/main/java/konkuk/thip/util/NaverApiUtil.java (1 hunks)
  • src/main/java/konkuk/thip/util/NaverBookXmlParser.java (1 hunks)
  • src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java (1 hunks)
  • src/test/java/konkuk/thip/util/NaverApiUtilTest.java (1 hunks)
💤 Files with no reviewable changes (3)
  • src/main/java/konkuk/thip/book/application/port/in/DummyUseCase.java
  • src/main/java/konkuk/thip/book/adapter/in/web/request/DummyRequest.java
  • src/main/java/konkuk/thip/book/application/service/BookService.java
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java (2)
src/main/java/konkuk/thip/util/NaverApiUtil.java (1)
  • RequiredArgsConstructor (18-107)
src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java (1)
  • RestController (6-10)
🔇 Additional comments (20)
src/main/java/konkuk/thip/common/dto/ErrorResponse.java (1)

11-11: JSON 프로퍼티 어노테이션 수정이 올바릅니다.

불필요한 콜론을 제거하여 JSON 직렬화 시 올바른 키 이름이 생성되도록 수정되었습니다.

src/main/java/konkuk/thip/book/application/port/in/BookSearchUseCase.java (1)

5-9: 인터페이스 설계가 간결하고 명확합니다.

단일 책임 원칙을 잘 따르고 있으며, 메서드 시그니처가 직관적입니다.

src/main/java/konkuk/thip/book/adapter/in/web/response/BookDto.java (1)

3-9: Java record를 활용한 깔끔한 DTO 구현입니다.

불변성이 보장되고 필드명이 직관적이며, 웹 응답용 DTO로 적절하게 설계되었습니다.

src/main/java/konkuk/thip/book/application/port/out/SearchBookQueryPort.java (1)

5-7: 아웃바운드 포트 인터페이스가 잘 설계되었습니다.

외부 API 호출을 추상화하는 역할을 명확히 하고 있으며, 메서드명과 매개변수가 직관적입니다.

src/main/java/konkuk/thip/common/exception/BusinessException.java (2)

10-13: 예외 메시지 전달 로직이 개선되었습니다.

ErrorCode의 메시지를 상위 클래스에 전달하여 예외 메시지가 올바르게 설정되도록 수정되었습니다. 디버깅과 로깅에 도움이 됩니다.


15-18: 예외 체이닝도 올바르게 구현되었습니다.

원본 예외와 함께 에러 메시지도 적절히 전달하여 예외 추적이 용이하도록 개선되었습니다.

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

10-14: 아키텍처 구조가 잘 설계되었습니다.

헥사고날 아키텍처의 아웃바운드 어댑터 패턴을 정확히 따르고 있으며, 의존성 주입도 적절히 구현되어 있습니다.

src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverBookParseResult.java (1)

8-11: Record 구조가 적절하게 설계되었습니다.

페이징 정보와 검색 결과를 담기에 적합한 구조로 설계되었습니다.

src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java (1)

13-13: 의존성 주입이 적절하게 구현되었습니다.

헥사고날 아키텍처에서 인바운드 어댑터가 유스케이스 인터페이스에 의존하는 구조가 올바르게 적용되어 있습니다.

src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookSearchListResponse.java (1)

8-14: 페이징 응답 구조가 잘 설계되었습니다.

검색 결과와 페이징 메타데이터를 포함한 응답 구조가 일반적인 페이징 API 패턴을 잘 따르고 있으며, 주석도 명확합니다.

src/test/java/konkuk/thip/util/NaverApiUtilTest.java (1)

33-48: 예외 처리 테스트가 잘 구현되었습니다.

BusinessException 발생 시나리오를 적절히 테스트하고 있으며, 예외 메시지까지 검증하는 것이 좋습니다.

src/main/java/konkuk/thip/util/NaverApiUtil.java (1)

67-69: 오류 응답 처리 방식을 재검토해주세요.

HTTP 오류 상태 코드일 때도 응답 본문을 읽어서 반환하고 있습니다. 이 경우 오류 정보가 포함된 응답을 그대로 반환하는 것이 적절한지 검토가 필요합니다. 오류 응답에 대해서는 별도의 예외 처리를 고려해보세요.

src/main/java/konkuk/thip/util/NaverBookXmlParser.java (1)

56-62: 안전한 파싱 로직이 잘 구현되었습니다.

getTagValue 메소드에서 null 체크를 통해 안전한 파싱을 구현한 것이 좋습니다.

src/main/java/konkuk/thip/book/application/service/BookSearchService.java (3)

27-33: 입력 검증 로직이 잘 구현되었습니다.

키워드와 페이지 번호에 대한 적절한 검증 로직이 구현되어 있습니다.


37-44: 페이징 로직이 정확하게 구현되었습니다.

시작 위치 계산과 총 페이지 수 계산, 범위 검증이 올바르게 구현되어 있습니다.


35-35: 향후 개발 계획이 명확히 표시되었습니다.

최근 검색어 로직에 대한 TODO 주석이 적절히 남겨져 있습니다.

src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java (3)

26-38: 테스트 케이스가 적절하게 구성되었습니다.

성공 케이스에 대한 응답 구조 검증이 잘 되어 있습니다.


40-76: 실패 케이스 테스트가 체계적으로 작성되었습니다.

다양한 실패 시나리오(페이지 범위 초과, 키워드 누락, 잘못된 페이지 번호)에 대한 테스트가 잘 구성되어 있습니다.


17-19: ```bash
#!/bin/bash
set -e

echo "== BookSearchUseCase Interface =="
iface=$(find . -type f -name BookSearchUseCase.java | head -n1)
echo "$iface"
sed -n '1,200p' "$iface"
echo

echo "== BookSearchUseCase Implementation =="
impl=$(grep -R "implements BookSearchUseCase" -n src/main/java | cut -d: -f1 | head -n1)
echo "$impl"
sed -n '1,200p' "$impl"
echo

echo "== HTTP Client Usage Across Project =="
grep -R "RestTemplate" -n src/main/java || true
grep -R "WebClient" -n src/main/java || true
grep -R "@FeignClient" -n src/main/java || true


</details>
<details>
<summary>src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)</summary>

`26-38`: **에러 코드가 체계적으로 잘 정의되었습니다.**

책 검색 관련 에러 코드들이 80000번대로 체계적으로 분류되어 있고, 각 에러에 대한 명확한 한국어 메시지가 제공되어 있습니다. HTTP 상태 코드 매핑도 적절합니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

@@ -0,0 +1,9 @@
package konkuk.thip.book.application.port.in;

import konkuk.thip.book.adapter.in.web.response.GetBookSearchListResponse;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

아키텍처 의존성 방향 위반을 수정해주세요.

인바운드 포트에서 어댑터 계층의 응답 DTO를 직접 참조하고 있어 헥사고날 아키텍처 원칙에 위배됩니다. 응용 계층은 어댑터 계층에 의존하지 않아야 합니다.

다음 중 하나의 방식으로 개선을 권장합니다:

  1. 도메인 모델을 직접 반환
  2. 응용 계층에 별도의 응답 DTO 생성
  3. 어댑터에서 도메인 모델을 웹 응답 DTO로 변환
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/application/port/in/BookSearchUseCase.java at
line 3, the inbound port improperly depends on the adapter layer's response DTO,
violating hexagonal architecture principles. To fix this, remove the import of
the adapter's response DTO and instead have the application layer return either
the domain model directly or define a separate response DTO within the
application layer. Then, perform any necessary conversion to the web response
DTO within the adapter layer, ensuring the application layer does not depend on
the adapter layer.

Comment on lines +17 to +20
public NaverBookParseResult findBooksByKeyword(String keyword, int start) {
String xml = naverApiUtil.searchBook(keyword, start); // 네이버 API 호출
return NaverBookXmlParser.parse(xml); // XML 파싱 + 페이징 정보 포함
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

입력 매개변수 검증과 예외 처리 개선이 필요합니다.

현재 구현에서 다음 사항들을 고려해보세요:

  1. keyword 매개변수가 null이거나 빈 문자열일 경우에 대한 검증
  2. start 매개변수가 음수일 경우에 대한 검증
  3. naverApiUtil.searchBook() 호출 시 발생할 수 있는 예외에 대한 명시적 처리
@Override
public NaverBookParseResult findBooksByKeyword(String keyword, int start) {
+   if (keyword == null || keyword.trim().isEmpty()) {
+       throw new IllegalArgumentException("검색 키워드는 필수입니다.");
+   }
+   if (start < 1) {
+       throw new IllegalArgumentException("시작 인덱스는 1 이상이어야 합니다.");
+   }
+   
    String xml = naverApiUtil.searchBook(keyword, start); // 네이버 API 호출
    return NaverBookXmlParser.parse(xml);                // XML 파싱 + 페이징 정보 포함
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/adapter/out/api/BookApiAdapter.java around
lines 17 to 20, add input validation to check if the keyword is null or empty
and if start is negative, throwing appropriate exceptions if invalid. Also, wrap
the call to naverApiUtil.searchBook(keyword, start) in a try-catch block to
explicitly handle any exceptions it may throw, and handle or rethrow them with
meaningful messages.

Comment on lines 15 to 19
@GetMapping("/books")
public BaseResponse<GetBookSearchListResponse> getBookSearchList(@RequestParam final String keyword,
@RequestParam final int page) {
return BaseResponse.ok(bookSearchUseCase.searchBooks(keyword, page));
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

API 엔드포인트 설계를 개선해보세요.

다음 사항들을 고려해보시기 바랍니다:

  1. 매개변수 검증: @RequestParam에 검증 어노테이션 추가
  2. 엔드포인트 경로: /books보다는 /books/search 같은 더 명확한 경로 고려
  3. API 문서화: @Operation 등의 어노테이션으로 API 문서화 추가
-@GetMapping("/books")
-public BaseResponse<GetBookSearchListResponse> getBookSearchList(@RequestParam final String keyword,
-                                                                 @RequestParam final int page) {
+@GetMapping("/books/search")
+public BaseResponse<GetBookSearchListResponse> getBookSearchList(
+        @RequestParam @NotBlank(message = "검색 키워드는 필수입니다.") final String keyword,
+        @RequestParam @Min(value = 1, message = "페이지는 1 이상이어야 합니다.") final int page) {
    return BaseResponse.ok(bookSearchUseCase.searchBooks(keyword, page));
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java around
lines 15 to 19, improve the API endpoint by renaming the path from "/books" to a
more descriptive "/books/search", add validation annotations like @NotBlank for
the keyword and @Min(1) for the page parameter on the @RequestParam fields, and
include API documentation annotations such as @Operation to describe the
endpoint's purpose and parameters.

Comment on lines 13 to 49
@Test
@DisplayName("정상 응답을 모킹하여 반환")
void searchBook_success_mocking() {
// given
NaverApiUtil naverApiUtil = Mockito.spy(new NaverApiUtil());

// get 메서드를 spy로 모킹 (실제 네트워크 호출 없이 원하는 응답 반환)
String expectedXml = "<rss><channel><total>1</total><start>1</start></channel></rss>";
Mockito.doReturn(expectedXml)
.when(naverApiUtil)
.get(Mockito.anyString(), Mockito.anyMap());

// when
String result = naverApiUtil.searchBook("테스트", 1);

// then
assertThat(result).contains("<total>1</total>");
assertThat(result).contains("<start>1</start>");
}

@Test
@DisplayName("get 메서드에서 예외가 발생하면 BusinessException이 발생")
void searchBook_ioException() {
// given
NaverApiUtil naverApiUtil = Mockito.spy(new NaverApiUtil());

// get 메서드가 예외를 던지도록 설정
Mockito.doThrow(new BusinessException(BOOK_NAVER_API_REQUEST_ERROR))
.when(naverApiUtil)
.get(Mockito.anyString(), Mockito.anyMap());

// when & then
assertThatThrownBy(() -> naverApiUtil.searchBook("테스트", 1))
.isInstanceOf(BusinessException.class)
.hasMessageContaining(BOOK_NAVER_API_REQUEST_ERROR.getMessage());
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

추가 테스트 케이스를 고려해보세요.

현재 테스트는 기본적인 성공/실패 케이스만 다루고 있습니다. 다음 케이스들도 추가해보세요:

  • 빈 검색 결과 응답 테스트
  • 잘못된 XML 형식 응답 테스트
  • 네트워크 타임아웃 테스트
  • 다양한 HTTP 상태 코드 응답 테스트
  • 키워드 인코딩 실패 테스트
@Test
@DisplayName("빈 검색 결과 응답 처리")
void searchBook_emptyResult() {
    // 구현 예시
}

@Test
@DisplayName("잘못된 XML 형식 응답 처리")
void searchBook_malformedXml() {
    // 구현 예시
}
🤖 Prompt for AI Agents
In src/test/java/konkuk/thip/util/NaverApiUtilTest.java around lines 13 to 49,
the current tests only cover basic success and exception cases. Add new test
methods to cover additional scenarios: one for handling empty search results by
mocking the get method to return an XML with no items, another for malformed XML
responses by returning invalid XML strings, a test simulating network timeouts
by throwing a timeout exception from get, tests for various HTTP status codes by
mocking responses accordingly, and a test for keyword encoding failures by
simulating encoding exceptions. Each test should assert that the searchBook
method handles these cases gracefully or throws expected exceptions.

Comment on lines 17 to 31
NaverApiUtil naverApiUtil = Mockito.spy(new NaverApiUtil());

// get 메서드를 spy로 모킹 (실제 네트워크 호출 없이 원하는 응답 반환)
String expectedXml = "<rss><channel><total>1</total><start>1</start></channel></rss>";
Mockito.doReturn(expectedXml)
.when(naverApiUtil)
.get(Mockito.anyString(), Mockito.anyMap());

// when
String result = naverApiUtil.searchBook("테스트", 1);

// then
assertThat(result).contains("<total>1</total>");
assertThat(result).contains("<start>1</start>");
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

테스트 접근법을 개선해보세요.

현재 Spy를 사용한 접근법보다는 의존성을 모킹하는 방식이 더 적절할 수 있습니다:

@Test
@DisplayName("정상 응답을 모킹하여 반환")
void searchBook_success_mocking() {
    // given
-   NaverApiUtil naverApiUtil = Mockito.spy(new NaverApiUtil());
+   NaverApiUtil naverApiUtil = new NaverApiUtil();
+   // 또는 @MockBean을 사용하여 스프링 컨텍스트에서 테스트

-   // get 메서드를 spy로 모킹 (실제 네트워크 호출 없이 원하는 응답 반환)
    String expectedXml = "<rss><channel><total>1</total><start>1</start></channel></rss>";
-   Mockito.doReturn(expectedXml)
-           .when(naverApiUtil)
-           .get(Mockito.anyString(), Mockito.anyMap());
+   // 실제 통합 테스트를 위해 WireMock 등을 사용하거나
+   // 유닛 테스트를 위해 NaverApiUtil의 의존성을 주입받도록 리팩토링

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

🤖 Prompt for AI Agents
In src/test/java/konkuk/thip/util/NaverApiUtilTest.java around lines 17 to 31,
the test uses a spy on NaverApiUtil to mock the get method, which is not ideal.
Instead, refactor the test to mock the dependency that NaverApiUtil uses for
network calls (e.g., an HTTP client) rather than spying on the class itself.
This will isolate the unit test better and avoid partial mocking, improving test
reliability and clarity.

Comment on lines 50 to 52
} catch (Exception e) {
throw new BusinessException(ErrorCode.BOOK_NAVER_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

예외 처리를 구체적으로 개선해주세요.

모든 예외를 Exception으로 catch하고 있습니다. XML 파싱 관련 예외들(ParserConfigurationException, SAXException, IOException)을 개별적으로 처리하거나, 최소한 로그를 남겨서 디버깅에 도움이 되도록 하는 것이 좋겠습니다.

-} catch (Exception e) {
-   throw new BusinessException(ErrorCode.BOOK_NAVER_API_PARSING_ERROR);
-}
+} catch (ParserConfigurationException | SAXException | IOException e) {
+   // 로그 추가를 고려해보세요
+   throw new BusinessException(ErrorCode.BOOK_NAVER_API_PARSING_ERROR);
+}
📝 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 (Exception e) {
throw new BusinessException(ErrorCode.BOOK_NAVER_API_PARSING_ERROR);
}
} catch (ParserConfigurationException | SAXException | IOException e) {
// 로그 추가를 고려해보세요
throw new BusinessException(ErrorCode.BOOK_NAVER_API_PARSING_ERROR);
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/util/NaverBookXmlParser.java around lines 50 to 52,
the code catches a generic Exception which obscures the specific XML parsing
errors. Refactor the catch block to handle ParserConfigurationException,
SAXException, and IOException separately or at least log the exception details
before throwing the BusinessException. This will improve error traceability and
debugging.

Copy link
Collaborator

@seongjunnoh seongjunnoh left a comment

Choose a reason for hiding this comment

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

고생하셨습니다! 리뷰단 부분 확인해주세요!

Comment on lines 33 to 48
for (int i = 0; i < itemNodes.getLength(); i++) {
Element item = (Element) itemNodes.item(i);
String title = getTagValue(item, "title");
String imageUrl = getTagValue(item, "image");
String authorName = getTagValue(item, "author");
String publisher = getTagValue(item, "publisher");
String isbn = getTagValue(item, "isbn");
Book book = Book.builder()
.title(title)
.imageUrl(imageUrl)
.authorName(authorName)
.publisher(publisher)
.isbn(isbn)
.build();
books.add(book);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

p2 : 현재 플로우가 xml 에서 검색한 책 하나의 정보를 파싱해서 Book 도메인 엔티티로 변환 -> List 을 포함하는 NaverBookParseResult 를 구성 -> service에서 result를 다시 response dto로 변환 하는 구조인데,

NaverBookParseResult 가 내부에 하나의 책 조회 정보에 해당하는 inner class 를 가지고, List 를 필드값으로 가진 후, Response dto 객체가 내부적으로 result를 받아서 자기 자신으로 매핑하도록 수정하는건 어떤가요??

adapter 계층에서 Naver 조회 결과를 담을 dto 로 Book 도메인 엔티티를 사용하는 것보다는, dto class 를 새로 만드는 것이 더 좋지 않나 생각합니다! (저희 Book 도메인의 구조가 바뀔 수도 있으니)

예시코드

record NaverBookParseResult(LIst<NaverBook> naverBooks, ,,, ) {
  recore NaverBook(
    String title,
    String imageUrl,
    String author,
    String publisher,
    String isbn) { }
}

public record GetBookSearchListResponse(
        List<BookDto> searchResult, // 책 목록
        int page,                   // 현재 페이지 (1부터 시작)
        long totalElements,         // 전체 데이터 개수
        int totalPages,             // 전체 페이지 수
        boolean last,               // 마지막 페이지 여부
        boolean first               // 첫 페이지 여부
) {
  public static GetBookSearchListResponse of(NaverBookParseResult result) {
    // result -> response 로의 매핑 작업 진행
  }
}

public class BookSearchservice {
  // 1. 네이버 api 통신 -> 반환값 = NaverBookParseResult
  // 2. 반환받은 result에 대한 예외 처리 (필요하다면)
  // 3. return -> 반환 형식을 Response dto 로 유지해도 되고, 아니면 Result로 수정해도 될 듯
  // 대신 resutl -> response dto의 매핑 책임은 response dto 내부로 위임
}

Copy link
Contributor

Choose a reason for hiding this comment

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

@seongjunnoh 성준님 말은 네이버로 파싱 받는 단일 책 정보와 저희 서비스의 책 도메인을 분리하자는 말씀이시죠? 제가 이해한게 맞다면 저는 좋다고 생각합니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

넵 수정할게용

Copy link
Collaborator

Choose a reason for hiding this comment

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

@seongjunnoh 성준님 말은 네이버로 파싱 받는 단일 책 정보와 저희 서비스의 책 도메인을 분리하자는 말씀이시죠? 제가 이해한게 맞다면 저는 좋다고 생각합니다!

넵 맞습니다! 네이버 api 로부터 받은 데이터들을 담는 컨테이너의 역할로 저희 Book 도메인 엔티티를 사용하는 것보다는, 추가적으로 dto를 생성하는게 어떤가 라는 말이었습니다!

Comment on lines 24 to 25
@Override
public GetBookSearchListResponse searchBooks(String keyword, int page) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3 : 보통 controller의 request, response dto 는 application 계층과 분리시키는 것이 헥사고날에서는 일반적인 것으로 알고 있습니다만 조회 로직에 한해서는 괜찮다고 생각합니다 (어차피 controller에서의 response나 service에서 반환하는 것이나 동일한 정보일 것이므로)

저같은 경우는 조회 api에서
service에서 Result 라는 record 반환(DummyResult 참고) -> 이를 controller 에서 response dto 가 result -> response 로의 변환 책임을 갖도록 한 후에 response 를 반환
하도록 구성하긴 합니다!

service의 반환 dto인 Result는 영속성 계층에서도 공유하도록 하고(이것마저 분리하면 피곤하므로), controller 와 application 계층의 의존성만을 끊어주는 식으로 평소에 구현하는 편인데, 이 부분은 저희가 정하기 나름이라 @buzz0331 님과 같이 의논해보면 좋을거 같습니다!

Copy link
Contributor

Choose a reason for hiding this comment

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

엇 이부분은 위 리뷰에 적어두었습니다! 저도 성준님 의견에 동의합니다~ 👍🏻

Copy link
Member Author

Choose a reason for hiding this comment

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

아 확인했습니다 수정하겠습니다!

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class BookQueryControllerTest {
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

Comment on lines 13 to 15
@Test
@DisplayName("정상 응답을 모킹하여 반환")
void searchBook_success_mocking() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3 : displayName 부분에는 개발한 부분이 어떤 기능을 담당하는지, 현재 테스트 메서드에서는 해당 기능의 어떤 부분을 테스트하는 건지에 대한 정보를 담아주시면 좋을거 같습니다!

Comment on lines 19 to 23
// get 메서드를 spy로 모킹 (실제 네트워크 호출 없이 원하는 응답 반환)
String expectedXml = "<rss><channel><total>1</total><start>1</start></channel></rss>";
Mockito.doReturn(expectedXml)
.when(naverApiUtil)
.get(Mockito.anyString(), Mockito.anyMap());
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@buzz0331 buzz0331 left a comment

Choose a reason for hiding this comment

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

고생 많으셨어요~!! 리뷰 확인 부탁드릴게요!

제가 알기론 Spring 프레임워크에서 외부 api 호출시 HttpUrlConnection보다 RestTemplate이 더 적절하다고 아는데 추후에 리팩토링할때 한번 바꿔봐욥!

Comment on lines 5 to 9
public interface BookSearchUseCase {

GetBookSearchListResponse searchBooks(String keyword, int page);

}
Copy link
Contributor

Choose a reason for hiding this comment

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

p3: 여기서 코래 말대로 GetBookSearchListResponse는 adapter.in.web에서 사용하는 dto이기 때문에 의존성 방향에 위반되는 것 같아요. 여기선 ListOfBookSearch 처럼 특정 dto 하나를 반환해서 controller에서 매핑하는게 맞을 것 같아요.

성준님 레포지토리 참고하였습니다 ! ㅎ 스페이스 참고 코드

Copy link
Member Author

Choose a reason for hiding this comment

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

음그럼 같은 역할을 하는 dto를 하나 더 만들자는 말씀이신가요?

Copy link
Collaborator

@seongjunnoh seongjunnoh Jun 29, 2025

Choose a reason for hiding this comment

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

음 service의 메서드가 희진님이 정의하신 Result dto를 반환하도록 하고, controller 에서는 response dto 가 반환된 result를 response dto 자기 자신으로 변환해 반환하도록 수정하면 추가적인 dto를 만들지는 않아도 될 것 같습니다!
제 리뷰 중 예시코드를 포함한 리뷰에서의 답변과 동일합니다!

Comment on lines 24 to 25
@Override
public GetBookSearchListResponse searchBooks(String keyword, int page) {
Copy link
Contributor

Choose a reason for hiding this comment

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

엇 이부분은 위 리뷰에 적어두었습니다! 저도 성준님 의견에 동의합니다~ 👍🏻

public class ErrorResponse {

@JsonProperty("isSuccess:")
@JsonProperty("isSuccess")
Copy link
Contributor

Choose a reason for hiding this comment

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

👍🏻

Comment on lines +11 to +16
super(errorCode.getMessage());
this.errorCode = errorCode;
}

public BusinessException(ErrorCode errorCode, Exception e) {
super(e);
super(errorCode.getMessage(), e);
Copy link
Contributor

Choose a reason for hiding this comment

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

혹시 이 부분 수정하신 이유가 따로 있나요??

Copy link
Member Author

Choose a reason for hiding this comment

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

테스트코드를 작성하는 도중에 원래 기존에 코드에서는 에러메세지가 부모클래스에게 전달되지않아 getMessage()가 null이 되었던 문제를 수정하기위해 코드를 수정했습니닷!

@Value("${naver.clientSecret}")
private String clientSecret;

private final String NAVER_BOOK_SEARCH_URL = "https://openapi.naver.com/v1/search/book.xml?query="; //책 검색 결과 조회
Copy link
Contributor

Choose a reason for hiding this comment

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

이것도 환경변수로 넣는거 어떻게 생각하시나요!

Copy link
Member Author

Choose a reason for hiding this comment

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

넵넵 넣는게 좋을것같네요

Comment on lines 33 to 48
for (int i = 0; i < itemNodes.getLength(); i++) {
Element item = (Element) itemNodes.item(i);
String title = getTagValue(item, "title");
String imageUrl = getTagValue(item, "image");
String authorName = getTagValue(item, "author");
String publisher = getTagValue(item, "publisher");
String isbn = getTagValue(item, "isbn");
Book book = Book.builder()
.title(title)
.imageUrl(imageUrl)
.authorName(authorName)
.publisher(publisher)
.isbn(isbn)
.build();
books.add(book);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

@seongjunnoh 성준님 말은 네이버로 파싱 받는 단일 책 정보와 저희 서비스의 책 도메인을 분리하자는 말씀이시죠? 제가 이해한게 맞다면 저는 좋다고 생각합니다!

@hd0rable
Copy link
Member Author

고생 많으셨어요~!! 리뷰 확인 부탁드릴게요!

제가 알기론 Spring 프레임워크에서 외부 api 호출시 HttpUrlConnection보다 RestTemplate이 더 적절하다고 아는데 추후에 리팩토링할때 한번 바꿔봐욥!

RestTemplate로 바꾸려고했느데 ㅎ.. 통신은되는데 반환값이 안넘어오는 문제가 발생해서.. 일단 급하게 pr올린거였어욥..! 추후에 리펙할때 고려해보도록하겠습니다!

@sonarqubecloud
Copy link

@hd0rable
Copy link
Member Author

hd0rable commented Jun 29, 2025

리뷰사항 반영해서 코드 수정했습니다!

  1. NaverBookParseResult에서 도메인 Book이 아닌 자체 NaverBook으로 변경
  2. naverAPI에 display추가, page에 size값 추가
  3. controller의 response dto application 계층과 분리

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: 2

♻️ Duplicate comments (1)
src/main/java/konkuk/thip/book/application/port/in/BookSearchUseCase.java (1)

3-7: 헥사고날 아키텍처 의존성 방향 위반을 수정해주세요.

인바운드 포트에서 어댑터 계층의 응답 DTO를 직접 참조하고 있어 헥사고날 아키텍처 원칙에 위배됩니다. 응용 계층은 어댑터 계층에 의존하지 않아야 합니다.

응용 계층에 별도의 응답 DTO를 생성하고, 어댑터에서 도메인 모델을 웹 응답 DTO로 변환하는 방식으로 개선을 권장합니다.

🧹 Nitpick comments (3)
src/main/java/konkuk/thip/book/adapter/out/api/NaverBookXmlParser.java (2)

16-16: 변수 명명 규칙을 수정해주세요.

변수명이 PascalCase로 되어 있습니다. Java 컨벤션에 따라 camelCase로 수정해주세요.

-List<NaverBookParseResult.NaverBook> Naverbooks = new ArrayList<>();
+List<NaverBookParseResult.NaverBook> naverBooks = new ArrayList<>();

그리고 line 46에서도 함께 수정이 필요합니다:

-Naverbooks.add(naverBook);
+naverBooks.add(naverBook);

마지막으로 line 52에서도 수정해주세요:

-return NaverBookParseResult.of(Naverbooks, total, start);
+return NaverBookParseResult.of(naverBooks, total, start);

28-29: parseInt 호출 시 예외 처리를 개선해주세요.

Integer.parseInt() 호출 시 NumberFormatException이 발생할 수 있습니다. 현재는 catch 블록에서 모든 예외를 처리하고 있지만, 더 명확한 에러 메시지를 제공할 수 있습니다.

-total = Integer.parseInt(getTagValue(channel, "total"));
-start = Integer.parseInt(getTagValue(channel, "start"));
+try {
+    total = Integer.parseInt(getTagValue(channel, "total"));
+    start = Integer.parseInt(getTagValue(channel, "start"));
+} catch (NumberFormatException e) {
+    throw new BusinessException(ErrorCode.BOOK_NAVER_API_PARSING_ERROR);
+}
src/main/java/konkuk/thip/book/application/service/BookSearchService.java (1)

10-10: 어댑터 계층과의 결합도를 줄여주세요.

SERVICE 계층에서 어댑터 계층의 상수(PAGE_SIZE)를 직접 참조하고 있습니다. 이는 계층 간 결합도를 높입니다.

-import static konkuk.thip.book.adapter.out.api.NaverApiUtil.PAGE_SIZE;

그리고 상수를 응용 계층에 정의하거나 설정 파일로 분리하는 것을 권장합니다:

+private static final int PAGE_SIZE = 10;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02a8d41 and 7f884da.

📒 Files selected for processing (9)
  • src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookSearchListResponse.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/api/BookApiAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/api/NaverApiUtil.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/api/NaverBookXmlParser.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverBookParseResult.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/port/in/BookSearchUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/service/BookSearchService.java (1 hunks)
  • src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/konkuk/thip/book/adapter/out/api/BookApiAdapter.java
  • src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java
  • src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverBookParseResult.java
🔇 Additional comments (10)
src/main/java/konkuk/thip/book/adapter/out/api/NaverBookXmlParser.java (1)

55-61: Helper 메서드 구현이 잘 되어있습니다.

null-safe한 태그 값 추출 로직이 적절하게 구현되어 있어 XML 파싱 시 발생할 수 있는 NPE를 방지합니다.

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

29-43: PAGE_SIZE 상수와 URL 구성 로직이 적절합니다.

페이지 크기를 상수로 정의하고 URL 파라미터를 명확하게 구성하는 방식이 좋습니다. API 호출 시 필요한 헤더 설정도 적절하게 처리되었습니다.

src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java (3)

15-34: 테스트 설정을 위한 리플렉션 사용이 적절합니다.

@value 어노테이션으로 주입되는 필드들을 테스트에서 설정하기 위해 리플렉션을 사용하는 것은 일반적인 접근 방식입니다. 예외 처리도 적절하게 되어 있습니다.

대안으로 @TestPropertySource나 @MockBean을 사용하는 방법도 고려해볼 수 있지만, 현재 방식도 충분히 효과적입니다.


36-53: 성공 케이스 테스트가 잘 구성되어있습니다.

Mockito를 사용하여 외부 의존성을 적절히 모킹하고, 예상되는 XML 응답 구조를 검증하는 방식이 효과적입니다.


55-69: 예외 상황 테스트가 포함되어 좋습니다.

BusinessException 발생 시나리오를 테스트하여 에러 처리 로직을 검증하는 것이 좋은 테스트 관행입니다.

src/main/java/konkuk/thip/book/application/service/BookSearchService.java (2)

22-28: 입력값 검증 로직이 잘 구현되어있습니다.

키워드와 페이지 번호에 대한 적절한 검증 로직이 포함되어 있고, 구체적인 에러 코드로 예외를 발생시키는 방식이 좋습니다.


32-39: 페이징 로직이 정확하게 구현되어있습니다.

시작 인덱스 계산과 총 페이지 수 계산, 페이지 범위 검증이 올바르게 구현되어 있습니다. 빈 결과에 대한 예외 처리도 적절합니다.

src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookSearchListResponse.java (3)

10-19: 레코드 구조가 깔끔하게 잘 설계되었습니다.

페이지네이션에 필요한 모든 필드가 적절하게 포함되어 있고, 한국어 주석으로 각 필드의 역할이 명확하게 설명되어 있습니다.


26-28: 스트림을 이용한 DTO 변환이 적절합니다.

toList()를 사용한 것이 좋고, 팩토리 메서드 패턴을 통한 변환도 깔끔합니다.


40-56: 중첩 레코드를 통한 DTO 설계가 잘 되어 있습니다.

BookDto의 구조가 간결하고 팩토리 메서드도 적절하게 구현되었습니다. 필드명도 명확하고 일관성이 있습니다.

Comment on lines +66 to +70
if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
return readBody(con.getInputStream());
} else { // 오류 발생
return readBody(con.getErrorStream());
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

HTTP 에러 응답 처리 방식을 개선해주세요.

현재 HTTP 에러 상태 코드를 받았을 때 에러 스트림의 내용을 그대로 반환하고 있습니다. 이는 상위 계층에서 에러 응답을 정상 응답으로 오해할 수 있습니다.

int responseCode = con.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
    return readBody(con.getInputStream());
} else { // 오류 발생
-    return readBody(con.getErrorStream());
+    String errorBody = readBody(con.getErrorStream());
+    throw new BusinessException(BOOK_NAVER_API_REQUEST_ERROR);
}
📝 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
if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
return readBody(con.getInputStream());
} else { // 오류 발생
return readBody(con.getErrorStream());
}
int responseCode = con.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
return readBody(con.getInputStream());
} else { // 오류 발생
String errorBody = readBody(con.getErrorStream());
throw new BusinessException(BOOK_NAVER_API_REQUEST_ERROR);
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/adapter/out/api/NaverApiUtil.java lines 66 to
70, the method returns the error stream content as a normal response when the
HTTP status is not OK, which can cause confusion in upper layers. Modify the
code to detect error responses and throw an exception or return an error
indicator instead of returning the error stream content directly, so that error
handling can be properly distinguished from successful responses.

Comment on lines +20 to +24
public static GetBookSearchListResponse of(NaverBookParseResult result, int page) {
int totalElements = result.total();
int totalPages = (int) Math.ceil((double) totalElements / PAGE_SIZE);
boolean last = (page >= totalPages);
boolean first = (page == 1);
Copy link

@coderabbitai coderabbitai bot Jun 29, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

페이지네이션 로직에서 엣지 케이스 처리가 필요합니다.

다음과 같은 잠재적 문제들이 있습니다:

  • totalElements가 0일 때 totalPages도 0이 되어 last 계산에 오류가 발생할 수 있습니다
  • page 매개변수에 대한 유효성 검증이 없습니다 (음수나 0값 처리)

다음과 같이 개선할 수 있습니다:

 public static GetBookSearchListResponse of(NaverBookParseResult result, int page) {
+    if (page <= 0) {
+        throw new IllegalArgumentException("페이지 번호는 1 이상이어야 합니다");
+    }
+    
     int totalElements = result.total();
-    int totalPages = (int) Math.ceil((double) totalElements / PAGE_SIZE);
-    boolean last = (page >= totalPages);
+    int totalPages = totalElements == 0 ? 1 : (int) Math.ceil((double) totalElements / PAGE_SIZE);
+    boolean last = (page >= totalPages);
     boolean first = (page == 1);
📝 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
public static GetBookSearchListResponse of(NaverBookParseResult result, int page) {
int totalElements = result.total();
int totalPages = (int) Math.ceil((double) totalElements / PAGE_SIZE);
boolean last = (page >= totalPages);
boolean first = (page == 1);
public static GetBookSearchListResponse of(NaverBookParseResult result, int page) {
if (page <= 0) {
throw new IllegalArgumentException("페이지 번호는 1 이상이어야 합니다");
}
int totalElements = result.total();
int totalPages = totalElements == 0
? 1
: (int) Math.ceil((double) totalElements / PAGE_SIZE);
boolean last = (page >= totalPages);
boolean first = (page == 1);
// …rest of method…
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookSearchListResponse.java
around lines 20 to 24, the pagination logic does not handle edge cases properly.
Fix this by adding validation for the page parameter to ensure it is at least 1,
and adjust the calculation of totalPages so that when totalElements is 0,
totalPages is set to 1 to avoid incorrect last page determination. Update the
last and first boolean calculations accordingly to reflect these changes.

Copy link
Collaborator

Choose a reason for hiding this comment

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

검색 keyword에 대한 totalElements 값이 0일 경우, totalPages 또한 0이 되는데, 이 경우 response dto는

searchResult = emptyList, page = 1, size = 10, totalElements = 0, totalPages = 0, last = true, first = true

이므로 문제없을 것 같습니다

Copy link

Choose a reason for hiding this comment

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

@seongjunnoh 맞는 말씀입니다! totalElements = 0일 때의 응답 구조를 정확히 분석해주셨네요.

totalPages = 0으로 설정하는 것은 수학적으로 올바르고, 응답 DTO의 각 필드들이 일관성 있게 설정됩니다:

  • last = true (1페이지가 0페이지보다 크거나 같으므로 마지막 페이지)
  • first = true (1페이지이므로 첫 페이지)

이는 유효한 설계 선택입니다. 빈 결과에 대해 totalPages = 0을 사용하는 시스템과 totalPages = 1을 사용하는 시스템이 모두 존재하며, 현재 구현은 수학적으로 정확합니다.

다만 제가 언급한 page 매개변수의 음수/0 값에 대한 유효성 검증 부분은 여전히 고려해볼 만한 것 같습니다. 하지만 이미 BookSearchService에서 검증하고 있다면 중복 검증일 수도 있겠네요.

🐰
╭─────╮
│ 분석 │
│ 완료 │
╰─────╯

@hd0rable hd0rable requested review from buzz0331 and seongjunnoh June 29, 2025 08:49
Copy link
Collaborator

@seongjunnoh seongjunnoh left a comment

Choose a reason for hiding this comment

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

고생하셨습니다!


private String buildSearchApiUrl(String query,Integer start) {
return NAVER_BOOK_SEARCH_URL+query+"&start="+start;
return bookSearchUrl+query+"&display="+PAGE_SIZE+"&start="+start;
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

public record GetBookSearchListResponse(
List<BookDto> searchResult, // 책 목록
int page, // 현재 페이지 (1부터 시작)
int size, // 한 페이지에 포함되는 데이터 수 (페이지 크기)
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

Comment on lines +20 to +24
public static GetBookSearchListResponse of(NaverBookParseResult result, int page) {
int totalElements = result.total();
int totalPages = (int) Math.ceil((double) totalElements / PAGE_SIZE);
boolean last = (page >= totalPages);
boolean first = (page == 1);
Copy link
Collaborator

Choose a reason for hiding this comment

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

검색 keyword에 대한 totalElements 값이 0일 경우, totalPages 또한 0이 되는데, 이 경우 response dto는

searchResult = emptyList, page = 1, size = 10, totalElements = 0, totalPages = 0, last = true, first = true

이므로 문제없을 것 같습니다


public record NaverBookParseResult(
List<Book> books,
List<NaverBook> naverBooks,
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

Comment on lines 53 to 52
return NaverBookParseResult.of(books, total, start);
return NaverBookParseResult.of(Naverbooks, total, start);
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

@hd0rable hd0rable merged commit f448ee3 into develop Jun 29, 2025
3 checks passed
@hd0rable hd0rable deleted the feat/#32-get-book-search branch June 29, 2025 12:54
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.

[THIP2025-67] [feat] 책 검색 결과 조회 api 개발

3 participants