본문 바로가기
Spring/DB 1편

트랜잭션 이해

by JHyun0302 2023. 8. 9.
728x90

트랜잭션 ≒ 거래

  

트랜잭션 ACID(http://en.wikipedia.org/wiki/ACID)

  • 원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패해야 한다.
  • 일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.
  • 격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야 한다. 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준 (Isolation level)을 선택할 수 있다.
  • 지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.

 

 

 

트랜잭션 격리 수준 - Isolation level

  • READ UNCOMMITED(커밋되지 않은 읽기)
  • READ COMMITTED(커밋된 읽기)
  • REPEATABLE READ(반복 가능한 읽기)
  • SERIALIZABLE(직렬화 가능)



 


데이터베이스 연결 구조와 DB 세션

 

DB 연결 구조

 

  • 개발자가 클라이언트를 통해 SQL을 전달하면 현재 커넥션에 연결된 세션이 SQL을 실행한다.
  • 세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료한다. 그리고 이후에 새로운 트랜잭션을 다시 시작할 수 있다.
  • 사용자가 커넥션을 닫거나, 또는 DBA(DB 관리자)가 세션을 강제로 종료하면 세션은 종료된다.
  • 커넥션 풀이 10개의 커넥션을 생성하면, 세션도 10개 만들어진다.

 

 

 

 


트랜잭션 - DB 예제1 - 개념 이해

 

 

커밋을 호출하기 전까지는 임시로 데이터를 저장

 

 

 

 

세션1 신규 데이터 추가

 

  • 세션1은 select 쿼리를 실행해서 본인이 입력한 신규 회원1, 신규 회원2를 조회할 수 있다.
  • 세션2는 select 쿼리를 실행해도 신규 회원들을 조회할 수 없다. (세션1이 아직 커밋을 하지 않았기 때문)
  • 세션1 commit 시 세션2도 신규 데이터 조회 가능
  • 세션1 rollback 시 모두 트랜잭션 시작하기 직전 상태로 복구

 

 


트랜잭션 - DB 예제2 - 자동 커밋, 수동 커밋

 

 

★ 자동 커밋

set autocommit true; //자동 커밋 모드 설정
insert into member(member_id, money) values ('data1',10000); //자동 커밋 
insert into member(member_id, money) values ('data2',10000); //자동 커밋

 

 

 

★ 수동 커밋 설정

set autocommit false; //수동 커밋 모드 설정
insert into member(member_id, money) values ('data3',10000);
insert into member(member_id, money) values ('data4',10000); 
commit; //수동 커밋

 

 

  •  수동 커밋 모드로 설정하는 것을 트랜잭션을 시작한다고 표현한다.
  • 수동 커밋 설정 시 꼭 commit, rollback 호출 해야 한다.
 

 

 

 


트랜잭션 - DB 예제3 - 계좌 이체 (문제상황)

 

 

  • 계좌이체 문제 상황 - commit
  • 계좌이체 문제 상황 - rollback

 

쿼리 문법 오류 발생

 

→ 강제 커밋 : 계좌이체는 실패하고 memberA 의 돈만 2000원 줄어드는 아주 심각한 문제가 발생

 

 

 

 

 

 

→ 롤백 : 계좌이체를 실행하기 전 상태로 돌아옴. memberA 의 돈: 10000 원으로 돌아오고, memberB 의 돈: 10000 원으로 유지

 

 

 

 

 

 

 

 

 


DB - 개념 이해

 

  • 세션1이 트랜잭션 중 세션2가 같은 데이터 수정할 경우 원자성이 깨지게 된다.
  • 세션1이 트랜잭션을 시작하고 데이터 수정하고 커밋 or 롤백 전까지 다른 세션이 해당 데이터를 수정 할 수 없게 막아야한다.

 

 

세션1 데이터 수정완료 & 락 반납

 

 

세션2 데이터 수정완료 & 락 반납

 

 

 

락 타임아웃

 

 

  • SET LOCK_TIMEOUT <milliseconds> : 락 타임아웃 시간을 설정한다.
        ex) SET LOCK_TIMEOUT 10000 : 10초
              세션2에 설정하면 세션2가 10초 동안 대기해도 락을 얻지 못하면 락 타임아웃 오류가 발생한다.

 

