Skip to content

Commit cb2234d

Browse files
authored
[Refactor] 송금 엔드포인트 성능 최적화 진행 (#125)
* feat: 캐시에 codef 토큰을 자동 저장하도록 로직 변경 * feat: 동기식 알림으로 변경 후 에러 추적 * feat: Hikari 커넥션 풀 사이즈 조정 * feat: 최적화를 위한 쿼리 변경 단건 insert를 batch insert로 변경하였습니다. * feat: 멤버 조회에 캐시 적용 * feat: 쿼리 최적화를 위한 배치 인서트로 변경 * feat: 쿼리 최적화를 위한 배치 인서트로 변경 * feat: 쿼리 최적화를 위한 배치 인서트로 변경 지갑의 잔액 업데이트를 배치로 변경 * feat: 회계 코드 리스트 조회로 변경 * feat: 회계 코드 리스트 조회로 변경 * feat: 회계 코드 리스트 조회에 캐시 적용 * feat: 최적화 변경 메서드 모두 반영 * feat: 최적화를 위해 휴대폰 번호에 유니크 인덱스 적용
1 parent b9734c9 commit cb2234d

18 files changed

Lines changed: 319 additions & 45 deletions

File tree

src/main/java/org/scoula/domain/codef/service/CodefAccountService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public class CodefAccountService {
5252

5353
private final RedisUtil redisUtil;
5454
private final MemberService memberService;
55+
private final CodefApiClient codefApiClient;
5556

5657
public void verifyCode(CodefVerifyCodeRequest codefVerifyCodeRequest, Long memberId, HttpServletRequest request) {
5758
String redisAuthCode = redisUtil.get(REDIS_CODEF_VERIFY_KEY + memberId);
@@ -155,7 +156,7 @@ public void requestConnectedIdCreate(CodefConnectedIdRequest req, Member member,
155156
}
156157

157158
private HttpURLConnection getAuthorizedConnection(String urlStr, HttpServletRequest request) throws Exception {
158-
String cachedAccessToken = redisUtil.get(REDIS_ACCESS_TOKEN_KEY);
159+
String cachedAccessToken = codefApiClient.getCachedAccessToken(request);
159160
if (cachedAccessToken == null) {
160161
log.error("CODEF Access Token이 레디스에 없습니다.");
161162
Common common = Common.builder()

src/main/java/org/scoula/domain/codef/service/CodefApiClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public DepositorResponse verifyAccountHolder(BankType bankType, String accountNu
117117
}
118118
}
119119

120-
private String getCachedAccessToken(HttpServletRequest request) {
120+
public String getCachedAccessToken(HttpServletRequest request) {
121121
String cachedAccessToken = redisUtil.get(REDIS_ACCESS_TOKEN_KEY);
122122

123123
if (cachedAccessToken == null) {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
package org.scoula.domain.ledger.mapper;
22

3+
import java.util.List;
4+
import java.util.Map;
35
import java.util.Optional;
46

7+
import org.apache.ibatis.annotations.MapKey;
58
import org.apache.ibatis.annotations.Mapper;
9+
import org.apache.ibatis.annotations.Param;
610
import org.scoula.domain.ledger.entity.LedgerCode;
711

812
@Mapper
913
public interface LedgerCodeMapper {
1014
Optional<LedgerCode> findByName(String name);
15+
16+
@MapKey("name")
17+
Map<String, LedgerCode> findByNames(@Param("names") List<String> names);
1118
}

src/main/java/org/scoula/domain/ledger/service/LedgerServiceImpl.java

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.scoula.domain.ledger.mapper.LedgerCodeMapper;
2222
import org.scoula.domain.ledger.mapper.LedgerEntryMapper;
2323
import org.scoula.domain.ledger.mapper.LedgerVoucherMapper;
24+
import org.scoula.domain.ledger.util.LedgerCodeCacheHelper;
2425
import org.scoula.domain.remittancegroup.batch.dto.MemberWithInformationDto;
2526
import org.scoula.domain.transaction.entity.TransactionType;
2627
import org.scoula.domain.wallet.dto.request.ChargeWalletRequest;
@@ -42,6 +43,7 @@ public class LedgerServiceImpl implements LedgerService {
4243
private final LedgerCodeMapper ledgerCodeMapper;
4344
private final LedgerVoucherMapper ledgerVoucherMapper;
4445
private final LedgerEntryMapper ledgerEntryMapper;
46+
private final LedgerCodeCacheHelper ledgerCodeCacheHelper;
4547

4648
@Override
4749
public void accountForWalletTransfer(TransferToWalletRequest request, String transactionGroupId,
@@ -50,23 +52,27 @@ public void accountForWalletTransfer(TransferToWalletRequest request, String tra
5052

5153
List<LedgerEntry> ledgerEntries = new ArrayList<>();
5254

53-
LedgerCode bankPayable = ledgerCodeMapper.findByName("BANK_PAYABLE")
54-
.orElseThrow(() -> new CustomException(LEDGER_CODE_NOT_FOUND, LogLevel.ERROR, null, Common.builder()
55-
.ledgerCode("BANK_PAYABLE")
56-
.srcIp(servletRequest.getRemoteAddr())
57-
.callApiPath(servletRequest.getRequestURI())
58-
.apiMethod(servletRequest.getMethod())
59-
.deviceInfo(servletRequest.getHeader("user-agent"))
60-
.build()));
61-
62-
LedgerCode bankAsset = ledgerCodeMapper.findByName("BANK_ASSET")
63-
.orElseThrow(() -> new CustomException(LEDGER_CODE_NOT_FOUND, LogLevel.ERROR, null, Common.builder()
64-
.ledgerCode("BANK_ASSET")
65-
.srcIp(servletRequest.getRemoteAddr())
66-
.callApiPath(servletRequest.getRequestURI())
67-
.apiMethod(servletRequest.getMethod())
68-
.deviceInfo(servletRequest.getHeader("user-agent"))
69-
.build()));
55+
LedgerCode bankPayable = ledgerCodeCacheHelper.getByName("BANK_PAYABLE")
56+
.orElseThrow(() -> new CustomException(LEDGER_CODE_NOT_FOUND, LogLevel.ERROR, null,
57+
Common.builder()
58+
.ledgerCode("BANK_PAYABLE")
59+
.srcIp(servletRequest.getRemoteAddr())
60+
.callApiPath(servletRequest.getRequestURI())
61+
.apiMethod(servletRequest.getMethod())
62+
.deviceInfo(servletRequest.getHeader("user-agent"))
63+
.build()
64+
));
65+
66+
LedgerCode bankAsset = ledgerCodeCacheHelper.getByName("BANK_ASSET")
67+
.orElseThrow(() -> new CustomException(LEDGER_CODE_NOT_FOUND, LogLevel.ERROR, null,
68+
Common.builder()
69+
.ledgerCode("BANK_ASSET")
70+
.srcIp(servletRequest.getRemoteAddr())
71+
.callApiPath(servletRequest.getRequestURI())
72+
.apiMethod(servletRequest.getMethod())
73+
.deviceInfo(servletRequest.getHeader("user-agent"))
74+
.build()
75+
));
7076

7177
LedgerEntry debitEntry = LedgerEntry.builder()
7278
.ledgerVoucherId(ledgerVoucher.getLedgerVoucherId())
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package org.scoula.domain.ledger.util;
2+
3+
import java.util.Locale;
4+
import java.util.Optional;
5+
6+
import org.scoula.domain.ledger.entity.LedgerCode;
7+
import org.scoula.domain.ledger.mapper.LedgerCodeMapper;
8+
import org.scoula.global.redis.util.RedisUtil;
9+
import org.springframework.stereotype.Component;
10+
11+
import com.fasterxml.jackson.databind.ObjectMapper;
12+
13+
import lombok.RequiredArgsConstructor;
14+
import lombok.extern.slf4j.Slf4j;
15+
16+
@Slf4j
17+
@Component
18+
@RequiredArgsConstructor
19+
public class LedgerCodeCacheHelper {
20+
21+
private final RedisUtil redisUtil;
22+
private final ObjectMapper objectMapper;
23+
private final LedgerCodeMapper ledgerCodeMapper;
24+
25+
private static final String KEY_PREFIX = "LedgerCode:"; // e.g. LedgerCode:BANK_PAYABLE
26+
private static final int TTL_MINUTES = 24 * 60; // 24시간 고정
27+
28+
private static String normalize(String name) {
29+
return name == null ? "" : name.trim().toUpperCase(Locale.ROOT);
30+
}
31+
32+
private static String keyOf(String name) {
33+
return KEY_PREFIX + normalize(name);
34+
}
35+
36+
public Optional<LedgerCode> getByName(String name) {
37+
final String key = keyOf(name);
38+
39+
String cached = redisUtil.get(key);
40+
if (cached != null) {
41+
try {
42+
return Optional.of(objectMapper.readValue(cached, LedgerCode.class));
43+
} catch (Exception e) {
44+
redisUtil.delete(key);
45+
}
46+
}
47+
48+
Optional<LedgerCode> fromDb = ledgerCodeMapper.findByName(normalize(name));
49+
50+
fromDb.ifPresent(code -> {
51+
try {
52+
String json = objectMapper.writeValueAsString(code);
53+
redisUtil.set(key, json, TTL_MINUTES);
54+
} catch (Exception ignore) {
55+
throw new RuntimeException(ignore);
56+
}
57+
});
58+
59+
return fromDb;
60+
}
61+
62+
public void put(String name, LedgerCode code) {
63+
try {
64+
redisUtil.set(keyOf(name), objectMapper.writeValueAsString(code), TTL_MINUTES);
65+
} catch (Exception ignore) {
66+
throw new RuntimeException(ignore);
67+
}
68+
}
69+
70+
public void evict(String name) {
71+
redisUtil.delete(keyOf(name));
72+
}
73+
74+
public void warm(String... names) {
75+
for (String n : names) {
76+
getByName(n); // 미스면 DB→캐시 적재
77+
}
78+
}
79+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.scoula.domain.member.dto.response;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
import lombok.Setter;
8+
9+
@Getter
10+
@Setter
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
@Builder
14+
public class MemberByPhoneNumberCacheResponse {
15+
private Long memberId;
16+
private String name;
17+
}

src/main/java/org/scoula/domain/member/service/MemberServiceImpl.java

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
import javax.servlet.http.HttpServletRequest;
1010

1111
import org.scoula.domain.member.dto.MemberDTO;
12+
import org.scoula.domain.member.dto.response.MemberByPhoneNumberCacheResponse;
1213
import org.scoula.domain.member.entity.Member;
1314
import org.scoula.domain.member.mapper.MemberMapper;
15+
import org.scoula.domain.member.util.MemberCache;
1416
import org.scoula.domain.remittancegroup.batch.dto.MemberWithInformationDto;
1517
import org.scoula.domain.remittancegroup.mapper.RemittanceGroupMapper;
1618
import org.scoula.global.exception.CustomException;
1719
import org.scoula.global.kafka.dto.Common;
1820
import org.scoula.global.kafka.dto.LogLevel;
19-
import org.springframework.security.crypto.password.PasswordEncoder;
2021
import org.springframework.stereotype.Service;
2122
import org.springframework.transaction.annotation.Transactional;
2223

@@ -30,8 +31,8 @@
3031
public class MemberServiceImpl implements MemberService {
3132

3233
private final MemberMapper memberMapper;
33-
private final PasswordEncoder passwordEncoder;
3434
private final RemittanceGroupMapper remittanceGroupMapper;
35+
private final MemberCache memberCache;
3536

3637
@Override
3738
public List<MemberDTO> getAllMembers() {
@@ -109,13 +110,30 @@ public void updateFcmToken(Long memberId, String fcmToken) {
109110

110111
@Override
111112
public Member getMemberByPhoneNumber(String phoneNumber, HttpServletRequest request) {
112-
return memberMapper.selectMemberByPhoneNumber(phoneNumber)
113-
.orElseThrow(() -> new CustomException(MEMBER_NOT_FOUND, LogLevel.WARNING, null, Common.builder()
114-
.apiMethod(request.getMethod())
115-
.srcIp(request.getRemoteAddr())
116-
.callApiPath(request.getRequestURI())
117-
.deviceInfo(request.getHeader("user-agent"))
118-
.build()));
113+
114+
MemberByPhoneNumberCacheResponse cached = memberCache.get(phoneNumber);
115+
if (cached != null) {
116+
return Member.builder()
117+
.memberId(cached.getMemberId())
118+
.name(cached.getName())
119+
.build();
120+
}
121+
122+
Member m = memberMapper.selectMemberByPhoneNumber(phoneNumber)
123+
.orElseThrow(() -> new CustomException(
124+
MEMBER_NOT_FOUND, LogLevel.WARNING, null,
125+
Common.builder()
126+
.apiMethod(request.getMethod())
127+
.srcIp(request.getRemoteAddr())
128+
.callApiPath(request.getRequestURI())
129+
.deviceInfo(request.getHeader("user-agent"))
130+
.build()
131+
));
132+
133+
memberCache.put(phoneNumber,
134+
MemberByPhoneNumberCacheResponse.builder().memberId(m.getMemberId()).name(m.getName()).build());
135+
136+
return m;
119137
}
120138

121139
@Override
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.scoula.domain.member.util;
2+
3+
import org.scoula.domain.member.dto.response.MemberByPhoneNumberCacheResponse;
4+
import org.scoula.global.redis.util.RedisUtil;
5+
import org.springframework.stereotype.Component;
6+
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
9+
import lombok.RequiredArgsConstructor;
10+
11+
@Component
12+
@RequiredArgsConstructor
13+
public class MemberCache {
14+
private final RedisUtil redis;
15+
private final ObjectMapper om;
16+
17+
private static final String PREFIX = "MemberMapper:selectMemberByPhoneNumber:";
18+
private static final int SELECT_BY_PHONE_TTL_MINUTES = 60 * 12;
19+
20+
private String normalize(String raw) {
21+
return raw.replaceAll("[^0-9]", "");
22+
}
23+
24+
private String key(String phone) {
25+
return PREFIX + normalize(phone);
26+
}
27+
28+
public MemberByPhoneNumberCacheResponse get(String phone) {
29+
String v = redis.get(key(phone));
30+
if (v == null)
31+
return null;
32+
try {
33+
return om.readValue(v, MemberByPhoneNumberCacheResponse.class);
34+
} catch (Exception e) {
35+
redis.delete(key(phone));
36+
return null;
37+
}
38+
}
39+
40+
public void put(String phone, MemberByPhoneNumberCacheResponse lite) {
41+
try {
42+
redis.set(key(phone), om.writeValueAsString(lite), SELECT_BY_PHONE_TTL_MINUTES);
43+
} catch (Exception ignore) {
44+
}
45+
}
46+
47+
public void evictByPhone(String phone) {
48+
redis.delete(key(phone));
49+
}
50+
}
51+

src/main/java/org/scoula/domain/transaction/mapper/TransactionMapper.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
public interface TransactionMapper {
1616
void insert(Transaction transaction);
1717

18+
void insertTransferPair(
19+
@Param("sender") Transaction sender,
20+
@Param("receiver") Transaction receiver
21+
);
22+
1823
List<Transaction> findByMemberIdAndAccountTransfer(@Param("memberId") Long memberId);
1924

2025
List<Transaction> findByMemberIdAndWalletTransfer(@Param("memberId") Long memberId);

src/main/java/org/scoula/domain/transaction/service/TransactionServiceImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ public void saveWalletTransferTransaction(Wallet senderWallet, BigDecimal sender
6161
.counterPartyName(receiver.getName())
6262
.status(SUCCESS)
6363
.build();
64-
transactionMapper.insert(senderTransaction);
6564

6665
Transaction receiverTransaction = Transaction.builder()
6766
.walletId(receiverWallet.getWalletId())
@@ -76,7 +75,8 @@ public void saveWalletTransferTransaction(Wallet senderWallet, BigDecimal sender
7675
.counterPartyName(member.getName())
7776
.status(SUCCESS)
7877
.build();
79-
transactionMapper.insert(receiverTransaction);
78+
79+
transactionMapper.insertTransferPair(senderTransaction, receiverTransaction);
8080
}
8181

8282
@Override

0 commit comments

Comments
 (0)