Skip to content

Commit 443759b

Browse files
authored
[REFACTOR/#72] 가계부 동기화 로직 성능 최적화 및 중복 처리 개선 (#91)
## 🔗 Related Issue <!-- 이슈 번호를 작성하여 종료시켜주세요 --> - Closes #72 ## 📝 Summary <!-- 작업한 기능을 설명해주세요 --> 가계부 동기화 로직의 성능 저하 및 중복 처리 오류를 개선했습니다. - IN 절 과다 파라미터 문제 해결 - LedgerEntry에 미등록된 CardApproval/BankTransaction만 조회하도록 JPQL 기반 조회 메서드(`findUnsynced...`)를 추가 - 애플리케이션 단에서 ID 목록을 모아 IN 절로 조회하던 방식을 제거하고, DB 레벨에서 필터링하도록 개선 - 카드/은행 중복 처리 로직 정확도 개선 - 중복 비교 대상 카드 내역 조회 시 `memberId` 조건을 추가하여, 해당 회원의 카드 내역만 비교하도록 수정 ## 🔄 Changes <!-- 구체적으로 어떤 파일/로직이 변경되었는지 체크해주세요 --> - [ ] API 변경 (추가/수정) - [ ] 데이터 및 도메인 변경 (DB, 비즈니스 로직) - [ ] 설정 또는 인프라 관련 변경 - [X] 리팩토링 ## 💬 Questions & Review Points <!-- 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요 --> ## 📸 API Test Results (Swagger) <!-- API 테스트 스크린샷 첨부 --> ## ✅ Checklist - [x] API 테스트 완료 - [ ] 테스트 결과 사진 첨부 - [x] 빌드 성공 확인 (./gradlew build)
1 parent 7651f51 commit 443759b

10 files changed

Lines changed: 58 additions & 74 deletions

File tree

src/main/java/org/umc/valuedi/domain/asset/repository/bank/bankTransaction/BankTransactionRepository.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,11 @@ public interface BankTransactionRepository extends JpaRepository<BankTransaction
1818
Optional<LocalDate> findLatestTransactionDateByAccount(@Param("account") BankAccount account);
1919

2020
List<BankTransaction> findByBankAccountInAndTrDatetimeAfter(List<BankAccount> accounts, LocalDateTime startTime);
21+
22+
@Query("SELECT bt FROM BankTransaction bt " +
23+
"LEFT JOIN LedgerEntry le ON le.bankTransaction = bt " +
24+
"WHERE bt.bankAccount.codefConnection.member.id = :memberId " +
25+
"AND bt.trDate BETWEEN :from AND :to " +
26+
"AND le.id IS NULL")
27+
List<BankTransaction> findUnsyncedBankTransactions(@Param("memberId") Long memberId, @Param("from") LocalDate from, @Param("to") LocalDate to);
2128
}

src/main/java/org/umc/valuedi/domain/asset/repository/card/cardApproval/CardApprovalRepository.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,23 @@
1515

1616
public interface CardApprovalRepository extends JpaRepository<CardApproval, Long>, CardApprovalRepositoryCustom {
1717
List<CardApproval> findByUsedDateBetween(LocalDate fromDate, LocalDate toDate);
18+
19+
@Query("SELECT ca FROM CardApproval ca " +
20+
"WHERE ca.card.codefConnection.member.id = :memberId " +
21+
"AND ca.usedDate BETWEEN :from AND :to")
22+
List<CardApproval> findMemberCardApprovalsBetween(@Param("memberId") Long memberId, @Param("from") LocalDate from, @Param("to") LocalDate to);
23+
1824
boolean existsByUsedAmountAndUsedDatetimeBetween(Long amount, LocalDateTime start, LocalDateTime end);
1925

2026
@Query("SELECT MAX(ca.usedDate) FROM CardApproval ca JOIN ca.card c WHERE c.codefConnection.member = :member")
2127
Optional<LocalDate> findLatestApprovalDateByMember(@Param("member") Member member);
2228

2329
List<CardApproval> findByCardInAndApprovalNoIn(List<Card> cards, List<String> approvalNos);
30+
31+
@Query("SELECT ca FROM CardApproval ca " +
32+
"LEFT JOIN LedgerEntry le ON le.cardApproval = ca " +
33+
"WHERE ca.card.codefConnection.member.id = :memberId " +
34+
"AND ca.usedDate BETWEEN :from AND :to " +
35+
"AND le.id IS NULL")
36+
List<CardApproval> findUnsyncedCardApprovals(@Param("memberId") Long memberId, @Param("from") LocalDate from, @Param("to") LocalDate to);
2437
}

