Skip to content
Closed
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
Expand Up @@ -2,6 +2,10 @@

import org.springframework.data.jpa.repository.JpaRepository;
import org.umc.valuedi.domain.asset.entity.BankAccount;
import org.umc.valuedi.domain.connection.entity.CodefConnection;

import java.util.List;

public interface BankAccountRepository extends JpaRepository<BankAccount, Long>, BankAccountRepositoryCustom {
List<BankAccount> findByCodefConnection(CodefConnection codefConnection);
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package org.umc.valuedi.domain.asset.service.command;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
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 org.umc.valuedi.domain.asset.repository.card.card.CardRepository;
import org.umc.valuedi.domain.asset.repository.card.cardApproval.CardApprovalRepository;
import org.umc.valuedi.domain.connection.entity.CodefConnection;
import org.umc.valuedi.domain.connection.enums.BusinessType;
import org.umc.valuedi.domain.ledger.service.command.LedgerSyncService;
Expand All @@ -26,7 +26,6 @@
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class AssetSyncService {

private static final int DEFAULT_SYNC_PERIOD_MONTHS = 3;
Expand All @@ -37,145 +36,154 @@ public class AssetSyncService {
private final CardRepository cardRepository;
private final CardApprovalRepository cardApprovalRepository;
private final LedgerSyncService ledgerSyncService;
private final ObjectMapper objectMapper;

/**
* 금융사 종류에 따라 자산 동기화 분기
*/
public void syncAssets(CodefConnection connection) {
log.info("[AssetSync] 시작 - Connection ID: {}, Org: {}", connection.getId(), connection.getOrganization());

if (connection.getBusinessType() == BusinessType.BK) {
syncBankAssets(connection);
} else if (connection.getBusinessType() == BusinessType.CD) {
syncCardAssets(connection);
}

log.info("[AssetSync] 완료 - Connection ID: {}", connection.getId());
}

/**
* 은행 관련 자산 동기화 (계좌 목록, 거래 내역)
*/
private void syncBankAssets(CodefConnection connection) {
log.info("은행 자산 동기화 시작 - Connection ID: {}", connection.getId());

// 보유 계좌 목록 조회 및 저장
// API 호출 (트랜잭션 없음)
List<BankAccount> accounts = codefAssetService.getBankAccounts(connection);

try {
bankAccountRepository.saveAll(accounts);
} catch (DataIntegrityViolationException e) {
log.warn("계좌 저장 중 중복 발생 - 개별 저장 시도");
for (BankAccount account : accounts) {
try {
bankAccountRepository.save(account);
} catch (DataIntegrityViolationException ex) {
// 이미 존재하는 계좌는 무시
}
}
}
log.info("보유 계좌 목록 동기화 완료 - {}개 계좌", accounts.size());
if (accounts.isEmpty()) return;

// 각 계좌별 거래 내역 조회 및 저장
boolean anyUpdated = false;
for (BankAccount account : accounts) {
if (syncBankTransactions(connection, account)) {
anyUpdated = true;
// DB 저장 (별도 트랜잭션)
saveBankAccounts(accounts);

// 동기화된 모든 계좌 조회
List<BankAccount> allAccounts = bankAccountRepository.findByCodefConnection(connection);
int updatedAccountCount = 0;

for (BankAccount account : allAccounts) {
if (syncBankTransactionsForAccount(connection, account)) {
updatedAccountCount++;
}
}

if (anyUpdated) {
// 가계부 동기화 (필요시)
if (updatedAccountCount > 0) {
syncLedger(connection.getMember());
}

log.info("은행 자산 동기화 완료 - Connection ID: {}", connection.getId());
log.info("[AssetSync] 은행 동기화 요약 - 계좌: {}개, 거래내역 업데이트 계좌: {}", allAccounts.size(), updatedAccountCount);
}

/**
* 카드 관련 자산 동기화 (카드 목록, 승인 내역)
*/
private void syncCardAssets(CodefConnection connection) {
log.info("카드사 자산 동기화 시작 - Connection ID: {}", connection.getId());

// 보유 카드 목록 조회 및 저장
// API 호출 (트랜잭션 없음)
List<Card> cards = codefAssetService.getCards(connection);

try {
cardRepository.saveAll(cards);
} catch (DataIntegrityViolationException e) {
log.warn("카드 저장 중 중복 발생 - 개별 저장 시도");
for (Card card : cards) {
try {
cardRepository.save(card);
} catch (DataIntegrityViolationException ex) {
// 이미 존재하는 카드는 무시
}
}
}
log.info("보유 카드 목록 동기화 완료 - {}개 카드", cards.size());
if (cards.isEmpty()) return;

// DB 저장 (별도 트랜잭션)
saveCards(cards);

// API 호출 및 DB 저장: 카드 승인 내역 동기화
boolean approvalsSynced = syncCardApprovals(connection);

// 전체 승인 내역 조회 및 카드 매칭 후 저장
if (syncCardApprovals(connection)) {
// 가계부 동기화 (필요시)
if (approvalsSynced) {
syncLedger(connection.getMember());
}

log.info("카드사 자산 동기화 완료 - Connection ID: {}", connection.getId());
log.info("[AssetSync] 카드 동기화 요약 - 카드: {}개, 승인내역 업데이트: {}", cards.size(), approvalsSynced ? "성공" : "없음");
}

/**
* 가계부 동기화 헬퍼 메서드
* 특정 계좌의 거래 내역 동기화 (트랜잭션 없음)
*/
private void syncLedger(Member member) {
// 기존 syncTransactions 대신 rebuildLedger 호출
// 범위: 최근 3개월 (기존 정책 유지)
ledgerSyncService.rebuildLedger(member, LocalDate.now().minusMonths(DEFAULT_SYNC_PERIOD_MONTHS), LocalDate.now());
private boolean syncBankTransactionsForAccount(CodefConnection connection, BankAccount account) {
// API 호출
List<BankTransaction> transactions = codefAssetService.getBankTransactions(connection, account);
if (transactions.isEmpty()) return false;

// DB 저장 (별도 트랜잭션)
saveBankTransactions(transactions);
return true;
}

/**
* 특정 계좌의 거래 내역 동기화
* 카드 승인 내역 동기화 (트랜잭션 없음)
*/
private boolean syncBankTransactions(CodefConnection connection, BankAccount account) {
log.info("계좌 거래내역 동기화 시작 - Account: {}", account.getAccountDisplay());

List<BankTransaction> transactions = codefAssetService.getBankTransactions(connection, account);

if (transactions.isEmpty()) {
return false;
}
private boolean syncCardApprovals(CodefConnection connection) {
List<Card> cards = cardRepository.findByCodefConnection(connection);
if (cards.isEmpty()) return false;

bankTransactionRepository.bulkInsert(transactions);
log.info("계좌 거래내역 Bulk Insert 완료 - {}건", transactions.size());
// API 호출: 3개월치 승인 내역 조회 (트랜잭션 없음)
LocalDate endDate = LocalDate.now();
LocalDate startDate = endDate.minusMonths(DEFAULT_SYNC_PERIOD_MONTHS);

List<CardApproval> approvals = codefAssetService.getCardApprovals(connection, cards, startDate, endDate);
if (approvals.isEmpty()) return false;

// DB 저장 (별도 트랜잭션)
saveCardApprovals(approvals);
return true;
}

/**
* 카드 승인 내역 동기화 (전체 조회 후 매칭)
* 가계부 동기화 헬퍼 메서드
*/
private boolean syncCardApprovals(CodefConnection connection) {
log.info("카드 승인내역 동기화 시작 - Connection ID: {}", connection.getId());
private void syncLedger(Member member) {
// LedgerSyncService의 rebuildLedger는 이미 @Transactional 이므로 그대로 호출
ledgerSyncService.rebuildLedger(member, LocalDate.now().minusMonths(DEFAULT_SYNC_PERIOD_MONTHS), LocalDate.now());
}

// 해당 연동의 모든 카드 목록 조회 (DB)
List<Card> cards = cardRepository.findByCodefConnection(connection);
if (cards.isEmpty()) {
log.warn("연동된 카드가 없어 승인내역 동기화를 건너뜁니다.");
return false;
}
try {
// --- DB 저장을 위한 트랜잭션 메서드 ---

connection.getCardList().clear();
connection.getCardList().addAll(cards);
} catch (Exception e) {
log.warn("Connection 객체의 카드 리스트 갱신 중 오류 (무시하고 진행): {}", e.getMessage());
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveBankAccounts(List<BankAccount> accounts) {
if (accounts.isEmpty()) return;
try {
bankAccountRepository.saveAll(accounts);
} catch (DataIntegrityViolationException e) {
for (BankAccount account : accounts) {
try {
bankAccountRepository.save(account);
} catch (DataIntegrityViolationException ex) {
// 이미 존재하는 계좌는 무시
}
}
}
}

// 전체 승인 내역 조회 (API)
// CodefAssetService 내부에서 CodefAssetConverter를 통해 매칭까지 완료된 리스트 반환
List<CardApproval> approvals = codefAssetService.getCardApprovals(connection);
if (approvals.isEmpty()) {
log.info("조회된 승인내역이 없습니다.");
return false;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveBankTransactions(List<BankTransaction> transactions) {
bankTransactionRepository.bulkInsert(transactions);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveCards(List<Card> cards) {
if (cards.isEmpty()) return;
try {
cardRepository.saveAll(cards);
} catch (DataIntegrityViolationException e) {
for (Card card : cards) {
try {
cardRepository.save(card);
} catch (DataIntegrityViolationException ex) {
// 이미 존재하는 카드는 무시
}
}
}
}

// 저장
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveCardApprovals(List<CardApproval> approvals) {
cardApprovalRepository.bulkInsert(approvals);
log.info("카드 승인내역 Bulk Insert 완료 - {}건", approvals.size());
return true;
}
}
Comment on lines +148 to 189

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

새로운 트랜잭션을 시작하기 위해 save... 메서드들을 public으로 만들고 @Transactional(propagation = Propagation.REQUIRES_NEW)를 사용한 것은 좋은 접근입니다. 하지만 스프링의 트랜잭션 프록시를 적용하기 위해 메서드를 public으로만 만드는 것은 캡슐화를 해칠 수 있습니다. 이 메서드들이 의도치 않게 다른 서비스에서 호출될 수 있기 때문입니다. 장기적인 유지보수성을 위해 다음과 같은 대안을 고려해볼 수 있습니다.

  1. save 메서드들을 담을 새로운 컴포넌트(예: AssetPersistenceService)를 만듭니다. AssetSyncService는 이 새 컴포넌트를 주입받아 사용합니다. 이것이 가장 깔끔한 접근 방식입니다.
  2. AssetSyncService에 자기 자신을 주입하여 프록시 인스턴스를 통해 public 트랜잭션 메서드를 호출합니다. 이는 흔한 해결 방법이지만 코드가 혼란스러워질 수 있습니다.

현재 구현도 동작은 하지만, 더 나은 캡슐화를 위해 리팩토링을 권장합니다.

Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,23 @@ public class ConnectionCommandService {
* 금융사 계정 연동
*/
public void connect(Long memberId, ConnectionReqDTO.Connect request) {
log.info("[ConnectionCommandService] [connect] START - Member ID: {}, Org: {}", memberId, request.getOrganization());
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));
codefAccountService.connectAccount(member, request);
log.info("금융사 연동 완료 - memberId: {}, organization: {}", memberId, request.getOrganization());
log.info("[ConnectionCommandService] [connect] END - Member ID: {}", memberId);
}

/**
* 금융사 연동 해제
*/
public void disconnect(Long memberId, Long connectionId) {
log.info("[ConnectionCommandService] [disconnect] START - Member ID: {}, Connection ID: {}", memberId, connectionId);
CodefConnection connection = codefConnectionRepository.findByIdWithMember(connectionId)
.orElseThrow(() -> new ConnectionException(ConnectionErrorCode.CONNECTION_NOT_FOUND));

if (!connection.getMember().getId().equals(memberId)) {
log.error("[ConnectionCommandService] [disconnect] ERROR - Access Denied. Member ID {} tried to access Connection ID {}", memberId, connectionId);
throw new ConnectionException(ConnectionErrorCode.CONNECTION_ACCESS_DENIED);
}

Expand All @@ -60,7 +63,9 @@ public void disconnect(Long memberId, Long connectionId) {

if (connection.getBusinessType() == BusinessType.BK) {
// 은행 계좌와 연결된 목표 Soft Delete 처리 (서브쿼리 사용)
log.debug("[ConnectionCommandService] [disconnect] Deactivating goals for bank connection");
goalRepository.softDeleteGoalsByConnectionId(connection.getId());
}
log.info("[ConnectionCommandService] [disconnect] END - Member ID: {}", memberId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,10 @@ public class ConnectionEventListener {
@Async("assetFetchExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleConnectionSuccess(ConnectionSuccessEvent event) {
log.info("금융사 연동 성공 이벤트 수신 - Connection ID: {}, Organization: {}",
event.getConnection().getId(), event.getConnection().getOrganization());

try {
assetSyncService.syncAssets(event.getConnection());
} catch (Exception e) {
log.error("자산 동기화 중 오류 발생", e);
log.error("[ConnectionEventListener] [handleConnectionSuccess] ERROR - 자산 동기화 중 오류 발생. Connection ID: {}", event.getConnection().getId(), e);
}
}
}
2 changes: 2 additions & 0 deletions src/main/java/org/umc/valuedi/global/config/AsyncConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public Executor assetFetchExecutor() {
executor.setMaxPoolSize(10); // 최대 스레드 수
executor.setQueueCapacity(100); // 큐 용량
executor.setThreadNamePrefix("AssetFetch-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(120);
executor.initialize();
return executor;
}
Expand Down
Loading