๋ค์ด๊ฐ๊ธฐ ์์
ํ๋ก์ ํธ๋ฅผ ์งํ ์ค, ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๋ถ ๊ป์ ๋ณ์ ๊ธฐ๋ฅ์์ ๋ณ์ ๊ฐ์ด ์ ๋๋ก ์ ๋ฐ์ดํธ๋์ง ์๋ ๋ฌธ์ ๋ฅผ ๋ฐ๊ฒฌํ์ จ์ต๋๋ค. ๋ฌธ์ ๋ฅผ ์ ํํ ํ์ ํ๊ธฐ ์ํด ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ํ์ธํด๋ณธ ๊ฒฐ๊ณผ, ๋์ผํ ๋ณ์ ๋ฐ์ดํฐ๊ฐ 2๊ฐ ์ ์ฅ๋ ๊ฒ์ ํ์ธํ ์ ์์์ต๋๋ค.
public RatingCreateResponse createRating(Long userId, RatingCreateServiceRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(USER_NOT_FOUND));
Book book = bookRepository.findById(request.getBookIsbn())
.orElseThrow(() -> new BusinessException(BOOK_NOT_FOUND));
// ์กฐํํ user์ book์ ํตํด์ rating์ ์ค๋ณต ์์ฑํ๋์ง ํ์ธํ๋ค.
if (ratingRepository.existByUserAndBook(user, book)) {
throw new BusinessException(RATING_ALREADY_EXIST);
}
Rating rating = ratingRepository.save(request.toEntity());
return RatingCreateResponse.of(rating, book);
}
ํฅ๋ฏธ๋ก์ด ์ ์ ์ ํ๋ฆฌ์ผ์ด์ ์ฝ๋์ ์ค๋ณต ๋ฐ์ดํฐ๋ฅผ ๋ฐฉ์งํ๋ ๋ก์ง์ด ์ด๋ฏธ ๊ตฌํ๋์ด ์์์์๋ ๋ถ๊ตฌํ๊ณ , ์ค๋ณต ๋ฐ์ดํฐ๊ฐ ์์ฑ๋์๋ค๋ ์ ์ ๋๋ค. ๊ทธ๋ ๋ค๋ฉด, ์์๋๋ก ์ฝ๋๊ฐ ์๋ํ์ง ์์ ์์ธ์ ๋ถ์ํด๋ด์ผ๋ก์จ ๋ฌธ์ ์ ๊ทผ๋ณธ์ ์ธ ์์ธ์ ์ฐพ์๋ณด๊ฒ ์ต๋๋ค.
๋์์ฑ ๋ฌธ์ ๋ก ์ธํด ๋ฐ์ํ ์ค๋ณต ๋ฐ์ดํฐ ์ ์ฅ
๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ธฐ๋ก์ ์ดํด๋ณด๋, ๋ณ์ ๋ฐ์ดํฐ๊ฐ ๊ฑฐ์ ๋์์ ์์ฑ๋ ๊ฒ์ด ํ์ธ๋์์ต๋๋ค. ๋ ๊ฐ์ ๋ฐ์ดํฐ๊ฐ ๊ฑฐ์ ๋์ผํ ์๊ฐ์ ์ ์ฅ๋ ๊ฒ์ผ๋ก ๋ณด์, ์ด๋ ๋์์ฑ ๋ฌธ์ , ํนํ ๊ฒฝ์ ์ํ(Race Condition)๋ก ์ธํด ๋ฐ์ํ ๊ฒ์ผ๋ก ์ถ์ธก๋ฉ๋๋ค. ๊ทธ๋ ๋ค๋ฉด ๊ฒฝ์ ์ํ๋ ๋ฌด์์ด๋ฉฐ, ์ ๋ฐ์ํ๋ ๊ฒ์ผ๊น์?
๊ฒฝ์ ์ํ (Race Condition)
๊ฒฝ์ ์ํ๋ ๋ค์์ ํ๋ก์ธ์ค๋ ์ค๋ ๋๊ฐ ๋์์ ๋์ผํ ๋ฐ์ดํฐ์ ์ ๊ทผํ์ฌ ์์ ํ ๋ ๋ฐ์ํ๋ ๋ฌธ์ ๋ก, ์ด๋ก ์ธํด ์๊ธฐ์น ์์ ๊ฒฐ๊ณผ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
์ ์ฌ์ง์ฒ๋ผ ํ ์ฌ์ฉ์๊ฐ ๋ณ์ ์ ์ ์ถํ ๋ ๋น ๋ฅด๊ฒ ์์ฒญํ์ฌ 2๋ฒ ์์ฑ๋๋๋ก ์๋ํ ๊ฒฝ์ฐ, ๋ ์์ฒญ์ด ๊ฑฐ์ ๋์์ ๋ฐ์ดํฐ ๋ฒ ์ด์ค์ ๋๋ฌํ๊ฒ ๋๋ฉด์ ๋ณ์ ์ด ์๋์ง ํ์ธํ๋ ๋ก์ง์์ ๊ฒ์ฆ๋์ง ์๊ณ ์ค๋ณต ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋ ๊ฐ๋ฅ์ฑ์ด ์๊น๋๋ค.
์ด ๊ฐ์ ์ํฉ์ ์๋์ ์ฝ๋๋ฅผ ํตํด ํ์ธํ ์ ์์ต๋๋ค.
public RatingCreateResponse createRating(Long userId, RatingCreateServiceRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(USER_NOT_FOUND));
Book book = bookRepository.findById(request.getBookIsbn())
.orElseThrow(() -> new BusinessException(BOOK_NOT_FOUND));
// ์กฐํํ user์ book์ ํตํด์ rating์ ์ค๋ณต ์์ฑํ๋์ง ํ์ธํ๋ค.
if (ratingRepository.existByUserAndBook(user, book)) {
throw new BusinessException(RATING_ALREADY_EXIST);
}
Rating rating = ratingRepository.save(request.toEntity());
return RatingCreateResponse.of(rating, book);
}
@Test
@DisplayName("๋์ผํ ๋์์ ๋ํด ์ฌ์ฉ์๊ฐ ๋์์ ๋ณ์ ์ ์ถ๊ฐํ๋ฉด ์ฌ๋ฌ๊ฐ์ ๋ณ์ ์ด ์์ฑ๋๋ค.")
public void createRatingWithThreads() throws Exception {
// given
User user = userRepository.save(createUser());
Book book = bookRepository.save(createBook());
RatingCreateServiceRequest request = RatingCreateServiceRequest.builder()
.rating(4.5)
.bookIsbn(book.getIsbn())
.build();
int threadCount = 2;
ExecutorService executorService = Executores.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
// when - ์ฌ์ฉ์๊ฐ ๋์์ ๋ณ์ ์์ฑ์ ์์ฒญํ ์ํฉ
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
ratingService.createRating(user.getId(), request);
} finally {
latch.countDown();
}
});
}
latch.await();
// then
List<Rating> ratings = ratingRepository.findAll();
assertThat(ratings).hasSize(2);
}
์๋น์ค ๋ก์ง์์์๋ ์ด๋ฏธ ์ฌ์ฉ์๊ฐ ์ด๋ฏธ ๋ณ์ ์ ์ถ๊ฐํ์ ๊ฒฝ์ฐ ์ค๋ณต ์์ฑ์ด ๋์ง ์๋๋ก ๋์ด ์์ผ๋, ๋์์ ๋ณ์ ์์ฑ์ ์์ฒญํ ๊ฒฝ์ฐ์๋ ๋ณ์ ์ด 2๊ฐ๊ฐ ์ ์ฅ๋๋ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
๋ฌธ์ ํด๊ฒฐ ๋ฐฉ๋ฒ
์ค๋ณต ๋ฐ์ดํฐ ์ฝ์ ์ ๋ฐฉ์งํ๊ธฐ ์ํ ๋ฐฉ๋ฒ์ ์ฌ๋ฌ๊ฐ์ง ๋ฐฉ๋ฒ์ด ์์ต๋๋ค. ๋ํ์ ์ผ๋ก ์ ๋ํฌ ์ธ๋ฑ์ค ์ค์ ๊ณผ ๋ค์๋ ๋ฝ์ ํตํ Redis ๋ถ์ฐ ๋ฝ์ ์ฌ์ฉํ๋ ๋ฐฉ์์ด ์์ต๋๋ค.
์ ํฌ ํ๋ก์ ํธ์์๋ ์ด ๋ ๊ฐ์ง ํด๊ฒฐ ๋ฐฉ์ ์ค ์ ๋ํฌ ์ธ๋ฑ์ค๋ฅผ ์ฌ์ฉํ์ฌ ์ค๋ณต ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์์ต๋๋ค. Redis ๋ถ์ฐ ๋ฝ์ ๋๊ท๋ชจ ๋ถ์ฐ ์์คํ ์ด๋ ๋ฉํฐ ์ธ์คํด์ค ํ๊ฒฝ์์ ์ฌ๋ฌ ์๋น์ค ์ธ์คํด์ค๊ฐ ๋์์ ์ ๊ทผํ๋ ๋ฆฌ์์ค์ ๋ํ ์ค๋ณต ์ ์ด์ ์ ์ฉํฉ๋๋ค. ๊ทธ๋ฌ๋ ์ ํฌ ์๋น์ค๋ ๋จ์ผ ์ธ์คํด์ค ํ๊ฒฝ์ผ๋ก ์ด์๋๊ณ ์๊ธฐ ๋๋ฌธ์ Redis ๋ถ์ฐ ๋ฝ์ ๊ตฌํํ๋ ๊ฒ์ ๊ณผ๋ํ ํ๋จ์ด๋ผ ์๊ฐํ์ต๋๋ค.
โ ๋๊ด์ /๋น๊ด์ ๋ฝ์ ์ค๋ณต ์์ฑ์ ๋ง์ ์ ์๋์?
๋๊ด์ /๋น๊ด์ ๋ฝ์ ์ด๋ฏธ ์กด์ฌํ๋ ๋ฐ์ดํฐ์ ์ ํฉ์ฑ์ ๋ง๊ธฐ ์ํด ์ฌ์ฉ๋๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ INSERT์ ๊ฒฝ์ฐ ์๋ก์ด ๋ฐ์ดํฐ๊ฐ ์์ฑ๋๋ ๊ฒ์ด๋ฏ๋ก, ์ด๋ฏธ ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ์ง ์๊ธฐ์ ๋ฝ์ ๊ฐ๋ ์ด ์๋ํ์ง ์์ต๋๋ค.
โ ์ ๋ํฌ ์ธ๋ฑ์ค๋ ๋ฝ ์์ด ๋์์ฑ์ ์ ์ดํ๋ ๊ฑด๊ฐ์?
์ ๋ํฌ ์ธ๋ฑ์ค๋ INSERT ์ ์ค๋ณต ์ฌ๋ถ๋ฅผ ํ์ธํ๊ธฐ ์ํด ๊ณต์ ๋ฝ(S-Lock)์ ์ฌ์ฉํฉ๋๋ค. ์ด๋ ๋ค๋ฅธ ํธ๋์ญ์ ์์ INSERT ์์ ์ ํด๋ ๊ณต์ ๋ฝ์ ๊ฑธ ์ ์๊ธฐ ๋๋ฌธ์ ํด๋น ํธ๋์ญ์ ์์ ์ ๋๊ธฐํ๊ฒ ๋ฉ๋๋ค. ์ค๋ณต๋ ๊ฐ์ด ์๋ค๊ณ ํ์ธ๋๋ฉด, InnoDB๋ ๊ณต์ ๋ฝ์ ํด์ ํ๊ณ , ์ค์ ์ฝ์ ํ๋ ๊ณผ์ ์์ ์ฐ๊ธฐ ๋ฝ(X-Lock)์ผ๋ก ์ ํํ์ฌ ์ฝ์ ์์ ์ ์๋ฃํฉ๋๋ค. ์ฝ์ ์๋ฃ ํ, X-Lock๋ ํด์ ๋๋ฉด์ ๋ค๋ฅธ ํธ๋์ญ์ ์ด ์ด ๋ ์ฝ๋์ ์ ๊ทผํ ์ ์๊ฒ๋ฉ๋๋ค.
์ํฐํฐ์ ์ ๋ํฌ ์ธ๋ฑ์ค ์ ์ฉ
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(uniqueConstraint = {
@UniqueConstraint(columnNames = {"user_id", "book_isbn"})
})
public class Rating extends BaseEntity {
...
}
ํด๋น ์ํฐํฐ์๋ ์ฌ์ฉ์์ ๋์์ ๊ดํ ๋ณ์ ์ด ์ค๋ณต๋์ง ์๊ฒ ์์ฑ๋์ด์ผ ํ๊ธฐ ๋๋ฌธ์ ํด๋น ID๋ค์ ์ ๋ํฌ ๋ณตํฉ ์ธ๋ฑ์ค๋ก ์์ฑํ์ฌ ์ค์ ํ์ต๋๋ค.
@Test
@DisplayName("์ฌ์ฉ์๊ฐ ๋์ผํ ๋์์ ๋ํด ๋์์ ๋ณ์ ์ ์ถ๊ฐํด๋ ํ๋์ ๋ณ์ ๋ง ์์ฑ๋๋ค.")
public void createRatingWithThreads() throws Exception {
// given
User user = userRepository.save(createUser());
Book book = bookRepository.save(createBook());
RatingCreateServiceRequest reqeust = RatingCreateServiceRequest.builder()
.rating(4.5)
.bookIsbn(book.getIsbn())
.build();
int threadCount = 2;
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch();
// when
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
ratingService.createRating(user.getId(), request);
} finally {
latch.countDown();
}
});
}
latch.await();
// then
List<Rating> ratings = ratingRepository.findAll();
assertThat(ratings).hasSize(1);
}
ํด๋น ์ค์ ์ ๋ง์น๊ณ ์์ ํ ์คํธ๋ฅผ ์ํ์์ผ๋ณด๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ 1๊ฐ์ ๋ณ์ ๋ฐ์ดํฐ๋ง ์กฐํ๋๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
DataIntergrityViolationException ์ฒ๋ฆฌ
์ ๋ํฌ ์ธ๋ฑ์ค๋ฅผ ์ค์ ํ ํ ๋ฐ์ดํฐ ์ค๋ณต ์ ์ฅ ์, ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ด๋ถ์์ ์ค๋ณต์ ๋ํ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ฌ ์์ ๊ฐ์ ์์ธ๊ฐ ๋ฐ์ํฉ๋๋ค.
// Service์์ try/catch ๊ตฌ๋ฌธ์ ์ด์ฉํ์ฌ ์ฒ๋ฆฌ
@Transactional
public RatingCreateResponse createRating(Long userId, RatingCreateServiceRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessExeption(USER_NOT_FOUND));
Book book = bookRepository.findById(request.getBookIsbn())
.orElseThrow(() -> new BusinessException(BOOK_NOT_FOUND));
if (ratingRepository.existByUserAndBook(user, book)) {
throw new BusinessException(RATING_ALREADY_EXIST);
}
// ์ค๋ณต ๋ฐ์ดํฐ ์ฒ๋ฆฌ
try {
Rating rating = ratingRepository.save(request.toEntity(user, book));
} catch (DataIntergrityViolationException e) {
throw new BusinessException(DATA_ALREADY_EXIST);
}
return RatingCreateResponse.of(rating, book);
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(DataIntergrityViolationException.class)
public ResponseEntity<ErrorResponse> handleDataIntergrityViolationException(
DataIntergrityViolationException e
) {
// ์ค๋ฅ ๋ฐ์ ์ ๋ฆฌํดํ ๋ฐ์ดํฐ๋ฅผ ์์ฑํ๋ ๋ก์ง ์์ฑ
...
}
}
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์๋น์ค ๋ก์ง์์ try/catch ๊ตฌ๋ฌธ์ ์ฌ์ฉํด ์์ธ๋ฅผ ์ฒ๋ฆฌํ๊ฑฐ๋, @ControllerAdvice์ ๊ฐ์ ๊ณตํต ์์ธ ์ฒ๋ฆฌ ํธ๋ค๋ฌ์์ ์์ธ๋ฅผ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. ์ ํฌ ์๋น์ค์์๋ @ControllerAdvice๋ฅผ ํตํด ์์ธ๋ฅผ ๊ณตํต ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ ์ ํํ์ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ต๋๋ค.
'Project' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
์ฑํ ๋ฉ์ธ์ง ์ฝ์ ์ฒ๋ฆฌ ๊ธฐ๋ฅ ๊ตฌ์กฐ ๊ฐ์ ๊ธฐ (0) | 2024.12.18 |
---|---|
Embedded Mongo/Redis ์ ์ฉํ๊ธฐ (0) | 2024.11.26 |
CompletableFuture๋ฅผ ํ์ฉํ ์ฑ๋ฅ ๊ฐ์ ๊ธฐ (0) | 2024.08.01 |