Skip to content

Commit e4b0c4d

Browse files
authored
Merge pull request #146 from Team-Festimate/feat/#145
[feat] #145 매칭 후보 조회 로직에 QueryDSL 도입 및 최적화
2 parents 1022668 + 66a3a39 commit e4b0c4d

7 files changed

Lines changed: 144 additions & 44 deletions

File tree

build.gradle

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ plugins {
44
id 'io.spring.dependency-management' version '1.1.7'
55
}
66

7-
group = 'org.festiamte'
7+
group = 'org.festimate'
88
version = '0.0.1-SNAPSHOT'
99

1010
java {
@@ -58,8 +58,26 @@ dependencies {
5858
testImplementation 'io.projectreactor:reactor-test' // WebFlux 테스트 지원
5959
testImplementation 'org.springframework.security:spring-security-test' // Security 테스트 지원
6060
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' // JUnit 테스트 실행기
61+
62+
// --- QueryDSL ---
63+
implementation "com.querydsl:querydsl-jpa:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
64+
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
65+
66+
// --- Jakarta Persistence API ---
67+
implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'
68+
annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'
69+
}
70+
71+
sourceSets {
72+
main {
73+
java {
74+
srcDir 'src/main/java'
75+
// Q-타입 생성 위치를 IDE가 인식하도록 추가
76+
srcDir 'build/generated/sources/annotationProcessor/java/main'
77+
}
78+
}
6179
}
6280

6381
tasks.named('test') {
6482
useJUnitPlatform()
65-
}
83+
}

src/main/java/org/festimate/team/domain/matching/repository/MatchingRepository.java

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,7 @@
1212
import java.util.List;
1313
import java.util.Optional;
1414

