Skip to content

Commit e54717d

Browse files
kih1015claude
andauthored
fix: 버스 공지 n+1 쿼리 개선 (#2199)
* fix: getBusNoticeArticle N+1 쿼리 제거 - Projection 적용 버스 공지 게시글 조회 시 Article 엔티티를 전체 로드하면서 @PostLoad updateAuthor()가 트리거되어 @OnetoOne LAZY 관계 (koinArticle, koreatechArticle 등)에 대한 N+1 쿼리가 발생하던 문제 수정. 필요한 필드(id, title, createdAt)만 조회하는 BusArticleProjection을 도입해 엔티티 로드 없이 단일 쿼리로 처리되도록 개선. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: BusArticleProjection을 인터페이스에서 일반 클래스로 변경 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: BusArticleProjection을 record로 변경 및 JPQL NEW 명령어 적용 - BusArticleProjection class → record로 교체 - Native Query + REGEXP → JPQL + NEW 키워드 + LIKE 조건으로 변경 - LIMIT 5 → Pageable(PageRequest.of(0, 5))로 처리 - BusNoticeArticle.from() → of()로 정리 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 33ec82c commit e54717d

4 files changed

Lines changed: 64 additions & 35 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package in.koreatech.koin.domain.community.article.dto;
2+
3+
import java.time.LocalDateTime;
4+
5+
public record BusArticleProjection(
6+
Integer id,
7+
String title,
8+
LocalDateTime createdAt
9+
) {
10+
11+
}

src/main/java/in/koreatech/koin/domain/community/article/model/redis/BusNoticeArticle.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package in.koreatech.koin.domain.community.article.model.redis;
22

3-
import in.koreatech.koin.domain.community.article.model.Article;
43
import org.springframework.data.annotation.Id;
4+
import org.springframework.data.redis.core.RedisHash;
5+
56
import lombok.Builder;
67
import lombok.Getter;
7-
import org.springframework.data.redis.core.RedisHash;
88

99
@Getter
1010
@RedisHash(value = "busNoticeArticle")
@@ -21,10 +21,10 @@ private BusNoticeArticle(Integer id, String title) {
2121
this.title = title;
2222
}
2323

24-
public static BusNoticeArticle from(Article article) {
24+
public static BusNoticeArticle of(int id, String title) {
2525
return BusNoticeArticle.builder()
26-
.id(article.getId())
27-
.title(article.getTitle())
28-
.build();
26+
.id(id)
27+
.title(title)
28+
.build();
2929
}
3030
}

src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.springframework.data.repository.Repository;
1515
import org.springframework.data.repository.query.Param;
1616

17+
import in.koreatech.koin.domain.community.article.dto.BusArticleProjection;
1718
import in.koreatech.koin.domain.community.article.exception.ArticleNotFoundException;
1819
import in.koreatech.koin.domain.community.article.exception.BoardNotFoundException;
1920
import in.koreatech.koin.domain.community.article.model.Article;
@@ -184,8 +185,19 @@ List<Article> findAllByRegisteredAtIsAfterExcludingBoardId(@Param("registeredAt"
184185
@Query("SELECT a.title FROM Article a WHERE a.id = :id")
185186
String getTitleById(@Param("id") Integer id);
186187

187-
@Query(value = "SELECT * FROM new_articles a "
188-
+ "WHERE a.title REGEXP '통학버스|등교버스|셔틀버스|하교버스' AND a.is_notice = true "
189-
+ "ORDER BY a.created_at DESC LIMIT 5", nativeQuery = true)
190-
List<Article> findBusArticlesTop5OrderByCreatedAtDesc();
188+
@Query("""
189+
SELECT new in.koreatech.koin.domain.community.article.dto.BusArticleProjection(
190+
a.id, a.title, a.createdAt
191+
)
192+
FROM Article a
193+
WHERE (
194+
a.title LIKE '%통학버스%'
195+
OR a.title LIKE '%등교버스%'
196+
OR a.title LIKE '%셔틀버스%'
197+
OR a.title LIKE '%하교버스%'
198+
)
199+
AND a.isNotice = true
200+
ORDER BY a.createdAt DESC
201+
""")
202+
List<BusArticleProjection> findBusArticlesTop5OrderByCreatedAtDesc(Pageable pageable);
191203
}

src/main/java/in/koreatech/koin/domain/community/article/service/ArticleSyncService.java

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
package in.koreatech.koin.domain.community.article.service;
22

3+
import java.time.Clock;
4+
import java.time.LocalDate;
5+
import java.time.LocalDateTime;
6+
import java.util.HashMap;
7+
import java.util.List;
8+
import java.util.Map;
9+
import java.util.Optional;
10+
import java.util.Set;
11+
12+
import org.springframework.data.domain.PageRequest;
13+
import org.springframework.data.redis.core.RedisTemplate;
14+
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
15+
import org.springframework.stereotype.Service;
16+
import org.springframework.transaction.annotation.Transactional;
17+
18+
import in.koreatech.koin.domain.community.article.dto.BusArticleProjection;
319
import in.koreatech.koin.domain.community.article.model.Article;
420
import in.koreatech.koin.domain.community.article.model.ArticleSearchKeyword;
521
import in.koreatech.koin.domain.community.article.model.ArticleSearchKeywordIpMap;
@@ -14,20 +30,6 @@
1430
import lombok.RequiredArgsConstructor;
1531
import lombok.extern.slf4j.Slf4j;
1632

17-
import org.springframework.data.redis.core.RedisTemplate;
18-
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
19-
import org.springframework.stereotype.Service;
20-
import org.springframework.transaction.annotation.Transactional;
21-
22-
import java.time.Clock;
23-
import java.time.LocalDate;
24-
import java.time.LocalDateTime;
25-
import java.util.HashMap;
26-
import java.util.List;
27-
import java.util.Map;
28-
import java.util.Optional;
29-
import java.util.Set;
30-
3133
@Slf4j
3234
@Service
3335
@RequiredArgsConstructor
@@ -75,10 +77,11 @@ public void updateHotArticles() {
7577

7678
@Transactional
7779
public void updateBusNoticeArticle() {
78-
List<Article> articles = articleRepository.findBusArticlesTop5OrderByCreatedAtDesc();
79-
LocalDate latestDate = articles.get(0).getCreatedAt().toLocalDate();
80-
List<Article> latestArticles = articles.stream()
81-
.filter(article -> article.getCreatedAt().toLocalDate().isEqual(latestDate))
80+
List<BusArticleProjection> articles = articleRepository.findBusArticlesTop5OrderByCreatedAtDesc(
81+
PageRequest.of(0, 5));
82+
LocalDate latestDate = articles.get(0).createdAt().toLocalDate();
83+
List<BusArticleProjection> latestArticles = articles.stream()
84+
.filter(article -> article.createdAt().toLocalDate().isEqual(latestDate))
8285
.toList();
8386

8487
if (latestArticles.size() >= 2) {
@@ -88,21 +91,23 @@ public void updateBusNoticeArticle() {
8891
int secondWeight = 0;
8992

9093
// 제목(title)에 "사과"가 들어가면 후순위, "긴급"이 포함되면 우선순위
91-
if (first.getTitle().contains("사과"))
94+
if (first.title().contains("사과"))
9295
firstWeight++;
93-
if (first.getTitle().contains("긴급"))
96+
if (first.title().contains("긴급"))
9497
firstWeight--;
9598

96-
if (second.getTitle().contains("사과"))
99+
if (second.title().contains("사과"))
97100
secondWeight++;
98-
if (second.getTitle().contains("긴급"))
101+
if (second.title().contains("긴급"))
99102
secondWeight--;
100103

101104
return Integer.compare(firstWeight, secondWeight);
102105
})
103106
.toList();
104107
}
105-
busArticleRepository.save(BusNoticeArticle.from(latestArticles.get(0)));
108+
109+
BusArticleProjection latestArticle = latestArticles.get(0);
110+
busArticleRepository.save(BusNoticeArticle.of(latestArticle.id(), latestArticle.title()));
106111
}
107112

108113
@Transactional
@@ -130,9 +135,10 @@ private void syncAllIpSearchCounts() {
130135
String ipAddress = ipKey.replace(IP_SEARCH_COUNT_PREFIX, "");
131136

132137
for (Map.Entry<Object, Object> entry : keywordSearchCounts.entrySet()) {
133-
String searchedKeyword = (String) entry.getKey();
138+
String searchedKeyword = (String)entry.getKey();
134139
int searchCount = Integer.parseInt(entry.getValue().toString());
135-
if (searchCount <= 0) continue;
140+
if (searchCount <= 0)
141+
continue;
136142

137143
articleSearchKeywordRepository.findByKeyword(searchedKeyword).ifPresent(keywordEntity -> {
138144
ipMapRepository.findByArticleSearchKeywordAndIpAddress(keywordEntity, ipAddress)

0 commit comments

Comments
 (0)