πŸ“• Backend/DB Access

Spring을 μ‚¬μš©ν•˜μ§€ μ•Šκ³  Transaction ν•΄κ²°

Dongwoongkim 2023. 4. 4. 17:34

μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ λ°μ΄ν„°λ² μ΄μŠ€μ— λŒ€ν•œ νŠΈλžœμž­μ…˜μ„ μˆ˜ν–‰ν•˜κΈ° μœ„ν•΄μ„œ λ°μ΄ν„°λ² μ΄μŠ€ μ„œλ²„μ— 연결을 μš”μ²­ν•΄ 컀λ„₯μ…˜μ„ μ–»λŠ”λ‹€κ³  λ°°μ› μŠ΅λ‹ˆλ‹€.

이 λ•Œ λ°μ΄ν„°λ² μ΄μŠ€ μ„œλ²„λŠ” 사싀 내뢀에 μ„Έμ…˜μ„ λ§Œλ“­λ‹ˆλ‹€. 그리고 ν•΄λ‹Ή 컀λ„₯μ…˜μ„ μ΄μš©ν•œ λͺ¨λ“  μš”μ²­μ€ 각 컀λ„₯μ…˜μ˜ μ„Έμ…˜μ„ ν†΅ν•΄μ„œ μ‹€ν–‰ν•˜κ²Œ λ©λ‹ˆλ‹€. λ§Œμ•½ 컀λ„₯μ…˜ 풀을 μ΄μš©ν•œλ‹€λ©΄ 컀λ„₯μ…˜ 풀이 10개의 컀λ„₯μ…˜μ„ μƒμ„±ν•˜λ©΄ μ„Έμ…˜λ„ 10κ°œκ°€ λ§Œλ“€μ–΄μ§€κ² μ£ ?

 

νŠΈλžœμž­μ…˜ μ‚¬μš©λ²•

  • νŠΈλžœμž­μ…˜μ„ μ‚¬μš©ν•˜κΈ° μœ„ν•΄μ„  λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ set autocommit falseλ₯Ό ν˜ΈμΆœν•΄μ„œ μ„Έμ…˜μ—μ„œ λͺ…λ Ήμ–΄ μˆ˜ν–‰ ν›„ μžλ™μœΌλ‘œ commitλ˜μ§€ μ•Šλ„λ‘ μ„€μ •ν•΄ μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€. set autocommit false 섀정을 νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•œλ‹€κ³  ν‘œν˜„
  • 참고둜 ν•œλ²ˆ 섀정해두면 ν•΄λ‹Ή μ„Έμ…˜μ—μ„œλŠ” 섀정값이 계속 μœ μ§€ λ©λ‹ˆλ‹€.
  • 데이터 λ³€κ²½ 쿼리λ₯Ό μ‹€ν–‰ν•˜κ³  κ²°κ³Όλ₯Ό λ°˜μ˜ν•˜λ €λ©΄ commit을 ν˜ΈμΆœν•˜κ³ , κ²°κ³Όλ₯Ό λ°˜μ˜ν•˜κ³  μ‹Άμ§€ μ•ŠμœΌλ©΄ rollback을 ν˜ΈμΆœν•˜λ©΄ λ©λ‹ˆλ‹€.

κ³„μ’Œμ΄μ²΄ 예제λ₯Ό 톡해 μŠ€ν”„λ§μ„ μ‚¬μš©ν•˜μ§€ μ•Šμ€ μžλ°”μ—μ„œ μ–΄λ–»κ²Œ νŠΈλžœμž­μ…˜μ„ μˆ˜ν–‰ν•  수 μžˆλŠ”μ§€ μ•Œμ•„λ³΄λ„λ‘ ν•˜κ² μŠ΅λ‹ˆλ‹€.


κ³„μ’Œμ΄μ²΄ 문제 

1. κΈ°λ³Έ 데이터 μž…λ ₯ 

set autocommit true;
delete from member;
insert into member(member_id, money) values ('memberA',10000);
insert into member(member_id, money) values ('memberB',10000);

2. κ³„μ’Œμ΄μ²΄ μ‹€ν–‰ ( memberA -> memberB둜 2000원 μ†‘κΈˆ) 도쀑 였λ₯˜ λ°œμƒ

 

