JPA ๋ฝ์ผ๋ก ๋์์ฑ ๋ฌธ์ ํด๊ฒฐํ๊ธฐ
๊ฐ์
ํ์ฌ ํ๋ก์ ํธ์์๋ ํํฐ ๊ธฐ๋ฅ์ ๊ตฌํ ์ค์
๋๋ค. ์ ์ ๊ฐ ์ต๋ ์ธ์์๋ฅผ ์ค์ ํ์ฌ ํํฐ๋ฅผ ์์ฑํ๋ฉด ํด๋น ์ธ์๊น์ง๋ง ์ฐธ์ฌํ ์ ์์ต๋๋ค.
๊ทธ๋ฐ๋ฐ ์ฌ๋ฌ ์ ์ ๊ฐ ๋์์ ์ฐธ์ฌ ์ ์ฒญ์ ํ๋ ๊ฒฝ์ฐ ์ฐธ์ฌ ์ธ์์ ๋ณ๊ฒฝ์ด ๋ฐ์๋๊ธฐ ์ ์ ๋ค๋ฅธ ์ค๋ ๋์์ ์ถ๊ฐ์ ์ธ ์ฐธ์ฌ ๋ก์ง์ด ์งํ๋์ด ์ต๋ ์ธ์์ ์ด๊ณผํ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
์๋ฐ์ ๋์์ฑ ์ ์ด ๋ฐฉ์
ํ์ฌ ํ๋ก์ ํธ๋ ๋จ์ผ JVM ํ๊ฒฝ์ด๊ธฐ ๋๋ฌธ์ ์๋ฐ ์์ค์ ๋์์ฑ ์ ์ด ๋ฐฉ์์ธ synchronized ํค์๋์ Lock ์ธํฐํ์ด์ค๋ฅผ ๋จผ์ ๊ฒํ ํด ๋ณด์์ต๋๋ค.
synchronized
์๋ฐ์ ๋ชจ๋ ๊ฐ์ฒด๋ ๋ด๋ถ์ ๋ชจ๋ํฐ ๋ฝ์ ๊ฐ๊ณ ์์ต๋๋ค. synchronized ํค์๋๋ฅผ ๋ฉ์๋ ํน์ ์ฝ๋ ๋ธ๋ก ๋จ์๋ก ์ ์ฉํ๋ฉด ๋ฝ์ ํ๋ํด์ผ๋ง ํด๋น ์์ญ์ ์ ๊ทผํ ์ ์์ด ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค.
์ฌ์ฉ ๋ฐฉ๋ฒ์ด ํธ๋ฆฌํ๋ค๋ ์ฅ์ ์ด ์์ง๋ง, ๋ฝ ํด์ ๊น์ง ์ค๋ ๋๊ฐ ๋ฌดํ์ ๋๊ธฐํด์ผ ํ๋ ๋ฌธ์ ์ ์ด๋ค ์ค๋ ๋๊ฐ ๋ฝ์ ํ๋ํ ์ง ์ ์ ์๋ค๋ ๊ณต์ ์ฑ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
Lock ์ธํฐํ์ด์ค
๋ํ์ ์ธ ๊ตฌํ์ฒด๋ก ReentrantLock์ด ์์ต๋๋ค. ๊ฐ์ฒด๊ฐ ๊ฐ๊ณ ์๋ ๋ชจ๋ํฐ ๋ฝ์ด ์๋ ๋ช ์์ ๋ฝ ๋ฉ์ปค๋์ฆ์ ์ ๊ณตํ์ฌ synchronized๋ณด๋ค ๋ ์ ์ฐํ ๋์์ฑ ์ ์ด๋ฅผ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค. ๊ทธ ์๋ก synchronized์ ๋จ์ ์ ๋ณด์ํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋๋ฐ, ํ์์์ ์ค์ ๊ณผ ์ธํฐ๋ฝํธ ํ์ฉ ๋ฑ์ ํตํด ๋ฌดํ ๋๊ธฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๊ณ , ๊ณต์ ๋ชจ๋ ์ค์ ์ ํตํด ๊ณต์ ์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค.
ํ์ง๋ง ์ด๋ฌํ ๋ฐฉ์๋ค์ ํธ๋์ญ์ ๋ฒ์ ์ ์ฒด๋ฅผ ์ ์ดํ์ง๋ ๋ชปํฉ๋๋ค. โ ํธ๋์ญ์ ์์๊ณผ ๋ฉ์๋ ์ง์ ์ฌ์ด, โก๋ฉ์๋ ์ข ๋ฃ์ ํธ๋์ญ์ ์ปค๋ฐ ์ฌ์ด์๋ ์ ์ด๊ฐ ๋ฟ์ง ์๊ธฐ ๋๋ฌธ์ ํธ๋์ญ์ ๋จ์์ ๋์์ฑ์ ์จ์ ํ ๋ณด์ฅํ ์ ์์ต๋๋ค.

