본문 바로가기

Spring Boot

트랜잭션의 개념과 @Transacional 어노테이션

📌 트랜잭션

트랜잭션이란,
데이터베이스의 상태를 변화시키기 위해 수행하는 작업 단위
트랜젝션을 기반으로 작업을 하면
불완전한 데이터에 의해 DB가 더렵혀지지 않는다는 것을 보장할 수 있음

📌 트랜젝션의 네가지 속성 : ACID

원자성(Atimicity) - 완전히 되거나, 하나도 되지 않거나 / all or nothing
일관성(Cinsistency) - DB의 상태는 항상 일관적이여야 함 / 트랜젝션이 완료되었을 때 DB는 여전히 제약을 만족해야 함
독립성(Isolation) - 모든 트랜잭션은 다른 트랜젝션으로부터 독립되어야 함 (e.g. 서로 간섭할 수 없음)
지속성(Durability) - 트랜젝션이 성공적으로 끝나면, 그 결과가 반영, 저장되어야 함

📌 트랜젝션의 결과

트랜젝션 연산의 결과는 두 종류 뿐이다. => 커밋 or 롤백
즉, 결과를 DB에 완전 반영을 하던지 롤백으로 결과를 아예 없던 셈 치던지 한다.
이때 롤백이란 1,2,3번 코드는 돌아갔는데 4번 코드에서 문제가 발생했을 때
1,2,3까지 모두 무효로 만들어 버리는게 롤백이다.

 📌여러 트랜잭션이 경쟁하면 생기는 문제

cf. 문제들의 정확한 정의보다 어떻게 방지할 수 있는지가 더 중요하므로
이해만 하고 넘어가고, '정확한 정의'에는 목메지 말자.

1. Dirty Read : 존재하지 않는 값을 읽어오는 것
트랜잭션 A가 값을 1->2로 바꾼 상태에서 B가 그것을 읽었다.
그런데 그 후 A에서 문제가 발생하여 바뀐 값이 저장되지 않고 롤백되었다.
그럼 B는 존재하지 않는 2라는 값을 읽은 것이므로 문제가 된다.

2. Non-Repeatable Read : 값을 조회하는 트랜잭션의 결과과 일관적이지 못한 것
트랜잭션 A가 테이블의 어떤 값을 두번 조회하는 중이었는데, 중간에 B가 끼어들어 값을 수정했다.
이때 A의 결과가 B 전후로 달라지게 되므로, 트랜잭션의 일관성이 깨지는 문제가 발생한다.

3. Phantom Read : 일정 범위의 레코드를 조회하는 트랜잭션의 결과가 일관적이지 못한 것
2와 동일하게 트랜잭션의 일관성이 깨지는 문제가 발생한다.


📌 스프링에서의 트랜잭션 : @Transacional

스프링에서 트랜잭션을 지원하는 방법에는 여러가지가 있는데, 가장 간단한 방법이 @Transacional 을 이용하는 것이다.
@Transacional은 클래스나 메서드 위에 추가할 수 있는 어노테이션이다.
클래스, 메소드 위에 @Transactional이 추가되면, 이 클래스에 트랜잭션 기능이 적용된다. 
이 객체는 @Transactional이 포함된 메소드가 호출 될 경우
구현된 PlatformTransactionManager를 사용하여 트랜잭션을 시작하고, 
정상 여부에 따라 결과를 Commit 또는 Rollback 한다.

💡@Transactional 사용 TIP
▷ 메소드들이 모여있는 클래스에 @Transational를 달면, 내부 모든 메소드에 @Transational이 적용된다.

▷ 일반적으로 DB에 접근이 많은 Service 클래스에 @Transational를 달아준다.
▷ test 코드에서 쓰는 @Transactional은 'test로 인해 발생한 변동된 사항은 무조건 롤백하라'는 의미로 쓰인다.

📌 스프링 트랜잭션 세부 설정

트랜잭션의 충돌을 원천 방지하기 위해 모든 작업을 직렬로 수행하면
일관성이 깨지는 문제는 없겠지만 성능이 매우 느려진다는 단점이 생긴다.
이를 조절하기 위해서 트랜잭션에서는 여러 옵션을 제공한다.

1. Isolation(격리수준)
2. Propagation(전파수준)
3. ReadOnly 속성
4. 트랜잭션 롤백 예외
5. timeout 속성

📌 Isolation(격리수준)

여러 트랜잭션이 동시에 처리될 때,
다른 트랜잭션에서 해당 트랜잭션의 데이터를 변경 or 조회할 수 있는지를 결정하는 속성

