Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a0ccc91
[refactor] : 회원가입 api request dto 수정 및 테스트 코드 수정 (#31)
seongjunnoh Jun 27, 2025
290e7c6
[refactor] : 테스트 메서드에 DB clear 코드 추가 (#31)
seongjunnoh Jun 27, 2025
fe71388
[feat] : 닉네임 중복 검사 api controller 구현 (#31)
seongjunnoh Jun 27, 2025
8e02a9b
[feat] : 닉네임 중복 검사 api use case 구현 (#31)
seongjunnoh Jun 27, 2025
f665e7a
[feat] : 닉네임 중복 검사 영속성 어댑터 구현 (#31)
seongjunnoh Jun 27, 2025
cfd2d7f
[test] : 닉네임 중복 검사 api 통합 테스트 코드, controller 테스트 코드 작성 (#31)
seongjunnoh Jun 27, 2025
1fa761c
[refactor] : tearDown 메서드를 @AfterEach로 변경 (#31)
seongjunnoh Jun 27, 2025
67819df
[feat] : querydsl 의존성 주입 (#31)
seongjunnoh Jun 27, 2025
d37adb3
[refactor] : querydsl 의존성 주입에 따른 기존 테스트 코드 수정 (#31)
seongjunnoh Jun 27, 2025
c869199
[feat] : 칭호 선택 view api controller 개발 (#31)
seongjunnoh Jun 27, 2025
a74ee80
[feat] : 칭호 선택 view api use case 개발 (#31)
seongjunnoh Jun 27, 2025
5d3839e
[feat] : 칭호 선택 view api query adapter 개발 (#31)
seongjunnoh Jun 27, 2025
5135aa1
[test] : 칭호 선택 view api 통합 테스트 코드 작성 (#31)
seongjunnoh Jun 27, 2025
047ea40
[refactor] : Querydsl 전용 repository 네이밍 수정 (#31)
seongjunnoh Jun 29, 2025
5cab3ff
[refactor] : aliasId 기준 오름차순 정렬 후 반환하도록 수정 (#31)
seongjunnoh Jun 29, 2025
d375564
[refactor] : request, response dto 네이밍 수정 (#31)
seongjunnoh Jun 29, 2025
32fb424
[fix] : querydsl 관련 build.gradle 에러 코드 수정 (#31)
seongjunnoh Jun 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,53 @@ repositories {
}

dependencies {
// Spring Boot 스타터
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-validation'

// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// Runtime DB 드라이버
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'

// Test
testImplementation 'org.assertj:assertj-core:3.24.2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'com.h2database:h2:2.1.214'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// Querydsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile

// QueryDSL Q-클래스는 compileJava 단계에서만 생성
tasks.named('compileJava', org.gradle.api.tasks.compile.JavaCompile) {
options.generatedSourceOutputDirectory.set(querydslDir)
}

// test 컴파일 단계에서는 어노테이션 프로세서를 비워서 Q-클래스 중복 생성 방지
tasks.named('compileTestJava', org.gradle.api.tasks.compile.JavaCompile) {
options.annotationProcessorPath = files()
}

sourceSets {
main.java.srcDirs += [ querydslDir ]
}

clean.doLast {
file(querydslDir).deleteDir()
}

tasks.named('test') {
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/konkuk/thip/config/QuerydslConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package konkuk.thip.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuerydslConfig {

@PersistenceContext
private EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package konkuk.thip.user.adapter.in.web;

import konkuk.thip.common.dto.BaseResponse;
import konkuk.thip.user.adapter.in.web.request.UserSignupRequest;
import konkuk.thip.user.adapter.in.web.response.UserSignupResponse;
import konkuk.thip.user.adapter.in.web.request.PostUserSignupRequest;
import konkuk.thip.user.adapter.in.web.request.PostUserVerifyNicknameRequest;
import konkuk.thip.user.adapter.in.web.response.PostUserSignupResponse;
import konkuk.thip.user.adapter.in.web.response.PostUserVerifyNicknameResponse;
import konkuk.thip.user.application.port.in.UserSignupUseCase;
import konkuk.thip.user.application.port.in.dto.UserSignupCommand;
import konkuk.thip.user.application.port.in.VerifyNicknameUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -16,11 +18,19 @@
public class UserCommandController {

private final UserSignupUseCase userSignupUseCase;
private final VerifyNicknameUseCase verifyNicknameUseCase;

@PostMapping("/users/signup")
public BaseResponse<UserSignupResponse> signup(@Validated @RequestBody UserSignupRequest request) {
return BaseResponse.ok(UserSignupResponse.of(
public BaseResponse<PostUserSignupResponse> signup(@Validated @RequestBody PostUserSignupRequest request) {
return BaseResponse.ok(PostUserSignupResponse.of(
userSignupUseCase.signup(request.toCommand()))
);
}

@PostMapping("/users/nickname")
public BaseResponse<PostUserVerifyNicknameResponse> verifyNickname(@Validated @RequestBody PostUserVerifyNicknameRequest request) {
return BaseResponse.ok(PostUserVerifyNicknameResponse.of(
verifyNicknameUseCase.isNicknameUnique(request.nickname()))
);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
package konkuk.thip.user.adapter.in.web;

import konkuk.thip.common.dto.BaseResponse;
import konkuk.thip.user.adapter.in.web.response.GetUserShowAliasChoiceResponse;
import konkuk.thip.user.application.port.in.ShowAliasChoiceViewUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class UserQueryController {

private final ShowAliasChoiceViewUseCase showAliasChoiceViewUseCase;

@GetMapping("/users/alias")
public BaseResponse<GetUserShowAliasChoiceResponse> showAliasChoiceView() {
return BaseResponse.ok(GetUserShowAliasChoiceResponse.of(
showAliasChoiceViewUseCase.getAllAliasesAndCategories()
));
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
package konkuk.thip.user.adapter.in.web.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.*;
import konkuk.thip.user.application.port.in.dto.UserSignupCommand;
import org.hibernate.validator.constraints.Length;

public record UserSignupRequest(
public record PostUserSignupRequest(
@NotNull(message = "aliasId는 필수입니다.")
Long aliasId,

@NotBlank(message = "닉네임은 공백일 수 없습니다.")
@Length(max = 10, message = "닉네임은 최대 10자 입니다.")
@Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)")
@Size(max = 10, message = "닉네임은 최대 10자 입니다.")
String nickname,
Comment on lines +10 to 12

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
@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.

Comment on lines +10 to 12

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

닉네임 검증에 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.

Suggested change
@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.


@NotBlank(message = "이메일은 공백일 수 없습니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package konkuk.thip.user.adapter.in.web.request;

import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

public record PostUserVerifyNicknameRequest(
@Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)")
@Size(max = 10, message = "닉네임은 최대 10자 입니다.")
String nickname
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package konkuk.thip.user.adapter.in.web.response;

import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult;

import java.util.List;

public record GetUserShowAliasChoiceResponse(List<AliasChoice> aliasChoices) {

public static GetUserShowAliasChoiceResponse of(AliasChoiceViewResult result) {
List<AliasChoice> choices = result.aliasChoices().stream()
.map(ac -> new AliasChoice(
ac.aliasId(),
ac.aliasName(),
ac.categoryName(),
ac.imageUrl(),
ac.color()
))
.toList();
return new GetUserShowAliasChoiceResponse(choices);
}

public record AliasChoice(
Long aliasId,
String aliasName,
String categoryName,
String imageUrl,
String color
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package konkuk.thip.user.adapter.in.web.response;

public record PostUserSignupResponse(Long userId) {
public static PostUserSignupResponse of(Long userId) {
return new PostUserSignupResponse(userId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package konkuk.thip.user.adapter.in.web.response;

public record PostUserVerifyNicknameResponse(boolean isVerified) {
public static PostUserVerifyNicknameResponse of(boolean isVerified) {
return new PostUserVerifyNicknameResponse(isVerified);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AliasJpaRepository extends JpaRepository<AliasJpaEntity, Long> {
public interface AliasJpaRepository extends JpaRepository<AliasJpaEntity, Long>, AliasQueryRepository {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package konkuk.thip.user.adapter.out.persistence;

import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult;
import konkuk.thip.user.application.port.out.AliasQueryPort;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class AliasQueryPersistenceAdapter implements AliasQueryPort {

private final AliasJpaRepository aliasJpaRepository;

@Override
public AliasChoiceViewResult getAllAliasesAndCategories() {
return aliasJpaRepository.getAllAliasesAndCategories();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package konkuk.thip.user.adapter.out.persistence;

import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

아키텍처 의존성 방향 위반 검토 필요

퍼시스턴스 레이어에서 애플리케이션 레이어의 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.


public interface AliasQueryRepository {

AliasChoiceViewResult getAllAliasesAndCategories();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
@RequiredArgsConstructor
public class AliasQueryRepositoryImpl implements AliasQueryRepository {

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))
.orderBy(alias.aliasId.asc())
.fetch();
Comment on lines +24 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

QueryDSL 쿼리 로직 검증 및 성능 최적화 검토

QueryDSL 구현이 전반적으로 올바르게 작성되어 있습니다. 하지만 다음 사항들을 검토해주세요:

  1. 페이지네이션 부재: 모든 별칭과 카테고리를 한 번에 조회하고 있어 데이터가 많을 경우 성능 문제가 발생할 수 있습니다.
  2. 조인 조건 검증: 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/persistence

Length 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/java

Length of output: 4731


페이징 처리 적용 권고
현재 AliasQueryRepositoryImpl의 쿼리가 모든 별칭과 카테고리를 한 번에 조회하도록 작성되어 있어, 데이터가 많아질 경우 성능 저하가 발생할 수 있습니다.

  • 파일: src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java
  • 위치: 24–37행 (jpaQueryFactory.fetch 호출 부분)
  • 개선안: offset/limit 또는 Spring Data Pageable을 활용해 페이지네이션을 적용해주세요.

조인 조건(.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

쿼리 최적화 및 에러 핸들링 개선

현재 구현에서 몇 가지 개선사항을 제안합니다:

  1. Left Join 검증: category가 항상 존재하는지 확인이 필요합니다. Inner Join이 더 적절할 수 있습니다.
  2. 성능 고려: 모든 데이터를 한 번에 조회하므로 데이터양이 많을 경우 성능 이슈가 발생할 수 있습니다.
  3. 에러 핸들링: 쿼리 실행 중 예외 처리가 없습니다.
  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 -C3

Length of output: 92


아래 스크립트로 CategoryJpaEntity 클래스에서 aliasForCategoryJpaEntity 필드의 어노테이션과 옵션 설정을 직접 확인해 주세요:


🏁 Script executed:

#!/bin/bash
# aliasForCategoryJpaEntity 필드와 어노테이션 전체 맥락 출력
rg -C3 "aliasForCategoryJpaEntity" -n src/main/java/konkuk/thip/room/adapter/out/jpa/CategoryJpaEntity.java

Length 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.


return new AliasChoiceViewResult(aliasChoices);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
import konkuk.thip.user.adapter.out.jpa.UserJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserJpaRepository extends JpaRepository<UserJpaEntity, Long> {
public interface UserJpaRepository extends JpaRepository<UserJpaEntity, Long>, UserQueryRepository {

boolean existsByNickname(String nickname);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
@RequiredArgsConstructor
public class UserQueryPersistenceAdapter implements UserQueryPort {

private final UserJpaRepository jpaRepository;
private final UserJpaRepository userJpaRepository;
private final UserMapper userMapper;

@Override
public boolean existsByNickname(String nickname) {
return userJpaRepository.existsByNickname(nickname);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package konkuk.thip.user.adapter.out.persistence;

public interface UserQueryRepository {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package konkuk.thip.user.adapter.out.persistence;

import org.springframework.stereotype.Repository;

@Repository
public class UserQueryRepositoryImpl implements UserQueryRepository {

/**
* QueryDsl 을 활용한 복잡한 조회 로직 구현
*/
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package konkuk.thip.user.application.port.in;

import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult;

public interface ShowAliasChoiceViewUseCase {

AliasChoiceViewResult getAllAliasesAndCategories();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package konkuk.thip.user.application.port.in;

public interface VerifyNicknameUseCase {

boolean isNicknameUnique(String nickname);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package konkuk.thip.user.application.port.in.dto;

import java.util.List;

public record AliasChoiceViewResult(List<AliasChoice> aliasChoices) {

public record AliasChoice(
Long aliasId,
String aliasName,
String categoryName,
String imageUrl,
String color
) {}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package konkuk.thip.user.application.port.out;

import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult;

public interface AliasQueryPort {

AliasChoiceViewResult getAllAliasesAndCategories();
}
Loading