-
Notifications
You must be signed in to change notification settings - Fork 2
MOSU-373 refactor: profile 등록 동시 요청 처리 #379
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
10ef4d5
fb2b5f8
83a88a2
3e8ca30
0627278
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,110 @@ | ||||||||||||||
| package life.mosu.mosuserver.application.profile; | ||||||||||||||
|
|
||||||||||||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||||||||||||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||||||||||||||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||||||||||||||
|
|
||||||||||||||
| import java.time.LocalDate; | ||||||||||||||
| import java.util.List; | ||||||||||||||
| import java.util.concurrent.CopyOnWriteArrayList; | ||||||||||||||
| import java.util.concurrent.CountDownLatch; | ||||||||||||||
| import java.util.concurrent.ExecutorService; | ||||||||||||||
| import java.util.concurrent.Executors; | ||||||||||||||
| import java.util.concurrent.TimeUnit; | ||||||||||||||
| import life.mosu.mosuserver.domain.profile.entity.Education; | ||||||||||||||
| import life.mosu.mosuserver.domain.profile.entity.Grade; | ||||||||||||||
| import life.mosu.mosuserver.domain.profile.repository.ProfileJpaRepository; | ||||||||||||||
| import life.mosu.mosuserver.domain.user.entity.AuthProvider; | ||||||||||||||
| import life.mosu.mosuserver.domain.user.entity.UserJpaEntity; | ||||||||||||||
| import life.mosu.mosuserver.domain.user.entity.UserRole; | ||||||||||||||
| import life.mosu.mosuserver.domain.user.repository.UserJpaRepository; | ||||||||||||||
| import life.mosu.mosuserver.global.exception.CustomRuntimeException; | ||||||||||||||
| import life.mosu.mosuserver.presentation.profile.dto.SchoolInfoRequest; | ||||||||||||||
| import life.mosu.mosuserver.presentation.profile.dto.SignUpProfileRequest; | ||||||||||||||
| import org.junit.jupiter.api.BeforeEach; | ||||||||||||||
| import org.junit.jupiter.api.DisplayName; | ||||||||||||||
| import org.junit.jupiter.api.Test; | ||||||||||||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||||||||||||
| import org.springframework.boot.test.context.SpringBootTest; | ||||||||||||||
|
|
||||||||||||||
| @SpringBootTest | ||||||||||||||
| class ProfileServiceConcurrencyTest { | ||||||||||||||
|
|
||||||||||||||
| @Autowired | ||||||||||||||
| private ProfileService profileService; | ||||||||||||||
|
|
||||||||||||||
| @Autowired | ||||||||||||||
| private UserJpaRepository userRepository; | ||||||||||||||
|
|
||||||||||||||
| @Autowired | ||||||||||||||
| private ProfileJpaRepository profileJpaRepository; | ||||||||||||||
|
|
||||||||||||||
| private UserJpaEntity testUser; | ||||||||||||||
| private SignUpProfileRequest request; | ||||||||||||||
|
|
||||||||||||||
| @BeforeEach | ||||||||||||||
| void setUp() { | ||||||||||||||
| profileJpaRepository.deleteAllInBatch(); | ||||||||||||||
| userRepository.deleteAllInBatch(); | ||||||||||||||
|
|
||||||||||||||
| testUser = UserJpaEntity.builder() | ||||||||||||||
| .loginId("testUser@example.com") | ||||||||||||||
| .userRole(UserRole.ROLE_PENDING) | ||||||||||||||
| .phoneNumber("010-1234-5678") | ||||||||||||||
| .name("김영숙") | ||||||||||||||
| .provider(AuthProvider.KAKAO) | ||||||||||||||
| .birth(LocalDate.of(2007, 1, 1)) | ||||||||||||||
| .build(); | ||||||||||||||
| userRepository.save(testUser); | ||||||||||||||
|
|
||||||||||||||
| request = new SignUpProfileRequest( | ||||||||||||||
| "김영숙", | ||||||||||||||
| LocalDate.of(2007, 1, 1), | ||||||||||||||
| "여자", | ||||||||||||||
| "010-1234-5678", | ||||||||||||||
| "test@example.com", | ||||||||||||||
| Education.ENROLLED, | ||||||||||||||
| new SchoolInfoRequest("test school", "12345", "test street"), | ||||||||||||||
| Grade.HIGH_1 | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| @Test | ||||||||||||||
| @DisplayName("동일한 사용자에 대한 프로필 등록이 동시에 요청되면 하나는 성공하고 하나는 Unique 제약조건 위반 예외를 던진다") | ||||||||||||||
| void registerProfile_concurrency_test() throws InterruptedException { | ||||||||||||||
| // given | ||||||||||||||
| int threadCount = 2; | ||||||||||||||
| ExecutorService executorService = Executors.newFixedThreadPool(threadCount); | ||||||||||||||
| CountDownLatch latch = new CountDownLatch(threadCount); | ||||||||||||||
| List<Exception> exceptions = new CopyOnWriteArrayList<>(); | ||||||||||||||
|
|
||||||||||||||
| // when | ||||||||||||||
| for (int i = 0; i < threadCount; i++) { | ||||||||||||||
| executorService.submit(() -> { | ||||||||||||||
| try { | ||||||||||||||
| latch.countDown(); | ||||||||||||||
| latch.await(); | ||||||||||||||
|
|
||||||||||||||
| profileService.registerProfile(testUser.getId(), request); | ||||||||||||||
| } catch (InterruptedException e) { | ||||||||||||||
| Thread.currentThread().interrupt(); | ||||||||||||||
| } catch (Exception e) { | ||||||||||||||
| exceptions.add(e); | ||||||||||||||
| } | ||||||||||||||
| }); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| executorService.shutdown(); | ||||||||||||||
| assertTrue(executorService.awaitTermination(5, TimeUnit.SECONDS), | ||||||||||||||
| "스레드 풀이 시간 내에 종료되지 않았습니다."); | ||||||||||||||
|
|
||||||||||||||
| // then | ||||||||||||||
| long profileCount = profileJpaRepository.count(); | ||||||||||||||
| assertEquals(1, profileCount, "경쟁 조건으로 인해 프로필이 중복 생성되거나 생성되지 않았습니다."); | ||||||||||||||
|
|
||||||||||||||
| assertThat(exceptions).hasSize(1); | ||||||||||||||
|
|
||||||||||||||
| Exception thrownException = exceptions.getFirst(); | ||||||||||||||
| assertThat(thrownException).isInstanceOf(CustomRuntimeException.class); | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋은 테스트입니다. 테스트의 정확성을 더 높이기 위해, 발생하는 예외가 assertThat(thrownException).isInstanceOfSatisfying(CustomRuntimeException.class, e ->
assertThat(e.getCode()).isEqualTo("PROFILE_ALREADY_EXISTS")); |
||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+107
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid Java 21-only List.getFirst(); use get(0) for wider JDK compatibility. This fails on JDK 17 (common LTS). Replace with get(0). - Exception thrownException = exceptions.getFirst();
+ Exception thrownException = exceptions.get(0);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| } | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Don’t hide the new concurrency test; make it opt-in/opt-out instead of permanently excluded.
Excluding
ProfileServiceConcurrencyTest.javafrom compilation defeats the PR’s objective and risks regressions. Prefer a property-gated exclude so CI can still run it when desired.Apply this diff to make the exclusion conditional (run with
-PincludeConcurrencyTests=trueto include the test):If you want a cleaner separation, move it to an
integrationTestsource set and wire a dedicated task; I can provide a minimal snippet.📝 Committable suggestion
🤖 Prompt for AI Agents