- DEFAULT : 디비의 기본 isolation을 따르겠다.
- READ_UNCOMMITED : dirty read 발생 (사실상 격리를 하지 않는 것)
- READ_COMMITED : dirty read 방지 (커밋이 된 데이터만 읽기 허용)
- REPEATABLE_READ : non-repeatable read 까지 방지 (사용하는 영역에 락이 걸림)
- SERIALIZABLE : phantom read 까지 방지 (사용하는 데이터 자체에 락이 걸림)

이때 성능과 격리수준은 트레이드 오프 관계에 있다.
READ_UNCOMMITTED > READ_COMMITTED > REPEATABLE_READ > SERIALIZABLE 순서로
성능은 떨어지고 격리성(고립성)은 증가한다.
=> 대부분 디폴트로 하고, 아주 중요한 부분만 isolation을 높여 설정함
e.g. @Transational(isolation=DEFAULT)

📌 Propagation(전파수준)

하나의 트랜잭션에서 다른 트랜잭션을 호출하는 상황에서
새로운 트랜잭션을 시작할지, 기존 트랜잭션에 참여할지를 결정하는 속성

- REQUIRED : 디폴트 / 콜러 안에서 콜리까지 같이 실행하되, 콜리만 단독으로 실행하는 경우 단톡 트랜잭션으로 여김
- SUPPORTS : 이미 시작된 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 실행
- REQUIRES_NEW : 콜러 안에서 콜리가 실행될 때, 콜리만의 새로운 트랜잭션을 만들어줌
- NESTED : 이미 진행중인 트랜잭션이 있는 경우, 그 안에 중첩 트랜잭션을 만듦
  이때 내부 트랜잭션은 외부 트랜잭션의 영향을 받지만, 외부 트랜잭션은 내부의 영향을 받지 않음
e.g. @Transactional(propagation = Propagation.NESTED)

cf. NESTED를 유용하게 쓸 수 있는 상황 :
데이터 저장이 실패한다면, 로그 작성까지 롤백되어야 함
하지만 로그 작성이 실패했다고 해서 데이터 저장까지 롤백되면 안됨

📌 읽기 전용 속성 - readOnly

트랜잭션을 읽기 전용으로 지정하기 위한 속성
ReadOnly를 설정해두면 read끼리는 동시에 실행되어도 상관 없으므로 성능이 향상됨
그리고 조회만 하려는데 내부에 수정이나 삭제가 있는 경우 에러가 뜨므로
조회 목적이면 readOnly=true를 안할 이유가 없음!
e.g. @Transational(readOnly=true)

📌 트랜잭션 롤백 예외 - noRollbackFor

예외가 발생했을 때 롤백을 할지 말지 설정하는 속성
'중요하지 않은 예외가 발생했을 때는 롤백을 하지 않아도 된다'라는 목적으로 사용됨
e.g. @Transational(noRollbackFor=xxException.class)

📌 타임아웃 속성 - timeout

일정 시간 안에 트랜잭션을 끝내지 못하면 롤백하는 속성
DB에 문제가 있어서 오랫동안 트랜잭션이 끝나지 않는 경우, 이 속성을 사용해서 종료할 수 있음
e.g. @Transational(timeout=10)

📌 트랜잭션을 코드에 적용하기

💡 알아두면 좋은 것
▷ @Transational 어노테이션과 함께 @EnableTransactionManagement 어노테이션을 붙여줘야 

    내부 트랜잭션들이 정상적으로 동작한다. 
▷ 메소드들이 모여있는 클래스에 @Transational를 달면, 내부 모든 메소드에 @Transational이 적용된다.
▷ @Transational이 붙은 클래스 안에서
    메소드에 클래스와는 다른 @Transational 옵션을 달아주면, 메소드의 어노테이션이 우선시된다.
▷ 일반적으로, DB에 접근이 많은 Service 클래스에 @Transational를 달아준다.
▷ @Transational을 사용할 때 javax가 아니라 springframework의 Transactional을 임포트 해야
    더 많은 속성을 설정할 수 있다. (javax는 readOnly밖에 안됨)

@Service
@Transactional // 서비스 클래스에는 @Transactional를 달아주는게 좋음
public class DiaryService {
    private final DiaryRepository diaryRepository;
    
    @Transactional(readOnly = true) // 조회 목적이면 readOnly를 쓰는게 무조건 이득
    public List<Diary> readDiary(LocalDate date){
        return diaryRepository.findAllByDate(date);
    }

    @Transactional(readOnly = true)
    public List<Diary> readDiaries(LocalDate startDate, LocalDate endDate){
        return diaryRepository.findAllByDateBetween(startDate, endDate);
    }
    
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void updateDiary(LocalDate date, String text){
        Diary nowdiary = diaryRepository.getFirstByDate(date);
        nowdiary.setText(text);
        diaryRepository.save(nowdiary);
    }
}