Home > Java > 트랜잭션

트랜잭션
java

트랜잭션과 세션

  • 트랜잭션은 더 이상 쪼갤 수 없는 업무 처리의 최소 단위를 의미한다.
    • 트랜잭션은 ACID를 보장해야 한다. ACID란 트랜잭션이 안전하게 수행되기 위한 4가지 필수적인 성질을 말한다.
      • Atomicity(원자성) : 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패해야 한다.
      • Consistenecy(일관성) : 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어, 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.
      • Isolation(격리성) : 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어, 동시에 같은 데이터를 수정하지 못하도록 해야 한다.
        • 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(Isolation Level)을 선택할 수 있다.
      • Durability(지속성) : 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.
    • 트랜잭션의 모든 작업이 성공해서 데이터베이스에 정상 반영하는 것을 커밋(Commit)이라 하고, 작업 중 하나라도 실패해서 트랜잭션 이전으로 되돌리는 것을 롤백(Rollback)이라 한다.
  • 트랜잭션 격리 수준(Isolation Level)
    • 격리성(Isolation)을 완벽히 보장하려면 트랜잭션을 거의 순서대로 실행해야 하는데, 이렇게 하면 동시 처리 성능이 매우 나빠진다. 이런 문제로 인해 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다.
    • READ UNCOMMITED(커밋되지 않은 읽기)
    • READ COMMITTED(커밋된 읽기)
    • REPEATABLE READ(반복 가능한 읽기)
    • SERIALIZABLE(직렬화 가능)
      -> 일반적으로 READ COMMITTED(커밋된 읽기) 트랜잭션 격리 수준을 많이 사용한다.
  • 데이터베이스 연결 구조와 세션
  • 사용자는 애플리케이션 서버, DB 접근 툴과 같은 클라이언트를 사용해서 데이터베이스 서버에 접근할 수 있다.
    • 클라이언트는 데이터베이스 서버에 연결을 요청하고 커넥션을 맺는데, 이때 데이터베이스 서버는 내부에 세션이라는 것을 만든다. 그리고 앞으로 해당 커넥션을 통한 모든 요청은 이 세션을 통해서 실행하게 된다.
      • 개발자가 클라이언트를 통해 SQL을 전달하면 현재 커넥션에 연결된 세션이 SQL을 실행한다.
    • 세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료한다. 그리고 이후에 새로운 트랜잭션을 다시 시작할 수 있다.
      • 사용자가 커넥션을 닫거나, DB 관리자가 세션을 강제로 종료하면 세션은 종료된다.

  • 트랜잭션 모드 - 자동 커밋과 수동 커밋
    • DBMS가 트랜잭션을 처리하는 모드는 자동 커밋과 수동 커밋이 있다.
      • 트랜잭션 모드는 연결 단위(세션)별로 설정하며, 연결 중에도 언제든지 변경할 수 있다.
    • 자동 커밋으로 설정하면 각각의 쿼리 실행 직후에 자동으로 commit을 호출한다.
        set autocommit true; // 자동 커밋 모드 설정
        insert into member(member_id, money) values ('data1',10000); 
            // 자동 커밋  
        insert into member(member_id, money) values ('data2',10000); 
            // 자동 커밋
      
      • 자동 커밋에서는 쿼리를 하나하나 실행할 때마다 자동으로 커밋이 되어버리기 때문에 우리가 원하는 트랜잭션 기능을 제대로 사용할 수 없다. 따라서, 트랜잭션 기능을 제대로 수행하려면 수동 커밋을 사용해야 한다.
    • 수동 커밋으로 설정하면 이후에 꼭 commit이나 rollback을 호출해야 한다.
        set autocommit false; // 수동 커밋 모드 설정
        insert into member(member_id, money) values ('data3',10000);
        insert into member(member_id, money) values ('data4',10000);
        commit; // 수동 커밋
      
      • 보통 자동 커밋 모드가 기본으로 설정된 경우가 많기 때문에, 수동 커밋 모드로 설정하는 것을 트랜잭션을 시작한다고 표현할 수 있다.

트랜잭션 간단한 예시와 문제

이전 글 참고

  • MemberRepositoryV1 클래스
      @Repository
      public class MemberRepositoryV1 implements MemberRepository {
          // 기존 메소드
              // ...
              // findById(String memberId)...
              // update(String memberId, int money)...
              // ...
    
          // 커넥션 유지가 필요한 findById, update 메소드 추가
          // 기존 findById, update 메소드와 같은 내용이지만, 2가지 부분이 다르다.
              // 1. con = getConnection() 삭제
              // 2. dbcUtils.closeConnection(con) 삭제
                
          public Member findById(Connection con, String memberId) throws SQLException {
              ...
          }
    
          public void update(Connection con, String memberId, int money) throws SQLException {
              ...
          }
      }
    
  • MemberServiceV0 클래스
      @Service
      public class MemberServiceV1 {
          private static final Logger log = LoggerFactory.getLogger(MemberRepositoryJdbc.class);
          private final DataSource dataSource;
          private final MemberRepository memberRepository;
          public MemberServiceV1(DataSource dataSource, MemberRepository memberRepository) {
              this.dataSource = dataSource;
              this.memberRepository = memberRepository;
          }
    
          // 간단한 계좌이체
          public void accountTransfer(String fromId, String toId, int money) throws SQLException {
              Connection con = dataSource.getConnection();
              try {
                  con.setAutoCommit(false); //트랜잭션 시작
                  bizLogic(con, fromId, toId, money); //비즈니스 로직
                  con.commit(); //성공시 커밋
              } catch (Exception e) {
                  con.rollback(); //실패시 롤백
                  throw new IllegalStateException(e);
              } finally {
                  if (con != null) {
                      try {
                          con.setAutoCommit(true); //커넥션 풀 고려
                          con.close(); //커넥션 풀을 사용하므로 커넥션이 종료되는 것이 아니라 풀에 반납됨
                      } catch (Exception e) {
                          log.info("error", e);
                      }
                  }
              }
          }
            
          private void bizLogic(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);
              memberRepository.update(con, toId, toMember.getMoney() + money);
          }
      }
    
  • 문제
    1. 서비스 계층은 트랜잭션을 사용하기 위해서 JDBC 기술에 의존하고 있다. 향후 JPA 같은 다른 기술로 바꾸어 사용하게 되면 서비스 코드도 모두 함께 변경해야 한다. (데이터 접근 기술마다 트랜잭션을 사용하는 코드가 다름)
    2. Repository에서 Connection을 인자로 받는 새로운 findById, update 메소드를 작성한 것처럼, 똑같은 기능도 트랜잭션용 기능과 트랜잭션을 유지하지 않아도 되는 기능으로 분리해야 한다.
    3. 트랜잭션 적용 코드(try, catch, finally 부분)에 반복이 많다.

  • 스프링은 위의 문제들을 해결할 수 있는 다양한 방법과 기술들을 제공한다.
    -> 스프링이 제공하는 트랜잭션 기술

참고 자료

“김영한 스프링 DB 1편” http://wiki1.kr/index.php/트랜잭션 http://soen.kr/book/sql/book/19-2.htm