μ„Έμ…˜ 1번 νŠΈλžœμž­μ…˜ 
set autocommit false; // νŠΈλžœμž­μ…˜ μ‹œμž‘
update member set money=10000 - 2000 where member_id = 'memberA'; //성곡 
update member set money=10000 + 2000 where member_iddd = 'memberB'; //쿼리 μ˜ˆμ™Έ λ°œμƒ

 

  • ν•œ 번의 νŠΈλžœμž­μ…˜μ—μ„œ 였λ₯˜κ°€ λ°œμƒν•œ 경우 commit을 μˆ˜ν–‰ν•˜κ²Œ 되면 memberA의 money만 2000원이 μ€„μ–΄λ“œλŠ” λ¬Έμ œκ°€ λ°œμƒν•©λ‹ˆλ‹€.
  • μ΄λ ‡κ²Œ 쀑간에 λ¬Έμ œκ°€ λ°œμƒν•œ κ²½μš°μ—λŠ” commit을 ν˜ΈμΆœν•˜λŠ”κ²Œ μ•„λ‹ˆλΌ rollback을 ν˜ΈμΆœν•΄μ„œ 데이터λ₯Ό νŠΈλžœμž­μ…˜ μ‹œμž‘ μ‹œμ μœΌλ‘œ 원상볡ꡬ ν•΄μ•Όν•©λ‹ˆλ‹€. rollback을 μˆ˜ν–‰ν•˜λ©΄ λ‹€μ‹œ 1번 μƒνƒœλ‘œ λŒμ•„κ°‘λ‹ˆλ‹€.
  • 그런데 λ§Œμ•½ μ„Έμ…˜1이 νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•˜κ³  데이터λ₯Ό μˆ˜μ •ν•˜λŠ” λ™μ•ˆ 아직 컀밋을 μˆ˜ν–‰ν•˜μ§€ μ•Šμ•˜λŠ”λ°, μ„Έμ…˜ 2μ—μ„œ λ™μ‹œμ— 같은 데이터λ₯Ό μˆ˜μ •ν•˜κ²Œλ˜λ©΄ νŠΈλžœμž­μ…˜μ˜ μ›μžμ„±μ΄ κΉ¨μ§€κ²Œ 되고 λ§Œμ•½ μ„Έμ…˜1이 쀑간에 rollback을 ν•˜κ²Œ 되면 μ„Έμ…˜2λŠ” 잘λͺ»λœ 데이터λ₯Ό μˆ˜μ •ν•˜λŠ” λ¬Έμ œκ°€ λ°œμƒν•©λ‹ˆλ‹€.

 

이런 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ μ„Έμ…˜μ΄ νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•˜κ³  데이터λ₯Ό μˆ˜μ •ν•˜λŠ” λ™μ•ˆ λ‹€λ₯Έ μ„Έμ…˜μ—μ„œ ν•΄λ‹Ή λ°μ΄ν„°λ² μ΄μŠ€μ— μ ‘κ·Όν•˜μ§€ λͺ»ν•˜λ„둝 막아야 ν•©λ‹ˆλ‹€. μ΄λŠ” lock을 톡해 μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.


DB 락 - update μˆ˜μ • case

κΈ°λ³Έ 데이터가 λ°μ΄ν„°λ² μ΄μŠ€μ— μ €μž₯λ˜μ–΄ μžˆλŠ” μƒν™©μ—μ„œ 

 

μ„Έμ…˜1 / μ„Έμ…˜2

μ„Έμ…˜ 1μ—μ„œ νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•˜κ³  memberA moneyλ₯Ό 500μ›μœΌλ‘œ μ—…λ°μ΄νŠΈ. (아직 commit은 ν•˜μ§€ μ•Šμ€ μƒνƒœ)

이 λ•Œ memberA row에 λŒ€ν•œ 락은 μ„Έμ…˜1이 κ°€μ§€κ²Œ λ©λ‹ˆλ‹€. 

μ„Έμ…˜μ΄ νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•˜κ³  DB μˆ˜μ •μ˜ 경우 commitν•˜κΈ° μ „κΉŒμ§€ μžλ™μœΌλ‘œ 락을 κ°€μ§‘λ‹ˆλ‹€.

 

μ„Έμ…˜ 2λŠ” μ„Έμ…˜1이 락을 κ°€μ§€κ³  있기 λ•Œλ¬Έμ— μ„Έμ…˜ 1이 μ»€λ°‹ν•˜κ±°λ‚˜ λ‘€λ°±ν•˜μ§€ μ•Šμ•˜μœΌλ―€λ‘œ 데이터λ₯Ό μˆ˜μ •ν•  수 μ—†μŠ΅λ‹ˆλ‹€. 

