From 02cdc49dd1fa9d7356467c51b33385a507b40216 Mon Sep 17 00:00:00 2001 From: Kang Dong Hyeon Date: Thu, 14 Aug 2025 17:07:35 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat=20:=20=EB=AA=A8=EB=8B=88=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account-service/account-service/build.gradle | 5 +++++ .../configuration/SecurityConfig.java | 4 +++- .../src/main/resources/application-local.yml | 6 ++++++ .../src/main/resources/application.yml | 21 +++++++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/account-service/account-service/build.gradle b/account-service/account-service/build.gradle index 84f58e4..c23ee60 100644 --- a/account-service/account-service/build.gradle +++ b/account-service/account-service/build.gradle @@ -4,6 +4,11 @@ repositories { dependencies { implementation project(':account-service-api') + + //Spring Actuator + implementation 'org.springframework.boot:spring-boot-starter-actuator' + //Spring Prometheus + implementation 'io.micrometer:micrometer-registry-prometheus' } def generated = 'src/main/generated' diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/configuration/SecurityConfig.java b/account-service/account-service/src/main/java/com/synapse/account_service/configuration/SecurityConfig.java index f514a5a..820353d 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/configuration/SecurityConfig.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/configuration/SecurityConfig.java @@ -57,7 +57,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticat .authorizeHttpRequests(auth -> auth .requestMatchers("/api/accounts/signup", "/api/accounts/login", "/", - "/api/accounts/token/reissue") + "/api/accounts/token/reissue", + "/actuator/health", "/actuator/info", + "/actuator/prometheus", "/actuator/metrics", "/actuator/mappings") .permitAll() .anyRequest().authenticated()) .addFilterAt(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) diff --git a/account-service/account-service/src/main/resources/application-local.yml b/account-service/account-service/src/main/resources/application-local.yml index a5583f6..6409229 100644 --- a/account-service/account-service/src/main/resources/application-local.yml +++ b/account-service/account-service/src/main/resources/application-local.yml @@ -4,6 +4,8 @@ spring: url: jdbc:postgresql://${local-db.postgres.host}:${local-db.postgres.port}/${local-db.postgres.name} username: ${local-db.postgres.username} password: ${local-db.postgres.password} + hikari: + maximum-pool-size: 30 jpa: properties: @@ -15,6 +17,10 @@ spring: hbm2ddl: auto: create dialect: org.hibernate.dialect.PostgreSQLDialect + jdbc: + batch_size: 30 + order_inserts: true + order_updates: true open-in-view: false show-sql: true diff --git a/account-service/account-service/src/main/resources/application.yml b/account-service/account-service/src/main/resources/application.yml index c9a94eb..838e6dc 100644 --- a/account-service/account-service/src/main/resources/application.yml +++ b/account-service/account-service/src/main/resources/application.yml @@ -1,5 +1,8 @@ server: port: 1001 + tomcat: + mbeanregistry: + enabled: true spring: main: @@ -16,3 +19,21 @@ spring: - security/application-db.yml - security/application-jwt.yml - security/application-oauth2.yml + +management: + server: + port: 6001 + endpoints: + web: + exposure: + include: health,info,prometheus,mappings, metrics + endpoint: + health: + show-details: always + prometheus: + metrics: + export: + enabled: true + metrics: + tags: + application: ${spring.application.name} From b9633b39ec3aae3480ce0143beb309330d624fb5 Mon Sep 17 00:00:00 2001 From: Kang Dong Hyeon Date: Thu, 14 Aug 2025 18:44:35 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20api=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account_service/AccountServiceConfig.java | 16 ++++++++ .../controller/AccountController.java | 15 +++++++- .../service/AccountService.java | 9 +++-- .../controller/AccountControllerTest.java | 36 +++++++++--------- .../service/AccountServiceTest.java | 38 +++++++++---------- 5 files changed, 72 insertions(+), 42 deletions(-) diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/AccountServiceConfig.java b/account-service/account-service/src/main/java/com/synapse/account_service/AccountServiceConfig.java index b1065a5..4dcc2cd 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/AccountServiceConfig.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/AccountServiceConfig.java @@ -1,12 +1,28 @@ package com.synapse.account_service; +import java.util.concurrent.Executor; + import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +@EnableAsync @EnableJpaAuditing @ComponentScan @EnableAutoConfiguration public class AccountServiceConfig { + @Bean(name = "signupTaskExecutor") + public Executor signupTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(10); + executor.setMaxPoolSize(20); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("Signup-"); + executor.initialize(); + return executor; + } } diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java b/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java index 1509e4d..8e7b34a 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java @@ -1,6 +1,9 @@ package com.synapse.account_service.controller; import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +import java.util.concurrent.CompletableFuture; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -22,7 +25,15 @@ public class AccountController { private final AccountService accountService; @PostMapping("/signup") - public ResponseEntity signUp(@Valid @RequestBody SignUpRequest request) { - return ResponseEntity.status(CREATED).body(accountService.registerMember(request)); + public CompletableFuture> signUp(@Valid @RequestBody SignUpRequest request) { + CompletableFuture futureResponse = accountService.registerMember(request); + + return futureResponse + .thenApply( + responseBody -> ResponseEntity.status(CREATED).body(responseBody) + ) + .exceptionally(ex -> { + return ResponseEntity.status(INTERNAL_SERVER_ERROR).build(); + }); } } diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java b/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java index 9732e94..fb51615 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java @@ -4,7 +4,9 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.springframework.scheduling.annotation.Async; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,8 +36,9 @@ public class AccountService { private final PasswordEncoder passwordEncoder; // private final MemberDomainEventPublisher memberDomainEventPublisher; + @Async("signupTaskExecutor") @Transactional - public SignUpResponse registerMember(SignUpRequest request) { + public CompletableFuture registerMember(SignUpRequest request) { String encodedPassword = passwordEncoder.encode(request.password()); @@ -60,12 +63,12 @@ public SignUpResponse registerMember(SignUpRequest request) { // memberDomainEventPublisher.publish(memberResult, memberAndEvents.events); - return new SignUpResponse( + return CompletableFuture.completedFuture(new SignUpResponse( memberResult.getId(), memberResult.getEmail(), memberResult.getUsername(), memberResult.getRole().name() - ); + )); } private void createAndSetDefaultSubscription(Member member) { diff --git a/account-service/account-service/src/test/java/com/synapse/account_service/controller/AccountControllerTest.java b/account-service/account-service/src/test/java/com/synapse/account_service/controller/AccountControllerTest.java index 96a40d4..947db05 100644 --- a/account-service/account-service/src/test/java/com/synapse/account_service/controller/AccountControllerTest.java +++ b/account-service/account-service/src/test/java/com/synapse/account_service/controller/AccountControllerTest.java @@ -35,26 +35,26 @@ public class AccountControllerTest extends TestConfig { @MockitoBean private AccountService accountService; - @Test - @DisplayName("회원가입 API 호출 성공") - void signUpApi_success() throws Exception { - // given - UUID expectedId = UUID.randomUUID(); - SignUpRequest request = new SignUpRequest("test@example.com", "유저", "password1234"); - SignUpResponse response = new SignUpResponse(expectedId, "test@example.com", "유저", "USER"); + // @Test + // @DisplayName("회원가입 API 호출 성공") + // void signUpApi_success() throws Exception { + // // given + // UUID expectedId = UUID.randomUUID(); + // SignUpRequest request = new SignUpRequest("test@example.com", "유저", "password1234"); + // SignUpResponse response = new SignUpResponse(expectedId, "test@example.com", "유저", "USER"); - given(accountService.registerMember(any(SignUpRequest.class))).willReturn(response); + // given(accountService.registerMember(any(SignUpRequest.class))).willReturn(response); - // when & then - mockMvc.perform(post("/api/accounts/signup") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.id").value(expectedId.toString())) - .andExpect(jsonPath("$.email").value("test@example.com")) - .andExpect(jsonPath("$.username").value("유저")) - .andExpect(jsonPath("$.role").value("USER")); - } + // // when & then + // mockMvc.perform(post("/api/accounts/signup") + // .contentType(MediaType.APPLICATION_JSON) + // .content(objectMapper.writeValueAsString(request))) + // .andExpect(status().isCreated()) + // .andExpect(jsonPath("$.id").value(expectedId.toString())) + // .andExpect(jsonPath("$.email").value("test@example.com")) + // .andExpect(jsonPath("$.username").value("유저")) + // .andExpect(jsonPath("$.role").value("USER")); + // } @Test @DisplayName("이메일 중복 시 409 Conflict 응답") diff --git a/account-service/account-service/src/test/java/com/synapse/account_service/service/AccountServiceTest.java b/account-service/account-service/src/test/java/com/synapse/account_service/service/AccountServiceTest.java index a5d6608..3d7ad64 100644 --- a/account-service/account-service/src/test/java/com/synapse/account_service/service/AccountServiceTest.java +++ b/account-service/account-service/src/test/java/com/synapse/account_service/service/AccountServiceTest.java @@ -34,29 +34,29 @@ public class AccountServiceTest extends TestConfig { @Mock private PasswordEncoder passwordEncoder; - @Test - @DisplayName("회원가입 성공") - void signUp_success() { - // given: 테스트 준비 - SignUpRequest request = new SignUpRequest("test@example.com", "테스트유저", "password123"); + // @Test + // @DisplayName("회원가입 성공") + // void signUp_success() { + // // given: 테스트 준비 + // SignUpRequest request = new SignUpRequest("test@example.com", "테스트유저", "password123"); - given(memberRepository.findByUsernameAndEmail(anyString(), anyString())).willReturn(Optional.empty()); - given(passwordEncoder.encode(anyString())).willReturn("encodedPassword"); - given(memberRepository.save(any(Member.class))).willAnswer(invocation -> { - Member memberToSave = invocation.getArgument(0); - return memberToSave; - }); + // given(memberRepository.findByUsernameAndEmail(anyString(), anyString())).willReturn(Optional.empty()); + // given(passwordEncoder.encode(anyString())).willReturn("encodedPassword"); + // given(memberRepository.save(any(Member.class))).willAnswer(invocation -> { + // Member memberToSave = invocation.getArgument(0); + // return memberToSave; + // }); - // when: 실제 테스트할 메서드 호출 - SignUpResponse response = accountService.registerMember(request); + // // when: 실제 테스트할 메서드 호출 + // SignUpResponse response = accountService.registerMember(request); - // then: 결과 검증 - assertThat(response.email()).isEqualTo("test@example.com"); - assertThat(response.username()).isEqualTo("테스트유저"); + // // then: 결과 검증 + // assertThat(response.email()).isEqualTo("test@example.com"); + // assertThat(response.username()).isEqualTo("테스트유저"); - verify(passwordEncoder).encode("password123"); - verify(memberRepository).save(any(Member.class)); - } + // verify(passwordEncoder).encode("password123"); + // verify(memberRepository).save(any(Member.class)); + // } @Test @DisplayName("이메일 또는 사용자명 중복으로 회원가입 실패") From b10b1403ca5b34c4446aaaa0a86f9ec74347a82e Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Thu, 14 Aug 2025 22:26:17 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix=20:=20=EB=B9=84=EB=8F=99=EA=B8=B0=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=B6=80=EB=B6=84=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account_service/AccountServiceConfig.java | 14 ------- .../configuration/AsyncConfig.java | 21 ++++++++++ .../controller/AccountController.java | 15 +------- .../service/AccountService.java | 7 ++-- .../controller/AccountControllerTest.java | 36 +++++++++--------- .../service/AccountServiceTest.java | 38 +++++++++---------- 6 files changed, 63 insertions(+), 68 deletions(-) create mode 100644 account-service/account-service/src/main/java/com/synapse/account_service/configuration/AsyncConfig.java diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/AccountServiceConfig.java b/account-service/account-service/src/main/java/com/synapse/account_service/AccountServiceConfig.java index 4dcc2cd..a68467c 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/AccountServiceConfig.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/AccountServiceConfig.java @@ -1,13 +1,9 @@ package com.synapse.account_service; -import java.util.concurrent.Executor; - import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @EnableAsync @EnableJpaAuditing @@ -15,14 +11,4 @@ @EnableAutoConfiguration public class AccountServiceConfig { - @Bean(name = "signupTaskExecutor") - public Executor signupTaskExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(10); - executor.setMaxPoolSize(20); - executor.setQueueCapacity(100); - executor.setThreadNamePrefix("Signup-"); - executor.initialize(); - return executor; - } } diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/configuration/AsyncConfig.java b/account-service/account-service/src/main/java/com/synapse/account_service/configuration/AsyncConfig.java new file mode 100644 index 0000000..0c43345 --- /dev/null +++ b/account-service/account-service/src/main/java/com/synapse/account_service/configuration/AsyncConfig.java @@ -0,0 +1,21 @@ +package com.synapse.account_service.configuration; + +import java.util.concurrent.Executor; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +public class AsyncConfig { + @Bean(name = "signupTaskExecutor") + public Executor signupTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(10); + executor.setMaxPoolSize(20); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("Signup-"); + executor.initialize(); + return executor; + } +} diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java b/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java index 8e7b34a..1509e4d 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java @@ -1,9 +1,6 @@ package com.synapse.account_service.controller; import static org.springframework.http.HttpStatus.CREATED; -import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; - -import java.util.concurrent.CompletableFuture; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -25,15 +22,7 @@ public class AccountController { private final AccountService accountService; @PostMapping("/signup") - public CompletableFuture> signUp(@Valid @RequestBody SignUpRequest request) { - CompletableFuture futureResponse = accountService.registerMember(request); - - return futureResponse - .thenApply( - responseBody -> ResponseEntity.status(CREATED).body(responseBody) - ) - .exceptionally(ex -> { - return ResponseEntity.status(INTERNAL_SERVER_ERROR).build(); - }); + public ResponseEntity signUp(@Valid @RequestBody SignUpRequest request) { + return ResponseEntity.status(CREATED).body(accountService.registerMember(request)); } } diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java b/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java index fb51615..26eb6f9 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java @@ -4,7 +4,6 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.UUID; -import java.util.concurrent.CompletableFuture; import org.springframework.scheduling.annotation.Async; import org.springframework.security.crypto.password.PasswordEncoder; @@ -38,7 +37,7 @@ public class AccountService { @Async("signupTaskExecutor") @Transactional - public CompletableFuture registerMember(SignUpRequest request) { + public SignUpResponse registerMember(SignUpRequest request) { String encodedPassword = passwordEncoder.encode(request.password()); @@ -63,12 +62,12 @@ public CompletableFuture registerMember(SignUpRequest request) { // memberDomainEventPublisher.publish(memberResult, memberAndEvents.events); - return CompletableFuture.completedFuture(new SignUpResponse( + return new SignUpResponse( memberResult.getId(), memberResult.getEmail(), memberResult.getUsername(), memberResult.getRole().name() - )); + ); } private void createAndSetDefaultSubscription(Member member) { diff --git a/account-service/account-service/src/test/java/com/synapse/account_service/controller/AccountControllerTest.java b/account-service/account-service/src/test/java/com/synapse/account_service/controller/AccountControllerTest.java index 947db05..96a40d4 100644 --- a/account-service/account-service/src/test/java/com/synapse/account_service/controller/AccountControllerTest.java +++ b/account-service/account-service/src/test/java/com/synapse/account_service/controller/AccountControllerTest.java @@ -35,26 +35,26 @@ public class AccountControllerTest extends TestConfig { @MockitoBean private AccountService accountService; - // @Test - // @DisplayName("회원가입 API 호출 성공") - // void signUpApi_success() throws Exception { - // // given - // UUID expectedId = UUID.randomUUID(); - // SignUpRequest request = new SignUpRequest("test@example.com", "유저", "password1234"); - // SignUpResponse response = new SignUpResponse(expectedId, "test@example.com", "유저", "USER"); + @Test + @DisplayName("회원가입 API 호출 성공") + void signUpApi_success() throws Exception { + // given + UUID expectedId = UUID.randomUUID(); + SignUpRequest request = new SignUpRequest("test@example.com", "유저", "password1234"); + SignUpResponse response = new SignUpResponse(expectedId, "test@example.com", "유저", "USER"); - // given(accountService.registerMember(any(SignUpRequest.class))).willReturn(response); + given(accountService.registerMember(any(SignUpRequest.class))).willReturn(response); - // // when & then - // mockMvc.perform(post("/api/accounts/signup") - // .contentType(MediaType.APPLICATION_JSON) - // .content(objectMapper.writeValueAsString(request))) - // .andExpect(status().isCreated()) - // .andExpect(jsonPath("$.id").value(expectedId.toString())) - // .andExpect(jsonPath("$.email").value("test@example.com")) - // .andExpect(jsonPath("$.username").value("유저")) - // .andExpect(jsonPath("$.role").value("USER")); - // } + // when & then + mockMvc.perform(post("/api/accounts/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(expectedId.toString())) + .andExpect(jsonPath("$.email").value("test@example.com")) + .andExpect(jsonPath("$.username").value("유저")) + .andExpect(jsonPath("$.role").value("USER")); + } @Test @DisplayName("이메일 중복 시 409 Conflict 응답") diff --git a/account-service/account-service/src/test/java/com/synapse/account_service/service/AccountServiceTest.java b/account-service/account-service/src/test/java/com/synapse/account_service/service/AccountServiceTest.java index 3d7ad64..a5d6608 100644 --- a/account-service/account-service/src/test/java/com/synapse/account_service/service/AccountServiceTest.java +++ b/account-service/account-service/src/test/java/com/synapse/account_service/service/AccountServiceTest.java @@ -34,29 +34,29 @@ public class AccountServiceTest extends TestConfig { @Mock private PasswordEncoder passwordEncoder; - // @Test - // @DisplayName("회원가입 성공") - // void signUp_success() { - // // given: 테스트 준비 - // SignUpRequest request = new SignUpRequest("test@example.com", "테스트유저", "password123"); + @Test + @DisplayName("회원가입 성공") + void signUp_success() { + // given: 테스트 준비 + SignUpRequest request = new SignUpRequest("test@example.com", "테스트유저", "password123"); - // given(memberRepository.findByUsernameAndEmail(anyString(), anyString())).willReturn(Optional.empty()); - // given(passwordEncoder.encode(anyString())).willReturn("encodedPassword"); - // given(memberRepository.save(any(Member.class))).willAnswer(invocation -> { - // Member memberToSave = invocation.getArgument(0); - // return memberToSave; - // }); + given(memberRepository.findByUsernameAndEmail(anyString(), anyString())).willReturn(Optional.empty()); + given(passwordEncoder.encode(anyString())).willReturn("encodedPassword"); + given(memberRepository.save(any(Member.class))).willAnswer(invocation -> { + Member memberToSave = invocation.getArgument(0); + return memberToSave; + }); - // // when: 실제 테스트할 메서드 호출 - // SignUpResponse response = accountService.registerMember(request); + // when: 실제 테스트할 메서드 호출 + SignUpResponse response = accountService.registerMember(request); - // // then: 결과 검증 - // assertThat(response.email()).isEqualTo("test@example.com"); - // assertThat(response.username()).isEqualTo("테스트유저"); + // then: 결과 검증 + assertThat(response.email()).isEqualTo("test@example.com"); + assertThat(response.username()).isEqualTo("테스트유저"); - // verify(passwordEncoder).encode("password123"); - // verify(memberRepository).save(any(Member.class)); - // } + verify(passwordEncoder).encode("password123"); + verify(memberRepository).save(any(Member.class)); + } @Test @DisplayName("이메일 또는 사용자명 중복으로 회원가입 실패") From 4761d5a13671e092280879e080452e4ec661c553 Mon Sep 17 00:00:00 2001 From: Kang Dong Hyeon Date: Mon, 18 Aug 2025 18:51:07 +0900 Subject: [PATCH 4/4] =?UTF-8?q?perf=20:=20refreshToken=EC=9D=84=20db?= =?UTF-8?q?=EC=97=90=EC=84=9C=20redis=EB=A1=9C=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95,=20=20username?= =?UTF-8?q?=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account-service/account-service/build.gradle | 2 + .../configuration/AsyncConfig.java | 21 ---- .../configuration/RedisConfig.java | 32 ++++++ .../configuration/SecurityConfig.java | 2 +- .../controller/AccountController.java | 1 - .../account_service/domain/RefreshToken.java | 18 +++ .../account_service/domain/entity/Member.java | 4 +- .../domain/entity/RefreshToken.java | 31 ----- .../repository/RefreshTokenRepository.java | 11 -- .../service/AccountService.java | 4 +- .../service/CustomUserDetailsService.java | 2 +- .../service/TokenManagementService.java | 38 ++++--- .../src/main/resources/application-local.yml | 16 ++- .../src/main/resources/db/QueryPlan.sql | 3 + .../integrationtest/LoginIntegrationTest.java | 7 +- .../TokenReissueIntegrationTest.java | 17 +-- .../repository/MemberRepositoryTest.java | 4 + .../service/TokenManagementServiceTest.java | 107 ------------------ 18 files changed, 118 insertions(+), 202 deletions(-) delete mode 100644 account-service/account-service/src/main/java/com/synapse/account_service/configuration/AsyncConfig.java create mode 100644 account-service/account-service/src/main/java/com/synapse/account_service/configuration/RedisConfig.java create mode 100644 account-service/account-service/src/main/java/com/synapse/account_service/domain/RefreshToken.java delete mode 100644 account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/RefreshToken.java delete mode 100644 account-service/account-service/src/main/java/com/synapse/account_service/domain/repository/RefreshTokenRepository.java create mode 100644 account-service/account-service/src/main/resources/db/QueryPlan.sql delete mode 100644 account-service/account-service/src/test/java/com/synapse/account_service/service/TokenManagementServiceTest.java diff --git a/account-service/account-service/build.gradle b/account-service/account-service/build.gradle index c23ee60..a0fc512 100644 --- a/account-service/account-service/build.gradle +++ b/account-service/account-service/build.gradle @@ -9,6 +9,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' //Spring Prometheus implementation 'io.micrometer:micrometer-registry-prometheus' + //Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } def generated = 'src/main/generated' diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/configuration/AsyncConfig.java b/account-service/account-service/src/main/java/com/synapse/account_service/configuration/AsyncConfig.java deleted file mode 100644 index 0c43345..0000000 --- a/account-service/account-service/src/main/java/com/synapse/account_service/configuration/AsyncConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.synapse.account_service.configuration; - -import java.util.concurrent.Executor; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; - -@Configuration -public class AsyncConfig { - @Bean(name = "signupTaskExecutor") - public Executor signupTaskExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(10); - executor.setMaxPoolSize(20); - executor.setQueueCapacity(100); - executor.setThreadNamePrefix("Signup-"); - executor.initialize(); - return executor; - } -} diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/configuration/RedisConfig.java b/account-service/account-service/src/main/java/com/synapse/account_service/configuration/RedisConfig.java new file mode 100644 index 0000000..4706933 --- /dev/null +++ b/account-service/account-service/src/main/java/com/synapse/account_service/configuration/RedisConfig.java @@ -0,0 +1,32 @@ +package com.synapse.account_service.configuration; + +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.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.synapse.account_service.domain.RefreshToken; + +@Configuration +public class RedisConfig { + @Bean + RedisTemplate refreshTokenRedisTemplate(RedisConnectionFactory connectionFactory) { + var objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS); + + var template = new RedisTemplate(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new Jackson2JsonRedisSerializer<>(objectMapper, RefreshToken.class)); + + return template; + } +} diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/configuration/SecurityConfig.java b/account-service/account-service/src/main/java/com/synapse/account_service/configuration/SecurityConfig.java index 820353d..cffa34a 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/configuration/SecurityConfig.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/configuration/SecurityConfig.java @@ -33,9 +33,9 @@ public class SecurityConfig { private final CustomUserDetailsService customUserDetailsService; private final LoginSuccessHandler loginSuccessHandler; private final LoginFailureHandler loginFailureHandler; + private final ObjectMapper objectMapper; private final CustomOAuth2UserService customOAuth2UserService; private final CustomOidcUserService customOidcUserService; - private final ObjectMapper objectMapper; private final PasswordEncoder passwordEncoder; diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java b/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java index 1509e4d..a3ba6f4 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/controller/AccountController.java @@ -11,7 +11,6 @@ import com.synapse.account_service.service.AccountService; import com.synapse.account_service_api.dto.request.SignUpRequest; import com.synapse.account_service_api.dto.response.SignUpResponse; - import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/domain/RefreshToken.java b/account-service/account-service/src/main/java/com/synapse/account_service/domain/RefreshToken.java new file mode 100644 index 0000000..2b0fb13 --- /dev/null +++ b/account-service/account-service/src/main/java/com/synapse/account_service/domain/RefreshToken.java @@ -0,0 +1,18 @@ +package com.synapse.account_service.domain; + +import java.util.UUID; + +import lombok.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RefreshToken { + private UUID memberId; + private String token; + + @Builder + public RefreshToken(UUID memberId, String token) { + this.memberId = memberId; + this.token = token; + } +} diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/Member.java b/account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/Member.java index ece378c..52d9c25 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/Member.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/Member.java @@ -17,7 +17,9 @@ @Getter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "members") +@Table(name = "members", indexes = { + @Index(name = "idx_member_username", columnList = "username") +}) public class Member extends BaseEntity { @Id diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/RefreshToken.java b/account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/RefreshToken.java deleted file mode 100644 index 61e34eb..0000000 --- a/account-service/account-service/src/main/java/com/synapse/account_service/domain/entity/RefreshToken.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.synapse.account_service.domain.entity; - -import java.util.UUID; - -import com.synapse.account_service.domain.common.BaseTimeEntity; - -import jakarta.persistence.*; -import lombok.*; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "refresh_token") -public class RefreshToken extends BaseTimeEntity { - @Id - @Column(columnDefinition = "uuid") - private UUID memberId; - - @Column(nullable = false, length = 512) - private String token; - - @Builder - public RefreshToken(UUID memberId, String token) { - this.memberId = memberId; - this.token = token; - } - - public void updateToken(String newToken) { - this.token = newToken; - } -} diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/domain/repository/RefreshTokenRepository.java b/account-service/account-service/src/main/java/com/synapse/account_service/domain/repository/RefreshTokenRepository.java deleted file mode 100644 index afc9c30..0000000 --- a/account-service/account-service/src/main/java/com/synapse/account_service/domain/repository/RefreshTokenRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.synapse.account_service.domain.repository; - -import java.util.UUID; - -import org.springframework.data.jpa.repository.JpaRepository; - -import com.synapse.account_service.domain.entity.RefreshToken; - -public interface RefreshTokenRepository extends JpaRepository { - -} diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java b/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java index 26eb6f9..05bfb56 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/service/AccountService.java @@ -5,7 +5,6 @@ import java.time.ZonedDateTime; import java.util.UUID; -import org.springframework.scheduling.annotation.Async; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,6 +18,7 @@ import com.synapse.account_service.exception.ExceptionType; import com.synapse.account_service_api.dto.request.SignUpRequest; import com.synapse.account_service_api.dto.response.SignUpResponse; + import com.synapse.account_service_api.event.MemberDomainEvent; import io.eventuate.tram.events.aggregates.ResultWithDomainEvents; @@ -33,9 +33,9 @@ public class AccountService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; + // private final MemberDomainEventPublisher memberDomainEventPublisher; - @Async("signupTaskExecutor") @Transactional public SignUpResponse registerMember(SignUpRequest request) { diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/service/CustomUserDetailsService.java b/account-service/account-service/src/main/java/com/synapse/account_service/service/CustomUserDetailsService.java index bff117b..094ab89 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/service/CustomUserDetailsService.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/service/CustomUserDetailsService.java @@ -23,6 +23,6 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx ProviderUserRequest providerUserRequest = new ProviderUserRequest(member); ProviderUser providerUser = providerUser(providerUserRequest); - return new PrincipalUser(providerUser); + return new PrincipalUser(providerUser, member); } } diff --git a/account-service/account-service/src/main/java/com/synapse/account_service/service/TokenManagementService.java b/account-service/account-service/src/main/java/com/synapse/account_service/service/TokenManagementService.java index bd648f6..aea06b6 100644 --- a/account-service/account-service/src/main/java/com/synapse/account_service/service/TokenManagementService.java +++ b/account-service/account-service/src/main/java/com/synapse/account_service/service/TokenManagementService.java @@ -1,14 +1,18 @@ package com.synapse.account_service.service; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; import java.util.UUID; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.synapse.account_service.domain.RefreshToken; import com.synapse.account_service.domain.entity.Member; -import com.synapse.account_service.domain.entity.RefreshToken; import com.synapse.account_service.domain.repository.MemberRepository; -import com.synapse.account_service.domain.repository.RefreshTokenRepository; import com.synapse.account_service.exception.ExceptionType; import com.synapse.account_service.exception.JWTValidationException; import com.synapse.account_service.exception.NotFoundException; @@ -22,29 +26,28 @@ @RequiredArgsConstructor public class TokenManagementService { private final JwtTokenService jwtTokenService; - private final RefreshTokenRepository refreshTokenRepository; private final MemberRepository memberRepository; + private final RedisTemplate refreshTokenRedisTemplate; public void saveOrUpdateRefreshToken(UUID memberId, TokenResult refreshToken) { - refreshTokenRepository.findById(memberId) - .ifPresentOrElse( - // 기존 토큰이 있으면, 새 토큰으로 값을 업데이트 (재로그인 시) - existingToken -> existingToken.updateToken(refreshToken.token()), - // 기존 토큰이 없으면, 새로 생성하여 저장 (최초 로그인) - () -> { - RefreshToken newRefreshToken = new RefreshToken(memberId, refreshToken.token()); - refreshTokenRepository.save(newRefreshToken); - }); + String redisKey = "refresh_token:" + memberId.toString(); + RefreshToken refreshTokenEntity = new RefreshToken(memberId, refreshToken.token()); + + long ttlSeconds = ChronoUnit.SECONDS.between(Instant.now(), refreshToken.expiresAt()); + refreshTokenRedisTemplate.opsForValue().set(redisKey, refreshTokenEntity, ttlSeconds, TimeUnit.SECONDS); } public TokenResponse reissueTokens(String requestRefreshToken) { UUID memberId = jwtTokenService.getMemberIdFrom(requestRefreshToken); + String redisKey = "refresh_token:" + memberId.toString(); - RefreshToken storedToken = refreshTokenRepository.findById(memberId) - .orElseThrow(() -> new JWTValidationException(ExceptionType.INVALID_REFRESH_TOKEN)); + RefreshToken storedToken = refreshTokenRedisTemplate.opsForValue().get(redisKey); + if (storedToken == null) { + throw new JWTValidationException(ExceptionType.INVALID_REFRESH_TOKEN); + } if (!storedToken.getToken().equals(requestRefreshToken)) { - refreshTokenRepository.delete(storedToken); + refreshTokenRedisTemplate.delete(redisKey); throw new JWTValidationException(ExceptionType.TAMPERED_REFRESH_TOKEN); } @@ -55,7 +58,10 @@ public TokenResponse reissueTokens(String requestRefreshToken) { TokenResponse newTokens = jwtTokenService.createTokenResponse(memberId.toString(), role); - storedToken.updateToken(newTokens.refreshToken().token()); + // Redis에 새로운 RefreshToken 저장 + RefreshToken newRefreshToken = new RefreshToken(memberId, newTokens.refreshToken().token()); + long ttlSeconds = Duration.between(Instant.now(), newTokens.refreshToken().expiresAt()).getSeconds(); + refreshTokenRedisTemplate.opsForValue().set(redisKey, newRefreshToken, ttlSeconds, TimeUnit.SECONDS); return newTokens; } diff --git a/account-service/account-service/src/main/resources/application-local.yml b/account-service/account-service/src/main/resources/application-local.yml index 6409229..4dc757a 100644 --- a/account-service/account-service/src/main/resources/application-local.yml +++ b/account-service/account-service/src/main/resources/application-local.yml @@ -5,7 +5,18 @@ spring: username: ${local-db.postgres.username} password: ${local-db.postgres.password} hikari: - maximum-pool-size: 30 + maximum-pool-size: 70 + minimum-idle: 10 + + data: + redis: + host: ${local-redis.host} + port: ${local-redis.port} + lettuce: + pool: + max-active: 32 + max-idle: 16 + min-idle: 8 jpa: properties: @@ -15,12 +26,13 @@ spring: highlight: sql: true hbm2ddl: - auto: create + auto: update dialect: org.hibernate.dialect.PostgreSQLDialect jdbc: batch_size: 30 order_inserts: true order_updates: true + default_batch_fetch_size: 100 open-in-view: false show-sql: true diff --git a/account-service/account-service/src/main/resources/db/QueryPlan.sql b/account-service/account-service/src/main/resources/db/QueryPlan.sql new file mode 100644 index 0000000..5b3a6ed --- /dev/null +++ b/account-service/account-service/src/main/resources/db/QueryPlan.sql @@ -0,0 +1,3 @@ +# 로그인 시 username 조회 쿼리 플랜 +EXPLAIN SELECT * FROM members WHERE username = 'test'; + diff --git a/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/LoginIntegrationTest.java b/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/LoginIntegrationTest.java index 6870799..441ade8 100644 --- a/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/LoginIntegrationTest.java +++ b/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/LoginIntegrationTest.java @@ -3,6 +3,9 @@ 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.status; + +import java.util.UUID; + import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -41,14 +44,16 @@ public class LoginIntegrationTest extends TestConfig{ @BeforeEach void setUp() { + memberRepository.deleteAll(); Member testMember = Member.builder() + .id(UUID.randomUUID()) .email("test_user1234@example.com") .username(TEST_USERNAME) .password(passwordEncoder.encode(TEST_PASSWORD)) .role(MemberRole.USER) .provider("local") .build(); - memberRepository.save(testMember); + testMember = memberRepository.save(testMember); } @Test diff --git a/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/TokenReissueIntegrationTest.java b/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/TokenReissueIntegrationTest.java index 1c00e4d..6ecfa3a 100644 --- a/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/TokenReissueIntegrationTest.java +++ b/account-service/account-service/src/test/java/com/synapse/account_service/integrationtest/TokenReissueIntegrationTest.java @@ -3,6 +3,9 @@ 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.status; + +import java.util.UUID; + import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -15,11 +18,10 @@ import com.synapse.account_service.TestConfig; import com.synapse.account_service.domain.entity.Member; -import com.synapse.account_service.domain.entity.RefreshToken; import com.synapse.account_service.domain.enums.MemberRole; import com.synapse.account_service.domain.repository.MemberRepository; -import com.synapse.account_service.domain.repository.RefreshTokenRepository; import com.synapse.account_service.service.JwtTokenService; +import com.synapse.account_service.service.TokenManagementService; import com.synapse.account_service_api.dto.response.TokenResponse; import jakarta.servlet.http.Cookie; @@ -33,10 +35,10 @@ public class TokenReissueIntegrationTest extends TestConfig { private JwtTokenService jwtTokenService; @Autowired - private RefreshTokenRepository refreshTokenRepository; + private MemberRepository memberRepository; @Autowired - private MemberRepository memberRepository; + private TokenManagementService tokenManagementService; private Member testMember; private String validRefreshToken; @@ -45,18 +47,19 @@ public class TokenReissueIntegrationTest extends TestConfig { void setUp() { // 테스트용 사용자 생성 및 저장 testMember = Member.builder() + .id(UUID.randomUUID()) .email("reissue_user@example.com") .username("reissue_user") .password("password") .role(MemberRole.USER) .provider("local") .build(); - memberRepository.save(testMember); + testMember = memberRepository.save(testMember); - // 테스트용 유효한 리프레시 토큰 생성 및 DB에 저장 + // 테스트용 유효한 리프레시 토큰 생성 및 Redis에 저장 TokenResponse tokens = jwtTokenService.createTokenResponse(testMember.getId().toString(), "USER"); validRefreshToken = tokens.refreshToken().token(); - refreshTokenRepository.save(new RefreshToken(testMember.getId(), validRefreshToken)); + tokenManagementService.saveOrUpdateRefreshToken(testMember.getId(), tokens.refreshToken()); } @Test diff --git a/account-service/account-service/src/test/java/com/synapse/account_service/repository/MemberRepositoryTest.java b/account-service/account-service/src/test/java/com/synapse/account_service/repository/MemberRepositoryTest.java index a21160a..b45803d 100644 --- a/account-service/account-service/src/test/java/com/synapse/account_service/repository/MemberRepositoryTest.java +++ b/account-service/account-service/src/test/java/com/synapse/account_service/repository/MemberRepositoryTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.Optional; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -32,6 +33,7 @@ public class MemberRepositoryTest extends TestConfig { @BeforeEach void setUp() { testMember = Member.builder() + .id(UUID.randomUUID()) .email("test@example.com") .password("encrypted_password") .username("테스트유저") @@ -64,6 +66,7 @@ void save_withDuplicateEmail_shouldThrowException() { // when: 동일한 이메일을 가진 새로운 회원을 만듭니다. Member duplicateMember = Member.builder() + .id(UUID.randomUUID()) .email("test@example.com") // 중복된 이메일 .password("another_password") .role(MemberRole.USER) @@ -104,6 +107,7 @@ void save_shouldSetCreatedAt() { void findByProviderAndRegistrationId_shouldReturnMember() { // given Member oauthMember = Member.builder() + .id(UUID.randomUUID()) .email("google_user@example.com") .password("social_login_password") // 실제로는 비밀번호가 없을 수도 있습니다. .username("구글유저") diff --git a/account-service/account-service/src/test/java/com/synapse/account_service/service/TokenManagementServiceTest.java b/account-service/account-service/src/test/java/com/synapse/account_service/service/TokenManagementServiceTest.java deleted file mode 100644 index fe92e34..0000000 --- a/account-service/account-service/src/test/java/com/synapse/account_service/service/TokenManagementServiceTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.synapse.account_service.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; - -import java.time.Instant; -import java.util.Optional; -import java.util.UUID; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -import com.synapse.account_service.TestConfig; -import com.synapse.account_service.domain.entity.Member; -import com.synapse.account_service.domain.entity.RefreshToken; -import com.synapse.account_service.domain.enums.MemberRole; -import com.synapse.account_service.domain.repository.MemberRepository; -import com.synapse.account_service.domain.repository.RefreshTokenRepository; -import com.synapse.account_service.exception.JWTValidationException; -import com.synapse.account_service_api.dto.TokenResult; -import com.synapse.account_service_api.dto.response.TokenResponse; - -public class TokenManagementServiceTest extends TestConfig { - - @InjectMocks - private TokenManagementService tokenManagementService; - - @Mock - private JwtTokenService jwtTokenService; - @Mock - private RefreshTokenRepository refreshTokenRepository; - @Mock - private MemberRepository memberRepository; - - private UUID memberId; - private String validRefreshToken; - - @BeforeEach - void setUp() { - memberId = UUID.randomUUID(); - validRefreshToken = "valid.refresh.token"; - } - - @Test - @DisplayName("토큰 재발급 성공: 유효한 리프레시 토큰으로 요청 시, 새로운 토큰 쌍을 반환하고 DB를 갱신한다") - void reissueTokens_success() { - // given - RefreshToken storedToken = new RefreshToken(memberId, validRefreshToken); - Member member = Member.builder() - .role(MemberRole.USER) - .build(); - TokenResponse newTokens = new TokenResponse( - new TokenResult("new.access.token", Instant.now().plusSeconds(1800)), - new TokenResult("new.refresh.token", Instant.now().plusSeconds(86400)) - ); - - given(jwtTokenService.getMemberIdFrom(validRefreshToken)).willReturn(memberId); - given(refreshTokenRepository.findById(memberId)).willReturn(Optional.of(storedToken)); - given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); - given(jwtTokenService.createTokenResponse(memberId.toString(), "USER")).willReturn(newTokens); - - // when - TokenResponse result = tokenManagementService.reissueTokens(validRefreshToken); - - // then - assertThat(result.accessToken().token()).isEqualTo("new.access.token"); - assertThat(storedToken.getToken()).isEqualTo("new.refresh.token"); // Rotation 검증 - verify(refreshTokenRepository).findById(memberId); - verify(memberRepository).findById(memberId); - } - - @Test - @DisplayName("토큰 재발급 실패: DB에 저장된 토큰과 일치하지 않으면 InvalidTokenException을 던지고 DB에서 삭제한다 (탈취 의심)") - void reissueTokens_fail_whenTokenMismatched() { - // given - RefreshToken storedToken = new RefreshToken(memberId, "different.token.in.db"); - - given(jwtTokenService.getMemberIdFrom(validRefreshToken)).willReturn(memberId); - given(refreshTokenRepository.findById(memberId)).willReturn(Optional.of(storedToken)); - - // when & then - assertThrows(JWTValidationException.class, () -> { - tokenManagementService.reissueTokens(validRefreshToken); - }); - - // 탈취 시도로 간주하고, DB에서 해당 토큰을 삭제했는지 검증 - verify(refreshTokenRepository).delete(storedToken); - } - - @Test - @DisplayName("토큰 재발급 실패: DB에 리프레시 토큰이 없으면 InvalidTokenException을 던진다") - void reissueTokens_fail_whenTokenNotFoundInDb() { - // given - given(jwtTokenService.getMemberIdFrom(validRefreshToken)).willReturn(memberId); - given(refreshTokenRepository.findById(memberId)).willReturn(Optional.empty()); - - // when & then - assertThrows(JWTValidationException.class, () -> { - tokenManagementService.reissueTokens(validRefreshToken); - }); - } -}