Skip to content

Commit

Permalink
Merge pull request #34 from this-is-spear/feature/find-account
Browse files Browse the repository at this point in the history
자신 계좌 조회할 수 있는 기능을 추가한다.
  • Loading branch information
this-is-spear authored Jan 17, 2024
2 parents 7a66b15 + 01833f7 commit 43158bc
Show file tree
Hide file tree
Showing 25 changed files with 383 additions and 161 deletions.
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

0 comments on commit 43158bc

Please sign in to comment.