(commitμ΄λ‚˜ rollback을 μˆ˜ν–‰ν•΄μ•Ό 락을 λ°˜λ‚©ν•©λ‹ˆλ‹€.)

  • SET LOCK_TIMEOUT = 60000 : 60초 내에 락을 μ–»μ§€ λͺ»ν•˜λ©΄ μ˜ˆμ™Έ λ°œμƒ (즉 , μ„Έμ…˜ 1μ—μ„œ 60초 내에 μ»€λ°‹μ΄λ‚˜ 둀백을 μˆ˜ν–‰ν•˜μ§€ μ•Šμ€ 경우 μ˜ˆμ™Έκ°€ λ°œμƒ)

DB 락 - select 쑰회 case

DB Updateκ°€ μ•„λ‹Œ select 쑰회λ₯Ό ν•  λ•Œμ—λŠ” update와 달리 μžλ™μœΌλ‘œ 락을 κ°–μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— for update ꡬ문을 μΆ”κ°€μ μœΌλ‘œ μ„€μ •ν•΄ 락을 μˆ˜λ™μœΌλ‘œ μ–»κ²Œ ν•΄μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€. 

 

select 쑰회λ₯Ό ν•  λ•Œμ— lock을 μ–»μ–΄μ•Ό ν•˜λŠ” κ²½μš°λŠ” μ–Έμ œμΌκΉŒμš”?

예λ₯Ό λ“€μ–΄μ„œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ‘œμ§μ—μ„œ memberA μ˜ κΈˆμ•‘μ„ μ‘°νšŒν•œ λ‹€μŒμ— 이 κΈˆμ•‘ μ •λ³΄λ‘œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ μ–΄λ–€ 계산을 μˆ˜ν–‰ν•˜λŠ”λ°, μ΄ 계산이 돈과 κ΄€λ ¨λœ 맀우 μ€‘μš”ν•œ κ³„μ‚°μ΄μ–΄μ„œ 계산을 μ™„λ£Œν•  λ•Œ κΉŒμ§€ memberA μ˜ κΈˆμ•‘μ„ λ‹€λ₯Έκ³³μ—μ„œ λ³€κ²½ν•˜λ©΄ μ•ˆλ˜λŠ” 경우 μ΄λŸ΄ λ•Œ 쑰회 μ‹œμ μ— 락을 νšλ“ν•΄μ•Ό ν•©λ‹ˆλ‹€.

이럴 λ•ŒλŠ” select for update ꡬ문을 μ‚¬μš©ν•˜λ©΄  λ©λ‹ˆλ‹€.

μ΄λ ‡κ²Œ ν•˜λ©΄ μ„Έμ…˜1이 쑰회 μ‹œμ μ— 락을 가져가버리기 λ•Œλ¬Έμ— λ‹€λ₯Έ μ„Έμ…˜μ—μ„œ ν•΄λ‹Ή 데이터λ₯Ό λ³€κ²½ν•  수 μ—†μŠ΅λ‹ˆλ‹€.

μ„Έμ…˜ 1 / μ„Έμ…˜ 2
  • μ„Έμ…˜ 1이 commit을 ν•΄μ•Ό μ„Έμ…˜ 2둜 락이 λ„˜μ–΄κ°€ updateλ₯Ό μˆ˜ν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • μ„Έμ…˜ 2 λ˜ν•œ νŠΈλžœμž­μ…˜μ΄λ―€λ‘œ commit을 μˆ˜ν–‰ν•΄μ•Ό updateκ°€ λ°˜μ˜λ©λ‹ˆλ‹€.

μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ νŠΈλžœμž­μ…˜ μ μš©ν•˜κΈ°

