๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
BackEnd๐ŸŒฑ/DB & SQL

select .. for update ๋Œ€์ƒ ์œ ๋ฌด์— ๋”ฐ๋ฅธ ์ž ๊ธˆ ์ƒํƒœ

by ์•ˆ์ฃผํ˜• 2024. 4. 11.

๊ฐœ์š”

InnoDB ์—”์ง„์€ ๊ธฐ๋ณธ์ ์œผ๋กœ DDL ์ฟผ๋ฆฌ๋ฅผ ์ œ์™ธํ•œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์กฐ์ž‘ ์ž‘์—…์—์„œ ํ…Œ์ด๋ธ” ๋ฝ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๋ ˆ์ฝ”๋“œ ๊ธฐ๋ฐ˜์˜ ์ž ๊ธˆ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋” ์ž์„ธํžˆ๋Š” ๋ ˆ์ฝ”๋“œ ์ž์ฒด๋ณด๋‹ค๋Š” ์ธ๋ฑ์Šค์— ์ž ๊ธˆ์„ ์„ค์ •ํ•˜์—ฌ ์ด๋ฃจ์–ด์ง€๋ฉฐ, ํ…Œ์ด๋ธ”์— ๋ช…์‹œ์ ์ธ ์ธ๋ฑ์Šค๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ์—๋„ ๋‚ด๋ถ€์ ์œผ๋กœ ์ƒ์„ฑ๋œ ํด๋Ÿฌ์Šคํ„ฐ ์ธ๋ฑ์Šค๋ฅผ ํ†ตํ•ด ์ž ๊ธˆ์ด ์ด๋ฃจ์–ด์ง‘๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  Repetable-Read ๊ฒฉ๋ฆฌ ์ˆ˜์ค€์—์„œ InnoDB๋Š” record lock๊ณผ gap lock์„ ๊ฒฐํ•ฉํ•œ Next-key lock์„ ํ™œ์šฉํ•˜์—ฌ Phantom Read๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

์กฐ๊ฑด์— ๋ถ€ํ•ฉํ•˜๋Š” ํŠน์ • ํ–‰์„ ์ฐพ๊ธฐ ์œ„ํ•ด ์ธ๋ฑ์Šค๋ฅผ ์Šค์บ”ํ•˜๋Š” ๊ณผ์ •์—์„œ, InnoDB๋Š” ํ•ด๋‹น ์ธ๋ฑ์Šค ๋ ˆ์ฝ”๋“œ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๊ทธ ์ด์ „ ๊ณต๊ฐ„์—๋„ ์ž ๊ธˆ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ฒซ ๋ฒˆ์งธ ๋ฐœ๊ฒฌ๋œ ๋ ˆ์ฝ”๋“œ์™€ ์ฟผ๋ฆฌ๊ฐ€ ์ •์˜ํ•œ ๋ฒ”์œ„ ๋‚ด์˜ ๋นˆ ๊ณต๊ฐ„์—์„œ ์ƒˆ๋กœ์šด insert ์ž‘์—…์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด id ๊ฐ’์ด 1, 8, 12, 13, 16์ธ ๋ ˆ์ฝ”๋“œ๊ฐ€ ํฌํ•จ๋œ ํ…Œ์ด๋ธ”์— ๋Œ€ํ•ด ์•„๋ž˜์˜ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ด…์‹œ๋‹ค.

SELECT * FROM t_user WHERE id > 10 FOR UPDATE;

์ด ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด id๊ฐ€ 10 ๋ณด๋‹ค ํฐ ๋ ˆ์ฝ”๋“œ๋“ค์— ๋Œ€ํ•ด record lock์ด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฐ’(ex: 11)๊ณผ ์ฟผ๋ฆฌ ์กฐ๊ฑด ๋ฒ”์œ„ ๋ฐ–์— ์žˆ๋Š” ๊ฐ’(ex: 9)์— ๋Œ€ํ•ด์„œ๋„ ์‹ค์ œ๋กœ ์กด์žฌํ•˜๋Š” ๋ ˆ์ฝ”๋“œ ์‚ฌ์ด์˜ ๊ฐ„๊ฒฉ(gap)์— gap lock์ด ์„ค์ •๋˜์–ด, ํ•ด๋‹น ๋ฒ”์œ„ ๋‚ด์—์„œ ์ƒˆ๋กœ์šด ๋ ˆ์ฝ”๋“œ์˜ insert๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

  1. id > 10์„ ๋งŒ์กฑํ•˜๋Š” ์ฒซ ๋ฒˆ์งธ ์ธ๋ฑ์Šค ๋ ˆ์ฝ”๋“œ 12๋ฅผ ๋ฐœ๊ฒฌ
  2. ์ฒซ ๋ฒˆ์งธ ์ธ๋ฑ์Šค ๋ฐœ๊ฒฌ ์ง์ „์˜ ์ธ๋ฑ์Šค ๋ ˆ์ฝ”๋“œ id 8๋ถ€ํ„ฐ id 12 ์‚ฌ์ด์— gap lock ์ ์šฉ
  3. id> > 10 ์ธ ๋ชจ๋“  ์ธ๋ฑ์Šค ๋ ˆ์ฝ”๋“œ๋“ค์˜ ์‚ฌ์ด์—๋„ gap lock ์ ์šฉ
  4. id > 10์ธ ๋ชจ๋“  ์ธ๋ฑ์Šค ๋ ˆ์ฝ”๋“œ๋“ค์— ๊ฐ๊ฐ record lock ์ ์šฉ