15-
public interface MatchingRepository extends JpaRepository<Matching, Long> {
16-
@Query("""
17-
SELECT p FROM Participant p
18-
JOIN FETCH p.user
19-
LEFT JOIN Matching m1
20-
ON (m1.applicantParticipant = p OR m1.targetParticipant = p)
21-
WHERE p.festival.festivalId = :festivalId
22-
AND p.typeResult = :typeResult
23-
AND p.user.gender != :gender
24-
AND p.participantId != :participantId
25-
AND p.participantId NOT IN (
26-
SELECT m.targetParticipant.participantId
27-
FROM Matching m
28-
WHERE m.applicantParticipant.participantId = :participantId
29-
AND m.status = 'COMPLETED'
30-
)
31-
GROUP BY p
32-
ORDER BY COUNT(m1) ASC
33-
""")
34-
List<Participant> findMatchingCandidates(
35-
@Param("participantId") Long participantId,
36-
@Param("typeResult") TypeResult typeResult,
37-
@Param("gender") Gender gender,
38-
@Param("festivalId") Long festivalId,
39-
Pageable pageable
40-
);
41-
42-
15+
public interface MatchingRepository extends JpaRepository<Matching, Long>, MatchingRepositoryCustom {
4316
@Query("""
4417
SELECT m FROM Matching m
4518
WHERE m.festival.festivalId = :festivalId
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.festimate.team.domain.matching.repository;
2+
3+
import org.festimate.team.domain.participant.entity.Participant;
4+
import org.festimate.team.domain.participant.entity.TypeResult;
5+
import org.festimate.team.domain.user.entity.Gender;
6+
import org.springframework.data.domain.Pageable;
7+
8+
import java.util.List;
9+
10+
public interface MatchingRepositoryCustom {
11+
List<Long> findExcludeIds(Long applicantId);
12+
13+
List<Participant> findMatchingCandidatesDsl(
14+
Long applicantId,
15+
TypeResult typeResult,
16+
Gender gender,
17+
Long festivalId,
18+
Pageable pageable,
19+
List<Long> excludedIds
20+
);
21+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package org.festimate.team.domain.matching.repository;
2+
3+
import com.querydsl.jpa.impl.JPAQueryFactory;
4+
import lombok.RequiredArgsConstructor;
5+
import org.festimate.team.domain.matching.entity.MatchingStatus;
6+
import org.festimate.team.domain.matching.entity.QMatching;
7+
import org.festimate.team.domain.participant.entity.Participant;
8+
import org.festimate.team.domain.participant.entity.QParticipant;
9+
import org.festimate.team.domain.participant.entity.TypeResult;
10+
import org.festimate.team.domain.user.entity.Gender;
11+
import org.festimate.team.domain.user.entity.QUser;
12+
import org.springframework.data.domain.Pageable;
13+
14+
import java.util.List;
15+
16+
@RequiredArgsConstructor
17+
public class MatchingRepositoryImpl implements MatchingRepositoryCustom {
18+
19+
private final JPAQueryFactory queryFactory;
20+
21+
private final QParticipant p = QParticipant.participant;
22+
private final QMatching m = QMatching.matching;
23+
private final QMatching mSub = new QMatching("mSub");
24+
private final QUser u = QUser.user;
25+
26+
@Override
27+
public List<Long> findExcludeIds(Long applicantId) {
28+
return queryFactory
29+
.select(mSub.targetParticipant.participantId)
30+
.from(mSub)
31+
.where(
32+
mSub.applicantParticipant.participantId.eq(applicantId)
33+
.and(mSub.status.eq(MatchingStatus.COMPLETED))
34+
)
35+
.fetch();
36+
}
37+
38+
@Override
39+
public List<Participant> findMatchingCandidatesDsl(
40+
Long applicantId,
41+
TypeResult typeResult,
42+
Gender gender,
43+
Long festivalId,
44+
Pageable pageable,
45+
List<Long> excludedIds
46+
) {
47+
return queryFactory
48+
.selectFrom(p)
49+
.join(p.user, u).fetchJoin()
50+
.leftJoin(m)
51+
.on(m.applicantParticipant.eq(p).or(m.targetParticipant.eq(p)))
52+
.where(
53+
p.festival.festivalId.eq(festivalId),
54+
p.typeResult.eq(typeResult),
55+
u.gender.ne(gender),
56+
p.participantId.ne(applicantId),
57+
excludedIds.isEmpty() ? null : p.participantId.notIn(excludedIds)
58+
)
59+
.groupBy(p)
60+
.orderBy(m.count().asc())
61+
.offset(pageable.getOffset())
62+
.limit(pageable.getPageSize())
63+
.fetch();
64+
}
65+
}

src/main/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImpl.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,18 @@ protected Matching saveMatching(Festival festival, Optional<Participant> targetP
9090
@Transactional
9191
@Override
9292
public Optional<Participant> findBestCandidateByPriority(long festivalId, Participant participant) {
93+
List<Long> excludedIds = matchingRepository.findExcludeIds(participant.getParticipantId());
9394
List<TypeResult> priorities = MATCHING_PRIORITIES.get(participant.getTypeResult());
9495
Gender myGender = participant.getUser().getGender();
9596

9697
for (TypeResult priorityType : priorities) {
97-
Optional<Participant> candidate = matchingRepository.findMatchingCandidates(
98+
Optional<Participant> candidate = matchingRepository.findMatchingCandidatesDsl(
9899
participant.getParticipantId(),
99100
priorityType,
100101
myGender,
101102
festivalId,
102-
PageRequest.of(0, 1)
103+
PageRequest.of(0, 1),
104+
excludedIds
103105
).stream().findFirst();
104106

105107
if (candidate.isPresent()) {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.festimate.team.infra.config;
2+
3+
import com.querydsl.jpa.impl.JPAQueryFactory;
4+
import jakarta.persistence.EntityManager;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
8+
@Configuration
9+
public class QuerydslConfig {
10+
@Bean
11+
public JPAQueryFactory jpaQueryFactory(EntityManager em) {
12+
return new JPAQueryFactory(em);
13+
}
14+
}

src/test/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImplTest.java

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,15 @@ void findBestCandidateByPriority_success() {
116116
.festival(festival)
117117
.build();
118118
ReflectionTestUtils.setField(applicant, "participantId", 1L);
119+
List<Long> excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId());
119120

120121
Participant target = Participant.builder()
121122
.user(targetUser)
122123
.typeResult(TypeResult.PHOTO)
123124
.festival(festival)
124125
.build();
125126

126-
when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1)))
127+
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
127128
.thenReturn(List.of(target));
128129

129130
var result = matchingService.findBestCandidateByPriority(festival.getFestivalId(), applicant);
@@ -145,9 +146,10 @@ void findBestCandidate_prioritySecond_success() {
145146
.festival(festival)
146147
.build();
147148
ReflectionTestUtils.setField(applicant, "participantId", 1L);
149+
List<Long> excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId());
148150

149151
// 1순위 PHOTO에 대상 없음
150-
when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1)))
152+
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
151153
.thenReturn(Collections.emptyList());
152154

153155
// 2순위 INFLUENCER에 대상 있음
@@ -157,7 +159,7 @@ void findBestCandidate_prioritySecond_success() {
157159
.festival(festival)
158160
.build();
159161

160-
when(matchingRepository.findMatchingCandidates(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1)))
162+
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
161163
.thenReturn(List.of(secondPriorityCandidate));
162164

163165
var result = matchingService.findBestCandidateByPriority(festival.getFestivalId(), applicant);
@@ -176,16 +178,18 @@ void findBestCandidate_priorityThird_success() {
176178

177179
// 신청자
178180
Participant applicant = mockParticipant(applicantUser, festival, TypeResult.INFLUENCER, 1L);
181+
List<Long> excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId());
182+
179183
// 3순위 NEWBIE에 대상 있음
180184
Participant thirdPriorityCandidate = mockParticipant(thirdPriorityUser, festival, TypeResult.NEWBIE, 2L);
181185

182186
// 1순위 PHOTO, 2순위 INFLUENCER 대상 없음
183-
when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1)))
187+
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
184188
.thenReturn(Collections.emptyList());
185-
when(matchingRepository.findMatchingCandidates(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1)))
189+
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
186190
.thenReturn(Collections.emptyList());
187191
// 3순위 대상 있음
188-
when(matchingRepository.findMatchingCandidates(1L, TypeResult.NEWBIE, Gender.MAN, 1L, PageRequest.of(0, 1)))
192+
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.NEWBIE, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
189193
.thenReturn(List.of(thirdPriorityCandidate));
190194

191195
var result = matchingService.findBestCandidateByPriority(festival.getFestivalId(), applicant);
@@ -206,13 +210,14 @@ void findBestCandidate_alreadyMatchedCandidate_empty() {
206210
.festival(festival)
207211
.build();
208212
ReflectionTestUtils.setField(applicant, "participantId", 1L);
213+
List<Long> excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId());
209214

210215
// 대상이 존재하나 이미 매칭됨 (Repository에서 필터링 됨)
211-
when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1)))
216+
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
212217
.thenReturn(Collections.emptyList());
213-
when(matchingRepository.findMatchingCandidates(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1)))
218+
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.INFLUENCER, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
214219
.thenReturn(Collections.emptyList());
215-
when(matchingRepository.findMatchingCandidates(1L, TypeResult.NEWBIE, Gender.MAN, 1L, PageRequest.of(0, 1)))
220+
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.NEWBIE, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
216221
.thenReturn(Collections.emptyList());
217222

218223
Optional<Participant> result = matchingService.findBestCandidateByPriority(festival.getFestivalId(), applicant);
@@ -231,9 +236,10 @@ void findBestCandidateByPriority_empty() {
231236
.typeResult(TypeResult.INFLUENCER)
232237
.festival(festival)
233238
.build();
239+
List<Long> excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId());
234240

235-
when(matchingRepository.findMatchingCandidates(
236-
applicant.getParticipantId(), TypeResult.PHOTO, Gender.MAN, festival.getFestivalId(), PageRequest.of(0, 1)
241+
when(matchingRepository.findMatchingCandidatesDsl(
242+
applicant.getParticipantId(), TypeResult.PHOTO, Gender.MAN, festival.getFestivalId(), PageRequest.of(0, 1), excludedIds
237243
)).thenReturn(Collections.emptyList());
238244

239245
Optional<Participant> result = matchingService.findBestCandidateByPriority(
@@ -369,14 +375,15 @@ void findBestCandidate_multipleCandidates_returnsFirstOnly() {
369375
User applicantUser = mockUser("신청자", Gender.MAN, 1L);
370376
Festival festival = mockFestival(applicantUser, 1L, LocalDate.now().minusDays(1), LocalDate.now().plusDays(1));
371377
Participant applicant = mockParticipant(applicantUser, festival, TypeResult.INFLUENCER, 1L);
378+
List<Long> excludedIds = matchingRepository.findExcludeIds(applicant.getParticipantId());
372379

373380
User candidate1 = mockUser("후보1", Gender.WOMAN, 2L);
374381
User candidate2 = mockUser("후보2", Gender.WOMAN, 3L);
375382
Participant p1 = mockParticipant(candidate1, festival, TypeResult.PHOTO, 2L);
376383
Participant p2 = mockParticipant(candidate2, festival, TypeResult.PHOTO, 3L);
377384

378385
// 후보 2명 반환
379-
when(matchingRepository.findMatchingCandidates(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1)))
386+
when(matchingRepository.findMatchingCandidatesDsl(1L, TypeResult.PHOTO, Gender.MAN, 1L, PageRequest.of(0, 1), excludedIds))
380387
.thenReturn(List.of(p1, p2));
381388

382389
// when

0 commit comments

Comments
 (0)