Compare commits

..

2 Commits

Author SHA1 Message Date
ByungCheol 79fc8fea16 Merge branch 'dev'
Deploy / deploy (push) Failing after 13m49s
2026-07-02 01:47:29 +09:00
ByungCheol 190fbe835f perf(account): 내역 목록 태그 조회 N+1 → 배치 단일 쿼리로 개선
list() 에서 항목마다 findTagNamesByEntryId() 를 개별 호출하던 N+1 을
findTagsByEntryIds() 배치 쿼리 1회로 대체.
항목 수가 많을 때 타임아웃/과부하 방지.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 01:47:08 +09:00
3 changed files with 28 additions and 3 deletions
@@ -93,4 +93,7 @@ public interface AccountEntryMapper {
int deleteEntryTagsByEntryId(@Param("entryId") Long entryId); int deleteEntryTagsByEntryId(@Param("entryId") Long entryId);
List<String> findTagNamesByEntryId(@Param("entryId") Long entryId); List<String> findTagNamesByEntryId(@Param("entryId") Long entryId);
/** 복수 항목의 태그를 한 번에 조회 — {entryId, tagName} 맵 반환 */
List<Map<String, Object>> findTagsByEntryIds(@Param("entryIds") List<Long> entryIds);
} }
@@ -53,9 +53,19 @@ public class AccountService {
public List<AccountEntryResponse> list(Long memberId, Integer year, Integer month, public List<AccountEntryResponse> list(Long memberId, Integer year, Integer month,
String type, String category, Long walletId, String keyword, Long tagId) { String type, String category, Long walletId, String keyword, Long tagId) {
return mapper.findByMember(memberId, year, month, List<AccountEntry> entries = mapper.findByMember(memberId, year, month,
blankToNull(type), blankToNull(category), walletId, blankToNull(keyword), tagId).stream() blankToNull(type), blankToNull(category), walletId, blankToNull(keyword), tagId);
.map(this::toResponse) if (entries.isEmpty()) return List.of();
// 태그 N+1 방지: 한 번에 모든 entry 의 태그를 조회 후 Map 으로 그룹화
List<Long> ids = entries.stream().map(AccountEntry::getId).toList();
Map<Long, List<String>> tagsByEntry = new HashMap<>();
for (Map<String, Object> row : mapper.findTagsByEntryIds(ids)) {
Long eid = ((Number) row.get("entryId")).longValue();
String name = (String) row.get("tagName");
tagsByEntry.computeIfAbsent(eid, k -> new ArrayList<>()).add(name);
}
return entries.stream()
.map(e -> AccountEntryResponse.from(e, tagsByEntry.getOrDefault(e.getId(), List.of())))
.toList(); .toList();
} }
@@ -234,4 +234,16 @@
ORDER BY t.name ORDER BY t.name
</select> </select>
<!-- 복수 항목의 태그를 한 번에 조회 — {entry_id, tag_name} 행 반환 -->
<select id="findTagsByEntryIds" resultType="map">
SELECT aet.entry_id AS entryId, t.name AS tagName
FROM account_tag t
JOIN account_entry_tag aet ON aet.tag_id = t.id
WHERE aet.entry_id IN
<foreach item="id" collection="entryIds" open="(" separator="," close=")">
#{id}
</foreach>
ORDER BY aet.entry_id, t.name
</select>
</mapper> </mapper>