Skip to content

Commit

Permalink
refactor: 충전 한도 관리 변경
Browse files Browse the repository at this point in the history
- 일일 충전 한도를 별도의 테이블이 아닌 메인 계좌에서 함께 관리합니다.
- 기존은 별도의 테이블로 분리하여 매일 초기화를 했지만 더 좋은 방법이 생각나 변경했습니다.
- 굳이 매일 변경할 필요가 없이, 최근 업데이트 날짜와 오늘 날짜가 다르다면 한도를 초기화하고 남은 로직을 이어서 실행합니다.
  • Loading branch information
seungh1024 committed Mar 4, 2024
1 parent 7257f4c commit 03f496f
Show file tree
Hide file tree
Showing 21 changed files with 109 additions and 317 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;

@Configuration
@EnableCaching
Expand All @@ -17,20 +14,4 @@ public class AccountCacheConfig {
public CacheManager savingProductCacheManager() {
return new ConcurrentMapCacheManager("savingProduct");
}

/**
*
* DB에 저장된 일일 충전한도 값을 한 번 읽으면 Redis에 저장하여 관리합니다.
* DB에 별도의 테이블로 관리하면 디스크 공간과 인덱스를 위한 메모리 공간이 사용됩니다.
* 그렇게 하기 보다는 Redis에서 <PK, 충전한도>로 관리하여 인덱스 테이블과 비슷한 메모리만 사용하고자 했습니다.
*/
@Bean
public RedisTemplate<Long, Long> redisTemplate(RedisConnectionFactory connectionFactory) {
final RedisTemplate<Long, Long> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new GenericToStringSerializer<>(Long.class));
template.setValueSerializer(new GenericToStringSerializer<>(Long.class));

return template;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,15 @@ public class MainAccountController {
@PostMapping("/charge")
public long chargeMoney(@Login SessionMemberInfo memberInfo,
@Valid @RequestBody ChargeMoneyRequestDto requestDto) {
return mainAccountService.chargeMoney(memberInfo.mainAccountPk(), requestDto.money(),
memberInfo.chargeLimitPk());
return mainAccountService.chargeMoney(memberInfo.mainAccountPk(), requestDto.money());
}

@ResponseStatus(HttpStatus.OK)
@PostMapping("/send/saving")
public void sendToSavingAccount(@Login SessionMemberInfo memberInfo,
@Valid @RequestBody SendMoneyRequestDto sendAccountInfo) {
mainAccountService.sendToSavingAccount(memberInfo.mainAccountPk(), sendAccountInfo.accountPk(),
sendAccountInfo.money(), memberInfo.chargeLimitPk());
sendAccountInfo.money());
}

@ResponseStatus(HttpStatus.OK)
Expand All @@ -53,6 +52,6 @@ public MainAccountResponseDto getMainAccountInfo(@Login SessionMemberInfo member
public void sendToOtherAccount(@Login SessionMemberInfo memberInfo,
@Valid @RequestBody SendMoneyRequestDto sendAccountInfo) {
mainAccountService.sendToOtherAccount(memberInfo.mainAccountPk(), sendAccountInfo.accountPk(),
sendAccountInfo.money(), memberInfo.chargeLimitPk());
sendAccountInfo.money());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

public record MainAccountResponseDto(
long accountPk,
long chargeLimit,
long spareMoney,
long money
) {
public MainAccountResponseDto(MainAccount mainAccount) {
this(mainAccount.getAccountPk(), mainAccount.getMoney());
this(mainAccount.getAccountPk(), mainAccount.getChargeLimit(), mainAccount.getSpareMoney(),
mainAccount.getMoney());
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.c4marathon.assignment.bankaccount.entity;

import java.time.LocalDateTime;

import org.c4marathon.assignment.common.entity.BaseEntity;
import org.c4marathon.assignment.common.utils.ConstValue;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand All @@ -25,19 +28,41 @@ public class MainAccount extends BaseEntity {
@Column(name = "money", nullable = false)
private long money;

// 최대 충전 한도
@Column(name = "charge_money", nullable = false)
private long chargeLimit;

// 추가로 충전할 수 있는 금액
@Column(name = "spare_money", nullable = false)
private long spareMoney;

public MainAccount() {
this.money = 0L;
this.chargeLimit = ConstValue.LimitConst.CHARGE_LIMIT;
this.spareMoney = ConstValue.LimitConst.CHARGE_LIMIT;
}

public MainAccount(long money) {
this.money = money;
public void minusMoney(long money) {
this.money -= money;
}

public void chargeMoney(long money) {
this.money += money;
public void chargeCheck() {
int lastDay = this.getUpdatedAt().getDayOfMonth();
LocalDateTime now = LocalDateTime.now();
int nowDay = now.getDayOfMonth();
if (lastDay != nowDay) {
this.setUpdatedAt(now);
this.chargeLimit = ConstValue.LimitConst.CHARGE_LIMIT;
this.spareMoney = ConstValue.LimitConst.CHARGE_LIMIT;
}
}

public void minusMoney(long money) {
this.money -= money;
public boolean charge(long money) {
if (this.spareMoney >= money) {
this.spareMoney -= money;
this.money += money;
return true;
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ public enum AccountErrorCode implements ErrorCode {
CHARGE_LIMIT_EXCESS(HttpStatus.BAD_REQUEST, "일일 충전 한도를 초과했습니다."),
ACCOUNT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 계좌입니다."),
PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 적금 상품입니다."),
INVALID_MONEY_SEND(HttpStatus.BAD_REQUEST, "잔고가 부족합니다."),
CHARGE_LIMIT_NOT_FOUND(HttpStatus.NOT_FOUND, "충전 한도 정보를 찾을 수 없습니다.");
INVALID_MONEY_SEND(HttpStatus.BAD_REQUEST, "잔고가 부족합니다.");
private final HttpStatus httpStatus;
private final String message;

Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package org.c4marathon.assignment.bankaccount.service;

import org.c4marathon.assignment.bankaccount.dto.response.MainAccountResponseDto;
import org.c4marathon.assignment.bankaccount.entity.ChargeLimit;
import org.c4marathon.assignment.bankaccount.entity.MainAccount;
import org.c4marathon.assignment.bankaccount.entity.SavingAccount;
import org.c4marathon.assignment.bankaccount.entity.SendRecord;
import org.c4marathon.assignment.bankaccount.exception.AccountErrorCode;
import org.c4marathon.assignment.bankaccount.repository.ChargeLimitRepository;
import org.c4marathon.assignment.bankaccount.repository.MainAccountRepository;
import org.c4marathon.assignment.bankaccount.repository.SavingAccountRepository;
import org.c4marathon.assignment.bankaccount.repository.SendRecordRepository;
Expand All @@ -25,7 +23,6 @@ public class MainAccountService {
private final SavingAccountRepository savingAccountRepository;
private final DepositHandlerService depositHandlerService;
private final SendRecordRepository sendRecordRepository;
private final ChargeLimitRepository chargeLimitRepository;

/**
*
Expand All @@ -36,24 +33,21 @@ public class MainAccountService {
* ChargeLimitManager를 통해 충전이 가능한지 확인하고 money만큼 충전 후 계좌 잔고를 리턴합니다.
*/
@Transactional(isolation = Isolation.READ_COMMITTED)
public long chargeMoney(long mainAccountPk, long money, long chargeLimitPk) {
checkAndCharge(money, chargeLimitPk);

public long chargeMoney(long mainAccountPk, long money) {
MainAccount mainAccount = mainAccountRepository.findByPkForUpdate(mainAccountPk)
.orElseThrow(() -> AccountErrorCode.ACCOUNT_NOT_FOUND.accountException());

mainAccount.chargeMoney(money);
checkAndCharge(money, mainAccount);
mainAccountRepository.save(mainAccount);

return mainAccount.getMoney();
}

@Transactional(isolation = Isolation.READ_COMMITTED)
public void sendToSavingAccount(long mainAccountPk, long savingAccountPk, long money, long chargeLimitPk) {
public void sendToSavingAccount(long mainAccountPk, long savingAccountPk, long money) {
MainAccount mainAccount = mainAccountRepository.findByPkForUpdate(mainAccountPk)
.orElseThrow(AccountErrorCode.ACCOUNT_NOT_FOUND::accountException);

autoMoneyChange(mainAccount, money, chargeLimitPk);
autoMoneyChange(mainAccount, money);

SavingAccount savingAccount = savingAccountRepository.findByPkForUpdate(savingAccountPk)
.orElseThrow(AccountErrorCode.ACCOUNT_NOT_FOUND::accountException);
Expand Down Expand Up @@ -86,12 +80,12 @@ public MainAccountResponseDto getMainAccountInfo(long mainAccountPk) {
* 그래서 현재의 이체 로그를 이후 step에서 구현할 로그로 사용하지 않는다면, A->B의 이체 로직을 한 번에 묶은 것과 큰 성능 차이가 없는 것 아닌가?라는 의문이 들었습니다.
* */
@Transactional(isolation = Isolation.READ_COMMITTED)
public void sendToOtherAccount(long senderPk, long depositPk, long money, long chargeLimitPk) {
public void sendToOtherAccount(long senderPk, long depositPk, long money) {
// 1. 나의 계좌에서 이체할 금액을 빼준다.
MainAccount myAccount = mainAccountRepository.findByPkForUpdate(senderPk)
.orElseThrow(AccountErrorCode.ACCOUNT_NOT_FOUND::accountException);

autoMoneyChange(myAccount, money, chargeLimitPk);
autoMoneyChange(myAccount, money);
mainAccountRepository.save(myAccount);
// 2. 이체 로그를 남겨준다.
SendRecord sendRecord = new SendRecord(senderPk, depositPk, money);
Expand All @@ -110,16 +104,18 @@ public boolean isSendValid(long myMoney, long sendMoney) {
*
* 메인 계좌의 돈을 자동으로 차감 또는 충전 후 차감 해주는 메소드
*/
public void autoMoneyChange(MainAccount mainAccount, long money, long chargeLimitPk) {
public void autoMoneyChange(MainAccount mainAccount, long money) {
// 잔고가 부족한 경우 자동 충전 시작
if (!isSendValid(mainAccount.getMoney(), money)) {
long minusMoney =
money - mainAccount.getMoney(); // chargeMoney 계산 편의를 위해(양수로 만들기 위해) money - mainAccount.getMoney()
long chargeMoney = (minusMoney / ConstValue.LimitConst.CHARGE_AMOUNT + 1)
* ConstValue.LimitConst.CHARGE_AMOUNT; // 만 원 단위로 충전해야 할 금액
checkAndCharge(chargeMoney, chargeLimitPk); // 충전 한도 확인 및 변화
chargeMoney = chargeMoney - money; // 실제로 계좌에 더해야 하는 금액
mainAccount.chargeMoney(chargeMoney);
long chargeMoney = minusMoney / ConstValue.LimitConst.CHARGE_AMOUNT;
if (minusMoney % ConstValue.LimitConst.CHARGE_AMOUNT > 0) {
chargeMoney++;
}
chargeMoney *= ConstValue.LimitConst.CHARGE_AMOUNT;
checkAndCharge(chargeMoney, mainAccount); // 충전 한도 확인 및 변화
mainAccount.minusMoney(money);
} else {
mainAccount.minusMoney(money);
}
Expand All @@ -129,14 +125,10 @@ public void autoMoneyChange(MainAccount mainAccount, long money, long chargeLimi
*
* 충전 한도 테이블에서 충전 한도를 확인하고 가능하면 충전해주는 메소드
*/
public void checkAndCharge(long money, long chargeLimitPk) {
ChargeLimit chargeLimit = chargeLimitRepository.findByPkForUpdate(chargeLimitPk).orElseThrow(() ->
AccountErrorCode.CHARGE_LIMIT_NOT_FOUND.accountException(
"충전 한도 정보를 찾을 수 없음, chargeLimitPk = " + chargeLimitPk)
);
if (!chargeLimit.charge(money)) {
public void checkAndCharge(long money, MainAccount mainAccount) {
mainAccount.chargeCheck();
if (!mainAccount.charge(money)) {
throw AccountErrorCode.CHARGE_LIMIT_EXCESS.accountException("충전 한도 초과, money = " + money);
}
chargeLimitRepository.save(chargeLimit);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ public class BaseEntity {
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;

public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,13 @@ public Member toEntity(String encodedPassword) {
.build();
}

public Member toEntity(long mainAccountPk, long chargeLimitPk, String encodedPassword) {
public Member toEntity(long mainAccountPk, String encodedPassword) {
return Member.builder()
.memberId(memberId)
.password(encodedPassword)
.memberName(memberName)
.phoneNumber(phoneNumber)
.mainAccountPk(mainAccountPk)
.chargeLimitPk(chargeLimitPk)
.build();
}
}
Loading

0 comments on commit 03f496f

Please sign in to comment.