728x90
목차
- 도입 - DSL을 선택한 이유
- 전체 아키텍처 흐름
- DSL 쿼리 설계
- 응답 모델 구조
- 페이징 최적화 방식
- 정리 및 다음 편 예고
1. 도입 - DSL을 선택한 이유
우리는 사용자 프로필을 다음 조건들로 검색해야 합니다:
- 닉네임 (like 검색)
- 성별
- 지역
- 관심사 (Instrument)
- 선호 장르 (Genre)
이러한 조건은 모두 **선택적(optional)**이고, 동시에 다중 값도 허용됩니다. 이를 만족하기 위한 쿼리 조건 생성과 join이 필요한데,
Spring Data JPA의 기본 Repository만으로는 아래와 같은 제약이 있습니다:
| 방식 | 장점 | 단점 |
|---|---|---|
| Method Query | 간단한 조건 처리 | 복잡한 조합 불가 |
| Specification | 동적 조합 가능 | 연관 관계 Join Fetch 어렵고 DSL보다 Verbose |
| Querydsl | 타입 안정성, Join, 동적 조건 모두 가능 | 복잡도는 다소 있음 |
→ 따라서 DSL을 기반으로 쿼리 작성하기로 결정했습니다.
2. 전체 아키텍처 흐름
텍스트 기반 흐름도로 정리하면 다음과 같습니다:
[요청 DTO (ProfileSearchCondition)]
|
v
[DSL Repository]
├─ 조건에 따라 BooleanBuilder 구성
├─ Step1: id만 추출 (offset + limit 적용)
├─ Step2: fetchJoin으로 관계 엔티티 로딩
|
v
[UserProfile + 관심사 + 장르 데이터 로드]
|
v
[UserProfileResponse DTO 변환]
|
v
[Page<UserProfileResponse> 반환]
3. DSL 쿼리 설계
DSL 주요 구성
BooleanBuilder builder = new BooleanBuilder();
if (nickname != null) builder.and(profile.nickname.containsIgnoreCase(nickname));
if (location != null) builder.and(profile.location.eq(location));
...
Step1: ID 기준 페이징 쿼리
List<Long> ids = queryFactory
.select(profile.userId)
.from(profile)
.where(builder)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
Step2: fetchJoin 조회
List<UserProfile> results = queryFactory
.selectFrom(profile)
.leftJoin(profile.userInterests, userInterest).fetchJoin()
.leftJoin(profile.userGenres, userGenre).fetchJoin()
.where(profile.userId.in(ids))
.fetch();
이중 쿼리 방식은 N+1 문제를 근본적으로 회피하고, 페이징도 정확히 처리할 수 있는 전략입니다.
4. 응답 모델 구조
UserProfileResponse DTO
@Builder
@Getter
public class UserProfileResponse {
private Long userId;
private String nickname;
private String profileImageUrl;
private String introduction;
private String gender;
private String location;
private List<String> interests;
private List<String> genres;
}
서비스 계층에서는 UserProfile + 연관 관심사/장르를 다음처럼 변환합니다:
UserProfileResponse.builder()
.userId(profile.getUserId())
.nickname(profile.getNickname())
.interests(profile.getUserInterests().stream()
.map(i -> i.getInterest().name())
.collect(Collectors.toList()))
...
.build();
5. 페이징 최적화 방식
JPA는 fetchJoin과 offset/limit을 함께 쓰는 걸 공식적으로 금지합니다.
그렇기 때문에 우리는 다음과 같은 방식으로 분리합니다:
Step 1: userId만 추출 → 페이징 적용
Step 2: in(userIds) 조건으로 fetch join
Step 3: count 쿼리는 profile만 따로 카운트
장점:
- Join 된 테이블이 아무리 커도 페이징 정확도 보장
- 관계 데이터도 N+1 없이 한 번에 로드
6. 정리
정리
| 항목 | 구현 방식 |
|---|---|
| 필터링 조건 | BooleanBuilder 동적 조합 |
| N+1 회피 | fetchJoin + 2단계 조회 |
| 페이징 처리 | ID 기준 추출 + IN 쿼리 |
| 응답 DTO | 관심사/장르 포함 변환 |
728x90
'BindProject' 카테고리의 다른 글
| [Backend] 유저 프로필 모듈 구현5편: 응답 DTO 최적화와 Kafka 연동 흐름 정리 (0) | 2025.06.23 |
|---|---|
| [Backend] 유저 프로필 모듈 구현4-1편 N+1 문제 완전 정복: 유저 프로필 조회 최적화 여정 (1) | 2025.06.23 |
| [Backend] 유저 프로필 모듈 구현 3편. 유저 프로필 변경 이력 추적 설계와 구현 (0) | 2025.06.22 |
| [Backend] 유저 프로필 모듈 구현 2편. 유저 프로필 도입과 설계 구조: 유저 프로필 생성/삭제 처리와 관심사/장르 연관 구조 설계 (1) | 2025.06.22 |
| [Backend] 유저 프로필 모듈 구현 1편. 유저 프로필 도입과 설계 구조: Enum 기반 프로필 시스템의 선택과 대안 비교 (0) | 2025.06.22 |