Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,26 +1,17 @@
package org.umc.valuedi.domain.asset.service.command;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.umc.valuedi.domain.asset.dto.res.AssetResDTO;
import org.umc.valuedi.domain.asset.entity.BankAccount;
import org.umc.valuedi.domain.asset.entity.BankTransaction;
import org.umc.valuedi.domain.asset.entity.Card;
import org.umc.valuedi.domain.asset.entity.CardApproval;
import org.umc.valuedi.domain.asset.repository.bank.bankAccount.BankAccountRepository;
import org.umc.valuedi.domain.asset.repository.bank.bankTransaction.BankTransactionRepository;
import org.umc.valuedi.domain.asset.repository.card.cardApproval.CardApprovalRepository;
import org.umc.valuedi.domain.asset.service.command.worker.AssetFetchWorker;
import org.umc.valuedi.domain.connection.entity.CodefConnection;
import org.umc.valuedi.domain.connection.repository.CodefConnectionRepository;
import org.umc.valuedi.domain.member.entity.Member;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
Expand All @@ -30,24 +21,20 @@
@RequiredArgsConstructor
public class AssetFetchService {

private final EntityManager entityManager;
private final CodefConnectionRepository codefConnectionRepository;
private final BankTransactionRepository bankTransactionRepository;
private final CardApprovalRepository cardApprovalRepository;
private final BankAccountRepository bankAccountRepository;
private final AssetFetchWorker assetFetchWorker;
private final AssetPersistService assetPersistService;

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

@Transactional(propagation = Propagation.REQUIRES_NEW)
/**
* 외부 API 호출 + DB 저장을 분리하여 커넥션 풀 점유 최소화
*/
public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) {
List<CodefConnection> connections = codefConnectionRepository.findByMemberIdWithMember(member.getId());
LocalDate today = LocalDate.now();

// 각 기관별로 비동기 API 호출 실행
List<CompletableFuture<AssetFetchWorker.FetchResult>> futures = connections.stream()
.map(connection -> assetFetchWorker.fetchAndConvertData(connection, member))
.map(connection -> assetFetchWorker.fetchAndConvertData(connection.getId(), member))
.toList();

// 모든 비동기 작업이 완료될 때까지 대기하고 결과 취합
Expand Down Expand Up @@ -80,17 +67,16 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) {

// 새로운 데이터 필터링 및 저장
int totalNewBankTransactions = 0;
if (!allFetchedBankTransactions.isEmpty()) {
List<BankTransaction> newBankTransactions = filterNewBankTransactions(allFetchedBankTransactions);
if (!newBankTransactions.isEmpty()) {
bankTransactionRepository.bulkInsert(newBankTransactions);
totalNewBankTransactions = newBankTransactions.size();
int totalNewCardApprovals = 0;

// 계좌 잔액 업데이트 (기존 엔티티 반영)
updateAccountBalances(newBankTransactions);
}
if (!allFetchedBankTransactions.isEmpty() || !allFetchedCardApprovals.isEmpty()) {
AssetPersistService.SaveResult saveResult = assetPersistService.saveNewAssetData(allFetchedBankTransactions, allFetchedCardApprovals);
totalNewBankTransactions = saveResult.newBankTransactionCount();
totalNewCardApprovals = saveResult.newCardApprovalCount();
}

// 수집된 모든 거래내역 중 계좌별 가장 최신 잔액을 추출하여 실시간 데이터 맵에 저장
// 실시간 잔액 추출
if (!allFetchedBankTransactions.isEmpty()) {
allFetchedBankTransactions.stream()
.collect(Collectors.groupingBy(tx -> tx.getBankAccount().getId(),
Collectors.maxBy(Comparator.comparing(BankTransaction::getTrDatetime))))
Expand All @@ -101,19 +87,6 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) {
}));
}

int totalNewCardApprovals = 0;
if (!allFetchedCardApprovals.isEmpty()) {
List<CardApproval> newCardApprovals = filterNewCardApprovals(allFetchedCardApprovals);
if (!newCardApprovals.isEmpty()) {
cardApprovalRepository.bulkInsert(newCardApprovals);
totalNewCardApprovals = newCardApprovals.size();
}
}

// JdbcTemplate 사용 후 영속성 컨텍스트 초기화
entityManager.flush();
entityManager.clear();

return AssetResDTO.AssetSyncResult.builder()
.newBankTransactionCount(totalNewBankTransactions)
.newCardApprovalCount(totalNewCardApprovals)
Expand All @@ -124,60 +97,4 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) {
.latestBalances(realTimeBalances) // 실시간 잔액 데이터 전달
.build();
}

