데이터베이스의 상태를 변화시키기 해서 수행하는 작업의 단위
- '데이터베이스의 상태를 변화' = 질의어(SQL) 를 이용하여 데이터베이스를 접근 하는 것
- '질의어(SQL)' : SELECT, INSERT, DELETE, UPDATE
- '작업의 단위' : 많은 질의어 명령문들을 사람이 정하는 기준에 따라 정하는 것 (질의어 한 문장이 아님)
데이터베이스에서 데이터에 대한 하나의 논리적 실행단계
더 이상 쪼갤 수 없는 최소단위의 작업
사용자가 시스템에 대한 서비스 요구 시 시스템이 응답하기 위한 상태 변환 과정의 작업단위
하나의 트랜잭션 설계를 잘 만드는 것이 데이터를 다룰 때 많은 이점을 가져다준다.
게시판 사용자는 게시글을 작성하고 올리기 버튼을 누른다.
그 후에 다시 게시판에 돌아왔을때, 자신의 글이 포함된 업데이트된 게시판을 보게 된다.
이러한 상황을 데이터베이스 작업으로 옮기면
사용자가 올리기 버튼을 눌렀을 시, Insert 문을 사용하여 사용자가 입력한 게시글의 데이터를 옮긴다.
그 후에, 게시판을 구성할 데이터를 다시 Select 하여 최신 정보로 유지한다.
여기서 작업의 단위는 insert문과 select문 둘다 를 합친 것이다.
이러한 작업단위를 하나의 트랜잭션이라 한다.
회원 등록 요청을 했으나
USERNAME, PASSWORD, EMAIL, ROLE 中 PASSWORD 에만 저장되지 않은 경우
@Transactional 이 있었다면?
DB 는 1개의 회원 정보가 안전하게 저장됨을 보장
A 계좌 잔고 200,000 원 이상 확인(A 계좌 잔고: 1,000,000 원)
→ A 계좌 잔고 200,000 원 금액 감소(A 계좌 잔고: 800,000 원)
→ B 계좌 잔고 200,000 원 금액 증가(B 계좌 잔고: 1,200,000 원)
만약, 3번 과정에서 에러가 발생 시 (A 계좌 잔고에는 추가되지만, B 계좌 잔고에는 추가되지 않은 경우)
@Transactional 이 있었다면?
모두 성공 시 ⇒ 트랜잭션 Commit
중간에 하나라도 실패 시 ⇒ 트랜잭션 Rollback (모두 초기화)
ACID
: 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어
트랜잭션이 데이터베이스에 모두 반영(저장)되던가, 아니면 전혀 반영되지 않아야 한다.
트랜잭션의 일처리는 작업단위 별로 이루어 져야 사람이 다루는데 무리가 없다.
(트랜잭션의 정의를 보면, 트랜잭션은 사람이 설계한 논리적인 작업 단위이다.)
그러나 트랜잭션 단위로 데이터가 처리되지 않는다면,
설계한 사람은 데이터 처리 시스템을 이해하기 힘들 뿐만 아니라, 오작동 했을시 원인을 찾기가 매우 힘들어질 것이다.
트랜잭션의 작업 처리 결과가 항상 일관성이 있어야 한다.
트랜잭션이 진행되는 동안에 데이터베이스가 변경 되더라도
업데이트된 데이터베이스로 트랜잭션이 진행되는것이 아니라, 처음에 트랜잭션을 진행 하기 위해 참조한 데이터베이스로 진행된다.
이렇게 함으로써 각 사용자는 일관성 있는 데이터를 볼 수 있다.
둘 이상의 트랜잭션이 동시에 실행되고 있을 경우,
어떤 하나의 트랜잭션이라도 다른 트랜잭션의 연산에 끼어들 수 없다.
하나의 특정 트랜잭션이 완료될때까지, 다른 트랜잭션이 특정 트랜잭션의 결과를 참조할 수 없다.
트랜잭션이 성공적으로 완료됬을 경우, 결과는 영구적으로 반영(저장)되어야 한다.
하나의 트랜잭션은 Commit 되거나 Rollback 된다.
Commit
하나의 트랜잭션이 성공적으로 끝났고,
데이터베이스가 다시 일관된 상태에 있을 때, 이 트랜잭션이 행한 갱신 연산이 완료된 것을 트랜잭션 관리자에게 알려주는 연산
이 연산을 사용하면, 수행했던 트랜잭션이 로그에 저장되며
후에 Rollback 연산을 수행했었던 트랜잭션 단위로 처리하는 것을 도와준다.
Rollback
하나의 트랜잭션 처리가 비정상적으로 종료되어 트랜잭션의 원자성이 깨진경우,
이 트랜잭션의 일부가 정상적으로 처리되었더라도 트랜잭션의 원자성을 구현하기 위해
트랜잭션을 처음부터 재시작 or 트랜잭션의 부분적으로만 연산된 결과를 다시 취소(Undo, 폐기)시킨다.
후에 사용자가 트랜잭션 처리된 단위대로 Rollback 을 진행할 수도 있다.
flush vs commit
flush
- 영속성 컨텍스트에 있는 엔티티 정보를 DB 에 동기화를 하는 작업- 아직 트랜잭션 commit 이 안 됐기 때문에, 에러가 발생 시 롤백 O
commit
- 트랜잭션 commit 된 이후, DB 에 동기화된 정보는 영구히 반영돼버린다.
(즉, 롤백 할 수 없는 상태가 되는 것)
활성화(활동) : 트랜잭션이 실행중인 상태
부분 완료 : 트랜잭션의 마지막 연산까지 실행했지만, Commit 연산이 실행되기 직전의 상태
완료 : 트랜잭션이 성공적으로 종료되어 Commit 연산을 실행한 후의 상태
실패 : 트랜잭션 실행에 오류가 발생하여 중단된 상태
철회 : 트랜잭션이 비정상적으로 종료되어 Rollback 연산을 수행한 상태
TrnasactionTemplate이나 개별 데이터 기술의 트랜잭션 API를 이용해, 직접 코드안에서 사용
선언적 트랜잭션과는 반대 개념
@Transactional이 붙으면 Spring 은 해당 타깃을 포인트 컷의 대상으로 자동 등록하며 트랜잭션 관리 대상이 된다.
→ 즉, 이 어노테이션을 통해 포인트 컷에 등록하고 트랜잭션 속성을 부여
(포인트 컷 : 어떤 포인트(Point)에 기능을 적용할지 하지 않을지 잘라서(cut) 구분하는 필터링 로직)
주로 Service 계층에서 사용
클래스, 인터페이스, 메소드에 부여 가능
데이터를 등록, 수정, 삭제하는 메소드에 트랜잭션 처리
- 외부에서는 단순히 메소드를 호출하는 것처럼 보이지만, Spring의 트랜잭션 AOP가 먼저 동작
AOP는 대상 메소드를 호출하기 직전에 트랜잭션을 시작,
메소드가 종료되면 트랜잭션을 commit하면서 종료
JPA는 영속성 컨텍스트를 flush(영속성 컨텍스트의 변경 내용을 DB에 반영하는 것)해서
변경 내용을 db에 반영한 후에 데이터베이스 트랜잭션을 commit 한다.
영속성 컨텍스트의 변경 내용이 db에 반영
@Service
class BoardService {
// 트랜잭션 시작
@Transactional
public ResponseDto logic(Long id) {
// board는 영속 상태
Board board = repository.findOne(id);
return board;
}
// 트랜잭션 종료
// board는 준영속 상태
}
@Transactional <<< ???
// 1. 폴더들과 이름을 인자로 넘겨받습니다.
public List<Folder> addFolders(List<String> folderNames, String name) {
User user = userRepository.findByUsername(name).orElseThrow(
() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
);
// 2. 입력으로 온 폴더 이름을 기준으로 회원이 이미 생성한 폴더를 조회
List<Folder> existFolderList = folderRepository.findAllByUserAndNameIn(user, folderNames);
List<Folder> folderList = new ArrayList<>();
// 3. 입력으로 온 폴더를 조회한 폴더들과 비교
for (String folderName : folderNames) {
// 4. 이미 생성한 폴더가 아닌 경우만 폴더 생성
if (isExistFolderName(folderName, existFolderList).equals("false")) {
Folder folder = new Folder(folderName, user);
folderList.add(folder);
// 5. 그런데 만약 그 중에 하나의 폴더가 이미 있었으면 폴더를 만들지 않고 에러를 리턴합니다.
// 만약, 5개의 폴더를 받아 처리하던 中 세 번째 폴더가 중복이었으면?
// 요청이 실패했으니, 해당 메서드의 실행 전과 데이터가 같아야하는데, 앞에 두개는 다시 지워줘야 할까?
//--> 이러한 것들을 위해서 트랜잭션을 활용
} else {
throw new IllegalArgumentException("중복된 폴더명 ('" + isExistFolderName(folderName, existFolderList) + "')을 삭제하고 재시도해 주세요");
}
}
return folderRepository.saveAll(folderList);
}
트랜잭션에서 일관성 없는 데이터 허용 수준을 설정
@Transactional(isolation=Isolation.DEFAULT)
public void addUser(UserDTO dto) throws Exception {
// 로직 구현
}
DEFAULT
READ_UNCOMMITED (level 0)
READ_COMMITED (level 1)
REPEATEABLE_READ (level 2)
SERIALIZABLE (level 3)
레벨이 높아질수록, 데이터 무결성을 유지할 수 있다.
그러나
무조건적인 상위 레벨을 사용할 시,
Locking 으로 동시에 수행되는 많은 트랜잭션들이 순차적으로 처리하게 되면서
DB의 성능↓ 비용↑
(단, Locking 의 범위를 줄이게 되면, 잘못된 값이 처리될 여지도 발생 → 상황에 따라 효율적인 방안 선택하기)
트랜잭션 동작 도중 다른 트랜잭션을 호출할 때, 어떻게 할 것인지 지정
@Transactional(propagation=Propagation.REQUIRED)
public void addUser(UserDTO dto) throws Exception {
// 로직 구현
}
REQUIRED (Defualt)
이미 진행중인 트랜잭션이 있다면, 해당 트랜잭션 속성을 따르고
진행중이 아니라면, 새로운 트랜잭션을 생성
REQUIRES_NEW
항생 새로운 트랜잭션을 생성.
이미 진행중인 트랜잭션이 있다면 잠깐 보류하고, 해당 트랜잭션 작업을 먼저 진행
SUPPORT
이미 진행 중인 트랜잭션이 있다면, 해당 트랜잭션 속성을 따르고
없다면, 트랜잭션을 설정하지 않는다.
NOT_SUPPORT
이미 진행중인 트랜잭션이 있다면 보류하고, 트랜잭션 없이 작업을 수행
MANDATORY
이미 진행중인 트랜잭션이 있어야만, 작업을 수행
없다면, Exception을 발생시킨다.
NEVER
트랜잭션이 진행중이지 않을 때 작업을 수행
트랜잭션이 있다면 Exception을 발생시킨다.
NESTED
진행중인 트랜잭션이 있다면, 중첩된 트랜잭션이 실행
존재하지 않으면, REQUIRED와 동일하게 실행된다. (= 새로운 트랜잭션을 생성)
특정 예외 발생 시, rollback 하지 않는다.
@Transactional(noRollbackFor=Exception.class)
public void addUser(UserDTO dto) throws Exception {
// 로직 구현
}
특정 예외 발생 시, rollback 한다.
@Transactional(rollbackFor=Exception.class)
public void addUser(UserDTO dto) throws Exception {
// 로직 구현
}
@Transactional 은 기본적으로 Unchecked Exception, Error 만을 rollback 한다.
따라서,
모든 예외에 대해서 rollback을 진행하고 싶을 경우, (rollbackFor = Exception.class)
를 붙여야 한다.
→ 이유?
Checked Exception 같은 경우는 예상된 에러이며
Unchecked Exception, Error 같은 경우는 예상치 못한 에러이기 때문
지정한 시간 내에 메소드 수행이 완료되지 않으면, rollback 한다.
(-1일 경우, timeout을 사용하지 않는다)
→ Default = -1
@Transactional(timeout=10)
public void addUser(UserDTO dto) throws Exception {
// 로직 구현
}
트랜잭션을 읽기 전용으로 설정
→ Default = flase
@Transactional(readonly = true)
public void addUser(UserDTO dto) throws Exception {
// 로직 구현
}
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
체크 예외라면 롤백이 되지 않는다.(참고: 예외 처리 (Exception))
→ 롤백시키기 위해서는 @Transactional의 rollbackFor 속성으로 해당 체크 예외를 적어주어야 한다.
만약 언체크 예외라면, rollback 처리를 진행한다.
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "] after exception: " + ex);
}
// rollbackOn : 스프링의 트랜잭션은 내부적으로 언체크 예외(런타임 예외)이거나 에러(Error) 인지 검사
// 맞으면 롤백 여부를 결정하는 rollback-only를 True로 변경
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
...
}
모든 메소드에 @Transactional이 붙어있으면 메소드가 지저분해진다.
@Transactional 을 적용할 때,
1. 타깃 메소드
2. 타깃 클래스
3. 선언 메소드
4. 선언 타입 (클래스 or 인터페이스)
순으로 @Transactional 이 적용되었는지 차례로 확인한 후
가장 먼저 발견되는 속성 정보를 사용
이를 4단계의 대체 정책(fallback policy)
이라고 부른다.
→ 어노테이션을 최소화 + 세밀한 제어 가능
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // 클래스 레벨에 @Transactional을 붙여주면, 메소드까지 적용된다.
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
public List<User> getUserList() {
return userRepository.findAll();
}
}
트랜잭션 경계와 특정 계층의 경계(서비스 계층의 메소드(비지니스 로직을 담고 있는))를 일치(결합)시키는 것이 좋다.
(단, 트랜잭션을 중구난방으로 적용하는 것은 bad)
→ 서비스 계층에서 데이터 저장 계층으로부터 읽어온 데이터를 사용, 변경하는 등...의 작업을 하기 때문
→ 서비스 계층을 트랜잭션의 시작과 종료 경계로 정한 것
다른 계층이나 모듈에서 DAO 에 직접 접근하는 것은 차단해야 한다. (테스트 같은 특별한 이유를 제외하고)
방법 1
DAO가 제공하는 주요 기능은 서비스 계층에 위임 메소드를 만들어두기
(트랜잭션은 보통 서비스 계층의 메소드 조합을 통해 만들어진다)
방법 2
다른 모듈의 DAO 에 접근할 때는 서비스 계층을 거치기
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // 클래스 레벨
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
public List<User> getUserList() {
return userRepository.findAll();
}
@Transactional // 메소드 레벨
public User signUp(final SignUpDTO signUpDTO) {
final User user = User.builder()
.email(signUpDTO.getEmail())
.pw(passwordEncoder.encode(signUpDTO.getPw()))
.role(UserRole.ROLE_USER)
.build();
return userRepository.save(user);
}
}
클래스 레벨에서는 공통적으로 적용되는 읽기전용 @Transactional 을,
메소드 레벨에서는 추가/삭제/수정이 있는 작업에 쓰기가 가능하도록 별도로 @Transacional 을 선언하는 것이 좋다.
→ 성능적 이점
@Transactional
@ExtendWith(SpringExtension.class)
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
void findByEmailAndPw() {
final User user = User.builder()
.email("email")
.pw("pw")
.role(UserRole.ROLE_USER).build();
userRepository.save(user);
assertThat(userRepository.findAll().size()).isEqualTo(1);
}
}
테스트에 @Transactional 을 붙이면, 테스트의 DB 커밋을 롤백해주는 기능이 있다.
방법
DB와 연동되는 테스트를 할 때의 문제는 테스트에서 DB에 쓰기 작업을 하면 DB의 데이터가 바뀌는 것
→ @Transactional 는 테스트를 진행하는 동안에 조작한 데이터를 모두 롤백하고 테스트 진행하기 전의 상태로 되돌려준다.
→ 커밋을 하지 않으므로, 테스트 성공/실패와 무관
→ 예외가 발생해도 문제 발생X (강제로 롤백시키도록 설정되어 있기 때문)
테스트 메소드 안에서 진행되는 작업을 하나의 트랜잭션으로 묶고는 싶지만, 강제 롤백을 원하지 않는 경우
테스트의 작업을 그대로 DB에 반영하고 싶은 경우 + 메소드에만 적용 가능 : @Rollback(false)
→ 롤백을 원하는 메소드에는 @Rollback(true)
테스트의 작업을 그대로 DB에 반영하고 싶은 경우 + 클래스 레벨에 부여 : @TransactionConfiguration(defaultRollback=false)
단, auto_increment 나 sequence 등...에 의해 증가된 값은 롤백 X
→ 따라서, 테스트를 위해서는 별도의 DB로 연결 or 휘발성(인메모리) DB(H2 같은) 사용을 추천
비효율적이다.
@Transactional
public List<Folder> addFolders(List<String> folderNames, User user) {
// ...
}
비즈니스 로직에 트랜잭션 코드가 포함되므로, 트랜잭션 매니저를 일일이 붙이지 않아도 된다.
참고: 트랜잭션(Transaction)이란?
참고: [DB기초] 트랜잭션이란 무엇인가?
참고: [SPRING]@Transactional Annotation 알고 쓰자
참고: MySQL의 Transaction Isolation Levels
참고: [Spring] Spring에서 트랜잭션의 사용법 - (3/3)
참고: [JPA] 영속성 컨텍스트 (Persistence context)