From b81b901514a6f240722c5a5880761fb0aba37a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= <92219795+this-is-spear@users.noreply.github.com> Date: Tue, 16 Jan 2024 12:56:15 +0900 Subject: [PATCH 01/14] =?UTF-8?q?Feature=20:=20=EC=9E=90=EC=8B=A0=20?= =?UTF-8?q?=EA=B3=84=EC=A2=8C=EB=A5=BC=20=EC=A1=B0=ED=9A=8C=ED=95=98?= =?UTF-8?q?=EB=8A=94=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/AccountApplicationService.java | 9 +++++++++ .../banking/domain/AccountRepository.java | 2 ++ .../bankingapi/banking/domain/AccountService.java | 5 +++++ .../banking/infra/JpaAccountRepository.java | 2 ++ .../bankingapi/banking/ui/AccountController.java | 11 +++++++++++ .../util/config/SecurityConfiguration.java | 12 +++++++++++- .../bankingapi/fake/FakeAccountRepository.java | 15 +++++++++++---- 7 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/main/java/bankingapi/banking/application/AccountApplicationService.java b/src/main/java/bankingapi/banking/application/AccountApplicationService.java index 9154cb5..f697c1b 100644 --- a/src/main/java/bankingapi/banking/application/AccountApplicationService.java +++ b/src/main/java/bankingapi/banking/application/AccountApplicationService.java @@ -1,5 +1,6 @@ package bankingapi.banking.application; +import java.util.List; import java.util.stream.Collectors; import bankingapi.alarm.dto.AlarmMessage; @@ -128,4 +129,12 @@ private HistoryResponse getHistoryResponse(AccountHistory accountHistory) { private AccountNumber getAccountNumber(String accountNumber) { return new AccountNumber(accountNumber); } + + public List findAccounts(String principal) { + final var member = memberService.findByEmail(principal); + return accountService.getAccountByMemberId(member.getId()) + .stream() + .map(Account::getAccountNumber) + .toList(); + } } diff --git a/src/main/java/bankingapi/banking/domain/AccountRepository.java b/src/main/java/bankingapi/banking/domain/AccountRepository.java index 19c7565..ad1c526 100644 --- a/src/main/java/bankingapi/banking/domain/AccountRepository.java +++ b/src/main/java/bankingapi/banking/domain/AccountRepository.java @@ -17,4 +17,6 @@ public interface AccountRepository { List findAll(); List findAllByUserIdIn(List userId); + + List findByUserId(Long memberId); } diff --git a/src/main/java/bankingapi/banking/domain/AccountService.java b/src/main/java/bankingapi/banking/domain/AccountService.java index d75aa40..b76a235 100644 --- a/src/main/java/bankingapi/banking/domain/AccountService.java +++ b/src/main/java/bankingapi/banking/domain/AccountService.java @@ -1,5 +1,6 @@ package bankingapi.banking.domain; +import java.util.Collection; import java.util.List; import bankingapi.util.generator.AccountNumberGenerator; @@ -100,4 +101,8 @@ private void recordCompletionTransferMoney(Account fromAccount, Account toAccoun accountHistoryRepository.save(AccountHistory.recordWithdrawHistory(fromAccount, toAccount, money)); accountHistoryRepository.save(AccountHistory.recordDepositHistory(toAccount, fromAccount, money)); } + + public List getAccountByMemberId(Long memberId) { + return accountRepository.findByUserId(memberId); + } } diff --git a/src/main/java/bankingapi/banking/infra/JpaAccountRepository.java b/src/main/java/bankingapi/banking/infra/JpaAccountRepository.java index 106fa11..42682fa 100644 --- a/src/main/java/bankingapi/banking/infra/JpaAccountRepository.java +++ b/src/main/java/bankingapi/banking/infra/JpaAccountRepository.java @@ -37,4 +37,6 @@ public interface JpaAccountRepository extends JpaRepository, Acco @Query("select a from Account a where a.userId in :userId") List findAllByUserIdIn(List userId); + @Override + List findByUserId(Long memberId); } diff --git a/src/main/java/bankingapi/banking/ui/AccountController.java b/src/main/java/bankingapi/banking/ui/AccountController.java index 1cb0857..b03935e 100644 --- a/src/main/java/bankingapi/banking/ui/AccountController.java +++ b/src/main/java/bankingapi/banking/ui/AccountController.java @@ -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; @@ -18,6 +19,8 @@ import bankingapi.banking.dto.TargetResponses; import bankingapi.banking.dto.TransferCommand; +import java.util.List; + @RestController @RequestMapping("account") @RequiredArgsConstructor @@ -73,4 +76,12 @@ public ResponseEntity 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> findAccounts(@AuthenticationPrincipal UserDetails principal) { + return ResponseEntity.ok(accountApplicationService.findAccounts(principal.getUsername())); + } } diff --git a/src/main/java/bankingapi/util/config/SecurityConfiguration.java b/src/main/java/bankingapi/util/config/SecurityConfiguration.java index d4507db..d00df6c 100644 --- a/src/main/java/bankingapi/util/config/SecurityConfiguration.java +++ b/src/main/java/bankingapi/util/config/SecurityConfiguration.java @@ -45,6 +45,16 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti @Bean public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); + return new PasswordEncoder() { + @Override + public String encode(CharSequence rawPassword) { + return rawPassword.toString(); + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + return rawPassword.equals(encodedPassword); + } + }; } } diff --git a/src/test/java/bankingapi/fake/FakeAccountRepository.java b/src/test/java/bankingapi/fake/FakeAccountRepository.java index 011d008..edba939 100644 --- a/src/test/java/bankingapi/fake/FakeAccountRepository.java +++ b/src/test/java/bankingapi/fake/FakeAccountRepository.java @@ -1,15 +1,15 @@ package bankingapi.fake; +import bankingapi.banking.domain.Account; +import bankingapi.banking.domain.AccountNumber; +import bankingapi.banking.domain.AccountRepository; + import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; -import bankingapi.banking.domain.Account; -import bankingapi.banking.domain.AccountNumber; -import bankingapi.banking.domain.AccountRepository; - public class FakeAccountRepository implements AccountRepository { Map maps = new HashMap<>(); @@ -56,4 +56,11 @@ public List findAllByUserIdIn(List userIds) { .filter(account -> userIds.contains(account.getUserId())) .collect(Collectors.toList()); } + + @Override + public List findByUserId(Long memberId) { + return maps.values().stream() + .filter(account -> account.getUserId().equals(memberId)) + .toList(); + } } From ed058316ebf2801be17bb377920a7ed9deb523e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= <92219795+this-is-spear@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:24:34 +0900 Subject: [PATCH 02/14] =?UTF-8?q?Refactor=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/bankingapi/alarm/infra/NumbleAlarmService.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/bankingapi/alarm/infra/NumbleAlarmService.java b/src/main/java/bankingapi/alarm/infra/NumbleAlarmService.java index 5ec8849..cc7f40e 100644 --- a/src/main/java/bankingapi/alarm/infra/NumbleAlarmService.java +++ b/src/main/java/bankingapi/alarm/infra/NumbleAlarmService.java @@ -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); } } From ab82c6933817ab195bd97eaec13b8bc8a526e23b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= <92219795+this-is-spear@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:36:42 +0900 Subject: [PATCH 03/14] =?UTF-8?q?Test=20:=20=EC=9E=90=EC=8B=A0=20=EA=B3=84?= =?UTF-8?q?=EC=A2=8C=EB=A5=BC=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8A=94=20API?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccountApplicationServiceTest.java | 23 ++++++++++++++++++- .../bankingapi/fixture/AccountFixture.java | 1 + 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/test/java/bankingapi/banking/application/AccountApplicationServiceTest.java b/src/test/java/bankingapi/banking/application/AccountApplicationServiceTest.java index b7fe2d8..f8181d5 100644 --- a/src/test/java/bankingapi/banking/application/AccountApplicationServiceTest.java +++ b/src/test/java/bankingapi/banking/application/AccountApplicationServiceTest.java @@ -58,7 +58,14 @@ class AccountApplicationServiceTest { .balance(AccountFixture.이만원) .userId(사용자_ID) .build(); - private static final Account 상대방_계좌 = Account.builder() + + private static final Account 자신의_계좌 = Account.builder() + .accountNumber(AccountFixture.자신의_계좌반호) + .balance(AccountFixture.이만원) + .userId(사용자_ID) + .build(); + + private static final Account 상대방_계좌 = Account.builder() .accountNumber(AccountFixture.상대방_계좌번호) .balance(AccountFixture.만원) .userId(상대방_ID) @@ -190,4 +197,18 @@ void getTargets_accessInvalidMember() { () -> accountApplicationService.getTargets(EMAIL, 계좌.getAccountNumber().getNumber()) ).isInstanceOf(InvalidMemberException.class); } + + @Test + @DisplayName("자신의 계좌를 조회한다.") + void findAccounts() { + when(memberService.findByEmail(EMAIL)).thenReturn(사용자); + when(accountService.getAccountByMemberId(사용자_ID)).thenReturn(List.of(자신의_계좌, 계좌)); + + var responses = assertDoesNotThrow( + () -> accountApplicationService.findAccounts(EMAIL) + ); + + assertThat(responses).hasSize(2); + assertThat(responses.get(0)).isEqualTo(계좌.getAccountNumber()); + } } diff --git a/src/test/java/bankingapi/fixture/AccountFixture.java b/src/test/java/bankingapi/fixture/AccountFixture.java index 038d1a1..53a36c2 100644 --- a/src/test/java/bankingapi/fixture/AccountFixture.java +++ b/src/test/java/bankingapi/fixture/AccountFixture.java @@ -8,6 +8,7 @@ public class AccountFixture { public static final AccountNumber 계좌번호 = AccountNumberGenerator.generate(); public static final AccountNumber 상대방_계좌번호 = AccountNumberGenerator.generate(); + public static final AccountNumber 자신의_계좌반호 = AccountNumberGenerator.generate(); public static final Money 오천원 = new Money(5_000); public static final Money 만원 = new Money(10_000); public static final Money 만오천원 = new Money(15_000); From 75f6c14c62c985e601e04c6730b514fbd84ca771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= <92219795+this-is-spear@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:42:59 +0900 Subject: [PATCH 04/14] =?UTF-8?q?Refactor=20:=20csrf=20deprecated=20?= =?UTF-8?q?=EB=90=9C=20=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/bankingapi/util/config/SecurityConfiguration.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/bankingapi/util/config/SecurityConfiguration.java b/src/main/java/bankingapi/util/config/SecurityConfiguration.java index d00df6c..4bbed67 100644 --- a/src/main/java/bankingapi/util/config/SecurityConfiguration.java +++ b/src/main/java/bankingapi/util/config/SecurityConfiguration.java @@ -7,6 +7,7 @@ import org.springframework.context.annotation.Primary; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @@ -24,7 +25,7 @@ public class SecurityConfiguration { @Primary public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.userDetailsService(userDetailsService); - http.csrf().disable(); + http.csrf(AbstractHttpConfigurer::disable); http .httpBasic(withDefaults()) From 4468da9b6689347196105b517b01e379cbf4999e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= <92219795+this-is-spear@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:45:45 +0900 Subject: [PATCH 05/14] =?UTF-8?q?Refactor=20:=20security=20deprecated=20?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bankingapi/util/config/SecurityConfiguration.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/bankingapi/util/config/SecurityConfiguration.java b/src/main/java/bankingapi/util/config/SecurityConfiguration.java index 4bbed67..7a583d5 100644 --- a/src/main/java/bankingapi/util/config/SecurityConfiguration.java +++ b/src/main/java/bankingapi/util/config/SecurityConfiguration.java @@ -27,19 +27,16 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http.userDetailsService(userDetailsService); http.csrf(AbstractHttpConfigurer::disable); - http - .httpBasic(withDefaults()) - .formLogin() - .successHandler((request, response, authentication) -> response.sendRedirect("/hello")) - .and() - .authorizeHttpRequests((authorize) -> authorize + http.httpBasic(withDefaults()).formLogin(withDefaults()); + + http.authorizeHttpRequests((authorize) -> authorize .requestMatchers("/hello").permitAll() .requestMatchers("/docs/index.html").permitAll() .requestMatchers("/members/register").anonymous() .requestMatchers("/login").anonymous() .requestMatchers("/account/**").authenticated() .requestMatchers("/members/**").authenticated() - ); + ); return http.build(); } From b9307d9b588cb9fd74c1a374fb142242e807e9f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= Date: Tue, 16 Jan 2024 23:26:51 +0900 Subject: [PATCH 06/14] =?UTF-8?q?Test=20:=20=EC=8B=A4=ED=8C=A8=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../banking/application/AccountApplicationServiceTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/bankingapi/banking/application/AccountApplicationServiceTest.java b/src/test/java/bankingapi/banking/application/AccountApplicationServiceTest.java index f8181d5..19f34a4 100644 --- a/src/test/java/bankingapi/banking/application/AccountApplicationServiceTest.java +++ b/src/test/java/bankingapi/banking/application/AccountApplicationServiceTest.java @@ -209,6 +209,5 @@ void findAccounts() { ); assertThat(responses).hasSize(2); - assertThat(responses.get(0)).isEqualTo(계좌.getAccountNumber()); } } From 58c66c72b56b5412314d81a342be7066b25c6d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= Date: Wed, 17 Jan 2024 09:25:33 +0900 Subject: [PATCH 07/14] =?UTF-8?q?Test=20:=20=EC=8B=A4=ED=8C=A8=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acceptance/BankingAcceptanceTest.java | 92 +++++++++++-------- 1 file changed, 54 insertions(+), 38 deletions(-) diff --git a/src/test/java/bankingapi/acceptance/BankingAcceptanceTest.java b/src/test/java/bankingapi/acceptance/BankingAcceptanceTest.java index 71a5546..c25083b 100644 --- a/src/test/java/bankingapi/acceptance/BankingAcceptanceTest.java +++ b/src/test/java/bankingapi/acceptance/BankingAcceptanceTest.java @@ -1,10 +1,9 @@ package bankingapi.acceptance; -import static org.junit.jupiter.api.Assertions.*; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import bankingapi.banking.dto.HistoryResponses; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; import java.util.HashMap; import java.util.Map; @@ -12,11 +11,15 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; -import org.junit.jupiter.api.Test; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.ResultActions; - -import bankingapi.banking.dto.HistoryResponses; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class BankingAcceptanceTest extends AcceptanceTest { private static final String IDEMPOTENT_KEY = "Idempotency-Key"; @@ -122,7 +125,7 @@ void transfer_failed() throws Exception { * * @Given : 사용자는 백만원을 입금하고 * @When : 상대방에게 동시에 천 원을 50 번 요청하면 - * @Then : 잔액에 95만원 남는다. + * @Then : 잔액이 95만원 이상 남는다. */ @Test void transfer_concurrency_50_times() throws Exception { @@ -156,8 +159,8 @@ void transfer_concurrency_50_times() throws Exception { * 서로 이체를 10번한다. * * @Given : 사용자와 상배방은 백만원씩 있고 - * @When : 서로 만원씩 10번 이체하면 - * @Then : 서로 계좌에 백만원이 남는다. + * @When : 서로 만원씩 10번 이체하면서 문제가 생겨도 + * @Then : 총 합 2백만원이 남는다. */ @Test void transfer_concurrency_10times() throws Exception { @@ -186,9 +189,10 @@ void transfer_concurrency_10times() throws Exception { try { 계좌_이체_요청(나의계좌, 상대방계좌, 출금할_돈, 이메일, 비밀번호).andExpect(status().isOk()); 계좌_이체_요청(상대방계좌, 나의계좌, 출금할_돈, 어드민이메일, 비밀번호).andExpect(status().isOk()); - latch.countDown(); } catch (Exception e) { throw new RuntimeException(e); + } finally { + latch.countDown(); } }); } @@ -196,20 +200,19 @@ void transfer_concurrency_10times() throws Exception { // then assertAll( - () -> 계좌_조회_요청(나의계좌, 이메일, 비밀번호).andExpect(jsonPath(AMOUNT) - .value(입금할_돈)), - () -> 계좌_조회_요청(상대방계좌, 어드민이메일, 비밀번호).andExpect(jsonPath(AMOUNT) - .value(입금할_돈)), - () -> assertEquals(objectMapper.readValue(계좌_조회_요청(나의계좌, 이메일, 비밀번호).andReturn().getResponse() - .getContentAsByteArray(), HistoryResponses.class).historyResponses().size(), 요청_횟수 * 2 + 1) + () -> assertEquals(getHistoryResponses(나의계좌, 이메일).balance().getAmount() + + getHistoryResponses(상대방계좌, 어드민이메일).balance().getAmount(), 백만원 * 2), + () -> assertThat(getHistoryResponses(상대방계좌, 어드민이메일).historyResponses()).hasSameSizeAs( + getHistoryResponses(나의계좌, 이메일).historyResponses()) ); } + /** * 입금을 동시에 10번한다. * - * @When : 사용자의 계좌에 만원씩 10번 입금하면 - * @Then : 사용자의 계좌에 백만원이 남는다. + * @When : 사용자의 계좌에 만원씩 10번 입금할 때 + * @Then : 요청 횟수 이력에 맞게 잔액이 남는다. */ @Test void deposit_concurrency_10times() throws Exception { @@ -226,28 +229,32 @@ void deposit_concurrency_10times() throws Exception { executorService.execute(() -> { try { 계좌_입금_요청(나의계좌, 입금할_돈, 이메일, 비밀번호).andExpect(status().isOk()); - latch.countDown(); } catch (Exception e) { throw new RuntimeException(e); + } finally { + latch.countDown(); } }); } latch.await(); + // then - assertAll( - () -> 계좌_조회_요청(나의계좌, 이메일, 비밀번호).andExpect(jsonPath(AMOUNT).value(입금할_돈 * 요청_횟수)), - () -> assertEquals(objectMapper.readValue(계좌_조회_요청(나의계좌, 이메일, 비밀번호).andReturn().getResponse() - .getContentAsByteArray(), HistoryResponses.class).historyResponses().size(), 요청_횟수) - ); - } + var successDepositTime = getHistoryResponses(나의계좌, 이메일).historyResponses().size(); + assertAll( + () -> 계좌_조회_요청(나의계좌, 이메일, 비밀번호).andExpect(jsonPath(AMOUNT).value(입금할_돈 * successDepositTime)), + ()-> assertThat( getHistoryResponses(나의계좌, 이메일).balance().getAmount()) + .isGreaterThan(0L) + .isLessThan(10_000L) + );} + /** * 출금을 동시에 10번한다. * - * @Given : 사용자와 상배방은 백만원씩 있고 + * @Given : 사용자와 상대방은 백만원씩 있고 * @When : 사용자의 계좌에 만원씩 10번 출금하면 - * @Then : 사용자의 계좌에 구십 만원이 남는다. + * @Then : 사용자의 계좌에 구십 만원에서 백만원 사이의 현금이 남는다. */ @Test void withdraw_concurrency_10times() throws Exception { @@ -269,20 +276,23 @@ void withdraw_concurrency_10times() throws Exception { executorService.execute(() -> { try { 계좌_출금_요청(나의계좌, 출금할_돈, 이메일, 비밀번호).andExpect(status().isOk()); - latch.countDown(); } catch (Exception e) { throw new RuntimeException(e); + } finally { + latch.countDown(); } }); } latch.await(); // then - assertAll( - () -> 계좌_조회_요청(나의계좌, 이메일, 비밀번호).andExpect(jsonPath(AMOUNT).value(입금할_돈 - 출금할_돈 * 요청_횟수)), - () -> assertEquals(objectMapper.readValue(계좌_조회_요청(나의계좌, 이메일, 비밀번호).andReturn().getResponse() - .getContentAsByteArray(), HistoryResponses.class).historyResponses().size(), 요청_횟수 + 1) - ); + var succeededDepositTime = getHistoryResponses(나의계좌, 이메일).historyResponses().size() - 1; + assertAll( + () -> 계좌_조회_요청(나의계좌, 이메일, 비밀번호).andExpect(jsonPath(AMOUNT).value(입금할_돈 - 출금할_돈 * succeededDepositTime)), + ()-> assertThat( getHistoryResponses(나의계좌, 이메일).balance().getAmount()) + .isGreaterThan(900_000L) + .isLessThan(1_000_000L) + ); } private void 계좌_이체_여러번_요청(String fromAccountNumber, String toAccountNumber, long transferMoney, int times, @@ -296,9 +306,10 @@ void withdraw_concurrency_10times() throws Exception { executorService.execute(() -> { try { 계좌_이체_요청(fromAccountNumber, toAccountNumber, transferMoney, username, password); - latch.countDown(); } catch (Exception e) { throw new RuntimeException(e); + } finally { + latch.countDown(); } }); } @@ -355,4 +366,9 @@ void withdraw_concurrency_10times() throws Exception { .accept(MediaType.APPLICATION_JSON) ); } + + private HistoryResponses getHistoryResponses(String account, String email) throws Exception { + return objectMapper.readValue(계좌_조회_요청(account, email, 비밀번호).andReturn().getResponse() + .getContentAsByteArray(), HistoryResponses.class); + } } From bda648fad4bdefee3ce31fe37d58b24697846452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= Date: Wed, 17 Jan 2024 09:25:54 +0900 Subject: [PATCH 08/14] =?UTF-8?q?Refactor=20:=20=EC=A0=95=EC=83=81?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EB=8F=99=EC=9E=91=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ConcurrencyFacade.java | 3 - .../banking/domain/AccountService.java | 6 +- .../banking/infra/JpaAccountRepository.java | 3 +- .../ConcurrencyManagerWithNamedLock.java | 147 +++++++++++------- 4 files changed, 96 insertions(+), 63 deletions(-) diff --git a/src/main/java/bankingapi/banking/application/ConcurrencyFacade.java b/src/main/java/bankingapi/banking/application/ConcurrencyFacade.java index 510a3b1..e0bee5c 100644 --- a/src/main/java/bankingapi/banking/application/ConcurrencyFacade.java +++ b/src/main/java/bankingapi/banking/application/ConcurrencyFacade.java @@ -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); diff --git a/src/main/java/bankingapi/banking/domain/AccountService.java b/src/main/java/bankingapi/banking/domain/AccountService.java index b76a235..5f82d73 100644 --- a/src/main/java/bankingapi/banking/domain/AccountService.java +++ b/src/main/java/bankingapi/banking/domain/AccountService.java @@ -1,14 +1,12 @@ package bankingapi.banking.domain; -import java.util.Collection; -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 diff --git a/src/main/java/bankingapi/banking/infra/JpaAccountRepository.java b/src/main/java/bankingapi/banking/infra/JpaAccountRepository.java index 42682fa..413e61b 100644 --- a/src/main/java/bankingapi/banking/infra/JpaAccountRepository.java +++ b/src/main/java/bankingapi/banking/infra/JpaAccountRepository.java @@ -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; @@ -23,7 +24,7 @@ public interface JpaAccountRepository extends JpaRepository, Acco @Lock(LockModeType.OPTIMISTIC) @Query("select a from Account a where a.accountNumber = :accountNumber") - Optional findByAccountNumberWithOptimisticLock(AccountNumber accountNumber); + Optional findByAccountNumberWithOptimisticLock(@Param("accountNumber") AccountNumber accountNumber); @Override S save(S entity); diff --git a/src/main/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLock.java b/src/main/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLock.java index a7c3f68..0be1f03 100644 --- a/src/main/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLock.java +++ b/src/main/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLock.java @@ -1,71 +1,108 @@ 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 = 2; + 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 dataSource; + + @Override + public void executeWithLock(String lockName1, String lockName2, Runnable runnable) { + try(var connection = dataSource.getConnection()) { + getLock(connection, getMultiLockName(lockName1, lockName2)); + getLock(connection, lockName1); + getLock(connection, lockName2); + runnable.run(); + releaseLock(connection, lockName2); + releaseLock(connection, lockName1); + releaseLock(connection, getMultiLockName(lockName1, lockName2)); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + releaseSessionLocks(); + } + } + + @Override + public void executeWithLock(String lockName, Runnable runnable) { + try(var connection = dataSource.getConnection()) { + getLock(connection, lockName); + runnable.run(); + releaseLock(connection, lockName); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + releaseSessionLocks(); + } + } + + private void releaseLock(Connection connection, String lockName) { + try (var preparedStatement = connection.prepareStatement(RELEASE_LOCK)) { + preparedStatement.setString(1, lockName); + var resultSet = preparedStatement.executeQuery(); + validateResult(resultSet, lockName, "ReleaseLock"); + } catch (SQLException e) { + log.error("ReleaseLock_{} : {}", lockName, e.getMessage()); + throw new IllegalStateException("SQL Exception"); + } + } + + private void getLock(Connection connection, String userLockName) { + try (var preparedStatement = connection.prepareStatement(GET_LOCK)) { + preparedStatement.setString(1, userLockName); + preparedStatement.setInt(2, TIMEOUT_SECONDS); - @Override - public void executeWithLock(String lockName1, String lockName2, Runnable runnable) { - try { - getLock(lockName1); - getLock(lockName2); - runnable.run(); - } finally { - releaseSessionLocks(); - } - } + synchronized (this) { + var resultSet = preparedStatement.executeQuery(); + validateResult(resultSet, userLockName, "GetLock"); + } - @Override - public void executeWithLock(String lockName, Runnable runnable) { - try { - getLock(lockName); - runnable.run(); - } finally { - releaseSessionLocks(); - } - } + } catch (SQLException e) { + log.error("GetLock_{} : {}", userLockName, e.getMessage()); + throw new IllegalStateException("SQL Exception"); + } + } - private void getLock(String userLockName) { - Map 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 params = new HashMap<>(); + namedParameterJdbcTemplate.queryForObject(RELEASE_SESSION_LOCKS, params, Integer.class); + } - private void releaseSessionLocks() { - Map 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(); + } } From 6812944b05891db58950707258991470a8ff9d63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= Date: Wed, 17 Jan 2024 10:00:33 +0900 Subject: [PATCH 09/14] =?UTF-8?q?Refactor=20:=20latch.countDown()=20?= =?UTF-8?q?=EC=9D=B4=20=EB=8F=99=EC=9E=91=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../concurrency/ConcurrencyManagerWithNamedLockTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java b/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java index fdf0791..ddd3a34 100644 --- a/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java +++ b/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java @@ -63,9 +63,9 @@ void calculateAtSameTime_controllingConcurrency() throws InterruptedException { for (int i = 0; i < NUMBER_OF_THREADS; i++) { service.execute(() -> { concurrencyManager.executeWithLock("lock1", "lock2", () -> { - account.deposit(new Money(1)); + account.deposit(new Money(1)); latch.countDown(); - } + } ); }); } From aae5a863164fd3bc21cdc2f6f2ff1889d5c63a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= Date: Wed, 17 Jan 2024 10:03:01 +0900 Subject: [PATCH 10/14] =?UTF-8?q?Refactor=20:=20latch.countDown()=20?= =?UTF-8?q?=EC=9D=B4=20=EB=8F=99=EC=9E=91=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../concurrency/ConcurrencyManagerWithNamedLockTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java b/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java index ddd3a34..fdf0791 100644 --- a/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java +++ b/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java @@ -63,9 +63,9 @@ void calculateAtSameTime_controllingConcurrency() throws InterruptedException { for (int i = 0; i < NUMBER_OF_THREADS; i++) { service.execute(() -> { concurrencyManager.executeWithLock("lock1", "lock2", () -> { - account.deposit(new Money(1)); + account.deposit(new Money(1)); latch.countDown(); - } + } ); }); } From 202d483aa26468dce0f26d995417ba72ffc1421d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= Date: Wed, 17 Jan 2024 10:05:02 +0900 Subject: [PATCH 11/14] =?UTF-8?q?Refactor=20:=20BankingAcceptanceTest=20?= =?UTF-8?q?=EC=9E=98=EB=AA=BB=EB=90=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acceptance/BankingAcceptanceTest.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/test/java/bankingapi/acceptance/BankingAcceptanceTest.java b/src/test/java/bankingapi/acceptance/BankingAcceptanceTest.java index c25083b..26be4d8 100644 --- a/src/test/java/bankingapi/acceptance/BankingAcceptanceTest.java +++ b/src/test/java/bankingapi/acceptance/BankingAcceptanceTest.java @@ -146,12 +146,15 @@ void transfer_concurrency_50_times() throws Exception { 계좌_이체_여러번_요청(나의계좌, 상대방계좌, 출금할_돈, 요청_횟수, 이메일, 비밀번호); // then + var transferTimes = getHistoryResponses(나의계좌, 이메일).historyResponses().size() - 1; assertAll( - () -> - 계좌_조회_요청(나의계좌, 이메일, 비밀번호).andExpect(jsonPath(AMOUNT) - .value(입금할_돈 - 출금할_돈 * 요청_횟수)), - () -> 계좌_조회_요청(상대방계좌, 어드민이메일, 비밀번호).andExpect(jsonPath(AMOUNT) - .value(출금할_돈 * 요청_횟수)) + () -> assertEquals(getHistoryResponses(나의계좌, 이메일).balance().getAmount() + + getHistoryResponses(상대방계좌, 어드민이메일).balance().getAmount(), 백만원), + () -> assertThat(getHistoryResponses(상대방계좌, 어드민이메일).historyResponses().size()) + .isEqualTo(transferTimes), + () -> assertThat(getHistoryResponses(나의계좌, 이메일).balance().getAmount()) + .isGreaterThan(950_000L) + .isLessThan(1_000_000L) ); } @@ -245,7 +248,7 @@ void deposit_concurrency_10times() throws Exception { () -> 계좌_조회_요청(나의계좌, 이메일, 비밀번호).andExpect(jsonPath(AMOUNT).value(입금할_돈 * successDepositTime)), ()-> assertThat( getHistoryResponses(나의계좌, 이메일).balance().getAmount()) .isGreaterThan(0L) - .isLessThan(10_000L) + .isLessThan(100_000L) );} From 7afaf368395df72823cb7755288e4bfe9493b2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= Date: Wed, 17 Jan 2024 10:07:30 +0900 Subject: [PATCH 12/14] =?UTF-8?q?Refactor=20:=20latch.countDown()=20?= =?UTF-8?q?=EC=9D=B4=20=EB=8F=99=EC=9E=91=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ConcurrencyManagerWithNamedLockTest.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java b/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java index fdf0791..35dfd12 100644 --- a/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java +++ b/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java @@ -18,7 +18,7 @@ @SpringBootTest class ConcurrencyManagerWithNamedLockTest { - private static final int NUMBER_OF_THREADS = 100; + private static final int NUMBER_OF_THREADS = 10; private static final int POLL_SIZE = 10; @Autowired private ConcurrencyManager concurrencyManager; @@ -62,11 +62,14 @@ void calculateAtSameTime_controllingConcurrency() throws InterruptedException { for (int i = 0; i < NUMBER_OF_THREADS; i++) { service.execute(() -> { - concurrencyManager.executeWithLock("lock1", "lock2", () -> { - account.deposit(new Money(1)); - latch.countDown(); - } - ); + try { + concurrencyManager.executeWithLock("lock1", "lock2", () -> { + account.deposit(new Money(1)); + } + ); + }finally { + latch.countDown(); + } }); } From 4323b4354f2c9387887703b6d38d977a4a0f6f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= Date: Wed, 17 Jan 2024 10:22:53 +0900 Subject: [PATCH 13/14] =?UTF-8?q?Refactor=20:=20timeout=20second=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bankingapi/concurrency/ConcurrencyManagerWithNamedLock.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLock.java b/src/main/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLock.java index 0be1f03..fca096d 100644 --- a/src/main/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLock.java +++ b/src/main/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLock.java @@ -22,7 +22,7 @@ public class ConcurrencyManagerWithNamedLock implements ConcurrencyManager { 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 = 2; + 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; From 01833f739e6c55c96ed6a648bd85edde3138cc33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= Date: Wed, 17 Jan 2024 11:38:12 +0900 Subject: [PATCH 14/14] =?UTF-8?q?Refactor=20:=20test=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ConcurrencyManagerWithNamedLock.java | 82 +++++++++++-------- .../util/config/DatasourceConfiguration.java | 26 ++++++ src/main/resources/application-local.yml | 12 --- src/main/resources/application-test.yml | 43 ++++++++++ src/main/resources/application.yml | 31 ++++++- .../BankingApiApplicationTests.java | 2 + .../ConcurrencyManagerWithNamedLockTest.java | 16 ++-- .../IdempotentRequestHistoryServiceTest.java | 2 + .../CustomUserDetailServiceTest.java | 2 + .../MemberApplicationServiceTest.java | 2 + .../social/domain/FriendServiceTest.java | 2 + .../domain/SocialNetworkServiceTest.java | 2 + src/test/resources/application.yml | 16 ---- 13 files changed, 171 insertions(+), 67 deletions(-) create mode 100644 src/main/java/bankingapi/util/config/DatasourceConfiguration.java create mode 100644 src/main/resources/application-test.yml delete mode 100644 src/test/resources/application.yml diff --git a/src/main/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLock.java b/src/main/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLock.java index fca096d..4507875 100644 --- a/src/main/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLock.java +++ b/src/main/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLock.java @@ -26,61 +26,79 @@ public class ConcurrencyManagerWithNamedLock implements ConcurrencyManager { 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 dataSource; + private final DataSource userLoackDataSource; @Override public void executeWithLock(String lockName1, String lockName2, Runnable runnable) { - try(var connection = dataSource.getConnection()) { - getLock(connection, getMultiLockName(lockName1, lockName2)); - getLock(connection, lockName1); - getLock(connection, lockName2); - runnable.run(); - releaseLock(connection, lockName2); - releaseLock(connection, lockName1); - releaseLock(connection, getMultiLockName(lockName1, lockName2)); + 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); - } finally { - releaseSessionLocks(); } } @Override public void executeWithLock(String lockName, Runnable runnable) { - try(var connection = dataSource.getConnection()) { + try (var connection = userLoackDataSource.getConnection()) { + log.info("start getLock=[{}], timeoutSeconds : [{}], connection=[{}]", lockName, TIMEOUT_SECONDS, connection); getLock(connection, lockName); - runnable.run(); - releaseLock(connection, lockName); + try { + runnable.run(); + } finally { + log.info("start releaseLock, connection=[{}]", connection); + releaseLock(connection, lockName); + } } catch (SQLException e) { throw new RuntimeException(e); - } finally { - releaseSessionLocks(); } } - private void releaseLock(Connection connection, String lockName) { - try (var preparedStatement = connection.prepareStatement(RELEASE_LOCK)) { - preparedStatement.setString(1, lockName); + 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, lockName, "ReleaseLock"); + validateResult(resultSet, userLockName, "GetLock"); } catch (SQLException e) { - log.error("ReleaseLock_{} : {}", lockName, e.getMessage()); + log.error("GetLock_{} : {}", userLockName, e.getMessage()); throw new IllegalStateException("SQL Exception"); } } - - private void getLock(Connection connection, String userLockName) { - try (var preparedStatement = connection.prepareStatement(GET_LOCK)) { + private void releaseLock(Connection connection, String userLockName) { + try (var preparedStatement = connection.prepareStatement(RELEASE_LOCK)) { preparedStatement.setString(1, userLockName); - preparedStatement.setInt(2, TIMEOUT_SECONDS); - - synchronized (this) { - var resultSet = preparedStatement.executeQuery(); - validateResult(resultSet, userLockName, "GetLock"); - } - + preparedStatement.executeQuery(); } catch (SQLException e) { - log.error("GetLock_{} : {}", userLockName, e.getMessage()); + 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"); } } diff --git a/src/main/java/bankingapi/util/config/DatasourceConfiguration.java b/src/main/java/bankingapi/util/config/DatasourceConfiguration.java new file mode 100644 index 0000000..1e6e28b --- /dev/null +++ b/src/main/java/bankingapi/util/config/DatasourceConfiguration.java @@ -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(); + } +} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index f5e3ce9..e69de29 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,12 +0,0 @@ -spring: - datasource: - url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/money_transfer_service?characterEncoding=UTF-8&serverTimezone=Asia/Seoul} - driver-class-name: com.mysql.cj.jdbc.Driver - username: ${SPRING_DATASOURCE_USERNAME:root} - password: ${SPRING_DATASOURCE_PASSWORD:password!} - jpa: - database: mysql - database-platform: org.hibernate.dialect.MySQLDialect - show-sql: true - hibernate: - ddl-auto: create diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 0000000..7c4ee42 --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,43 @@ +spring: + datasource: + hikari: + maximum-pool-size: 20 + max-lifetime: 60000 + driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver + jdbc-url: jdbc:tc:mysql:8.0.24://localhost:3306/test + connection-timeout: 5000 + pool-name: Spring-HikariPool + dbcp2: + driver-class-name: com.mysql.cj.jdbc.Driver + test-on-borrow: true + validation-query: SELECT 1 + jpa: + show-sql: true + hibernate: + ddl-auto: create + generate-ddl: true + jdbc: + template: + query-timeout: 2 + +userlock: + datasource: + hikari: + maximum-pool-size: 20 + max-lifetime: 60000 + driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver + jdbc-url: jdbc:tc:mysql:8.0.24://localhost:3306/test + connection-timeout: 5000 + pool-name: UserLock-HikariPool + +logging: + level: + org.hibernate: + SQL: debug + tool.hbm2ddl: debug + type: trace + stat: debug + type.BasicTypeRegistry: warn + org.springframework.jdbc: debug + org.springframework.transaction: debug + bankingapi.concurrency: debug diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d74c444..b448652 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,30 @@ spring: - profiles: - active: local + datasource: + hikari: + maximum-pool-size: 20 + max-lifetime: 60000 + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/money_transfer_service?characterEncoding=UTF-8&serverTimezone=Asia/Seoul} + username: ${SPRING_DATASOURCE_USERNAME:root} + password: ${SPRING_DATASOURCE_PASSWORD:password!} + connection-timeout: 5000 + pool-name: Spring-HikariPool + + jpa: + database: mysql + database-platform: org.hibernate.dialect.MySQLDialect + show-sql: true + hibernate: + ddl-auto: create + +userlock: + datasource: + hikari: + maximum-pool-size: 20 + max-lifetime: 60000 + jdbc-url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/money_transfer_service?characterEncoding=UTF-8&serverTimezone=Asia/Seoul} + username: ${SPRING_DATASOURCE_USERNAME:root} + password: ${SPRING_DATASOURCE_PASSWORD:password!} + driver-class-name: com.mysql.cj.jdbc.Driver + connection-timeout: 5000 + pool-name: UserLock-HikariPool diff --git a/src/test/java/bankingapi/BankingApiApplicationTests.java b/src/test/java/bankingapi/BankingApiApplicationTests.java index ee9624f..ead98d5 100644 --- a/src/test/java/bankingapi/BankingApiApplicationTests.java +++ b/src/test/java/bankingapi/BankingApiApplicationTests.java @@ -2,7 +2,9 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +@ActiveProfiles("test") @SpringBootTest class BankingApiApplicationTests { diff --git a/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java b/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java index 35dfd12..35c7131 100644 --- a/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java +++ b/src/test/java/bankingapi/concurrency/ConcurrencyManagerWithNamedLockTest.java @@ -2,8 +2,10 @@ import static org.assertj.core.api.Assertions.*; +import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; +import java.util.random.RandomGenerator; import bankingapi.concurrency.ConcurrencyManager; import org.junit.jupiter.api.BeforeEach; @@ -15,11 +17,15 @@ import bankingapi.banking.domain.Account; import bankingapi.banking.domain.Money; import bankingapi.util.generator.AccountNumberGenerator; +import org.springframework.context.annotation.Profile; +import org.springframework.test.context.ActiveProfiles; +@ActiveProfiles("test") @SpringBootTest class ConcurrencyManagerWithNamedLockTest { private static final int NUMBER_OF_THREADS = 10; private static final int POLL_SIZE = 10; + private static final Money ONE = new Money(1); @Autowired private ConcurrencyManager concurrencyManager; private CountDownLatch latch; @@ -63,11 +69,11 @@ void calculateAtSameTime_controllingConcurrency() throws InterruptedException { for (int i = 0; i < NUMBER_OF_THREADS; i++) { service.execute(() -> { try { - concurrencyManager.executeWithLock("lock1", "lock2", () -> { - account.deposit(new Money(1)); - } - ); - }finally { + Thread.sleep(RandomGenerator.getDefault().nextInt(0, 50)); + concurrencyManager.executeWithLock("lock1", "lock2", () -> account.deposit(ONE)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { latch.countDown(); } }); diff --git a/src/test/java/bankingapi/idempotent/domain/IdempotentRequestHistoryServiceTest.java b/src/test/java/bankingapi/idempotent/domain/IdempotentRequestHistoryServiceTest.java index 9bcc795..e19a867 100644 --- a/src/test/java/bankingapi/idempotent/domain/IdempotentRequestHistoryServiceTest.java +++ b/src/test/java/bankingapi/idempotent/domain/IdempotentRequestHistoryServiceTest.java @@ -13,7 +13,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +@ActiveProfiles("test") @SpringBootTest class IdempotentRequestHistoryServiceTest { diff --git a/src/test/java/bankingapi/member/application/CustomUserDetailServiceTest.java b/src/test/java/bankingapi/member/application/CustomUserDetailServiceTest.java index 826c017..163de07 100644 --- a/src/test/java/bankingapi/member/application/CustomUserDetailServiceTest.java +++ b/src/test/java/bankingapi/member/application/CustomUserDetailServiceTest.java @@ -11,12 +11,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import bankingapi.member.domain.Member; import bankingapi.member.domain.MemberRepository; import bankingapi.member.exception.NotExistMemberException; +@ActiveProfiles("test") @Transactional @SpringBootTest class CustomUserDetailServiceTest { diff --git a/src/test/java/bankingapi/member/application/MemberApplicationServiceTest.java b/src/test/java/bankingapi/member/application/MemberApplicationServiceTest.java index c3182d4..7f11429 100644 --- a/src/test/java/bankingapi/member/application/MemberApplicationServiceTest.java +++ b/src/test/java/bankingapi/member/application/MemberApplicationServiceTest.java @@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import bankingapi.member.domain.Member; @@ -16,6 +17,7 @@ import bankingapi.member.dto.RegisterCommand; import bankingapi.member.exception.NotExistMemberException; +@ActiveProfiles("test") @Transactional @SpringBootTest class MemberApplicationServiceTest { diff --git a/src/test/java/bankingapi/social/domain/FriendServiceTest.java b/src/test/java/bankingapi/social/domain/FriendServiceTest.java index f7d6812..bce86d1 100644 --- a/src/test/java/bankingapi/social/domain/FriendServiceTest.java +++ b/src/test/java/bankingapi/social/domain/FriendServiceTest.java @@ -10,8 +10,10 @@ import org.junit.jupiter.params.provider.EnumSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; +@ActiveProfiles("test") @Transactional @SpringBootTest class FriendServiceTest { diff --git a/src/test/java/bankingapi/social/domain/SocialNetworkServiceTest.java b/src/test/java/bankingapi/social/domain/SocialNetworkServiceTest.java index e267350..06980c4 100644 --- a/src/test/java/bankingapi/social/domain/SocialNetworkServiceTest.java +++ b/src/test/java/bankingapi/social/domain/SocialNetworkServiceTest.java @@ -9,11 +9,13 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import bankingapi.member.domain.Member; import bankingapi.member.domain.MemberRepository; +@ActiveProfiles("test") @Transactional @SpringBootTest class SocialNetworkServiceTest { diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml deleted file mode 100644 index 2feca6c..0000000 --- a/src/test/resources/application.yml +++ /dev/null @@ -1,16 +0,0 @@ -spring: - datasource: - driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver - url: jdbc:tc:mysql:8.0.24://localhost:3306/test - dbcp2: - driver-class-name: com.mysql.cj.jdbc.Driver - test-on-borrow: true - validation-query: SELECT 1 - jpa: - show-sql: true - hibernate: - ddl-auto: create - generate-ddl: true - jdbc: - template: - query-timeout: 2