Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ java {
}
}

sourceSets {
test {
java {
exclude '**/ProfileServiceConcurrencyTest.java'
}
}
}
Comment on lines +17 to +23
Copy link

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.java from 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=true to include the test):

-sourceSets {
-    test {
-        java {
-            exclude '**/ProfileServiceConcurrencyTest.java'
-        }
-    }
-}
+sourceSets {
+    test {
+        java {
+            if (!project.hasProperty('includeConcurrencyTests')) {
+                exclude '**/ProfileServiceConcurrencyTest.java'
+            }
+        }
+    }
+}

If you want a cleaner separation, move it to an integrationTest source set and wire a dedicated task; I can provide a minimal snippet.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sourceSets {
test {
java {
exclude '**/ProfileServiceConcurrencyTest.java'
}
}
}
sourceSets {
test {
java {
if (!project.hasProperty('includeConcurrencyTests')) {
exclude '**/ProfileServiceConcurrencyTest.java'
}
}
}
}
🤖 Prompt for AI Agents
In build.gradle around lines 17–23 the ProfileServiceConcurrencyTest.java is
permanently excluded which hides the new concurrency test; change the exclusion
to be property-gated so the test is excluded by default but included when the
build is invoked with -PincludeConcurrencyTests=true. Implement a conditional
check on project.hasProperty('includeConcurrencyTests') (or
project.property('includeConcurrencyTests') == 'true') and only add the exclude
when that property is not present/false; alternatively, move the test into an
integrationTest sourceSet and wire an integrationTest task if you prefer
separation.


configurations {
compileOnly {
extendsFrom annotationProcessor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

좋은 테스트입니다. 테스트의 정확성을 더 높이기 위해, 발생하는 예외가 CustomRuntimeException이라는 것만 검증하는 것보다 어떤 ErrorCode를 가지고 있는지까지 확인하는 것이 좋습니다. 이렇게 하면 의도치 않은 다른 CustomRuntimeException이 발생했을 때 테스트가 실패하여 문제를 더 빨리 발견할 수 있습니다.

        assertThat(thrownException).isInstanceOfSatisfying(CustomRuntimeException.class, e ->
                assertThat(e.getCode()).isEqualTo("PROFILE_ALREADY_EXISTS"));

}
Comment on lines +107 to +109
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Exception thrownException = exceptions.getFirst();
assertThat(thrownException).isInstanceOf(CustomRuntimeException.class);
}
Exception thrownException = exceptions.get(0);
assertThat(thrownException).isInstanceOf(CustomRuntimeException.class);
}
🤖 Prompt for AI Agents
In
src/test/java/life/mosu/mosuserver/application/profile/ProfileServiceConcurrencyTest.java
around lines 107 to 109, the test uses List.getFirst() which is Java 21-only;
replace the call with exceptions.get(0) to restore JDK 17 compatibility and keep
the same behavior, i.e., retrieve the first element from the list before
asserting its type.

}
Loading