์œ„ ์ง€์‹์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ €๋Š” 'select for update ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉ ์‹œ ํ…Œ์ด๋ธ”์— ์ด๋ฌด ๊ฐ’๋„ ์—†์œผ๋ฉด ๋ฝ์ด ๊ฑธ๋ฆฌ๊นŒ?'์— ๋Œ€ํ•œ ์˜๋ฌธ์ ์ด ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ๊ตฌ๊ฐ„์„ ์Šค์บ”ํ•˜์—ฌ ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ์ธ๋ฑ์Šค ๋ ˆ์ฝ”๋“œ์— ๋Œ€ํ•ด Lock์„ ์„ค์ •ํ•˜๋Š” ๊ณผ์ •์„ ๊ณ ๋ คํ–ˆ์„ ๋•Œ, ํ…Œ์ด๋ธ”์— ๋‹จ ํ•œ ๊ฐœ์˜ ๊ฐ’๋„ ์—†์œผ๋ฉด ์ž ๊ธˆ์„ ์„ค์ •ํ•  ๊ธฐ์ค€์ด ๋˜๋Š” ์ธ๋ฑ์Šค ๋ ˆ์ฝ”๋“œ๊ฐ€ ๋ถ€์žฌํ•˜๋ฏ€๋กœ ์ž ๊ธˆ์ด ์„ค์ •๋˜์ง€ ์•Š์„ ๊ฒƒ์ด๋ผ ์˜ˆ์ƒํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ์ƒํ™ฉ์„ ๋‘ ๊ฐ€์ง€ ์‹œ๋‚˜๋ฆฌ์˜ค ๋‚˜๋ˆ„๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ์–ด๋–ค ๊ฐ’(row)์ด ์กด์žฌํ•  ๋•Œ: select * for update๋ฅผ ํ†ตํ•ด ์ „์ฒด ๋ ˆ์ฝ”๋“œ๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ฒฝ์šฐ, ์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์—์„œ ์ƒˆ๋กœ์šด ๊ฐ’(row)์˜ ์ถ”๊ฐ€(Insert)๊ฐ€ ๊ฐ€๋Šฅํ• ๊นŒ?
  2. ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ๋‹จ ํ•œ ๊ฐœ์˜ ๊ฐ’(row)๋„ ์กด์žฌํ•˜์ง€ ์•Š์„ ๋•Œ: select * for update๋ฅผ ํ†ตํ•ด ์ „์ฒด ๋ ˆ์ฝ”๋“œ๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ฒฝ์šฐ, ์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์—์„œ ์ƒˆ๋กœ์šด ๊ฐ’(row)์˜ ์ถ”๊ฐ€(Insert)๊ฐ€ ๊ฐ€๋Šฅํ• ๊นŒ?

์‹ค์ œ ์˜ˆ์‹œ๋ฅผ ๋“ค์–ด, ์•„๋ž˜ MemberSerivce์—์„œ findAllWithLock ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ „์ฒด ํšŒ์›์„ ์กฐํšŒํ•˜๊ณ , ํŠน์ • ์กฐ๊ฑด์— ๋”ฐ๋ผ ์ƒˆ๋กœ์šด ํšŒ์›์„ ์ถ”๊ฐ€ํ•˜๋Š” ์ƒํ™ฉ์„ ๊ณ ๋ คํ–ˆ์Šต๋‹ˆ๋‹ค. Member ํ…Œ์ด๋ธ”์— 1๊ฐœ ์ด์ƒ์˜ ๊ฐ’(row)์˜ ์กด์žฌ ์œ ๋ฌด์— ๋”ฐ๋ผ, 10๊ฐœ๋ณด๋‹ค ๋งŽ์€ member๊ฐ€ ์ €์žฅ์ด ๋ ์ง€์— ๋Œ€ํ•ด ์ด ๊ธ€์„ ๋ณด์‹œ๋Š” ๋ถ„๋“ค๋„ ํ•œ๋ฒˆ ๊ณ ๋ฏผํ•ด ๋ณด์‹œ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    /**
     * ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ํ˜„์žฌ ๋ฉค๋ฒ„ ์ˆ˜๋ฅผ ์กฐํšŒํ•˜๊ณ , 10๋ช… ๋ฏธ๋งŒ์ธ ๊ฒฝ์šฐ ์ƒˆ๋กœ์šด ๋ฉค๋ฒ„๋ฅผ ์ถ”๊ฐ€.
     * ์ด ๊ณผ์ •์—์„œ SELECT ... FOR UPDATE๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋™์‹œ์„ฑ์„ ๊ด€๋ฆฌํ•˜๋ฉฐ,
     * findAllWithLock() ๋ฉ”์„œ๋“œ๊ฐ€ ๋ฉค๋ฒ„ ๋ชฉ๋ก์„ ์ž ๊ธˆ ์ƒํƒœ๋กœ ์กฐํšŒ
     */
    @Transactional
    public void registerMember() {
        if (memberRepository.findAllWithLock().size() < 10) {
            memberRepository.save(new Member());
        }
    }
    
}
public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query(value = "SELECT * FROM member for update", nativeQuery = true)
    List<Member> findAllWithLock();
}

