본문 바로가기
TIL

TIL - 24.01.23

by JHBang 2024. 1. 23.

이번 주 팀 프로젝트에서 프로젝트를 수행하던 중 아래와 같은 코드를 작성했다.

    fun getByIdOrNull(commentId: Long) = commentRepository.findByIdOrNull(commentId) ?: TODO("NoSuchEntityException()")

    @Transactional
    fun updateComment(postId: Long, commentId: Long, request: CommentRequest): CommentResponse{
        // TODO: 댓글을 작성한 사용자만 댓글 수정 가능해야 함
        val donate = donateRepository.findByMemberIdAndPostId(request.memberId, postId)
        val donationAmount = donate?.amount ?: 0
        
        
        val comment = getByIdOrNull(commentId)

        comment.update(request.content)
        
        return commentRepository.save(comment).toResponse(donationAmount)

    }

 

댓글을 업데이트하는 메소드였는데, 팀원중 한 분이 이 경우에는 .save()메소드를 사용할 필요가 없다고 하셨다.

 

왜 그런지 찾아보던 중 JPA를 이해하는데 필수적인 요소인 영속성 컨텍스트와 관련된 내용인 것을 알았다.

영속성 컨텍스트

영속성? 영속성이란 단어가 상당히 낮설다.

영속성이란 애플리케이션이 종료되도 데이터가 저장되는 상태를 말한다.

보통 데이터는 애플리케이션이 종료되는 순간 싹 다 날아간다. 예를들어 프로퍼티에 할당된 값이라던지, 함수의 결과값이라던지...

이런 값들을 날아가지 않게 해주는 방법은 결국 저장밖에 없다. 즉 DB에 저장하는것이다.

따라서 영속성 컨텍스트는 DB와 아주 밀접한 관계를 가진 개념인 것을 알 수 있다.

 

영속성 컨텍스트를 간단히 말하자면 애플리케이션과 DB 사이에서 객체를 임시 보관하는 가상의 DB라고 보면 된다.

(이 말은 영속성 컨텍스트는 실제 DB에 저장하는 형태가 아니라는 것이다.)

 

영속성 컨텍스트는 엔티티 매니저를 통해 생성되며 엔티티 매니저는 엔티티를 영속성 컨텍스트에 보관하고 관리한다.

또 모르는 개념이 나왔다. 엔티티 매니저?

 

엔티티 매니저(EntityManager)

영속성 컨텍스트에 접근해서 엔티티에 대한 DB작업을 제공해준다. 엔티티 매니저 팩토리라는 곳에서 각 요청에 대한 엔티티 매니저를 생성한다.  @PersistenceContext 어노테이션을 통해 직접 주입할 수도 있다.

엔티티 매니저는 em.persist(entity)라는 명령어로 영속성 컨텍스트에 엔티티를 저장한다.

 

영속성 컨텍스트의 기능

그렇다면 이 영속성 컨텍스트는 대체 왜 쓰고 어떤 기능이 .save() 메소드를 사용하지 않아도 되는 이유가 되는걸까?

차례차례 살펴보자.

 

1. 1차 캐시

영속성 컨텍스트는 DB와 애플리케이션의 사이에 존재한다고 위에서 언급했었다. 이러한 구조를 통해 얻을 수 있는 이점 중 하나가 바로 캐싱 기능이다. 실제로 영속성 컨텍스트는 캐싱 기능을 제공하며, 이를 1차 캐시라고 부른다.

영속성 컨텍스트는 내부에 엔티티를 map 형태로 임시저장해 놓고 동일한 엔티티 조회 시 데이터베이스를 직접 조회하지 않고 해당 엔티티를 사용할 수 있게 해준다.

key 값에는 엔티티의 PK가 저장되고 value값에는 객체가 저장된다.이 후 해당 엔티티를 조회하려는 시도가 발생하면 DB로 쿼리문을 보내기 전 em.find(PK) 명령어로 1차 캐시에 값이 있는지 확인하고 값이 있을 경우 해당 value를 반환한다.

값이 없으면 DB에서 값을 조회한 후 1차 캐시에 저장 후 반환한다.

 

2. 지연 로딩(Lazy Loading) 및 즉시 로딩(Eager Loading)

지연 로딩이란 연관관계가 있는 엔티티를 조회할 때 연관된 엔티티가 실제로 필요한 시점에서 쿼리문을 보내는 기능이다.

// 1.
val post = em.find(Post::class.java, postId)
// 2.
val author = post.member
// 3.
val authorName = member.name

1번에서 Post를 조회한다. 이 후 2번에서 Post와 연관된 member를 조회한다. 지연로딩의 경우 이 부분에서는 member를 찾는 쿼리문을 보내지 않는다. 3번에서 드디어 멤버에 접근할 일이 생기면 이 때 쿼리문을 보낸다.

만약 즉시 로딩일 경우 2번에서 바로 쿼리문을 보낸다.

 

3. 트랜잭션 쓰기지연(transactional write-behind) 

영속성 컨텍스트에 변경이 발생했을 때 바로 데이터베이스에 쿼리문을 보내지 않고 쿼리를 버퍼에 모아놓는다.

이 버퍼는 쓰기 지연 SQL 저장소라고 부른다.

이 후 영속성 컨텍스트가 flush하는 시점에 모아둔 쿼리를 데이터베이스에 보낸다.

 

flush?

flush는 영속성 컨텍스트에 저장되어 있는 쓰기 지연 SQL 저장소에 저장되어 있는 query들이 database에 반영되는 과정이다. flush는 엔티티 매니저를 통해 직접 호출할 수도 있고, 트랜잭션이 끝나는 commit이 실행되면 이때 내부적으로 호출된다.

 

4. 더티체킹(Dirty Checking)

이 더티 체킹이 .save() 메서드를 사용하지 않아도 되는 이유이다.

더티는 더럽다는 뜻이 아니라 상태의 변화를 의미한다고 한다. 즉 엔티티의 상태 변화를 확인하는 것이라고 할 수 있다.

 

더티 체킹을 간단히 설명하자면 jpa에서 save메소드 없이 자체적으로 값을 비교하고 동기화하는 기능이다.

 

해당 과정을 들여다보자.

영속성 컨텍스트는 엔티티를 1차 캐시에 저장할 때 원본과 스냅샷으로 나누어 저장한다. 스냅샷은 DB에서 최초로 영속성 컨텍스트에 들어온 상태를 저장해 놓은 곳이다. 

트랜잭션을 끝내기 위해 commit을 하면 엔티티와 스냅샷을 비교하는 작업을 진행한다. 변경된 사항은 쓰기 지연 SQL 저장소에 update 쿼리문으로 저장된다.

이 후 commit이 되기 전 flush가 호출되어 변경사항이 update쿼리로 발송된다.

이후 commit이 실행되고 나면 변경사항이 DB에 적용된 것이다.

 

이 일련의 과정을 통해 더티 체킹이 이루어진다.

 

참고로 1차 캐시는 flush를 한다고 사라지지는 않는다. 만약 그럼 캐시 역할을 못하지.

 

 

 

'TIL' 카테고리의 다른 글

TIL - 24.01.18  (1) 2024.01.18
TIL - 24.01.17  (0) 2024.01.17
TIL - 24.01.15 (팀 프로젝트 회고)  (1) 2024.01.15
TIL - 24.01.11  (0) 2024.01.11
TIL - 24.01.09  (0) 2024.01.09