private void updateAccountBalances(List<BankTransaction> transactions) {
Map<BankAccount, BankTransaction> latestTransactions = transactions.stream()
.collect(Collectors.groupingBy(BankTransaction::getBankAccount,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparing(BankTransaction::getTrDatetime)),
Optional::get
)));

List<BankAccount> updatedAccounts = new ArrayList<>();

latestTransactions.forEach((account, latestTransaction) -> {
if (latestTransaction.getAfterBalance() != null) {
account.updateBalance(latestTransaction.getAfterBalance());
updatedAccounts.add(account);
}
});

if (!updatedAccounts.isEmpty()) {
bankAccountRepository.saveAll(updatedAccounts);
}
}

private List<BankTransaction> filterNewBankTransactions(List<BankTransaction> allFetched) {
if (allFetched.isEmpty()) return List.of();

LocalDate minDate = allFetched.stream().map(BankTransaction::getTrDate).min(LocalDate::compareTo).orElse(LocalDate.now());
List<BankAccount> accounts = allFetched.stream().map(BankTransaction::getBankAccount).distinct().toList();

List<BankTransaction> existingTransactions = bankTransactionRepository.findByBankAccountInAndTrDatetimeAfter(accounts, minDate.atStartOfDay());

Set<BankTransactionKey> existingKeys = existingTransactions.stream()
.map(tx -> new BankTransactionKey(tx.getTrDatetime(), tx.getInAmount(), tx.getOutAmount(), Objects.toString(tx.getDesc3(), "")))
.collect(Collectors.toSet());

return allFetched.stream()
.filter(tx -> !existingKeys.contains(new BankTransactionKey(tx.getTrDatetime(), tx.getInAmount(), tx.getOutAmount(), Objects.toString(tx.getDesc3(), ""))))
.toList();
}