๋จผ์ € ์ฒซ ๋ฒˆ์งธ ์‹œ๋‚˜๋ฆฌ์˜ค์ธ 'ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ์–ด๋–ค ๊ฐ’(row)์ด ์กด์žฌํ•  ๋•Œ, select * for update๋ฅผ ํ†ตํ•ด ์ „์ฒด ๋ ˆ์ฝ”๋“œ๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ฒฝ์šฐ ์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์—์„œ ์ƒˆ๋กœ์šด ๊ฐ’(row)์˜ ์ถ”๊ฐ€(Insert)๊ฐ€ ๊ฐ€๋Šฅํ• ๊นŒ?'์— ๋Œ€ํ•ด์„œ๋Š” ์ €๋Š” ์ฒ˜์Œ์— Member ํ…Œ์ด๋ธ”์— ํ˜„์žฌ ์–ด๋– ํ•œ ๊ฐ’์ด๋ผ๋„ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ๋ผ๋ฉด ์กด์žฌํ•˜๋Š” ๊ฐ’๋“ค์„ ๊ธฐ์ค€์œผ๋กœ ์‚ผ์•„ Next-Key Lock์„ ๊ฑธ์–ด 10๊ฐœ ๋ณด๋‹ค ๋งŽ์€ Member๊ฐ€ ์ €์žฅ๋˜์ง€ ์•Š์„ ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ฒฐ๋ก ์ ์œผ๋กœ ์ด๋Š” ์ •๋‹ต์ด์—ˆ์Šต๋‹ˆ๋‹ค.

๋‘ ๋ฒˆ์งธ ์‹œ๋‚˜๋ฆฌ์˜ค์ธ 'ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ๋‹จ ํ•œ ๊ฐœ์˜ ๊ฐ’(row)๋„ ์กด์žฌํ•˜์ง€ ์•Š์„ ๋•Œ, select * for update๋ฅผ ํ†ตํ•ด ์ „์ฒด ๋ ˆ์ฝ”๋“œ๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ฒฝ์šฐ ์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์—์„œ ์ƒˆ๋กœ์šด ๊ฐ’(row)์˜ ์ถ”๊ฐ€(Insert)๊ฐ€ ๊ฐ€๋Šฅํ• ๊นŒ?'์— ๋Œ€ํ•ด์„œ๋Š” ์ €๋Š” ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ์–ด๋–ค ๊ฐ’๋„ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ๊ธฐ์ค€์ด ๋  ์ธ๋ฑ์Šค๊ฐ€ ์—†์–ด Lock์ด ์„ค์ •๋˜์ง€ ์•Š์„ ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋™์‹œ์— ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ์—์„œ if๋ฌธ์„ ํ†ต๊ณผํ•˜์—ฌ 10๊ฐœ ์ด์ƒ์˜ Member๊ฐ€ ์ €์žฅ๋  ์ˆ˜ ์žˆ๋Š” ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด๋Š” ๊ฒฐ๋ก ์ ์œผ๋กœ ํ‹€๋ ธ์Šต๋‹ˆ๋‹ค.

 

 

ํ…Œ์ŠคํŠธ

์•„๋ž˜๋Š” select for update ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•  ๋•Œ, ์กฐํšŒ ๋Œ€์ƒ์ด ์—†๋Š” ๊ฒฝ์šฐ์—๋„ ์ž ๊ธˆ์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ๋Š”์ง€์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค. InnoDB๋ฅผ ์‚ฌ์šฉํ•˜๋Š” MariaDB ํ™˜๊ฒฝ์—์„œ ์ง„ํ–‰ํ–ˆ์œผ๋ฉฐ, ํŠธ๋žœ์žญ์…˜์˜ ๊ฒฉ๋ฆฌ ์ˆ˜์ค€์€ ๋ณ„๋„๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ๋ณธ๊ฐ’์ธ REPETABLE-READ๊ฐ€ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

MariaDB [(none)]> show variables like '%isol%';

+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
| tx_isolation          | REPEATABLE-READ |
+-----------------------+-----------------+

member ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•˜๊ณ , ์ดˆ๊ธฐ์—๋Š” ํ…Œ์ด๋ธ”์— ์–ด๋– ํ•œ ๊ฐ’๋„ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํƒœ๋กœ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

CREATE TABLE member (
    id bigint AUTO_INCREMENT PRIMARY KEY
);

MariaDB [test]> select * from member;

Empty set (0.002 sec)

 

1. ์กฐ๊ฑด ์—†๋Š” ์กฐํšŒ

ํ…Œ์ด๋ธ”์— ์•„๋ฌด๋Ÿฐ ์กฐ๊ฑด ์—†์ด select .. for update๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ์ „์ฒด ํ…Œ์ด๋ธ”์— ์ž ๊ธˆ์ด ๊ฑธ๋ฆฌ๋Š”์ง€ ํ™•์ธํ•ด ๋ด…์‹œ๋‹ค.

ํŠธ๋žœ์žญ์…˜ A์—์„œ๋Š” ๋จผ์ € select * from member for update๋ฅผ ์‹คํ–‰ํ•œ ํ›„, ์ƒˆ๋กœ์šด ๊ฐ’์„ ์ถ”๊ฐ€ํ•˜๋ฉฐ, ํŠธ๋žœ์žญ์…˜ B์—์„œ๋„ ๋™์ผํ•˜๊ฒŒ select * from member for update๋ฅผ ์‹คํ–‰ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.

