트랜잭션 - 개념
트랜잭션이란 하나의 서비스 로직을 안전하게 처리하도록 보장해주는 것을 뜻한다.
트랜잭션 ACID
1. 원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야한다.
2. 일관성: 모든 트랜잭션은 일관성있는 데이터베이스 상태를 유지해야한다. 예를 들어 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.
3. 격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준을 선택할 수 있다.
4. 지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야한다.
DB 락
세션1이 트랜잭션을 시작하고 데이터를 수정하는 동안 아직 커밋을 수행하지 않았는데
세션2에서 동시에 같은 데이터를 수정하게 되면 여러가지 문제가 발생한다.
바로 트랜잭션의 원자성이 꺠지는 것이다.
여기 더해 세션1이 중간에 롤백을 하게되면 세션2는 잘못된 데이터를 수정하는 문제가 발생한다.
이러한 문제를 해결하기 위해 데이터베이스는 락이라는 개념을 제공한다.
세션2 락 타임아웃
세션1이 데이터를 변경하고 아직 커밋을 하지 않은 상태에서 세션2가 같은 데이터를 변경하려고 하면 설정한 시간만큼 대기해야한다.
만약 세션1이 커밋이나 롤백을 하게되면 세션2는 락을 획득하게된다.
또 설정한 시간까지 대기하게되면 락타임아웃 오류가 발생한다.
DB 락 - 조회
일반적인 조회는 락을 사용하지 않는다.
- 데이터베이스마다 다르지만, 보통 데이터를 조회할 때는 락을 획득하지 않고 바로 조회가 가능하다.
조회와 락
- 데이터를 조회할 때도 락을 획득하고 싶을 때가 있다. 이럴 때는
select for update
구문을 사용하면 된다.
조회 시점에 락이 필요한 경우는 언제일까?
- 트랜잭션 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야 할 때 사용한다.
트랜잭션 - 적용
- 트랜잭션은 비즈니스 로직이 있는
서비스 계층
에서 시작해야 한다.
비즈니스 로직이 잘못되면 해당 비즈니스 로직으로 인해 문제가 되는 부분을 함께 롤백해야 하기 때문이다. - 트랜잭션을 시작하려면 커넥션이 필요하다. 결국 서비스 계층에서 커넥션을 만들고, 트랜잭션 커밋 이후에 커넥션을 종료해야 한다.
- 애플리케이션에서 DB 트랜잭션을 사용하려면 트랜잭션을 사용하는 동안 같은 커넥션을 유지 해야한다. 그래야 같은 세션을 사용할 수 있다.
- 커넥션 유지가 필요한 메서드는 반드시 파라미터로 넘어온 커넥션을 사용해야 한다. 따라서 커넥션을 새로 얻으면 안된다!
또한 이후 서비스 로직이 끝날때 커넥션을 닫아야 한다.
트랜잭션 동기화
스프링이 제공하는 트랜잭션 매니저는 크게 2가지 역할을 한다.
- 트랜잭션 추상화
- 리소스 동기화
__리소스 동기화__ 트랜잭션을 유지하려면 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해야한다. 결국 같은 커넥션을 동기화하기 위해서는 파라미터로 커넥션을 전달하는 방법을 사용했었다. 파라미터로 커넥션을 전달하는 방법은 코드가 지저분해지는 것은 물론이고, 커넥션을 넘기는 메서드와 넘기지 않는 메서드를 중복해서 만들어야 하는 등 여러가지 단점들이 많다.
커넥션과 세션
- 클라이언트는 커넥션으로 데이터베이스에 연결하고 데이터베이스 내부적으로 세션이 생성되어 명령을 실행할 수 있다.
트랜잭션 매니저와 트랜잭션 동기화 매니저
- 스프링은 트랜잭션 동기화 매니저를 제공한다.
이것은 쓰레드 로컬을 사용해서 커넥션을 동기화 해준다.
트랜잭션 매니저는 내부에서 이 트랜잭션 동기화 매니저를 사용한다. - 트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하기 때문에 멀티쓰레드 상황에 안전하게 커넥션을 동기화 할 수 있다.
따라서 커넥션이 필요하면 트랜잭션 동기화 매니저를 통해 커넥션을 획득하면 된다.
따라서 이전처럼 파라미터로 커넥션을 전달하지 않아도 된다.
트랜잭션 AOP 진행 흐름
1. 클라이언트가 요청을 한다.
2. 요청 로직에 @Transactional을 하였다면 AOP 프록시를 만든다.
3. AOP 프록시에서 트랜잭션 로직(시작, 커밋, 롤백, 자원 해제)을 실행하고 실제 서비스를 호출한다.
3-1. 트랜잭션 시작시 스프링 컨테이너를 통해 빈으로 등록된 트랜잭션 매니저를 획득한다.
3-2. 획득한 트랜잭션 매니저를 시작한다.
3-3. 트랜잭션 매니저는 데이터 소스를 가지고 커넥션을 생성한다.
3-4. 커넥션 auto commit을 false로 한다.
3-5. 커넥션을 트랜잭션 동기화 매니저에 보관한다.
3-6. 보관된 커넥션을 가지고 비즈니스 로직에서 사용한 repository는 모두 트랜잭션 동기화 매니저에서 꺼내 쓴다.
3-7. 비즈니스 로직이 끝났으면 리턴되어 성공이면 commit, 런타임 예외가 발생하면 rollback을 한다.
3-8. 자원이 반환된다.
Checked Exception과 UnChecked Exception
Checked Exception
- Checked Exception은 컴파일 시점에 컴파일러에서 확인하는 예외이다.
- 반드시 에러 처리를 해야하는 특징을 갖고있다.(강제적)
- Exception 클래스의 하위 클래스이며 RuntimeException의 하위 클래스는 아니다.
- 예외 발생시 트랜잭션 처리를 하여도 roll-back 되지 않는다.
UnChecked Exception
- UnChecked Exception은 RuntimeException의 하위 클래스이다.
- 말 그대로 실행 중 발생할 수 있는 예외를 의미한다.
- 예외 발생시 트랜잭션 처리를 하면 roll-back 된다.
트랜잭션의 격리 수준(Isolation Level)
READ UNCOMMITED(커밋되지 않은 읽기) : 성능은 제일 좋지만 데이터 보장이 안된다
READ COMMITTED(커밋된 읽기) : 가장 많이 사용하는 트랜잭션 격리 수준이다.
REPEATABLE READ(반복 가능한 읽기)
SERIALIZABLE(직렬화 가능)