이제 μ½”λ“œλ₯Ό 톡해 μŠ€ν”„λ§ μ—†λŠ” μžλ°”μ—μ„œ DB에 μ ‘κ·Όν•΄ κ³„μ’Œμ΄μ²΄ νŠΈλžœμž­μ…˜μ„ κ΅¬ν˜„ν•΄λ³΄κ³  정상 μ²˜λ¦¬λœκ²½μš°μ™€ μ˜ˆμ™Έκ°€ λ°œμƒν•œ 경우 두 κ°€μ§€ μΌ€μ΄μŠ€μ— λŒ€ν•΄ ν…ŒμŠ€νŠΈ 해보도둝 ν•˜κ² μŠ΅λ‹ˆλ‹€. 

  • κ³„μ’Œμ΄μ²΄μ˜ 경우 ν•œ νŠΈλžœμž­μ…˜ λ‚΄μ—μ„œ 두 멀버λ₯Ό μ°Ύμ•„(findById), 두 λ©€λ²„μ˜ λˆμ„ κ°±μ‹ ν•΄μ£Όμ–΄μ•Ό(update) ν•©λ‹ˆλ‹€.
  • ν•˜μ§€λ§Œ 이 λ•Œ μ£Όμ˜ν•΄μ•Ό ν•  점은 νŠΈλžœμž­μ…˜ 이기 λ•Œλ¬Έμ— λ™μΌν•œ 컀λ„₯μ…˜ λ‹€μ‹œλ§ν•΄ 같은 μ„Έμ…˜μ—μ„œ 이루어져야 ν•©λ‹ˆλ‹€.
  • λ”°λΌμ„œ 이전과 달리 멀버λ₯Ό μ°Ύκ³ (findById), κ°±μ‹ (update) ν•΄ 쀄 λ•Œμ— 컀λ„₯μ…˜μ„ μƒˆλ‘œ λ¦¬ν„΄λ°›λŠ” 것이 μ•„λ‹Œ ν•œ 컀λ„₯μ…˜μ„ μ­‰ μ‚¬μš©ν•˜λ„λ‘ μ„€μ •ν•΄ μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€. 
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    Connection con = dataSource.getConnection();
    try
    {
        con.setAutoCommit(false); // νŠΈλžœμž­μ…˜ μ‹œμž‘
        buisnesslogic(con, fromId, toId, money);
        con.commit();
    } catch (Exception e) {
        log.error("error detected!");
        con.rollback();
        throw new IllegalStateException(e);
    } finally {
        release(con);
    }
}
private void release(Connection con) {
    if(con !=null)
    {
        try{
            con.setAutoCommit(true); // 컀λ„₯μ…˜ ν’€ κ³ λ €
            con.close();
        }catch (Exception e)
        {
            log.error("error", e);
        }
    }
}
private void buisnesslogic(Connection con, String fromId, String toId, int money) throws SQLException {
    // λΉ„μ¦ˆλ‹ˆμŠ€ 둜직
    Member fromMember = memberRepository.findById(con, fromId);
    Member toMember = memberRepository.findById(con, toId);

    memberRepository.update(con, fromId, fromMember.getMoney()- money);
    validation(toMember);
    memberRepository.update(con, toId,toMember.getMoney()+ money);
}
  • νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•˜κΈ° μœ„ν•΄ con.setAutoCommit을 false둜 μ„€μ •ν•©λ‹ˆλ‹€
  • κ³„μ’Œμ΄μ²΄ μˆ˜ν–‰ 도쀑 μ˜ˆμ™Έκ°€ λ°œμƒν•œ 경우 rollbackν•˜λ„λ‘ μ„€μ •ν•©λ‹ˆλ‹€.
  • νŠΈλžœμž­μ…˜μ΄ μ»€λ°‹λœ μ΄ν›„μ—λŠ” autocommit을 true둜 λ‹€μ‹œ μ„€μ •ν•΄μ£Όκ³  con을 λ°˜λ‚©ν•©λ‹ˆλ‹€.
  • validation(toMember) : toMember의 member_idκ°€ "ex"인 경우 μ˜ˆμ™Έλ₯Ό λ˜μ§€λ„λ‘ μ„€μ • For Test Code

참고둜 findbyId와 update λ©”μ†Œλ“œμ—λŠ” λ©”μ†Œλ“œ μ‹œμž‘λΆ€λΆ„μ—μ„œ 컀λ„₯μ…˜μ„ μƒμ„±ν•˜μ§€ μ•Šκ³  νŒŒλΌλ―Έν„°λ‘œ λ°›κ²Œ ν•œ ν›„, 둜직 μˆ˜ν–‰ ν›„ 컀λ„₯μ…˜μ€ λ°˜λ‚©ν•˜μ§€ μ•Šλ„λ‘ μ„€μ •ν–ˆμŠ΅λ‹ˆλ‹€. κ·Έ μ™Έ λ‘œμ§μ€ λ™μΌν•˜κΈ° λ•Œλ¬Έμ— λ”°λ‘œ μ½”λ“œλ₯Ό μ²¨λΆ€ν•˜μ§„ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.

 

ν…ŒμŠ€νŠΈ μ§„ν–‰

@BeforeEachμ—μ„œ

컀λ„₯μ…˜μ„ λ°›μ•„μ˜¬ dataSource둜 Hikari CP 생성 및 url, usename, password μ„€μ • 

Repository -> HikariCP datasource,

Service -> Repository, Hikari datasource μ£Όμž… 

 