-- Transaction A
start transaction;
select * from member for update;
insert into member (id) values(default);
commit;
-- Transaction B
start transaction;
select * from member for update;
insert into member (id) values(default);
commit;

๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด ํŠธ๋žœ์žญ์…˜ A๋Š” ์„ฑ๊ณต์ ์œผ๋กœ INSERT๋ฅผ ์ˆ˜ํ–‰ํ–ˆ์ง€๋งŒ, ์•„์ง commit์ด๋‚˜ rollback์„ ํ•˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ํŠธ๋žœ์žญ์…˜ B๋Š” ํŠธ๋žœ์žญ์…˜ A์˜ ์ž‘์—…์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ select ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜์ง€ ๋ชปํ•˜๊ณ  ๋Œ€๊ธฐ ์ƒํƒœ์— ๋†“์ด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ด๋ฅผ ํ†ตํ•ด select .. for update ๊ตฌ๋ฌธ์ด ์กฐํšŒ ๋Œ€์ƒ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ์‹ฌ์ง€์–ด ํ…Œ์ด๋ธ”์— ๋‹จ ํ•˜๋‚˜์˜ ๊ฐ’๋„ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์—๋„ ์ž ๊ธˆ์ด ๊ฑธ๋ฆฐ๋‹ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

MariaDB [test]> SELECT
    ->     r.trx_id AS waiting_trx_id,
    ->     r.trx_mysql_thread_id AS waiting_thread,
    ->     r.trx_query AS waiting_query,
    ->     b.trx_id AS blocking_trx_id,
    ->     b.trx_mysql_thread_id AS blocking_thread,
    ->     b.trx_query AS blocking_query,
    ->     l.lock_id AS requested_lock_id,
    ->     l.lock_mode AS requested_lock_mode,
    ->     l.lock_table,
    ->     l.lock_index,
    ->     l.lock_type
    -> FROM information_schema.innodb_lock_waits w
    -> INNER JOIN information_schema.innodb_locks l ON w.requested_lock_id = l.lock_id
    -> INNER JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id
    -> INNER JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;
   
+----------------+----------------+-------------------------------------------------------------------------------+
| waiting_trx_id | waiting_thread | waiting_query                                                                 |
+----------------+----------------+-------------------------------------------------------------------------------+
| 9044           | 6              | SET STATEMENT SQL_SELECT_LIMIT=501 FOR select * from member for update        |
+----------------+----------------+-------------------------------------------------------------------------------+
| blocking_trx_id| blocking_thread| blocking_query                                                                |
+----------------+----------------+-------------------------------------------------------------------------------+
| 9041           | 5              | NULL                                                                          |
+----------------+----------------+-------------------------------------------------------------------------------+
| requested_lock_id | requested_lock_mode | lock_table      | lock_index | lock_type                         |
+-------------------+---------------------+-----------------+------------+-----------------------------------+
| 9044:178:3:2      | X                   | `test`.`member` | PRIMARY    | RECORD                            |
+-------------------+---------------------+-----------------+------------+-----------------------------------+

์‹ค์ œ๋กœ information_schema ์กฐํšŒ๋ฅผ ํ†ตํ•ด ์ƒ์„ธํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด select * from member for update ์ฟผ๋ฆฌ๋Š” ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋”๋ผ๋„ ํ…Œ์ด๋ธ”์˜ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ๊ฐ€์ƒ ๋ ˆ์ฝ”๋“œ์— ๋Œ€ํ•ด ์ž ๊ธˆ์„ ์‹œ๋„ํ•œ๋‹ค๋Š” ๊ฒƒ์„ lock_type์ด record๋กœ ํ‘œ์‹œ๋˜์–ด ์žˆ๋Š” ๊ฒƒ์—์„œ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๋ก ์ ์œผ๋กœ ํŠธ๋žœ์žญ์…˜ A์™€ B๋Š” ํ…Œ์ด๋ธ”์˜ ํ…Œ์ด๋ธ”์— ๊ฐ’์ด ์—†์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ๋ชจ๋“  ๋ ˆ์ฝ”๋“œ๋ฅผ ๋Œ€์ƒ์œผ๋กœ Exclusive Lock์„ ์‹œ๋„ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ํŠธ๋žœ์žญ์…˜ A๊ฐ€ select * for member for update๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ์ž ๊ธˆ์„ ๊ฑธ๊ณ  ์žˆ๋‹ค๋ฉด ํŠธ๋žœ์žญ์…˜ B๋Š” ํŠธ๋žœ์žญ์…˜ A๊ฐ€ ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด์ „์˜ ์˜ˆ์‹œ ์ฝ”๋“œ์— ๋Œ€ํ•ด ์•„๋ž˜ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋ฉด 10๊ฐœ ์ดํ•˜์˜ ๋ฉค๋ฒ„๋งŒ ์ €์žฅ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋”๋ณด๊ธฐ
@SpringBootTest(classes = TestProjectApplication.class)
class MemberServiceConcurrentTest {

    @Autowired
    private MemberService memberService;

    @Autowired
    private MemberRepository memberRepository;

