From c6f31d8fe2d4408cd4c17296315588828828880e Mon Sep 17 00:00:00 2001 From: Kwon-DoHee <152317074+seamooll@users.noreply.github.com> Date: Tue, 17 Feb 2026 23:13:09 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20finance-mbti=20Lock=20wait=20timeout?= =?UTF-8?q?=20=EB=8D=B0=EB=93=9C=EB=9D=BD=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - disconnect 로직에서 CODEF 외부 API 호출을 트랜잭션 밖으로 분리 - 트랜잭션 범위 축소 및 락 보유 시간 단축 --- .../service/command/AssetSyncService.java | 1 + .../command/ConnectionCommandService.java | 17 ++++----- .../ConnectionDeleteCommandService.java | 35 +++++++++++++++++++ .../service/command/LedgerSyncService.java | 2 -- .../codef/service/CodefAccountService.java | 7 ++-- 5 files changed, 45 insertions(+), 17 deletions(-) create mode 100644 src/main/java/org/umc/valuedi/domain/connection/service/command/ConnectionDeleteCommandService.java diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java index 55a6e61..3fe8d5f 100644 --- a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java +++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java @@ -126,6 +126,7 @@ private void syncLedger(Member member) { // 기존 syncTransactions 대신 rebuildLedger 호출 // 범위: 최근 3개월 (기존 정책 유지) ledgerSyncService.rebuildLedger(member, LocalDate.now().minusMonths(DEFAULT_SYNC_PERIOD_MONTHS), LocalDate.now()); + member.updateLastSyncedAt(); } /** diff --git a/src/main/java/org/umc/valuedi/domain/connection/service/command/ConnectionCommandService.java b/src/main/java/org/umc/valuedi/domain/connection/service/command/ConnectionCommandService.java index 1e824c3..4a9cd94 100644 --- a/src/main/java/org/umc/valuedi/domain/connection/service/command/ConnectionCommandService.java +++ b/src/main/java/org/umc/valuedi/domain/connection/service/command/ConnectionCommandService.java @@ -6,11 +6,9 @@ import org.springframework.transaction.annotation.Transactional; import org.umc.valuedi.domain.connection.dto.req.ConnectionReqDTO; import org.umc.valuedi.domain.connection.entity.CodefConnection; -import org.umc.valuedi.domain.connection.enums.BusinessType; import org.umc.valuedi.domain.connection.exception.ConnectionException; import org.umc.valuedi.domain.connection.exception.code.ConnectionErrorCode; import org.umc.valuedi.domain.connection.repository.CodefConnectionRepository; -import org.umc.valuedi.domain.goal.repository.GoalRepository; import org.umc.valuedi.domain.member.entity.Member; import org.umc.valuedi.domain.member.exception.MemberException; import org.umc.valuedi.domain.member.exception.code.MemberErrorCode; @@ -20,17 +18,17 @@ @Slf4j @Service @RequiredArgsConstructor -@Transactional public class ConnectionCommandService { private final CodefAccountService codefAccountService; private final CodefConnectionRepository codefConnectionRepository; - private final GoalRepository goalRepository; + private final ConnectionDeleteCommandService connectionDeleteCommandService; private final MemberRepository memberRepository; /** * 금융사 계정 연동 */ + @Transactional public void connect(Long memberId, ConnectionReqDTO.Connect request) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); @@ -42,6 +40,7 @@ public void connect(Long memberId, ConnectionReqDTO.Connect request) { * 금융사 연동 해제 */ public void disconnect(Long memberId, Long connectionId) { + // 조회 및 권한 검증 (트랜잭션 없음) CodefConnection connection = codefConnectionRepository.findByIdWithMember(connectionId) .orElseThrow(() -> new ConnectionException(ConnectionErrorCode.CONNECTION_NOT_FOUND)); @@ -49,18 +48,14 @@ public void disconnect(Long memberId, Long connectionId) { throw new ConnectionException(ConnectionErrorCode.CONNECTION_ACCESS_DENIED); } + // 외부 API 호출 (트랜잭션 밖 — 락 보유하지 않음) codefAccountService.deleteAccount( connection.getConnectedId(), connection.getOrganization(), connection.getBusinessType() ); - //하위 계좌/카드도 Soft Delete - codefConnectionRepository.delete(connection); - - if (connection.getBusinessType() == BusinessType.BK) { - // 은행 계좌와 연결된 목표 Soft Delete 처리 (서브쿼리 사용) - goalRepository.softDeleteGoalsByConnectionId(connection.getId()); - } + // DB 삭제 (짧은 트랜잭션) + connectionDeleteCommandService.deleteConnectionData(connectionId, connection.getBusinessType()); } } diff --git a/src/main/java/org/umc/valuedi/domain/connection/service/command/ConnectionDeleteCommandService.java b/src/main/java/org/umc/valuedi/domain/connection/service/command/ConnectionDeleteCommandService.java new file mode 100644 index 0000000..6bfff11 --- /dev/null +++ b/src/main/java/org/umc/valuedi/domain/connection/service/command/ConnectionDeleteCommandService.java @@ -0,0 +1,35 @@ +package org.umc.valuedi.domain.connection.service.command; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.umc.valuedi.domain.connection.entity.CodefConnection; +import org.umc.valuedi.domain.connection.enums.BusinessType; +import org.umc.valuedi.domain.connection.exception.ConnectionException; +import org.umc.valuedi.domain.connection.exception.code.ConnectionErrorCode; +import org.umc.valuedi.domain.connection.repository.CodefConnectionRepository; +import org.umc.valuedi.domain.goal.repository.GoalRepository; + +@Service +@RequiredArgsConstructor +public class ConnectionDeleteCommandService { + + private final CodefConnectionRepository codefConnectionRepository; + private final GoalRepository goalRepository; + + /** + * CodefConnection 및 관련 데이터를 짧은 트랜잭션에서 소프트 딜리트한다. + * detached entity 문제 방지를 위해 connectionId로 재조회 후 삭제한다. + */ + @Transactional + public void deleteConnectionData(Long connectionId, BusinessType businessType) { + CodefConnection connection = codefConnectionRepository.findById(connectionId) + .orElseThrow(() -> new ConnectionException(ConnectionErrorCode.CONNECTION_NOT_FOUND)); + + if (businessType == BusinessType.BK) { + goalRepository.softDeleteGoalsByConnectionId(connectionId); + } + // 하위 계좌/카드도 Soft Delete (Cascade) + codefConnectionRepository.delete(connection); + } +} diff --git a/src/main/java/org/umc/valuedi/domain/ledger/service/command/LedgerSyncService.java b/src/main/java/org/umc/valuedi/domain/ledger/service/command/LedgerSyncService.java index f92b48a..b8fd9e8 100644 --- a/src/main/java/org/umc/valuedi/domain/ledger/service/command/LedgerSyncService.java +++ b/src/main/java/org/umc/valuedi/domain/ledger/service/command/LedgerSyncService.java @@ -107,8 +107,6 @@ public void rebuildLedger(Member member, LocalDate from, LocalDate to) { ledgerEntryRepository.bulkInsert(entries); log.info("Ledger Rebuild Complete: Member {}, {} entries created.", member.getId(), entries.size()); } - - member.updateLastSyncedAt(); } private LedgerEntry createFromCard(Member member, CardApproval ca, Category defaultCategory, String key) { diff --git a/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAccountService.java b/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAccountService.java index e065cb3..6a2cd6a 100644 --- a/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAccountService.java +++ b/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAccountService.java @@ -7,10 +7,8 @@ import org.springframework.transaction.annotation.Transactional; import org.umc.valuedi.domain.connection.dto.event.ConnectionSuccessEvent; import org.umc.valuedi.domain.connection.enums.BusinessType; +import org.umc.valuedi.domain.connection.enums.ConnectionStatus; import org.umc.valuedi.domain.member.entity.Member; -import org.umc.valuedi.domain.member.exception.MemberException; -import org.umc.valuedi.domain.member.exception.code.MemberErrorCode; -import org.umc.valuedi.domain.member.repository.MemberRepository; import org.umc.valuedi.global.external.codef.client.CodefApiClient; import org.umc.valuedi.domain.connection.dto.req.ConnectionReqDTO; import org.umc.valuedi.global.external.codef.dto.CodefApiResponse; @@ -38,6 +36,7 @@ public void connectAccount(Member member, ConnectionReqDTO.Connect request) { // 기존에 발급받은 Connected ID가 있는지 리스트에서 확인 String existingConnectedId = member.getCodefConnectionList().stream() + .filter(c -> c.getStatus() != ConnectionStatus.DELETED) // 활성 연동만 .map(CodefConnection::getConnectedId) .filter(Objects::nonNull) .distinct() @@ -91,7 +90,7 @@ public void deleteAccount(String connectedId, String organization, BusinessType private String handleFirstCreation(Map requestBody) { CodefApiResponse> response = codefApiClient.createConnectedId(requestBody); if (!response.isSuccess()) { - log.error("CODEF 계정 생성 실패: {}", response.getResult().getMessage()); + log.error("CODEF 계정 생성 실패 - code: {}, message: {}", response.getResult().getCode(), response.getResult().getMessage()); throw new CodefException(CodefErrorCode.CODEF_API_CREATE_FAILED); } Map data = response.getData(); From 1c6efe57c9031df1cad8934b6c78e70d76a2838f Mon Sep 17 00:00:00 2001 From: Kwon-DoHee <152317074+seamooll@users.noreply.github.com> Date: Wed, 18 Feb 2026 02:51:09 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EB=B2=94=EC=9C=84=20=EC=9E=AC=EC=A1=B0=EC=A0=95?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=BB=A4=EB=84=A5=EC=85=98=20=ED=92=80=20?= =?UTF-8?q?=EA=B3=A0=EA=B0=88=20=EB=AC=B8=EC=A0=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/command/AssetFetchService.java | 109 ++------------- .../service/command/AssetPersistService.java | 124 ++++++++++++++++++ .../service/command/AssetSyncProcessor.java | 6 +- .../service/command/AssetSyncService.java | 4 +- .../command/ConnectionCommandService.java | 10 +- .../service/RecommendationService.java | 2 - .../codef/service/CodefAccountService.java | 42 +++--- src/main/resources/application.yml | 4 + 8 files changed, 176 insertions(+), 125 deletions(-) create mode 100644 src/main/java/org/umc/valuedi/domain/asset/service/command/AssetPersistService.java 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 6f6e74d..0e11f9c 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 @@ -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; @@ -30,17 +21,13 @@ @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 connections = codefConnectionRepository.findByMemberIdWithMember(member.getId()); LocalDate today = LocalDate.now(); @@ -80,17 +67,16 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) { // 새로운 데이터 필터링 및 저장 int totalNewBankTransactions = 0; - if (!allFetchedBankTransactions.isEmpty()) { - List 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)))) @@ -101,19 +87,6 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) { })); } - int totalNewCardApprovals = 0; - if (!allFetchedCardApprovals.isEmpty()) { - List 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) @@ -124,60 +97,4 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) { .latestBalances(realTimeBalances) // 실시간 잔액 데이터 전달 .build(); } - - private void updateAccountBalances(List transactions) { - Map latestTransactions = transactions.stream() - .collect(Collectors.groupingBy(BankTransaction::getBankAccount, - Collectors.collectingAndThen( - Collectors.maxBy(Comparator.comparing(BankTransaction::getTrDatetime)), - Optional::get - ))); - - List 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 filterNewBankTransactions(List allFetched) { - 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().toList(); - - List existingTransactions = bankTransactionRepository.findByBankAccountInAndTrDatetimeAfter(accounts, minDate.atStartOfDay()); - - Set 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 filterNewCardApprovals(List allFetched) { - if (allFetched.isEmpty()) return List.of(); - - List cards = allFetched.stream().map(CardApproval::getCard).distinct().toList(); - List approvalNos = allFetched.stream().map(CardApproval::getApprovalNo).distinct().toList(); - - List existingApprovals = cardApprovalRepository.findByCardInAndApprovalNoIn(cards, approvalNos); - - Set 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(); - } -} \ No newline at end of file +} diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetPersistService.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetPersistService.java new file mode 100644 index 0000000..ddfaff5 --- /dev/null +++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetPersistService.java @@ -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 allFetchedBankTransactions, List allFetchedCardApprovals) { + int totalNewBankTransactions = 0; + if (!allFetchedBankTransactions.isEmpty()) { + List newBankTransactions = filterNewBankTransactions(allFetchedBankTransactions); + if (!newBankTransactions.isEmpty()) { + bankTransactionRepository.bulkInsert(newBankTransactions); + totalNewBankTransactions = newBankTransactions.size(); + updateAccountBalances(newBankTransactions); + } + } + + int totalNewCardApprovals = 0; + if (!allFetchedCardApprovals.isEmpty()) { + List 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 transactions) { + Map latestTransactions = transactions.stream() + .collect(Collectors.groupingBy(BankTransaction::getBankAccount, + Collectors.collectingAndThen( + Collectors.maxBy(Comparator.comparing(BankTransaction::getTrDatetime)), + Optional::get + ))); + + List 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 filterNewBankTransactions(List allFetched) { + 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().toList(); + + List existingTransactions = bankTransactionRepository.findByBankAccountInAndTrDatetimeAfter(accounts, minDate.atStartOfDay()); + + Set 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 filterNewCardApprovals(List allFetched) { + if (allFetched.isEmpty()) return List.of(); + + List cards = allFetched.stream().map(CardApproval::getCard).distinct().toList(); + List approvalNos = allFetched.stream().map(CardApproval::getApprovalNo).distinct().toList(); + + List existingApprovals = cardApprovalRepository.findByCardInAndApprovalNoIn(cards, approvalNos); + + Set 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(); + } +} diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncProcessor.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncProcessor.java index b9e04e1..c3bf940 100644 --- a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncProcessor.java +++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncProcessor.java @@ -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; @@ -29,7 +28,6 @@ public class AssetSyncProcessor { * 실제 동기화 로직을 수행하는 비동기 메서드 */ @Async("assetFetchExecutor") - @Transactional public void runSyncProcess(Long memberId, Long logId) { log.info("자산 동기화 백그라운드 작업을 시작합니다. 회원 ID: {}", memberId); try { @@ -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) { // 실패 로그 기록 diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java index 3fe8d5f..7f43905 100644 --- a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java +++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncService.java @@ -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; @@ -26,7 +25,6 @@ @Slf4j @Service @RequiredArgsConstructor -@Transactional public class AssetSyncService { private static final int DEFAULT_SYNC_PERIOD_MONTHS = 3; @@ -126,7 +124,7 @@ private void syncLedger(Member member) { // 기존 syncTransactions 대신 rebuildLedger 호출 // 범위: 최근 3개월 (기존 정책 유지) ledgerSyncService.rebuildLedger(member, LocalDate.now().minusMonths(DEFAULT_SYNC_PERIOD_MONTHS), LocalDate.now()); - member.updateLastSyncedAt(); + ledgerSyncService.updateMemberLastSyncedAt(member.getId()); } /** diff --git a/src/main/java/org/umc/valuedi/domain/connection/service/command/ConnectionCommandService.java b/src/main/java/org/umc/valuedi/domain/connection/service/command/ConnectionCommandService.java index 4a9cd94..7992618 100644 --- a/src/main/java/org/umc/valuedi/domain/connection/service/command/ConnectionCommandService.java +++ b/src/main/java/org/umc/valuedi/domain/connection/service/command/ConnectionCommandService.java @@ -3,9 +3,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.umc.valuedi.domain.connection.dto.req.ConnectionReqDTO; import org.umc.valuedi.domain.connection.entity.CodefConnection; +import org.umc.valuedi.domain.connection.enums.BusinessType; import org.umc.valuedi.domain.connection.exception.ConnectionException; import org.umc.valuedi.domain.connection.exception.code.ConnectionErrorCode; import org.umc.valuedi.domain.connection.repository.CodefConnectionRepository; @@ -28,11 +28,11 @@ public class ConnectionCommandService { /** * 금융사 계정 연동 */ - @Transactional public void connect(Long memberId, ConnectionReqDTO.Connect request) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); - codefAccountService.connectAccount(member, request); + String existingConnectedId = codefAccountService.findExistingConnectedId(memberId); + String targetConnectedId = codefAccountService.callCodefConnectApi(existingConnectedId, request); + + codefAccountService.saveConnection(memberId, targetConnectedId, request.getOrganization(), request.getBusinessTypeEnum()); log.info("금융사 연동 완료 - memberId: {}, organization: {}", memberId, request.getOrganization()); } diff --git a/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java b/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java index b0eeb84..f529580 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java +++ b/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java @@ -18,7 +18,6 @@ import org.umc.valuedi.domain.savings.entity.Recommendation; import org.umc.valuedi.domain.savings.entity.Savings; import org.umc.valuedi.domain.savings.entity.SavingsOption; -import org.umc.valuedi.domain.savings.enums.RecommendationStatus; import org.umc.valuedi.domain.savings.exception.SavingsException; import org.umc.valuedi.domain.savings.exception.code.SavingsErrorCode; import org.umc.valuedi.domain.savings.repository.RecommendationRepository; @@ -53,7 +52,6 @@ public class RecommendationService { private final RecommendationTxService recommendationTxService; - @Transactional public SavingsResponseDTO.SavingsListResponse generateAndSaveRecommendations(Long memberId) { // 금융 mbti 최신 결과 조회 MemberMbtiTest memberMbtiTest = memberMbtiTestRepository.findCurrentActiveTest(memberId) diff --git a/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAccountService.java b/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAccountService.java index 6a2cd6a..73df890 100644 --- a/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAccountService.java +++ b/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAccountService.java @@ -8,7 +8,11 @@ import org.umc.valuedi.domain.connection.dto.event.ConnectionSuccessEvent; import org.umc.valuedi.domain.connection.enums.BusinessType; import org.umc.valuedi.domain.connection.enums.ConnectionStatus; +import org.umc.valuedi.domain.connection.repository.CodefConnectionRepository; import org.umc.valuedi.domain.member.entity.Member; +import org.umc.valuedi.domain.member.exception.MemberException; +import org.umc.valuedi.domain.member.exception.code.MemberErrorCode; +import org.umc.valuedi.domain.member.repository.MemberRepository; import org.umc.valuedi.global.external.codef.client.CodefApiClient; import org.umc.valuedi.domain.connection.dto.req.ConnectionReqDTO; import org.umc.valuedi.global.external.codef.dto.CodefApiResponse; @@ -27,36 +31,44 @@ public class CodefAccountService { private final CodefApiClient codefApiClient; private final EncryptUtil encryptUtil; private final ApplicationEventPublisher eventPublisher; + private final CodefConnectionRepository codefConnectionRepository; + private final MemberRepository memberRepository; /** - * 금융사 계정 연동 메인 로직 + * 기존 connectedId 조회 */ - @Transactional - public void connectAccount(Member member, ConnectionReqDTO.Connect request) { - - // 기존에 발급받은 Connected ID가 있는지 리스트에서 확인 - String existingConnectedId = member.getCodefConnectionList().stream() - .filter(c -> c.getStatus() != ConnectionStatus.DELETED) // 활성 연동만 + public String findExistingConnectedId(Long memberId) { + return codefConnectionRepository.findByMemberId(memberId).stream() + .filter(c -> c.getStatus() != ConnectionStatus.DELETED) .map(CodefConnection::getConnectedId) .filter(Objects::nonNull) .distinct() .findFirst() .orElse(null); + } - // 비밀번호 암호화 + /** + * CODEF 외부 API 호출 (트랜잭션 밖에서 실행) + */ + public String callCodefConnectApi(String existingConnectedId, ConnectionReqDTO.Connect request) { String encryptedPassword = encryptUtil.encryptRSA(request.getLoginPassword()); Map requestBody = createRequestBody(request, encryptedPassword); - String targetConnectedId; - if (existingConnectedId == null) { - // 최초 등록 (Create) - targetConnectedId = handleFirstCreation(requestBody); + return handleFirstCreation(requestBody); } else { - // 계정 추가 (Add) - targetConnectedId = handleAddition(existingConnectedId, requestBody); + return handleAddition(existingConnectedId, requestBody); } - saveConnectionRecord(member, targetConnectedId, request.getOrganization(), request.getBusinessTypeEnum()); + } + + /** + * 연동 결과를 DB에 저장 + */ + @Transactional + public void saveConnection(Long memberId, String connectedId, String organization, BusinessType businessType) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + saveConnectionRecord(member, connectedId, organization, businessType); } /** diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c92d710..047435f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -35,6 +35,10 @@ spring: connectiontimeout: 5000 timeout: 5000 writetimeout: 5000 + hikari: + maximum-pool-size: 30 + leak-detection-threshold: 5000 # 커넥션 점유 메서드 파악 + codef: client-id: ${CODEF_CLIENT_ID} client-secret: ${CODEF_CLIENT_SECRET} From 6facecf6eff1f5282b3210ad8915451a653d611e Mon Sep 17 00:00:00 2001 From: Kwon-DoHee <152317074+seamooll@users.noreply.github.com> Date: Wed, 18 Feb 2026 06:03:31 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=EC=9E=90=EC=82=B0=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EC=8A=A4=EB=A0=88=EB=93=9C=ED=92=80=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20API=20=ED=83=80=EC=9E=84?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/command/AssetFetchService.java | 2 +- .../service/command/AssetSyncProcessor.java | 2 +- .../command/worker/AssetFetchWorker.java | 45 +++++++++++++------ .../repository/CodefConnectionRepository.java | 3 ++ .../event/ConnectionEventListener.java | 2 +- .../valuedi/global/config/AsyncConfig.java | 17 +++++-- .../codef/config/CodefFeignConfig.java | 13 +++++- 7 files changed, 64 insertions(+), 20 deletions(-) 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 0e11f9c..7953f1a 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 @@ -34,7 +34,7 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) { // 각 기관별로 비동기 API 호출 실행 List> futures = connections.stream() - .map(connection -> assetFetchWorker.fetchAndConvertData(connection, member)) + .map(connection -> assetFetchWorker.fetchAndConvertData(connection.getId(), member)) .toList(); // 모든 비동기 작업이 완료될 때까지 대기하고 결과 취합 diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncProcessor.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncProcessor.java index c3bf940..b3d87df 100644 --- a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncProcessor.java +++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetSyncProcessor.java @@ -27,7 +27,7 @@ public class AssetSyncProcessor { /** * 실제 동기화 로직을 수행하는 비동기 메서드 */ - @Async("assetFetchExecutor") + @Async("assetSyncExecutor") public void runSyncProcess(Long memberId, Long logId) { log.info("자산 동기화 백그라운드 작업을 시작합니다. 회원 ID: {}", memberId); try { diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/command/worker/AssetFetchWorker.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/worker/AssetFetchWorker.java index c048afb..e09c5b9 100644 --- a/src/main/java/org/umc/valuedi/domain/asset/service/command/worker/AssetFetchWorker.java +++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/worker/AssetFetchWorker.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; +import org.springframework.transaction.support.TransactionTemplate; import org.umc.valuedi.domain.asset.entity.BankAccount; import org.umc.valuedi.domain.asset.entity.BankTransaction; import org.umc.valuedi.domain.asset.entity.Card; @@ -13,6 +14,9 @@ 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.connection.exception.ConnectionException; +import org.umc.valuedi.domain.connection.exception.code.ConnectionErrorCode; +import org.umc.valuedi.domain.connection.repository.CodefConnectionRepository; import org.umc.valuedi.domain.member.entity.Member; import org.umc.valuedi.global.external.codef.service.CodefAssetService; @@ -33,6 +37,8 @@ public class AssetFetchWorker { private final BankTransactionRepository bankTransactionRepository; private final CardApprovalRepository cardApprovalRepository; private final CardRepository cardRepository; + private final CodefConnectionRepository codefConnectionRepository; + private final TransactionTemplate transactionTemplate; public record FetchResult( CodefConnection connection, @@ -43,11 +49,15 @@ public record FetchResult( ) {} @Async("assetFetchExecutor") - public CompletableFuture fetchAndConvertData(CodefConnection connection, Member member) { + public CompletableFuture fetchAndConvertData(Long connectionId, Member member) { LocalDate today = LocalDate.now(); LocalDate defaultStartDate = today.minusMonths(DEFAULT_SYNC_PERIOD_MONTHS); // 기본 시작일을 3개월 전으로 설정 LocalDate overallStartDate = today; // 전체 기관의 시작일 기록용 + // 비동기 스레드에서 새로운 트랜잭션으로 엔티티 조회 + CodefConnection connection = codefConnectionRepository.findByIdWithAccountsAndMember(connectionId) + .orElseThrow(() -> new ConnectionException(ConnectionErrorCode.CONNECTION_NOT_FOUND)); + try { if (connection.getBusinessType() == BusinessType.BK) { List accounts = connection.getBankAccountList(); @@ -79,18 +89,27 @@ public CompletableFuture fetchAndConvertData(CodefConnection connec List fetchedCards = codefAssetService.getCards(connection); List savedCards = Collections.emptyList(); if (!fetchedCards.isEmpty()) { - List existingCards = cardRepository.findAllByCodefConnection(connection); - - List cardsToSave = fetchedCards.stream().map(newCard -> { - return existingCards.stream() - .filter(oldCard -> oldCard.getCardNoMasked().equals(newCard.getCardNoMasked())) - .findFirst() - .map(oldCard -> oldCard) // 정보 갱신 필요 시 여기서 수행 - .orElse(newCard); - }).toList(); - - // saveAll의 결과(영속화된 객체들) 저장 - savedCards = cardRepository.saveAll(cardsToSave); + // 2. 카드 저장 로직만 트랜잭션으로 감싸서 처리 + savedCards = transactionTemplate.execute(status -> { + // 트랜잭션 내부에서 Connection을 다시 조회하여 영속 상태로 만듦 + CodefConnection managedConnection = codefConnectionRepository.findById(connectionId) + .orElseThrow(() -> new IllegalStateException("Connection not found during save")); + + List existingCards = cardRepository.findAllByCodefConnection(managedConnection); + + List cardsToSave = fetchedCards.stream().map(newCard -> { + // 새 카드 객체에 영속 상태의 Connection 할당 (Detached 엔티티 참조 문제 방지) + newCard.assignConnection(managedConnection); + + return existingCards.stream() + .filter(oldCard -> oldCard.getCardNoMasked().equals(newCard.getCardNoMasked())) + .findFirst() + .map(oldCard -> oldCard) // 정보 갱신 필요 시 여기서 수행 + .orElse(newCard); + }).toList(); + + return cardRepository.saveAll(cardsToSave); + }); } LocalDate cardStartDate = cardApprovalRepository.findLatestApprovalDateByMember(member) diff --git a/src/main/java/org/umc/valuedi/domain/connection/repository/CodefConnectionRepository.java b/src/main/java/org/umc/valuedi/domain/connection/repository/CodefConnectionRepository.java index 114911c..583fed8 100644 --- a/src/main/java/org/umc/valuedi/domain/connection/repository/CodefConnectionRepository.java +++ b/src/main/java/org/umc/valuedi/domain/connection/repository/CodefConnectionRepository.java @@ -14,6 +14,9 @@ public interface CodefConnectionRepository extends JpaRepository findByIdWithMember(@Param("id") Long id); + @Query("SELECT c FROM CodefConnection c JOIN FETCH c.member LEFT JOIN FETCH c.bankAccountList WHERE c.id = :id") + Optional findByIdWithAccountsAndMember(@Param("id") Long id); + @Query("SELECT c FROM CodefConnection c JOIN FETCH c.member WHERE c.member.id = :memberId") List findByMemberIdWithMember(@Param("memberId") Long memberId); diff --git a/src/main/java/org/umc/valuedi/domain/connection/service/event/ConnectionEventListener.java b/src/main/java/org/umc/valuedi/domain/connection/service/event/ConnectionEventListener.java index 3a37d11..ac5ffa3 100644 --- a/src/main/java/org/umc/valuedi/domain/connection/service/event/ConnectionEventListener.java +++ b/src/main/java/org/umc/valuedi/domain/connection/service/event/ConnectionEventListener.java @@ -16,7 +16,7 @@ public class ConnectionEventListener { private final AssetSyncService assetSyncService; - @Async("assetFetchExecutor") + @Async("assetSyncExecutor") @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleConnectionSuccess(ConnectionSuccessEvent event) { log.info("금융사 연동 성공 이벤트 수신 - Connection ID: {}, Organization: {}", diff --git a/src/main/java/org/umc/valuedi/global/config/AsyncConfig.java b/src/main/java/org/umc/valuedi/global/config/AsyncConfig.java index 0fb4480..aabcb99 100644 --- a/src/main/java/org/umc/valuedi/global/config/AsyncConfig.java +++ b/src/main/java/org/umc/valuedi/global/config/AsyncConfig.java @@ -23,12 +23,23 @@ public Executor mailExecutor() { return executor; } + @Bean(name = "assetSyncExecutor") + public Executor assetSyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("AssetSync-"); + executor.initialize(); + return executor; + } + @Bean(name = "assetFetchExecutor") public Executor assetFetchExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(5); // 기본 스레드 수 - 동시에 처리할 작업 수 (CPU 코어 수에 맞게 조절) - executor.setMaxPoolSize(10); // 최대 스레드 수 - executor.setQueueCapacity(100); // 큐 용량 + executor.setCorePoolSize(3); + executor.setMaxPoolSize(3); + executor.setQueueCapacity(50); executor.setThreadNamePrefix("AssetFetch-"); executor.initialize(); return executor; diff --git a/src/main/java/org/umc/valuedi/global/external/codef/config/CodefFeignConfig.java b/src/main/java/org/umc/valuedi/global/external/codef/config/CodefFeignConfig.java index 4469dfe..4f147a4 100644 --- a/src/main/java/org/umc/valuedi/global/external/codef/config/CodefFeignConfig.java +++ b/src/main/java/org/umc/valuedi/global/external/codef/config/CodefFeignConfig.java @@ -1,12 +1,14 @@ package org.umc.valuedi.global.external.codef.config; import feign.Logger; +import feign.Request; import feign.RequestInterceptor; import feign.codec.Decoder; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.umc.valuedi.global.external.codef.service.CodefTokenService; +import java.util.concurrent.TimeUnit; + public class CodefFeignConfig { @Bean @@ -23,4 +25,13 @@ Logger.Level feignLoggerLevel() { public Decoder codefDecoder() { return new CodefResponseDecoder(); } + + @Bean + public Request.Options options() { + return new Request.Options( + 10, TimeUnit.SECONDS, + 5, TimeUnit.MINUTES, + true + ); + } } \ No newline at end of file From 42925aad12adbf28cacb2ab64aa36dce67d3ec44 Mon Sep 17 00:00:00 2001 From: Kwon-DoHee <152317074+seamooll@users.noreply.github.com> Date: Thu, 19 Feb 2026 02:26:34 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20Gemini=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/asset/service/command/worker/AssetFetchWorker.java | 1 - .../valuedi/global/external/codef/config/CodefFeignConfig.java | 2 +- .../global/external/codef/service/CodefAccountService.java | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/command/worker/AssetFetchWorker.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/worker/AssetFetchWorker.java index e09c5b9..24d2718 100644 --- a/src/main/java/org/umc/valuedi/domain/asset/service/command/worker/AssetFetchWorker.java +++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/worker/AssetFetchWorker.java @@ -104,7 +104,6 @@ public CompletableFuture fetchAndConvertData(Long connectionId, Mem return existingCards.stream() .filter(oldCard -> oldCard.getCardNoMasked().equals(newCard.getCardNoMasked())) .findFirst() - .map(oldCard -> oldCard) // 정보 갱신 필요 시 여기서 수행 .orElse(newCard); }).toList(); diff --git a/src/main/java/org/umc/valuedi/global/external/codef/config/CodefFeignConfig.java b/src/main/java/org/umc/valuedi/global/external/codef/config/CodefFeignConfig.java index 4f147a4..06c4997 100644 --- a/src/main/java/org/umc/valuedi/global/external/codef/config/CodefFeignConfig.java +++ b/src/main/java/org/umc/valuedi/global/external/codef/config/CodefFeignConfig.java @@ -30,7 +30,7 @@ public Decoder codefDecoder() { public Request.Options options() { return new Request.Options( 10, TimeUnit.SECONDS, - 5, TimeUnit.MINUTES, + 3, TimeUnit.MINUTES, true ); } diff --git a/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAccountService.java b/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAccountService.java index 73df890..f499ac7 100644 --- a/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAccountService.java +++ b/src/main/java/org/umc/valuedi/global/external/codef/service/CodefAccountService.java @@ -39,7 +39,6 @@ public class CodefAccountService { */ public String findExistingConnectedId(Long memberId) { return codefConnectionRepository.findByMemberId(memberId).stream() - .filter(c -> c.getStatus() != ConnectionStatus.DELETED) .map(CodefConnection::getConnectedId) .filter(Objects::nonNull) .distinct()