@AfterEachμ—μ„œ repository μ΄ˆκΈ°ν™”

1. μ •μƒμ μœΌλ‘œ 이체 된 경우

@Test
@DisplayName("정상 이체")
void accountTransfer() throws SQLException {
    // given
    Member memberA = new Member(MEMBER_A, 10000);
    Member memberB = new Member(MEMBER_B, 10000);
    memberRepository.save(memberA);
    memberRepository.save(memberB);

    //when
    log.info("START TX");
    memberService.accountTransfer(memberA.getMemberId(),memberB.getMemberId(),2000);
    log.info("END TX");

    // then
    Member findMemberA = memberRepository.findById(memberA.getMemberId());
    Member findMemberB = memberRepository.findById(memberB.getMemberId());

    Assertions.assertThat(findMemberA.getMoney()).isEqualTo(8000);
    Assertions.assertThat(findMemberB.getMoney()).isEqualTo(12000);
}
  • 정상 μ΄μ²΄λ˜μ—ˆκΈ° λ•Œλ¬Έμ— A의 moneyλŠ” 8000, B의 moneyλŠ” 12000으둜 μ„€μ •λœ 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

2. 이체쀑 μ˜ˆμ™Έκ°€ λ°œμƒν•œ 경우 

@Test
    @DisplayName("이체쀑 μ˜ˆμ™Έ λ°œμƒ")
    void accountTransferEx() throws SQLException {
        // given
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberEx = new Member(MEMBER_EX, 10000);
        memberRepository.save(memberA);
        memberRepository.save(memberEx);


        //when
//        memberService.accountTransfer(memberA.getMemberId(),memberEx.getMemberId(),2000);

        Assertions.assertThatThrownBy(
                        () -> memberService.accountTransfer(memberA.getMemberId(),memberEx.getMemberId(),2000))
                .isInstanceOf(IllegalStateException.class);

        // then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberEx = memberRepository.findById(memberEx.getMemberId());
        log.info("findMemberA.money = {} ",findMemberA.getMoney());
        log.info("findMemberEx.money = {} ",findMemberEx.getMoney());

        Assertions.assertThat(findMemberA.getMoney()).isEqualTo(10000);
        Assertions.assertThat(findMemberEx.getMoney()).isEqualTo(10000);
    }
  • μ˜ˆμ™Έ λ°œμƒν•˜λ©΄ λ‘€λ°±ν•˜λ„λ‘ μ„€μ •ν–ˆκΈ° λ•Œλ¬Έμ— 원 μƒνƒœλ‘œ 볡ꡬ된 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ DB νŠΈλžœμž­μ…˜μ„ μ μš©ν•˜λ €λ©΄ μ„œλΉ„μŠ€ 계측이 맀우 μ§€μ €λΆ„ν•΄μ§€κ³ , 생각보닀 λ³΅μž‘ν•œ μ½”λ“œλ₯Ό μš”κ΅¬ν•˜κ³  μΆ”κ°€λ‘œ 컀λ„₯μ…˜μ„ μœ μ§€ν•˜λ„λ‘ μ½”λ“œλ₯Ό λ³€κ²½ν•˜λŠ” 것도 μ‰¬μš΄ 일은 μ•„λ‹™λ‹ˆλ‹€. λ‹€μŒ ν¬μŠ€νŒ…μ—μ„œ μŠ€ν”„λ§μ„ μ‚¬μš©ν•΄μ„œ 이런 λ¬Έμ œλ“€μ„ ν•˜λ‚˜μ”© 해결해보도둝 ν•˜κ² μŠ΅λ‹ˆλ‹€.

 

<참고자료>

 

 

μŠ€ν”„λ§ DB 1편 - 데이터 μ ‘κ·Ό 핡심 원리 - μΈν”„λŸ° | κ°•μ˜

λ°±μ—”λ“œ κ°œλ°œμ— ν•„μš”ν•œ DB 데이터 μ ‘κ·Ό κΈ°μˆ μ„ κΈ°μ΄ˆλΆ€ν„° μ΄ν•΄ν•˜κ³ , μ™„μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μŠ€ν”„λ§ DB μ ‘κ·Ό 기술의 원리와 ꡬ쑰λ₯Ό μ΄ν•΄ν•˜κ³ , 더 κΉŠμ΄μžˆλŠ” λ°±μ—”λ“œ 개발자둜 μ„±μž₯ν•  수 μžˆμŠ΅λ‹ˆλ‹€., - κ°•μ˜

www.inflearn.com