    @Test
    @DisplayName("10๊ฐœ ์ดํ•˜์˜ ๋ฉค๋ฒ„๋งŒ ๋“ฑ๋ก๋˜์–ด์•ผ ํ•œ๋‹ค.")
    void ensure_at_most_ten_members_registered_concurrently() throws InterruptedException {
        final int numberOfThreads = 200;
        CountDownLatch latch = new CountDownLatch(numberOfThreads);
        ExecutorService executor = Executors.newFixedThreadPool(64);

        for (int i = 0; i < numberOfThreads; i++) {
            executor.submit(() -> {
                try {
                    memberService.registerMember();
                }
                finally {
                    latch.countDown();
                }
            });
        }
        latch.await();

        long savedMembersCount = memberRepository.count();
        // ์ €์žฅ๋œ ๋ฉค๋ฒ„์˜ ์ˆ˜๊ฐ€ 10 ์ดํ•˜์ธ์ง€ ๊ฒ€์ฆ
        assertTrue(savedMembersCount <= 10, "๋“ฑ๋ก๋œ ๋ฉค๋ฒ„ ์ˆ˜๊ฐ€ 10 ์ดํ•˜์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.");
    }
}

 

2. ์„œ๋กœ ๋‹ค๋ฅธ ๋ฒ”์œ„์˜ ์กฐํšŒ

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋น„์–ด์žˆ๋Š” member ํ…Œ์ด๋ธ”์„ ๋Œ€์ƒ์œผ๋กœ ์ด๋ฒˆ์—๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ๋ฒ”์œ„๋ฅผ ๊ฐ€์ง„ select .. for update ์ฟผ๋ฆฌ๋ฅผ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์—์„œ ๋™์‹œ์— ์‹คํ–‰ํ•˜๋ฉด ์–ด๋–ป๊ฒŒ ๋˜๋Š”์ง€ ์‚ดํŽด๋ด…์‹œ๋‹ค. 

-- Transaction A
start transaction;
select * from member where id > 20 and id < 30 for update;
insert into member (id) values(default);
commit;
-- Transaction B
start transaction;
select * from member where id > 50 and id < 60 for update;
insert into member (id) values(default);
commit;

์ €๋Š” ์กฐ๊ฑด์ ˆ์— ๋ฒ”์œ„๋ฅผ ์ง€์ •ํ•ด ์ฃผ๋”๋ผ๋„ ํ…Œ์ด๋ธ”์— ๊ฐ’์ด ์—†์œผ๋ฏ€๋กœ ์ฒซ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ์™€ ๊ฐ™์ด ํ…Œ์ด๋ธ”์˜ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ๊ฐ€์ƒ ๋ ˆ์ฝ”๋“œ์— ๋Œ€ํ•ด ์ž ๊ธˆ์„ ์‹œ๋„ํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜ A์˜ ์ปค๋ฐ‹ ๋˜๋Š” ๋กค๋ฐฑ์ด ์™„๋ฃŒ๋˜๊ธฐ ์ „๊นŒ์ง€๋Š” ํŠธ๋žœ์žญ์…˜ B๊ฐ€ select .. for update ์‹คํ–‰ ๋ถ€๋ถ„์—์„œ ๋Œ€๊ธฐํ•  ๊ฒƒ์ด๋ผ ์˜ˆ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์‹ค์ œ๋กœ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ๋ฒ”์œ„์— ๋Œ€ํ•œ select .. for update ์ฟผ๋ฆฌ๋“ค์€ ๋™์‹œ์— ์‹คํ–‰๋˜์–ด, ๊ฐ๊ฐ์˜ ๋ฒ”์œ„์— ๋Œ€ํ•ด ๋…๋ฆฝ์ ์ธ ์ž ๊ธˆ์ด ์„ค์ •๋˜๋ฉฐ insert ๊ตฌ๋ฌธ์—์„œ ๋Œ€๊ธฐํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

MariaDB [test]> SELECT
    ->     r.trx_id AS waiting_trx_id,
    ->     r.trx_mysql_thread_id AS waiting_thread,
    ->     r.trx_query AS waiting_query,
    ->     b.trx_id AS blocking_trx_id,
    ->     b.trx_mysql_thread_id AS blocking_thread,
    ->     b.trx_query AS blocking_query,
    ->     l.lock_id AS requested_lock_id,
    ->     l.lock_mode AS requested_lock_mode,
    ->     l.lock_table,
    ->     l.lock_index,
    ->     l.lock_type
    -> FROM information_schema.innodb_lock_waits w
    -> INNER JOIN information_schema.innodb_locks l ON w.requested_lock_id = l.lock_id
    -> INNER JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id
    -> INNER JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;
   
+----------------+----------------+-----------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
| waiting_trx_id | waiting_thread | waiting_query                           | blocking_trx_id | blocking_thread | blocking_query | requested_lock_id | requested_lock_mode | lock_table      | lock_index | lock_type |
+----------------+----------------+-----------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
|           9049 |              6 | insert into member (id) values(default) |            9048 |               5 | NULL           | 9049:178:3:1      | X                   | `test`.`member` | PRIMARY    | RECORD    |
+----------------+----------------+-----------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+

