Conversation
- 투표 생성 시 페이지 범위, 총평 여부 유효성 검사 로직 추가
- 상세 error message 또한 다룰 수 있도록 코드 수정
|
""" Walkthrough투표 생성 API를 도입하기 위해 도메인, 어댑터, 서비스, 컨트롤러, DTO, 예외처리 등 전반에 걸쳐 대규모 변경이 이루어졌습니다. 투표 생성 요청 및 검증, 투표/투표항목 저장, 관련 엔터티 조회, 예외 코드 추가, 테스트 코드 작성 등이 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
actor 사용자
participant VoteCommandController
participant VoteService
participant RoomCommandPort
participant BookCommandPort
participant VoteCommandPort
사용자->>VoteCommandController: POST /rooms/{roomId}/vote (VoteCreateRequest)
VoteCommandController->>VoteService: createVote(VoteCreateCommand)
VoteService->>RoomCommandPort: findById(roomId)
VoteService->>BookCommandPort: findById(bookId)
VoteService->>VoteCommandPort: saveVote(Vote)
VoteService->>VoteCommandPort: saveAllVoteItems(List<VoteItem>)
VoteService-->>VoteCommandController: voteId 반환
VoteCommandController-->>사용자: BaseResponse<VoteCreateResponse>
Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
⏰ Context from checks skipped due to timeout of 90000ms (1)
✨ 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: 2
🧹 Nitpick comments (4)
src/main/java/konkuk/thip/vote/adapter/in/web/VoteCommandController.java (1)
21-21: API 문서화 추가 제안Swagger/OpenAPI 어노테이션을 추가하면 API 문서화가 개선될 것입니다.
+@Operation(summary = "투표 생성", description = "특정 방에 새로운 투표를 생성합니다") +@ApiResponses({ + @ApiResponse(responseCode = "200", description = "투표 생성 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청"), + @ApiResponse(responseCode = "404", description = "방을 찾을 수 없음") +}) @PostMapping("/rooms/{roomId}/vote")src/main/java/konkuk/thip/vote/application/service/VoteService.java (2)
46-53: 빈 투표 항목 리스트 처리 고려투표 항목이 없는 경우에 대한 처리가 필요할 수 있습니다.
// 3. vote item 저장 +if (command.voteItemCreateCommands() != null && !command.voteItemCreateCommands().isEmpty()) { List<VoteItem> voteItems = command.voteItemCreateCommands().stream() .map(itemCmd -> VoteItem.withoutId( itemCmd.itemName(), 0, savedVoteId )) .toList(); voteCommandPort.saveAllVoteItems(voteItems); +}
30-56: 로깅 추가로 디버깅 및 모니터링 개선중요한 비즈니스 로직이므로 로깅을 추가하면 운영 시 도움이 될 것입니다.
public Long createVote(VoteCreateCommand command) { + log.info("투표 생성 시작 - userId: {}, roomId: {}", command.userId(), command.roomId()); // 1. validate Vote vote = Vote.withoutId( command.content(), command.userId(), command.page(), command.isOverview(), command.roomId() ); validateVote(vote); // 2. vote 저장 Long savedVoteId = voteCommandPort.saveVote(vote); + log.info("투표 저장 완료 - voteId: {}", savedVoteId); // 3. vote item 저장 List<VoteItem> voteItems = command.voteItemCreateCommands().stream() .map(itemCmd -> VoteItem.withoutId( itemCmd.itemName(), 0, savedVoteId )) .toList(); voteCommandPort.saveAllVoteItems(voteItems); + log.info("투표 항목 저장 완료 - voteId: {}, itemCount: {}", savedVoteId, voteItems.size()); return savedVoteId; }src/main/java/konkuk/thip/vote/domain/Vote.java (1)
37-46: 총평 검증 로직이 명확합니다80% 진행률 체크와 구체적인 에러 메시지 제공이 좋습니다. 매직 넘버를 상수로 추출하면 더 좋을 것 같습니다.
+private static final double OVERVIEW_MINIMUM_PROGRESS_RATIO = 0.8; + public void validateOverview(int totalPageCount) { double ratio = (double) page / totalPageCount; - if (isOverview && ratio < 0.8) { + if (isOverview && ratio < OVERVIEW_MINIMUM_PROGRESS_RATIO) { String message = String.format( "총평(isOverview)은 진행률이 80%% 이상일 때만 가능합니다. 현재 진행률 = %.2f%% (%d/%d)", ratio * 100, page, totalPageCount ); throw new InvalidStateException(VOTE_CANNOT_BE_OVERVIEW, new IllegalStateException(message)); } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (23)
src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java(2 hunks)src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java(1 hunks)src/main/java/konkuk/thip/common/exception/code/ErrorCode.java(1 hunks)src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java(3 hunks)src/main/java/konkuk/thip/vote/adapter/in/web/VoteCommandController.java(1 hunks)src/main/java/konkuk/thip/vote/adapter/in/web/request/DummyRequest.java(0 hunks)src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java(1 hunks)src/main/java/konkuk/thip/vote/adapter/in/web/response/VoteCreateResponse.java(1 hunks)src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteItemJpaRepository.java(1 hunks)src/main/java/konkuk/thip/vote/application/port/in/DummyUseCase.java(0 hunks)src/main/java/konkuk/thip/vote/application/port/in/VoteCreateUseCase.java(1 hunks)src/main/java/konkuk/thip/vote/application/port/in/dto/DummyCommand.java(0 hunks)src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java(1 hunks)src/main/java/konkuk/thip/vote/application/port/out/VoteCommandPort.java(1 hunks)src/main/java/konkuk/thip/vote/application/service/VoteService.java(1 hunks)src/main/java/konkuk/thip/vote/domain/Vote.java(2 hunks)src/main/java/konkuk/thip/vote/domain/VoteItem.java(1 hunks)src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java(1 hunks)src/test/java/konkuk/thip/vote/domain/VoteTest.java(1 hunks)
💤 Files with no reviewable changes (3)
- src/main/java/konkuk/thip/vote/application/port/in/DummyUseCase.java
- src/main/java/konkuk/thip/vote/adapter/in/web/request/DummyRequest.java
- src/main/java/konkuk/thip/vote/application/port/in/dto/DummyCommand.java
🧰 Additional context used
🧠 Learnings (5)
src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/vote/application/port/out/VoteCommandPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java (2)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional<Entity> 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
🧬 Code Graph Analysis (1)
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java (3)
src/main/java/konkuk/thip/common/exception/EntityNotFoundException.java (1)
EntityNotFoundException(5-10)src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java (1)
Repository(16-37)src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java (1)
Repository(10-21)
🔇 Additional comments (32)
src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java (1)
3-7: CQRS 컨벤션을 잘 준수한 설계입니다.학습된 컨벤션에 따라 CommandPort에
findById메서드를 적절히 추가하였습니다. 도메인 엔티티를 반환하는 메서드 시그니처도 명확합니다.src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java (1)
12-12: 일관성 있는 인터페이스 설계입니다.기존
findByIsbn메서드와 동일한 패턴으로findById메서드를 추가하여 인터페이스의 일관성을 유지했습니다.src/main/java/konkuk/thip/vote/domain/VoteItem.java (1)
19-26: 명확한 의도를 가진 정적 팩토리 메서드입니다.
withoutId메서드명으로 새로운 엔티티 생성 시 ID가 없다는 의도를 명확히 표현했습니다. 빌더 패턴을 활용한 구현도 깔끔합니다.src/main/java/konkuk/thip/vote/application/port/in/VoteCreateUseCase.java (1)
1-8: 단일 책임 원칙을 잘 따른 유스케이스 인터페이스입니다.투표 생성이라는 하나의 책임만을 가지는 깔끔한 인터페이스 설계입니다. 메서드 시그니처도 명확하고 헥사고날 아키텍처의 포트 역할을 잘 수행합니다.
src/main/java/konkuk/thip/vote/adapter/in/web/response/VoteCreateResponse.java (1)
1-9: 간결하고 적절한 응답 DTO 설계입니다.레코드 클래스를 활용하여 불변성을 보장하면서도 간결한 응답 DTO를 구현했습니다. 정적 팩토리 메서드
of()로 편의성도 제공합니다.src/main/java/konkuk/thip/vote/application/port/out/VoteCommandPort.java (1)
10-12: 인터페이스 설계가 잘 되어있습니다.CQRS 패턴을 따르고 있으며, 투표 저장과 투표 항목 일괄 저장 기능을 명확하게 분리했습니다. 메서드명도 직관적이고 반환 타입도 적절합니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java (2)
17-18: 필드명 변경이 적절합니다.
jpaRepository→roomJpaRepository,userMapper→roomMapper로 변경하여 명확성을 높였습니다.
20-27: findById 메서드 구현이 우수합니다.EntityNotFoundException을 사용한 예외 처리가 적절하고, BookCommandPersistenceAdapter와 일관된 패턴을 따르고 있습니다. 도메인 특화 에러 코드 사용도 좋습니다.
src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java (1)
29-36: findById 메서드 구현이 일관성 있고 우수합니다.RoomCommandPersistenceAdapter와 동일한 패턴을 따르고 있으며, 도메인 특화 예외 처리가 적절합니다. 매퍼를 통한 도메인 엔티티 변환도 깔끔하게 구현되었습니다.
src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteItemJpaRepository.java (1)
8-11: JPA Repository 인터페이스가 잘 설계되었습니다.Spring Data JPA의 property expression을 사용한 커스텀 쿼리 메서드가 적절하고, 메서드명도 직관적입니다. 투표 ID로 투표 항목들을 조회하는 기능이 명확하게 구현되었습니다.
src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java (1)
95-102: 예외 처리 향상이 우수합니다.cause에서 상세 메시지를 추출하여 클라이언트에게 더 구체적인 오류 정보를 제공하는 개선사항이 좋습니다. Optional.ofNullable을 사용한 안전한 null 처리도 적절합니다. PR 목표에 부합하는 개선사항입니다.
src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java (1)
5-19: Record 사용이 적절합니다DTO로 record를 사용하여 불변성을 보장하는 것은 좋은 선택입니다. 명확한 필드명과 구조로 가독성도 우수합니다.
src/main/java/konkuk/thip/vote/adapter/in/web/VoteCommandController.java (1)
21-30: 투표 생성 API 구현이 깔끔합니다컨트롤러가 웹 요청 처리에만 집중하고, 비즈니스 로직은 UseCase에 위임하는 구조가 좋습니다. @Valid를 통한 요청 검증도 적절합니다.
src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java (1)
4-4: 검증 어노테이션 일관성 개선@validated에서 @Valid로의 변경은 프로젝트 전체의 일관성을 높이는 좋은 결정입니다. 그룹 검증이 필요하지 않은 경우 표준 @Valid 사용이 더 적절합니다.
Also applies to: 31-31, 41-41
src/main/java/konkuk/thip/vote/application/service/VoteService.java (1)
28-56: 투표 생성 로직이 잘 구현되었습니다트랜잭션 처리, 도메인 검증, 저장 로직의 흐름이 명확하고 적절합니다. 도메인 레벨에서의 검증을 통해 비즈니스 규칙을 잘 캡슐화했습니다.
src/main/java/konkuk/thip/vote/domain/Vote.java (2)
26-35: 팩토리 메서드 구현이 깔끔합니다ID 없이 Vote 엔티티를 생성하는 팩토리 메서드가 명확하고 사용하기 편리합니다.
48-58: 페이지 범위 검증이 적절합니다페이지 범위 검증 로직과 에러 메시지가 명확하고 사용자에게 도움이 되는 정보를 제공합니다.
src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java (2)
3-10: 의존성 주입이 적절히 구성되어 있습니다.필요한 repository들과 mapper들이 모두 주입되어 있어 투표 생성 기능을 위한 데이터 접근이 가능합니다.
34-45: 투표 저장 로직이 올바르게 구현되어 있습니다.사용자와 룸 엔터티를 조회한 후 해당 엔터티들과 함께 투표를 저장하는 로직이 적절합니다. 또한 엔터티가 존재하지 않을 경우 적절한 예외를 던지고 있습니다.
src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java (4)
76-85: 데이터베이스 정리가 적절하게 구현되어 있습니다.각 테스트 후 관련 엔터티들을 올바른 순서로 정리하여 테스트 간 격리를 보장합니다.
87-130: 테스트 데이터 설정이 체계적으로 구성되어 있습니다.테스트에 필요한 모든 엔터티(사용자, 책, 카테고리, 룸)를 생성하는 헬퍼 메소드가 잘 구성되어 있습니다.
132-179: 투표 생성 성공 테스트가 포괄적으로 구현되어 있습니다.API 호출부터 데이터베이스 검증까지 전체 플로우를 테스트하고 있으며, 투표와 투표 항목 모두 올바르게 저장되는지 확인합니다.
181-352: 다양한 검증 실패 시나리오를 철저히 테스트하고 있습니다.필수 필드 누락, 길이 제한 초과, 투표 항목 개수 제한 등 모든 검증 규칙을 테스트하여 API의 견고성을 확보합니다.
src/test/java/konkuk/thip/vote/domain/VoteTest.java (3)
11-17: 페이지 범위 검증 성공 케이스가 적절히 테스트되어 있습니다.유효한 페이지 범위에 대해 예외가 발생하지 않는지 확인하고, 경계값 테스트도 포함되어 있습니다.
19-37: 페이지 범위 검증 실패 케이스가 철저히 테스트되어 있습니다.페이지가 범위를 벗어날 때 적절한 예외가 발생하는지 확인하고, 예외 메시지도 검증합니다.
39-61: 총평 검증 로직이 올바르게 테스트되어 있습니다.진행률 80% 기준으로 총평 가능 여부를 판단하는 로직과 예외 상황을 모두 테스트합니다.
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (3)
53-57: 최근 검색 관련 에러 코드가 적절히 추가되었습니다.검색 타입 검증을 위한 에러 코드가 명확한 메시지와 함께 추가되었습니다.
59-63: 룸 관련 에러 코드가 적절히 추가되었습니다.룸 존재 여부 확인을 위한 에러 코드가 404 상태와 함께 적절히 정의되었습니다.
64-70: 투표 관련 에러 코드가 체계적으로 추가되었습니다.투표 존재 여부, 총평 가능 여부, 페이지 범위 검증에 대한 에러 코드가 적절한 HTTP 상태 코드와 함께 정의되었습니다.
src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java (3)
11-26: 투표 생성 요청 DTO의 검증 규칙이 적절히 정의되어 있습니다.모든 필수 필드에 대한 검증 어노테이션과 길이 제한이 명확하게 설정되어 있어 API의 견고성을 보장합니다.
27-32: 투표 항목 DTO가 깔끔하게 구성되어 있습니다.중첩된 record 구조로 투표 항목을 표현하고 적절한 검증 규칙을 적용했습니다.
33-47: 요청 DTO를 커맨드 객체로 변환하는 로직이 올바르게 구현되어 있습니다.웹 계층의 DTO를 애플리케이션 계층의 커맨드 객체로 적절히 변환하여 계층 간 분리를 유지합니다.
|
|
||
| String content, | ||
|
|
||
| List<VoteItemCreateCommand> voteItemCreateCommands |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
null safety 고려 필요
voteItemCreateCommands 리스트가 null일 경우를 대비한 방어 로직이 필요할 수 있습니다. 서비스 레이어에서 이를 처리하고 있는지 확인이 필요합니다.
🏁 Script executed:
#!/bin/bash
# voteItemCreateCommands null 체크가 서비스 레이어에서 이루어지는지 확인
ast-grep --pattern 'command.voteItemCreateCommands()' -A 5Length of output: 741
#!/bin/bash
# VoteCreateCommand 클래스 내용 확인
cat src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java다음으로 VoteCreateCommand DTO에서 voteItemCreateCommands 필드 정의와 null 처리 여부를 확인합니다:
🏁 Script executed:
#!/bin/bash
# VoteCreateCommand 클래스 내용 확인
cat src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.javaLength of output: 433
null 안전성 강화 필요
VoteCreateCommand의 voteItemCreateCommands가 null일 경우 서비스 레이어에서 바로 .stream()을 호출해 NPE가 발생할 수 있으므로, 다음 파일에 방어 로직을 추가해야 합니다.
-
src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java
레코드 정의에Objects.requireNonNull검증 추가public record VoteCreateCommand( Long userId, Long roomId, int page, boolean isOverview, String content, List<VoteItemCreateCommand> voteItemCreateCommands ) { + public VoteCreateCommand { + Objects.requireNonNull(voteItemCreateCommands, "voteItemCreateCommands must not be null"); + } public record VoteItemCreateCommand(String itemName) {} } -
src/main/java/konkuk/thip/vote/application/service/VoteService.java
서비스 호출부에서 null을 빈 리스트로 대체List<VoteItem> voteItems = - command.voteItemCreateCommands().stream() + Optional.ofNullable(command.voteItemCreateCommands()) + .orElseGet(List::of) + .stream() .map(itemCmd -> VoteItem.withoutId( itemCmd.itemName(), 0, savedVoteId ))
위 두 군데에 null 체크를 추가해 NPE를 방지하세요.
📝 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.
| List<VoteItemCreateCommand> voteItemCreateCommands | |
| // File: src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java | |
| public record VoteCreateCommand( | |
| Long userId, | |
| Long roomId, | |
| int page, | |
| boolean isOverview, | |
| String content, | |
| List<VoteItemCreateCommand> voteItemCreateCommands | |
| ) { | |
| public VoteCreateCommand { | |
| Objects.requireNonNull(voteItemCreateCommands, "voteItemCreateCommands must not be null"); | |
| } | |
| public record VoteItemCreateCommand(String itemName) {} | |
| } |
| List<VoteItemCreateCommand> voteItemCreateCommands | |
| // File: src/main/java/konkuk/thip/vote/application/service/VoteService.java | |
| List<VoteItem> voteItems = | |
| Optional.ofNullable(command.voteItemCreateCommands()) | |
| .orElseGet(List::of) | |
| .stream() | |
| .map(itemCmd -> VoteItem.withoutId( | |
| itemCmd.itemName(), | |
| 0, | |
| savedVoteId | |
| )) |
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java
at line 16, add a null check using Objects.requireNonNull for the
voteItemCreateCommands field in the record constructor to prevent null values.
Additionally, in
src/main/java/konkuk/thip/vote/application/service/VoteService.java, update the
service method that calls VoteCreateCommand to replace any null
voteItemCreateCommands with an empty list before processing, ensuring no
NullPointerException occurs when calling stream().
src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java
Show resolved
Hide resolved
buzz0331
left a comment
There was a problem hiding this comment.
수고많으셨습니다~!!
리뷰 간단하게 남겼으니 확인 부탁드릴게요~
기록 생성 api 개발 중인데 많이 참고하겠습니다 🤩
|
|
||
| @PostMapping("/users/signup") | ||
| public BaseResponse<UserSignupResponse> signup(@Validated @RequestBody UserSignupRequest request, | ||
| public BaseResponse<UserSignupResponse> signup(@Valid @RequestBody UserSignupRequest request, |
| @RequiredArgsConstructor | ||
| public class VoteService implements DummyUseCase { | ||
| @Slf4j | ||
| public class VoteService implements VoteCreateUseCase { |
There was a problem hiding this comment.
p2: VoteCreateService로 수정해야할 것 같습니다!
| // 페이지 유효성 검증 | ||
| vote.validatePage(book.getPageCount()); | ||
|
|
||
| // 총평 유효성 검증 | ||
| vote.validateOverview(book.getPageCount()); |
| "총평(isOverview)은 진행률이 80%% 이상일 때만 가능합니다. 현재 진행률 = %.2f%% (%d/%d)", | ||
| ratio * 100, page, totalPageCount | ||
| ); | ||
| throw new InvalidStateException(VOTE_CANNOT_BE_OVERVIEW, new IllegalStateException(message)); |
There was a problem hiding this comment.
오호 동적으로 예외 메시지를 발생시키려고 2번째 파라미터로 예외를 던지는거 맞나요?
There was a problem hiding this comment.
넵 맞습니다!
ErrorCode 로 정의한 정적인 에러 메시지에 덧붙여 현재 상황에 대한 동적인 예외 메시지 또한 클라이언트에게 응답하는게 더 좋지 않을까 싶어서, InvalidStateExcetption에 대한 ExceptionHandler 의 코드를 수정하여 2번째 파라미터로 전달받은 exception 객체의 message 도 클라이언트가 응답받을 수 있도록 하였습니다!
| Long userId = userJpaRepository.findAll().get(0).getUserId(); | ||
| Long roomId = roomJpaRepository.findAll().get(0).getRoomId(); | ||
|
|
||
| //when : 투표 생성 api 호출 (filter 통과 없이) | ||
| ResultActions result = mockMvc.perform(post("/rooms/{roomId}/vote", roomId) | ||
| .requestAttr("userId", userId) |
There was a problem hiding this comment.
필터 안쓰고 이런 방법이 있군요! ㅋㅋ하나 배워갑니다
| @NotNull(message = "투표 항목은 필수입니다.") | ||
| @Size(min = 1, max = 5, message = "투표 항목은 1개 이상, 최대 5개까지입니다.") | ||
| @Valid | ||
| List<VoteItemCreateRequest> voteItemCreateRequests |
There was a problem hiding this comment.
p3: api에 명세되는 이름이라 인스턴스 이름만 voteItemList로 바꾸는거 어떨까욥
| String content, | ||
|
|
||
| @NotNull(message = "투표 항목은 필수입니다.") | ||
| @Size(min = 1, max = 5, message = "투표 항목은 1개 이상, 최대 5개까지입니다.") |
There was a problem hiding this comment.
현재 "투표 항목은 1개 이상, 최대 5개까지이다"라는 요구사항은 도메인 규칙에 해당하는 것 같긴한데, 어떻게 생각하시나요..? 변동될 수 있는 규칙이라 도메인에서 검증하는 것이 더 유지보수하는게 좋을 것 같긴한데 따지고보면 다른 파라미터들도 다 비슷한 거 같아서.. 애매하네요 😅
There was a problem hiding this comment.
저도 현준님이 리뷰 남겨주신 것처럼 "투표 항목은 1개 이상, 5개 이하만 가능하다" 라는 요구사항을 투표 생성 api 구현시에 지켜야 하는 도메인 규칙이라고 생각합니다!
하지만 투표 생성 api 의 도메인 계층까지 가지않고, request body 에서도 충분히 검증할 수 있는 validation 이라 생각해서 request body 내부에서 validation 을 수행하도록 구현하였습니다!
There was a problem hiding this comment.
넵 확인했습니다! 뭐 투표를 생성하는 로직이 다른 api에 사용하는 것은 아니니 상관없을 것 같네요! 만약 추후에 투표를 생성하는 로직이 이 api가 아닌 다른 api에서도 필요해진다면 중복 코드를 방지하기 위해 도메인 계층으로 옮기는 것도 좋을 것 같아요~
| public ResponseEntity<ErrorResponse> businessExceptionHandler(BusinessException e) { | ||
| log.error("[BusinessExceptionHandler] {}", e.getMessage()); | ||
|
|
||
| // 1) cause 에 포함된 상세 메시지를 파싱, 없다면 빈 문자열로 설정 |
|
|
||
| @PostMapping("/users/signup") | ||
| public BaseResponse<UserSignupResponse> signup(@Validated @RequestBody UserSignupRequest request, | ||
| public BaseResponse<UserSignupResponse> signup(@Valid @RequestBody UserSignupRequest request, |
| List<VoteItemJpaEntity> voteItemJpaEntities = voteItems.stream() | ||
| .map(voteItem -> { | ||
| VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(voteItem.getVoteId()).orElseThrow( | ||
| () -> new EntityNotFoundException(VOTE_NOT_FOUND) | ||
| ); | ||
|
|
||
| return voteItemMapper.toJpaEntity(voteItem, voteJpaEntity); | ||
| }) | ||
| .toList(); |
There was a problem hiding this comment.
p2: 토래말처럼 n+1 문제가 발생할것같은데
서비스로직에서 해당 vote를 생성할때 이미 검증된 동일한 voteID로 voteItem을 생성하기 때문에 한 번만 findById로 검증하면 충분할 것 같습니다!
| List<VoteItemJpaEntity> voteItemJpaEntities = voteItems.stream() | |
| .map(voteItem -> { | |
| VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(voteItem.getVoteId()).orElseThrow( | |
| () -> new EntityNotFoundException(VOTE_NOT_FOUND) | |
| ); | |
| return voteItemMapper.toJpaEntity(voteItem, voteJpaEntity); | |
| }) | |
| .toList(); | |
| Long voteId = voteItems.get(0).getVoteId(); | |
| VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(voteId) | |
| .orElseThrow(() -> new EntityNotFoundException(VOTE_NOT_FOUND)); | |
| List<VoteItemJpaEntity> voteItemJpaEntities = voteItems.stream() | |
| .map(voteItem -> voteItemMapper.toJpaEntity(voteItem, voteJpaEntity)) | |
| .toList(); |
|
|
||
| private void saveUserAndRoom() { | ||
| AliasJpaEntity alias = aliasJpaRepository.save(AliasJpaEntity.builder() | ||
| .value("책벌레") |
| Long userId = userJpaRepository.findAll().get(0).getUserId(); | ||
| Long roomId = roomJpaRepository.findAll().get(0).getRoomId(); | ||
|
|
||
| //when : 투표 생성 api 호출 (filter 통과 없이) | ||
| ResultActions result = mockMvc.perform(post("/rooms/{roomId}/vote", roomId) | ||
| .requestAttr("userId", userId) |
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java (1)
47-61: N+1 문제가 효과적으로 해결되었습니다.이전 리뷰 코멘트에서 지적된 N+1 문제가 잘 해결되었습니다. 투표 엔터티를 한 번만 조회하여 재사용하는 방식으로 성능이 개선되었습니다.
빈 리스트 처리와 벌크 저장 로직도 적절하게 구현되어 있습니다.
🧹 Nitpick comments (1)
src/main/java/konkuk/thip/vote/application/service/VoteCreateService.java (1)
46-52: VoteItem 생성 로직에서 개선을 고려해보세요.현재 모든 투표 항목의 초기 투표 수가 0으로 하드코딩되어 있습니다. 도메인 객체에서 기본값을 관리하거나 상수로 정의하는 것이 좋겠습니다.
다음과 같이 개선할 수 있습니다:
.map(itemCmd -> VoteItem.withoutId( itemCmd.itemName(), - 0, + VoteItem.DEFAULT_VOTE_COUNT, savedVoteId ))
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java(1 hunks)src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/vote/application/service/VoteCreateService.java(1 hunks)src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java
- src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java (1)
src/main/java/konkuk/thip/common/exception/EntityNotFoundException.java (1)
EntityNotFoundException(5-10)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
🔇 Additional comments (7)
src/main/java/konkuk/thip/vote/application/service/VoteCreateService.java (4)
19-22: 클래스 구조가 깔끔하고 적절합니다.필요한 어노테이션들이 적절히 적용되어 있고, 의존성 주입도 올바르게 구성되어 있습니다.
28-30: 트랜잭션 경계가 적절하게 설정되었습니다.투표 생성과 투표 항목 저장이 하나의 원자적 작업으로 처리되어 데이터 일관성을 보장합니다.
32-38: 도메인 객체 생성 로직이 명확합니다.커맨드 객체의 데이터를 도메인 객체로 변환하는 과정이 명확하게 구현되어 있습니다.
58-67: 도메인 검증 로직이 적절하게 분리되어 있습니다.검증 로직이 도메인 레이어에 위임되어 있고, 필요한 데이터를 조회하여 검증하는 구조가 올바릅니다. 비즈니스 규칙이 도메인 객체에 캡슐화되어 있어 좋은 설계입니다.
src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java (3)
3-10: 필요한 의존성들이 적절히 추가되었습니다.투표 생성 기능을 위한 필수 의존성들이 명확하게 추가되어 있습니다.
27-32: 의존성 주입이 올바르게 구성되었습니다.새로 추가된 리포지토리와 매퍼들이 적절히 주입되어 있습니다.
34-45: 엔터티 존재 검증과 예외 처리가 적절합니다.사용자와 룸 엔터티의 존재를 검증하고, 적절한 에러 코드로 예외를 발생시키는 로직이 잘 구현되어 있습니다.
| String content, | ||
|
|
||
| @NotNull(message = "투표 항목은 필수입니다.") | ||
| @Size(min = 1, max = 5, message = "투표 항목은 1개 이상, 최대 5개까지입니다.") |
There was a problem hiding this comment.
넵 확인했습니다! 뭐 투표를 생성하는 로직이 다른 api에 사용하는 것은 아니니 상관없을 것 같네요! 만약 추후에 투표를 생성하는 로직이 이 api가 아닌 다른 api에서도 필요해진다면 중복 코드를 방지하기 위해 도메인 계층으로 옮기는 것도 좋을 것 같아요~
|



#️⃣ 연관된 이슈
📝 작업 내용
투표 생성 api 를 개발하였습니다.
controller
application
영속성 adapter
📸 스크린샷
💬 리뷰 요구사항
투표 생성 시, controller단에서의 bean validation을 통해서 검증하기 어려운
에 대한 유효성 검사를 Vote 도메인의 책임으로 위임하여 코드를 구현하였습니다.
이때 유효성 검사 실패의 정확한 원인을 클라이언트가 알면 좋지 않나 싶어서, BusinessException 발생 시 단순히 ErrorCode 의 메시지를 응답하는 기존의 코드를 상세 error message 까지 덧붙여서 응답할 수 있도록 exception handler 의 코드 또한 수정하였습니다.
이 부분 참고해서 리뷰해주시면 감사하겠습니다!
-> 현재 error message 는 위 캡쳐 화면처럼 나옵니다!
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
신규 기능
버그 수정
에러 코드 및 예외 처리
테스트
기타