[F-Lab 모각코 챌린지 38일차] @Transactional

부추·2023년 7월 8일
0

F-Lab 모각코 챌린지

목록 보기
38/66

TIL

  1. 영속성 컨텍스트란..?
  2. 엔티티 생명주기
  3. @Transactional..?



1. 영속성 컨텍스트란..?

웹 프로그램은 DB와 접근할 일이 많다. 사실 웹 어플리케이션 서버는 DB와의 상호작용으로 이뤄진다고 봐도 무방하다. 그렇기 때문에 DB 쓰기와 읽기 작업시 데이터 정합성을 보장하고, 예상치 못한 문제가 발생했을 때 이를 잘 처리하는 것이 굉!장!히! 중요하다.

JPA의 구현체인 Hibernate는 이를 위해 "영속성 컨텍스트"라는 영역을 제공한다. 영속성(persist) 이 무엇이냐? persist를 직역하면 "지속적인"이다. 보통 소프트웨어에서 "지속적인" 것들이라고 함은, 파일이나 DB에 저장되어있는 데이터들이다. (재부팅과 함꼐 사라지는 메인 메모리의 프로그램들은 지속적이라고 네이밍하지 않는다) 따라서 영속성 컨텍스트란, "컴퓨터 프로그램 수행과 무관하게 영속적으로 존재하는 DB 데이터들에 접근하고 관리하기 위한 영역" 정도로 이해하면 될 것 같다.
JPA는 이러한 영속성 컨텍스트 안에서 사용자 프로그램에 안전한 트랜잭션을 제공한다.

안전한 트랜잭션이란 무엇일까?? 그것은 ACID를 보장하는 트랜잭션이다. ACID란 무엇인가? 영속성 컨텍스트와 관련하여, 안전한 트랜잭션 환경을 제공하기 위해 제공해야하는 속성이다.

1) Atomicity(원자성) : OR or Not

하나의 트랜잭션 안에서 일어나는 DB 관련 작업들은 모두 이뤄지거나, 모두 이뤄지지 않아야 한다는 특성이다.

A의 계좌에서 B의 계좌로 돈이 이동하기 위해선 A 계좌 출금 -> B 계좌 입금 의 과정을 거쳐아한다. 그런데 만약 A 계좌에서 출금이 일어난 직후 네트워크 문제로 B 계좌에 입금 명령이 제대로 이뤄지지 않았다면 A만 돈을 잃고 상황은 끝나게 된다. 이런 일은 일어나선 안된다. A계좌 출금, B계좌 입금의 과정이 모두 이뤄지거나, 두 과정 모두 일어나지 않아야 한다. 이것이 Atomicity, 즉 원자성이다.

조금 유식한 말로 트랜잭션이 분리할 수 없는 단일 작업 단위로 취급된다고 한다. 방금 들었던 예시에서는 출금과 입금이 분리될 수 없는 작업으로서 존재한다고 말할 수 있다. 트랜잭션의 일부가 실패하면 해당 트랜잭션 내의 모든 변경 사항이 롤백되어 데이터베이스의 원래 상태로 돌아가게 된다. 앞선 예시에서, 출금이 일어난 뒤 DB 관련해 문제가 생긴다면 A 계좌에서 출금을 한 내역이 롤백된다.


2) Consistency(일관성) : 유효한 데이터

트랜잭션 전후에 데이터베이스가 유효한 상태로 유지되어야 한다. 이것은 DB 데이터 자체의 무결성 문제다. 숫자 형태로 돈이 저장되는 DB에 갑자기 문자열이 저장된다거나, 허용된 범위 이외의 데이터가 들어가선 안된다.


3) Isolation(격리성) : 트랜잭션 간 간섭 X

동시 트랜잭션이 서로 간섭하지 않아야 한다는 특징이다. A와 B가 C의 계좌에 만원씩 입금하는 상황을 가정해보자. 두 트랜잭션이 서로 간섭하여 동시성 문제가 일어난다면, 두 사람이 입금을 완료한 후 2만원이 아닌 1만원이 C의 계좌에 찍혀있을 수 있다. 영속성 컨텍스트가 제공하는 트랜잭션은 DB 동시 접근을 관리하고 적절한 락을 제공함으로써 이런 일을 막는다.

조금 더 유식한 말로 '각 트랜잭션은 다른 동시 트랜잭션의 영향을 받지 않고 실행 중인 유일한 트랜잭션인 것처럼 실행되어야 한다'라고 설명할 수도 있겠다.


4) Durability(내구성) : DB는 온전해야.

트랜잭션이 한 번 커밋되면 시스템 오류 또는 충돌이 발생하는 경우에도 DB 변경사항이 지속되어야 한다. 앞서 "영속성 컨텍스트"가 왜 그런 이름을 갖게되었는지 상기하자. 은행 프로그램에 문제가 생겨도 사용자 계좌의 잔액 데이터는 그대로 남아야한다는 것과 관련이 있는 특성이다.




2. 엔티티 생명주기

엔티티(Entity)는 간단히 말하면 식별자를 가진 객체를 말한다. JPA에서는 DB에 저장되기도 하고, 조회되기도 하는 영속성 관리의 대상이 된다. 이러한 엔티티에는 "생명주기"라는 것이 있다. 엔티티가 생성되고, 영속성 컨텍스트에 의해 관리되고, 삭제되거나 저장되는.. 뭐 그런 상태 변화라고 이해하면 된다.

1) 비영속 (transient,new)

새로 생성된 엔티티, 혹은 프로그램 수행중 실제 DB와는 관련없이 존재하는 엔티티 객체들의 상태이다.
뭐 DB랑 연관된게 없으니 영속성 컨텍스트에서 관리하고자시고 할 것도 없다.