src/main/java/org/umc/valuedi/domain/asset/service/command/AssetFetchService.java

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import org.umc.valuedi.domain.asset.service.command.worker.AssetFetchWorker;
1616
import org.umc.valuedi.domain.connection.entity.CodefConnection;
1717
import org.umc.valuedi.domain.connection.repository.CodefConnectionRepository;
18-
import org.umc.valuedi.domain.ledger.service.command.LedgerSyncService;
1918
import org.umc.valuedi.domain.member.entity.Member;
2019

2120
import java.time.LocalDate;
@@ -37,14 +36,13 @@ public class AssetFetchService {
3736
private final BankTransactionRepository bankTransactionRepository;
3837
private final CardApprovalRepository cardApprovalRepository;
3938
private final AssetFetchWorker assetFetchWorker;
40-
private final LedgerSyncService ledgerSyncService;
4139

4240
private record BankTransactionKey(LocalDateTime trDatetime, Long inAmount, Long outAmount, String desc3) {}
4341
private record CardApprovalKey(Card card, String approvalNo) {}
4442

4543
@Transactional
4644
public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) {
47-
List<CodefConnection> connections = codefConnectionRepository.findByMemberId(member.getId());
45+
List<CodefConnection> connections = codefConnectionRepository.findByMemberIdWithMember(member.getId());
4846
LocalDate today = LocalDate.now();
4947

5048
// 각 기관별로 비동기 API 호출 실행
@@ -103,11 +101,6 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) {
103101
entityManager.flush();
104102
entityManager.clear();
105103

106-
// 가계부 동기화
107-
if (totalNewBankTransactions > 0 || totalNewCardApprovals > 0) {
108-
ledgerSyncService.syncTransactions(member, overallMinDate, today);
109-
}
110-
111104
return AssetResDTO.AssetSyncResult.builder()
112105
.newBankTransactionCount(totalNewBankTransactions)
113106
.newCardApprovalCount(totalNewCardApprovals)

src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncProcessor.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import lombok.extern.slf4j.Slf4j;
55
import org.springframework.scheduling.annotation.Async;
66
import org.springframework.stereotype.Service;
7+
import org.springframework.transaction.annotation.Transactional;
78
import org.umc.valuedi.domain.asset.dto.res.AssetResDTO;
89
import org.umc.valuedi.domain.connection.service.command.SyncLogCommandService;
910
import org.umc.valuedi.domain.ledger.service.command.LedgerSyncService;
@@ -28,6 +29,7 @@ public class AssetSyncProcessor {
2829
* 실제 동기화 로직을 수행하는 비동기 메서드
2930
*/
3031
@Async("assetFetchExecutor")
32+
@Transactional
3133
public void runSyncProcess(Long memberId, Long logId) {
3234
log.info("자산 동기화 백그라운드 작업을 시작합니다. 회원 ID: {}", memberId);
3335
try {
@@ -42,12 +44,12 @@ public void runSyncProcess(Long memberId, Long logId) {
4244
LocalDate fromDate = syncResult.getFromDate();
4345
LocalDate toDate = syncResult.getToDate();
4446

45-
// 트랜잭션 2: 가계부 연동 및 최종 업데이트
46-
// Member 객체 대신 ID를 전달하여 영속성 컨텍스트 내에서 조회 및 업데이트하도록 변경
47+
// 트랜잭션 2: 가계부 연동
4748
ledgerSyncService.syncTransactions(member, fromDate, toDate);
4849
}
4950

50-
// 트랜잭션 3: 동기화 로그 업데이트
51+
// 트랜잭션 3: 동기화 로그 및 최종 시간 업데이트
52+
member.updateLastSyncedAt();
5153
syncLogCommandService.updateToSuccess(logId);
5254
log.info("자산 동기화 백그라운드 작업을 성공적으로 완료했습니다. 회원 ID: {}", member.getId());
5355

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
package org.umc.valuedi.domain.connection.repository;
22

33
import org.springframework.data.jpa.repository.JpaRepository;
4+
import org.springframework.data.jpa.repository.Query;
5+
import org.springframework.data.repository.query.Param;
46
import org.umc.valuedi.domain.connection.entity.CodefConnection;
57
import org.umc.valuedi.domain.connection.enums.BusinessType;
68

79
import java.util.List;
810
import java.util.Optional;
911

1012
public interface CodefConnectionRepository extends JpaRepository<CodefConnection, Long> {
11-
// 회원의 모든 연동
12-
List<CodefConnection> findByMemberId(Long memberId);
1313

14-
// 특정 기관 연동 여부
15-
boolean existsByMemberIdAndOrganization(Long memberId, String organization);
14+
@Query("SELECT c FROM CodefConnection c JOIN FETCH c.member WHERE c.id = :id")
15+
Optional<CodefConnection> findByIdWithMember(@Param("id") Long id);
1616

17-
// connectedId 조회용
18-
Optional<CodefConnection> findFirstByMemberId(Long memberId);
17+
@Query("SELECT c FROM CodefConnection c JOIN FETCH c.member WHERE c.member.id = :memberId")
18+
List<CodefConnection> findByMemberIdWithMember(@Param("memberId") Long memberId);
1919

20-
// 타입별 조회 (은행/카드사)
21-
List<CodefConnection> findByMemberIdAndBusinessType(Long memberId, BusinessType businessType);
20+
@Query("SELECT c FROM CodefConnection c JOIN FETCH c.member WHERE c.member.id = :memberId AND c.businessType = :businessType")
21+
List<CodefConnection> findByMemberIdAndBusinessTypeWithMember(@Param("memberId") Long memberId, @Param("businessType") BusinessType businessType);
2222

23+
List<CodefConnection> findByMemberId(Long memberId);
24+
25+
List<CodefConnection> findByMemberIdAndBusinessType(Long memberId, BusinessType businessType);
2326
}

src/main/java/org/umc/valuedi/domain/connection/service/command/ConnectionCommandService.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
import org.umc.valuedi.domain.connection.exception.code.ConnectionErrorCode;
1212
import org.umc.valuedi.domain.connection.repository.CodefConnectionRepository;
1313
import org.umc.valuedi.domain.goal.repository.GoalRepository;
14+
import org.umc.valuedi.domain.member.entity.Member;
15+
import org.umc.valuedi.domain.member.exception.MemberException;
16+
import org.umc.valuedi.domain.member.exception.code.MemberErrorCode;
17+
import org.umc.valuedi.domain.member.repository.MemberRepository;
1418
import org.umc.valuedi.global.external.codef.service.CodefAccountService;
1519

1620
@Slf4j
@@ -22,20 +26,23 @@ public class ConnectionCommandService {
2226
private final CodefAccountService codefAccountService;
2327
private final CodefConnectionRepository codefConnectionRepository;
2428
private final GoalRepository goalRepository;
29+
private final MemberRepository memberRepository;
2530

2631
/**
2732
* 금융사 계정 연동
2833
*/
2934
public void connect(Long memberId, ConnectionReqDTO.Connect request) {
30-
codefAccountService.connectAccount(memberId, request);
35+
Member member = memberRepository.findById(memberId)
36+
.orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));
37+
codefAccountService.connectAccount(member, request);
3138
log.info("금융사 연동 완료 - memberId: {}, organization: {}", memberId, request.getOrganization());
3239
}
3340

3441
/**
3542
* 금융사 연동 해제
3643
*/
3744
public void disconnect(Long memberId, Long connectionId) {
38-
CodefConnection connection = codefConnectionRepository.findById(connectionId)
45+
CodefConnection connection = codefConnectionRepository.findByIdWithMember(connectionId)
3946
.orElseThrow(() -> new ConnectionException(ConnectionErrorCode.CONNECTION_NOT_FOUND));
4047

4148
if (!connection.getMember().getId().equals(memberId)) {

src/main/java/org/umc/valuedi/domain/connection/service/query/ConnectionQueryService.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class ConnectionQueryService {
2626
*/
2727
public List<BankResDTO.BankConnection> getConnectedBanks(Long memberId) {
2828
List<CodefConnection> connections =
29-
connectionRepository.findByMemberIdAndBusinessType(
29+
connectionRepository.findByMemberIdAndBusinessTypeWithMember(
3030
memberId,
3131
BusinessType.BK
3232
);
@@ -41,7 +41,7 @@ public List<BankResDTO.BankConnection> getConnectedBanks(Long memberId) {
4141
*/
4242
public List<CardResDTO.CardIssuerConnection> getConnectedCardIssuers(Long memberId) {
4343
List<CodefConnection> connections =
44-
connectionRepository.findByMemberIdAndBusinessType(
44+
connectionRepository.findByMemberIdAndBusinessTypeWithMember(
4545
memberId,
4646
BusinessType.CD
4747
);
@@ -56,7 +56,7 @@ public List<CardResDTO.CardIssuerConnection> getConnectedCardIssuers(Long member
5656
*/
5757
public List<ConnectionResDTO.Connection> getAllConnections(Long memberId) {
5858
List<CodefConnection> connections =
59-
connectionRepository.findByMemberId(memberId);
59+
connectionRepository.findByMemberIdWithMember(memberId);
6060

6161
return connections.stream()
6262
.map(ConnectionConverter::toConnectionDTO)

src/main/java/org/umc/valuedi/domain/ledger/repository/LedgerEntryRepository.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,11 @@
99

1010
import java.time.LocalDateTime;
1111
import java.util.List;
12-
import java.util.Set;
1312

1413
public interface LedgerEntryRepository extends JpaRepository<LedgerEntry, Long>, LedgerEntryRepositoryCustom {
1514
boolean existsByBankTransactionId(Long bankTransactionId);
16-
boolean existsByCardApprovalId(Long cardApprovalId);
17-
18-
@Query("SELECT le.cardApproval.id FROM LedgerEntry le WHERE le.cardApproval.id IN :ids")
19-
Set<Long> findExistingCardApprovalIds(@Param("ids") List<Long> ids);
2015

21-
@Query("SELECT le.bankTransaction.id FROM LedgerEntry le WHERE le.bankTransaction.id IN :ids")
22-
Set<Long> findExistingBankTransactionIds(@Param("ids") List<Long> ids);
16+
boolean existsByCardApprovalId(Long cardApprovalId);
2317

2418
@Modifying
2519
@Query("DELETE FROM LedgerEntry le WHERE le.member = :member AND le.transactionAt >= :start AND le.transactionAt < :end")

src/main/java/org/umc/valuedi/domain/ledger/service/command/LedgerSyncService.java

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,6 @@ public void rebuildLedger(Member member, LocalDate from, LocalDate to) {
125125
ledgerEntryRepository.bulkInsert(entries);
126126
log.info("Ledger Rebuild Complete: Member {}, {} entries created.", member.getId(), entries.size());
127127
}
128-
129-
member.updateLastSyncedAt();
130128
}
131129

132130
private LedgerEntry createFromCard(Member member, CardApproval ca, Category defaultCategory, String key) {
@@ -179,22 +177,6 @@ private LedgerEntry createFromBank(Member member, BankTransaction bt, Category d
179177
.build();
180178
}
181179

182-
183-
@Transactional
184-
public void syncTransactionsAndUpdateMember(Long memberId, LocalDate from, LocalDate to) {
185-
Member member = memberRepository.findById(memberId)
186-
.orElseThrow(() -> new LedgerException(MemberErrorCode.MEMBER_NOT_FOUND));
187-
syncTransactions(member, from, to);
188-
member.updateLastSyncedAt();
189-
}
190-
191-
@Transactional
192-
public void updateMemberLastSyncedAt(Long memberId) {
193-
Member member = memberRepository.findById(memberId)
194-
.orElseThrow(() -> new LedgerException(MemberErrorCode.MEMBER_NOT_FOUND));
195-
member.updateLastSyncedAt();
196-
}
197-
198180
@Transactional
199181
public void syncTransactions(Long memberId, LedgerSyncRequest request) {
200182
Member member = memberRepository.findById(memberId).orElseThrow(() -> new LedgerException(MemberErrorCode.MEMBER_NOT_FOUND));
@@ -203,7 +185,7 @@ public void syncTransactions(Long memberId, LedgerSyncRequest request) {
203185
syncTransactions(member, from, to);
204186
}
205187

206-
// 트랜잭션 어노테이션 제거 (상위 메서드에서 관리)
188+
@Transactional
207189
public void syncTransactions(Member member, LocalDate from, LocalDate to) {
208190
if (to.isBefore(from)) {
209191
throw new LedgerException(LedgerErrorCode.INVALID_DATE_RANGE);
@@ -214,7 +196,8 @@ public void syncTransactions(Member member, LocalDate from, LocalDate to) {
214196
Category transferCategory = categoryRepository.findByCode("TRANSFER")
215197
.orElseThrow(() -> new LedgerException(LedgerErrorCode.CATEGORY_NOT_FOUND));
216198

217-
List<CardApproval> cards = cardApprovalRepository.findByUsedDateBetween(from.minusDays(1), to.plusDays(1));
199+
// 중복 체크를 위한 카드 내역 (은행 거래와 비교용)
200+
List<CardApproval> cards = cardApprovalRepository.findMemberCardApprovalsBetween(member.getId(), from.minusDays(1), to.plusDays(1));
218201

219202
List<LedgerEntry> allNewEntries = new ArrayList<>();
220203
syncCardApprovals(member, from, to, defaultCategory, allNewEntries);
@@ -274,18 +257,11 @@ public int rematchCategories(Long memberId, LocalDate from, LocalDate to) {
274257
}
275258

276259
private void syncCardApprovals(Member member, LocalDate from, LocalDate to, Category defaultCategory, List<LedgerEntry> allNewEntries) {
277-
List<CardApproval> cards = cardApprovalRepository.findByUsedDateBetween(from, to);
260+
// 가계부에 없는 카드 내역만 조회
261+
List<CardApproval> cards = cardApprovalRepository.findUnsyncedCardApprovals(member.getId(), from, to);
278262
if (cards.isEmpty()) return;
279263

280-
// ID 목록을 추출
281-
List<Long> cardApprovalIds = cards.stream().map(CardApproval::getId).collect(Collectors.toList());
282-
283-
// 이미 존재하는 LedgerEntry의 CardApproval ID를 한 번의 쿼리로 조회
284-
Set<Long> existingIds = ledgerEntryRepository.findExistingCardApprovalIds(cardApprovalIds);
285-
286264
for (CardApproval ca : cards) {
287-
// DB 쿼리 대신 메모리의 Set에서 확인
288-
if (existingIds.contains(ca.getId())) continue;
289265
if (ca.getUsedDatetime() == null) continue;
290266

291267
String merchantName = ca.getMerchantName();
@@ -308,18 +284,11 @@ private void syncCardApprovals(Member member, LocalDate from, LocalDate to, Cate
308284
}
309285

310286
private void syncBankTransactions(Member member, LocalDate from, LocalDate to, List<CardApproval> cards, Category defaultCategory, Category transferCategory, List<LedgerEntry> allNewEntries) {
311-
List<BankTransaction> banks = bankTransactionRepository.findByTrDateBetween(from, to);
287+
// 가계부에 없는 은행 내역만 조회
288+
List<BankTransaction> banks = bankTransactionRepository.findUnsyncedBankTransactions(member.getId(), from, to);
312289
if (banks.isEmpty()) return;
313290

314-
// ID 목록 추출
315-
List<Long> bankTransactionIds = banks.stream().map(BankTransaction::getId).collect(Collectors.toList());
316-
317-
// 한 번의 쿼리로 조회
318-
Set<Long> existingIds = ledgerEntryRepository.findExistingBankTransactionIds(bankTransactionIds);
319-
320291
for (BankTransaction bt : banks) {
321-
// 메모리에서 확인
322-
if (existingIds.contains(bt.getId())) continue;
323292
if (bt.getTrDatetime() == null) continue;
324293

325294
String combinedDesc = Stream.of(bt.getDesc2(), bt.getDesc3(), bt.getDesc4()).filter(Objects::nonNull).collect(Collectors.joining(" "));

src/main/java/org/umc/valuedi/global/external/codef/service/CodefAccountService.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,13 @@ public class CodefAccountService {
2828

2929
private final CodefApiClient codefApiClient;
3030
private final EncryptUtil encryptUtil;
31-
private final MemberRepository memberRepository;
3231
private final ApplicationEventPublisher eventPublisher;
3332

3433
/**
3534
* 금융사 계정 연동 메인 로직
3635
*/
3736
@Transactional
38-
public void connectAccount(Long memberId, ConnectionReqDTO.Connect request) {
39-
40-
Member member = memberRepository.findById(memberId)
41-
.orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));
37+
public void connectAccount(Member member, ConnectionReqDTO.Connect request) {
4238

4339
// 기존에 발급받은 Connected ID가 있는지 리스트에서 확인
4440
String existingConnectedId = member.getCodefConnectionList().stream()

0 commit comments

Comments
 (0)