Skip to content

Commit 20e381b

Browse files
authored
Merge pull request #379 from mosu-dev/refactor/mosu-373
MOSU-373 refactor: profile 등록 동시 요청 처리
2 parents 8a41fbf + 0627278 commit 20e381b

File tree

3 files changed

+121
-0
lines changed

3 files changed

+121
-0
lines changed

build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ java {
1414
}
1515
}
1616

17+
sourceSets {
18+
test {
19+
java {
20+
exclude '**/ProfileServiceConcurrencyTest.java'
21+
}
22+
}
23+
}
24+
1725
configurations {
1826
compileOnly {
1927
extendsFrom annotationProcessor

src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import life.mosu.mosuserver.presentation.profile.dto.SignUpProfileRequest;
1515
import lombok.RequiredArgsConstructor;
1616
import lombok.extern.slf4j.Slf4j;
17+
import org.springframework.dao.DataIntegrityViolationException;
1718
import org.springframework.stereotype.Service;
1819
import org.springframework.transaction.annotation.Propagation;
1920
import org.springframework.transaction.annotation.Transactional;
@@ -44,6 +45,8 @@ public void registerProfile(Long userId, SignUpProfileRequest request) {
4445

4546
eventTxService.publishSuccessEvent(userId);
4647

48+
} catch (DataIntegrityViolationException ex) {
49+
throw new CustomRuntimeException(ErrorCode.PROFILE_ALREADY_EXISTS, userId);
4750
} catch (Exception ex) {
4851
log.error("프로필 등록 실패: {}", ex.getMessage(), ex);
4952
throw ex;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package life.mosu.mosuserver.application.profile;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.time.LocalDate;
8+
import java.util.List;
9+
import java.util.concurrent.CopyOnWriteArrayList;
10+
import java.util.concurrent.CountDownLatch;
11+
import java.util.concurrent.ExecutorService;
12+
import java.util.concurrent.Executors;
13+
import java.util.concurrent.TimeUnit;
14+
import life.mosu.mosuserver.domain.profile.entity.Education;
15+
import life.mosu.mosuserver.domain.profile.entity.Grade;
16+
import life.mosu.mosuserver.domain.profile.repository.ProfileJpaRepository;
17+
import life.mosu.mosuserver.domain.user.entity.AuthProvider;
18+
import life.mosu.mosuserver.domain.user.entity.UserJpaEntity;
19+
import life.mosu.mosuserver.domain.user.entity.UserRole;
20+
import life.mosu.mosuserver.domain.user.repository.UserJpaRepository;
21+
import life.mosu.mosuserver.global.exception.CustomRuntimeException;
22+
import life.mosu.mosuserver.presentation.profile.dto.SchoolInfoRequest;
23+
import life.mosu.mosuserver.presentation.profile.dto.SignUpProfileRequest;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.DisplayName;
26+
import org.junit.jupiter.api.Test;
27+
import org.springframework.beans.factory.annotation.Autowired;
28+
import org.springframework.boot.test.context.SpringBootTest;
29+
30+
@SpringBootTest
31+
class ProfileServiceConcurrencyTest {
32+
33+
@Autowired
34+
private ProfileService profileService;
35+
36+
@Autowired
37+
private UserJpaRepository userRepository;
38+
39+
@Autowired
40+
private ProfileJpaRepository profileJpaRepository;
41+
42+
private UserJpaEntity testUser;
43+
private SignUpProfileRequest request;
44+
45+
@BeforeEach
46+
void setUp() {
47+
profileJpaRepository.deleteAllInBatch();
48+
userRepository.deleteAllInBatch();
49+
50+
testUser = UserJpaEntity.builder()
51+
.loginId("[email protected]")
52+
.userRole(UserRole.ROLE_PENDING)
53+
.phoneNumber("010-1234-5678")
54+
.name("김영숙")
55+
.provider(AuthProvider.KAKAO)
56+
.birth(LocalDate.of(2007, 1, 1))
57+
.build();
58+
userRepository.save(testUser);
59+
60+
request = new SignUpProfileRequest(
61+
"김영숙",
62+
LocalDate.of(2007, 1, 1),
63+
"여자",
64+
"010-1234-5678",
65+
66+
Education.ENROLLED,
67+
new SchoolInfoRequest("test school", "12345", "test street"),
68+
Grade.HIGH_1
69+
);
70+
}
71+
72+
@Test
73+
@DisplayName("동일한 사용자에 대한 프로필 등록이 동시에 요청되면 하나는 성공하고 하나는 Unique 제약조건 위반 예외를 던진다")
74+
void registerProfile_concurrency_test() throws InterruptedException {
75+
// given
76+
int threadCount = 2;
77+
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
78+
CountDownLatch latch = new CountDownLatch(threadCount);
79+
List<Exception> exceptions = new CopyOnWriteArrayList<>();
80+
81+
// when
82+
for (int i = 0; i < threadCount; i++) {
83+
executorService.submit(() -> {
84+
try {
85+
latch.countDown();
86+
latch.await();
87+
88+
profileService.registerProfile(testUser.getId(), request);
89+
} catch (InterruptedException e) {
90+
Thread.currentThread().interrupt();
91+
} catch (Exception e) {
92+
exceptions.add(e);
93+
}
94+
});
95+
}
96+
97+
executorService.shutdown();
98+
assertTrue(executorService.awaitTermination(5, TimeUnit.SECONDS),
99+
"스레드 풀이 시간 내에 종료되지 않았습니다.");
100+
101+
// then
102+
long profileCount = profileJpaRepository.count();
103+
assertEquals(1, profileCount, "경쟁 조건으로 인해 프로필이 중복 생성되거나 생성되지 않았습니다.");
104+
105+
assertThat(exceptions).hasSize(1);
106+
107+
Exception thrownException = exceptions.getFirst();
108+
assertThat(thrownException).isInstanceOf(CustomRuntimeException.class);
109+
}
110+
}

0 commit comments

Comments
 (0)