์ฐธ๊ณ )
ํํฐ ์ฐธ์ฌ ์ธ์์๋ฅผ AtomicInteger๋ก ๊ด๋ฆฌํ๊ฑฐ๋ Redis์ ์์์ ์ฐ์ฐ์ ์ฌ์ฉํ๋ฉด ์ธ์์ ์ฆ๊ฐ ์์ฒด์ ์์์ฑ์ ๋ณด์ฅํ ์ ์์ต๋๋ค. ๊ทธ๋ฌ๋ ์ด ์ญ์ ์์ ๋ฐฉ์๋ค๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก ํธ๋์ญ์ ์ด ์ปค๋ฐ๋๊ธฐ ์ ์ ๋ค๋ฅธ ํธ๋์ญ์ ์์ ๊ฐ์ ๋ณ๊ฒฝํ ์ ์๊ธฐ ๋๋ฌธ์ ์ฌ์ ํ ๊ฐฑ์ ์์ค์ด ๋ฐ์ํ ์ ์์ต๋๋ค. ๋ํ DB ์ ๋ํฌ ์ ์ฝ ์กฐ๊ฑด์ ํ์ฉํ๋ ๋ฐฉ์๋ ๊ฒํ ํ์ง๋ง, ์ด๋ ๋์ผ ์ ์ ์ ์ค๋ณต ์ฐธ์ฌ๋ง ๋ง์ ์ ์์ ๋ฟ ์ต๋ ์ธ์ ์ด๊ณผ ์์ฒด๋ฅผ ๋ฐฉ์งํ๋ ์ฉ๋๋ก๋ ์ ํฉํ์ง ์์์ต๋๋ค.
ํธ๋์ญ์ ๋จ์์ ๋์์ฑ ์ ์ด ๋ฐฉ์
์์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ํธ๋์ญ์ ๋จ์๋ก ๋์์ฑ์ ์ ์ดํ ์ ์๋ JPA ๋ฝ ๋ฉ์ปค๋์ฆ์ ์ ์ฉํ๊ธฐ๋ก ๊ฒฐ์ ํ์ต๋๋ค. JPA๋ ํฌ๊ฒ ๋๊ด์ ๋ฝ๊ณผ ๋น๊ด์ ๋ฝ ๋ ๊ฐ์ง ๋ฐฉ์์ ์ ๊ณตํฉ๋๋ค.
๋๊ด์ ๋ฝ (Optimistic Lock)
์ถฉ๋์ด ๊ฑฐ์ ์์ ๊ฒ์ด๋ผ ๊ฐ์ ํ๊ณ DB ๋ฝ์ ์ฌ์ฉํ์ง ์๋ ๋ฐฉ์์ ๋๋ค. ์ํฐํฐ์ version ๊ฐ์ ๊ธฐ์ค์ผ๋ก ์ถฉ๋์ ๊ฐ์งํ์ฌ ์ปค๋ฐ ์์ ์ version ๊ฐ์ด ๋ณ๊ฒฝ๋์ด ์์ผ๋ฉด ํธ๋์ญ์ ์ ๋กค๋ฐฑํฉ๋๋ค.
๋ณ๋์ DB ๋ฝ์ ์ฌ์ฉํ์ง ์์ ๋น๊ด์ ๋ฝ๋ณด๋ค ์ฑ๋ฅ์ ์ ๋ฆฌํ ๊ฒฝ์ฐ๊ฐ ๋ง์ง๋ง, ์ถฉ๋ ๋ฐ์ ์ ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋๋ฏ๋ก ๋ง์ฝ ์ถฉ๋์ด ์ฆ์ ์ํฉ์ด๋ผ๋ฉด ์คํ๋ ค ์ฑ๋ฅ์ด ์ ํ๋ ์ ์์ต๋๋ค. (+ ๋ณ๋์ ์ฌ์๋ ๋ก์ง๋ ํ์ํฉ๋๋ค.)
๋น๊ด์ ๋ฝ (Pessimistic Lock)
์ถฉ๋์ด ์์ฃผ ๋ฐ์ํ ๊ฒ์ด๋ผ ๊ฐ์ ํ๊ณ DB ๋ฝ์ ์ฌ์ฉํ๋ ๋ฐฉ์์ ๋๋ค. SELECT ... FOR UPDATE ์ฟผ๋ฆฌ๋ฅผ ํตํด ๋ฐ์ดํฐ ์ ๊ทผ ์์ ๋ถํฐ ๋ค๋ฅธ ํธ๋์ญ์ ์ ์ ๊ทผ์ ์ฐจ๋จํฉ๋๋ค.
์ถฉ๋์ ์ฌ์ ์ ๋ฐฉ์งํ์ฌ ๋ฐ์ดํฐ ์ ํฉ์ฑ์ ํ์คํ๊ฒ ๋ณด์ฅํด ์ฃผ์ง๋ง, ๋๊ธฐ ์ํ์ ํธ๋์ญ์ ์ด ๋์ด๋ ์ฑ๋ฅ์ด ์ ํ๋ ์ ์์ต๋๋ค. ํนํ ์ฌ๋ฌ ํธ๋์ญ์ ์ด ์๋ก ๋ค๋ฅธ ๋ฐ์ดํฐ์ ๋ฝ์ ๊ฑด ์ํ๋ก ์ํธ ๋๊ธฐํ ๊ฒฝ์ฐ ๋ฐ๋๋ฝ์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
ํ์ฌ ์๋น์ค๋ ํธ๋ํฝ ๊ท๋ชจ๊ฐ ํฌ์ง ์์ ์ค์ ์ถฉ๋ ๊ฐ๋ฅ์ฑ์ด ๋ฎ๋ค๊ณ ํ๋จํ์ต๋๋ค. ๋ฐ๋ผ์ ํธ๋์ญ์ ๋ง๋ค ๋ฝ์ ์ ์ ํ๋ ๋น๊ด์ ๋ฝ์ ๋นํจ์จ์ ์ด๋ผ๊ณ ์๊ฐํ์ฌ ๋๊ด์ ๋ฝ์ ์ฑํํ์ต๋๋ค.
๋๊ด์ ๋ฝ ์ ์ฉ โ
Party ์ํฐํฐ ํด๋์ค
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Party extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private Long version;
...
}
@Version์ ํ์ฉํ์ฌ ๊ฐ๋จํ๊ฒ ๋ฒ์ ํ๋๋ฅผ ์ถ๊ฐํด ์ค๋๋ค.
Party ์๋น์ค ํด๋์ค
@Retryable(
retryFor = {OptimisticLockingFailureException.class},
backoff = @Backoff(delay = 100)
)
@Transactional
public void joinParty(String loginId, Long partyId) {
User user = userRepository.findByLoginId(loginId)
.orElseThrow(() -> new ApplicationException(USER_NOT_FOUND));
// ์ด๋ฏธ ํํฐ์ ์ฐธ์ฌ์ค์ธ ์ ์ ๋ ์ค๋ณต ์ ์ฒญ ๋ถ๊ฐ๋ฅ
if (partyUserRepository.existsByPartyIdAndParticipantId(partyId, user.getId())) {
throw new ApplicationException(PARTY_ALREADY_JOINED);
}
Party party = partyRepository.findByIdWithFetchJoin(partyId)
.orElseThrow(() -> new ApplicationException(PARTY_NOT_FOUND));
// ํํฐ ์ญ์ ์ฌ๋ถ ํ์ธ
if (party.isDeleted()) {
throw new ApplicationException(PARTY_ALREADY_DELETED);
}
// ํํฐ ๋ง๊ฐ ์ฌ๋ถ ํ์ธ
if (party.isClosed()) {
throw new ApplicationException(PARTY_ALREADY_CLOSED);
}
// ํํฐ ์์ฑ์๋ ๋ณธ์ธ์ ํํฐ์ ์ฐธ์ฌ ์ ์ฒญ ๋ถ๊ฐ๋ฅ
if (loginId.equals(party.getHost().getLoginId())) {
throw new ApplicationException(PARTY_CREATOR_CANNOT_JOIN);
}
// ์ธ์ ์ด๊ณผ ์ฌ๋ถ ๊ฒ์ฆ ๋ฐ ์ฐธ์ฌ์ ์ ์ฆ๊ฐ
party.incrementParticipantCount();
// partyUser ๊ฐ์ฒด ์ ์ฅ ๋ฐ ์ ๋ํฌ ์ ์ฝ ์กฐ๊ฑด ์๋ฐ ์์ธ ์ฒ๋ฆฌ
PartyUser partyUser = new PartyUser(party, user);
partyUserRepository.save(partyUser);
}
@Retryable์ ํ์ฉํ์ฌ OptimisticLockingFailureException ๋ฐ์ ์ ํํฐ ์ฐธ์ฌ ๋ก์ง์ ์๋์ผ๋ก ์ฌ์๋ํ๋๋ก ๊ตฌ์ฑํ์ต๋๋ค. ์์ ์ธ๊ธํ ๊ฒ์ฒ๋ผ ํ์ฌ ์๋น์ค๋ ์ถฉ๋ ๊ฐ๋ฅ์ฑ์ด ๋ฎ๋ค๊ณ ํ๋จํ๊ธฐ ๋๋ฌธ์, maxAttempts(์ฌ์๋ ํ์)์ multiplier(์ง์ ๋ฐฑ์คํ ์ฆ๊ฐ ๋น์จ)๋ ๊ธฐ๋ณธ๊ฐ์ธ ๊ฐ๊ฐ 3ํ, 0.0์ ๊ทธ๋๋ก ์ฌ์ฉํ์ต๋๋ค. ๋ํ ์์ฒญ๋์ด ๋ง์ง ์์ ์ฌ์๋ ๊ฐ๊ฒฉ์ ์ข ๋ ์งง๊ฒ ๊ฐ์ ธ๊ฐ๋ ๋ถ๋ด์ด ์๋ค๊ณ ํ๋จํ์ฌ delay๋ 100ms๋ก ์ค์ ํ์ต๋๋ค.
ํ ์คํธ
์ด๊ธฐ ๋ฐ์ดํฐ ๊ตฌ์ฑ
INSERT INTO users (id, login_type, login_id, password, nickname, profile_image, gender, birth_date, is_deleted)
VALUES (1, 'NORMAL', 'user1', 'password1', 'UserA', null, 'MALE', '1990-01-01', false),
(2, 'NORMAL', 'user2', 'password2', 'UserB', null, 'FEMALE', '1992-02-02', false),
(3, 'NORMAL', 'user3', 'password3', 'UserC', null, 'MALE', '1994-03-03', false),
...
(31, 'NORMAL', 'user31', 'password31', 'UserEE', null, 'MALE', '2005-02-24', false);
INSERT INTO party (id, title, image_url, max_capacity, deadline, is_closed, host_id)
VALUES (1, 'PartyA', null, 6, '2024-12-31T23:59:59', false, 1);
ํ ์คํธ ์ฝ๋
@DisplayName("๋์์ ์ฌ๋ฌ ํํฐ ๊ฐ์
์์ฒญ์ด ์๋ ํํฐ์ ์ต๋ ์ธ์์ ์ด๊ณผํ์ง ์์ต๋๋ค.")
@Test
void prevent_exceeding_max_capacity_with_concurrent_requests() throws InterruptedException {
int threadCount = 30;
ExecutorService executorService = Executors.newFixedThreadPool(4);
CountDownLatch latch = new CountDownLatch(threadCount);
Long partyId = 1L;
String loginIdPrefix = "user";
for (int i = 2; i <= threadCount + 1; i++) {
String loginId = loginIdPrefix + i;
executorService.submit(() -> {
try {
partyService.joinParty(loginId, partyId);
} catch (ApiException e) {
System.out.println(e.getErrorCode().getMessage());
} finally {
latch.countDown();
}
});
}
latch.await();
assertThat(partyUserRepository.countByPartyId(1L)).isEqualTo(5);
}
ํ ์คํธ ๊ฒฐ๊ณผ
๐ด JPA ๋ฝ ๋ฏธ์ ์ฉ

- ๋์ ์์ฒญ์ผ๋ก ์ธํด ์ต๋ ์ธ์์ ์ด๊ณผํ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค.
- ๊ฐ ํธ๋์ญ์ ์ด ๋ ๋ฆฝ์ ์ผ๋ก ์ฒ๋ฆฌ๋๋ฏ๋ก, ํ ํธ๋์ญ์ ์์ ์ด๋ฏธ ์ต๋ ์ธ์์ด ์ฑ์์ก๋๋ผ๋ ๋ค๋ฅธ ํธ๋์ญ์ ์์ ์งํ ์ค์ธ ํํฐ ์ฐธ์ฌ ๋ก์ง์ ๊ณ์ ์งํ๋ฉ๋๋ค.
๐ข JPA ๋ฝ ์ ์ฉ

- ๋๊ด์ ๋ฝ์ ์ ์ฉํ์ฌ ๋์ ์์ฒญ ์ํฉ์์๋ ๋ฐ์ดํฐ ์ ํฉ์ฑ์ ์ ์งํ ์ ์์๊ณ , ํํฐ์ ์ต๋ ์ธ์์ ์ด๊ณผํ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ง ์์์ต๋๋ค.
๊ฒฐ๊ณผ
JPA ๋๊ด์ ๋ฝ์ ์ ์ฉํ์ฌ ๋์ ์์ฒญ ์ํฉ์์๋ ํธ๋์ญ์ ๋จ์๋ก ํํฐ ์ธ์์ ๊ฒ์ฆ์ ์ฌ๋ฐ๋ฅด๊ฒ ์ฒ๋ฆฌํ์ฌ ์ฐธ์ฌ ์ธ์์ ์ ํํ๊ฒ ์ ์งํ ์ ์์์ต๋๋ค.
๋ํ ์ด ๊ณผ์ ์ ํตํด ์ฌ๋ฌ ํด๊ฒฐ ๋ฐฉ์์ ๊ฒํ ํ๊ณ , ํ์ฌ ์ํฉ์ ๊ฐ์ฅ ์ ํฉํ ๋ฐฉ์์ ์ ํํด ์ ์ฉํ๋ ๊ฒฝํ์ ํ ์ ์์์ต๋๋ค.