LOCK_TIMEOUT 오류 메시지

Timeout trying to lock table {0}; SQL statement:
update member set money=10000 - 2000 where member_id = 'memberA' [50200-200]
HYT00/50200

 

 

 

 


DB - 조회

 

일반적인 조회는 락을 사용하지 않는다

  • 조회 시점에 락이 필요한 경우:  트랜잭션 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야 할 때
        ex) 애플리케이션 로직에서 memberA 의 금액을 조회해서 이 금액 정보로 어떤 계산을 수행한다. 이 계산이 돈과 관련된 매우 중요한 계산이어서 계산을 완료할 때 까지 memberA 의 금액을 다른곳에서 변경하면 안된다. 이럴 때 조회 시점에 락을 획득하면 된다.

 

조회 시 락 획득

set autocommit false;
select * from member where member_id='memberA' for update;

 

  • select for update 구문을 사용하면 조회를 하면서 동시에 선택한 로우의 락도 획득한다.
  • 세션1은 트랜잭션을 종료할 때 까지 memberA 의 로우의 락을 보유한다.

 

 

 

 

 


트랜잭션 - 적용

 

문제 : 트랜잭션 없이 비지니스 로직 실행시 롤백 불가능

/**
* 기본 동작, 트랜잭션이 없어서 문제 발생
*/
    class MemberServiceV1Test {
    public static final String MEMBER_A = "memberA";
    public static final String MEMBER_B = "memberB";
    public static final String MEMBER_EX = "ex";

	private MemberRepositoryV1 memberRepository;
	private MemberServiceV1 memberService;
   
   	@BeforeEach
    void before() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        memberRepository = new MemberRepositoryV1(dataSource);
        memberService = new MemberServiceV1(memberRepository);
    }
    
    @AfterEach
    void after() throws SQLException {
        memberRepository.delete(MEMBER_A);
        memberRepository.delete(MEMBER_B);
        memberRepository.delete(MEMBER_EX);
    }
    
    @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
        memberService.accountTransfer(memberA.getMemberId(),
        memberB.getMemberId(), 2000);
        //then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberB = memberRepository.findById(memberB.getMemberId());
        assertThat(findMemberA.getMoney()).isEqualTo(8000);
        assertThat(findMemberB.getMoney()).isEqualTo(12000);
    } 
    
    @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
    assertThatThrownBy(() -> memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000))
    					.isInstanceOf(IllegalStateException.class);
        //then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberEx = memberRepository.findById(memberEx.getMemberId());
        //memberA의 돈만 2000원 줄었고, ex의 돈은 10000원 그대로이다. 
        assertThat(findMemberA.getMoney()).isEqualTo(8000); 
        assertThat(findMemberEx.getMoney()).isEqualTo(10000);
    } 
}

 

테스트 데이터 제거

  • @BeforeEach : 각각의 테스트가 수행되기 전에 실행된다.
  • @AfterEach : 각각의 테스트가 실행되고 난 이후에 실행된다.

 

 

 

비지니스 로직과 트랜잭션

 

★  비지니스 로직이 있는 서비스 계층에서 트랜잭션 시작!

 

★ 애플리케이션에서 DB 트랜잭션을 사용하려면 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야한다.

 

커넥션을 파라미터로 전달해서 같은 커넥션이 사용되도록 유지

 

 

 

 

 

◎ 문제

  • 애플리케이션에서 DB 트랜잭션을 적용하려면 서비스 계층이 매우 지저분해지고, 생각보다 매우 복잡한 코드를 요구한다.
  • 추가로 커넥션을 유지하도록 코드를 변경하는 것도 쉬운 일은 아니다.

 

반응형

'Spring > DB 1편' 카테고리의 다른 글

자바 예외 이해  (0) 2023.08.09
스프링과 문제 해결 - 트랜잭션  (0) 2023.08.09
커넥션풀과 데이터 소스 이해  (0) 2023.08.09
JDBC 이해  (0) 2023.08.09
H2 데이터베이스 설정  (0) 2023.08.09