์ด๋Ÿฌํ•œ ์ด์œ ๋Š” ๊ฐ ํŠธ๋žœ์žญ์…˜์ด ์„œ๋กœ ๋‹ค๋ฅธ ๋ฒ”์œ„๋ฅผ ๋Œ€์ƒ์œผ๋กœ select .. for update ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด, InnoDB๋Š” ์ง€์ •๋œ ๊ฐ ๋ฒ”์œ„์— ๋Œ€ํ•ด ๋…๋ฆฝ์ ์œผ๋กœ Gap Lock์„ ์„ค์ •ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ํ…Œ์ด๋ธ”์— ์‹ค์ œ๋กœ ์กด์žฌํ•˜๋Š” ๊ฐ’๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฐ’์— ๋Œ€ํ•ด์„œ๋„ ์ง€์ •๋œ ๋ฒ”์œ„์— ๋”ฐ๋ผ ๋ณ„๋„์˜ ์ž ๊ธˆ์ด ๊ฑธ๋ฆฝ๋‹ˆ๋‹ค.

๊ฒฐ๋ก ์ ์œผ๋กœ InnoDB๋Š” ์กฐํšŒ๋˜๋Š” ๊ฐ’์ด ์—†๋Š” ์ƒํ™ฉ์—์„œ๋„ ์กฐ๊ฑด์ ˆ์— ์ฃผ์–ด์ง„ ๋ฒ”์œ„์— ๊ธฐ๋ฐ˜ํ•˜์—ฌ ์ž ๊ธˆ์„ ์„ค์ •ํ•˜๋ฉฐ, ์„œ๋กœ ๋‹ค๋ฅธ ๋ฒ”์œ„๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•˜๋Š” ํŠธ๋žœ์žญ์…˜๋“ค์€ ๊ฐ์ž์˜ ์ง€์ •๋œ ๋ฒ”์œ„ ๋‚ด์—์„œ๋งŒ ์ž ๊ธˆ์„ ๋ฐ›๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ๋™์ผ ํ…Œ์ด๋ธ”์—์„œ ์กฐ๊ฑด์ ˆ์— ๋”ฐ๋ผ ์„œ๋กœ ๋‹ค๋ฅธ ๋ฒ”์œ„์— ๋Œ€ํ•œ ์ฟผ๋ฆฌ๋“ค์ด ๋™์‹œ์— ์ˆ˜ํ–‰๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
 

3. ๊ฒน์น˜๋Š” ๋ฒ”์œ„ ์กฐํšŒ

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋น„์–ด์žˆ๋Š” member ํ…Œ์ด๋ธ”์—์„œ ๊ฒน์น˜๋Š” ๋ฒ”์œ„๋ฅผ ๋Œ€์ƒ์œผ๋กœ select .. for update ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ, ์ด๋ฒˆ์—๋Š” ๊ฒน์น˜๋Š” ๋ถ€๋ถ„์—์„œ ์ž ๊ธˆ์ด ์–ด๋–ป๊ฒŒ ๊ฑธ๋ฆฌ๋Š”์ง€ ํ™•์ธํ•ด ๋ด…์‹œ๋‹ค.

-- Transaction A
start transaction;
select * from member where id > 10 and id < 20 for update;
insert into member (id) values(default);
commit;
-- Transaction B
start transaction;
select * from member where id > 15 and id < 25 for update;
insert into member (id) values(default);
commit;

์œ„ ๊ฒฐ๊ณผ๋Š” ์–ด๋–จ๊นŒ์š”? ํ…Œ์ŠคํŠธ 1๋ฒˆ๊ณผ 2๋ฒˆ๊ณผ ๊ฒฐ๊ณผ๋ฅผ ๋ดค์„ ๋•Œ ์ด๋ฒˆ ํ…Œ์ŠคํŠธ๋Š” ์ผ๋‹จ์€ ๋ฒ”์œ„๊ฐ€ ์ฃผ์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์— ์ฃผ์–ด์ง„ ๋ฒ”์œ„๋กœ ๋ฝ์ด ๊ฑธ์–ด์งˆ ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋˜๋ฉฐ, ๋ฒ”์œ„๊ฐ€ ๊ฒน์น˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„๋กœ ์ž ๊ธˆ์ด ๊ฑธ๋ฆฌ์ง€ ์•Š์•„ ํŠธ๋žœ์žญ์…˜ B๋Š” select ๋ถ€๋ถ„์—์„œ ํŠธ๋žœ์žญ์…˜ A๋ฅผ ๊ธฐ๋‹ค๋ฆด ๊ฒƒ์œผ๋กœ ๊ธฐ๋Œ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์ด๊ฒŒ ๋ฌด์Šจ ์ผ์ผ๊นŒ์š”? ์‹ค์ œ ๊ฒฐ๊ณผ๋Š” ํŠธ๋žœ์žญ์…˜ B์—์„œ select .. for update ์ฟผ๋ฆฌ๊ฐ€ ๋ฐ”๋กœ ์‹คํ–‰๋˜๋ฉฐ, ๊ฒน์น˜๋Š” ๋ฒ”์œ„์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ํŠธ๋žœ์žญ์…˜ A์˜ ์™„๋ฃŒ๋ฅผ select๊ฐ€ ์•„๋‹Œ insert ๊ตฌ๋ฌธ์—์„œ ๋Œ€๊ธฐํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด์œ ๋Š” ํŠธ๋žœ์žญ์…˜ A์™€ ํŠธ๋žœ์žญ์…˜ B๊ฐ€ ๊ฒน์น˜๋Š” ๋ฒ”์œ„์— ๋Œ€ํ•ด ์ž ๊ธˆ์„ ์‹œ๋„ํ•˜์ง€๋งŒ, ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— InnoDB๊ฐ€ Gap-Lock์„ ์‚ฌ์šฉํ•˜์—ฌ ์กฐํšŒ๋Š” ํ—ˆ๋ฝํ•˜๊ณ , ์ด ๋ฒ”์œ„์— ๋Œ€ํ•œ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ์˜ ์‚ฝ์ž…์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํŠธ๋žœ์žญ์…˜ B๊ฐ€ ๋Œ€๊ธฐํ•˜๋Š” ์œ„์น˜๋Š” ์‚ฝ์ž…์„ ์‹œ๋„ํ•˜๋Š” ์ง€์ ์ด ๋ฉ๋‹ˆ๋‹ค.