private List<CardApproval> filterNewCardApprovals(List<CardApproval> allFetched) {
if (allFetched.isEmpty()) return List.of();

List<Card> cards = allFetched.stream().map(CardApproval::getCard).distinct().toList();
List<String> approvalNos = allFetched.stream().map(CardApproval::getApprovalNo).distinct().toList();

List<CardApproval> existingApprovals = cardApprovalRepository.findByCardInAndApprovalNoIn(cards, approvalNos);

Set<CardApprovalKey> existingKeys = existingApprovals.stream()
.map(ca -> new CardApprovalKey(ca.getCard(), ca.getApprovalNo()))
.collect(Collectors.toSet());

return allFetched.stream()
.filter(ca -> !existingKeys.contains(new CardApprovalKey(ca.getCard(), ca.getApprovalNo())))
.toList();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package org.umc.valuedi.domain.asset.service.command;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.umc.valuedi.domain.asset.entity.BankAccount;
import org.umc.valuedi.domain.asset.entity.BankTransaction;
import org.umc.valuedi.domain.asset.entity.Card;
import org.umc.valuedi.domain.asset.entity.CardApproval;
import org.umc.valuedi.domain.asset.repository.bank.bankAccount.BankAccountRepository;
import org.umc.valuedi.domain.asset.repository.bank.bankTransaction.BankTransactionRepository;
import org.umc.valuedi.domain.asset.repository.card.cardApproval.CardApprovalRepository;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
* 자산 데이터 DB 저장 전용 서비스.
* AssetFetchService의 self-invocation 문제를 해결하기 위해 분리.
*/
@Service
@RequiredArgsConstructor
public class AssetPersistService {

private final EntityManager entityManager;
private final BankTransactionRepository bankTransactionRepository;
private final CardApprovalRepository cardApprovalRepository;
private final BankAccountRepository bankAccountRepository;

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

public record SaveResult(int newBankTransactionCount, int newCardApprovalCount) {}

/**
* 새로운 자산 데이터를 짧은 트랜잭션에서 필터링 및 저장
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public SaveResult saveNewAssetData(List<BankTransaction> allFetchedBankTransactions, List<CardApproval> allFetchedCardApprovals) {
int totalNewBankTransactions = 0;
if (!allFetchedBankTransactions.isEmpty()) {
List<BankTransaction> newBankTransactions = filterNewBankTransactions(allFetchedBankTransactions);
if (!newBankTransactions.isEmpty()) {
bankTransactionRepository.bulkInsert(newBankTransactions);
totalNewBankTransactions = newBankTransactions.size();
updateAccountBalances(newBankTransactions);
}
}

int totalNewCardApprovals = 0;
if (!allFetchedCardApprovals.isEmpty()) {
List<CardApproval> newCardApprovals = filterNewCardApprovals(allFetchedCardApprovals);
if (!newCardApprovals.isEmpty()) {
cardApprovalRepository.bulkInsert(newCardApprovals);
totalNewCardApprovals = newCardApprovals.size();
}
}

entityManager.flush();
entityManager.clear();

return new SaveResult(totalNewBankTransactions, totalNewCardApprovals);
}

private void updateAccountBalances(List<BankTransaction> transactions) {
Map<BankAccount, BankTransaction> latestTransactions = transactions.stream()
.collect(Collectors.groupingBy(BankTransaction::getBankAccount,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparing(BankTransaction::getTrDatetime)),
Optional::get
)));

List<BankAccount> updatedAccounts = new ArrayList<>();

latestTransactions.forEach((account, latestTransaction) -> {
if (latestTransaction.getAfterBalance() != null) {
account.updateBalance(latestTransaction.getAfterBalance());
updatedAccounts.add(account);
}
});

if (!updatedAccounts.isEmpty()) {
bankAccountRepository.saveAll(updatedAccounts);
}
}

private List<BankTransaction> filterNewBankTransactions(List<BankTransaction> allFetched) {
if (allFetched.isEmpty()) return List.of();

LocalDate minDate = allFetched.stream().map(BankTransaction::getTrDate).min(LocalDate::compareTo).orElse(LocalDate.now());
List<BankAccount> accounts = allFetched.stream().map(BankTransaction::getBankAccount).distinct().toList();

List<BankTransaction> existingTransactions = bankTransactionRepository.findByBankAccountInAndTrDatetimeAfter(accounts, minDate.atStartOfDay());

Set<BankTransactionKey> existingKeys = existingTransactions.stream()
.map(tx -> new BankTransactionKey(tx.getTrDatetime(), tx.getInAmount(), tx.getOutAmount(), Objects.toString(tx.getDesc3(), "")))
.collect(Collectors.toSet());

return allFetched.stream()
.filter(tx -> !existingKeys.contains(new BankTransactionKey(tx.getTrDatetime(), tx.getInAmount(), tx.getOutAmount(), Objects.toString(tx.getDesc3(), ""))))
.toList();
}

private List<CardApproval> filterNewCardApprovals(List<CardApproval> allFetched) {
if (allFetched.isEmpty()) return List.of();

List<Card> cards = allFetched.stream().map(CardApproval::getCard).distinct().toList();
List<String> approvalNos = allFetched.stream().map(CardApproval::getApprovalNo).distinct().toList();

List<CardApproval> existingApprovals = cardApprovalRepository.findByCardInAndApprovalNoIn(cards, approvalNos);

Set<CardApprovalKey> existingKeys = existingApprovals.stream()
.map(ca -> new CardApprovalKey(ca.getCard(), ca.getApprovalNo()))
.collect(Collectors.toSet());

return allFetched.stream()
.filter(ca -> !existingKeys.contains(new CardApprovalKey(ca.getCard(), ca.getApprovalNo())))
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.umc.valuedi.domain.asset.dto.res.AssetResDTO;
import org.umc.valuedi.domain.connection.service.command.SyncLogCommandService;
import org.umc.valuedi.domain.ledger.service.command.LedgerSyncService;
Expand All @@ -28,8 +27,7 @@ public class AssetSyncProcessor {
/**
* 실제 동기화 로직을 수행하는 비동기 메서드
*/
@Async("assetFetchExecutor")
@Transactional
@Async("assetSyncExecutor")
public void runSyncProcess(Long memberId, Long logId) {
log.info("자산 동기화 백그라운드 작업을 시작합니다. 회원 ID: {}", memberId);
try {
Expand All @@ -49,9 +47,9 @@ public void runSyncProcess(Long memberId, Long logId) {
}

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

} catch (Exception e) {
// 실패 로그 기록
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.umc.valuedi.domain.asset.entity.BankAccount;
import org.umc.valuedi.domain.asset.entity.BankTransaction;
import org.umc.valuedi.domain.asset.entity.Card;
Expand All @@ -26,7 +25,6 @@
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class AssetSyncService {

private static final int DEFAULT_SYNC_PERIOD_MONTHS = 3;
Expand Down Expand Up @@ -126,6 +124,7 @@ private void syncLedger(Member member) {
// 기존 syncTransactions 대신 rebuildLedger 호출
// 범위: 최근 3개월 (기존 정책 유지)
ledgerSyncService.rebuildLedger(member, LocalDate.now().minusMonths(DEFAULT_SYNC_PERIOD_MONTHS), LocalDate.now());
ledgerSyncService.updateMemberLastSyncedAt(member.getId());
}

/**
Expand Down
Loading