-
Notifications
You must be signed in to change notification settings - Fork 0
#8 Store CRUD 구현 #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
#8 Store CRUD 구현 #12
Conversation
…ot, JPA, validation, OpenAPI, and testing
- Introduce abstract BaseTimeEntity class with createdAt field - Apply JPA @MappedSuperclass, @EntityListeners for auditing - Support JSON (de)serialization for LocalDateTime (createdAt) - Add Swagger, Lombok annotations for documentation and builder pattern
- Refactor createStore to return response from saved entity, not just from toEntity() - Ensures returned response contains values updated by persistence (e.g. id, timestamps) - Improves consistency with JPA entity lifecycle
|
""" Walkthrough이번 변경 사항은 Store(매장) 관리 기능을 대대적으로 도입하고, 관련된 엔티티, DTO, 서비스, 컨트롤러, 리포지토리 계층을 새로 추가하였습니다. 또한 JPA 감사(Auditing) 기능이 활성화되었고, 기존 TODO 관련 코드는 삭제되었습니다. 테스트 코드와 빌드 설정도 함께 보완되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant StoreController
participant StoreService
participant StoreRepository
participant Store
Client->>StoreController: POST /stores (StoreCreateRequest)
StoreController->>StoreService: createStore(request)
StoreService->>StoreRepository: save(Store)
StoreRepository-->>StoreService: Store
StoreService-->>StoreController: StoreCreateResponse
StoreController-->>Client: 201 Created + ApiUtils.success(response)
sequenceDiagram
participant Client
participant StoreController
participant StoreService
participant StoreRepository
Client->>StoreController: GET /stores/all-stores
StoreController->>StoreService: getAllStores()
StoreService->>StoreRepository: findAllByDeletedFalse()
StoreRepository-->>StoreService: List<Store>
StoreService-->>StoreController: StoreReadResponse
StoreController-->>Client: 200 OK + ApiUtils.success(response)
Poem
""" 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (7)
🚧 Files skipped from review as they are similar to previous changes (7)
✨ Finishing Touches
🪧 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🧹 Nitpick comments (12)
src/main/java/com/example/gtable/global/entity/BaseTimeEntity.java (1)
26-26: @Schema 설명을 더 정확하게 개선해보세요"시간 관련 VO"보다는 "생성 시간 추적을 위한 기본 엔티티"와 같이 더 구체적이고 정확한 설명을 사용하시면 좋겠습니다.
-@Schema(description = "시간 관련 VO") +@Schema(description = "생성 시간 추적을 위한 기본 엔티티")src/test/java/com/example/gtable/store/repository/StoreRepositoryTest.java (1)
35-47: 저장 및 조회 테스트 로직이 훌륭하지만 createdAt 검증이 누락되었습니다모든 필드에 대한 검증이 꼼꼼하게 되어 있어서 좋습니다. 다만 BaseTimeEntity의 핵심 기능인
createdAt필드에 대한 검증도 추가하시면 더 완전한 테스트가 될 것 같습니다.assertThat(found.get().getIsActive()).isTrue(); +assertThat(found.get().getCreatedAt()).isNotNull();src/main/java/com/example/gtable/store/dto/StoreReadResponse.java (1)
12-23: 페이지네이션 응답 DTO 설계가 잘 되어 있습니다!
StoreReadResponse는 리스트와hasNext플래그를 포함한 깔끔한 페이지네이션 응답 구조를 가지고 있습니다.다만
fromEntity메서드명이 약간 혼란스러울 수 있습니다. 실제로는 엔티티에서 변환하는 것이 아니라 DTO 리스트를 래핑하는 역할이므로, 더 명확한 이름을 고려해 보세요:- public static StoreReadResponse fromEntity(List<StoreReadDto> storeReadDtos, boolean hasNext) { + public static StoreReadResponse of(List<StoreReadDto> storeReadDtos, boolean hasNext) {src/main/java/com/example/gtable/store/repository/StoreRepository.java (1)
11-17: 소프트 삭제를 지원하는 깔끔한 레포지토리 설계입니다!
@Repository어노테이션과JpaRepository상속을 통한 표준적인 설계가 잘 되어 있습니다.deletedFalse조건을 포함한 커스텀 쿼리 메서드들이 소프트 삭제 패턴을 적절히 지원하고 있습니다.일관성을 위해 추가로 고려해 볼 메서드가 있습니다:
+ boolean existsByStoreIdAndDeletedFalse(Long storeId);이 메서드는 업데이트나 삭제 전 존재 여부 확인에 유용할 수 있습니다.
src/main/java/com/example/gtable/store/dto/StoreCreateRequest.java (1)
29-39: 엔티티 변환 메서드가 잘 구현되어 있습니다!
toEntity()메서드에서isActive와deleted필드를false로 초기화하는 것이 적절합니다. 새로 생성되는 매장의 기본 상태를 명확히 설정하고 있습니다.더 명확한 의미 전달을 위해 상수 사용을 고려해 보세요:
+ private static final boolean DEFAULT_ACTIVE_STATUS = false; + private static final boolean DEFAULT_DELETED_STATUS = false; + public Store toEntity() { return Store.builder() .departmentId(departmentId) .name(name) .location(location) .description(description) .storeImageUrl(storeImageUrl) - .isActive(false) - .deleted(false) + .isActive(DEFAULT_ACTIVE_STATUS) + .deleted(DEFAULT_DELETED_STATUS) .build(); }build.gradle (1)
32-32: Lombok 의존성 중복이 있는 것 같습니다.Lombok이 이미
compileOnly와annotationProcessor로 선언되어 있는데,implementation으로도 추가되었습니다. 일반적으로 Lombok은 컴파일 타임에만 필요하므로implementation선언이 불필요할 수 있습니다.- implementation 'org.projectlombok:lombok'src/main/java/com/example/gtable/store/dto/StoreUpdateRequest.java (1)
8-18: 유효성 검증 어노테이션 추가를 고려해보세요.DTO 구조는 잘 설계되었습니다. Lombok 어노테이션들이 적절히 사용되었고, Builder 패턴을 통해 가독성 좋은 객체 생성이 가능합니다. 다만 데이터 무결성을 위해 다음과 같은 유효성 검증을 추가하는 것을 권장합니다:
+import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.Pattern; @Getter @AllArgsConstructor @NoArgsConstructor @Builder public class StoreUpdateRequest { + @Size(max = 100, message = "매장명은 100자를 초과할 수 없습니다") private String name; + @Size(max = 200, message = "위치는 200자를 초과할 수 없습니다") private String location; + @Size(max = 500, message = "설명은 500자를 초과할 수 없습니다") private String description; + @Pattern(regexp = "^https?://.*", message = "올바른 URL 형식이어야 합니다") private String storeImageUrl; private Boolean isActive; private Boolean deleted; }src/main/java/com/example/gtable/store/service/StoreServiceImpl.java (1)
44-44: 페이징 처리를 위한 hasNext 로직을 구현해주세요.현재 hasNext가 항상 false로 하드코딩되어 있습니다. 실제 페이징 기능을 구현하거나, 페이징이 필요 없다면 해당 필드를 제거하는 것을 고려해보세요.
-boolean hasNext = false; +// 페이징이 필요한 경우: +boolean hasNext = stores.size() >= pageSize; + +// 또는 페이징이 불필요한 경우 StoreReadResponse에서 hasNext 필드 제거 고려src/main/java/com/example/gtable/store/model/Store.java (2)
47-58: 커스텀 생성자와 SuperBuilder의 충돌 가능성을 검토해주세요.SuperBuilder와 커스텀 생성자가 동시에 존재하면 혼란을 야기할 수 있습니다. SuperBuilder를 사용한다면 커스텀 생성자는 불필요할 수 있습니다.
다음 중 하나의 방식을 선택하는 것을 권장합니다:
방법 1: SuperBuilder만 사용
- public Store(LocalDateTime createdAt, Long storeId, Long departmentId, String name, String location, - String description, String storeImageUrl, Boolean isActive, Boolean deleted) { - super(createdAt); - this.storeId = storeId; - this.departmentId = departmentId; - this.name = name; - this.location = location; - this.description = description; - this.storeImageUrl = storeImageUrl; - this.isActive = isActive; - this.deleted = deleted; - }방법 2: @Builder.Default 사용으로 기본값 처리
+ @Builder.Default @Column(name = "is_active", nullable = false) private Boolean isActive = false; + @Builder.Default @Column private Boolean deleted = false;
31-31: departmentId 필드에 대한 연관관계 매핑을 고려해보세요.현재 departmentId가 단순 Long 타입으로 선언되어 있는데, 실제 Department 엔티티와의 연관관계가 있다면 JPA 연관관계 매핑을 사용하는 것이 더 좋습니다.
- private Long departmentId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "department_id") + private Department department;만약 Department 엔티티가 아직 없다면 현재 구조를 유지하는 것이 적절합니다.
src/main/java/com/example/gtable/store/controller/StoreController.java (2)
31-41: ResponseEntity 타입을 더 구체적으로 지정해보세요.현재
ResponseEntity<?>를 사용하고 있는데, 더 구체적인 타입을 사용하면 API 문서화와 타입 안전성이 향상됩니다.- public ResponseEntity<?> createStore(@Valid @RequestBody StoreCreateRequest request) { + public ResponseEntity<ApiUtils.ApiResult<StoreCreateResponse>> createStore(@Valid @RequestBody StoreCreateRequest request) {이렇게 하면 클라이언트와 개발자 모두에게 더 명확한 API 스펙을 제공할 수 있습니다.
23-26: OpenAPI 문서화 어노테이션 추가를 고려해보세요.API 문서화를 위해 Swagger/OpenAPI 어노테이션을 추가하는 것을 권장합니다.
+import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +@Tag(name = "Store", description = "매장 관리 API") @RestController @RequestMapping("/stores") @RequiredArgsConstructor public class StoreController {그리고 각 메서드에도 @operation 어노테이션을 추가해주세요:
+ @Operation(summary = "매장 생성", description = "새로운 매장을 생성합니다") @PostMapping public ResponseEntity<?> createStore(@Valid @RequestBody StoreCreateRequest request) {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
build.gradle(1 hunks)src/main/java/com/example/gtable/GTableApplication.java(1 hunks)src/main/java/com/example/gtable/TODO/TodoController.java(0 hunks)src/main/java/com/example/gtable/TODO/TodoRepository.java(0 hunks)src/main/java/com/example/gtable/TODO/TodoService.java(0 hunks)src/main/java/com/example/gtable/global/entity/BaseTimeEntity.java(1 hunks)src/main/java/com/example/gtable/store/controller/StoreController.java(1 hunks)src/main/java/com/example/gtable/store/dto/StoreCreateRequest.java(1 hunks)src/main/java/com/example/gtable/store/dto/StoreCreateResponse.java(1 hunks)src/main/java/com/example/gtable/store/dto/StoreReadDto.java(1 hunks)src/main/java/com/example/gtable/store/dto/StoreReadResponse.java(1 hunks)src/main/java/com/example/gtable/store/dto/StoreUpdateRequest.java(1 hunks)src/main/java/com/example/gtable/store/model/Store.java(1 hunks)src/main/java/com/example/gtable/store/repository/StoreRepository.java(1 hunks)src/main/java/com/example/gtable/store/service/StoreService.java(1 hunks)src/main/java/com/example/gtable/store/service/StoreServiceImpl.java(1 hunks)src/test/java/com/example/gtable/store/controller/StoreControllerTest.java(1 hunks)src/test/java/com/example/gtable/store/repository/StoreRepositoryTest.java(1 hunks)src/test/java/com/example/gtable/store/service/StoreServiceTest.java(1 hunks)
💤 Files with no reviewable changes (3)
- src/main/java/com/example/gtable/TODO/TodoRepository.java
- src/main/java/com/example/gtable/TODO/TodoService.java
- src/main/java/com/example/gtable/TODO/TodoController.java
🔇 Additional comments (14)
src/main/java/com/example/gtable/global/entity/BaseTimeEntity.java (3)
21-27: 클래스 정의와 어노테이션이 잘 구성되어 있습니다JPA 감사 기능을 위한 기본 엔티티 클래스가 깔끔하게 구현되었네요.
@MappedSuperclass,@EntityListeners,@SuperBuilder등의 어노테이션이 적절히 사용되어 있습니다.
29-33: createdAt 필드 설정이 훌륭합니다
@CreatedDate어노테이션과updatable = false설정, 그리고 Jackson 직렬화 설정이 모두 적절하게 구성되어 있어서 좋습니다.
35-37:Details
❓ Verification inconclusive
생성자와 JPA 감사 기능 간의 충돌 가능성을 확인해주세요
@CreatedDate어노테이션이 자동으로 생성 시간을 설정하는데, 생성자에서createdAt을 직접 설정하면 예상치 못한 동작이 발생할 수 있습니다. 실제 사용 시 어떤 값이 우선되는지 테스트해보시고, 필요하다면 생성자를 제거하거나 JPA 감사 기능을 우선하도록 조정해주세요.실제 동작을 확인하기 위해 다음 스크립트로 테스트 케이스를 찾아보겠습니다:
🏁 Script executed:
#!/bin/bash # BaseTimeEntity를 사용하는 테스트 케이스 찾기 rg -A 10 -B 5 "BaseTimeEntity|createdAt.*LocalDateTime\.now" --type javaLength of output: 10477
생성자와 JPA 감사 기능 충돌 여부 확인 필요
BaseTimeEntity에
@CreatedDate가 붙은createdAt필드public BaseTimeEntity(LocalDateTime createdAt)생성자가 동시에 존재합니다.
@SuperBuilder로 생성한 엔티티에createdAt을 직접 주입할 때,AuditingEntityListener가 이 값을 덮어쓰는지(또는 null일 때만 설정하는지) 실제 저장 시점을 통해 반드시 검증하세요.점검 및 대응 방안 예시:
- 통합 테스트를 추가해 “builder로 주입한 createdAt 값” vs. “JPA 감사 기능이 설정한 값” 중 어떤 값이 DB에 남는지 확인
- 충돌이 발생한다면
- 생성자를 제거하고 Auditing에만 위임
- 또는
AuditingEntityListener가 null일 때만 동작하도록 설정 조정src/main/java/com/example/gtable/GTableApplication.java (1)
5-7: JPA 감사 기능 활성화가 완벽합니다
@EnableJpaAuditing어노테이션을 추가하여 BaseTimeEntity의 자동 시간 추적 기능을 활성화하셨네요. 이 변경으로 Store 엔티티들의 생성 시간이 자동으로 관리될 것입니다. 매우 깔끔한 구현입니다!src/test/java/com/example/gtable/store/repository/StoreRepositoryTest.java (1)
15-20: 테스트 클래스 구조가 깔끔합니다
@DataJpaTest어노테이션을 사용하여 JPA 계층만 테스트하는 구조가 좋고, 의존성 주입도 적절하게 되어 있습니다.src/main/java/com/example/gtable/store/dto/StoreReadDto.java (2)
11-23: DTO 클래스 구조가 매우 훌륭합니다Lombok 어노테이션을 적절히 사용하여 보일러플레이트 코드를 줄이고, 필요한 모든 필드가 잘 정의되어 있습니다. 특히
Boolean타입 사용으로 null 처리도 고려되어 있어서 좋네요.
25-37: 정적 팩토리 메서드가 완벽하게 구현되었습니다
fromEntity메서드가 Store 엔티티의 모든 필드를 빠짐없이 DTO로 매핑하고 있어서 훌륭합니다. 이런 방식의 변환 메서드는 유지보수성과 가독성을 크게 향상시키는 우수한 패턴입니다. 매우 깔끔한 구현이에요!src/main/java/com/example/gtable/store/dto/StoreCreateResponse.java (1)
27-39: 팩토리 메서드 패턴 구현이 잘 되어 있습니다!
fromEntity정적 팩토리 메서드가 깔끔하게 구현되어 있어 엔티티에서 DTO로의 변환이 명확하고 재사용 가능합니다. 빌더 패턴과의 조합도 좋습니다.src/main/java/com/example/gtable/store/dto/StoreCreateRequest.java (1)
17-21: 검증 어노테이션이 적절히 적용되어 있습니다!
@NotNull과@NotBlank를 통한 필수 필드 검증이 잘 설정되어 있습니다.departmentId와name필드에 대한 검증이 비즈니스 요구사항을 잘 반영하고 있습니다.src/test/java/com/example/gtable/store/controller/StoreControllerTest.java (1)
26-84: 테스트 코드 구조가 매우 잘 작성되었습니다!컨트롤러 테스트의 전반적인 구조가 훌륭합니다. MockMvc를 사용한 단위 테스트 접근법이 적절하고, 성공 케이스(201 Created)와 검증 실패 케이스(400 Bad Request) 모두를 다루고 있어 테스트 커버리지가 좋습니다. 특히 StoreService를 모킹하여 컨트롤러 로직만을 격리해서 테스트하는 방식이 매우 적절합니다.
src/test/java/com/example/gtable/store/service/StoreServiceTest.java (1)
20-71: 서비스 계층 테스트가 매우 꼼꼼하게 작성되었습니다!MockitoExtension을 활용한 테스트 구조가 깔끔하고, StoreRepository를 적절히 모킹하여 서비스 로직만을 테스트하고 있습니다. 특히 응답 객체의 모든 필드에 대한 검증이 빠짐없이 이루어져 있고,
verify()를 통해 리포지토리 호출이 정확히 한 번 발생했는지까지 확인하는 부분이 매우 세심합니다.build.gradle (1)
33-34: 새로운 의존성들이 잘 추가되었습니다!
spring-boot-starter-validation,springdoc-openapi,awaitility,h2등의 의존성들이 Store CRUD 기능과 테스트를 위해 적절히 추가되었습니다. 특히 검증 기능과 API 문서화, 그리고 테스트 환경 구성을 위한 의존성들이 잘 선택되었습니다.Also applies to: 41-42
src/main/java/com/example/gtable/store/service/StoreService.java (1)
9-21: 서비스 인터페이스가 매우 깔끔하게 설계되었습니다!CRUD 기능을 위한 5개 메소드가 일관성 있게 정의되어 있고, 메소드명이 직관적이어서 각 기능을 명확히 알 수 있습니다. 특히 적절한 DTO 타입을 사용하여 요청과 응답을 분리한 점과, 각 메소드의 반환 타입이 해당 기능에 맞게 잘 설계된 점이 인상적입니다. 단일 책임 원칙을 잘 따르고 있는 깔끔한 인터페이스입니다.
src/main/java/com/example/gtable/store/controller/StoreController.java (1)
30-89: 전체적인 API 설계가 잘 되어 있습니다!HTTP 메서드와 상태 코드가 RESTful 원칙에 따라 적절히 사용되었고, 일관된 응답 구조를 유지하고 있습니다. 특히 다음 부분들이 좋습니다:
- POST 요청에 대한 201 Created 상태 코드 사용
- PATCH를 통한 부분 업데이트 구현
- 논리적 삭제 패턴 적용
- @Valid를 통한 요청 데이터 검증
- ApiUtils.success()를 통한 일관된 응답 래핑
이러한 패턴을 다른 컨트롤러에서도 동일하게 적용하면 API 일관성이 더욱 향상될 것입니다.
src/test/java/com/example/gtable/store/repository/StoreRepositoryTest.java
Show resolved
Hide resolved
src/main/java/com/example/gtable/store/dto/StoreCreateResponse.java
Outdated
Show resolved
Hide resolved
src/main/java/com/example/gtable/store/dto/StoreCreateRequest.java
Outdated
Show resolved
Hide resolved
src/main/java/com/example/gtable/store/service/StoreServiceImpl.java
Outdated
Show resolved
Hide resolved
src/main/java/com/example/gtable/store/service/StoreServiceImpl.java
Outdated
Show resolved
Hide resolved
…CreateResponse, StoreCreateRequest
…tion for entity lookup failures
작업 요약
Issue Link
#8
문제점 및 어려움
x
해결 방안
Reference
Summary by CodeRabbit
신규 기능
버그 수정
문서화
테스트
기타