Spring DB - 트랜잭션

SeungTaek·2022년 6월 29일
0
post-thumbnail

본 게시물은 스스로의 공부를 위한 글입니다.
잘못된 내용이 있으면 댓글로 알려주세요!

ACID

데이터베이스에서 트랜잭션은 데이터베이스의 상태를 변화시키기 해서 수행하는 작업의 단위를 뜻한다.

트랜잭션은 ACID를 보장해야 한다.

  1. 원자성(Atomicity) : 트랜잭션 내의 작업들은 모두 하나의 작업인것 처럼 모두 성공하거나 모두 실패해야 한다.
  2. 일관성(Consistency): 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.
  3. 격리성(Isolation): 동시에 실행되는 트랜잭션들이 서로에게 영향이 미치지 않아야 한다. 성능 이슈로 인해 개발자가 격리 수준(Isolation level)을 선택할 수 있다.
  4. 지속성(Durability): 성공적으로 수행된 트랜잭선은 그 결과가 항상 기록되어야 한다. 모든 트랜잭션은 로그로 남고 시스템 장애 발생 전 상태로 되돌릴 수 있다.

격리성을 완벽히 보장하려면 트랜잭션을 요청 순서대로 실행해야 한다. 이렇게 하면 동시 처리 성능이 매우 나빠진다. 따라서 ANSI 표준은 트랜잭션 격리 수준을 4단계로 나누어 정의했다.
  1. READ UNCOMMITED - 커밋되지 않은 읽기
  2. READ COMMITTED - 커밋된 읽기
  3. REPEATABLE READ - 반복 가능한 읽기
  4. SERIALIZABLE - 직렬화 가능

주로 실무에선 READ COMMITTED와 REPEATABLE READ을 사용한다.



트랜잭션

예를 들어, A라는 사람이 B라는 사람에게 돈을 이체한다고 하자. 이 과정에는 2가지의 과정이 필요하다.

  1. A의 잔고를 감소
  2. B의 잔고를 증가

만약 A의 잔고를 감소 후, B 잔고를 증가하는 과정에서 문제가 발생하게 된다면, 심각한 문제가 발생한다. 따라서 데이터베이스가 제공하는 트랜잭션 기능을 사용해 하나의 작업처럼 수행되어야 하고, 하나라도 실패의 경우 rollback하는 기능이 있어야 한다.

모든 작업이 성공해서 DB에 정상 반영되는 것을 커밋(Commit)이라 하고, 작업 중 하나라도 실패해서 이전으로 되돌리는 것을 롤백(Rollback)이라 한다.


과정

트랜잭션이 커밋 되기 전에는 임시로 데이터를 저장하는 것이다. 해당 트랜잭션을 시작한 세션에게만 변경 데이터가 보이고, 다른 세션에게는 변경 데이터가 보이지 않는다.

보통 자동 커밋 모드가 기본으로 설정된 경우가 많다. 자동 커밋이란 SQL 실행 직후 자동으로 커밋을 호출해준다. 따라서 트랜잭션 기능을 제대로 사용하려면 자동 커밋 기능을 꺼야 한다. 수동 커밋 이후엔 반드시 commit 또는 rollback을 호출해야 한다.

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



DB 락

세션1이 데이터를 수정하는 동안, 세션2에서 같은 데이터를 수정하게 되면 여러가지 문제가 발생한다. 이런 문제를 방지하려면, 세션이 트랜잭션을 시작하고 데이터를 수정하는 동안에는 커밋이나 롤백 전까지 다른 세션에서 해당 데이터를 수정할 수 없게 막아야 한다. DB에서는 이런 문제를 해결하기 위해 락(Lock)이란 개념을 제공한다.

세션1이 트랜잭션을 시작하면 해당 row의 락을 획득한다. 그러면 세션2는 해당 row가 락이 없으므로 대기하게 된다. 물론 무한정 대기하는 것은 아니다. 락 대기시간이 넘어가면 타임아웃 오류가 발생한다. 대기 시간은 설정할 수 있다.( SET LOCK_TIMEOUT 10000, 10초)

세션1이 commit 또는 rollback하여 락을 반환하게 되고, 세션2는 대기하다 해당 락을 획득 후 sql을 수행하게 된다.


일반적으로 select문은 락을 사용하지 않는다. 하지만 돈 계산 등 중요한 로직을 위해 조회할 경우, 조회 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야할 때가 있다. 이때는 select for update구문을 사용하면 된다.

트랜잭션과 락은 데이터베이스마다 실제 동작하는 방식이 조금씩 다르기 때문에, 해당 DB 메뉴얼을 참고해야 한다.



트랜잭션 사용 예시 - 계좌이체

 public void accountTransfer(String fromId, String toId, int money) throws SQLException {
   Connection con = dataSource.getConnection();

   try {
     con.setAutoCommit(false); // 트랜잭션 시작, 자동 커밋모드 false

     // 여기에 비즈니스 코드 넣기

     con.commit(); // 성공시 commit
   } catch (Exception e) {
     con.rollback(); // 실패시 rollback
     throw new IllegalStateException(e);
   } finally {
     release(con);
   }
 }

private void release(Connection con) {
  if (con != null) {
    try {
      con.setAutoCommit(true); // 자동 커밋모드 true!
      con.close();
    } catch (Exception e) {
      log.info("error", e);
    }
  }
}

만약 커넥션 풀을 사용한다면 connection.close()전에 다시 자동 커밋모드를 true로 바꿔야 한다. 이는 커넥션을 끄지않고 풀로 반환하기 때문인데, 만약 그대로 반환하다면 다음에 풀에서 꺼낼 때 자동 커밋모드가 꺼져있게 된다. 보통 자동 커밋모드가 true라고 가정하고 코딩을 하기 때문에 반환 전 전 다시 true로 바꿔주자.


아직 해결하지 못한 문제들

위 코드를 보자. 서비스 계층에 비즈니스 로직과 트랜잭션을 처리하는 로직이 함께 들어가있다. 끔찍한 코드이다. 이를 어떻게 하면 깔끔하게 처리할 수 있을까? 다음 게시물에서 스프링이 제공하는 트랜잭션 문제 해결 방법을 알아보자.




Reference

인프런 '스프링 DB - 데이터 접근 핵심 원리'(김영한)

profile
I Think So!

0개의 댓글