Transaction

yboy·2022년 8월 9일
0

Learning Log 

목록 보기
15/41
post-thumbnail

학습동기

우아한테크코스 레벨3 프로젝트에서 JPA를 쓰고 있다. JPA는 Transaction 단위로 동작한다. 따라서 JPA를 사용하는 입장에서 Transaction의 개념에 대해 잘 알아야 한다고 생각해 왔지만 그러지 못하고 있었기 때문에 학습을 다짐하게 됐다.(내가 아는 것이라고는 스프링에서 @Transcational을 붙이면 예외가 발생했을 때 Rollback해준다는 것 뿐...) 그럼 학습을 해보자.

학습내용

Transaction

데이터베이스의 상태를 변경시키기 위해 수행하는 작업 단위

데이터베이스를 사용하는 애플리케이션에서 하나의 작업 단위는 데이터베이스의 상태 변경(SELECT, UPDATE, INSERT, DELETE)을 기반으로 한다. 하나의 작업 단위에서 데이터베이스의 상태 변경은 한 번일 수도 있지만 여러 번일 가능성이 크다. 여러 번의 상태 변경이 일어날 때, 안전성을 확보하는 방법이 트랜잭션이다. 어떻게 안전성을 보장한다는 거지?
예를 들어 보자,

  public void updateSchedule(Long userId, ScheduleUpdateRequest request) {
        scheduleRepository.deleteAllByUserA(coachId, request);
        scheduleRepository.saveAllByUser(coachId, request);
    }
  1. db에서 해당 코치의 일정을 삭제한다.
  2. db에 요청 값으로 들어온 일정을 저장한다.

위의 함수는 1, 2번 순서대로 코치의 일정 수정에 대한 작업을 담당하고 있다.
delete와 save 두 개의 db 상태 변화를 요하고 있는데, 여기서 만약 1번 작업이 완료된 후, 2번 작업에서 에러가 발생했다고 생각해보자. 그럼 db에서 값은 삭제 되었는데 저장이 되지 않았다. 치명적인 에러가 될 수 있다. 위의 예시는 두 개의 상태 변화만을 요하고 있지만 만약 여러 개의 상태 변화를 요하고 있다고 생각하면 상상만 해도 끔직한 결과를 초래할 수 있다. 무조건적으로 모든 작업이 완료된 후에 데이터베이스에 작업 내용이 반영되어야 한다.

이러한 상황에서 transaction은 커밋과 롤백을 통해 안전성을 확보해 준다.

용어

커밋(Commit)

  • 하나의 트랜잭션이 성공적으로 끝나서 데이터베이스가 일관성있는 상태에 있음을 의미
  • 모든 부분작업이 정상적으로 완료되면 이 변경사항을 한꺼번에 DB에 반영한다.

롤백(Rollback)

  • 하나의 트랜잭션 처리가 비정상적으로 종료되었을 때의 상태를 의미
  • 롤백을 통해 트랜잭션을 다시 실행하거나 부분적으로 변경된 결과를 취소할 수 있다.
  • 하나의 트랜잭션 처리가 비정상적으로 종료되었을 때, 이 트랜잭션이 수행한 모든 연산(SELECT, UPDATE, INSERT, DELETE)을 취소하는 것

Transcation ACID(특징)

트랜잭션에는 4가지 특성이 존재한다. 이를 줄여서 ACID라고 한다.

원자성(Atomicity)

  • 트랜잭션이 DB에 모두 반영되거나, 혹은 전혀 반영되지 않아야 한다.

일관성(Consistency)

  • 트랜잭션의 작업 처리 결과는 항상 일관성이 있어야 한다.

독립성(Isolation)

  • 하나의 트랜잭션은 다른 트랜잭션에 끼어들 수 없고 독립적임을 의미한다. 각각의 트랜잭션은 서로 간섭이 불가능하다.

지속성(Durability)

  • 트랜잭션이 성공적으로 완료되면 영구적으로 결과에 반영되어야 함을 뜻한다. 이것을 commit으로 만족시킬 수 있다.

@Transactional

