-
스프링 DB 데이터접근 원리 - 3. 트랜잭션 이해Spring-Boot/스프링 DB 1편 - 데이터 접근 핵심 원리 2022. 6. 19. 14:49
트랜잭션 - 개념 이해
- 데이터 베이스에서 트랜잭션은 하나의 거래를 안전하게 처리하도록 보장해주는 것을 뜻한다
- 모든 작업이 성공해서 데이터베이스에 정상 반영하는 것을 커밋( Commit ) 이라 하고, 작업 중 하나라도 실패해서 거래 이전으로 되돌리는 것을 롤백 ( Rollback ) 이라 한다
트랜잭션 ACID
- * 원자성 * : 트랜잭션 내에서 실행한 작업들을 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 한다.
- * 일관성 * : 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.
ㆍ예를 들어 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야한다
- * 격리성 * : 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다.
ㆍ예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야한다
ㆍ격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준 (Isolation level)을 선택할 수 있다
- * 지속성 * : 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다
ㆍ중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다
- 트랜잭션은 원자성, 일관성, 지속성을 보장한다.
- 문제는 격리성인데 트랜잭션 간에 격리성을 완벽히 보장하려면 트랜잭션을 거의 순서대로 실행해야한다.
- 이렇게 하면 동시 처리 성능이 매우 나빠진다
- 이런 문제로 인해 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다
* 트랜잭션 격리 수준 - Isolation level *
- READ UNCOMMITED ( 커밋되지 않은 읽기 )
- READ COMMITTED ( 커밋된 읽기 )
- REPEATABLE READ ( 반복 가능한 읽기 )
- SERIALIZABLE ( 직렬화 가능 )
* 단계가 높아질수록 성능이 느려짐, 보통 커밋된 읽기를 많이 씀
* 참고 *
- 강의에서는 일반적으로 많이 사용하는 READ COMMITTED ( 커밋된 읽기 ) 트랜잭션 격리 수준을 기준으로 설명한다
- 트랜잭션 격리 수준은 데이터베이스에 관한 부분이어서 이 강의 내용을 넘어선다
데이터베이스 연결 구조와 DB 세션
* 데이터베이스 연결 구조 *
- 사용자는 웹 어플리케이션 서버(WAS)나 DB 접근 툴 같은 클라이언트를 사용해서 데이터베이스 서버에 접근
ㆍ클라이언트는 데이터베이스 서버에 연결을 요청하고 커넥션을 맺게된다.
ㆍ이때 데이터베이스 서버는 내부에 세션이라는 것을 만든다
ㆍ그리고 앞으로 해당 커넥션을 통해 모든 요청은 이 세션을 통해서 실행
- 쉽게 이야기해서 개발자가 클라이언트를 통해 SQL을 전달하면 현재 커넥션에 연결된 세션이 SQL을 실행한다
- 세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료한다
- 그리고 이후에 새로운 트랜잭션을 다시 시작할 수 있다
- 사용자가 커넥션을 닫거나, 또는 DBA(DB 관리자)가 세션을 강제로 종료하면 세션은 종료된다
- 커넥션 풀이 10개의 커넥션을 생성하면, 세션도 10개 만들어진다.
트랜잭션 - DB예제1 - 개념 이해
* 트랜잭션 사용법 *
- 데이터 변경 쿼리를 실행하고 데이터베이스에 그 결과를 반영하려면 커밋 명령어인 'commit'을 호출하고, 결과에 반영하고 싶지 않으면 롤백 명령어인 'rollback'을 호출하면 된다
- *커밋을 호출하기 전까지는 임시로 데이터를 저장*하는 것이다. 따라서 해당 트랜잭션을 시작한 세션(사용자)에게만 변경 데이터가 보이고 다른 세션(사용자)에게는 변경 데이터가 보이지 않는다
- 등록, 수정, 삭제 모두 같은 원리로 동작한다. 앞으로 등록, 수정, 삭제를 간단히 *변경*이라는 단어로 표현하겠다.
* 커밋하지 않은 데이터를 다른 곳에서 조회할 수 있으면 어떤 문제가 발생할까? *
- 예를 들어 커밋하지 않은 데이터가 보인다면,
ㆍ세션2가 데이터를 조회했을 때 어떤 로직을 수행
ㆍ그런데 세션1이 롤백을 수행하면 데이터가 사라진다ㆍ데이터 정합성에 큰 문제가 발생
- ' commit '으로 새로운 데이터가 실제 데이터베이스에 반영된다
ㆍ데이터의 상대도 임시 → 완료로 변경
- ' rollback ' 호출 시 수정하거나 삭제한 데이터도 모두 트랜잭션을 시작하기 직전의 상태로 복구
트랜잭션 - DB예제2 - 자동 커밋, 수동 커밋
- 자동 커밋으로 설정하면 각각의 쿼리 실행 직후에 자동으로 커밋을 호출한다.
- 따라서 커밋이나 롤백을 직접 호출하지 않아도 되는 편리함이 있다
- 하지만 쿼리를 하나하나 실행할 때마다 자동으로 커밋되어버리기 때문에 우리가 원하는 트랜잭션 기능 제대로 사용 X
- 따라서 commit, rollback을 직접 호출하면서 트랜잭션 기능을 제대로 수행하려면 자동 커밋을 끄고 수동 커밋을 사용해야한다
// 자동 커밋모드 설정 set autocommit true; // 수동 커밋 설정 set autocommit false; commit; // 수동 커밋
- 보통 자동 커밋모드가 기본으로 설정된 경우가 많기 때문에, *수동 커밋 모드로 설정하는 것을 트랜잭션을 시작 *한다고 표현할 수 있다
- 수정 커밋 설정을 하면 이후에 꼭 'commit', 'rollback'을 호출해야한다
- 참고로 수동 커밋모드나 자동 커밋모드는 한번 설정하면 해당 세션에서는 계속 유지된다. 중간에 변경하는 것은 가능
* 정리 *
*원자성* : 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 한다
- 트랜잭션의 원자성 덕분에 여러 SQL 명령어를 마치 하나의 작업인 것처럼 처리할 수 있었다.
- 성공하면 한번에 반영하고, 중간에 실패해도 마치 하나의 작업을 되돌리는 것처럼 간단히 되돌릴 수 있다.
*오토 커밋*
- 만약 오토 커밋 모드로 동작하는데, 쿼리를 하나 실행할 때 마다 바로바로 커밋
- 중간에 실패 시 심각한 문제 발생
*트랜잭션 시작*
- 이런 종류의 작업은 꼭 수동 커밋 모드를 사용해서 수동으로 커밋, 롤백 할 수 있도록 해야 한다.
- 보통 이렇게 자동 커밋 모드에서 수동 커밋 모드로 전환하는 것을 트랜잭션을 시작한다고 표현
DB 락 - 개념 이해
- 세션1이 트랜잭션을 시작하고 데이터를 수정하는 동안 아직 커밋을 수행하지 않았는데,
세션2에서 동시에 같은 데이터를 수정하게 되면 여러가지 문제가 발생
- 바로 트랜잭션의 원자성이 깨지는 것이다.
- 여기에 더해서 세션1이 중간에 롤백을 하게 되면 세션2는 잘못된 데이터를 수정하는 문제가 발생
- 이런 문제를 방지하려면, 세션이 트랜잭션을 시작하고 데이터를 수정하는 동안에는 커밋이나 롤백 전까지 다른 세션에서 해당 데이터를 수정할 수 없게 막아야 한다
락 ( Lock )
- 데이터베이스 두 세션이 동시에 데이터를 수정하고 싶을 때, 즉 위와 같은 문제가 생길때 락( Lock )이라는 개념을 제공
- 작동방식
1. 세션1은 트랜잭션을 시작
2. 세션1은 변경시도
ㆍ이때 해당 로우의 락을 먼저 획득해야한다
ㆍ락이 남아 있으므로 세션1은 락을 획득한다 ( 세션1이 세션2보다 조금 더 빨리 요청 )
3. 세션1은 락을 획득했으므로 해당 로우에 update sql 수행
4. 세션2는 트랜잭션을 시작
5. 세션2도 변경 시도
ㆍ이 때 해당 로우의 락을 먼저 획득해야한다
ㆍ락이 없으므로 락이 돌아올 때까지 대기
ㆍ참고로 세션2가 락을 무한정 대기하는 것이 아니다. 락 대기 시간을 넘어가면 락 타임아웃 오류가 발생한다. 락 대기 시간은 설정할 수 있다.
6. 세션1은 커밋을 수행한다.
ㆍ커밋으로 트랜잭션이 종료되었으므로 락도 반납한다.
ㆍ 락을 획득하기 위해 대기하던 세션2가 락을 획득한다
7. 세션2는 update sql을 수행한다.
8. 세션2는 커밋을 수행하고 트랜잭션이 종료되었으므로 락을 반납한다.
SET LOCK_TIMEOUT <milliseconds>
: 락 타임아웃 시간을 설정한다.
- 예) SET LOCK_TIMEOUT 10000 : 10초, 세션2에 설정하면 세션2가 10초동안 대기해도 락을 얻지 못하면 락 타임아웃 오류가 발생한다.
DB 락 - 조회
* 일반적인 조회는 락을 사용하지 않는다 *
- 데이터베이스마다 다르지만, 보통 데이터를 조회할 때는 락을 획득하지 않고 바로 데이터를 조회할 수 있다
- 예를 들어 세션1이 락을 획득하고 데이터를 변경하고 있어도, 세션2에서 데이터를 조회는 할 수 있다
* 조회와 락 *
- 데이터를 조회할 때도 락을 획득하고 싶을 땐 ' select for update ' 구문을 사용하면 된다.
- 이렇게 하면 세션1이 조회 시점에 락을 가져가버리기 떄문에 다른 세션에서 해당 데이터를 변경 X
- 물론 이 경우도 트랜잭션을 커밋하면 락을 반납한다
* 조회 시점에 락이 필요한 경우는 언제일까? *
- 트랜잭션 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야한다.
- 예를 들어 애플리케이션 로직에서 ' memberA '의 금액을 조회한 다음에 이 금액 정보로 애플리케이션에서 어떤 계산을 수행한다. 그런데 계산이 돈과 관련된 매우 중요한 계산이어서 계산을 완료할 때까지 변경하면 안된다. 이럴 때 조회 시점에 락을 획득하면된다.
set autocommit false; select * from member where member_id='memberA' for update;
- ' select for update ' 구문을 사용하면 조회를 하면서 동시에 선택한 로우의 락도 획득한다
ㆍ물론 락이 없다면 락을 획득할 때 까지 대기해야한다
- 세션 1은 트랜잭션을 종료할 때까지 로우의 락을 보유한다.
* 정리 *
- 트랜잭션과 락은 데이터베이스마다 실제 동작하는 방식이 조금씩 다르기 떄문에, 해당 데이터베이스 메뉴얼을 확인해보고, 의도한대로 동작하는지 테스트한 이후에 사용하자
트랜잭션 적용
- 애플케이션에서 트랜잭션을 어떤 계층에 걸어야 할까? 쉽게 이야기해서 트랜잭션을 어디에서 시작하고, 어디에서 커밋해야 할까?
- 트랜잭션은 비즈니스 로직이 있는 서비스 계층에서 시작해야 한다. 비즈니스 로직이 잘못되면 해당 비즈니스 로직으로 인해 문제가 되는 부분을 롤백해야 하기 때문이다.
- 그런데 트랜잭션을 시작(set autocommit false)할려면 커넥션이 필요하다. 결구 서비스 계층에서 커넥션을 만들고, 트랜잭션 커밋 이후에 커넥션을 종료해야 한다.
- 애플리케이션에서 DB 트랜잭션을 사용할려면 * 트랜잭션을 사용하는 동안 같은 커넥션을 유지 *해야한다. 그래야 같은 세션을 사용할 수 있다.
* 커넥션과 세션 *
- 애플리케이션에서 같은 커넥션을 유지하려면 어떻게 해야할까?
- 가장 단순한 방법은 커넥션을 파라미터로 전달해서 같은 커넥션이 사용되도록 유지하는 것이다.
- 먼저 리포지토리가 파라미터를 통해 같은 커넥션을 유지할 수 있도록 파라미터를 추가하자!
findById(Connection con, String memberId) update(Connection con, String memberId, int money)
* 주의 *
1. 커넥션 유지가 필요한 두 메서드는 파라미터로 넘어온 커넥션을 사용해야 한다. 따라서 con = getConnection( ) 코드가 있으면 안된다.
2. 커넥션 유지가 필요한 두 메서드는 리포지토리에서 커넥션을 닫으면 안된다. 커넥션을 전달 받은 리포지토리 뿐만 아니라 이후에도 커넥션을 계속 이어서 사용하기 때문이다
이후 서비스 로지이 끝날 때 트랜재션을 종료하고 닫아야 한다.
public void accountTransfer(String fromId, String toId, int money) throws SQLException { Connection con = dataSource.getConnection(); // 트랜잭션을 위한 커넥션 try { con.setAutoCommit(false); // 트랜잭션 시작 // 비즈니스 로직 con.commit(); // 성공시 커밋 } catch (Exception e) { con.rollback(); // 실패시 롤백 throw new IllegalStateException(e); } finally { con.setAutoCommit(true); // 커넥션 풀 고려, 기본값인 자동커밋모드로 변경 con.close(); } }
* 남은 문제 *
- 애플리케이션에서 DB 트랜잭션을 적용하려면 서비스 계층이 매우 지저분해지고, 생각보다 매우 복잡한 코드를 요구한다. 추가로 커넥션을 유지하도록 코드를 변경하는 것도 쉬운 일은 아니다.
'Spring-Boot > 스프링 DB 1편 - 데이터 접근 핵심 원리' 카테고리의 다른 글
스프링 DB 데이터접근 원리 - 6. 스프링과 문제해결-예외 처리, 반복 (0) 2022.08.29 스프링 DB 데이터접근 원리 - 5. 자바 예외 이해 (0) 2022.08.23 스프링 DB 데이터접근 원리 - 4. 스프링과 문제해결 - 트랜잭션 (0) 2022.06.22 스프링 DB 데이터접근 원리 - 2. 커넥션풀과 데이터소스 이해 (0) 2022.06.18 스프링 DB 데이터접근 원리 - 1. JDBC 이해 (0) 2022.06.18