이번에 프로젝트를 진행하면서 Transactional Annotation을 사용한다고 한다. 무작성 사용하기 보다는 Transaction이 무엇이며 Transactional Annotation은 왜 쓰는지 제대로 알고 사용을 하고 싶어서 이번 기회에 공부를 해보려 한다!
데이터베이스의 상태를 변경시키기 위해서 수행하는 작업의 단위
이렇게만 들으면 어려운 것 같다. 그렇다면 간단한 예를 들어서 설명을 해보겠다.
내가 식당에서 가서 밥을 먹고 계산을 하려고 한다. 오래된 가게라 카드를 받지 않기에 계좌 이체로 돈을 내려고 한다. 이때 계좌이체는 내 계좌에서 돈이 빠져 나가는 인출과 밥집의 계좌로 돈이 들어가는 입금이 진행이 된다.
하지만, 내 계좌에서 돈만 빠져나가고 밥집의 계좌로 돈이 들어가지 않는다면? 난 돈만 잃게 되는 것이다.
때문에 이러한 억울한 상황이 벌어지지 않기 위해서는 인출과 입금이 동시에 성공을 하던지 동시에 실패를 해야하는데 이처럼 동시에 진행되는 과정들을 하나로 묶는 방법을 바로 트랜잭션이라고 한다.
트랜잭션이 데이터베이스에 모두 반영이 되거나 혹은 모두 반영이 되지 않아야 한다
트랜잭션의 작업 처리 결과가 일관성이 있어야 한다
하나의 트랜잭션이 진행될 때 다른 트랜잭션이 끼어들 수 없다
트랜잭션이 성공적으로 완료되었을 경우, 영구적으로 반영이 되어야 한다
이 트랜잭션은 commit과 rollback 처리를 자동으로 수행해준다.
commit이란 무엇일까?
개발자들이라면 아마 지겹도록 들은 단어일 것이다.. 나도 지금 계속 듣고 있다.. 커밋 잘 찍어라… 아무튼 우리가 흔히 알고 있는 commit은 동일한 개념이라고 생각하면 된다. 트랜잭션의 작업 처리가 성공적으로 끝이 났으며, 데이터베이스가 일관성이 있는 상태라면 하나의 트랜잭션이 끝이 났다고 하는데, 이를 알려주기 위해서 사용하는 연산이다.
그렇다면 rollback은 무엇일까?
하나의 트랜잭션 처리가 비정상적으로 종료되어 트랜잭션의 원자성이 깨진 경우, 트랜잭션을 처음부터 다시 시작하거나 부분적으로 처리되었던 결과를 다시 취소 시키는 것이다. 아까 위의 예시에서 돈이 빠져나가기만 하고 들어오지 않는다면 빠져나간 결과도 다시 취소시켜서 내 돈을 안전하게 보관해주는 것이다..
자, 여기까지 트랜잭션에 대해서 간단하게 알아보았다.
어? 그렇다고 끝이 난 것은 아니다. Annotation이 아직 남아있으니 너무 실망하지 않길 바란다 ^^
이처럼 동시에 진행이 되는 연산 과정을 하나로 묶는 이 트랜잭션, commit과 rollback의 연산을 수행해준다고 하는데 이를 코드에서는 어떻게 적용을 시켜야할까?
스프링 부트에서는 어노테이션의 방식으로 사용을 한다.
@Transactional을 메소드, 클래스, 인터페이스 위에 추가하여 사용하는데 이를 선언적 트랜잭션이라고 부른다. 적용된 범위에서는 트랜잭션 기능이 포함된 프록시 객체가 생성되어 자동으로 commit 또는 rollback을 진행해준다.
💡 프록시 객체란?
원래 객체를 감싸고 있는 객체로, 원래 객체와 타입은 동일
@Transactional
@Service
public class TestService {
public void test1() {
// 로직 구현
}
public void test2() {
// 로직 구현
}
}
클래스에 선언을 하게 된다면 해당 클래스에 속하는 test1(), test2() 메소드에 적용이 된다.
@Service
public class TestService {
@Transactional
public void test1() {
// 로직 구현
}
public void test2() {
// 로직 구현
}
}
메소드에 선언을 하게 된다면 해당 메소드인 test1() 메소드에만 적용이 된다.
그렇다면 이렇게 어노테이션만 붙이면 적용이 되느냐?
물론 적용이 되긴 한다. 하지만 트랜잭션에는 다양한 옵션들이 존재한다. 이 옵션들과 함께 사용한다면 내가 원하는대로 동작시킬 수 있다!
트랜잭션의 특징 중 isolation, 즉 독립성과 관련된 옵션이다. 트랜잭션에서 일관성 없는 데이터의 허용 수준을 설정한다.
적용 방식
@Transactional(isolation=Isolation.DEFAULT)
public void test() {
// 로직 구현
}
기본 격리 수준이라고 하며, DB의 isolation Level에 따른다.
가장 낮은 레벨로 커밋이 되지 않는 데이터에 대한 읽기를 허용한다.
Dirty Read가 발생할 수 있다.
❗Dirty Read가 발생?
트랜잭션 A가 데이터가 변경되고 커밋을 하기 전에 해당 변경 사항을 트랜잭션 B가 조회할 수 있다는 뜻으로, 만약 트랜잭션 A가 변경한 사항이 커밋이 되지 않고 롤백이 된다면 트랜잭션 B는 무효화가 된 값을 읽고 처리를 한 것이므로 문제가 발생한다.
위의 문제를 해결하기 위해서 커밋된 데이터에 대해서만 읽기를 허용한다.
Non-Repeatable Read가 발생할 수 있다.
❗Non-Repeatable Read가 발생?
같은 트랜잭션에서 같은 데이터를 여러 번 조회를 했을 때 읽어온 데이터가 다른 경우를 뜻한다.
만일 커밋된 데이터에 대해서만 읽기를 허용한다면 트랜잭션 A에서 여러 번의 조회를 하는 와중에 트랜잭션 B가 A가 조회하고 있는 데이터에 수정된 값을 커밋했다면 트랜잭션 A에서는 이전의 결과와 현재의 결과가 다른 문제가 발생한다.
위의 문제를 해결하기 위해서 트랜잭션이 완료될 때까지 SELECT 문이 조회하는 데이터들에 대해 LOCK을 걸기 때문에 해당 영역의 데이터에 대한 수정을 막는 방식을 사용한다.
선행 트랜잭션이 읽은 데이터는 트랜잭션이 종료될 때까지 후행 트랜잭션이 갱신하거나 삭제가 불가능하기 때문에 한 트랜잭션에서 같은 쿼리 두번 실행에 대한 데이터의 일관성을 보장한다.
Phantom Read가 발생할 수 있다.
❗Phantom Read가 발생?
하나의 트랜잭션에서 두 번의 동일한 조회를 하였을 때 다른 트랜잭션의 INSERT로 인해 없던 데이터가 조회되거나 DELETE로 있던 데이터가 없어지는 데이터 불일치가 발생하는 현상이다. 역시나 마찬가지고 이전의 결과와 현재의 결과가 달라지는 문제가 발생한다.
⇒ 이를 해결하기 위해서 SERIALIZABLE이 있으나 이는 트랜잭션을 순차적으로 수행하는 것과 다를 바가 없기 때문에 성능이 매우 저하되어 잘 사용되지 않는다..
이처럼 격리레벨이 높아질수록 데이터의 무결성은 유지할 수 있으나 데이터에 Lock이 걸리게 되면서 많은 트랜잭션들을 순차적으로 처리하게 되고 이는 곧 데이터베이스의 성능 저하와 비용의 증가로 이어진다. 따라서 상황에 맞게 효율적인 방안을 사용하는 것이 제일 중요하다!
트랜잭션의 동작 도중 다른 트랜잭션을 호출하는 상황에 선택할 수 있는 옵션이다.
트랜잭션의 장점 중 하나는 여러 트랜잭션을 묶어서 커다란 하나의 트랜잭션 경계를 만들 수 있다는 점인데, 작업을 진행하다보면 기존에 트랜잭션이 진행 중일 때 추가적인 트랜잭션을 진행해야 하는 경우가 있다. 이때 트랜잭션이 진행 중이라면 추가 트랜잭션 진행을 어떻게 할지 결정하는 것이다.
적용 방식
@Transactional(propagation=Propagation.REQUIRED)
public void test() {
// 로직 구현
}
기본 설정이며, 활성화된 트랜잭션이 있는지 확인하고, 아무것도 없다면 새로운 트랜잭션을 생성한다.
현재 트랜잭션이 있다면 일시 중지하고 새로운 트랜잭션을 생성한다.
활성화된 트랜잭션이 존재하는지 확인하고 트랜잭션이 존재하는 경우 기존 트랜잭션이 사용된다. 트랜잭션이 없다면 비트랜잭션으로 실행된다.
REQUIRED와 비슷하게 이미 시작된 트랜잭션이 있으면 참여한다. 만일 트랜잭션이 시작된 것이 없으면 예외를 발생시킨다. 혼자서는 독립적으로 트랜잭션을 진행하면 안되는 경우에 사용한다.
트랜잭션을 사용하지 않게 한다. 이미 진행중인 트랜잭션이 있으면 보류하고 메소드를 실행한다.
트랜잭션을 사용하지 않도록 강제한다. 이미 진행 중인 트랜잭션이 존재한다면 예외를 발생시킨다.
이미 진행 중인 트랜잭션이 있다면 중첩 트랜잭션을 시작한다.
중첩 트랜잭션이란 트랜잭션 안에 다시 트랜잭션을 만드는 것을 의미한다. 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않는다. 부모 트랜잭션이 없을 경우 REQUIRED와 동일하게 작동된다.
특정 예외 발생시 rollback 처리를 하지 않음
적용 방식
@Transactional(noRollbackFor=Exception.class)
public void test() {
// 로직 구현
}
특정 예외 발생 시 강제로 Rollback
적용 방식
@Transactional(rollbackFor=Exception.class)
public void test() {
// 로직 구현
}
⇒ 모든 예외에 대해서 rollback을 진행하고 싶은 경우에는 rollbackFor를 사용해야한다.
그 이유는 @Transactional은 기본적으로 Unchecked Exception, Error 만을 Rollback 하기 때문이다. 따라서 Checked Exception도 포함을 시키고 싶을 때는 rollbackFor 옵션을 같이 사용해주면 된다!
지정한 시간 내에 해당 메소드 수행이 완료되지 않을 경우 rollback을 수행한다. 이때 timout=-1일 경우 no timeout이 되며 -1은 default값이다.
적용 방식
@Transactional(timeout=100)
public void test() {
// 로직 구현
}
true 일 때 insert, update, delete 실행이 된다면 예외를 발생시킨다. default 값은 false이다.
적용 방식
@Transactional(readonly=true)
public void test() {
// 로직 구현
}
트랜잭션이란 동시에 진행이 되는 연산의 과정을 하나로 묶는 방법이다!
commit과 rollback의 연산이 있으며, 원자성, 일관성, 독립성, 지속성의 특징을 가지고 있다!
어노테이션을 사용해서 간단하게 구현할 수 있다. 이를 선언적 트랜잭션이라고 한다!
옵션에는
가 있다!