[feat] 닉네임 중복 검사, 칭호 선택 view api 개발#35
Conversation
- nickname 필드값 bean validation 수정 - 테스트 코드의 메서드 네이밍 간결하게 수정
- 각각 테스트 메서드 종료 후 DB를 clear 해주는 @beforeeach 메서드 추가 - 테스트 클래스에 @transactional 을 명시하는 방법도 있지만, 명시적으로 DB 클리어를 보여주기 위해 @beforeeach 메서드 사용을 채택하였음
DataJpaTest 는 Jpa 관련 빈들만 로드하므로, 컴포넌트 스캔 대상이 아닌 JPAQueryFactory를 수동으로 주입하도록 코드 수정
querydsl 활용하여 조회로직 구현
Walkthrough닉네임 검증 및 칭호 선택 화면 API 개발을 위해 Querydsl 설정이 추가되고, 닉네임 중복 검증 및 칭호 선택 화면 조회를 위한 API 엔드포인트, 서비스, 포트, 어댑터, DTO, 테스트 코드가 새로 도입되었습니다. 기존 DTO 및 테스트 네이밍이 일관성 있게 리팩토링되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant UserCommandController
participant VerifyNicknameUseCase
participant UserQueryPort
Client->>UserCommandController: POST /users/nickname (nickname)
UserCommandController->>VerifyNicknameUseCase: isNicknameUnique(nickname)
VerifyNicknameUseCase->>UserQueryPort: existsByNickname(nickname)
UserQueryPort-->>VerifyNicknameUseCase: boolean
VerifyNicknameUseCase-->>UserCommandController: boolean
UserCommandController-->>Client: PostUserVerifyNicknameResponse
sequenceDiagram
participant Client
participant UserQueryController
participant ShowAliasChoiceViewUseCase
participant AliasQueryPort
Client->>UserQueryController: GET /users/alias
UserQueryController->>ShowAliasChoiceViewUseCase: getAllAliasesAndCategories()
ShowAliasChoiceViewUseCase->>AliasQueryPort: getAllAliasesAndCategories()
AliasQueryPort-->>ShowAliasChoiceViewUseCase: AliasChoiceViewResult
ShowAliasChoiceViewUseCase-->>UserQueryController: AliasChoiceViewResult
UserQueryController-->>Client: GetUserShowAliasChoiceResponse
Assessment against linked issues
Assessment against linked issues: Out-of-scope changes해당 이슈의 목적과 직접적으로 관련 없는 기능적 코드 변경은 발견되지 않았습니다. Suggested reviewers
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: 4
♻️ Duplicate comments (1)
src/main/java/konkuk/thip/user/application/port/out/AliasQueryPort.java (1)
3-3: 동일한 DTO import 경로 이슈가 있습니다.AliasRepositoryCustom과 동일한 import 경로 이슈입니다.
application.port.in.dto보다는application.port.out.dto가 더 적절할 것 같습니다.
🧹 Nitpick comments (10)
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasRepositoryCustom.java (1)
3-3: DTO import 경로 검토가 필요합니다.
AliasChoiceViewResult를konkuk.thip.user.application.port.in.dto패키지에서 import하고 있는데, 이는 persistence 레이어에서 application의 in port DTO를 참조하는 구조입니다. 헥사고날 아키텍처 관점에서 보면 persistence 어댑터가 application의 입력 포트 DTO를 직접 참조하는 것은 의존성 방향이 다소 부자연스럽습니다.작성자가 언급한 "persistence layer에서 result DTO 재사용" 설계 결정을 고려하더라도, DTO가
application.port.out.dto패키지에 위치하는 것이 더 적절할 것 같습니다.src/test/java/konkuk/thip/config/TestQuerydslConfig.java (1)
13-17: 코드 일관성을 개선해주세요.테스트 설정 클래스의 구현은 적절하지만, 메인 설정 클래스와 일관성을 위해 개선이 필요합니다.
다음과 같이 수정을 제안합니다:
@PersistenceContext - EntityManager em; + private EntityManager entityManager; @Bean - JPAQueryFactory jpaQueryFactory() { - return new JPAQueryFactory(em); + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); }src/main/java/konkuk/thip/user/application/port/in/dto/AliasChoiceViewResult.java (1)
5-14: DTO 구조가 잘 설계되었습니다.Record 패턴을 활용한 깔끔한 DTO 구조입니다. 중첩된
AliasChoicerecord로 데이터를 잘 캡슐화했습니다.다만 아키텍처 관점에서 한 가지 고려사항이 있습니다: 현재 이 DTO가
port.in.dto패키지에 위치하면서도 persistence 계층에서 재사용되고 있습니다. 일반적으로 Clean Architecture에서는 계층 간 의존성 방향을 고려하지만, 작성자가 언급한 바와 같이 DTO 매핑 오버헤드를 줄이기 위한 의도적인 설계 결정으로 이해됩니다.src/test/java/konkuk/thip/user/adapter/in/web/VerifyNicknameControllerTest.java (3)
78-94: 테스트 데이터 생성 로직을 개선할 수 있습니다.테스트용 엔티티 생성 로직이 반복되고 있습니다. 테스트 헬퍼 메서드나 @beforeeach 설정을 고려해보세요.
다음과 같이 헬퍼 메서드를 추가하는 것을 고려해보세요:
private void createTestUserWithNickname(String nickname) { AliasJpaEntity aliasJpaEntity = AliasJpaEntity.builder() .value("칭호") .color("blue") .imageUrl("http://image.url") .build(); aliasJpaRepository.save(aliasJpaEntity); UserJpaEntity userJpaEntity = UserJpaEntity.builder() .email("test@test.com") .nickname(nickname) .imageUrl("http://image.url") .role(USER) .aliasForUserJpaEntity(aliasJpaEntity) .build(); userJpaRepository.save(userJpaEntity); }
68-72: 응답 파싱 로직을 단순화할 수 있습니다.현재 JsonNode를 직접 파싱하는 방식보다는 jsonPath 매처를 활용하면 더 간단하고 읽기 쉬운 테스트가 됩니다.
다음과 같이 개선을 고려해보세요:
//then result.andExpect(status().isOk()) - .andExpect(jsonPath("$.data.isVerified").exists()); - -String json = result.andReturn().getResponse().getContentAsString(); -JsonNode jsonNode = objectMapper.readTree(json); -boolean isVerified = jsonNode.path("data").path("isVerified").asBoolean(); - -assertThat(isVerified).isTrue(); + .andExpect(jsonPath("$.data.isVerified").value(true));
79-93: 테스트 데이터 생성 로직을 헬퍼 메서드로 추출하는 것을 고려해보세요.동일한 테스트 데이터 생성 로직이 반복되고 있습니다. 유지보수성을 위해 별도 메서드로 추출하면 좋겠습니다.
다음과 같이 헬퍼 메서드를 추가해보세요:
+private void createTestUser(String nickname) { + AliasJpaEntity aliasJpaEntity = AliasJpaEntity.builder() + .value("칭호") + .color("blue") + .imageUrl("http://image.url") + .build(); + aliasJpaRepository.save(aliasJpaEntity); + + UserJpaEntity userJpaEntity = UserJpaEntity.builder() + .email("test@test.com") + .nickname(nickname) + .imageUrl("http://image.url") + .role(USER) + .aliasForUserJpaEntity(aliasJpaEntity) + .build(); + userJpaRepository.save(userJpaEntity); +}src/test/java/konkuk/thip/user/adapter/in/web/ShowAliasChoiceViewControllerTest.java (3)
67-79: JSON 파싱 로직 단순화 가능현재 JsonNode를 거쳐 객체로 변환하는 방식이 복잡합니다. MockMvc의 응답을 직접 검증하거나 더 간단한 방식으로 개선할 수 있습니다.
방법 1: MockMvc 직접 검증 (권장)
-String json = result.andReturn().getResponse().getContentAsString(); -JsonNode jsonNode = objectMapper.readTree(json); -ShowAliasChoiceResponse showResponse = objectMapper.treeToValue(jsonNode.get("data"), ShowAliasChoiceResponse.class); -List<ShowAliasChoiceResponse.AliasChoice> choices = showResponse.aliasChoices(); - -assertThat(choices).hasSize(2); -assertThat(choices) - .extracting("aliasName", "categoryName", "imageUrl", "color") - .containsExactlyInAnyOrder( - tuple("문학가", "문학", "문학가_image", "red"), - tuple("과학자", "과학/IT", "과학자_image", "blue") - ); +result.andExpect(jsonPath("$.data.aliasChoices", hasSize(2))) + .andExpect(jsonPath("$.data.aliasChoices[*].aliasName", containsInAnyOrder("문학가", "과학자"))) + .andExpect(jsonPath("$.data.aliasChoices[*].categoryName", containsInAnyOrder("문학", "과학/IT")));방법 2: 응답 객체 직접 변환
-String json = result.andReturn().getResponse().getContentAsString(); -JsonNode jsonNode = objectMapper.readTree(json); -ShowAliasChoiceResponse showResponse = objectMapper.treeToValue(jsonNode.get("data"), ShowAliasChoiceResponse.class); +String json = result.andReturn().getResponse().getContentAsString(); +JsonNode rootNode = objectMapper.readTree(json); +ShowAliasChoiceResponse showResponse = objectMapper.convertValue(rootNode.get("data"), ShowAliasChoiceResponse.class);
81-107: 테스트 데이터 설정 메서드 개선 가능현재 하드코딩된 테스트 데이터 설정을 더 유연하고 가독성 있게 개선할 수 있습니다.
private void saveAliasesAndCategories() { - AliasJpaEntity alias1 = AliasJpaEntity.builder() - .value("문학가") - .imageUrl("문학가_image") - .color("red") - .build(); - aliasJpaRepository.save(alias1); - - CategoryJpaEntity category1 = CategoryJpaEntity.builder() - .value("문학") - .aliasForCategoryJpaEntity(alias1) - .build(); - categoryJpaRepository.save(category1); - - AliasJpaEntity alias2 = AliasJpaEntity.builder() - .value("과학자") - .imageUrl("과학자_image") - .color("blue") - .build(); - aliasJpaRepository.save(alias2); - - CategoryJpaEntity category2 = CategoryJpaEntity.builder() - .value("과학/IT") - .aliasForCategoryJpaEntity(alias2) - .build(); - categoryJpaRepository.save(category2); + createAliasWithCategory("문학가", "문학가_image", "red", "문학"); + createAliasWithCategory("과학자", "과학자_image", "blue", "과학/IT"); +} + +private void createAliasWithCategory(String aliasValue, String imageUrl, String color, String categoryValue) { + AliasJpaEntity alias = AliasJpaEntity.builder() + .value(aliasValue) + .imageUrl(imageUrl) + .color(color) + .build(); + aliasJpaRepository.save(alias); + + CategoryJpaEntity category = CategoryJpaEntity.builder() + .value(categoryValue) + .aliasForCategoryJpaEntity(alias) + .build(); + categoryJpaRepository.save(category); }
81-107: 테스트 데이터를 상수화하는 것을 고려해보세요.하드코딩된 테스트 데이터들을 클래스 상수로 추출하면 유지보수성이 향상될 것입니다.
다음과 같이 상수를 추가해보세요:
+private static final String ALIAS1_NAME = "문학가"; +private static final String ALIAS1_IMAGE = "문학가_image"; +private static final String ALIAS1_COLOR = "red"; +private static final String CATEGORY1_NAME = "문학"; + +private static final String ALIAS2_NAME = "과학자"; +private static final String ALIAS2_IMAGE = "과학자_image"; +private static final String ALIAS2_COLOR = "blue"; +private static final String CATEGORY2_NAME = "과학/IT"; private void saveAliasesAndCategories() { AliasJpaEntity alias1 = AliasJpaEntity.builder() - .value("문학가") - .imageUrl("문학가_image") - .color("red") + .value(ALIAS1_NAME) + .imageUrl(ALIAS1_IMAGE) + .color(ALIAS1_COLOR) .build();src/main/java/konkuk/thip/user/adapter/in/web/response/ShowAliasChoiceResponse.java (1)
9-20: null 안전성을 고려한 방어 코드 추가를 권장합니다.현재 구현은 깔끔하지만, null 값에 대한 처리가 부족합니다. 특히 left join 결과로 category 정보가 null일 수 있는 상황을 고려해야 합니다.
다음과 같이 null 체크를 추가하는 것을 고려해보세요:
public static ShowAliasChoiceResponse of(AliasChoiceViewResult result) { + if (result == null || result.aliasChoices() == null) { + return new ShowAliasChoiceResponse(List.of()); + } List<AliasChoice> choices = result.aliasChoices().stream() .map(ac -> new AliasChoice( ac.aliasId(), ac.aliasName(), - ac.categoryName(), + ac.categoryName() != null ? ac.categoryName() : "", ac.imageUrl(), ac.color() )) .toList(); return new ShowAliasChoiceResponse(choices); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (33)
build.gradle(1 hunks)src/main/java/konkuk/thip/config/QuerydslConfig.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java(2 hunks)src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/request/VerifyNicknameRequest.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/ShowAliasChoiceResponse.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/VerifyNicknameResponse.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/AliasJpaRepository.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/AliasRepositoryCustom.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/AliasRepositoryCustomImpl.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/UserRepositoryCustom.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/UserRepositoryCustomImpl.java(1 hunks)src/main/java/konkuk/thip/user/application/port/in/ShowAliasChoiceViewUseCase.java(1 hunks)src/main/java/konkuk/thip/user/application/port/in/VerifyNicknameUseCase.java(1 hunks)src/main/java/konkuk/thip/user/application/port/in/dto/AliasChoiceViewResult.java(1 hunks)src/main/java/konkuk/thip/user/application/port/in/dto/DummyResult.java(0 hunks)src/main/java/konkuk/thip/user/application/port/out/AliasQueryPort.java(1 hunks)src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java(1 hunks)src/main/java/konkuk/thip/user/application/service/ShowAliasChoiceViewService.java(1 hunks)src/main/java/konkuk/thip/user/application/service/VerifyNicknameService.java(1 hunks)src/test/java/konkuk/thip/config/TestQuerydslConfig.java(1 hunks)src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java(1 hunks)src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java(2 hunks)src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java(1 hunks)src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java(2 hunks)src/test/java/konkuk/thip/user/adapter/in/web/ShowAliasChoiceViewControllerTest.java(1 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java(9 hunks)src/test/java/konkuk/thip/user/adapter/in/web/VerifyNicknameControllerTest.java(1 hunks)src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java(1 hunks)
💤 Files with no reviewable changes (1)
- src/main/java/konkuk/thip/user/application/port/in/dto/DummyResult.java
🧰 Additional context used
🧬 Code Graph Analysis (6)
src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java (4)
src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java (1)
DataJpaTest(22-96)src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java (1)
DataJpaTest(26-135)src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java (1)
DataJpaTest(16-61)src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java (1)
DataJpaTest(27-135)
src/main/java/konkuk/thip/user/adapter/out/persistence/UserRepositoryCustomImpl.java (1)
src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java (1)
Repository(8-19)
src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java (4)
src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java (1)
DataJpaTest(22-96)src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java (1)
DataJpaTest(26-135)src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java (1)
DataJpaTest(20-103)src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java (1)
DataJpaTest(27-135)
src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java (4)
src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java (1)
DataJpaTest(26-135)src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java (1)
DataJpaTest(16-61)src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java (1)
DataJpaTest(20-103)src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java (1)
DataJpaTest(27-135)
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasRepositoryCustomImpl.java (1)
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryPersistenceAdapter.java (1)
Repository(8-18)
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryPersistenceAdapter.java (1)
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasRepositoryCustomImpl.java (1)
Repository(13-40)
🔇 Additional comments (57)
src/main/java/konkuk/thip/user/adapter/out/persistence/UserRepositoryCustom.java (1)
3-5: 커스텀 리포지토리 인터페이스가 올바르게 정의되었습니다.현재는 빈 인터페이스이지만, 이는 Spring Data JPA의 커스텀 리포지토리 패턴을 따른 올바른 접근법입니다. 향후 복잡한 Querydsl 쿼리가 필요할 때 이 인터페이스를 통해 확장할 수 있습니다.
src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java (1)
5-5: 닉네임 존재 여부 확인 메서드가 적절하게 추가되었습니다.메서드명이 명확하고 반환 타입이 직관적입니다. 클린 아키텍처의 포트 패턴을 잘 따르고 있습니다.
src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java (2)
15-15: Import 추가가 적절합니다.
22-22: Querydsl 지원을 위한 테스트 설정이 올바르게 추가되었습니다.@DataJpaTest는 기본적으로 JPA 관련 빈만 로드하므로, TestQuerydslConfig를 임포트하여 Querydsl 관련 빈을 추가하는 것은 적절한 접근법입니다. 다른 JPA 엔티티 테스트 클래스들과 일관된 패턴을 따르고 있습니다.
src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java (2)
20-20: Import 추가가 적절합니다.
29-29: 테스트 설정이 다른 JPA 엔티티 테스트들과 일관되게 업데이트되었습니다.TestQuerydslConfig를 임포트하여 @DataJpaTest 환경에서 Querydsl 지원을 활성화하는 것은 올바른 접근법입니다.
src/main/java/konkuk/thip/user/application/port/in/VerifyNicknameUseCase.java (1)
3-6: 유스케이스 인터페이스가 클린 아키텍처 원칙에 따라 적절하게 정의되었습니다.메서드명이 명확하고 단일 책임 원칙을 따르고 있습니다. 닉네임 중복 검사라는 비즈니스 로직을 잘 캡슐화하고 있습니다.
src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java (1)
11-11: 테스트에서 Querydsl 지원을 위한 일관된 패턴 적용다른 JPA 엔티티 테스트 클래스들과 동일한 패턴으로
TestQuerydslConfig를 임포트하여@DataJpaTest에서 제외되는 Querydsl 관련 빈들을 추가했습니다. 이는 프로젝트 전반에 걸쳐 일관된 테스트 설정을 유지하는 좋은 접근 방식입니다.Also applies to: 18-18
src/main/java/konkuk/thip/user/adapter/in/web/response/VerifyNicknameResponse.java (1)
3-7: 간결하고 명확한 응답 DTO 구현Java record를 사용한 간결한 구현이며, 정적 팩토리 메서드를 제공하여 인스턴스 생성을 편리하게 했습니다. 필드명
isVerified도 검증 결과를 명확하게 표현하고 있어 적절합니다.src/main/java/konkuk/thip/user/adapter/out/persistence/UserRepositoryCustomImpl.java (1)
5-11: 커스텀 레포지토리 구조 적절함Querydsl을 활용한 복잡한 조회 로직을 위한 커스텀 레포지토리 구현체의 기본 구조가 적절하게 설정되었습니다. Spring Data JPA의 커스텀 레포지토리 패턴을 올바르게 따르고 있으며, 향후 Querydsl 구현을 위한 준비가 완료되었습니다.
src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java (1)
19-19: 일관된 Querydsl 테스트 설정 적용다른 JPA 엔티티 테스트들과 동일하게
TestQuerydslConfig를 임포트하여 Querydsl 지원을 활성화했습니다. 프로젝트 전반의 일관성을 유지하는 좋은 변경사항입니다.Also applies to: 28-28
src/main/java/konkuk/thip/user/application/port/in/ShowAliasChoiceViewUseCase.java (1)
5-8: 명확한 유스케이스 인터페이스 정의칭호 선택 화면을 위한 유스케이스 인터페이스가 명확하게 정의되었습니다. 메서드명
getAllAliasesAndCategories()가 기능을 잘 표현하고 있으며, 헥사고날 아키텍처의 인바운드 포트 패턴을 적절히 따르고 있습니다.src/main/java/konkuk/thip/user/adapter/out/persistence/AliasRepositoryCustom.java (1)
7-7: 메서드 네이밍이 명확하고 좋습니다.
getAllAliasesAndCategories()메서드명이 기능을 명확히 표현하고 있어 가독성이 우수합니다.src/main/java/konkuk/thip/user/application/port/out/AliasQueryPort.java (1)
5-8: 포트 인터페이스 설계가 우수합니다.헥사고날 아키텍처의 출력 포트 역할을 명확히 하고 있으며, 단일 책임 원칙을 잘 따르고 있습니다.
src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java (1)
17-17: Querydsl 지원을 위한 일관성 있는 설정입니다.다른 JPA 엔티티 테스트 파일들(
UserJpaEntityTest,RoomJpaEntityTest,VoteJpaEntityTest,RecordJpaEntityTest)과 동일한 패턴으로TestQuerydslConfig를 import해서 일관성이 우수합니다.@DataJpaTest에서 기본적으로 제외되는 Querydsl bean을 추가하는 적절한 설정입니다.Also applies to: 24-24
src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java (1)
3-3: Wildcard import 사용이 적절합니다.여러 validation annotation을 사용하고 있어 wildcard import가 코드를 더 깔끔하게 만들어줍니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java (2)
6-6: 커스텀 리포지토리 확장이 적절합니다.
UserRepositoryCustom인터페이스를 확장하여 Querydsl을 활용한 복잡한 쿼리 로직을 분리할 수 있게 되었습니다. 향후 확장성을 고려한 좋은 설계입니다.
8-8: 닉네임 중복 검사 메서드가 명확합니다.
existsByNickname메서드명이 Spring Data JPA의 네이밍 규칙을 잘 따르고 있으며, 반환 타입도 중복 검사 용도에 적합합니다.src/main/java/konkuk/thip/user/adapter/out/persistence/AliasJpaRepository.java (1)
6-6: 깔끔한 Repository 패턴 구현입니다.사용자 정의 쿼리를 위한 인터페이스 확장이 적절하게 이루어졌습니다. Spring Data JPA의 표준 패턴을 잘 따르고 있습니다.
src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java (1)
14-21: REST API 설계가 잘 되어 있습니다.새로운 칭호 선택 엔드포인트가 깔끔하게 구현되었습니다. Use Case 패턴을 활용한 의존성 주입과 BaseResponse를 통한 일관된 응답 구조가 좋습니다.
src/main/java/konkuk/thip/config/QuerydslConfig.java (1)
9-18: 표준적인 Querydsl 설정입니다.EntityManager를 통한 JPAQueryFactory 빈 설정이 올바르게 구현되었습니다. Querydsl 통합을 위한 표준 패턴을 잘 따르고 있습니다.
src/main/java/konkuk/thip/user/application/service/ShowAliasChoiceViewService.java (1)
9-18: 잘 구현된 어플리케이션 서비스입니다.
- 단일 책임 원칙을 잘 따르고 있습니다
- Port에 대한 단순한 위임으로 계층 간 역할이 명확합니다
- Lombok과 Spring 어노테이션을 적절히 활용했습니다
src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java (2)
12-12: 필드명 개선이 좋습니다.
jpaRepository에서userJpaRepository로 변경하여 더 구체적이고 명확한 네이밍이 되었습니다.
15-18: 닉네임 중복 검사 로직이 올바르게 구현되었습니다.JPA Repository에 적절히 위임하는 깔끔한 구현입니다.
src/main/java/konkuk/thip/user/application/service/VerifyNicknameService.java (1)
8-18: 닉네임 유일성 검증 로직이 정확합니다.
!userQueryPort.existsByNickname(nickname)로직이 올바릅니다:
- 닉네임이 존재하면 유일하지 않으므로
false반환- 닉네임이 존재하지 않으면 유일하므로
true반환Clean Architecture 패턴을 잘 따르고 있으며, 단일 책임을 가진 깔끔한 서비스입니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryPersistenceAdapter.java (1)
8-17: Persistence Adapter가 적절히 구현되었습니다.
- Repository에 대한 단순한 위임으로 계층 간 역할이 명확합니다
@Repository어노테이션과 생성자 주입을 올바르게 사용했습니다관련 코드 스니펫(
AliasRepositoryCustomImpl.java)을 보면 실제 쿼리는 Querydsl을 사용하여 alias와 category 간 left join을 수행하고 있어 적절한 구현으로 보입니다.앞서 언급한 것처럼,
port.in.dto패키지의 DTO를 persistence 계층에서 사용하는 점은 작성자가 언급한 설계 결정으로 이해됩니다.src/main/java/konkuk/thip/user/adapter/out/persistence/AliasRepositoryCustomImpl.java (1)
24-36: 엔티티 클래스 정의를 통해 Join 조건을 확실히 검증하기 위해 아래 스크립트를 실행해주세요:#!/bin/bash # AliasJpaEntity 클래스 정의 확인 rg -n "class\s\+AliasJpaEntity" -C 3 # CategoryJpaEntity 클래스 정의 확인 rg -n "class\s\+CategoryJpaEntity" -C 3 # CategoryJpaEntity 내 aliasForCategoryJpaEntity 필드 확인 rg -n "aliasForCategoryJpaEntity" -C 3src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java (3)
30-35: 닉네임 검증 엔드포인트가 올바르게 구현되었습니다.API 설계가 기존 패턴과 일관성을 유지하고 있으며, 적절한 검증 어노테이션을 사용하고 있습니다. 응답 형식도 일관성 있게 구현되었습니다.
21-21: 의존성 주입이 적절히 구현되었습니다.VerifyNicknameUseCase 의존성이 올바르게 주입되어 관심사 분리가 잘 이루어졌습니다.
30-35: 닉네임 중복 검사 API 구현이 우수합니다.기존 코드 스타일과 일관성을 유지하면서 새로운 엔드포인트를 깔끔하게 추가했습니다. BaseResponse 래핑과 @validated 어노테이션 사용이 적절합니다.
src/main/java/konkuk/thip/user/adapter/in/web/response/ShowAliasChoiceResponse.java (2)
7-29: Record 클래스와 팩토리 메서드가 잘 구현되었습니다.Java 17의 record 기능을 적절히 활용하여 불변 데이터를 표현했습니다. 스트림을 사용한 매핑 로직도 깔끔하게 구현되었습니다.
9-20: 데이터 변환 로직이 효율적입니다.
AliasChoiceViewResult에서ShowAliasChoiceResponse로의 변환이 명확하고 타입 안전하게 구현되었습니다.toList()사용도 적절합니다.build.gradle (3)
49-54: Querydsl 의존성이 올바르게 구성되었습니다.Spring Boot 3.x와 호환되는 jakarta 버전을 사용한 것이 적절합니다. 어노테이션 프로세서 설정도 올바르게 구성되었습니다.
56-68: 빌드 설정이 적절히 구성되었습니다.생성된 소스 디렉토리 설정과 clean 태스크 구성이 올바릅니다. 이를 통해 Querydsl 생성 클래스들이 적절히 관리됩니다.
50-50: Querydsl 버전 확인이 필요합니다.5.0.0 버전이 최신인지 확인하고, 보안 취약점이 없는지 검증해보세요.
Querydsl 5.0.0 버전의 최신성과 보안 취약점 여부 확인src/test/java/konkuk/thip/user/adapter/in/web/VerifyNicknameControllerTest.java (5)
30-51: 테스트 클래스 구조가 잘 설계되었습니다.Spring Boot 테스트 설정과 MockMvc 사용이 적절하며,
@AfterEach를 통한 데이터 정리도 올바르게 구현되었습니다.
53-73: 고유 닉네임 테스트가 올바르게 구현되었습니다.정상적인 케이스에 대한 테스트 로직이 명확하고, JSON 응답 검증도 적절히 수행되고 있습니다.
75-111: 중복 닉네임 테스트가 포괄적으로 구현되었습니다.테스트 데이터 생성과 중복 케이스 검증 로직이 올바르게 구현되어 있습니다. 별명과 사용자 엔티티 생성 로직도 적절합니다.
113-156: 검증 실패 케이스들이 철저히 테스트되었습니다.공백, 잘못된 패턴, 길이 초과 등 다양한 검증 실패 시나리오가 모두 테스트되어 있으며, 오류 코드와 메시지 검증도 적절합니다.
53-156: 테스트 커버리지가 우수합니다.다양한 시나리오(성공, 실패, 검증 오류)를 모두 포함한 포괄적인 테스트입니다. 에러 메시지와 상태 코드 검증도 적절합니다.
src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java (15)
46-50: 테스트 격리를 위한 tearDown 메서드 추가 - 좋은 개선테스트 간 격리를 보장하기 위한 tearDown 메서드 추가는 훌륭한 개선사항입니다. 삭제 순서도 올바르게 설정되어 있습니다.
65-65: 테스트 데이터의 일관성 개선기존 "테스트유저" 닉네임이 한글로 통일되어 더 일관성 있는 테스트 데이터가 되었습니다.
124-125: 닉네임 검증 에러 메시지 개선공백 닉네임에 대한 에러 메시지가 더 구체적이고 명확해졌습니다. 사용자에게 허용되는 문자와 제약사항을 명확히 전달합니다.
127-144: 닉네임 패턴 검증 테스트 케이스 추가 - 훌륭한 보완특수문자가 포함된 닉네임에 대한 검증 테스트 케이스 추가는 닉네임 검증 로직의 완성도를 높입니다.
152-152: 11자 닉네임 테스트 케이스 개선"11글자닉네임입니다아"는 정확히 11자로 경계값 테스트에 적합합니다. 기존보다 더 정확한 테스트 케이스입니다.
91-91: 테스트 메서드명 단순화 - 가독성 향상메서드명이 더 간결하고 명확해졌습니다:
signup_alias_id_null_exception→signup_alias_id_nullsignup_nickname_blank_exception→signup_nickname_blanksignup_nickname_length_over_exception→signup_nickname_too_longsignup_email_blank_exception→signup_email_blanksignup_email_format_invalid_exception→signup_email_invalid_formatAlso applies to: 110-110, 148-148, 167-167, 186-186
10-11: 테스트 설정 개선을 위한 import 추가 확인됨
@AfterEachimport 추가로 테스트 격리를 위한 설정이 잘 준비되었습니다.
32-32: 클래스명 변경이 적절합니다
UserCommandControllerTest에서UserSignupControllerTest로 변경하여 테스트 대상을 더 명확하게 표현했습니다.
65-65: 테스트 데이터 닉네임 변경 확인됨닉네임이 "테스트유저"로 변경되어 한글 닉네임 테스트를 더 명확하게 수행할 수 있습니다.
91-91: 테스트 메서드명 개선메서드명이
signup_alias_id_null로 단순화되어 가독성이 향상되었습니다.
110-110: 닉네임 검증 로직 강화에 따른 에러 메시지 업데이트공백 닉네임에 대한 에러 메시지가 더 구체적으로 변경되었습니다. 새로운 검증 규칙에 맞는 메시지로 업데이트된 것으로 보입니다.
Also applies to: 124-125
127-144: 닉네임 패턴 검증 테스트 추가특수문자를 포함한 닉네임에 대한 새로운 테스트 케이스가 추가되어 검증 커버리지가 향상되었습니다. 한글, 영어, 숫자만 허용하는 정책이 제대로 테스트되고 있습니다.
148-149: 닉네임 길이 테스트 케이스 개선11글자 닉네임 "11글자닉네임입니다아"를 사용하여 경계값 테스트를 더 명확하게 수행합니다. 정확히 11자를 테스트하는 것이 적절합니다.
Also applies to: 152-152
167-167: 이메일 관련 테스트 메서드명 단순화
signup_email_blank와signup_email_invalid_format으로 메서드명이 단순화되어 일관성과 가독성이 향상되었습니다.Also applies to: 186-186
46-50: AliasJpaEntity 클래스의 정의와 매핑 정보를 확인하기 위해 아래 스크립트를 실행해주세요.#!/bin/bash # 1) AliasJpaEntity 파일 경로 및 클래스 선언 확인 rg -n "class AliasJpaEntity" -g "*AliasJpaEntity.java" # 2) AliasJpaEntity 내부의 mappedBy 확인 rg -n "mappedBy" -g "*AliasJpaEntity.java" # 3) AliasJpaEntity 내부의 @JoinColumn 확인 rg -n "@JoinColumn" -g "*AliasJpaEntity.java"src/test/java/konkuk/thip/user/adapter/in/web/ShowAliasChoiceViewControllerTest.java (2)
67-79: 응답 검증 방식이 효과적입니다.ObjectMapper를 사용한 역직렬화와 tuple을 활용한 다중 필드 검증이 매우 깔끔합니다. 테스트의 의도가 명확하게 드러납니다.
48-51: 다음 스크립트를 실행해CategoryJpaEntity와AliasJpaEntity정의 및 매핑 어노테이션(@ManyToOne,@OneToMany등)을 확인하겠습니다:#!/bin/bash # CategoryJpaEntity 클래스 정의 검색 rg "class CategoryJpaEntity" -n . # AliasJpaEntity 클래스 정의 검색 rg "class AliasJpaEntity" -n . # 외래키 매핑 어노테이션 검색 rg "@ManyToOne.*Category" -n . rg "@OneToMany.*Alias" -n .
| @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") | ||
| @Size(max = 10, message = "닉네임은 최대 10자 입니다.") | ||
| String nickname, |
There was a problem hiding this comment.
🛠️ Refactor suggestion
닉네임 검증 로직 강화가 우수하지만 null 체크 검토가 필요합니다.
@Pattern을 통한 문자 제한과 @Size를 통한 길이 제한으로 더 엄격한 검증이 가능해졌습니다. 하지만 기존 @NotBlank를 제거했기 때문에 null이나 빈 문자열에 대한 검증이 누락될 수 있습니다.
@Pattern annotation은 null 값에 대해서는 검증을 통과시키므로, @NotBlank 또는 @NotNull과 함께 사용하는 것을 권장합니다.
다음과 같이 수정하는 것을 제안합니다:
+ @NotBlank(message = "닉네임은 필수입니다.")
@Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)")
@Size(max = 10, message = "닉네임은 최대 10자 입니다.")📝 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.
| @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") | |
| @Size(max = 10, message = "닉네임은 최대 10자 입니다.") | |
| String nickname, | |
| @NotBlank(message = "닉네임은 필수입니다.") | |
| @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") | |
| @Size(max = 10, message = "닉네임은 최대 10자 입니다.") | |
| String nickname, |
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java
around lines 10 to 12, the nickname field lacks null or blank string validation
because @NotBlank was removed. Add the @NotBlank annotation back to ensure the
nickname is neither null nor empty, complementing the existing @Pattern and
@Size validations for stricter input checks.
| public record VerifyNicknameRequest( | ||
| @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") | ||
| @Size(max = 10, message = "닉네임은 최대 10자 입니다.") | ||
| String nickname | ||
| ) { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
null/빈 값 검증을 추가해주세요.
닉네임 검증 로직은 잘 구현되었지만, null이나 빈 문자열에 대한 검증이 누락되었습니다.
@notblank 어노테이션을 추가하여 완전한 검증을 구현해주세요:
+import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
public record VerifyNicknameRequest(
+ @NotBlank(message = "닉네임을 입력해주세요.")
@Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)")
@Size(max = 10, message = "닉네임은 최대 10자 입니다.")
String nickname
) {
}📝 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 record VerifyNicknameRequest( | |
| @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") | |
| @Size(max = 10, message = "닉네임은 최대 10자 입니다.") | |
| String nickname | |
| ) { | |
| import jakarta.validation.constraints.NotBlank; | |
| import jakarta.validation.constraints.Pattern; | |
| import jakarta.validation.constraints.Size; | |
| public record VerifyNicknameRequest( | |
| @NotBlank(message = "닉네임을 입력해주세요.") | |
| @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") | |
| @Size(max = 10, message = "닉네임은 최대 10자 입니다.") | |
| String nickname | |
| ) { | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/in/web/request/VerifyNicknameRequest.java
around lines 6 to 10, the nickname field lacks validation for null or empty
values. Add the @NotBlank annotation to the nickname field to ensure it is
neither null nor blank, complementing the existing pattern and size validations
for complete input validation.
| @AfterEach | ||
| void tearDown() { | ||
| categoryJpaRepository.deleteAll(); | ||
| aliasJpaRepository.deleteAll(); | ||
| } |
There was a problem hiding this comment.
외래 키 제약 조건으로 인한 삭제 순서 문제 발생 가능
카테고리가 별칭을 참조하는 구조에서 카테고리를 먼저 삭제하지 않으면 외래 키 제약 조건 오류가 발생할 수 있습니다.
@AfterEach
void tearDown() {
- categoryJpaRepository.deleteAll();
- aliasJpaRepository.deleteAll();
+ categoryJpaRepository.deleteAll();
+ aliasJpaRepository.deleteAll();
}현재 코드가 올바른 순서로 되어 있지만, 명확성을 위해 주석을 추가하는 것을 권장합니다:
@AfterEach
void tearDown() {
+ // 외래 키 제약 조건으로 인해 카테고리를 먼저 삭제
categoryJpaRepository.deleteAll();
aliasJpaRepository.deleteAll();
}📝 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.
| @AfterEach | |
| void tearDown() { | |
| categoryJpaRepository.deleteAll(); | |
| aliasJpaRepository.deleteAll(); | |
| } | |
| @AfterEach | |
| void tearDown() { | |
| // 외래 키 제약 조건으로 인해 카테고리를 먼저 삭제 | |
| categoryJpaRepository.deleteAll(); | |
| aliasJpaRepository.deleteAll(); | |
| } |
🤖 Prompt for AI Agents
In
src/test/java/konkuk/thip/user/adapter/in/web/ShowAliasChoiceViewControllerTest.java
around lines 47 to 51, the tearDown method deletes categoryJpaRepository before
aliasJpaRepository, which is the correct order to avoid foreign key constraint
issues. To improve clarity, add a comment explaining that aliasJpaRepository
must be deleted after categoryJpaRepository due to foreign key constraints,
ensuring future maintainers understand the deletion order rationale.
| package konkuk.thip.user.adapter.out.persistence; | ||
|
|
||
| import com.querydsl.core.types.Projections; | ||
| import com.querydsl.jpa.impl.JPAQueryFactory; | ||
| import konkuk.thip.room.adapter.out.jpa.QCategoryJpaEntity; | ||
| import konkuk.thip.user.adapter.out.jpa.QAliasJpaEntity; | ||
| import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Repository | ||
| @RequiredArgsConstructor | ||
| public class AliasRepositoryCustomImpl implements AliasRepositoryCustom { | ||
|
|
||
| private final JPAQueryFactory jpaQueryFactory; | ||
|
|
||
| @Override | ||
| public AliasChoiceViewResult getAllAliasesAndCategories() { | ||
| QAliasJpaEntity alias = QAliasJpaEntity.aliasJpaEntity; | ||
| QCategoryJpaEntity category = QCategoryJpaEntity.categoryJpaEntity; | ||
|
|
||
| List<AliasChoiceViewResult.AliasChoice> aliasChoices = jpaQueryFactory | ||
| .select(Projections.constructor( | ||
| AliasChoiceViewResult.AliasChoice.class, | ||
| alias.aliasId, | ||
| alias.value, | ||
| category.value, | ||
| alias.imageUrl, | ||
| alias.color | ||
| )) | ||
| .from(alias) | ||
| .leftJoin(category) | ||
| .on(category.aliasForCategoryJpaEntity.eq(alias)) | ||
| .fetch(); | ||
|
|
||
| return new AliasChoiceViewResult(aliasChoices); | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
코드 구조는 좋으나 몇 가지 개선점이 있습니다.
전반적으로 Querydsl을 잘 활용한 구현입니다. 하지만 다음 사항들을 고려해보세요:
- null 체크 누락: left join 결과로 category 값이 null일 수 있는데, DTO 생성 시 NPE 가능성이 있습니다.
- 정렬 조건 부재: 결과 순서가 보장되지 않아 클라이언트에서 예측하기 어려울 수 있습니다.
다음과 같이 개선을 제안합니다:
List<AliasChoiceViewResult.AliasChoice> aliasChoices = jpaQueryFactory
.select(Projections.constructor(
AliasChoiceViewResult.AliasChoice.class,
alias.aliasId,
alias.value,
- category.value,
+ category.value.coalesce(""),
alias.imageUrl,
alias.color
))
.from(alias)
.leftJoin(category)
.on(category.aliasForCategoryJpaEntity.eq(alias))
+ .orderBy(alias.aliasId.asc())
.fetch();📝 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.
| package konkuk.thip.user.adapter.out.persistence; | |
| import com.querydsl.core.types.Projections; | |
| import com.querydsl.jpa.impl.JPAQueryFactory; | |
| import konkuk.thip.room.adapter.out.jpa.QCategoryJpaEntity; | |
| import konkuk.thip.user.adapter.out.jpa.QAliasJpaEntity; | |
| import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; | |
| import lombok.RequiredArgsConstructor; | |
| import org.springframework.stereotype.Repository; | |
| import java.util.List; | |
| @Repository | |
| @RequiredArgsConstructor | |
| public class AliasRepositoryCustomImpl implements AliasRepositoryCustom { | |
| private final JPAQueryFactory jpaQueryFactory; | |
| @Override | |
| public AliasChoiceViewResult getAllAliasesAndCategories() { | |
| QAliasJpaEntity alias = QAliasJpaEntity.aliasJpaEntity; | |
| QCategoryJpaEntity category = QCategoryJpaEntity.categoryJpaEntity; | |
| List<AliasChoiceViewResult.AliasChoice> aliasChoices = jpaQueryFactory | |
| .select(Projections.constructor( | |
| AliasChoiceViewResult.AliasChoice.class, | |
| alias.aliasId, | |
| alias.value, | |
| category.value, | |
| alias.imageUrl, | |
| alias.color | |
| )) | |
| .from(alias) | |
| .leftJoin(category) | |
| .on(category.aliasForCategoryJpaEntity.eq(alias)) | |
| .fetch(); | |
| return new AliasChoiceViewResult(aliasChoices); | |
| } | |
| } | |
| package konkuk.thip.user.adapter.out.persistence; | |
| import com.querydsl.core.types.Projections; | |
| import com.querydsl.jpa.impl.JPAQueryFactory; | |
| import konkuk.thip.room.adapter.out.jpa.QCategoryJpaEntity; | |
| import konkuk.thip.user.adapter.out.jpa.QAliasJpaEntity; | |
| import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; | |
| import lombok.RequiredArgsConstructor; | |
| import org.springframework.stereotype.Repository; | |
| import java.util.List; | |
| @Repository | |
| @RequiredArgsConstructor | |
| public class AliasRepositoryCustomImpl implements AliasRepositoryCustom { | |
| private final JPAQueryFactory jpaQueryFactory; | |
| @Override | |
| public AliasChoiceViewResult getAllAliasesAndCategories() { | |
| QAliasJpaEntity alias = QAliasJpaEntity.aliasJpaEntity; | |
| QCategoryJpaEntity category = QCategoryJpaEntity.categoryJpaEntity; | |
| List<AliasChoiceViewResult.AliasChoice> aliasChoices = jpaQueryFactory | |
| .select(Projections.constructor( | |
| AliasChoiceViewResult.AliasChoice.class, | |
| alias.aliasId, | |
| alias.value, | |
| category.value.coalesce(""), // coalesce to avoid null | |
| alias.imageUrl, | |
| alias.color | |
| )) | |
| .from(alias) | |
| .leftJoin(category) | |
| .on(category.aliasForCategoryJpaEntity.eq(alias)) | |
| .orderBy(alias.aliasId.asc()) // ensure stable ordering | |
| .fetch(); | |
| return new AliasChoiceViewResult(aliasChoices); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasRepositoryCustomImpl.java
lines 1 to 40, the current code does not handle potential null values from the
left join on category, which can cause NullPointerExceptions when constructing
the DTO, and it lacks an explicit order by clause, making the result order
unpredictable. To fix this, add null-safe handling for category fields when
projecting to AliasChoiceViewResult.AliasChoice, such as using conditional
expressions or default values, and include an orderBy clause on a relevant field
like alias.aliasId to ensure consistent result ordering.
There was a problem hiding this comment.
@buzz0331 좋은 질문입니다! 정렬 조건의 필요성은 비즈니스 요구사항에 따라 달라집니다.
정렬이 필요한 경우:
- UI에서 칭호 목록이 일정한 순서로 표시되어야 하는 경우
- 사용자 경험상 매번 동일한 순서가 중요한 경우
- API 응답의 일관성이 중요한 경우
정렬이 불필요한 경우:
- 프론트엔드에서 별도로 정렬을 처리하는 경우
- 칭호 표시 순서가 비즈니스적으로 중요하지 않은 경우
- 성능상 정렬 오버헤드를 피하고 싶은 경우
질문드립니다:
- 프론트엔드에서 칭호 목록을 어떻게 표시하나요? (그리드, 리스트 등)
- 특정 순서(예: 이름순, ID순)로 보여줘야 하는 요구사항이 있나요?
- 프론트엔드에서 별도 정렬 로직을 처리하고 있나요?
이런 정보를 바탕으로 정렬 조건 추가 여부를 결정하는 것이 좋겠습니다.
There was a problem hiding this comment.
@buzz0331 음 딱히 피그마에 보여지는 칭호들의 정렬조건에 대한 말이 없으니, aliasId 기준으로 정렬하면 좋을것 같습니다
반영해서 수정하겠습니다
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
buzz0331
left a comment
There was a problem hiding this comment.
고생하셨습니다! 헥사고날 사용하니까 확실히 간단한 로직도 코드양이 많네요 😅
| @Repository | ||
| @RequiredArgsConstructor | ||
| public class AliasRepositoryCustomImpl implements AliasRepositoryCustom { | ||
|
|
||
| private final JPAQueryFactory jpaQueryFactory; |
There was a problem hiding this comment.
p3: 음 명칭을 AliasQueryRepositoryImpl과 AliasQueryRepository 어떤가요!! 단순 의견이긴 합니다.
There was a problem hiding this comment.
오 좋은것 같습니다! 반영해서 수정하겠습ㄴ다
|
|
||
| import java.util.List; | ||
|
|
||
| public record ShowAliasChoiceResponse(List<AliasChoice> aliasChoices) { |
| public record AliasChoice( | ||
| Long aliasId, | ||
| String aliasName, | ||
| String categoryName, | ||
| String imageUrl, | ||
| String color | ||
| ) {} |
There was a problem hiding this comment.
p3: inner class로 사용하는 건 좋긴 한데 다른 클래스의 inner class와 중복된 이름을 사용하면 유지보수하는데 별로 좋지 않다고 생각합니다! 현재 application.port.in.dto의 AliasChoiceViewResult와 adapter.in.web.response의 ShowAliasChoiceResponse내부 AliasChoice가 동일해서 이 부분에 대해서 컨벤션을 한번 정해보는거 어떨까요.
예를 들면, adapter.in.web.response의 innerclass는 AliasChoiceResponse 이런식으로 하거나..?
There was a problem hiding this comment.
현준님 말처럼 Result, Response dto가 담고 있는 데이터가 동일할 경우, 이 dto 들을 굳이 분리할 필요가 있을까, 분리한다면 inner class 의 네이밍을 어떻게 할까 의 컨벤션을 정하는 것도 좋은 것 같습니다
There was a problem hiding this comment.
어차피 innerclass라면 ShowAliasChoiceResponse.AliasChoice AliasChoiceViewResult.AliasChoice 이렇게 구분지을수 있지않나요..? 데이터가 동일하다는걸 뜻하기도하니까 저도 굳이 분리해야 싶은듯하네요..🤔🤔
There was a problem hiding this comment.
저도 히진님 의견처럼 inner class 니 네이밍을 자유롭게 해도 괜찮지 않나라는 생각에 동일한 네이밍으로 일단 구현한 거긴 합니다! (네이밍 정하기 힘듦 이슈)
이 부분은 저희끼리 의견한번 맞춰보면 좋을거 같습니다!
There was a problem hiding this comment.
그럼 우선 inner class의 중복 이름을 허용하는 것으로 해보죠! 희진님 말대로 결국 다른 클래스에서 참조하려면 outerclass와 함께 참조해줘야 하니...👍🏻👍🏻
| package konkuk.thip.user.adapter.out.persistence; | ||
|
|
||
| import com.querydsl.core.types.Projections; | ||
| import com.querydsl.jpa.impl.JPAQueryFactory; | ||
| import konkuk.thip.room.adapter.out.jpa.QCategoryJpaEntity; | ||
| import konkuk.thip.user.adapter.out.jpa.QAliasJpaEntity; | ||
| import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Repository | ||
| @RequiredArgsConstructor | ||
| public class AliasRepositoryCustomImpl implements AliasRepositoryCustom { | ||
|
|
||
| private final JPAQueryFactory jpaQueryFactory; | ||
|
|
||
| @Override | ||
| public AliasChoiceViewResult getAllAliasesAndCategories() { | ||
| QAliasJpaEntity alias = QAliasJpaEntity.aliasJpaEntity; | ||
| QCategoryJpaEntity category = QCategoryJpaEntity.categoryJpaEntity; | ||
|
|
||
| List<AliasChoiceViewResult.AliasChoice> aliasChoices = jpaQueryFactory | ||
| .select(Projections.constructor( | ||
| AliasChoiceViewResult.AliasChoice.class, | ||
| alias.aliasId, | ||
| alias.value, | ||
| category.value, | ||
| alias.imageUrl, | ||
| alias.color | ||
| )) | ||
| .from(alias) | ||
| .leftJoin(category) | ||
| .on(category.aliasForCategoryJpaEntity.eq(alias)) | ||
| .fetch(); | ||
|
|
||
| return new AliasChoiceViewResult(aliasChoices); | ||
| } | ||
| } |
hd0rable
left a comment
There was a problem hiding this comment.
수고하셨어요 !!🤩👍🏻 리뷰남긴것처럼 alias delete관련해서 한번 생각해볼필요가 있을것같아욥..
| alias.value, | ||
| category.value, | ||
| alias.imageUrl, | ||
| alias.color |
There was a problem hiding this comment.
P2:status값 관련해서 ACTIVE한 Alias만 조회하지않아도 괜찮을까요? Alias같은 경우 저희가 직접 다루는값이니까 버전업데이트라던가 클라이언트(PM)요구사항에 많이 변경되는 값이면 INACTIVE로 소프트DELETE하는게 맞을것같아서요! 소프트DELETE하는 값이라면 jpa엔티티에 @SQLRestriction를 추가해야할것같습니다!
@buzz0331 현준님 의견은 어떠신가요??
There was a problem hiding this comment.
아 아직 AliasJpaEntity 에 대해 @SQLRestriction을 고려하지 않았습니다!
희진님 리뷰처럼 alias 에 db 제약조건 추가하는게 좋을듯 합니다
There was a problem hiding this comment.
넵 저도 희진님 의견에 동의합니다! 다만, 제 기억으로 삭제 전략은 다음 회의에 한번에 적용하기로 했던 것으로 기억하긴합니다 ㅎ
- interface : xxxQueryRepository - 구현체 : xxxQueryRepositoryImpl
팀 컨벤션에 맞추어 수정
- 기존 build.gradle 코드 중 tasks.withType(JavaCompile) 로 인해 테스트 컴파일 단계에서 Q클래스 중복 생성되는 에러 확인 - compileJava 테스크에서만(= main 소스를 컴파일할 때만) Q클래스 생성하도록 강제
|
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java (1)
7-7: 애플리케이션 레이어 DTO 의존성 문제퍼시스턴스 구현체에서 애플리케이션 레이어의 DTO를 직접 사용하고 있습니다. 앞서 인터페이스에서 언급한 것과 동일한 아키텍처 의존성 방향 위반 문제입니다.
🧹 Nitpick comments (2)
src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserSignupRequest.java (1)
3-3: 와일드카드 import 사용을 구체적인 import로 변경하는 것을 권장합니다.와일드카드 import(
*)는 네임스페이스 오염을 야기하고 실제 사용되는 클래스를 명확하게 파악하기 어렵게 만듭니다.-import jakarta.validation.constraints.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size;src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepository.java (1)
7-7: 메서드 시그니처 개선 고려현재 메서드는 파라미터가 없어 모든 칭호와 카테고리를 조회합니다. 향후 필터링이나 페이징이 필요할 수 있으니 확장 가능성을 고려해보세요.
- AliasChoiceViewResult getAllAliasesAndCategories(); + AliasChoiceViewResult getAllAliasesAndCategories(); // 현재 요구사항에 맞게 유지 + // 향후 확장을 위한 오버로드 메서드 고려
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (18)
build.gradle(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java(2 hunks)src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserSignupRequest.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserVerifyNicknameRequest.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/GetUserShowAliasChoiceResponse.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserSignupResponse.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserVerifyNicknameResponse.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/UserSignupResponse.java(0 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/AliasJpaRepository.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepository.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepository.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepositoryImpl.java(1 hunks)src/test/java/konkuk/thip/user/adapter/in/web/ShowAliasChoiceViewControllerTest.java(1 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java(9 hunks)src/test/java/konkuk/thip/user/adapter/in/web/VerifyNicknameControllerTest.java(1 hunks)
💤 Files with no reviewable changes (1)
- src/main/java/konkuk/thip/user/adapter/in/web/response/UserSignupResponse.java
✅ Files skipped from review due to trivial changes (8)
- src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepository.java
- src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepositoryImpl.java
- src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserSignupResponse.java
- src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserVerifyNicknameRequest.java
- src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserVerifyNicknameResponse.java
- src/main/java/konkuk/thip/user/adapter/in/web/response/GetUserShowAliasChoiceResponse.java
- src/test/java/konkuk/thip/user/adapter/in/web/VerifyNicknameControllerTest.java
- src/test/java/konkuk/thip/user/adapter/in/web/ShowAliasChoiceViewControllerTest.java
🚧 Files skipped from review as they are similar to previous changes (6)
- src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java
- src/main/java/konkuk/thip/user/adapter/out/persistence/AliasJpaRepository.java
- src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java
- build.gradle
- src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java
- src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java
🔇 Additional comments (5)
src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserSignupRequest.java (2)
6-6: 클래스명 변경이 RESTful 명명 규칙에 잘 부합합니다.
PostUserSignupRequest로의 변경은 HTTP 메서드를 명확히 나타내고 API 엔드포인트와의 일관성을 제공합니다.
10-11: 닉네임 패턴 검증 로직이 요구사항에 잘 맞습니다.한글, 영어, 숫자만 허용하고 공백을 제거하는 패턴과 최대 10자 제한이 적절하게 구현되어 있습니다. 정규식
[가-힣a-zA-Z0-9]+이 정확합니다.src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java (3)
25-32: AliasChoiceViewResult.AliasChoice 생성자 매개변수 매핑 확인 완료검증 결과, 프로젝션에서 전달하는 값
- alias.aliasId
- alias.value
- category.value
- alias.imageUrl
- alias.color
가
AliasChoiceViewResult.AliasChoice레코드 생성자의 순서
- Long aliasId
- String aliasName
- String categoryName
- String imageUrl
- String color
와 완전히 일치하므로 별도 수정이 필요 없습니다.
35-35: Join 조건 검증 필요
category.aliasForCategoryJpaEntity.eq(alias)조건이 올바른 관계 매핑인지 확인이 필요합니다. 일반적으로 FK 관계에서는alias.category.eq(category)형태가 더 일반적입니다.엔티티 간 관계 설정을 다시 확인해주세요.
39-39: 반환 객체 생성 검증
AliasChoiceViewResult생성자가 빈 리스트를 적절히 처리하는지 확인이 필요합니다. 칭호가 하나도 없는 경우의 동작을 검증해보세요.
| @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") | ||
| @Size(max = 10, message = "닉네임은 최대 10자 입니다.") | ||
| String nickname, |
There was a problem hiding this comment.
닉네임 검증에 null 값 처리를 위한 @NotNull 어노테이션 추가가 필요합니다.
현재 @Pattern과 @Size 어노테이션만으로는 null 값이 검증을 통과할 수 있습니다. @Pattern 어노테이션은 null 값에 대해 검증하지 않기 때문입니다.
+ @NotNull(message = "닉네임은 필수입니다.")
@Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)")
@Size(max = 10, message = "닉네임은 최대 10자 입니다.")📝 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.
| @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") | |
| @Size(max = 10, message = "닉네임은 최대 10자 입니다.") | |
| String nickname, | |
| @NotNull(message = "닉네임은 필수입니다.") | |
| @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") | |
| @Size(max = 10, message = "닉네임은 최대 10자 입니다.") | |
| String nickname, |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserSignupRequest.java
around lines 10 to 12, the nickname field lacks null value validation. Add the
@NotNull annotation to the nickname field to ensure null values are properly
rejected during validation, complementing the existing @Pattern and @Size
constraints.
| @@ -0,0 +1,8 @@ | |||
| package konkuk.thip.user.adapter.out.persistence; | |||
|
|
|||
| import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; | |||
There was a problem hiding this comment.
아키텍처 의존성 방향 위반 검토 필요
퍼시스턴스 레이어에서 애플리케이션 레이어의 DTO(AliasChoiceViewResult)를 직접 임포트하고 있습니다. 이는 클린 아키텍처 원칙에 위배됩니다. 인프라스트럭처 레이어가 애플리케이션 레이어에 의존하지 않도록 별도의 퍼시스턴스 전용 DTO를 고려하거나, 의존성 방향을 재검토해주세요.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepository.java
at line 3, the persistence layer is directly importing the application layer DTO
AliasChoiceViewResult, violating clean architecture dependency rules. To fix
this, remove the direct import of the application layer DTO and create a
separate persistence-specific DTO or data structure within the persistence
layer. Then map the persistence entities to this new DTO internally, avoiding
dependency on the application layer.
🛠️ Refactor suggestion
아키텍처 레이어 의존성 검토 필요
persistence layer에서 application port in DTO(AliasChoiceViewResult)를 직접 사용하고 있습니다. 일반적으로 각 레이어는 자신만의 DTO를 가져야 하며, persistence layer는 application layer에 의존하지 않는 것이 바람직합니다.
persistence layer 전용 DTO를 만들고 adapter에서 매핑을 수행하는 방식을 고려해보세요.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepository.java
at line 3, the persistence layer is directly using the application port in DTO
AliasChoiceViewResult, causing an undesirable dependency on the application
layer. To fix this, create a dedicated DTO within the persistence layer and
perform mapping between this persistence DTO and the application layer DTO in
the adapter, ensuring the persistence layer does not depend on the application
layer.
| List<AliasChoiceViewResult.AliasChoice> aliasChoices = jpaQueryFactory | ||
| .select(Projections.constructor( | ||
| AliasChoiceViewResult.AliasChoice.class, | ||
| alias.aliasId, | ||
| alias.value, | ||
| category.value, | ||
| alias.imageUrl, | ||
| alias.color | ||
| )) | ||
| .from(alias) | ||
| .leftJoin(category) | ||
| .on(category.aliasForCategoryJpaEntity.eq(alias)) | ||
| .orderBy(alias.aliasId.asc()) | ||
| .fetch(); |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
QueryDSL 쿼리 로직 검증 및 성능 최적화 검토
QueryDSL 구현이 전반적으로 올바르게 작성되어 있습니다. 하지만 다음 사항들을 검토해주세요:
- 페이지네이션 부재: 모든 별칭과 카테고리를 한 번에 조회하고 있어 데이터가 많을 경우 성능 문제가 발생할 수 있습니다.
- 조인 조건 검증:
category.aliasForCategoryJpaEntity.eq(alias)조건이 올바른 엔티티 관계를 반영하는지 확인이 필요합니다.
다음 스크립트로 엔티티 관계를 확인해주세요:
🏁 Script executed:
#!/bin/bash
# Description: Verify entity relationships between Alias and Category
# Expected: Confirm the relationship mapping is correct
# Search for entity relationship mappings
ast-grep --pattern $'class $_JpaEntity {
$$$
@$_
private $_ aliasForCategoryJpaEntity;
$$$
}'
# Search for alias entity definition
ast-grep --pattern $'class AliasJpaEntity {
$$$
}'Length of output: 156
매핑 정보를 재확인하기 위해 다음 스크립트를 실행해주세요:
🏁 Script executed:
#!/bin/bash
# Alias–Category 매핑 필드 검색
rg "aliasForCategoryJpaEntity" -n src/main/java/konkuk/thip/user/adapter/out/persistence
# JPA 엔티티 클래스 목록 확인
rg "class .*JpaEntity" -n src/main/java/konkuk/thip/user/adapter/out/persistenceLength of output: 325
Alias–Category 엔티티 매핑 정의를 프로젝트 전체에서 재확인해주세요:
🏁 Script executed:
#!/bin/bash
# 프로젝트 전체에서 aliasForCategoryJpaEntity 필드 정의 검색
rg "aliasForCategoryJpaEntity" -n .
# @Entity 어노테이션이 붙은 Category 관련 클래스 확인
rg "@Entity" -n src/main/java | grep -i category
rg "class .*Category.*" -n src/main/java
# 전체 JPA 엔티티 클래스 목록 확인
rg "class .*JpaEntity" -n src/main/javaLength of output: 4731
페이징 처리 적용 권고
현재 AliasQueryRepositoryImpl의 쿼리가 모든 별칭과 카테고리를 한 번에 조회하도록 작성되어 있어, 데이터가 많아질 경우 성능 저하가 발생할 수 있습니다.
- 파일:
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java - 위치: 24–37행 (jpaQueryFactory.fetch 호출 부분)
- 개선안:
offset/limit또는 Spring DataPageable을 활용해 페이지네이션을 적용해주세요.
조인 조건(.on(category.aliasForCategoryJpaEntity.eq(alias)))은 CategoryJpaEntity에 정의된 AliasJpaEntity aliasForCategoryJpaEntity 필드와 일치하므로 올바르게 맵핑되어 있습니다.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java
around lines 24 to 37, the query fetches all alias and category records at once,
which can cause performance issues with large data sets. To fix this, modify the
query to apply pagination by adding offset and limit parameters or by
integrating Spring Data's Pageable interface. Adjust the method signature to
accept pagination parameters and use them in the jpaQueryFactory query to limit
the result size accordingly.
💡 Verification agent
🧩 Analysis chain
쿼리 최적화 및 에러 핸들링 개선
현재 구현에서 몇 가지 개선사항을 제안합니다:
- Left Join 검증: category가 항상 존재하는지 확인이 필요합니다. Inner Join이 더 적절할 수 있습니다.
- 성능 고려: 모든 데이터를 한 번에 조회하므로 데이터양이 많을 경우 성능 이슈가 발생할 수 있습니다.
- 에러 핸들링: 쿼리 실행 중 예외 처리가 없습니다.
List<AliasChoiceViewResult.AliasChoice> aliasChoices = jpaQueryFactory
.select(Projections.constructor(
AliasChoiceViewResult.AliasChoice.class,
alias.aliasId,
alias.value,
category.value,
alias.imageUrl,
alias.color
))
.from(alias)
- .leftJoin(category)
+ .innerJoin(category) // 카테고리가 필수인 경우
.on(category.aliasForCategoryJpaEntity.eq(alias))
.orderBy(alias.aliasId.asc())
.fetch();다음 스크립트로 Join 조건을 검증해보세요:
🏁 Script executed:
#!/bin/bash
# 칭호-카테고리 관계 및 null 값 확인
ast-grep --pattern $'class $_JpaEntity {
$$$
@JoinColumn$$$
private $_JpaEntity $category$;
$$$
}'Length of output: 107
아래 스크립트로 aliasForCategoryJpaEntity 필드의 매핑 정보를 다시 확인해 주세요:
🏁 Script executed:
#!/bin/bash
# aliasForCategoryJpaEntity 매핑 위치 확인
rg "aliasForCategoryJpaEntity" -n .
# aliasForCategoryJpaEntity 필드의 @ManyToOne(optional) 설정 확인
rg "@ManyToOne.*aliasForCategoryJpaEntity" -n .Length of output: 1174
아래 스크립트로 CategoryJpaEntity 파일에서 @ManyToOne 어노테이션의 위치와 optional 설정 여부를 확인해 주세요.
🏁 Script executed:
#!/bin/bash
# CategoryJpaEntity 내 @ManyToOne 어노테이션과 optional 설정 확인
rg "@ManyToOne" -n src/main/java/konkuk/thip/room/adapter/out/jpa/CategoryJpaEntity.java -C3Length of output: 92
아래 스크립트로 CategoryJpaEntity 클래스에서 aliasForCategoryJpaEntity 필드의 어노테이션과 옵션 설정을 직접 확인해 주세요:
🏁 Script executed:
#!/bin/bash
# aliasForCategoryJpaEntity 필드와 어노테이션 전체 맥락 출력
rg -C3 "aliasForCategoryJpaEntity" -n src/main/java/konkuk/thip/room/adapter/out/jpa/CategoryJpaEntity.javaLength of output: 287
쿼리 Join 타입 및 예외 처리 개선 제안
CategoryJpaEntity의 aliasForCategoryJpaEntity 필드가
@OnetoOne(fetch = LAZY)
@joincolumn(nullable = false)
로 설정되어 있어, 외래키는 항상 존재합니다.
이에 따라:
- Category가 항상 존재해야 하는 조회라면 innerJoin 으로 변경해 불필요한 outer join 비용을 줄입니다.
- alias가 Category에 매핑되지 않는 케이스가 있으면 leftJoin을 유지할지 도메인 로직을 재검토하세요.
- 현재 .fetch() 호출에는 예외 처리가 없으므로, JPAQueryException 또는 Spring DataAccessException을 잡아 적절한 커스텀 예외 반환 또는 빈 리스트 처리 로직을 추가하시기 바랍니다.
수정 예시:
List<AliasChoiceViewResult.AliasChoice> aliasChoices = jpaQueryFactory
.select(Projections.constructor(
AliasChoiceViewResult.AliasChoice.class,
alias.aliasId,
alias.value,
category.value,
alias.imageUrl,
alias.color
))
.from(alias)
- .leftJoin(category)
+ .innerJoin(category) // Category가 항상 매핑되어야 함
.on(category.aliasForCategoryJpaEntity.eq(alias))
.orderBy(alias.aliasId.asc())
- .fetch();
+ .fetch(); // TODO: 예외 처리 추가점검 포인트:
aliasForCategoryJpaEntity가 optional인지 비즈니스 규칙 확인AliasQueryRepositoryImpl메서드에 try–catch 또는 Spring 예외 변환 로직 추가
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java
around lines 24 to 37, change the join from leftJoin to innerJoin for category
since the foreign key is non-nullable and category must always exist, reducing
unnecessary outer join overhead. Additionally, wrap the fetch() call in a
try-catch block to handle JPAQueryException or Spring DataAccessException, and
return either a custom exception or an empty list as appropriate. Confirm the
business logic regarding whether alias can exist without a category before
changing the join type.
| import com.querydsl.jpa.impl.JPAQueryFactory; | ||
| import konkuk.thip.room.adapter.out.jpa.QCategoryJpaEntity; | ||
| import konkuk.thip.user.adapter.out.jpa.QAliasJpaEntity; | ||
| import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
아키텍처 레이어 의존성 재검토
persistence layer에서 application port in DTO를 직접 사용하는 것은 레이어 간 의존성 규칙을 위반합니다. 작성자가 언급한 대로 DTO 매핑 오버헤드를 줄이기 위한 결정이지만, 장기적으로 유지보수성에 영향을 줄 수 있습니다.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java
at line 7, the persistence layer is directly importing and using a DTO from the
application port in layer, which violates architectural layering principles. To
fix this, remove the import of AliasChoiceViewResult from the persistence layer
and instead create a dedicated domain or persistence-specific data structure for
query results. Perform any necessary mapping to the application DTO in the
service or application layer to maintain proper separation of concerns and layer
independence.



#️⃣ 연관된 이슈
📝 작업 내용
📸 스크린샷
💬 리뷰 요구사항
칭호 선택 view api 에서
controller에서의 response dto와 use case에서의 result dto는 구분하였지만, 영속성 계층에서는 result dto를 그대로 사용하도록 하였습니다.
모든 계층에서 서로 의존성을 가지지 않는 것이 좋긴 하지만, 조회로직 특성상 계층별로 전파되는 dto의 구성이 동일하기도 하고, 계층별로 모두 서로 다른 dto들을 사용하고 이를 매핑하는 것의 로드가 크다고 생각해서 application, persistence adapter 는 서로 동일한 dto(= Result) 를 공유하도록 하였습니다.
이 부분 참고해서 리뷰해주시면 감사하겠습니다!
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
신규 기능
POST /users/nickname) 및 닉네임 검증 기능이 추가되었습니다.GET /users/alias)가 추가되었습니다.버그 수정
리팩터링/스타일
테스트
기타