Conversation
Walkthrough책 검색 결과 조회 API가 네이버 검색 API와 연동되어 새롭게 구현되었습니다. 주요 변경사항으로는 검색 요청을 처리하는 컨트롤러, 서비스, 포트, 어댑터, 외부 API 유틸리티 및 XML 파서가 추가되었고, 관련 예외 및 응답 DTO, 테스트 코드가 도입되었습니다. 불필요한 더미 클래스들은 삭제되었습니다. Changes
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>
Assessment against linked issues
Suggested labels
Poem
✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
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
📒 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 -eecho "== BookSearchUseCase Interface =="
iface=$(find . -type f -name BookSearchUseCase.java | head -n1)
echo "$iface"
sed -n '1,200p' "$iface"
echoecho "== BookSearchUseCase Implementation =="
impl=$(grep -R "implements BookSearchUseCase" -n src/main/java | cut -d: -f1 | head -n1)
echo "$impl"
sed -n '1,200p' "$impl"
echoecho "== 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; | |||
There was a problem hiding this comment.
🛠️ Refactor suggestion
아키텍처 의존성 방향 위반을 수정해주세요.
인바운드 포트에서 어댑터 계층의 응답 DTO를 직접 참조하고 있어 헥사고날 아키텍처 원칙에 위배됩니다. 응용 계층은 어댑터 계층에 의존하지 않아야 합니다.
다음 중 하나의 방식으로 개선을 권장합니다:
- 도메인 모델을 직접 반환
- 응용 계층에 별도의 응답 DTO 생성
- 어댑터에서 도메인 모델을 웹 응답 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.
| public NaverBookParseResult findBooksByKeyword(String keyword, int start) { | ||
| String xml = naverApiUtil.searchBook(keyword, start); // 네이버 API 호출 | ||
| return NaverBookXmlParser.parse(xml); // XML 파싱 + 페이징 정보 포함 | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
입력 매개변수 검증과 예외 처리 개선이 필요합니다.
현재 구현에서 다음 사항들을 고려해보세요:
keyword매개변수가 null이거나 빈 문자열일 경우에 대한 검증start매개변수가 음수일 경우에 대한 검증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.
| @GetMapping("/books") | ||
| public BaseResponse<GetBookSearchListResponse> getBookSearchList(@RequestParam final String keyword, | ||
| @RequestParam final int page) { | ||
| return BaseResponse.ok(bookSearchUseCase.searchBooks(keyword, page)); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
API 엔드포인트 설계를 개선해보세요.
다음 사항들을 고려해보시기 바랍니다:
- 매개변수 검증:
@RequestParam에 검증 어노테이션 추가 - 엔드포인트 경로:
/books보다는/books/search같은 더 명확한 경로 고려 - 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.
| @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()); | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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>"); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| } catch (Exception e) { | ||
| throw new BusinessException(ErrorCode.BOOK_NAVER_API_PARSING_ERROR); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| } 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.
src/main/java/konkuk/thip/book/application/service/BookSearchService.java
Outdated
Show resolved
Hide resolved
seongjunnoh
left a comment
There was a problem hiding this comment.
고생하셨습니다! 리뷰단 부분 확인해주세요!
src/main/java/konkuk/thip/book/adapter/in/web/response/BookDto.java
Outdated
Show resolved
Hide resolved
src/main/java/konkuk/thip/book/application/service/BookSearchService.java
Outdated
Show resolved
Hide resolved
| 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); | ||
| } |
There was a problem hiding this comment.
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 내부로 위임
}There was a problem hiding this comment.
@seongjunnoh 성준님 말은 네이버로 파싱 받는 단일 책 정보와 저희 서비스의 책 도메인을 분리하자는 말씀이시죠? 제가 이해한게 맞다면 저는 좋다고 생각합니다!
There was a problem hiding this comment.
@seongjunnoh 성준님 말은 네이버로 파싱 받는 단일 책 정보와 저희 서비스의 책 도메인을 분리하자는 말씀이시죠? 제가 이해한게 맞다면 저는 좋다고 생각합니다!
넵 맞습니다! 네이버 api 로부터 받은 데이터들을 담는 컨테이너의 역할로 저희 Book 도메인 엔티티를 사용하는 것보다는, 추가적으로 dto를 생성하는게 어떤가 라는 말이었습니다!
| @Override | ||
| public GetBookSearchListResponse searchBooks(String keyword, int page) { |
There was a problem hiding this comment.
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 님과 같이 의논해보면 좋을거 같습니다!
There was a problem hiding this comment.
엇 이부분은 위 리뷰에 적어두었습니다! 저도 성준님 의견에 동의합니다~ 👍🏻
| @SpringBootTest | ||
| @AutoConfigureMockMvc | ||
| @ActiveProfiles("test") | ||
| class BookQueryControllerTest { |
| @Test | ||
| @DisplayName("정상 응답을 모킹하여 반환") | ||
| void searchBook_success_mocking() { |
There was a problem hiding this comment.
p3 : displayName 부분에는 개발한 부분이 어떤 기능을 담당하는지, 현재 테스트 메서드에서는 해당 기능의 어떤 부분을 테스트하는 건지에 대한 정보를 담아주시면 좋을거 같습니다!
| // get 메서드를 spy로 모킹 (실제 네트워크 호출 없이 원하는 응답 반환) | ||
| String expectedXml = "<rss><channel><total>1</total><start>1</start></channel></rss>"; | ||
| Mockito.doReturn(expectedXml) | ||
| .when(naverApiUtil) | ||
| .get(Mockito.anyString(), Mockito.anyMap()); |
| public interface BookSearchUseCase { | ||
|
|
||
| GetBookSearchListResponse searchBooks(String keyword, int page); | ||
|
|
||
| } |
There was a problem hiding this comment.
p3: 여기서 코래 말대로 GetBookSearchListResponse는 adapter.in.web에서 사용하는 dto이기 때문에 의존성 방향에 위반되는 것 같아요. 여기선 ListOfBookSearch 처럼 특정 dto 하나를 반환해서 controller에서 매핑하는게 맞을 것 같아요.
성준님 레포지토리 참고하였습니다 ! ㅎ 스페이스 참고 코드
There was a problem hiding this comment.
음그럼 같은 역할을 하는 dto를 하나 더 만들자는 말씀이신가요?
There was a problem hiding this comment.
음 service의 메서드가 희진님이 정의하신 Result dto를 반환하도록 하고, controller 에서는 response dto 가 반환된 result를 response dto 자기 자신으로 변환해 반환하도록 수정하면 추가적인 dto를 만들지는 않아도 될 것 같습니다!
제 리뷰 중 예시코드를 포함한 리뷰에서의 답변과 동일합니다!
| @Override | ||
| public GetBookSearchListResponse searchBooks(String keyword, int page) { |
There was a problem hiding this comment.
엇 이부분은 위 리뷰에 적어두었습니다! 저도 성준님 의견에 동의합니다~ 👍🏻
| public class ErrorResponse { | ||
|
|
||
| @JsonProperty("isSuccess:") | ||
| @JsonProperty("isSuccess") |
| super(errorCode.getMessage()); | ||
| this.errorCode = errorCode; | ||
| } | ||
|
|
||
| public BusinessException(ErrorCode errorCode, Exception e) { | ||
| super(e); | ||
| super(errorCode.getMessage(), e); |
There was a problem hiding this comment.
혹시 이 부분 수정하신 이유가 따로 있나요??
There was a problem hiding this comment.
테스트코드를 작성하는 도중에 원래 기존에 코드에서는 에러메세지가 부모클래스에게 전달되지않아 getMessage()가 null이 되었던 문제를 수정하기위해 코드를 수정했습니닷!
src/main/java/konkuk/thip/book/application/service/BookSearchService.java
Outdated
Show resolved
Hide resolved
| @Value("${naver.clientSecret}") | ||
| private String clientSecret; | ||
|
|
||
| private final String NAVER_BOOK_SEARCH_URL = "https://openapi.naver.com/v1/search/book.xml?query="; //책 검색 결과 조회 |
There was a problem hiding this comment.
이것도 환경변수로 넣는거 어떻게 생각하시나요!
| 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); | ||
| } |
There was a problem hiding this comment.
@seongjunnoh 성준님 말은 네이버로 파싱 받는 단일 책 정보와 저희 서비스의 책 도메인을 분리하자는 말씀이시죠? 제가 이해한게 맞다면 저는 좋다고 생각합니다!
RestTemplate로 바꾸려고했느데 ㅎ.. 통신은되는데 반환값이 안넘어오는 문제가 발생해서.. 일단 급하게 pr올린거였어욥..! 추후에 리펙할때 고려해보도록하겠습니다! |
|
|
리뷰사항 반영해서 코드 수정했습니다!
|
There was a problem hiding this comment.
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
📒 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의 구조가 간결하고 팩토리 메서드도 적절하게 구현되었습니다. 필드명도 명확하고 일관성이 있습니다.
| if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출 | ||
| return readBody(con.getInputStream()); | ||
| } else { // 오류 발생 | ||
| return readBody(con.getErrorStream()); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| 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); |
There was a problem hiding this comment.
🛠️ 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.
| 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.
There was a problem hiding this comment.
검색 keyword에 대한 totalElements 값이 0일 경우, totalPages 또한 0이 되는데, 이 경우 response dto는
searchResult = emptyList, page = 1, size = 10, totalElements = 0, totalPages = 0, last = true, first = true
이므로 문제없을 것 같습니다
There was a problem hiding this comment.
@seongjunnoh 맞는 말씀입니다! totalElements = 0일 때의 응답 구조를 정확히 분석해주셨네요.
totalPages = 0으로 설정하는 것은 수학적으로 올바르고, 응답 DTO의 각 필드들이 일관성 있게 설정됩니다:
last = true(1페이지가 0페이지보다 크거나 같으므로 마지막 페이지)first = true(1페이지이므로 첫 페이지)
이는 유효한 설계 선택입니다. 빈 결과에 대해 totalPages = 0을 사용하는 시스템과 totalPages = 1을 사용하는 시스템이 모두 존재하며, 현재 구현은 수학적으로 정확합니다.
다만 제가 언급한 page 매개변수의 음수/0 값에 대한 유효성 검증 부분은 여전히 고려해볼 만한 것 같습니다. 하지만 이미 BookSearchService에서 검증하고 있다면 중복 검증일 수도 있겠네요.
🐰
╭─────╮
│ 분석 │
│ 완료 │
╰─────╯
|
|
||
| private String buildSearchApiUrl(String query,Integer start) { | ||
| return NAVER_BOOK_SEARCH_URL+query+"&start="+start; | ||
| return bookSearchUrl+query+"&display="+PAGE_SIZE+"&start="+start; |
| public record GetBookSearchListResponse( | ||
| List<BookDto> searchResult, // 책 목록 | ||
| int page, // 현재 페이지 (1부터 시작) | ||
| int size, // 한 페이지에 포함되는 데이터 수 (페이지 크기) |
| 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); |
There was a problem hiding this comment.
검색 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, |
| return NaverBookParseResult.of(books, total, start); | ||
| return NaverBookParseResult.of(Naverbooks, total, start); |



#️⃣ 연관된 이슈
📝 작업 내용
BookQueryControllerTest와NaverApiUtilTest테스트도 진행하였습니다.📸 스크린샷
💬 리뷰 요구사항
헥사고날 아키텍쳐와 외부 api연동을 같이하다보니 오래걸렸던것같습니다.. ㅜㅜ 아직 헥사고날에 익숙하지않아서 어색한 부분이있다면 알려주세용..!
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
신규 기능
버그 수정
테스트
기타