Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

자신 계좌 조회할 수 있는 기능을 추가한다. #34

Merged
merged 14 commits into from
Jan 17, 2024
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
8 changes: 3 additions & 5 deletions src/main/java/bankingapi/alarm/infra/NumbleAlarmService.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package bankingapi.alarm.infra;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import bankingapi.alarm.domain.AlarmService;

@Slf4j
@Service
public class NumbleAlarmService implements AlarmService {
@Async
public void notify(Long userId, String message) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("send message user id is {}, {}", userId, message);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package bankingapi.banking.application;

import java.util.List;
import java.util.stream.Collectors;

import bankingapi.alarm.dto.AlarmMessage;
Expand Down Expand Up @@ -128,4 +129,12 @@ private HistoryResponse getHistoryResponse(AccountHistory accountHistory) {
private AccountNumber getAccountNumber(String accountNumber) {
return new AccountNumber(accountNumber);
}

public List<AccountNumber> findAccounts(String principal) {
final var member = memberService.findByEmail(principal);
return accountService.getAccountByMemberId(member.getId())
.stream()
.map(Account::getAccountNumber)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,18 @@ public class ConcurrencyFacade {
private final ConcurrencyManager concurrencyManager;
private final AccountService accountService;

@Transactional
public void transferWithLock(AccountNumber accountNumber, AccountNumber toAccountNumber,
Money amount) {
concurrencyManager.executeWithLock(accountNumber.getNumber(), toAccountNumber.getNumber(),
() -> accountService.transferMoney(accountNumber, toAccountNumber, amount)
);
}
@Transactional
public void depositWithLock(AccountNumber accountNumber, Money amount) {
concurrencyManager.executeWithLock(accountNumber.getNumber(), () -> {
accountService.depositMoney(accountNumber, amount);
});
}

@Transactional
public void withdrawWithLock(AccountNumber accountNumber, Money amount) {
concurrencyManager.executeWithLock(accountNumber.getNumber(), () -> {
accountService.withdrawMoney(accountNumber, amount);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ public interface AccountRepository {
List<Account> findAll();

List<Account> findAllByUserIdIn(List<Long> userId);

List<Account> findByUserId(Long memberId);
}
9 changes: 6 additions & 3 deletions src/main/java/bankingapi/banking/domain/AccountService.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package bankingapi.banking.domain;

import java.util.List;

import bankingapi.util.generator.AccountNumberGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;
import java.util.List;

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -100,4 +99,8 @@ private void recordCompletionTransferMoney(Account fromAccount, Account toAccoun
accountHistoryRepository.save(AccountHistory.recordWithdrawHistory(fromAccount, toAccount, money));
accountHistoryRepository.save(AccountHistory.recordDepositHistory(toAccount, fromAccount, money));
}

public List<Account> getAccountByMemberId(Long memberId) {
return accountRepository.findByUserId(memberId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import jakarta.persistence.LockModeType;
Expand All @@ -23,7 +24,7 @@ public interface JpaAccountRepository extends JpaRepository<Account, Long>, Acco

@Lock(LockModeType.OPTIMISTIC)
@Query("select a from Account a where a.accountNumber = :accountNumber")
Optional<Account> findByAccountNumberWithOptimisticLock(AccountNumber accountNumber);
Optional<Account> findByAccountNumberWithOptimisticLock(@Param("accountNumber") AccountNumber accountNumber);

@Override
<S extends Account> S save(S entity);
Expand All @@ -37,4 +38,6 @@ public interface JpaAccountRepository extends JpaRepository<Account, Long>, Acco
@Query("select a from Account a where a.userId in :userId")
List<Account> findAllByUserIdIn(List<Long> userId);

@Override
List<Account> findByUserId(Long memberId);
}
11 changes: 11 additions & 0 deletions src/main/java/bankingapi/banking/ui/AccountController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package bankingapi.banking.ui;

import bankingapi.banking.domain.AccountNumber;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand All @@ -18,6 +19,8 @@
import bankingapi.banking.dto.TargetResponses;
import bankingapi.banking.dto.TransferCommand;

import java.util.List;

@RestController
@RequestMapping("account")
@RequiredArgsConstructor
Expand Down Expand Up @@ -73,4 +76,12 @@ public ResponseEntity<TargetResponses> getTargets(@AuthenticationPrincipal UserD
@PathVariable String accountNumber) {
return ResponseEntity.ok(accountApplicationService.getTargets(principal.getUsername(), accountNumber));
}

@GetMapping(
value = "/{accountNumber}/targets",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<List<AccountNumber>> findAccounts(@AuthenticationPrincipal UserDetails principal) {
return ResponseEntity.ok(accountApplicationService.findAccounts(principal.getUsername()));
}
}
Original file line number Diff line number Diff line change
@@ -1,71 +1,126 @@
package bankingapi.concurrency;

import java.util.HashMap;
import java.util.Map;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;

@Slf4j
@Service
@Slf4j
@RequiredArgsConstructor
public class ConcurrencyManagerWithNamedLock implements ConcurrencyManager {
private static final String GET_LOCK = "SELECT GET_LOCK(:userLockName, :timeoutSeconds)";
private static final String RELEASE_SESSION_LOCKS = "SELECT RELEASE_ALL_LOCKS()";
private static final String EXCEPTION_MESSAGE = "LOCK 을 수행하는 중에 오류가 발생하였습니다.";
private static final int TIMEOUT_SECONDS = 2;
private static final String EMPTY_RESULT_MESSAGE = "USER LEVEL LOCK 쿼리 결과 값이 없습니다. type = [{}], userLockName : [{}]";
private static final String INVALID_RESULT_MESSAGE = "USER LEVEL LOCK 이 존재하지 않습니다. type = [{}], result : [{}] userLockName : [{}]";
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
private static final String GET_LOCK = "SELECT GET_LOCK(?, ?)";
private static final String RELEASE_SESSION_LOCKS = "SELECT RELEASE_ALL_LOCKS()";
private static final String RELEASE_LOCK = "SELECT RELEASE_LOCK(?)";
private static final String EXCEPTION_MESSAGE = "LOCK 을 수행하는 중에 오류가 발생하였습니다.";
private static final int TIMEOUT_SECONDS = 5;
private static final String EMPTY_RESULT_MESSAGE = "USER LEVEL LOCK 쿼리 결과 값이 NULL 입니다. type = [{}], userLockName : [{}]";
private static final String INVALID_RESULT_MESSAGE = "USER LEVEL LOCK 쿼리 결과 값이 0 입니다. type = [{}], result : [{}] userLockName : [{}]";
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
private final DataSource userLoackDataSource;

@Override
public void executeWithLock(String lockName1, String lockName2, Runnable runnable) {
try (var connection = userLoackDataSource.getConnection()) {
try {
log.debug("start getLock=[{}], timeoutSeconds : [{}], connection=[{}]", getMultiLockName(lockName1, lockName2), TIMEOUT_SECONDS, connection);
getLock(connection, getMultiLockName(lockName1, lockName2));
try {
log.debug("start getLock=[{}], timeoutSeconds : [{}], connection=[{}]", lockName1, TIMEOUT_SECONDS, connection);
getLock(connection, lockName1);
try {
log.debug("start getLock=[{}], timeoutSeconds : [{}], connection=[{}]", lockName2, TIMEOUT_SECONDS, connection);
getLock(connection, lockName2);
runnable.run();
} finally {
log.debug("start releaseLock=[{}], connection=[{}]", lockName2, connection);
releaseLock(connection, lockName2);
}
}finally {
log.debug("start releaseLock=[{}], connection=[{}]", lockName1, connection);
releaseLock(connection, lockName1);
}
} finally {
log.debug("start releaseLock=[{}], connection=[{}]", getMultiLockName(lockName1, lockName2), connection);
releaseLock(connection, getMultiLockName(lockName1, lockName2));
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

@Override
public void executeWithLock(String lockName1, String lockName2, Runnable runnable) {
try {
getLock(lockName1);
getLock(lockName2);
runnable.run();
} finally {
releaseSessionLocks();
}
}
@Override
public void executeWithLock(String lockName, Runnable runnable) {
try (var connection = userLoackDataSource.getConnection()) {
log.info("start getLock=[{}], timeoutSeconds : [{}], connection=[{}]", lockName, TIMEOUT_SECONDS, connection);
getLock(connection, lockName);
try {
runnable.run();
} finally {
log.info("start releaseLock, connection=[{}]", connection);
releaseLock(connection, lockName);
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

@Override
public void executeWithLock(String lockName, Runnable runnable) {
try {
getLock(lockName);
runnable.run();
} finally {
releaseSessionLocks();
}
}
private void getLock(Connection connection, String userLockName) {
try (var preparedStatement = connection.prepareStatement(GET_LOCK)) {
preparedStatement.setString(1, userLockName);
preparedStatement.setInt(2, TIMEOUT_SECONDS);
var resultSet = preparedStatement.executeQuery();
validateResult(resultSet, userLockName, "GetLock");
} catch (SQLException e) {
log.error("GetLock_{} : {}", userLockName, e.getMessage());
throw new IllegalStateException("SQL Exception");
}
}
private void releaseLock(Connection connection, String userLockName) {
try (var preparedStatement = connection.prepareStatement(RELEASE_LOCK)) {
preparedStatement.setString(1, userLockName);
preparedStatement.executeQuery();
} catch (SQLException e) {
log.error("Release Lock : {}", e.getMessage());
throw new IllegalStateException("SQL Exception");
}
}
private void releaseSessionLocks(Connection connection) {
try (var preparedStatement = connection.prepareStatement(RELEASE_SESSION_LOCKS)) {
preparedStatement.executeQuery();
} catch (SQLException e) {
log.error("ReleaseSessionLocks : {}", e.getMessage());
throw new IllegalStateException("SQL Exception");
}
}

private void getLock(String userLockName) {
Map<String, Object> params = new HashMap<>();
params.put("userLockName", userLockName);
params.put("timeoutSeconds", ConcurrencyManagerWithNamedLock.TIMEOUT_SECONDS);
Integer result = namedParameterJdbcTemplate.queryForObject(GET_LOCK, params, Integer.class);
validateResult(result, userLockName, "GetLock");
}
private void releaseSessionLocks() {
Map<String, Object> params = new HashMap<>();
namedParameterJdbcTemplate.queryForObject(RELEASE_SESSION_LOCKS, params, Integer.class);
}

private void releaseSessionLocks() {
Map<String, Object> params = new HashMap<>();
Integer result = namedParameterJdbcTemplate.queryForObject(RELEASE_SESSION_LOCKS, params, Integer.class);
validateResult(result, "SESSION", "ReleaseLock");
}
private void validateResult(ResultSet resultSet, String userLockName, String type) throws SQLException {
if (!resultSet.next()) {
log.error(EMPTY_RESULT_MESSAGE, type, userLockName);
throw new ConcurrencyFailureException(EXCEPTION_MESSAGE);
}
int result = resultSet.getInt(1);
if (result == 0) {
log.error(INVALID_RESULT_MESSAGE, type, result, userLockName);
throw new ConcurrencyFailureException(EXCEPTION_MESSAGE);
}
}

private void validateResult(Integer result, String userLockName, String type) {
if (result == null) {
log.error(EMPTY_RESULT_MESSAGE, type, userLockName);
throw new ConcurrencyFailureException(EXCEPTION_MESSAGE);
}
if (result == 0) {
log.error(INVALID_RESULT_MESSAGE, type, result,
userLockName);
throw new ConcurrencyFailureException(EXCEPTION_MESSAGE);
}
}
private static String getMultiLockName(String lockName1, String lockName2) {
return Stream.of(lockName1, lockName2).sorted().reduce((a, b) -> a + b).get();
}
}
26 changes: 26 additions & 0 deletions src/main/java/bankingapi/util/config/DatasourceConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package bankingapi.util.config;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

@Configuration
public class DatasourceConfiguration {
@Primary
@Bean
@ConfigurationProperties("spring.datasource.hikari")
public DataSource dataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}

@Bean
@ConfigurationProperties("userlock.datasource.hikari")
public DataSource userLockDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
}
Loading
Loading