JPA를 공부했다면 잘알지는 못해도 반드시 본다고 해도 과언이 아닌 @Transaction
이다. 하지만 내가 아는 트랜잭션은 데이터베이스상의 하나의 작업단위정도로만 알고 있었다. 하지만 조사해본 결과 트랜잭션은 내가아는 것 이상으로 복잡한 기능을 가지고 있다. 그렇기에 트랜잭션의 이론과 @Transactional
을 정리하는 차원에서 이렇게 게시글을 작성하기로 했다.
트랜잭션이란 DBMS 또는 유사한 시스템에서 상호작용의 단위이다. 여기서 유사한 시스템이란 트랜잭션이 성공과 실패가 분명하고 상호 독립적이며, 일관되고 믿을 수 있는 시스템을 의미한다. 데이터의 정합성을 보장하기 위해 고안된 방법이다.
트랜잭션은 다음과 같은 목적으로 사용한다.
트랜잭션을 수행하는데 있어서 지켜야하는 4가지 규칙이 있는데 원자성(Atomicity), 일관성(Consistency), 독립성(Isolation), 영구성(Durability)이라고 하며 이 앞의 글자를 따서 ACID라고 부른다.
트랜잭션을 완전히 성공하거나 완전히 실패하도록 중간까지 실행되고 실패하는 일이 없이 단일 단위로 처리되도록 보장하는 능력이다.
(트랜잭션이 시작했으면 트랜잭션이 끝날 때까지 진행해야하는 것이 원자성)
트랜잭션이 실행되는 동안 데이터베이스가 일관성있는 상태로 있어야한다는 의미이다. 트랜잭션이 성공적으로 완료되면 동일한 데이터베이스 상태로 유지되어야한다.
(트랜잭션이 실행되는 동안 데이터베이스에 변화가 있으면 안되는 것이 일관성)
트랜잭션 수행중 다른 트랜잭션이 연산작업중 끼어들지 못하도록 보장하는 규칙이다. 즉 여러개의 트랜잭션이 동시에 발생해도 트랜잭션이 개별적으로 발생해야하는 것과 같아야 한다.
(트랜잭션이 진행되는동안 다른 트랜잭션이 끼어들어서는 안되는 것이 독립성)
성공적으로 트랜잭션을 완료했다면 그 결과가 반드시 영구적으로 남아 있어야 함을 의미한다. 트랜잭션은 로그에 모든 것이 저장된 후에만 commit 상태로 간주될 수 있다.
(트랜잭션이 끝나고 난 후 결과가 데이터베이스에 그대로 있어야하는 것이 영구성)
만약 비즈니스 로직에서 하나라도 실패하면 시스템은 해당 트랜잭션을 RollBack한다.
트랜잭션의 이론에 대해 간략하게나마 알아봤으니 이제 본격적으로 @Transactional
을 알아보도록 하자
@Transactional
은 보통 Service계층에서 주로 사용되고 있다. @Transactional
은 해당 메서드 내에서 오류가 발생하면 RollBack을 하고 작업이 모두 성공해야만 Commit이 되도록 관리를 해주는 어노테이션이다. @Transactional
을 사용한다면 트랜잭션이 적용되는 범위와 RollBack되는 범위를 정할 수 있다.
@Transactional
을 클래스 혹은 메서드에 명시하면, AOP를 통해 타겟이 상속하고 있는 인터페잇 또는 타겟을 상속한 프록시 객체가 생성되고 이때 프록시 객체의 메서드를 호출하면 타겟 메서드 전 후로 트랜잭션을 수행한다. (하단의 이미지 참고)
참고로 프록시 패턴을 별도로 생성하지 않았는데도 프록시를 타는게 가능한 이유는 CGLIB(Code Generator Library)라이브러리로 클래스릐 바이트 코드를 조작하여 프록시 객체를 생성하는데 타겟으로 지정된 인터페이스를 제외한 클래스나 메서드에 대해서 프록시를 생성할 수 있다.
트랜잭션 Advisor : 트랜잭션 관리를 결합하여 트랜잭션을 자동으로 관리하는 기능
기본적으로 @Transactional
은 이렇게 사용된다.
@Transactional
public void readMethod() {
...
}
하지만 이것은 @Transactional
의 옵션을 기본으로 했을 때와 같은 의미이다. 앞에서 @Transactional
은 트랜잭션이 적용되는 범위와 RollBack되는 범위가 있다고 설명했는데 이제 그 옵션들에 대해 알아보도록 하자
@Transactional
을 사용하고 트랜잭션이 적용되는 범위를 정하고 싶으면 propagation옵션에 값을 넣어주어야한다. 이 옵션을 사용하면 새롭게 트랜잭션을 만들 수 있고 부모 트랜잭션에 합류하도록 정해줄 수 있다.
옵션 | 설명 |
---|---|
REQUIRED(기본값) | 부모 트랜잭션이 존재한다면 합류하고 없다면 새로운 트랜잭션 생성 |
REQUIRES_NEW | 무조건 새로운 트랜잭션 생성한다, 각각의 트랜잭션이 롤백되더라도 서로 영향을 주지 않는다. |
MANDATORY | 부모 트랜잭션에 합류한다, 만약 부모 트랜잭션이 없다면 롤백을 발생시킨다. |
NESTED | 이미 실행중인 트랜잭션이 있다면 중첩하여 트랜잭션을 진행한다, 부모 트랜잭션은 중첩 트랜잭션에게 영향을 주지만 중첩은 부모에게 영향을 주지 않는다. |
SUPPORTS | 부모 트랜잭션이 있다면 합류하고 부모 트랜잭션이 없으면 생성하지 않는다. |
NOT_SUPPORTED | 부모 트랜잭션이 있다면 보류하고 없다면 트랜잭션을 생성하지 않는다. |
NEVER | 트랜잭션을 생성하지 않고 부모 트랜잭션이 존재한다면 예외를 발생시킨다. |
@Transactional(propagation = Propagation.SUPPORTS)
public void readMethod() {
...
}
트랜잭션이 동시에 발생했다면 변경사항을 어떻게 적용할지에 대한 설정이다. 트랜잭션이 동시에 여러개인 경우에는 다음과 같은 문제가 발생할 수 있다.
문제 | 내용 |
---|---|
Dirty | 변경사항이 반영되지 않은 값을 다른 트랜잭션에서 읽도록 허용하는 경우에 발생하는 데이터 불일치 |
Non-Repeatable Read | 한 트랜잭션 내에서 값을 조회할 때, 동시성 문제로 인해 같은 쿼리를 반환하는 경우 즉 쿼리 결과가 일관성을 가지지 못하는 경우 |
Phantom Read | 외부에서 수행되는 입력/삭제로 인해 트랜잭션 내에서 동일한 쿼리가 다른 값으로 반환하는 경우 |
위와 같은 여러개의 트랜잭션으로 인한 동시성 문제를 해결하고자 isolation속성이 존재한다.
옵션명 | 내용 | 발생문제 |
---|---|---|
DEFAULT(기본값) | 별도의 값을 설정하지 않은 경우 DBMS의 isolation level을 따른다. | - |
READ_UNCOMMITED | 아직 commit되지 않은 데이터를 다른 트랜잭션이 읽는 것을 허용 | Dirty Read, Non-repeatable Read, Phantom Read |
READ_COMMITED | commit이 이유러진 트랜잭션만 조회가능 | Non-repeatable Read, Phantom Read |
REPEATABLE_READ | 트랜잭션 범위 내에서 조회한 데이터가 항상 동일함을 보장하며 다른 사용자는 트랜잭션 영역에 해당하는 데이터는 수정 불가능 | Phantom Read |
SERIALIZABLE | 읽기 일관성 모드를 제공한다. 다른 사용자는 트랜잭션 영역에 해당하는 데이터 수정 및 입력이 불가능하다. 성능 저하의 우려가 있다. | - |
각 DB별 isolation level
DB | 격리수준 |
---|---|
MySQL | REPEATABLE READ |
Oracle | READ COMMITTED |
H2 | READ COMMITTED |
@Transactional(isolation=Isolation.READ_COMMITED)
public void readMethod() {
...
}
이렇게 트랜잭션에 대해 설명했는데 우연히 카카오페이의 백엔드 개발자의 게시글을 보게 되었는데 내용이 상당히 흥미로웠다.
결론부터 말하자면 @Transactional을 사용할 상황을 줄이자였다. 이부분 부터 흥미로워서 계속 읽게 되었는데
특히 동시성 제어부분이 가장 흥미로웠는데 예를 들어 트랜잭션에서 select, update 작업이 하나의 트랜잭션이고 DB가 MySQL인 경우 기본 격리 수준이 REPEATABLE READ이라고 앞에서 작성했다. 그로 인해 Phantom Read 문제가 발생할 수 있는데 이 문제는 외부의 조작으로 인해 다른 값으로 반환되는 문제이다. 이런 경우에는 격리 수준을 최고 단계인 SERIALIZABLE 올리는 방법도 있지만 성능 이슈가 생길 수 있기에 예외로 하고 Redis등을 활용하여 중복 제어요청을 하거나 아니면 동시성이 발생할 수 있는 비즈니스 로직 구조 자체에 문제가 있는지 확인해야 한다고 한다.
나도 솔직히 정리하면서 이론적인 이해는 되었지만 과연 얼마나 사용할지 라는 의문을 가지긴 했다. 그때 카카오페이 백엔드 개발자 글을 읽게 되면서 그 의문이 풀린 거 같다. 결국 완벽한 기술은 없고 그때마다 필요한 경우에 사용하면 된다가 결론이다. 그래도 데이터베이스의 트랜잭션과 ACID 특징을 배울 수 있어서 의미 있는 시간이었다.
https://simsim231.tistory.com/280
https://steady-coding.tistory.com/610
https://velog.io/@betterfuture4/Spring-Transactional-총정리
(항상 감사합니다.)