JPA에서 Transaction 활용하기

TransactionManager

  • Transaction은 계속 나왔다. @Transactional를 사용할때나, 영혹성 컨택스트에서 Transaction이 끝날때 merge가 된다고 할때도... 그때마다 지나간 이론적인 내용을 살펴보겠다.

Transaction ?

  • DB에서 명령어들의 논리적 묶음
  • 자바에서 메서드를 통해 물리적인 기능들을 묶는거 처럼 DB에서는 Transaction의 단위로 물리적인 기능을 묶는다.
  • ex) 물건 구매 행위에서 결제와 주문이 한 트랜젝션으로 묶여야한다. 결제는 성공했는데 주문이 안들어가면 돈만 빠져나가는 경우가 생길수 있기 때문에, 주문이 실패하면 rollback이 되어야 한다.
  • All OR Nothing, 모 아니면 도
  • 특성
    - A 원자성
    - 부분적인 성공은 허용하지 않음
    - 송금을 실패하면 출금도 실패해야함.
    • C 일관성
      • 내가 송금을 하려면 돈이 있어야한다.
        • Data 간의 정확성을 맞춰야한다.
    • I 독립성
      • 트랜젝션내 기능은 다른 트랜젝션의 영향을 끼치면 안된다.
        - D 지속성
      • 데이터는 영구적으로 가지고 있어야한다.

Transaction 실습해보자.

@Service
@RequiredArgsConstructor
public class BookService {
    private final BookRepository bookRepository;
    private final AuthorRepository authorRepository;