2) 영속 (managed)

엔티티가 영속성 컨텍스트에서 관리되고 있는 상태이다.
헷갈리면 안되는게, "DB에 저장된 상태"가 아니라 "관리되고 있는 상태"이다. 영속성 컨텍스트는 위 그림과 같이 '캐시 저장소'와 '쿼리문 저장소'로 나뉜다. 캐시 저장소에는 managed 상태의 엔티티가, 쿼리문 저장소에는 해당 트랜잭션 단위에서 한꺼번에 수행되는 쿼리문들이 나열되어있다.

이전의 transient 상태의 entity는 아예 영속성 컨텍스트 바깥에 존재했지만, 영속 상태의 객체는 캐시 저장소에 존재한다. 이는 트랜잭션 과정이 끝나고 쿼리문이 한꺼번에 flush() 될 때 캐시 저장소에 존재하는 엔티티 객체가 사용될 것을 뜻한다.

위 예시 그림은 아마.. repository.save(entity1)을 한 부분일 것이다. 그러면 INSERT ... 쿼리가 쿼리문 저장소에 추가되고, entity1은 영속성 컨텍스트 하에서 관리되는 엔티티 객체가 된다.
트랜잭션 과정동안 여러 엔티티 객체들이 persist 상태가 되고, 쿼리문 저장소에도 여러 개의 쿼리문이 추가된다. 해당 쿼리문들은 ACID의 'A'를 지키기 위해, 트랜잭션 메소드가 끝나고 한꺼번에 flush()를 통해 실제 DB서버로 쿼리를 날린다.

추가로, save()한 객체 외에 find()를 통해 조회된 객체 역시 persist 상태이다. 여기선 주로 Dirty Checking에 쓰이는데, 이건 좀이따..

3) 준영속 (detached)

영속성 컨텍스트에서 관리되던 엔티티가 더이상 관리되지 않는 상태이다. 준영속 상태의 엔티티는 merge()를 통해 다시 persist 상태로 변화시킬 수 있다는데.. 이걸 굳이 왜 사용하는지는 모르겠다.

4) 삭제 (removed)

준영속 상태와 마찬가지로, 엔티티를 영속성 컨텍스트에서 관리하지 않고 해당 엔티티를 DB에서 삭제하는 DELETE 쿼리를 보관한다.
알아둬야 할 점은, DELETE .. 쿼리 역시 트랜잭션이 끝날 때 flush()가 호출되는 순간 한꺼번에 일어난다는 점이다.




3. @Transactional

앞서 설명한 영속성 컨텍스트 하에서 원자 단위의 DB 작업을 수행하는 메소드에 붙이는 어노테이션이다. ACID를 보장하고, 영속성 컨텍스트가 프로그램의 DB 관리에 제공하는 대부분의 기능을 제공한다. 그 중에서도 가장 중요한 dirty checking에 관해 간단히 알아보자 !

# Dirty Checking?

이전 단축URL 프로젝트를 수행하면서, URL 엔티티 객체의 특정 필드값을 +1 해야하는 상황이 있었다. 서비스에서 다음과 같은 로직을 수행했다.

@Transactional
public String getOriginalUrl(String shortenUrl) {
    ShortenUrl found = urlRepository.findByShortenUrl(shortenUrl)
            .orElseThrow(UrlNotFoundException::new);
    found.addRequestedNumber(); // 필드값 +1
    return found.getOriginalUrl();
}

found는 영속성 컨텍스트 하에서 관리되는 persist 상태의 객체이다. 근데,found의 필드값을 수정한 후 update()save()를 하는 부분이 보이지 않는다. 안해도..괜찮나..?

괜찮다. @Transactional의 더티체킹 덕분에.

앞서 짧게 언급했는데, DB에서 조회가 된 객체 역시 영속성 컨텍스트 하에서 관리되는 persist 객체가 된다. 이 때, DB에서 막 조회된 데이터 값을 hibernate는 "스냅샷"으로 캐시 저장소에 저장해놓는다. 그리고 트랜잭션 과정이 끝나고, 현재 엔티티 객체와 객체의 초기 스냅샷을 비교하는 "더티 체킹" 과정을 수행하고 변경 사항을 자동으로 DB에 반영한다.


# readOnly = true ?

같은 프로젝트에서, 아래와 같은 코드가 있었다. 간단하게 repository에서 엔티티 객체를 읽어들이는 기능이었다.

@Transactional(readOnly = true)
public ShortenUrl getByShortenUrl(String shortenUrl) {
    return urlRepository.findByShortenUrl(shortenUrl)
            .orElseThrow(UrlNotFoundException::new);
}

readOnly = true를 하면,

  1. 스냅샷 저장과 더티체킹 과정이 생략되므로 성능상 이점이 존재한다.
  2. 이 메소드의 트랜잭션은 읽기 과정만 수행한다는 것을 읽는 사람들이 알 수 있도록 한다.
  3. readOnly 옵션을 이용해서 읽기 전용 DB로 라우팅하는 동작을 구성할 수도 있다.

사실 성능상의 커다란 이점이라기보다는, DB 접근 메소드 자체에 대해 일관된 코딩 패턴을 유지하고, DB와의 모든 상호 작용이 트랜잭션 접근 방식을 따르도록 하는데 의의가 있다고 느낀다.

여러 블로그 글들을 돌아보니 이것 역시 '읽기 전용임을 고지한다' 외적으로는 취향에 따라 갈리는듯..



REFERENCE

https://siyoon210.tistory.com/138
https://cupeanimus.tistory.com/102?category=868009

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글