MariaDB [test]> SELECT
    ->     r.trx_id AS waiting_trx_id,
    ->     r.trx_mysql_thread_id AS waiting_thread,
    ->     r.trx_query AS waiting_query,
    ->     b.trx_id AS blocking_trx_id,
    ->     b.trx_mysql_thread_id AS blocking_thread,
    ->     b.trx_query AS blocking_query,
    ->     l.lock_id AS requested_lock_id,
    ->     l.lock_mode AS requested_lock_mode,
    ->     l.lock_table,
    ->     l.lock_index,
    ->     l.lock_type
    -> FROM information_schema.innodb_lock_waits w
    -> INNER JOIN information_schema.innodb_locks l ON w.requested_lock_id = l.lock_id
    -> INNER JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id
    -> INNER JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;
   
+----------------+----------------+-----------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
| waiting_trx_id | waiting_thread | waiting_query                           | blocking_trx_id | blocking_thread | blocking_query | requested_lock_id | requested_lock_mode | lock_table      | lock_index | lock_type |
+----------------+----------------+-----------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
| 9073           | 6              | insert into member (id) values(default) | 9072            | 5               | NULL           | 9073:179:3:1      | X                   | `test`.`member` | PRIMARY    | RECORD    |
+----------------+----------------+-----------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+

๋” ์ž์„ธํžˆ๋Š” ๋‹น๊ทผ ๊ธฐ์ˆ ๋ธ”๋กœ๊ทธ์˜ ์•„๋ž˜ ๋ถ€๋ถ„์„ ์ฐธ๊ณ ํ•˜๋ฉด ๋„์›€์ด ๋˜๋Š”๋ฐ, 

๋‘ ํŠธ๋žœ์žญ์…˜์ด ์„œ๋กœ ๋Œ€๊ธฐํ•˜์ง€ ์•Š๊ณ  select .. for update ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ์ด์œ ๋Š”, Gap Lock์ด Shared Lock์˜ ํ˜•ํƒœ๋กœ๋งŒ ์กด์žฌํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ฆ‰ ๋น„์–ด์žˆ๋Š” member ํ…Œ์ด๋ธ”์—์„œ ํŠธ๋žœ์žญ์…˜ A์™€ B๊ฐ€ ๊ฒน์น˜๋Š” ๋ฒ”์œ„์— ๋Œ€ํ•ด select .. for update ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•  ๋•Œ ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ํ•ด๋‹น ๋ฒ”์œ„์— ๋Œ€ํ•ด Gap Lock์ด ์ ์šฉ๋˜์ง€๋งŒ, Gap Lock์€ Shared Lock ํ˜•ํƒœ๋กœ๋งŒ ์กด์žฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์—์„œ ๋™์ผํ•œ ๋ฒ”์œ„์— ๋Œ€ํ•ด select .. for update๋ฅผ ์‹คํ–‰ํ•ด๋„ ์ž ๊ธˆ ๊ฒฝํ•ฉ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. 

๊ทธ๋ ‡๋‹ค๋ฉด ๋งŒ์•ฝ ๋ฒ”์œ„ ๋‚ด์ธ member id๊ฐ€ 16์ด๋ผ๋Š” ๊ฐ’์ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋Š” ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”?

MariaDB [test]> select * from member;

+----+
| id |
+----+
| 16 |
+----+

1 row in set (0.002 sec)

+----------------+----------------+--------------------------------------------------------------------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
| waiting_trx_id | waiting_thread | waiting_query                                                                                    | blocking_trx_id | blocking_thread | blocking_query | requested_lock_id | requested_lock_mode | lock_table      | lock_index | lock_type |
+----------------+----------------+--------------------------------------------------------------------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+
| 9086           | 6              | SET STATEMENT SQL_SELECT_LIMIT=501 FOR select * from member where id > 15 and id < 25 for update | 9083            | 5               | NULL           | 9086:180:3:2      | X                   | `test`.`member` | PRIMARY    | RECORD    |
+----------------+----------------+--------------------------------------------------------------------------------------------------+-----------------+-----------------+----------------+-------------------+---------------------+-----------------+------------+-----------+

