[Anifriends] ๋ด์ฌ ์ ์ฒญ์์ ๋ฐ์ํ ๋์์ฑ ์ด์ ํด๊ฒฐ๊ธฐ
๐ ๊ฐ์
๋ฐ๋ธ์ฝ์ค 4๊ธฐ์์ ๋ด์ฌ ํ๋ซํผ์ธ Anifriends ๋ฅผ ๊ฐ๋ฐํ๋ฉด์ ๊ฒช์ ๋์์ฑ ์ด์ ํด๊ฒฐ๊ธฐ์ด๋ค. ๋ณดํธ์๊ฐ ๊ฒ์ํ ๋ด์ฌ ๋ชจ์ง์๋ ์ ์์ด ์กด์ฌํ๋ฉฐ ๋ด์ฌ์๋ค์ด ๋์์ ๋์ผํ ๋ด์ฌ ๋ชจ์ง์ ๋ด์ฌ ์ ์ฒญ์ ํ๋ ๊ฒฝ์ฐ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ ๋ฐฉ๋ฒ์๋ ์ฌ๋ฌ๊ฐ์ง๊ฐ ์๋ค. ๋น๊ด์ ๋ฝ, ๋๊ด์ ๋ฝ, Atomic Query, MySQL Named lock, Redis (Lettuce, Redisson)์ ์ฌ์ฉํ์ฌ ์ฑ๋ฅ์ ๋น๊ตํด๋ณด๊ณ ์ํ๋ค.
๐จ ๋์์ฑ ๋ฌธ์ ๋ฐ์
๋ด์ฌ๋ฅผ ์ ์ฒญํ์ ๋ ์ฒ๋ฆฌ๋๋ ๋ก์ง์ ๋ค์๊ณผ ๊ฐ๋ค.
@Transactional
public void registerApplicant(Long recruitmentId, Long volunteerId) {
Recruitment recruitment = getRecruitment(recruitmentId);
Volunteer volunteer = getVolunteer(volunteerId);
Applicant applicant = new Applicant(recruitment, volunteer);
applicantRepository.save(applicant);
}
- recruitmentId ์ ํด๋นํ๋ recruitment ๋ฅผ ์กฐํ
- volunteerId ์ ํด๋นํ๋ volunteer ๋ฅผ ์กฐํ
- applicant ์์ฑ
- ์ฐธ์ฌ ๊ฐ๋ฅํ(์ ์์ด ๊ฐ๋์ฐจ์ง ์์) recruitment ์ธ์ง ๊ฒ์ฆ
- recruitment ์ applicantCount + 1
- ์์ฑ๋ applicant ์ ์ฅ
๐งช ๋์์ฑ ํ ์คํธ
์ ์์ด 50๋ช ์ธ ๋ด์ฌ ๋ชจ์ง์ ๋ด์ฌ์ 100๋ช ์ด ๋์์ ๋ด์ฌ ์ ์ฒญ์ ํ๋ค๊ณ ๊ฐ์ ํ์๋ค. ์ ์์ ์ธ ๋ก์ง์์๋ 50๋ช ์ ๋ด์ฌ์๊ฐ ๋ด์ฌ ์ ์ฒญ์ ์ฑ๊ณตํ์ฌ ํด๋น ๋ด์ฌ ๋ชจ์ง์ ์ ์ฒญ์๋ 50๋ช ์ด์ด์ผํ๋ค.
@Test
@DisplayName("์ฑ๊ณต: 50๋ช
์ ์, 100๋ช
๋์ ์ ์ฒญ")
void registerApplicant() throws InterruptedException {
// given
int capacity = 50;
List<Volunteer> volunteers = VolunteerFixture.volunteers(capacity);
Recruitment recruitment = RecruitmentFixture.recruitment(shelter, capacity);
volunteerRepository.saveAll(volunteers);
recruitmentRepository.save(recruitment);
int poolSize = 100;
ExecutorService executorService = Executors.newFixedThreadPool(poolSize);
CountDownLatch latch = new CountDownLatch(poolSize);
// when
for (int i = 0; i < poolSize; i++) {
int finalI = i;
executorService.submit(() -> {
try {
applicantService.registerApplicant(shelter.getShelterId(),volunteers.get(finalI).getVolunteerId());
} finally {
latch.countDown();
}
});
}
latch.await();
// then
Recruitment persistRecruitment = recruitmentRepository.findById(recruitment.getRecruitmentId()).get();
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(getApplicants(persistRecruitment)).hasSize(capacity);
softly.assertThat(persistRecruitment.getApplicantCount()).isEqualTo(capacity);
});
}
์คํ ๊ฒฐ๊ณผ
ํ์ง๋ง ๊ฒฐ๊ณผ๋ 50๋ช ์ด์ด์ผํ๋ applicant๋ 9๋ช , recruitment์ applicant_count๋ 5๋ช ์ด๋ค.
๊ทธ ์ด์ ๋ ๋ ๊ฐ ์ด์์ ํธ๋์ญ์ ์์ ๊ณต์ ์์์ธ recruitment ์ ๋์์ ์ ๊ทผํ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค. ์ด๋ฌํ ์ํฉ์ ํฐ ๋ฌธ์ ๋ฅผ ์ด๋ํ๋ค.
์ฒซ ๋ฒ์งธ๋ ์ ๋ฐ์ดํธ ๋๋ฝ์ด๋ค. ๋ง์ฝ ์ ์์ด 1๋ช ์ธ ๋ด์ฌ ๋ชจ์ง์ 2๋ช ์ ์ฌ์ฉ์๊ฐ ๋์์ ์ ์ฒญ์ ํ๋ค๊ณ ๊ฐ์ ํ์. ๊ทธ๋ ๋ค๋ฉด ๋๊ฐ์ ํธ๋์ญ์ ์์ ๋ค์๊ณผ ๊ฐ์ด ์ ์ฒญ ์ธ์์ด 0์ธ recruitment ๋ฅผ ์กฐํํ๊ณ ์ ์ฒญ ์ธ์์ (0 + 1) ๋ช ์ผ๋ก ๊ฐฑ์ ํ๋ค. ๋ถ๋ช ํด๋น ๋ชจ์ง์ ์ ์์ 1๋ช ์ธ๋ฐ 2๋ช ์ ๋ด์ฌ์๊ฐ ์ ์ฒญ์ ์ฑ๊ณตํ๊ฒ ๋๋ค. ์ต์ข ์ ์ผ๋ก ์ ์ฒญ ์ธ์์ 1๋ช ์ผ๋ก ์ ์ฅ๋๋ฉฐ ์ ๋ฐ์ดํธ๊ฐ ๋๋ฝ๋๋ค. ์ด ๊ฒฝ์ฐ, ์ ํฉ์ฑ์ด ๋ง์ง ์๊ฒ ๋๊ณ ๋ด์ฌ ๋ชจ์ง์ ์ ์์ ์ด๊ณผํ์ฌ ๋ด์ฌ ์ ์ฒญ์ ํ๊ฒ ๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
๋ ๋ฒ์งธ๋ ๋ฐ๋๋ฝ์ด๋ค. ๋ก๊ทธ๋ฅผ ์ดํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ด ๋ฐ๋๋ฝ์ด ๋ฐ์ํ ๊ฒ์ ํ์ธํ ์ ์๋ค. ๋ฐ๋๋ฝ์ ๋ฌด์์ด๋ฉฐ ์ ๋ฐ์ํ๋๊ฑธ๊น?
๐ ๋ฐ๋๋ฝ
MySQL ๊ณต์ ๋ฌธ์์ ๋์์๋ ๋ฐ๋๋ฝ์ ์ ์๋ ๋ค์๊ณผ ๊ฐ๋ค.
A deadlock is a situation where different transactions are unable to proceed because each holds a lock that the other needs. Because both transactions are waiting for a resource to become available, neither ever release the locks it holds.
์๋ก ๋ค๋ฅธ ํธ๋์ญ์ ์ด ์๋ก์๊ฒ ํ์ํ ์ ๊ธ์ ๋ณด์ ํ๊ณ ์๊ธฐ ๋๋ฌธ์ ์งํํ ์ ์๋ ์ํฉ์ด๋ค. ๋ ํธ๋์ญ์ ๋ชจ๋ ๋ฆฌ์์ค๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ๋๊ธฐ๋ฅผ ๊ธฐ๋ค๋ฆฌ๊ณ ์๊ธฐ ๋๋ฌธ์ ์ด๋ ์ชฝ๋ ๋ณด์ ํ๊ณ ์๋ ์ ๊ธ์ ํด์ ํ์ง ๋ชปํ๋ค.
๊ทธ๋ ๋ค๋ฉด ํธ๋์ญ์ ์ด ์ด๋ค ์ ๊ธ์ ๋ณด์ ํ๊ธธ๋ ๋ฐ๋๋ฝ์ด ๋ฐ์ํ ๊ฑธ๊น? ๋ค์ ๋ช ๋ น์ด๋ฅผ ํตํด ๊ฐ์ฅ ์ต๊ทผ์ ๋ฐ์ํ ๋ฐ๋๋ฝ ์ ๋ณด๋ฅผ ํ์ธํด๋ณด์.
show engine innodb status;
.... ๋ณต์กํ๊ณ ๋ ์ํ๋ค. ์ค์ํ ๋ถ๋ถ๋ง ํ์ธํด๋ณด์.
1. 90105675๋ฒ ํธ๋์ญ์ ์ด recruitment ์ s-lock ์ ๋ณด์ ํ๋ค.
2. 90105676๋ฒ ํธ๋์ญ์ ์ด recruitment ์ s-lock ์ ๋ณด์ ํ๋ค.
3. 90105675๋ฒ ํธ๋์ญ์ ์ด recruitment ์ x-lock ์ ์ป๊ธฐ ์ํด ๋๊ธฐ ์ค์ด๋ค.
4. 90105676๋ฒ ํธ๋์ญ์ ์ด recruitment ์ x-lock ์ ์ป๊ธฐ ์ํด ๋๊ธฐ ์ค์ด๋ค.
5. ๋ ๋ฒ์งธ ํธ๋์ญ์ ์ฆ, 90105676๋ฒ ํธ๋์ญ์ ์ ๋กค๋ฐฑํ๋ค.
์ ๋ฆฌํด๋ณด๋ฉด ๋ ๊ฐ์ ํธ๋์ญ์ ์ด recruitment ์ ๋ํ s-lock ์ ๋ณด์ ํ๊ณ ์๋ ์ํ์์ x-lock ์ ์ป๊ธฐ๋ฅผ ๋๊ธฐํ๊ณ ์๋ค. x-lock ์ ์ป๊ธฐ ์ํด์ ์๋ก์ s-lock ๋ฐ๋ฉ์ด ํ์ํ๊ธฐ ๋๋ฌธ์ ๋ฐ๋๋ฝ์ด ๋ฐ์ํ ๊ฒ์ด๋ค.
๋ฐ๋๋ฝ ๋ฐ์ ์์ธ์ ๋ํด์๋ ํ์ ํ์ง๋ง, ํ๊ฐ์ง ์๋ฌธ์ด ์๋ค. recruitment ์ s-lock ์ค์ ์ ๋ฐ๋ก ํ์ง ์์๋๋ฐ s-lock ์ด ๊ฑธ๋ฆฐ ์ด์ ๊ฐ ๋ฌด์์ผ๊น?
๊ทธ ์ด์ ๋ FK ์ ์ฝ ์กฐ๊ฑด์ ์๋ค. MySQL ๊ณต์๋ฌธ์์ ๋ฐ๋ฅด๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋ด์ฉ์ ํ์ธํ ์ ์๋ค.
In an SQL statement that inserts, deletes, or updates many rows, foreign key constraints (like unique constraints) are checked row-by-row. When performing foreign key checks, InnoDB sets shared row-level locks on child or parent records that it must examine.
๋ง์ ํ์ ์ฝ์ , ์ญ์ ๋๋ ๊ฐฑ์ ํ๋ SQL๋ฌธ์์ ์ธ๋ํค ์ ์ฝ์กฐ๊ฑด(๊ณ ์ ์ ์ฝ์กฐ๊ฑด ๋ฑ)์ ํ๋จ์๋ก ๊ฒ์ฌ๋๋ค. ์ธ๋ํค ๊ฒ์ฌ๋ฅผ ์ํํ ๋ InnoDB๋ ๊ฒ์ฌํด์ผํ๋ ์์ ๋๋ ๋ถ๋ชจ ๋ ์ฝ๋์ shared row-level lock์ ์ค์ ํ๋ค.
Anifriends ์ ERD ์ผ๋ถ๋ถ์ ๋ค์๊ณผ ๊ฐ๋ค. recruitment(๋ชจ์ง๊ธ)์ applicant(๋ด์ฌ ์ ์ฒญ์)๊ฐ 1:N ๊ด๊ณ์ด๋ฉฐ applicant ๋ recruitment_id ๋ฅผ FK ๋ก ๊ฐ๋๋ค.
๋ฐ๋ผ์ applicant ๋ฅผ insert ํ ๋ FK ์ธ recruitment ์ s-lock ์ด ๊ฑธ๋ฆฌ๊ฒ ๋๋ ๊ฒ์ด๋ค. ๋ด์ฌ ์ ์ฒญ์ ์ฒ๋ฆฌํ๋ ๋ก์ง์ ๋ค์ ํ๋ฒ ์ดํด๋ณด์.
@Transactional
public void registerApplicant(Long recruitmentId, Long volunteerId) {
Recruitment recruitment = getRecruitment(recruitmentId);
Volunteer volunteer = getVolunteer(volunteerId);
Applicant applicant = new Applicant(recruitment, volunteer);
applicantRepository.save(applicant);
}
์ฐธ๊ณ ๋ก Hibernate flush() ์ ์ฟผ๋ฆฌ ๋์ ์์๋ ๋ค์๊ณผ ๊ฐ๋ค.
๋ฐ๋๋ฝ์ด ๋ฐ์ํ ๊ณผ์ ์ ๊ฐ๋จํ๊ฒ ์ ๋ฆฌํด๋ณด์.
- 1๋ฒ ํธ๋์ญ์ ์ด applicant ๋ฅผ insert ํ๋ ๊ณผ์ ์์ FK ์ธ recruitment ์ s-lock ํ๋
- 2๋ฒ ํธ๋์ญ์ ์ด applicant ๋ฅผ insert ํ๋ ๊ณผ์ ์์ FK ์ธ recruitment ์ s-lock ํ๋
- 1๋ฒ ํธ๋์ญ์ ์ด recruitment ์ applicant_count ๋ฅผ update ํ๊ธฐ ์ํด x-lock ํ๋ ๋๊ธฐ
- 2๋ฒ ํธ๋์ญ์ ์ด recruitment ์ applicant_count ๋ฅผ update ํ๊ธฐ ์ํด x-lock ํ๋ ๋๊ธฐ
- ๋ฐ๋๋ฝ ๋ฐ์
- 1, 2๋ฒ ํธ๋์ญ์ ์ค ํ๋์ ํธ๋์ญ์ ๋กค๋ฐฑ (MySQL Deadlock Detection)
โ ๋น๊ด์ ๋ฝ
๋น๊ด์ ๋ฝ์ด๋ ์์ ์์ฒญ์ ๋ฐ๋ฅธ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒ์ด๋ผ๊ณ ์์ํ๊ณ DB ๋ฝ์ ๊ฑธ์ด๋ฒ๋ฆฌ๋ ๋ฐฉ์์ด๋ค. ๋จผ์ ๋ฝ์ ํ๋ํ ์ค๋ ๋๋ง์ด ํด๋น ์์์ด ์ ๊ทผํ ๊ถํ์ ๊ฐ๊ฒ ๋๊ณ , ๋ค๋ฅธ ์ค๋ ๋๋ค์ ๋ฝ์ ํ๋ํ๊ธฐ ์ํด ๋๊ธฐํ๊ฒ ๋๋ค.
LockModeType ์ข ๋ฅ๋ ๋ค์๊ณผ ๊ฐ๋ค.
- PESSIMISTIC_READ: s-lock ์ ํ๋ํ๊ณ ๋ฐ์ดํฐ๊ฐ ์ ๋ฐ์ดํธ๋๊ฑฐ๋ ์ญ์ ๋๋ ๊ฒ์ ๋ฐฉ์ง
- PESSIMISTIC_WRITE: x-lock ์ ํ๋ํ๊ณ ๋ฐ์ดํฐ ์ฝ๊ธฐ(locking ์กฐํ์ผ ๊ฒฝ์ฐ), ์ ๋ฐ์ดํธ ๋๋ ์ญ์ ๋ฅผ ๋ฐฉ์ง
- PESSIMISTIC_FORCE_INCREMENT: PESSIMISTIC_WRITE ์ฒ๋ผ ์๋ํ๋ฉฐ ๋ฒ์ ์ด ์ง์ ๋ ์ํฐํฐ์ ๋ฒ์ ์์ฑ์ ์ถ๊ฐ๋ก ์ฆ๊ฐ
์กฐํ ์ x-lock ์ ํ๋ํ๋๋ก ํ๋๊ฒ ๋ชฉ์ ์ด๊ธฐ ๋๋ฌธ์ PESSIMISTIC_WRITE ๋ก ์ค์ ํ๋ค. (์ฐธ๊ณ ๋ก Mysql ์์๋ PESSIMISTIC_READ ๋ PESSIMISTIC_WRITE ์ฒ๋ผ ๋์ํ๋ค.)
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select r from Recruitment r where r.recruitmentId = :recruitmentId")
Optional<Recruitment> findByIdPessimistic(@Param("recruitmentId") Long recruitmentId);
๋น๊ด์ ๋ฝ์ ์ฌ์ฉํ ๊ฒฝ์ฐ select for update ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ค. select for update ๊ฐ ๋ฐ์ํ๋ฉด Exclusive lock (x lock)์ด ๊ฑธ๋ฆฌ๊ฒ ๋๋ค. x lock์ด ๊ฑธ๋ฆฌ๊ฒ ๋๋ฉด, ๋ค๋ฅธ ํธ๋์ญ์ ์์ s lock ์ด๋ x lock ์ ํ๋ํ ์ ์๋ค. ์ฆ, ๋ค๋ฅธ ํธ๋์ญ์ ์์๋ ์กฐํ, ์์ , ์ญ์ ๋ชจ๋ ๋ถ๊ฐ๋ฅํ๋ค.(๋จ, non-locking ์กฐํ๋ ๊ฐ๋ฅ) ์ฆ, x lock์ ํ๋ํ์ง ๋ชปํ ํธ๋์ญ์ ์ x lock์ ํ๋ํ ํธ๋์ญ์ ์ด ๋๋ ๋๊น์ง ๋๊ธฐํ๊ฒ ๋๋ค.
๋ฐ๋ผ์ ๋ ๊ฐ ์ด์์ ํธ๋์ญ์ ์ด ๋์์ recruitment ๋ฅผ ์กฐํํ์ฌ ์์ ํ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ง ์๊ฒ ๋๋ค.
โ ๋๊ด์ ๋ฝ
๋๊ด์ ๋ฝ์ด๋ DB ๋ฝ์ ์ฌ์ฉํ์ง ์๊ณ ์ ํ๋ฆฌ์ผ์ด์ ๋จ์์ version์ ์ด์ฉํ๋ค. ๋น๊ด์ ๋ฝ๊ณผ ๋ฌ๋ฆฌ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ง ์๋๋ค๊ณ ๊ฐ์ ํ๊ณ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด ๊ทธ๋ ๊ฐ์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ด๋ค. ์ํฐํฐ๋ฅผ ์์ ํ ๋(update ์ฟผ๋ฆฌ๊ฐ ๋ ์๊ฐ ๋) ๋ฒ์ ์ด ์ฆ๊ฐํ๋ค. ์ด๋, ์กฐํ ๋น์์ version๊ณผ ๋์ผํ version์ด๋ผ๋ฉด ์ ์์ ์ผ๋ก ๊ฐฑ์ ๋๊ณ ์๋๋ผ๋ฉด ์คํ๋ง ์ถ์ ์์ธ์ธ ObjectOptimisticLockingFailureException ๊ฐ ๋ฐ์ํ๋ค.
Recruitment Entity ์ version ํ๋๋ฅผ ์ถ๊ฐํด๋ณด์.
@Entity
public class Recruitment extends BaseTimeEntity {
@Version
private Long version;
}
์ด์ ๋ชจ์ง ์ํฐํฐ๊ฐ ๊ฐฑ์ ๋ ๋ ๋ง๋ค version์ด 1์ฉ ์ฆ๊ฐํ๋ค.
์๋ฅผ ๋ค์ด recruitment_id ๊ฐ 1์ธ ๋ชจ์ง์ ์กฐํํ์ ๋ version ์ด 0์ด์๋ค๋ฉด ๋ค์๊ณผ ๊ฐ์ ๊ฐฑ์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ค.
update recruitment
set applicant_count = 1,
version=1 // 0 + 1
where recruitment_id=1 and version=0
์ด๋, version์ด 0์ด๋ผ๋ฉด 1๋ก ๊ฐฑ์ ๋๊ณ , version์ด 0์ด ์๋๋ผ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์์ธ๊ฐ ๋ฐ์ํ๋ค.
๋๊ด์ ๋ฝ์ DB์ ๋ฝ์ ์ฌ์ฉํ์ง ์๊ณ ์ ํ๋ฆฌ์ผ์ด์ ๋จ์์ version ํ๋๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ๊ฐ๋ฐ์๋ ์ถ๊ฐ์ ์ผ๋ก ๋์ ์์ฒญ์ผ๋ก ์ธํ ์ถฉ๋์ด ๋ฐ์ํ์ ๋๋ฅผ ์ฒ๋ฆฌํด์ค์ผ ํ๋ค. ๋ค์๊ณผ ๊ฐ์ด update๋ฅผ ํ ๋ version ์ฐจ์ด๋ก ์ธํด ๋ฐ์ํ ์์ธ๋ฅผ catch ํ์ฌ ์ฌ์๋ํ๋๋ก ์ฒ๋ฆฌํ์๋ค.
๊ฐฑ์ ์์ ๋ฐ์ํ๋ ObjectOptimisticLockingFailureException ๋ฅผ ์ธ๋ถ์์ catch ํ๊ณ , ์ฌ์๋๋ฅผ ํ ๋ ๋ง๋ค ๊ฐฑ์ ๋ version ์ recruitment ๋ฅผ ์กฐํํ๊ธฐ ์ํด ํธ๋์ญ์ ์ ํ์์ฑ์ธ REQUIRES_NEW ๋ฅผ ์ฌ์ฉํ์ฌ ํธ๋์ญ์ ์ ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ถ๋ฆฌํ์๋ค. recruitment ๋ฅผ ๊ฐฑ์ ํ๋ ์ฟผ๋ฆฌ๋ ๋ถ๋ฆฌ๋ ํธ๋์ญ์ ์์ ์ฒ๋ฆฌ๋๊ธฐ ๋๋ฌธ์ ๋ฐ๋๋ฝ์ด ๋ฐ์ํ์ง ์๋๋ค.
@Service
@RequiredArgsConstructor
public class OptimisticLockQuantityService {
private final RecruitmentRepository recruitmentRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Recruitment increaseApplicantCount(long recruitmentId) {
Recruitment recruitment = getRecruitment(recruitmentId);
validateApply(recruitment);
recruitment.increaseApplicantCount();
return recruitment;
}
}
์กฐํ ๋น์์ version๊ณผ ๋์ผํ version์ด๋ผ๋ฉด ์ ์์ ์ผ๋ก ๊ฐฑ์ ๋๊ณ ์๋๋ผ๋ฉด ์์ธ๋ฅผ catch ํ์ฌ ์ฌ์๋ํ๋๋ก ๋ก์ง์ ์์ ํ๋ค.
@Transactional
public void registerApplicantWithOptimisticLock(Long recruitmentId, Long volunteerId) {
Volunteer volunteer = getVolunteer(volunteerId);
while (true) {
try {
Recruitment recruitment = optimisticLockQuantityService.increaseApplicantCount(recruitmentId);
Applicant applicant = new Applicant(recruitment, volunteer);
applicantRepository.save(applicant);
break;
} catch (ObjectOptimisticLockingFailureException e) {
log.info("์ถฉ๋์ด ๋ฐ์ํ์ต๋๋ค. ์ฌ์๋ํฉ๋๋ค.");
}
}
}
โ Atomic Query
Atomic Query ๋ ๋ฐ์ดํฐ๋ฅผ update ํ๋ ์์ ์์ ๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ์ด์ฉํด์ ๋ณ๊ฒฝํ๋ ๊ฒ์ ์๋ฏธํ๋ค.
where ์ ์ applicant_count < capacity ์กฐ๊ฑด์ ๊ฑธ์ด ํ์ฌ ๋น์๋ฆฌ๊ฐ ์๋ ๋ด์ฌ ๋ชจ์ง์ ์ ์ฒญ์ ์๋ฅผ + 1 ํ๋ Atomic Query ๋ฅผ ์์ฑํ๋ค. ํด๋น ์ฟผ๋ฆฌ๊ฐ ์คํ๋ ํ, update ๋ ํ์ ๊ฐ์๋ฅผ ๋ฐํํ๋ค.
(์ฟผ๋ฆฌ ๋ฌธ์ด ์คํ๋๋ฉด 1์ฐจ ์บ์์ ์๋ recruitment ์ db์ recruitment ์ applicant_count ๊ฐ์ด ๋ฌ๋ผ์ง๊ธฐ ๋๋ฌธ์ clearAutomatically=true ๋ฅผ ์ค์ ํ์ฌ 1์ฐจ ์บ์๋ฅผ ์ด๊ธฐํํด์ฃผ์๋ค.)
public interface RecruitmentRepository extends JpaRepository<Recruitment, Long> {
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query("update Recruitment r set r.applicantCount.applicantCount = r.applicantCount.applicantCount + 1 "
+ "where r.recruitmentId = :recruitmentId "
+ "and r.applicantCount.applicantCount < r.info.capacity")
int increaseApplicantCount(@Param("recruitmentId") long recruitmentId);
}
์ด์ ๋ฐ๋ผ ๋ณ๊ฒฝ๋๋ ์๋น์ค ๋ก์ง์ ๋ค์๊ณผ ๊ฐ๋ค. ์ด์ recruitment ๋ฅผ update ํ๊ธฐ ์ํด x-lock ์ด ๊ฑธ๋ฆฌ๊ฒ๋๊ณ ๋ค๋ฅธ ํธ๋์ญ์ ๋ค์ ๋๊ธฐํ๊ฒ ๋๋ค. recruitment ๊ฐฑ์ ์ ์ฑ๊ณตํ(๋ด์ฌ ์ ์ฒญ ๊ฐ๋ฅ) ํธ๋์ญ์ ์ Applicant ๋ฅผ ์์ฑํ์ฌ ์ ์ฅํ๋ค.
@Transactional
public void registerApplicant(Long recruitmentId, Long volunteerId) {
int updateCount = recruitmentRepository.increaseApplicantCount(recruitmentId);
if (updateCount == 0) {
throw new RuntimeException("๋ชจ์ง์ด ๋ง๊ฐ๋์์ต๋๋ค.");
}
Recruitment recruitment = getRecruitment(recruitmentId);
Volunteer volunteer = getVolunteer(volunteerId);
Applicant applicant = new Applicant(recruitment, volunteer);
applicantRepository.save(applicant);
}
โ MySQL Named Lock
Named Lock ์ด๋ ๊ณ ์ ํ ์ด๋ฆ์ผ๋ก ์๋ณ๋๋ ์ ๊ธ์ด๋ค. MySQL ์ด ์ ๊ณตํด์ฃผ๋ ํจ์๋ก ์ฌ์ฉ๊ฐ๋ฅํ๋ค. ์ ๊ณต๋๋ ํจ์์ ์ค๋ช ์ ๋ค์๊ณผ ๊ฐ๋ค.
GET_LOCK()
- ํ์ ์์์ ์ฌ์ฉํ์ฌ named lock ์ ์ป์ผ๋ ค๊ณ ์๋ํ๋ค (ํ์์์์ด ์์์ผ ๊ฒฝ์ฐ ๋ฌดํ ํ์์์)
- ๋ฐํ ๊ฐ๊ณผ ์๋ฏธ
- 1: ์ ๊ธ์ ์ฑ๊ณต์ ์ผ๋ก ์ป์
- 0: ํ์ ์์
- NULL: ์ค๋ฅ ๋ฐ์
- named lock ํด์ ์กฐ๊ฑด
- GET_LOCK()์ ํ ๋์ผํ ์ธ์ ์์ RELEASE_LOCK()์ ์คํํ์ฌ ๋ช ์์ ์ผ๋ก ํด์
- ์ธ์ ์ด ์ข ๋ฃ๋ ๋(์ ์ ๋๋ ๋น์ ์์ ์ผ๋ก) ์์์ ์ผ๋ก ํด์
- GET_LOCK()์ผ๋ก ์ป์ ์ ๊ธ์ ํธ๋์ญ์ ์ด ์ปค๋ฐ๋๊ฑฐ๋ ๋กค๋ฐฑ๋ ๋ ํด์ ๋์ง ์๋๋ค
- ์ฌ๋ฌ ๊ฐ์ ๋์ ์ ๊ธ์ ํ๋ํ ์ ์๋ค
RELEASE_LOCK()
- GET_LOCK()์ผ๋ก ์ป์ ๋ฌธ์์ด๋ก ๋ช ๋ช ๋ ์ ๊ธ์ ํด์ ํ๋ค.
- ๋ฐํ ๊ฐ๊ณผ ์๋ฏธ
- 1: ์ ๊ธ์ด ํด์ ๋จ
- 0: ์ด ์ธ์ ์ ์ํด ์ ๊ธ์ด ์ค์ ๋์ง ์์ ๊ฒฝ์ฐ(์ด ๊ฒฝ์ฐ ์ ๊ธ์ด ํด์ ๋์ง ์์)
- NULL: ๋ช ๋ช ๋ ์ ๊ธ์ด ์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ
์ด์ GET_LOCK()๊ณผ RELEASE_LOCK() ์ ์ฌ์ฉํ์ฌ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด๋ณด์.
์ฐ์ Named lock ํ๋๊ณผ ๋ฐํ์ ํ๋ RecruitmentLockRepository ๋ฅผ ๊ตฌํํ๋ค.
public interface RecruitmentLockRepository extends JpaRepository<Recruitment, Long> {
@Query(value = "SELECT GET_LOCK(:name, -1)", nativeQuery = true)
int getLock(@Param("name") String name);
@Query(value = "SELECT RELEASE_LOCK(:name)", nativeQuery = true)
int releaseLock(@Param("name") String name);
}
์ด์ ๋ด์ฌ ์ ์ฒญ์ ์ฒ๋ฆฌํ๋ ์๋น์ค ๋ก์ง์ ๋ค์๊ณผ ๊ฐ์ด ์์ ํด๋ณด์.
@Transactional
public void registerApplicant(Long recruitmentId, Long volunteerId) {
String lockName = LOCK_NAME_PREFIX + recruitmentId;
try {
int lock = recruitmentLockRepository.getLock(lockName);
validateLock(lock);
Recruitment recruitment = getRecruitment(recruitmentId);
Volunteer volunteer = getVolunteer(volunteerId);
Applicant applicant = new Applicant(recruitment, volunteer);
applicantRepository.save(applicant);
} finally {
recruitmentLockRepository.releaseLock(lockName);
}
}
์ด์ ๋ด์ฌ ์ ์ฒญ์ ์ฒ๋ฆฌํ๋ ๋ก์ง์ด ์คํ๋๊ธฐ ์ํด์ , GET_LOCK() ์ ํตํด lock ์ ์ป์ด์ผํ๋ค. ํ์ง๋ง ํ ์คํธ๋ฅผ ํด๋ณธ ๊ฒฐ๊ณผ ์ฌ์ ํ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค. ๋ํ ์ด์ํ ์ ์ applicant ์๋ 50๋ช ์ผ๋ก ํต๊ณผ๋์์ง๋ง, recruitment ์ applicant_count ๋ 50 ๋ฏธ๋ง์ ๊ฐ์ผ๋ก ๊ฐฑ์ ์์ค์ด ๋ฐ์ํ์๋ค.
๊ทธ ์ด์ ๋ ๋ค์๊ณผ ๊ฐ์ด Named lock ์ด ๋ฐํ๋๋ ์์ ๊ณผ ํธ๋์ญ์ ์ด ์ปค๋ฐ๋๋ ์์ ์ฌ์ด์ ๋ค๋ฅธ ํธ๋์ญ์ ์ด Named lock ์ ํ๋ํ์ฌ ๊ฐฑ์ ๋์ง ์์ recruitment ๋ฅผ ์กฐํํ๊ธฐ ๋๋ฌธ์ด๋ค.
Named lock ์ ํ๋/๋ฐํํ๋ ํธ๋์ญ์ ๊ณผ ๋ด์ฌ ์ ์ฒญ์ ์ฒ๋ฆฌํ๋ ํธ๋์ญ์ ์ ๋ถ๋ฆฌํ์ฌ ๋ด์ฌ ์ ์ฒญ์ ์ฒ๋ฆฌํ๋ ํธ๋์ญ์ ์ด ์ปค๋ฐ๋ ํ์ Named lock ์ ๋ฐํํ๋๋ก ์์ ํด๋ณด์.
Named lock ์ ํ๋/๋ฐํํ๋ ApplicantLockService.registerApplicant() ๋ฅผ ๊ตฌํํ๋ค.
@Service
@RequiredArgsConstructor
public class ApplicantLockService {
private static final String LOCK_NAME_PREFIX = "recruitment_";
private final RecruitmentLockRepository recruitmentLockRepository;
private final ApplicantService applicantService;
@Transactional
public void registerApplicant(Long recruitmentId, Long volunteerId) {
String lockName = LOCK_NAME_PREFIX + recruitmentId;
try {
int lock = recruitmentLockRepository.getLock(lockName);
validateLock(lock);
applicantService.registerApplicant(recruitmentId, volunteerId);
} finally {
recruitmentLockRepository.releaseLock(lockName);
}
}
}
ApplicantService.registerApplicant() ์ ํธ๋์ญ์ ์ ํ ์์ฑ์ REQUIRES_NEW ๋ก ์ค์ ํ์ฌ ํธ๋์ญ์ ์ ๋ถ๋ฆฌํ๋ค.
@Transactional(propagation = REQUIRES_NEW)
public void registerApplicant(Long recruitmentId, Long volunteerId) {
Recruitment recruitment = getRecruitment(recruitmentId);
Volunteer volunteer = getVolunteer(volunteerId);
Applicant applicant = new Applicant(recruitment, volunteer);
applicantRepository.save(applicant);
}
ํธ๋์ญ์ ์ ๋ถ๋ฆฌํ์๊ธฐ ๋๋ฌธ์ ์ปค๋ฅ์ ์ฌ์ฉ์ด ๋ ๋ฐฐ๊ฐ ๋๋ฏ๋ก ์ปค๋ฅ์ ํ์ 100์ผ๋ก ๋๋ ค์คฌ๋ค.
spring:
datasource:
hikari:
maximum-pool-size: 100
ํ ์คํธ ์ฝ๋์์ RecruitmentLockService.registerApplicant() ๋ฅผ ํธ์ถํ๋๋ก ์์ ํ๋ฉด ํ ์คํธ๊ฐ ํต๊ณผํ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
โ Redis
Redis ๋ฅผ ์ฌ์ฉํด์ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค. Java ์ Redis Client ์ธ Lettuce ์ Redisson ์ ์ฌ์ฉํ์ฌ ๋ฝ์ ๊ตฌํํ์๋ค.
Lettuce
Lettuce ์์๋ SETNX ๋ฅผ ์ฌ์ฉํ์ฌ ๋ฝ์ ๊ตฌํํ ์ ์๋ค. SETNX ์ ๋ฐํ๊ฐ๊ณผ ๊ทธ ์๋ฏธ๋ ๋ค์๊ณผ ๊ฐ๋ค.
- false: key ๊ฐ ์ด๋ฏธ ์กด์ฌํจ
- true: key ๊ฐ ์กด์ฌํ์ง ์๊ณ , ์๋ก ์์ฑํจ
key ๋ฅผ ํตํด Lock ์ ํ๋/๋ฐํํ๊ธฐ ์ํด RedisLockRepository ๋ฅผ ์์ฑํด๋ณด์. getLock() ์ ํตํด key ์ ํด๋นํ๋ lock ์ ํ๋ํ๊ณ releaseLock() ์ ํตํด key ์ ํด๋นํ๋ lock ์ ๋ฐํํ๋ค.
@Repository
public class RedisLockRepository {
private final RedisTemplate<String, Object> redisTemplate;
private final ValueOperations<String, Object> valueOperations;
public RedisLockRepository(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
this.valueOperations = redisTemplate.opsForValue();
}
public Boolean getLock(String key) {
return valueOperations.setIfAbsent(key, true);
}
public Boolean releaseLock(String key) {
return redisTemplate.delete(key);
}
}
์ด์ RedisLockRepository ๋ฅผ ์ฌ์ฉํ์ฌ ApplicantService.registerApplicant ๋ฅผ ๊ตฌํํด๋ณด์. getLock(String key) ์ผ๋ก key ์ ํด๋นํ๋ lock ์ด ์ด๋ฏธ ์กด์ฌํ๋ ๊ฒฝ์ฐ lock ์ ํ๋ํ ์ ์์ ๋๊น์ง ๋ฐ๋ณต๋ฌธ์ ๋๋ฆฐ๋ค. lock ์ ํ๋ํ์ ๊ฒฝ์ฐ ๋ด์ฌ ์ ์ฒญ ๋ก์ง์ด ์คํ๋๊ณ lock ์ ๋ฐํํ๋ค.
@Service
@Slf4j
@RequiredArgsConstructor
public class ApplicantLockService {
private static final String LOCK_NAME_PREFIX = "recruitment:lock:";
private final RedisLockRepository redisLockRepository;
private final ApplicantService applicantService;
public void registerApplicant(Long recruitmentId, Long volunteerId) {
String key = LOCK_NAME_PREFIX + recruitmentId;
while (!redisLockRepository.getLock(key)) {
log.info("Locking... {}", key);
}
try {
applicantService.registerApplicant(recruitmentId, volunteerId);
} finally {
redisLockRepository.releaseLock(key);
}
}
}
Redisson
Redisson ์ Redis pub/sub ๊ธฐ๋ฐ์ ๋ฝ์ ์ ๊ณตํ๋ค. ๋ฐ๋ผ์ Lettuce ์ ๊ฐ์ด ์คํ๋ฝ ๋ฐฉ์์ ์ฌ์ฉํ์ง ์์๋ ๋๋ค. trylock() ์ ํตํด timeout ๋์ lock ํ๋ ์๋ํ๊ณ lock ์ ๋ฐํํ๋๋ก ๊ตฌํํด๋ณด์.
@Service
@Slf4j
@RequiredArgsConstructor
public class ApplicantLockService {
private static final String LOCK_NAME_PREFIX = "recruitment:lock:";
private final RedissonClient redissonClient;
private final ApplicantService applicantService;
public void registerApplicant(Long recruitmentId, Long volunteerId) {
String key = LOCK_NAME_PREFIX + recruitmentId;
RLock lock = redissonClient.getLock(key);
try {
boolean acquireLock = lock.tryLock(10, TimeUnit.SECONDS);
if (!acquireLock) {
return;
}
applicantService.registerApplicant(recruitmentId, volunteerId);
} catch (InterruptedException e) {
log.error("Failed to acquire lock", e);
throw new RuntimeException("Failed to acquire lock");
} finally {
lock.unlock();
}
}
}
๐งช ์ฑ๋ฅ ํ ์คํธ
NGrinder ๋ฅผ ์ฌ์ฉํ์ฌ ์ฑ๋ฅ ํ ์คํธ๋ฅผ ์งํํ๋ค. ์ ์์ด 100๋ง๋ช ์ธ ๋ชจ์ง์ 20๋ช ์ด 1๋ถ ๋์ ๋์์ ๋ด์ฌ ์ ์ฒญ์ ํ๋๋ก ์คํฌ๋ฆฝํธ๋ฅผ ์์ฑํ์๋ค. ์ค์ ๋ด์ฌ ๋ชจ์ง์ ์ต๋๊ฐ์ 99๋ช ์ด์ง๋ง, ๋์ ์์ฒญ์ด ๋ฐ์ํ์ ๋ race condition ์ ๋ฐ์์ํค์ง ์์ผ๋ฉด์ ์ผ๋ง ๋งํผ์ ์ฑ๋ฅ์ ๋ผ ์ ์๋ ์ง์ ์ค์ ์ ๋ ์ฑ๋ฅํ ์คํธ์ด๊ธฐ ๋๋ฌธ์ ๋๋ํ 100๋ง๋ช ์ผ๋ก ์ค์ ํ์๋ค. ํ ์คํธ ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ๋ค.
๋น๊ด์ ๋ฝ | ๋๊ด์ ๋ฝ | Atomic Query |
![]() |
![]() |
![]() |
Named Lock | Redis(Lettuce) | Redis(Redisson) |
![]() |
![]() |
![]() |
๐ ๊ฒฐ๋ก
๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด 6๊ฐ์ง ๋ฐฉ๋ฒ์ผ๋ก ๊ตฌํํด๋ณด๊ณ ์ฑ๋ฅํ ์คํธ๋ฅผ ์งํํ์๋ค. ๊ฐ ๋ฐฉ๋ฒ์ ๋ํ ๋ด๊ฐ ์๊ฐํ๋ ์ฅ์ ๊ณผ ๋จ์ ์ ๋ค์๊ณผ ๊ฐ๋ค.
๋น๊ด์ ๋ฝ
- ์ฅ์
- ๋ถ์ฐ ์น ์๋ฒ ํ๊ฒฝ์์ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.
- ๊ตฌํ์ด ๋จ์ํ๋ค.
- ์ถฉ๋๋ก ์ธํ ๋กค๋ฐฑ์ ํ์ง ์์๋ ๋๋ค.
- ๋จ์
- ๋ถ์ฐ db ์๋ฒ ํ๊ฒฝ์์ ์ฌ์ฉ ๋ถ๊ฐ๋ฅํ๋ค.
- ๋ฝ์ ๊ฑธ๊ธฐ ๋๋ฌธ์ locking ์กฐํ๊ฐ ํ์ํ ๋ค๋ฅธ ์๋น์ค ๋ก์ง๋ ๋๊ธฐํด์ผํ๋ค.
๋๊ด์ ๋ฝ
- ์ฅ์
- ๋ถ์ฐ ์น ์๋ฒ์์ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.
- ์ถฉ๋์ด ์ ๊ฒ ๋ฐ์ํ๋ค๋ฉด ์ข์ ์ฑ๋ฅ์ ๋ผ ์ ์๋ค.
- ๋จ์
- ๋ถ์ฐ db ์๋ฒ ํ๊ฒฝ์์ ์ฌ์ฉ ๋ถ๊ฐ๋ฅํ๋ค.
- ์ถฉ๋์ด ๋ฐ์ํ์ ๋ ์ฌ์๋ ๋ก์ง์ ๊ตฌํํด์ผํ๋ค.
- ๋กค๋ฐฑ ๋ก์ง์ ๊ฐ๋ฐ์๊ฐ ์ง์ ๊ตฌํํด์ผํ๋ค.
- ๋งค๋ฒ version ์ด ๊ฐฑ์ ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ผํ๊ธฐ ๋๋ฌธ์ ํ์ํ ๊ฒฝ์ฐ ์ฌ์๋๋ฅผ ํ ๋๋ง๋ค ๋ฐ์ดํฐ๋ฅผ ์กฐํํด์์ผํ๋ค.
Atomic Query
- ์ฅ์
- ๋ถ์ฐ ์น ์๋ฒ์์ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.
- ํ๋ฒ์ ์ฟผ๋ฆฌ๋ก ์กฐ๊ฑด ํ์ธ + ๊ฐฑ์ ์ ์ฒ๋ฆฌํ ์ ์๋ค.
- ๋จ์
- ๋ถ์ฐ db ์๋ฒ ํ๊ฒฝ์์ ์ฌ์ฉ ๋ถ๊ฐ๋ฅํ๋ค.
- ๋๋ฉ์ธ ๋ก์ง์ด db ์ฟผ๋ฆฌ์ ์์กดํ๊ฒ ๋๋ค.
- ๋กค๋ฐฑ ๋ก์ง์ ๊ฐ๋ฐ์๊ฐ ์ง์ ๊ตฌํํด์ผํ๋ค.
- ์ฟผ๋ฆฌ๋ก ์กฐ๊ฑด์ ํ์ธํ๊ธฐ ๋๋ฌธ์ ๋ช ํํ ์์ธ ์ฒ๋ฆฌ๊ฐ ๋ถ๊ฐ๋ฅํ๋ค.
- 1์ฐจ ์บ์๋ฅผ ๋น์์ค์ผํ๊ธฐ ๋๋ฌธ์ ์ง์ฐ ๋ก๋ฉ์ ์ฌ์ฉํ๋ ค๋ฉด ์ถ๊ฐ ์กฐํ๊ฐ ํ์ํ๋ค.
Named Lock
- ์ฅ์
- db lock ์ ์ฌ์ฉํ์ง ์๋๋ค.
- ๋ถ์ฐ ์น ์๋ฒ, db ํ๊ฒฝ์์ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.
- ๋จ์
- lock ์ ํ๋/๋ฐ๋ฉ ์ํด db ์๋ฒ์์ ํต์ ์ด ํ์ํ๋ค.
- ์ถ๊ฐ์ ์ธ ์ปค๋ฅ์ ์ด ํ์ํ๋ค.
- MySQL ์ ์ข ์๋๋ค.
Redis Lettuce
- ์ฅ์
- db lock ์ ์ฌ์ฉํ์ง ์๋๋ค.
- ๋ถ์ฐ ์น ์๋ฒ, db ํ๊ฒฝ์์ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.
- ๋จ์
- ์คํ๋ฝ ๋ฐฉ์์ผ๋ก ์ธํด Redis ์ ๋ถํ๊ฐ ์ปค์ง๋ค.
Redis Redisson
- ์ฅ์
- db lock ์ ์ฌ์ฉํ์ง ์๋๋ค.
- ๋ถ์ฐ ์น ์๋ฒ, db ํ๊ฒฝ์์ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.
- ์ฌ์ฉํ๊ธฐ ์ฝ๋ค.
- ๋ณ๋์ ๋ฝ ํ๋ ๋ก์ง์ด ํ์ํ์ง ์๋ค.
- ๋จ์
- ๋ณ๋์ ์์กด์ฑ์ ์ถ๊ฐํด์ผํ๋ค.
๋ค์๊ณผ ๊ฐ์ ์ด์ ๋๋ฌธ์ ์ด๋ฒ ํ๋ก์ ํธ์์๋ ๋น๊ด์ ๋ฝ์ ์ ํํ์๋ค.
- ๋ถ์ฐ db ํ๊ฒฝ์ด ์๋๋ฏ๋ก ์ฑ๋ฅ์ด ๋ฎ์ ์ชฝ์ ์ํ๋ Redis ๋ฝ๊ณผ Named lock ์ ์ ์ธํ์๋ค.
- ๋ด์ฌ ์ ์ฒญ๊ฐ์ ๊ฒฝ์ฐ ํน์ ์์ผ์ด๋ ํน์ ๋ณดํธ์์ ๋ด์ฌ ๋ชจ์ง์์ ์ถฉ๋์ด ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ด ๋์ผ๋ฏ๋ก ๋๊ด์ ๋ฝ์ ์ ์ธํ์๋ค.
- Anifriends ์ ๋ด์ฌ ์ ์ฒญ์๋ ์ ์ฒญ ์ธ์ ๋ฟ๋ง ์๋๋ผ ๊ฒ์ฆํด์ผํ ์ฌํญ(์ ์ฒญ ๋ง๊ฐ ์ฌ๋ถ, ์ ์ฒญ ๋ง๊ฐ ์๊ฐ ๋ฑ)์ด ํ์ํ๊ธฐ ๋๋ฌธ์ ๋น์ง๋์ค ๋ก์ง์ด ์ฟผ๋ฆฌ์ ์์กดํ๋ ๊ฒ์ ๋ง๊ณ ๋ช ํํ ์์ธ์ฒ๋ฆฌ๋ฅผ ํ๊ธฐ ์ํด Atomic Query ๋ ์ ์ธํ์๋ค.
- ์ฑ๋ฅ์ด ๋์ ์ชฝ์ ์ํ๊ณ ๋น์ง๋์ค ๋ก์ง์ ์ดํ๋ฆฌ์ผ์ด์ ๋จ์์ ์ฒ๋ฆฌํ ์ ์๋ ๋น๊ด์ ๋ฝ์ ์ ํํ๋ค.
๋น๊ด์ ๋ฝ์ ์ฌ์ฉํ ๊ฒฝ์ฐ x-lock ์ผ๋ก ์ธํด locking ์กฐํ๊ฐ ํ์ํ ๋ค๋ฅธ ๋ก์ง๋ ๋๊ธฐํด์ผํ๋ค๋ ๋จ์ ์๋ค. ํ์ง๋ง ํ์ฌ ํ๋ก์ ํธ์์๋ ์ด์ ํด๋นํ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ๋ก์ง์ด ์๋ค. ์ถํ ํด๋น ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค๋ฉด FK ์ ์ฝ์กฐ๊ฑด์ ์ ๊ฑฐํ์ฌ ๋์์ฑ์ ๋์ด๋ ๋ฐฉ๋ฒ์ ๊ณ ๋ คํ ๊ฒ ๊ฐ๋ค.