실무에서 rabbitmq send message 부분이 데이터 업데이트 하는 @transactional 함수에 같이 포함되어 있는 부분이 있었다.
이로 인해, 실제로 데이터가 업데이트가 되던지 말던지 rabbitmq에는 이미 업데이트가 되었다는 메시지가 보내지고 있는 중이었다.
이를 해결하기 위해 데이터를 업데이트 하는 부분만 @transactional 속성을 가진 함수로 구현을 하고 그 함수를 호출하여 실제 업데이트에 성공을 하면 mq에 메시지를 보내는 형식으로 분리하여 수정을 하였다.
@Transactional은 동시에 여러 데이터를 관리하는 경우,
하나의 실패로 다른 데이터 영역까지 영향을 주면 안되므로 모든 데이터가 성공적으로 업데이트가 되어야 commit 처리를 하고 하나라도 실패를 하면 rollback을 한다.
정확하게 @Transactional을 이해하고 다음부터는 일련의 작업들이 정상적으로 동작하도록 구현해야겠다.
- 트랜잭션 이란?
데이터베이스의 상태를 변경시키는 작업 또는 한번에 수행되어야하는 연산들을 의미한다.
한번에 수행되어야하는 연산들은 모두 에러 없이 끝나야 하며, 만약 중간에 에러가 발생 한다면 에러 발생 이전 시점까지 작업되었던 내용은 모두 원상복구 되어야 한다.
즉, 트랜잭션 작업이 끝나면 Commit 또는 Rollback 되어야한다.
예를 들어, FLO 서비스 이용권 구매를 생각해보면 고객이 이용권을 구매하려고 선택을 하고 결재를 하였다.
결재를 했으니 고객에겐 구매한 이용권이 표시가 되고 FLO 이용권 관라자에게는 구매자인 고객의 정보와 함께 '결재완료' 내용을 보내준다.
그런데 중간에 문제가 생기면 '결재완료' 정보는 업데이트 되었는데 판매자 시스템에 문제가 생겨
구매자의 정보와 '결재완료' 내용이 업데이트 되지 않고 당연히 고객은 '결재완료' 데이터가 없으므로
FLO 음악 서비스를 이용하지 못하게 된다.
이처럼 일련의 작업에 대한 에러가 발생한 경우, 원상복구 시키는 작업이 바로 트랙잭션 처리이다.
- 트랜잭션의 성질
원자성(Atomicity)
한 트랜잭션 내에서 실행한 작업들은 하나의 단위로 처리한다. 즉, 모두 성공 또는 모두 실패
일관성(Consistency)
트랜잭션은 일관성 있는 데이터베이스 상태를 유지한다.
격리성(Isolation)
동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 격리해야한다.
영속성(Durability)
트랜잭션을 성공적으로 처리되면 결과가 항상 저장되어야한다.
- 스프링에서 트랙잭션을 처리하는 방법
1.선언적 트랜잭션
2.@Transactional 어노테이션을 통한 트랜잭션
스프링에서는 xml 또는 Javaconfig를 통해 설정 할 수 있다.
Spring boot에서는 별도의 설정이 필요 없으며, 클래스 또는 메소드에 선언할 수 있다.
import com.example.bamdule.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
@Transactional
//@Transactional(propagation = , isolation = ,noRollbackFor = ,readOnly = ,rollbackFor = ,timeout = )
public class UserServiceImpl implements UserService {
...
}
@Transactional을 클래스 단위 혹은 메서드 단위에 선언해주면 된다.
클래스에 선언하게 되면, 해당 클래스에 속하는 메서드에 공통적으로 적용된다.
메서드에 선언하게 되면, 해당 메서드에만 적용된다.
- @Transactional 동작원리
트랜잭션은 어노테이션 기반 AOP를 통해 구현되어있다.
AOP ( Aspect Oriented Programming )
관점 지향 프로그래밍. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.
예로들어 핵심적인 관점은 결국 우리가 적용하고자 하는 핵심 비즈니스 로직이 된다. 또한 부가적인 관점은 핵심 로직을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.
AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어서 모듈화하겠다는 의미다. 위와 같이 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지다.
import org.springframework.transaction.annotation.Transactional;
따라서, 아래와 같은 특징이 있다
클래스, 메소드에 @Transactional이 선언되면 해당 클래스에 트랜잭션이 적용된 프록시 객체 생성
프록시 객체는 @Transactional이 포함된 메서드가 호출될 경우,
트랜잭션을 시작하고 Commit or Rollback을 수행
CheckedException or 예외가 없을 때는 Commit
UncheckedException이 발생하면 Rollback
- @Transactional 모드
@Transactional은 Proxy Mode와 AspectJ Mode가 있는데 Proxy Mode가 Default로 설정되어있다.
Proxy Mode는 다음과 같은 경우 동작하지 않는다.
private method에서는 동작하지 않음
Service 객체를 Proxy를 통해서 얻어오고, 얻어온 객체를 접근할 때 Proxy에 의하여 Transaction이 시작되기 때문에 당연히 이런 상황에서는 Transaction이 적용되지 못한다.
Non-Public 메서드에 적용하고 싶으면 AspectJ Mode를 고려해야한다.
transaction이 적용되지 않은 public method 내부에서 transaction이 적용된 public method를 호출하는 경우, transaction이 동작하지 않는다.
readonly transaction이 적용된 public method 내부에서 not-readonly transaction이 적용된 public method를 호출하는 경우, 모든 method는 readonly transaction으로 동작하게 된다.
3의 경우는 반대로도 적용된다.
- @Transactional 옵션
propagation
트랜잭션 동작 도중 다른 트랜잭션을 호출할 때, 어떻게 할 것인지 지정하는 옵션이다
isolation
트랜잭션에서 일관성없는 데이터 허용 수준을 설정한다
noRollbackFor=Exception.class
특정 예외 발생 시 rollback하지 않는다.
rollbackFor=Exception.class
특정 예외 발생 시 rollback한다.
timeout
지정한 시간 내에 메소드 수행이 완료되지 않으면 rollback 한다. (-1일 경우 timeout을 사용하지 않는다)
readOnly
트랜잭션을 읽기 전용으로 설정한다.
- 1. propagation
REQUIRED (Default)
이미 진행중인 트랜잭션이 있다면 해당 트랜잭션 속성을 따르고, 진행중이 아니라면 새로운 트랜잭션을 생성한다
REQUIRES_NEW
항생 새로운 트랜잭션을 생성한다. 이미 진행중인 트랜잭션이 있다면 잠깐 보류하고 해당 트랜잭션 작업을 먼저 진행한다
SUPPORT
이미 진행 중인 트랜잭션이 있다면 해당 트랜잭션 속성을 따르고, 없다면 트랜잭션을 설정하지 않는다.
NOT_SUPPORT
이미 진행중인 트랜잭션이 있다면 보류하고, 트랜잭션 없이 작업을 수행한다.
MANDATORY
이미 진행중인 트랜잭션이 있어야만, 작업을 수행한다. 없다면 Exception을 발생시킨다.
NEVER
트랜잭션이 진행중이지 않을 때 작업을 수행한다. 트랜잭션이 있다면 Exception을 발생시킨다.
NESTED
진행중인 트랜잭션이 있다면 중첩된 트랜잭션이 실행되며, 존재하지 않으면 REQUIRED와 동일하게 실행된다.
- 2. isolation
Default
사용하는 DB 드라이버의 디폴트 설정을 따른다. 대부분 READ_COMMITED를 기본 격리수준으로 설정한다.
READ_COMMITED
트랜잭션이 커밋하지 않은 정보는 읽을 수 없다. 하지만 트랜잭션이 읽은 로우를 다른 트랜잭션에서 수정 할 수 있다
그래서 트랜잭션이 같은 로우를 읽었어도 시간에 따라서 다른 내용이 발견될 수 있다.
READ_UNCOMMITED
가장 낮은 격리 수준이다. 트랜잭션이 커밋되기 전에 그 변화가 다른 트랜잭션에 그대로 노출된다.
하지만 속도가 빠르기 떄문에 데이터의 일관성이 떨어지더라도, 성능 극대화를 위해 의도적으로 사용하기도 한다.
REPEATABLE_READ
트랜잭션이 읽은 로우를 다른 트랜잭션에서 수정되는 것을 막아준다. 하지만 새로운 로우를 추가하는 것은 제한하지 않는다.
SERIALIZABLE
가장 강력한 트랜잭션 격리수준이다. 여러 트랜잭션이 동시에 같은 테이블 로우에 액세스하지 못하게 한다.
가장 안전하지만 가장 성능이 떨어진다.
- 3. rollbackFor
트랜잭션 작업 중 런타임 예외가 발생하면 롤백한다. 반면에 예외가 발생하지 않거나 체크 예외가 발생하면 커밋한다.
체크 예외를 커밋 대상으로 삼는 이유는 체크 예외가 예외적인 상황에서 사용되기 보다는 리턴 값을 대신해서 비즈니스 적인 의미를 담은 결과로 돌려주는 용도로 사용되기 때문이다.
스프링에서는 데이터 엑세스 기술의 예외를 런타임 예외로 전환해서 던지므로 런타임 예외만 롤백대상으로 삼는다.
하지만 원한다면 체크예외지만 롤백 대상으로 삼을 수 있다. rollbackFor또는 rollbackForClassName 속성을 이용해서 예외를 지정한다.
- 4. noRollbackFor
rollbackFor 속성과는 반대로 런타임 예외가 발생해도 지정한 런타임 예외면 커밋을 진행한다.
5. timeout
트랜잭션에 제한시간을 지정한다. 초 단위로 지정하고, 디폴트 설정으로 트랜잭션 시스템의 제한시간을 따른다.
-1 입력 시, 트랜잭션 제한시간을 사용하지 않는다.
6. readOnly
트랜잭션을 읽기 전용으로 설정한다. 특정 트랜잭션 안에서 쓰기 작업이 일어나는 것을 의도적으로 방지하기 위해 사용된다. insert,update,delete 작업이 진행되면 예외가 발생한다.