Dev/JPA

JPA에서 Transaction 활용하기

OK-가자 2022. 3. 2. 17:30

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 등의 문제점을 고려해야한다는 거다.

'Dev > JPA' 카테고리의 다른 글

JPA @Query  (0) 2022.03.02
JPA OrphanRemoval(+@Where)  (0) 2022.03.02
JPA Cascade  (0) 2022.03.02
JPA에서 Transaction의 전파  (0) 2022.03.02
영속성 컨텍스트(Entity LifeCycle)  (0) 2022.03.02