From 10ef4d5add4288c6cff5ef77b1633153a10e54f5 Mon Sep 17 00:00:00 2001 From: wlgns12370 Date: Mon, 8 Sep 2025 20:02:20 +0900 Subject: [PATCH 1/5] =?UTF-8?q?MOSU-373=20refactor:=20profile=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EB=8F=99=EC=8B=9C=20=EC=9A=94=EC=B2=AD=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mosu/mosuserver/application/profile/ProfileService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java b/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java index aea173b1..fe42a9c2 100644 --- a/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java +++ b/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java @@ -14,6 +14,7 @@ import life.mosu.mosuserver.presentation.profile.dto.SignUpProfileRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -44,6 +45,8 @@ public void registerProfile(Long userId, SignUpProfileRequest request) { eventTxService.publishSuccessEvent(userId); + } catch (DataIntegrityViolationException ex) { + throw new CustomRuntimeException(ErrorCode.PROFILE_ALREADY_EXISTS, userId); } catch (Exception ex) { log.error("프로필 등록 실패: {}", ex.getMessage(), ex); throw ex; From fb2b5f8f145dd04d06c547e35fefdc13e331e7ec Mon Sep 17 00:00:00 2001 From: wlgns12370 Date: Mon, 8 Sep 2025 20:03:42 +0900 Subject: [PATCH 2/5] =?UTF-8?q?MOSU-373=20test:=20profile=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EB=8F=99=EC=8B=9C=20=EC=9A=94=EC=B2=AD=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=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 --- .../ProfileServiceConcurrencyTest.java | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/test/java/life/mosu/mosuserver/application/profile/ProfileServiceConcurrencyTest.java diff --git a/src/test/java/life/mosu/mosuserver/application/profile/ProfileServiceConcurrencyTest.java b/src/test/java/life/mosu/mosuserver/application/profile/ProfileServiceConcurrencyTest.java new file mode 100644 index 00000000..8349838f --- /dev/null +++ b/src/test/java/life/mosu/mosuserver/application/profile/ProfileServiceConcurrencyTest.java @@ -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 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); + } +} From 83a88a2febea1584500a3e2b663d374847cff536 Mon Sep 17 00:00:00 2001 From: wlgns12370 Date: Mon, 8 Sep 2025 21:57:08 +0900 Subject: [PATCH 3/5] =?UTF-8?q?Revert=20"MOSU-373=20refactor:=20profile=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EB=8F=99=EC=8B=9C=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 10ef4d5add4288c6cff5ef77b1633153a10e54f5. --- .../mosu/mosuserver/application/profile/ProfileService.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java b/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java index fe42a9c2..aea173b1 100644 --- a/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java +++ b/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java @@ -14,7 +14,6 @@ import life.mosu.mosuserver.presentation.profile.dto.SignUpProfileRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -45,8 +44,6 @@ public void registerProfile(Long userId, SignUpProfileRequest request) { eventTxService.publishSuccessEvent(userId); - } catch (DataIntegrityViolationException ex) { - throw new CustomRuntimeException(ErrorCode.PROFILE_ALREADY_EXISTS, userId); } catch (Exception ex) { log.error("프로필 등록 실패: {}", ex.getMessage(), ex); throw ex; From 0627278826c2b20abf8c7152c8f6f99d178255b8 Mon Sep 17 00:00:00 2001 From: wlgns12370 Date: Sat, 13 Sep 2025 20:52:33 +0900 Subject: [PATCH 4/5] =?UTF-8?q?MOSU-373=20test:=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EC=A0=91=EA=B7=BC=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20exclude=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 8 ++++++++ .../mosuserver/application/profile/ProfileService.java | 3 +++ 2 files changed, 11 insertions(+) diff --git a/build.gradle b/build.gradle index d90c98a1..1bf88d98 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,14 @@ java { } } +sourceSets { + test { + java { + exclude '**/ProfileServiceConcurrencyTest.java' + } + } +} + configurations { compileOnly { extendsFrom annotationProcessor diff --git a/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java b/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java index aea173b1..fe42a9c2 100644 --- a/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java +++ b/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java @@ -14,6 +14,7 @@ import life.mosu.mosuserver.presentation.profile.dto.SignUpProfileRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -44,6 +45,8 @@ public void registerProfile(Long userId, SignUpProfileRequest request) { eventTxService.publishSuccessEvent(userId); + } catch (DataIntegrityViolationException ex) { + throw new CustomRuntimeException(ErrorCode.PROFILE_ALREADY_EXISTS, userId); } catch (Exception ex) { log.error("프로필 등록 실패: {}", ex.getMessage(), ex); throw ex; From ec371ba3fcba81f1c274c47aa99040a01d9ec603 Mon Sep 17 00:00:00 2001 From: chominju02 Date: Thu, 16 Oct 2025 20:46:56 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20examNumberUtil=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/examapplication/util/ExamNumberUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/life/mosu/mosuserver/application/examapplication/util/ExamNumberUtil.java b/src/main/java/life/mosu/mosuserver/application/examapplication/util/ExamNumberUtil.java index 86ab20a1..54e0bafb 100644 --- a/src/main/java/life/mosu/mosuserver/application/examapplication/util/ExamNumberUtil.java +++ b/src/main/java/life/mosu/mosuserver/application/examapplication/util/ExamNumberUtil.java @@ -13,7 +13,7 @@ public static String formatExamNumber(Integer roundCode, Integer areaCode, Integ } return String.format( - "%d%02d%02d%04d", + "%d%d%02d%04d", roundCode, areaCode, schoolCode,