μ€λλ μ€μ μ€νμκ° λμ νλ‘μ νΈλ₯Ό μ§ννμκ³ , μν μ¬κ³ μ λν λΉκ΄μ λ½μ μ μ©νμλ€.
Index λ λ³λμ μΆκ°μ μΈ μμ
μ ν΅ν΄μ ν
μ΄λΈμ μ‘°ν μλλ₯Ό ν₯μ μμΌμ£Όλ μν μ ν©λλ€. μ±
μ΄λ μ‘μ§λ₯Ό λ³Όλ μ±
κ°νΌ μν μ Index κ° νλ κ² μ
λλ€.
μ‘°ν μ±λ₯μ ν₯μμ κΈ°λνκ³ , 무쑰건 Index λ₯Ό μ¬μ©νλ λ°©λ²μ μ³μ§ μμ λ°©λ² μ΄λΌκ³ ν©λλ€.
μ‘°ν κ³Όμ μ μννμ§ μλ : Insert 쿼리
μ‘°ν κ³Όμ μ μννμ§ μλ Insertμ κ²½μ°μλ μΆκ°μ μΈ μ°μ°μ΄ νμ νλ―λ‘ μ±λ₯ μ νλ₯Ό μΌμΌν¬μ μμ΅λλ€.
μ‘°ν κ³Όμ μ μννλ : Select, Delete, Update
Select, Delete, Updateλ μ‘°νλ‘ μμνλ©°, μ‘°νμ λν μ±λ₯μ ν₯μ λ©λλ€.
μΈλ ν€ κ΄κ³μμλ μμ£Ό μ¬μ©λλ 컬λΌμ μΈλ±μ€λ₯Ό μμ±νμ¬ JOIN μ°μ°μ μ±λ₯μ ν₯μμν¬ μ μμ΅λλ€.
ORDER BYλ GROUP BY μ μμ μ¬μ©λλ 컬λΌμ μΈλ±μ€λ₯Ό μΆκ°νλ©΄ μ λ ¬ λ° κ·Έλ£Ήν μμ μ΄ λΉ¨λΌμ§ μ μμ΅λλ€.
@Entity
@Getter
@Table(name = "product_cart", indexes = {
@Index(name = "idx_member_id", columnList = "member_id")
})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Cart {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@Column(name = "item_count_in_cart")
private Integer totalCount;
}
Member μν°ν° ν΄λμ€μ λν μΈλν€λ₯Ό κ°μ§κ³ μμΌλ©° κ°λ¨ν Cart(μΉ΄νΈ) μν°ν° ν΄λμ€ μ
λλ€.
@Table(indexes = { @Index(name = "μΈλ±μ€ μ΄λ¦", columnList = "λ°μ΄ν° λ² μ΄μ€μ μ½μ
λ μ΄λ¦, μ¬κΈ°μλ μ»¬λΌ μ΄λ¦")})
β‘ Indexλ μ¬λ¬κ° μμ±μ΄ κ°λ₯νλ©°, μ¬λ¬κ°μ 컬λΌμ μ¬μ©ν΄μ λ§λ€ μ μμ΅λλ€.
μ΄λ° λμμ± λ¬Έμ μ λν΄ ν΄κ²° ν μ μλλ°©λ²μ΄ μ¬λ¬κ°μ§ μ΄μ§λ§, μ¬λ¬ λΈλ‘κ·Έλ₯Ό μ°Ύμ보며 μ λ λ½ (Lock)μ μ μ© ν΄μΌκ² λ€κ³ μκ° νκ² λμμ΅λλ€.
μ΄ κΈμμλ λ½μ κ°λ , λΉκ΄μ λ½μ μ νν μ΄μ , λ½ μ¬μ© λ°©λ²μ λν΄μ μ 리 ν΄λ³΄κ² μ΅λλ€.
λ½μ λνμ μΈ λμμ± μ μ΄ κΈ°λ² μ€ νλλ‘, νΈλμμ μ μμ°¨μ μ§νμ 보μ₯ ν μ μλ μ§λ ¬ν μ₯μΉ μ λλ€.
- μΌλ°μ μΌλ‘ "λμμ" κ°μ λ°μ΄ν°λ₯Ό μ κ·Ό νλ κ²μ λ°©μ§
- λμμ λ°μνλ μ¦ κ°μ μκ°μ λμ μμ²μ ν΄κ²°ν λ μ¬μ©
κ°λ¨ν μμλ‘ μνμ μ¬κ³ λ₯Ό μμλ‘ λ€κ² μ΅λλ€.
β‘ μλ₯Ό λ€μ΄, A μ¬μ©μκ° μνμ μ¬κ³ λ₯Ό 5κ°λ‘ μ‘°ννκ³ , B μ¬μ©μλ "λμμ" 5κ°λ‘ μ‘°νν νμ λ μ¬μ©μκ° λμμ 1μ© κ°μμν€λ©΄,
μ€μ λ‘λ -1κ°μ μ¬κ³ κ° λ μ μμ΅λλ€.
μ¦ νκ°μ νΈλμμ μμ²λ§ λ°μμ΄ λλ€λ κ² μ λλ€.
β‘ μΆκ°μ μΈ μμλ‘ A μ¬μ©μκ° 5κ°μ μ¬κ³ λ₯Ό 4κ°λ‘ κ°μμν€κ³ , B μ¬μ©μκ° λμμ 5κ°μμ 3κ°λ‘ κ°μμν€λ©΄, B μ¬μ©μμ μ λ°μ΄νΈκ° λ°μλκ³
A μ¬μ©μμ μ λ°μ΄νΈλ μμ€λ μ μμ΅λλ€.
μ¦ λ§μ§λ§μ μ λ°μ΄νΈν κ°λ§ λ°μλκ³ λλ¨Έμ§λ μμ€λ μ μμ΅λλ€.
μμ κ°μ λ°μ΄ν°μ μΌκ΄μ±κ³Ό 무결μ±μ μ§ν€κΈ° μν΄ λ½μ μ¬μ©νμ¬ ν΄λΉ λ¬Έμ μ λμμ± μ μ΄ λ¬Έμ λ₯Ό μ¬μ μ λ°©μ§ ν μ μμ΅λλ€.
- JPA μμλ μΌλ°μ μΌλ‘
@Versionμ΄λ Έν μ΄μ μ μ¬μ©νλ©°, λ°ν νμ μΌλ‘λLong,Integerλ±μΌλ‘ μ¬μ©ν©λλ€.- μ½μ΄μ¬λ
Versionκ³Ό μμ ν νΈλμμ μ΄ μ»€λ° (commit) λλ μμ μVersionμ΄ λ€λ₯΄λ©΄ μΆ©λμ΄ λ°μνκ² λ©λλ€.update쿼리 λ°μμ verison νλμ λ°μ΄ν° κ°μ΄ 1μ© μ¦κ° ν©λλ€.
β‘ μμ ν λ°μ΄ν°λ₯Ό μ‘°νμ, μμμ μ μ νμ§ λ§κ³ , 컀λ°μ ν λ λμμ± λ¬Έμ κ° λ°μνλ©΄ κ·Έλ μ²λ¦¬ νμλ λ°©μ
- νΈλμμ μ μΆ©λμ΄ λ¬΄μ‘°κ±΄ μλ€κ³ κ°μ νμ¬, λ°μ΄ν°λ₯Ό μ‘°ν ν λ λΆν° λ½μ κ±°λ λ°©λ² μ λλ€.
- SQL 쿼리문μ΄
select for updateλ‘ λ°μνκ² λλ©°,
λ°μ΄ν°λ₯Ό μμ νκΈ° μν΄ μ°Ύμ κ² μ΄λ€.- λΉκ΄μ λ½μ μ¬μ©νκ² λλ©΄, νΈλμμ μ΄ λκΈ°νλ λμ 무νν κΈ°λ€λ¦΄μ μμΌλ―λ‘, νμμμ μκ°μ κ±Έμ΄ λμ μμ΅λλ€.
β‘ λ°μ΄ν° μ‘°νμλΆν° λ½μ κ±Έμ΄ λμμ± λ¬Έμ κ° λ°μνμ§ λͺ»νκ² λ―Έλ¦¬ μ²λ¦¬ νμλ λ°©μ
μν μ¬κ³ μ λν λ‘μ§μ΄ λ€μνκ² μ§λ§, μνμ μ¬κ³ κ° 0κ° λ°μΌλ‘ λ¨μ΄μ§κ² λλ€λ©΄ μ¬κ°ν λ¬Έμ μ΄κ³ λμμ μ¬λ¬λͺ μ μ¬μ©μμ μμ²
μ¦ μνμ μ¬κ³ λ₯Ό λ΄μμλλκ΄μ μΈ λ½μΈ κ²½μ° λ¨Όμ μμ²μ μ±κ³΅ν μ¬λμΈ κ²½μ° μ΄μΈμ λλ¨Έμ§μ μμ²λ€μ μΆ©λμ΄ λ°μνλλ°, κ·Έ λͺ¨λ μΆ©λμ μμΈμ²λ¦¬μ λ‘€λ°±μ μ€ν ν΄μΌ ν©λλ€.
κ·Έ λΉμ©μ΄ ν¨μ¬ λ λΉμ κ² κ°λ€λ μκ°μ νμκ³ ,
κ·Έμ λ°ν΄λΉκ΄μ λ½μ μμ λ μ¦μμ μ¬κ³ κ° 0κ°λ‘ 보μ΄κ² λ¨μΌλ‘ λ‘μ§μ μΌλ‘ 0κ°μ΄λ©΄ ꡬ맀λ₯Ό ν μ μκ² λ§κΈ°λ§ νλ€λ©΄, λ€λ₯Έ μΆκ°μ μΈ λΉμ©μ΄ λ€ κ² κ°μ§ μμλΉκ΄μ λ½μ μ¬μ© νμμ΅λλ€.
μν°ν° ν΄λμ€ νλμ @Version μ΄λ
Έν
μ΄μ
μ μ¬μ©νμ¬ μν°ν°μ "λ²μ "μ κ΄λ¦¬ν μ μμ΅λλ€.
μ£Όμ μ¬νμΌλ‘λ κ° μν°ν° ν΄λμ€μλ νλμ λ²μ μμ±λ§ μμ΄μΌ ν©λλ€.
κ·Έ λ€μμΌλ‘ μ€μ ν κ²μ
@LockModeTypeμ ν΅ν λ½ μ΅μ μ μ€μ ν΄μ€μ μμ΅λλ€.
(@LockModeType.PESSIMISTIC_WRITEλ κ°λ¨νκ² λ½μ κ±Έμ΄ μ‘°νλ κ°λ₯νλ°, μμ νκ±°λ μμ νλ κ²μ λ°©μ§)
λ€μν λ½ μ΅μ μ΄ μ‘΄μ¬νλλ° νμμ λ°λΌ μ μλ₯Ό μ°Ύμ보며 μ μ© νλ©΄ μ’μ κ² κ°μ΅λλ€.
λΉκ΄μ λ½μ λκ΄μ λ½κ³Ό λ¬λ¦¬ @Version μ΄λ
Έν
μ΄μ
μ μ¬μ©νμ§ μκ³
@Lock κ³Ό @LockModeType μ μ¬μ©ν©λλ€.
μλλ μνμ μ μ₯νκ³ μ₯λ°κ΅¬λμ λ΄λ νλμΈ removeStock λ©μλμ λμ μμ²μ λν λΉκ΄μ λ½ ν
μ€νΈ μ
λλ€.
@SpringBootTest
public class ItemCartPessimisticLockTest {
@Autowired
private ProductRepository productRepository;
@Autowired
private ItemCartServiceImpl itemCartService;
private Product product;
@BeforeEach
void init() {
product = Product.builder().productName("κ°μμ§ μ¬λ£")
.price("25_000")
.content("μ ν΅κΈ°ν 1λ
λ¨μ μ¬λ£μ
λλ€.")
.count(1000) // μνμ μ¬κ³ 1000κ° μ μ₯
.category(Category.PET_FEED)
.status("Y")
.build();
productRepository.save(product);
}
@Test
public void λμμ_1000κ°_μμ²() throws InterruptedException {
int threadCount = 1000; // 1000λ²μ μμ²
// 20κ°μ μ°λ λν μμ±
ExecutorService executorService = Executors.newFixedThreadPool(20);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
itemCartService.removeStock(product.getId(), 1);
} catch (Exception e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
latch.await();
Product updatedProduct = productRepository.findById(product.getId()).orElse(null);
Assertions.assertThat(updatedProduct).isNotNull();
Assertions.assertThat(updatedProduct.getCount()).isEqualTo(0);
}
}
μμ ν
μ€νΈ μ½λλ₯Ό μ€ν νκ² λλ©΄μ λμμ± μμ²μλ μμ°¨μ μΌλ‘ μνμ μ¬κ³ μ κ°―μκ° μ€μ΄λ€μ΄ 0κ° λ‘ μλ ΄νμ¬ ν
μ€νΈλ₯Ό ν΅κ³Ό νμμ΅λλ€.
select for update μΏΌλ¦¬κ° 1000κ°κ° λ°μνκ² λκ³ , μ΄λ¬ν 쿼리λ @LockModeType.PESSIMISTIC_WRITE μ μ΅μ
μΌλ‘ μ€μ ν¨μΌλ‘μ¨ μ°κΈ° μ κΈμ νλ€λ κ²μ μμ μμ΅λλ€.
π€ λ ΈνΈλΆμ λ°κΏλκ°.....