우아한테크코스 레벨3 프로젝트에서 JPA를 쓰고 있다. JPA는 Transaction 단위로 동작한다. 따라서 JPA를 사용하는 입장에서 Transaction의 개념에 대해 잘 알아야 한다고 생각해 왔지만 그러지 못하고 있었기 때문에 학습을 다짐하게 됐다.(내가 아는 것이라고는 스프링에서 @Transcational을 붙이면 예외가 발생했을 때 Rollback해준다는 것 뿐...) 그럼 학습을 해보자.
데이터베이스의 상태를 변경시키기 위해 수행하는 작업 단위
데이터베이스를 사용하는 애플리케이션에서 하나의 작업 단위는 데이터베이스의 상태 변경(SELECT, UPDATE, INSERT, DELETE)을 기반으로 한다. 하나의 작업 단위에서 데이터베이스의 상태 변경은 한 번일 수도 있지만 여러 번일 가능성이 크다. 여러 번의 상태 변경이 일어날 때, 안전성을 확보하는 방법이 트랜잭션이다. 어떻게 안전성을 보장한다는 거지?
예를 들어 보자,
public void updateSchedule(Long userId, ScheduleUpdateRequest request) {
scheduleRepository.deleteAllByUserA(coachId, request);
scheduleRepository.saveAllByUser(coachId, request);
}
위의 함수는 1, 2번 순서대로 코치의 일정 수정에 대한 작업을 담당하고 있다.
delete와 save 두 개의 db 상태 변화를 요하고 있는데, 여기서 만약 1번 작업이 완료된 후, 2번 작업에서 에러가 발생했다고 생각해보자. 그럼 db에서 값은 삭제 되었는데 저장이 되지 않았다. 치명적인 에러가 될 수 있다. 위의 예시는 두 개의 상태 변화만을 요하고 있지만 만약 여러 개의 상태 변화를 요하고 있다고 생각하면 상상만 해도 끔직한 결과를 초래할 수 있다. 무조건적으로 모든 작업이 완료된 후에 데이터베이스에 작업 내용이 반영되어야 한다.
이러한 상황에서 transaction은 커밋과 롤백을 통해 안전성을 확보해 준다.
커밋(Commit)
롤백(Rollback)
트랜잭션에는 4가지 특성이 존재한다. 이를 줄여서 ACID라고 한다.
원자성(Atomicity)
일관성(Consistency)
독립성(Isolation)
지속성(Durability)
스프링에선 아주 간단한 방법으로 트랜잭션 처리를 지원한다. 클래스, 메서드에 @Transactional을 선언하여 사용하기만 하면 된다.
@Transactional
public void updateSchedule(Long userId, ScheduleUpdateRequest request) {
scheduleRepository.deleteAllByUserA(coachId, request);
scheduleRepository.saveAllByUser(coachId, request);
}
여기서 주의해야 할 사항이 있는데 데이터베이스의 Auto Increment 옵션을 적용했다면, insert 동작으로 실행된 id는 트랜잭션이 롤백되어도 다시 감소하지 않는 다는 것이다. 즉, Auto Increment는 트랜잭션과 별개로 동작한다. 이유는 동시성 문제 때문인데 테코블: Transactional 어노테이션
링크에 이유가 잘 나와 있다.
다수의 트랜잭션이 실행되는 상황에서는 많은 문제점이 발생할 수 있다.
하나의 트랜잭션이 값을 변경하고 커밋하지 않은 상황에서 다른 트랜잭션이 같은 값을 조회하는 경우에 변경된 값이 조회된다던지(Dirty Read),
하나의 트랜잭션이 어떤 값을 조회한 후, 한 번더 똑같은 값을 조회하는 경우에 두 조회 사이에 다른 트랜잭션에서 값을 수정해 버려서 조회 값이 다르게 된다던지(Non-Repeatable Read),
또는 두 조회 사이에 다른 트랜잭션에서 값을 등록해 버려서 조회 값이 다르게 된다던지(존재하지 않던 녀석이 갑자기 생겨나는 것)(Phantom Read)....
그럼 이제 문제점을 해결해 줄 단서가 될 수 있는? 격리수준에 대해 알아보자.
READ_UNCOMMITED(level 0)
READ_COMMITTED(level 1)
REPEATABLE_READ(level 2)
SERIALIZABLE(level 3)
참고: Spring에서 격리 수준을 설정하는 방법
@Transactional(isolation=Isolation.DEFAULT)
public void updateSchedule(Long userId, ScheduleUpdateRequest request) {
scheduleRepository.deleteAllByUserA(coachId, request);
scheduleRepository.saveAllByUser(coachId, request);
}
DEFAULT 설정은 설정된 DB의 격리 수준을 따른다는 의미이다.
데이터베이스 트랜잭션 개념과 격리 수준에 대해 알아봤다. 다음에는 이 기세를 이어서 트랜잭션 전파에 대해 학습해 봐야 겠다.