From 2508d8308c712013f988612d90999ac9a4f7a46b Mon Sep 17 00:00:00 2001 From: Dohyeon Date: Thu, 12 Feb 2026 05:27:53 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20AssetBalanceService=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/asset/dto/res/AssetResDTO.java | 26 ++++++++-- .../asset/service/AssetBalanceService.java | 19 ++++---- .../service/command/AssetFetchService.java | 48 +++++++++++-------- 3 files changed, 56 insertions(+), 37 deletions(-) diff --git a/src/main/java/org/umc/valuedi/domain/asset/dto/res/AssetResDTO.java b/src/main/java/org/umc/valuedi/domain/asset/dto/res/AssetResDTO.java index ace3564..a2ab1f8 100644 --- a/src/main/java/org/umc/valuedi/domain/asset/dto/res/AssetResDTO.java +++ b/src/main/java/org/umc/valuedi/domain/asset/dto/res/AssetResDTO.java @@ -1,13 +1,12 @@ package org.umc.valuedi.domain.asset.dto.res; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import java.time.LocalDate; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Schema(description = "자산 통합 응답 DTO") public class AssetResDTO { @@ -30,6 +29,7 @@ public static class AssetSummaryCountDTO { @Getter @AllArgsConstructor + @NoArgsConstructor // Map 초기화를 위해 기본 생성자 추가 @Builder @Schema(description = "AssetFetchService의 내부 처리 결과 DTO (API 응답으로 직접 사용되지 않음)") public static class AssetSyncResult { @@ -50,5 +50,21 @@ public static class AssetSyncResult { @Schema(description = "가계부 동기화에 사용될 조회 종료일") private LocalDate toDate; + + @Builder.Default + @Schema(description = "계좌 ID별 최신 잔액 맵") + private Map latestBalances = new HashMap<>(); + + public boolean hasLatestBalanceFor(Long accountId) { + return latestBalances != null && latestBalances.containsKey(accountId); + } + + public Long getLatestBalanceFor(Long accountId) { + return latestBalances.get(accountId); + } + + public void addLatestBalance(Long accountId, Long balance) { + this.latestBalances.put(accountId, balance); + } } -} +} \ No newline at end of file diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/AssetBalanceService.java b/src/main/java/org/umc/valuedi/domain/asset/service/AssetBalanceService.java index 7f11781..56044f3 100644 --- a/src/main/java/org/umc/valuedi/domain/asset/service/AssetBalanceService.java +++ b/src/main/java/org/umc/valuedi/domain/asset/service/AssetBalanceService.java @@ -28,33 +28,30 @@ public class AssetBalanceService { private final BankAccountRepository bankAccountRepository; private final BankTransactionRepository bankTransactionRepository; - /** - * 자산 동기화를 수행하고, 특정 계좌의 최신 잔액을 반환합니다. - * 동기화 실패 시 기존 잔액을 반환합니다. - */ @Transactional(propagation = Propagation.NOT_SUPPORTED) public Long syncAndGetLatestBalance(Long memberId, Long accountId) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); - // 자산 동기화 시도 try { AssetResDTO.AssetSyncResult result = assetFetchService.fetchAndSaveLatestData(member); - log.info("[AssetBalanceService] 자산 동기화 완료. MemberID: {}, NewTransactions: {}, SuccessOrgs: {}, FailedOrgs: {}", - memberId, result.getNewBankTransactionCount(), result.getSuccessOrganizations(), result.getFailureOrganizations()); + // 1. 동기화 결과 DTO에 방금 수집한 실시간 잔액이 있다면 DB 조회 없이 즉시 반환 (레이스 컨디션 방지) + if (result.hasLatestBalanceFor(accountId)) { + log.info("[AssetBalanceService] 실시간 동기화 데이터 사용. AccountID: {}, Balance: {}", accountId, result.getLatestBalanceFor(accountId)); + return result.getLatestBalanceFor(accountId); + } } catch (Exception e) { - log.warn("잔액 조회 중 자산 동기화 실패 (기존 잔액 사용): {}", e.getMessage()); + log.warn("[AssetBalanceService] 잔액 조회 중 자산 동기화 실패 (기존 DB 잔액 사용): {}", e.getMessage()); } - // 계좌 조회 (영속성 컨텍스트 초기화 가능성 고려하여 재조회 권장) + // 2. Fallback: 실시간 데이터가 없거나 동기화 실패 시 DB에서 최신 데이터 조회 BankAccount account = bankAccountRepository.findByIdAndMemberId(accountId, memberId) .orElseThrow(() -> new GoalException(GoalErrorCode.ACCOUNT_NOT_FOUND)); - // 최신 거래내역 기반 잔액 조회 return bankTransactionRepository.findTopByBankAccountOrderByTrDatetimeDesc(account) .map(BankTransaction::getAfterBalance) .orElse(account.getBalanceAmount()); } -} +} \ No newline at end of file diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetFetchService.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetFetchService.java index 8d7d5e0..6f6e74d 100644 --- a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetFetchService.java +++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetFetchService.java @@ -21,11 +21,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -59,15 +55,13 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) { .map(CompletableFuture::join) .toList(); - // 모든 거래내역을 한번에 조회하기 위한 준비 List allFetchedBankTransactions = new ArrayList<>(); List allFetchedCardApprovals = new ArrayList<>(); - + List successOrganizations = new ArrayList<>(); List failureOrganizations = new ArrayList<>(); LocalDate overallMinDate = today; - // 취합된 결과를 바탕으로 DB 저장 및 중복 제거 로직 실행 for (AssetFetchWorker.FetchResult result : fetchResults) { if (result.isSuccess()) { successOrganizations.add(result.connection().getOrganization()); @@ -82,6 +76,8 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) { } } + Map realTimeBalances = new HashMap<>(); + // 새로운 데이터 필터링 및 저장 int totalNewBankTransactions = 0; if (!allFetchedBankTransactions.isEmpty()) { @@ -90,9 +86,19 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) { bankTransactionRepository.bulkInsert(newBankTransactions); totalNewBankTransactions = newBankTransactions.size(); - // 계좌 잔액 업데이트 로직 추가 + // 계좌 잔액 업데이트 (기존 엔티티 반영) updateAccountBalances(newBankTransactions); } + + // 수집된 모든 거래내역 중 계좌별 가장 최신 잔액을 추출하여 실시간 데이터 맵에 저장 + allFetchedBankTransactions.stream() + .collect(Collectors.groupingBy(tx -> tx.getBankAccount().getId(), + Collectors.maxBy(Comparator.comparing(BankTransaction::getTrDatetime)))) + .forEach((accountId, optTx) -> optTx.ifPresent(tx -> { + if (tx.getAfterBalance() != null) { + realTimeBalances.put(accountId, tx.getAfterBalance()); + } + })); } int totalNewCardApprovals = 0; @@ -115,20 +121,20 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) { .failureOrganizations(failureOrganizations) .fromDate(overallMinDate) .toDate(today) + .latestBalances(realTimeBalances) // 실시간 잔액 데이터 전달 .build(); } private void updateAccountBalances(List transactions) { - // 계좌별로 가장 최신 거래내역을 찾아서 잔액 업데이트 Map latestTransactions = transactions.stream() - .collect(Collectors.groupingBy(BankTransaction::getBankAccount, + .collect(Collectors.groupingBy(BankTransaction::getBankAccount, Collectors.collectingAndThen( - Collectors.maxBy((t1, t2) -> t1.getTrDatetime().compareTo(t2.getTrDatetime())), - java.util.Optional::get + Collectors.maxBy(Comparator.comparing(BankTransaction::getTrDatetime)), + Optional::get ))); List updatedAccounts = new ArrayList<>(); - + latestTransactions.forEach((account, latestTransaction) -> { if (latestTransaction.getAfterBalance() != null) { account.updateBalance(latestTransaction.getAfterBalance()); @@ -145,8 +151,8 @@ private List filterNewBankTransactions(List al if (allFetched.isEmpty()) return List.of(); LocalDate minDate = allFetched.stream().map(BankTransaction::getTrDate).min(LocalDate::compareTo).orElse(LocalDate.now()); - List accounts = allFetched.stream().map(BankTransaction::getBankAccount).distinct().collect(Collectors.toList()); - + List accounts = allFetched.stream().map(BankTransaction::getBankAccount).distinct().toList(); + List existingTransactions = bankTransactionRepository.findByBankAccountInAndTrDatetimeAfter(accounts, minDate.atStartOfDay()); Set existingKeys = existingTransactions.stream() @@ -155,14 +161,14 @@ private List filterNewBankTransactions(List al return allFetched.stream() .filter(tx -> !existingKeys.contains(new BankTransactionKey(tx.getTrDatetime(), tx.getInAmount(), tx.getOutAmount(), Objects.toString(tx.getDesc3(), "")))) - .collect(Collectors.toList()); + .toList(); } private List filterNewCardApprovals(List allFetched) { if (allFetched.isEmpty()) return List.of(); - List cards = allFetched.stream().map(CardApproval::getCard).distinct().collect(Collectors.toList()); - List approvalNos = allFetched.stream().map(CardApproval::getApprovalNo).distinct().collect(Collectors.toList()); + List cards = allFetched.stream().map(CardApproval::getCard).distinct().toList(); + List approvalNos = allFetched.stream().map(CardApproval::getApprovalNo).distinct().toList(); List existingApprovals = cardApprovalRepository.findByCardInAndApprovalNoIn(cards, approvalNos); @@ -172,6 +178,6 @@ private List filterNewCardApprovals(List allFetched) return allFetched.stream() .filter(ca -> !existingKeys.contains(new CardApprovalKey(ca.getCard(), ca.getApprovalNo()))) - .collect(Collectors.toList()); + .toList(); } -} +} \ No newline at end of file From 5376631991a41b2de886a423d71b9526ceb20c93 Mon Sep 17 00:00:00 2001 From: Dohyeon Date: Thu, 12 Feb 2026 05:50:44 +0900 Subject: [PATCH 2/2] =?UTF-8?q?remove:=20=EB=AF=BC=EA=B0=90=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=A1=9C=EA=B9=85=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc/valuedi/domain/asset/service/AssetBalanceService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/AssetBalanceService.java b/src/main/java/org/umc/valuedi/domain/asset/service/AssetBalanceService.java index 56044f3..588954e 100644 --- a/src/main/java/org/umc/valuedi/domain/asset/service/AssetBalanceService.java +++ b/src/main/java/org/umc/valuedi/domain/asset/service/AssetBalanceService.java @@ -38,7 +38,7 @@ public Long syncAndGetLatestBalance(Long memberId, Long accountId) { // 1. 동기화 결과 DTO에 방금 수집한 실시간 잔액이 있다면 DB 조회 없이 즉시 반환 (레이스 컨디션 방지) if (result.hasLatestBalanceFor(accountId)) { - log.info("[AssetBalanceService] 실시간 동기화 데이터 사용. AccountID: {}, Balance: {}", accountId, result.getLatestBalanceFor(accountId)); + log.info("[AssetBalanceService] 실시간 동기화 데이터 사용. AccountID: {}", accountId); return result.getLatestBalanceFor(accountId); }