스프링에선 아주 간단한 방법으로 트랜잭션 처리를 지원한다. 클래스, 메서드에 @Transactional을 선언하여 사용하기만 하면 된다.

  @Transactional
  public void updateSchedule(Long userId, ScheduleUpdateRequest request) {
        scheduleRepository.deleteAllByUserA(coachId, request);
        scheduleRepository.saveAllByUser(coachId, request);
    }

여기서 주의해야 할 사항이 있는데 데이터베이스의 Auto Increment 옵션을 적용했다면, insert 동작으로 실행된 id는 트랜잭션이 롤백되어도 다시 감소하지 않는 다는 것이다. 즉, Auto Increment는 트랜잭션과 별개로 동작한다. 이유는 동시성 문제 때문인데 테코블: Transactional 어노테이션
링크에 이유가 잘 나와 있다.

Insolation level(격리수준)

다수의 트랜잭션이 실행되는 상황에서는 많은 문제점이 발생할 수 있다.
하나의 트랜잭션이 값을 변경하고 커밋하지 않은 상황에서 다른 트랜잭션이 같은 값을 조회하는 경우에 변경된 값이 조회된다던지(Dirty Read),
하나의 트랜잭션이 어떤 값을 조회한 후, 한 번더 똑같은 값을 조회하는 경우에 두 조회 사이에 다른 트랜잭션에서 값을 수정해 버려서 조회 값이 다르게 된다던지(Non-Repeatable Read),
또는 두 조회 사이에 다른 트랜잭션에서 값을 등록해 버려서 조회 값이 다르게 된다던지(존재하지 않던 녀석이 갑자기 생겨나는 것)(Phantom Read)....

그럼 이제 문제점을 해결해 줄 단서가 될 수 있는? 격리수준에 대해 알아보자.
READ_UNCOMMITED(level 0)

  • 커밋되지 않는(트랜잭션 처리중인) 데이터에 대한 읽기를 허용
  • 각 트랙잭션에서의 변경 내용이 커밋이나 롤백 여부에 상관 없이 다른 트랜잭션에서 보여지게 된다.
  • Dirty Read가 허용된다.
  • RDBMS 표준에서 트랜잭션의 격리 수준으로 인정하지 않을 정도로 문제가 많은 격리 수준

READ_COMMITTED(level 1)

  • 트랜잭션이 커밋된 확정 데이터만 읽기 허용
  • 어떤 트랜젝션에서 변경한 내용이 커밋되기 전까지는 다른 트랜잭션에서 변경 내역을 조회할 수 없다.
  • 가장 많이 선택되는 격리 수준
  • Dirty Read 방지
  • Non-Repeatable Read 허용
  • 오라클 DBMS의 기본적인 격리 수준

REPEATABLE_READ(level 2)

  • 언두 영역에 백업된 이전 데이터를 통해 동일한 트랜잭션 내에서는 동일한 결과를 보여줄 수 있도록 보장한다.
  • 쿼리가 두 번 나갔을 때 일관성있는 결과를 리턴한다.
  • Non-Repeatable read 방지
  • Phantom Read 허용
  • MySQL의 InnoDB 스토리지 엔진에서 기본적으로 사용되는 격리 수준

SERIALIZABLE(level 3)

  • 트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에 shared lock이 걸리므로 다른 사용자는 그 영역에 해당되는 데이터에 대한 수정 및 입력이 불가능하다
  • 한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 절대 접근불가
  • Phantom Read 방지
  • 성능상 많이 떨어진다.

참고: Spring에서 격리 수준을 설정하는 방법

@Transactional(isolation=Isolation.DEFAULT)
public void updateSchedule(Long userId, ScheduleUpdateRequest request) {
        scheduleRepository.deleteAllByUserA(coachId, request);
        scheduleRepository.saveAllByUser(coachId, request);
    }

DEFAULT 설정은 설정된 DB의 격리 수준을 따른다는 의미이다.

마무리

데이터베이스 트랜잭션 개념과 격리 수준에 대해 알아봤다. 다음에는 이 기세를 이어서 트랜잭션 전파에 대해 학습해 봐야 겠다.

0개의 댓글