    public void putBookAndAuthor() {
        Book book = new Book();
        book.setName("JPA 시작하기");

        bookRepository.save(book);

        Author author = new Author();
        author.setName("martin");

        authorRepository.save(author);
    }
}
@SpringBootTest
public class BookServiceTest {
    @Autowired
    private BookService bookService;
    @Autowired
    private BookRepository bookRepository;
    @Autowired
    private AuthorRepository authorRepository;
    @Test
    void transactionTest() {
        bookService.putBookAndAuthor();
        System.out.println("books : " + bookRepository.findAll());
        System.out.println("authors : " + authorRepository.findAll());
    }
}

  • debug mode로 확인하면 중간중간에 query를 확인할수 있다.

  • 이제 Transactional 걸어주자
    @Transactional
    public void putBookAndAuthor() {

  • putBookAndAuthor() 메서드가 끝나기 전까지, 즉 트렌젝션이 끝나기 전까지 쿼리를 때려도 Book Table과 Author Table에는 아무 정보가 들어오지 않는걸 확인 할 수 있다.
  • 참고로 Transactional 없으면 save쿼리에 있는 Transactional 때문에 각각의 데이터가 DB 전달된다.

Transaction 끝에 오류가 나면?


		...
        ...
		...
        throw new RuntimeException("오류나서 DB commit 발생안함");
    @Test
    void transactionTest() {
        // 이런 코드는 지양하지만 학습을위한 Test기 때문에 이렇게 한다.
        try{
            bookService.putBookAndAuthor();
        }catch (RuntimeException e){
            System.out.println(">>>> " + e.getMessage());
        }
        System.out.println("books : " + bookRepository.findAll());
        System.out.println("authors : " + authorRepository.findAll());
    }
  • All or Nothing 때문에 오류가 발생하면 save는 되지만 commit이 아닌 rollback이 때문에 DB에 저장되지 않는다.
  • A 원자성

😡Transaction 잘못된 사용.

  • RuntimeException(uncheckedException)이 Transaction 내에서 발생하면 Rollback 되지만 Exception(checkedException)은 Transcation 내에 있어도 그냥 Commit 되버린다.!!
  • checkedException 반드시 개발자가 책임지고 Exception을 처리해야한다.

만약 그냥 checkException이라도 Rollback시키려면 아래 어노테이션을 적용하면된다.
@Transactional(rollbackFor = Exception.class)

  • 같은 클래스의 Method호출 할때 Transaction 사용.
    public void put(){
        this.putBookAndAuthor();
    }
//    @Transactional(rollbackFor = Exception.class)
    @Transactional
    void putBookAndAuthor() {
        Book book = new Book();
        book.setName("JPA 시작하기");

        bookRepository.save(book);

        Author author = new Author();
        author.setName("martin");

        authorRepository.save(author);

        throw new RuntimeException("오류나서 DB commit 발생안함"); // unchecked Exception
    }
  • 위와같이 put Methed가 putBookAndAuthor()를 호출하면 Transactional이 무시된다.

isolation() : 격리()

  • org.springframework.transaction.annotation.Transactional 에 있다.
  • DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE 모두 5개의 격리 수준을 지원하고 있다.

DEFAULT : 특별히 설정하지 않으면 Mysql의 경우에는 REPEATABLE_READ 수준으로 설정된다.

Test 실행하고 JPA에서 Book을 Insert한 상태

Hibernate: 
    insert 
    into
        book
        (created_at, updated_at, author_id, category, name, publisher_id) 
    values
        (?, ?, ?, ?, ?, ?)
-- mYsql에서 다른 트랜젝션을 실행 시켜서 독립성을 위반시켜보자.
start transaction ;
update book set category="none";
commit;
// Debug를 한번더 실행 시켜보면 JPA에서 실행한 트랜젝션에 영향을 끼치이 않을걸 볼 수 있다.
>>findAll>> [Book(super=BaseEntity(createdAt=2022-02-13T17:24:28.699, updatedAt=2022-02-13T17:24:28.699), id=1, name=JPA 강의, category=null, authorId=null)]

READ_UNCOMMITTED

  • commit되지 않은 Data들을 읽을 수 있다.
  • durtyRead 라고 한다.
    @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    public void get(Long id){
        System.out.println(">>findById>> " + bookRepository.findById(id));
        System.out.println(">>findAll>> " + bookRepository.findAll());

        System.out.println(">>findById>> " + bookRepository.findById(id));
        System.out.println(">>findAll>> " + bookRepository.findAll());
        Book book = bookRepository.findById(id).get();
        book.setName("바뀔까?");
        bookRepository.save(book);
    }
    
start transaction ;
update book set category="none" where id=1;
rollback ;
  • 중요한건 독립성을 침범한 DB transction이 commit을 하던 rollback을 하던 결국 name=바뀔까? category="none"으로 update 된다.
// 이유는 JPA 쿼리때문이다.
Hibernate: 
    update
        book 
    set
        created_at=?,
        updated_at=?,
        author_id=?,  
        category=?, // 여기는 이미 category = "none" 으로 변경되어 있다.
        name=?,
        publisher_id=? 
    where
        id=?
  • 이러한 문제가 생기면 아래와 같이 해결하면 된다.
@DynamicUpdate
public class Book extends BaseEntity {
    update
        book 
    set
        updated_at=?,
        name=? 
    where
        id=?
  • 이렇게 필요한 쿼리(name)만 update하게 된다.
  • 여기 중요한거는 독립성을 해쳤다는 거고 정확성이 낮아졌다는 거다. 일반적으로 사용하지 않는다.

READ_COMMITTED

    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void get(Long id){
        System.out.println(">>findById>> " + bookRepository.findById(id));
        System.out.println(">>findAll>> " + bookRepository.findAll());

        System.out.println(">>findById>> " + bookRepository.findById(id));
        System.out.println(">>findAll>> " + bookRepository.findAll());
        Book book = bookRepository.findById(id).get();
        book.setName("바뀔까?");
        bookRepository.save(book);
    }
    
start transaction ;
update book set category="none" where id=1;
commit ;
  • DB transction이 commit을 하면 name=바뀔까? category="none"으로 update 된다.
  • rollback 하면 Db에서 실행된 쿼리는 rollback된다.
  • 하지만 READ_COMMITTED 하더라도 영속성 컨텍스트의 cache때문에 원하는 순간에 원하는 값을 볼수 없다는 점도 있다.
  • 조작을 하지 않았는데 조회되는 값이 변경되는 현상(unrepeatable read)이라고 한다.

REPEATABLE_READ

  • unrepeatable read 현상을 대비해서 나온 수준이 READ_COMMITTED의 이다.
  • 연속적으로 Transaction을 돌리더라고 항상 같은 값을 출력한다.
  • tanscation이 실행 할 떄 스냅샷을 계속 리턴해준다.
  • 디폴트 수준이기는한대 그래도 문제는 있다.
  • phantom read 구현하기위해 아직 안 배운 커스텀쿼리를 쓰겠다.
public interface BookRepository extends JpaRepository<Book, Long> {
    @Modifying
    @Query(value = "update book set category='none'", nativeQuery = true)
    void update();
}
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void get(Long id) {
        System.out.println(">>findById>> " + bookRepository.findById(id));
        System.out.println(">>findAll>> " + bookRepository.findAll());

        System.out.println(">>findById>> " + bookRepository.findById(id));
        System.out.println(">>findAll>> " + bookRepository.findAll());
        bookRepository.update();
    }
  • TEST

  • 디버그를 실행 시키고 DB transaction 실행해서 id 2를 만들어주자.

start transaction ;
insert into book ('id','name') values (2,'jpa 강의 2');
  • 다음 브레이킹포인트로 가자.
>>findAll>> [Book(super=BaseEntity(createdAt=2022-02-13T18:11:28.489, updatedAt=2022-02-13T18:11:28.489), id=1, name=JPA 강의, category=null, authorId=null)]
Hibernate: 
    update
        book 
    set
        category='none'
  • insert했지만 아직은 Data가 ID : 1 밖에 없다. commit 해보자.
>>>> [Book(super=BaseEntity(createdAt=2022-02-13T18:20:07.023, updatedAt=2022-02-13T18:20:07.023), id=1, name=JPA 강의, category=none, authorId=null),
Book(super=BaseEntity(createdAt=null, updatedAt=null), id=2, name=jpa 강의 2, category=none, authorId=null)]
  • 최종 결과가 Book이 2개 잘 왔는데 두 데이터 모두 category=none으로 되어있다. 분명히 id 1이 있으때 update 했는데 id 2도 none으로 되었다.
  • 경우에 따라 데이터가 안보이는데 처리되는것이 phantom read 라고 한다. 그래서 나온 최고수준의 격리수준이 SERIALIZABLE 이다.

SERIALIZABLE

  • @Transactional(isolation = Isolation.SERIALIZABLE) 추가하고 똑같이 TEST 해보자.
  • 이렇게 하면 commit이 일어나지 않은 트랜젝션이 있으면 lock을 통해 waiting상태가 된다.
  • 그럼 위 테스트를 실행 시켰을때 update 쿼리를 실행시키 전에 waiting하여 commit을 기다리고 commit이되면 넘어가게 된다.
  • 정확성은 100%이지만 waiting이 길어져서 성능에 문제가 생길수 있다.

😎😎트랜잭션은 정확성은 중요하다. 원하는 값이 나왔다고 하더라도 언제 쿼리가 어떻게 실행됬는지 알고있어야하며, durty read, unrepeatable read, phantom read 등의 문제점을 고려해야한다는 거다.

좋은 웹페이지 즐겨찾기