트랜잭션과 세션
- 트랜잭션은 더 이상 쪼갤 수 없는 업무 처리의 최소 단위를 의미한다.
- 트랜잭션은 ACID를 보장해야 한다. ACID란 트랜잭션이 안전하게 수행되기 위한 4가지 필수적인 성질을 말한다.
- Atomicity(원자성) : 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패해야 한다.
- Consistenecy(일관성) : 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어, 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.
- Isolation(격리성) : 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어, 동시에 같은 데이터를 수정하지 못하도록 해야 한다.
- 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(Isolation Level)을 선택할 수 있다.
- Durability(지속성) : 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.
- 트랜잭션의 모든 작업이 성공해서 데이터베이스에 정상 반영하는 것을 커밋(Commit)이라 하고, 작업 중 하나라도 실패해서 트랜잭션 이전으로 되돌리는 것을 롤백(Rollback)이라 한다.
- 트랜잭션은 ACID를 보장해야 한다. ACID란 트랜잭션이 안전하게 수행되기 위한 4가지 필수적인 성질을 말한다.
- 트랜잭션 격리 수준(Isolation Level)
- 격리성(Isolation)을 완벽히 보장하려면 트랜잭션을 거의 순서대로 실행해야 하는데, 이렇게 하면 동시 처리 성능이 매우 나빠진다. 이런 문제로 인해 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다.
- READ UNCOMMITED(커밋되지 않은 읽기)
- READ COMMITTED(커밋된 읽기)
- REPEATABLE READ(반복 가능한 읽기)
- SERIALIZABLE(직렬화 가능)
-> 일반적으로 READ COMMITTED(커밋된 읽기) 트랜잭션 격리 수준을 많이 사용한다.
- 데이터베이스 연결 구조와 세션
- 사용자는 애플리케이션 서버, DB 접근 툴과 같은 클라이언트를 사용해서 데이터베이스 서버에 접근할 수 있다.
- 클라이언트는 데이터베이스 서버에 연결을 요청하고 커넥션을 맺는데, 이때 데이터베이스 서버는 내부에 세션이라는 것을 만든다. 그리고 앞으로 해당 커넥션을 통한 모든 요청은 이 세션을 통해서 실행하게 된다.
- 개발자가 클라이언트를 통해 SQL을 전달하면 현재 커넥션에 연결된 세션이 SQL을 실행한다.
- 세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료한다. 그리고 이후에 새로운 트랜잭션을 다시 시작할 수 있다.
- 사용자가 커넥션을 닫거나, DB 관리자가 세션을 강제로 종료하면 세션은 종료된다.
- 사용자가 커넥션을 닫거나, 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; // 수동 커밋
- 보통 자동 커밋 모드가 기본으로 설정된 경우가 많기 때문에, 수동 커밋 모드로 설정하는 것을 트랜잭션을 시작한다고 표현할 수 있다.
- DBMS가 트랜잭션을 처리하는 모드는 자동 커밋과 수동 커밋이 있다.
트랜잭션 간단한 예시와 문제
이전 글 참고
- 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); } }
- 문제
- 서비스 계층은 트랜잭션을 사용하기 위해서 JDBC 기술에 의존하고 있다. 향후 JPA 같은 다른 기술로 바꾸어 사용하게 되면 서비스 코드도 모두 함께 변경해야 한다. (데이터 접근 기술마다 트랜잭션을 사용하는 코드가 다름)
- Repository에서 Connection을 인자로 받는 새로운 findById, update 메소드를 작성한 것처럼, 똑같은 기능도 트랜잭션용 기능과 트랜잭션을 유지하지 않아도 되는 기능으로 분리해야 한다.
- 트랜잭션 적용 코드(try, catch, finally 부분)에 반복이 많다.
- 스프링은 위의 문제들을 해결할 수 있는 다양한 방법과 기술들을 제공한다.
-> 스프링이 제공하는 트랜잭션 기술
참고 자료
“김영한 스프링 DB 1편” http://wiki1.kr/index.php/트랜잭션 http://soen.kr/book/sql/book/19-2.htm