@Transactional 사용과 영속성 컨텍스트(persistence context)

Doyeon·2023년 2월 7일
0
post-thumbnail

질문

스프링부트를 활용해서 메모장 백엔드 서버를 만들어보는 실습을 하다가 궁금한 점이 생겼다.

// 메모 생성하기
@Transactional
public Memo createMemo(MemoRequestDto requestDto) {
    Memo memo = new Memo(requestDto);
    memoRepository.save(memo); // repository에 저장한다.
    return memo;
}

// 메모 삭제하기
@Transactional
public Long deleteMemo(Long id) {
    memoRepository.deleteById(id); // repository에서 삭제한다.
    return id;
}

// 메모 수정하기
@Transactional
public Long update(Long id, MemoRequestDto requestDto) {
    Memo memo = memoRepository.findById(id).orElseThrow(
            () -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
    );
    memo.update(requestDto); // memo 객체의 값만 바꾸고 다시 repository에는 안 넣는다?!
    return memo.getId();
}

Service에서 Repository에 저장(save)을 하거나, 삭제(delete)를 할 때는 repository 객체의 함수(memoRepository.save, memoRepository.delete)를 사용해서 데이터를 직접 저장하고 삭제했다.

그런데 수정을 할 때는 repository에서 데이터를 가져와 Memo 객체에 넣고, 그 객체를 수정한 후 다시 repository에 저장하지 않고 update 메서드를 끝냈다.

repository에서 꺼내 와서 수정한 memo를 다시 repository에 넣지 않았는데도 update 메서드가 끝난 후, DB에는 수정된 값이 잘 들어와있다.

왜 수정이 정상적으로 잘 되었는지 찾아보다가 그 이유가 @Transactional 어노테이션 덕분이라는 것을 알게 되었다.

@Transactional 어노테이션이 무슨 역할을 하기에 memoRepository.save 함수를 호출하지 않아도 변경된 부분이 제대로 업데이트 되었는지, 지금부터 알아보자!


@Transactional

트랜잭션이란?

  • 데이터베이스 트랜잭션은 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위이다.
  • 데이터베이스의 상태를 변경시키기 위해 수행하는 작업 단위 또는 한꺼번에 모두 수행되어야 할 일련의 연산을 의미한다.

예를 들어, 티켓팅을 생각해보자.

분명히 비어 있는 좌석을 선택했는데, 결제를 하려고 하면 “이미 선택된 좌석입니다”라는 문구를 자주 보게 된다.

자리를 조회할 때는 빈 좌석이었지만, 누군가가 나와 같은 좌석을 선택하고 나보다 빠르게 먼저 결제까지 성공했다면, 나의 결제 시도는 실패하고 내가 선택한 자리는 내 것이 아니게 된다.

내가 선택했던 자리와 결제 정보들이 커밋되지 않고 롤백되는 것이다.

자리를 선택하고 결제까지 완료하는 작업 단위가 하나의 트랜잭션이라고 볼 수 있다.

트랜잭션이 걸려있지 않다면, 같은 자리를 여러 명이 결제하게 되는 문제가 발생할 수 있다.

@Transactional 어노테이션

  • @Transactional을 클래스나 메서드에 붙이면, 해당 범위가 트랜잭션이 되도록 보장해준다.
  • 적용된 범위에서는 프록시 객체가 생성되어 자동으로 커밋(commit) 또는 롤백(rollback)을 진행한다.
  • 예외 발생 시, rollback을 통해 DB에 결과가 반영되지 않는다.

처음으로 돌아와서, 이 어노테이션이 데이터를 수정하는 함수에 붙을 때 어떻게 DB에 저장되는지 알기 위해 살펴봐야 할 개념이 있다. 바로 영속성 컨텍스트이다.


영속성 컨텍스트(pesistent context)

영속성 컨텍스트란?

  • 엔티티를 저장하고 관리하는 저장소
  • 엔티티 매니저가 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
    • Spring Data JPA 에서 사용하는 save, findById 등의 안을 살펴보면, 엔티티 매니저가 영속성 컨텍스트에 저장하고 조회하는 내용이 구현되어 있다.
  • 영속성 컨텍스트에 저장된 상태를 영속 상태, 저장되었다가 분리된 상태를 준영속 상태라고 한다.

영속성 컨텍스트의 특징

  • 식별자 값
    • 영속성 컨텍스트는 엔티티를 식별자 값(@Id로 기본키(PK)와 매핑된 값)으로 구분한다.
    • 영속 상태가 되기 위해서는 반드시 식별자가 존재해야 한다.
  • 데이터베이스 저장
    • 플러시(flush) : 트랜잭션을 커밋하는 순간에 영속성 컨텍스트에 새로 저장된 엔티티가 데이터베이스에 반영된다.

영속성 컨텍스트의 기능

  • 1차 캐시
    • 영속성 컨텍스트는 내부에 1차 캐시를 갖고 있다.

    • em.persist(member) → member 엔티티가 영속성 컨텍스트 안에 있는 1차 캐시에 저장된다.

    • em.find() → 1차 캐시에 찾는 엔티티가 있으면 DB에 SQL을 수행하지 않아도, 1차 캐시를 통해 엔티티를 조회할 수 있다.

    • 만약 1차 캐시에 찾는 엔티티가 없으면, DB를 조회해서 엔티티를 생성하고 1차 캐시에 저장 후, 영속 상태의 엔티티를 반환한다.

  • 동일성 보장
    • 하나의 트랜잭션 안에 존재하는 Id가 같은 엔티티는 동일성이 보장된다. → 1차 캐시에 있는 같은 엔티티 인스턴스를 반환
    • 동일성 비교 : ==
    • 동등성 비교 : equals
  • 트랜잭션을 지원하는 쓰기 지연(transactional wirte-behind)
    • 트랜잭션을 커밋하기 직전까지 내부 쿼리 저장소에 SQL을 모아두고, 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스로 보낸다.
  • 변경 감지(dirty checking)
    • 1차 캐시에 들어있는 엔티티가 변경되면, 그 변경 내용을 감지해서 트랜잭션 커밋 시점에 변경된 내용을 데이터베이스에 반영한다.
    • 엔티티가 영속성 컨텍스트에 저장되면, 최초 상태를 저장해둔다. → 스냅샷
    • 플러시(flush) 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾아 DB에 반영한다.
    • 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다.
  • 지연 로딩(lazy loading)
    • 연관 관계 매핑이 되어 있는 엔티티를 조회할 때, 조회 시점에는 연관된 엔티티를 실제 객체 대신 프록시 객체로 로딩한다.
      • 프록시 객체 : 실제 엔티티 객체 대신 사용되는 객체. 실제 객체의 참조 값을 지닌다.
    • 연관 매핑된 엔티티의 해당 객체를 실제 사용할 때 비로소 영속성 컨텍스트를 통해 데이터를 불러온다. → 데이터가 진짜 필요할 때 쿼리를 보낸다.

정리와 답변

처음 질문에 답변을 찾기 위해 많은 내용을 훑어보았다.

돌아와서 처음 질문에 답을 하자면, 영속성 컨텍스트의 1차 캐시변경 감지(dirty checking) 기능 덕분에 repository에서 가져온 엔티티의 데이터를 수정하면, 트랜잭션이 종료될 때(해당 메서드가 끝날 때) 변경된 엔티티의 값이 그대로 데이터베이스에 반영된다.


[참고자료]
<자바 ORM 표준 JPA 프로그래밍>
https://kafcamus.tistory.com/30
https://coding-factory.tistory.com/226
https://ttl-blog.tistory.com/108

profile
🔥

0개의 댓글