Skip to content

Commit 9e69920

Browse files
authored
[BE] FestivalNotificationService 조회 쿼리 튜닝 및 N+1 해결 (#1030)
* refactor: Join 삭제를 위한 Native Query 변경 * refactor: Native Query 변경으로 로직 변경 * test: Native Query 테스트 코드 추가 * refactor: 축제 전체 조회 쿼리 개선 * test: 개선된 축제 조회 쿼리 테스트 추가 * refactor: 축제 조회 쿼리 메서드 네이밍 변경 * refactor: 일관성 유지를 위한 소문자 변경 * test: 테스트 네이밍 일관성 적용 * refactor: 불필요한 람다 삭제
1 parent be1ae1b commit 9e69920

File tree

5 files changed

+201
-12
lines changed

5 files changed

+201
-12
lines changed

backend/src/main/java/com/daedan/festabook/festival/infrastructure/FestivalNotificationJpaRepository.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,30 @@
33
import com.daedan.festabook.festival.domain.FestivalNotification;
44
import java.util.List;
55
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
68

79
public interface FestivalNotificationJpaRepository extends JpaRepository<FestivalNotification, Long> {
810

9-
boolean existsByFestivalIdAndDeviceId(Long festivalId, Long deviceId);
11+
@Query(value = """
12+
SELECT EXISTS(
13+
SELECT 1
14+
FROM festival_notification fn
15+
WHERE fn.festival_id = :festivalId
16+
AND fn.device_id = :deviceId
17+
AND fn.deleted = 0
18+
)
19+
""", nativeQuery = true)
20+
int getExistsFlagByFestivalIdAndDeviceId(
21+
@Param("festivalId") Long festivalId,
22+
@Param("deviceId") Long deviceId
23+
);
1024

11-
List<FestivalNotification> getAllByDeviceId(Long deviceId);
25+
@Query("""
26+
SELECT fn
27+
FROM FestivalNotification fn
28+
JOIN FETCH fn.festival f
29+
WHERE fn.device.id = :deviceId
30+
""")
31+
List<FestivalNotification> findAllWithFestivalByDeviceId(Long deviceId);
1232
}

backend/src/main/java/com/daedan/festabook/festival/service/FestivalNotificationService.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public FestivalNotificationResponse subscribeIosFestivalNotification(
8383
@Transactional(readOnly = true)
8484
public FestivalNotificationReadResponses getAllFestivalNotificationByDeviceId(Long deviceId) {
8585
Device device = getDeviceById(deviceId);
86-
List<FestivalNotification> festivalNotifications = festivalNotificationJpaRepository.getAllByDeviceId(
86+
List<FestivalNotification> festivalNotifications = festivalNotificationJpaRepository.findAllWithFestivalByDeviceId(
8787
device.getId()
8888
);
8989

@@ -94,13 +94,13 @@ public FestivalNotificationReadResponses getAllFestivalNotificationByDeviceId(Lo
9494
public void unsubscribeFestivalNotification(Long festivalNotificationId) {
9595
FestivalNotification festivalNotification = festivalNotificationJpaRepository
9696
.findById(festivalNotificationId)
97-
.orElseGet(() -> null);
97+
.orElse(null);
9898
if (festivalNotification == null) {
9999
return;
100100
}
101101

102102
Device device = deviceJpaRepository.findById(festivalNotification.getDevice().getId())
103-
.orElseGet(() -> null);
103+
.orElse(null);
104104
if (device == null) {
105105
return;
106106
}
@@ -113,7 +113,7 @@ public void unsubscribeFestivalNotification(Long festivalNotificationId) {
113113
}
114114

115115
private void validateDuplicatedFestivalNotification(Long festivalId, Long deviceId) {
116-
if (festivalNotificationJpaRepository.existsByFestivalIdAndDeviceId(festivalId, deviceId)) {
116+
if (festivalNotificationJpaRepository.getExistsFlagByFestivalIdAndDeviceId(festivalId, deviceId) > 0) {
117117
throw new BusinessException("이미 알림을 구독한 축제입니다.", HttpStatus.BAD_REQUEST);
118118
}
119119
}

backend/src/main/java/com/daedan/festabook/festival/service/TestFestivalNotificationService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public FestivalNotificationResponse subscribeFestivalNotification(
4848
@Transactional(readOnly = true)
4949
public FestivalNotificationReadResponses getAllFestivalNotificationByDeviceId(Long deviceId) {
5050
Device device = getDeviceById(deviceId);
51-
List<FestivalNotification> festivalNotifications = festivalNotificationJpaRepository.getAllByDeviceId(
51+
List<FestivalNotification> festivalNotifications = festivalNotificationJpaRepository.findAllWithFestivalByDeviceId(
5252
device.getId()
5353
);
5454

@@ -81,7 +81,7 @@ public void unsubscribeFestivalNotification(Long festivalNotificationId) {
8181
}
8282

8383
private void validateDuplicatedFestivalNotification(Long festivalId, Long deviceId) {
84-
if (festivalNotificationJpaRepository.existsByFestivalIdAndDeviceId(festivalId, deviceId)) {
84+
if (festivalNotificationJpaRepository.getExistsFlagByFestivalIdAndDeviceId(festivalId, deviceId) > 0) {
8585
throw new BusinessException("이미 알림을 구독한 축제입니다.", HttpStatus.BAD_REQUEST);
8686
}
8787
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package com.daedan.festabook.festival.infrastructure;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.daedan.festabook.device.domain.Device;
6+
import com.daedan.festabook.device.domain.DeviceFixture;
7+
import com.daedan.festabook.device.infrastructure.DeviceJpaRepository;
8+
import com.daedan.festabook.festival.domain.Festival;
9+
import com.daedan.festabook.festival.domain.FestivalFixture;
10+
import com.daedan.festabook.festival.domain.FestivalNotification;
11+
import com.daedan.festabook.festival.domain.FestivalNotificationFixture;
12+
import java.util.List;
13+
import org.junit.jupiter.api.DisplayNameGeneration;
14+
import org.junit.jupiter.api.DisplayNameGenerator;
15+
import org.junit.jupiter.api.Nested;
16+
import org.junit.jupiter.api.Test;
17+
import org.springframework.beans.factory.annotation.Autowired;
18+
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
19+
20+
@DataJpaTest
21+
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
22+
class FestivalNotificationJpaRepositoryTest {
23+
24+
@Autowired
25+
private FestivalJpaRepository festivalJpaRepository;
26+
27+
@Autowired
28+
private DeviceJpaRepository deviceJpaRepository;
29+
30+
@Autowired
31+
private FestivalNotificationJpaRepository festivalNotificationJpaRepository;
32+
33+
@Nested
34+
class getExistsFlagByFestivalIdAndDeviceId {
35+
36+
@Test
37+
void 성공_일치하는_알림이_있는_경우_1_반환() {
38+
// given
39+
Festival festival = festivalJpaRepository.save(FestivalFixture.create());
40+
Device device = deviceJpaRepository.save(DeviceFixture.create());
41+
42+
FestivalNotification festivalNotification = FestivalNotificationFixture.create(festival, device);
43+
festivalNotificationJpaRepository.save(festivalNotification);
44+
45+
// when
46+
int existsFlag = festivalNotificationJpaRepository.getExistsFlagByFestivalIdAndDeviceId(
47+
festival.getId(),
48+
device.getId()
49+
);
50+
51+
// then
52+
assertThat(existsFlag).isEqualTo(1);
53+
}
54+
55+
@Test
56+
void 성공_삭제된_알림인_경우_없다고_판단해_0_반환() {
57+
// given
58+
Festival festival = festivalJpaRepository.save(FestivalFixture.create());
59+
Device device = deviceJpaRepository.save(DeviceFixture.create());
60+
61+
FestivalNotification festivalNotification = FestivalNotificationFixture.create(festival, device);
62+
FestivalNotification savedNotification = festivalNotificationJpaRepository.save(festivalNotification);
63+
festivalNotificationJpaRepository.delete(savedNotification);
64+
65+
// when
66+
int existsFlag = festivalNotificationJpaRepository.getExistsFlagByFestivalIdAndDeviceId(
67+
festival.getId(),
68+
device.getId()
69+
);
70+
71+
// then
72+
assertThat(existsFlag).isZero();
73+
}
74+
75+
@Test
76+
void 성공_알림이_존재하지_않는_경우_0_반환() {
77+
// given
78+
Festival festival = festivalJpaRepository.save(FestivalFixture.create());
79+
Device device = deviceJpaRepository.save(DeviceFixture.create());
80+
81+
// when
82+
int existsFlag = festivalNotificationJpaRepository.getExistsFlagByFestivalIdAndDeviceId(
83+
festival.getId(),
84+
device.getId()
85+
);
86+
87+
// then
88+
assertThat(existsFlag).isZero();
89+
}
90+
}
91+
92+
@Nested
93+
class findAllWithFestivalByDeviceId {
94+
95+
@Test
96+
void 성공_하나의_디바이스가_구독한_여러_축제_조회() {
97+
// given
98+
Device device = deviceJpaRepository.save(DeviceFixture.create());
99+
100+
Festival firstFestival = festivalJpaRepository.save(FestivalFixture.create());
101+
Festival secondFestival = festivalJpaRepository.save(FestivalFixture.create());
102+
103+
FestivalNotification first = festivalNotificationJpaRepository.save(
104+
FestivalNotificationFixture.create(firstFestival, device)
105+
);
106+
107+
FestivalNotification second = festivalNotificationJpaRepository.save(
108+
FestivalNotificationFixture.create(secondFestival, device)
109+
);
110+
111+
// when
112+
List<FestivalNotification> result = festivalNotificationJpaRepository.findAllWithFestivalByDeviceId(
113+
device.getId());
114+
115+
// then
116+
assertThat(result)
117+
.extracting(FestivalNotification::getId)
118+
.containsExactlyInAnyOrder(
119+
first.getId(),
120+
second.getId()
121+
);
122+
}
123+
124+
@Test
125+
void 성공_다른_디바이스_알림은_제외하고_조회() {
126+
// given
127+
Festival targetFestival = festivalJpaRepository.save(FestivalFixture.create());
128+
Device targetDevice = deviceJpaRepository.save(DeviceFixture.create());
129+
130+
FestivalNotification targetNotification = festivalNotificationJpaRepository.save(
131+
FestivalNotificationFixture.create(targetFestival, targetDevice)
132+
);
133+
134+
Festival otherFestival = festivalJpaRepository.save(FestivalFixture.create());
135+
Device otherDevice = deviceJpaRepository.save(DeviceFixture.create());
136+
137+
festivalNotificationJpaRepository.save(FestivalNotificationFixture.create(otherFestival, otherDevice));
138+
139+
// when
140+
List<FestivalNotification> result = festivalNotificationJpaRepository.findAllWithFestivalByDeviceId(
141+
targetDevice.getId()
142+
);
143+
144+
// then
145+
assertThat(result).containsExactly(targetNotification);
146+
}
147+
148+
@Test
149+
void 성공_삭제된_알림은_조회_안됨() {
150+
// given
151+
Festival festival = festivalJpaRepository.save(FestivalFixture.create());
152+
Device device = deviceJpaRepository.save(DeviceFixture.create());
153+
154+
FestivalNotification festivalNotification = festivalNotificationJpaRepository.save(
155+
FestivalNotificationFixture.create(festival, device)
156+
);
157+
158+
festivalNotificationJpaRepository.delete(festivalNotification);
159+
160+
// when
161+
List<FestivalNotification> result = festivalNotificationJpaRepository.findAllWithFestivalByDeviceId(
162+
device.getId()
163+
);
164+
165+
// then
166+
assertThat(result).isEmpty();
167+
}
168+
}
169+
}

backend/src/test/java/com/daedan/festabook/festival/service/FestivalNotificationServiceTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ class subscribeFestivalNotification {
9797

9898
FestivalNotificationRequest request = FestivalNotificationRequestFixture.create(deviceId);
9999

100-
given(festivalNotificationJpaRepository.existsByFestivalIdAndDeviceId(festivalId, deviceId))
101-
.willReturn(true);
100+
given(festivalNotificationJpaRepository.getExistsFlagByFestivalIdAndDeviceId(festivalId, deviceId))
101+
.willReturn(1);
102102

103103
// when & then
104104
assertThatThrownBy(() ->
@@ -165,7 +165,7 @@ class getAllFestivalNotificationByDeviceId {
165165

166166
given(deviceJpaRepository.findById(deviceId))
167167
.willReturn(Optional.of(device));
168-
given(festivalNotificationJpaRepository.getAllByDeviceId(deviceId))
168+
given(festivalNotificationJpaRepository.findAllWithFestivalByDeviceId(deviceId))
169169
.willReturn(festivalNotifications);
170170

171171
// when
@@ -180,7 +180,7 @@ class getAllFestivalNotificationByDeviceId {
180180
.isEqualTo(festivalNotification1.getFestival().getUniversityName());
181181
s.assertThat(response1.festivalName())
182182
.isEqualTo(festivalNotification1.getFestival().getFestivalName());
183-
183+
184184
FestivalNotificationReadResponse response2 = result.responses().get(1);
185185
s.assertThat(response2.universityName())
186186
.isEqualTo(festivalNotification2.getFestival().getUniversityName());

0 commit comments

Comments
 (0)