ํ•ด๋‹น ๋ฒ”์œ„์— ํ•ด๋‹นํ•˜๋Š” ๊ฐ’์ด ์กด์žฌํ•œ๋‹ค๋ฉด ์ด๋ฒˆ์—๋Š” ํŠธ๋žœ์žญ์…˜ B๊ฐ€ select .. for update  ๊ตฌ๋ฌธ์—์„œ๋ถ€ํ„ฐ ํŠธ๋žœ์žญ์…˜ A์˜ ์ž‘์—… ์™„๋ฃŒ๋ฅผ ๋Œ€๊ธฐํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” select .. for update ๊ตฌ๋ฌธ์ด ํ•ด๋‹น ๋ฒ”์œ„ ๋‚ด ์กด์žฌํ•˜๋Š” ๋ชจ๋“  ์ธ๋ฑ์Šค ๋ ˆ์ฝ”๋“œ์— ๋Œ€ํ•ด ์ž ๊ธˆ์„ ์„ค์ •ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋ฉฐ, ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋Š” ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์ด ๊ทธ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์ž‘์—…์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ๊ธฐ์กด ํŠธ๋žœ์žญ์…˜์˜ ์™„๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ ค์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

  • ํŠธ๋žœ์žญ์…˜ A ์‹คํ–‰: ํŠธ๋žœ์žญ์…˜ A๋Š” select .. for update ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ id ๊ฐ’์ด 16์ธ ํ–‰์— ๋Œ€ํ•ด Exclusive Lock์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ํ–‰์„ ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜ ์ ‘๊ทผํ•˜๋Š” ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์˜ ์ž‘์—…์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  • ํŠธ๋žœ์žญ์…˜ B ๋Œ€๊ธฐ: ํŠธ๋žœ์žญ์…˜ B ์—ญ์‹œ ๊ฒน์น˜๋Š” ๋ฒ”์œ„์—์„œ select .. for update๋ฅผ ์‹คํ–‰ํ•˜์—ฌ id๊ฐ€ 16์ธ ํ–‰์„ ํฌํ•จํ•˜๋ ค ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด ํ–‰์€ ์ด๋ฏธ ํŠธ๋žœ์žญ์…˜ A์— ์˜ํ•ด ์ž ๊ฒจ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ํŠธ๋žœ์žญ์…˜ B๋Š” ํŠธ๋žœ์žญ์…˜ A์˜ ์ปค๋ฐ‹ ๋˜๋Š” ๋กค๋ฐฑ์ด ์™„๋ฃŒ๋˜์–ด ํ•ด๋‹น ์ž ๊ธˆ์ด ํ•ด์ œ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ ์ƒํƒœ์— ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค.

๊ฒฐ๋ก ์ ์œผ๋กœ ๊ฒน์น˜๋Š” ๋ฒ”์œ„ ์กฐํšŒ์˜ ๊ฒฝ์šฐ ๋น„์–ด ์žˆ๋Š” ํ…Œ์ด๋ธ” ์ƒํ™ฉ์—์„œ๋Š” select .. for update ๊ตฌ๋ฌธ์ด ์ฆ‰์‹œ ์‹คํ–‰๋˜๊ณ  ์‹ค์ œ ๋ฐ์ดํ„ฐ ์‚ฝ์ž… ์‹œ์ ์—์„œ๋งŒ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์˜ ์™„๋ฃŒ๋ฅผ ๋Œ€๊ธฐํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด ๋ฒ”์œ„ ๋‚ด์— ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ์กฐํšŒ ๋‹จ๊ณ„๋ถ€ํ„ฐ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์˜ ์™„๋ฃŒ๋ฅผ ๋Œ€๊ธฐํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

 

์ •๋ฆฌ

select .. for update๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ์˜ ์‚ฝ์ž…์„ ํšจ๊ณผ์ ์œผ๋กœ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Œ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์‹ค์ œ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ํ™•์ธํ–ˆ๋“ฏ์ด ์ž ๊ธˆ์ด ๋ฐœ์ƒํ•˜๋Š” ์‹œ์ ์ด ์ฃผ์–ด์ง„ ์ƒํ™ฉ์— ๋”ฐ๋ผ select ๋ช…๋ น์–ด์˜ ์‹คํ–‰ ๋‹จ๊ณ„์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜๋„ ์žˆ๊ณ , insert ๋ช…๋ น์–ด์˜ ์‹คํ–‰ ๋‹จ๊ณ„์—์„œ๋„ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์„ ๊ณ ๋ คํ•˜์—ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒฐ๊ณผ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ฃผ์˜ํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ์ฃผ์–ด์ง„ ์ƒํ™ฉ์— ๋”ฐ๋ผ select ๋ถ€๋ถ„์ด ์ •์ƒ์ ์œผ๋กœ ์‹คํ–‰๋˜์–ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”๋“œ์—์„œ something()๋กœ์ง์ด ์ˆ˜ํ–‰๋  ๊ฒฝ์šฐ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    @Transactional
    public void registerMember() {
        if (memberRepository.findAllWithLock().size() < 10) {
            something();
            memberRepository.save(new Member());
        }
    }
    
}

ํ”ผ๋“œ๋ฐฑ์€ ์–ธ์ œ๋“ ์ง€ ํ™˜์˜์ด๋ฉฐ ํ‹€๋ฆฐ ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด ๋Œ“๊ธ€ ๋‹ฌ์•„์ฃผ์‹œ๋ฉด ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 

์ฐธ๊ณ 

๋Œ“๊ธ€