Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Archives
Today
Total
관리 메뉴

사부작사부작

[Spring] 영속성 컨텍스트 이해하기 본문

스프링

[Spring] 영속성 컨텍스트 이해하기

민철킴 2022. 3. 27. 20:16

Intro

영속성 컨텍스트가 무엇인지, 왜 사용하는지, 어떤 방식으로 작동하는지에 중점을 두고 쓴 글 입니다.

이 글에 나오는 엔티티 생명 주기, 식별자 생성 전략 등 다른 개념들은 깊게 다루지 않습니다.
틀린 내용이 있을 수 있습니다. 피드백 해주신다면 너무 감사하겠습니다.


 

# 프로그래밍에서 Context 

먼저 컨텍스트를 살펴보자. 서블릿 컨텍스트, 영속성 컨텍스트 등 컨텍스트는 여러 곳에서 쓰인다. '문맥', '맥락' 이라는 사전적 뜻에 의해 위 단어들이 느낌은 오지만 쉽게 이해되지는 않았다.  찾아보다가 CTO 님께 질문을 드렸고, 답을 얻었다.

 

Task는 운영체제 상에서 실행되는 작업의 단위라고 한다면, Context는 해당 Task의 정보를 메모리 등에 저장하여 관리하기 위한 단위 정도라면 도움이 될지 모르겠네요.
-  CTO 님 답변

 

 

이를 쉽게 내 표현으로 받아들이면,  컨텍스트는 작업을 관리하는 환경이다. 

 

# 영속성 컨텍스트와 엔티티의 역할

책에 의하면 영속성 컨텍스트를 ‘엔티티를 영구 저장하는 환경’ (1) 이라 해석하고 있다. 내가 아는 엔티티는 엔티티 클래스를 통해 생성된 객체이다. 엔티티 클래스는 @Entity 어노테이션을 이용해 클래스를 데이터베이스(DB) 테이블과 매핑시킨다. 클래스에 @Table, @Column 등의 어노테이션을 사용해서 테이블과 칼럼의 이름 등을 정하고 @Id 를 통해 테이블 식별자를 지정한다. 이 밖에도 여러 어노테이션으로 테이블 명세를 작성한다.

 

다시 내 표현으로 해석해보면, 영속성 컨텍스트는 ‘엔티티 클래스를 통해 생성된 객체를 영구 저장하는 환경’ 이라 풀이할 수 있다. 그렇다면 이제 엔티티 클래스를 통해 생성된 엔티티 객체가 하는 역할과 이를 저장하는 환경에 대해 더 알아보자.

 

<예시를 위한 엔티티 클래스>

엔티티 객체는 최종적으로 테이블의 인스턴스를 구성한다. 그렇다고 생성된 모든 엔티티가 테이블 인스턴스(데이터)가 되는 것은 아니다. 엔티티 객체를 저장하는 환경에 보관하고 그 뒤 DB에 온전히 저장될 때, 테이블의 인스턴스가 된다. 여기서 저장하는 환경이 영속성 컨텍스트다. 즉, 엔티티가 최종적인 역할을 하기위해서는 영속성 컨텍스트에 보관되는 상태가 필요하다.

<엔티티 생명주기> (2)

 

# 영속 상태의 장점

영속성(Persistence)은 데이터의 지속성을 의미한다. 그렇기에 애플리케이션을 종료하고 다시 실행하더라도 이전에 저장한 데이터를 불러올 수 있는 기술(3) 이다. 다시 말하면, DB에 저장하고 불러오는 모든 데이터들이 영속성 컨텍스트를 거치게 된다. 그렇다면 어떤 장점이 있기에, JPA 는 DB 에 바로 저장하지 않고 영속성 컨텍스트(Persistence Context)를 두는 걸까?

 

기본적으로 영속 상태에 들어온 엔티티들은 식별자 값(@Id로 매핑한 값)으로 구분된다. 구분된 엔티티들은 영속성 컨텍스트 안에 존재하는 내부 캐시(1차 캐시라고 부름)에 저장되는데 식별자 값을 키로, 엔티티 객체를 값으로 저장한다. 이 식별자 값은 DB 테이블의 pk 와 매핑되어 있다.

 

캐시가 존재하기에 값을 찾을 때, 캐시를 먼저 조회한다. 그리고 캐시 안에 데이터가 있다면 DB 를 조회하지 않는다. 만약 캐시에 데이터가 없으면 DB 를 조회한 후, 엔티티를 생성해 캐시에 저장한다. DB 를 직접 조회하지 않아도 되기에 성능은 좋아진다. 이 과정에서 동일성도 보장하고 있다. 즉, 같은 식별자 값을 가지면 참조하는 주소도 같다.

 

또한 영속성 컨텍스트 안에 쓰기 지연 저장소를 두어 트랜잭션 범위 내의 쿼리를 한 번에 DB 에 보낸다. 지연 로딩 기능에도 영속성 컨텍스트가 이용된다. 지연 로딩 설정을 걸어둔 객체가 실제 사용될 때, 영속성 컨텍스트가 DB를 조회해 필요한 엔티티 객체를 생성한다. 영속 상태에 있지 않은 객체는 지연 로딩을 사용할 수 없다.

 

더티 체킹이라 부르는 변경 감지 기능도 제공한다. 영속 상태 안에 있는 엔티티 객체를 대상으로 호출 하는 시점과 트랜잭션이 끝나는 시점을 비교한다. 변경이 있다면, 이를 감지하고 update 쿼리를 DB 에 자동으로 보내준다.

<영속성 컨텍스트&nbsp; 예시 코드>

 

코드로 영속성 컨텍스트의 장점을 다시 살펴보겠다. 예시 코드에서 확인할 부분들은 아래와 같다.

1. findBy~ 메서드를 통해서 엔티티를 조회함

2. 빌더패턴을 통해 엔티티 객체를 생성하고, blogRepository는 상속받은 JpaRepository의 save() 메서드를 호출함

3. update~ 메서드를 통해 객체의 데이터를 업데이트 시킨 후, 어떠한 메서드도 호출하지 않음

4. 맨 위에 @Transactional 어노테이션이 걸려있음

5. 실행 순서를 파악하기 위한 프린트문

 

이제 번호들을 해석해보자.

1. 조회 메서드로 엔티티를 찾을 땐 먼저 영속성 컨텍스트(1차 캐시)에 존재하는지 확인한다. 영속성 컨텍스트에 존재하지 않는다면 DB에 접근해서 엔티티를 찾는다. 찾은 엔티티를 영속성 컨텍스트(1차 캐시)에 저장하고 엔티티를 반환한다.

 

2. 조회에 실패할 경우, 새로 Blog 객체를 생성하고 save 한다. save 메서드가 호출된 시점에 DB에 반영되는 것은 아니고, 영속성 컨텍스트 안에 존재하는 내부 쓰기 지연 저장소에 insert 쿼리를 보관해둔다. 

 

3. 조회된 객체들은 영속성 컨텍스트에 존재한다. 그렇기에 앞서 말한 변경감지가 이루어지기에 따로 save 메서드를 호출하지 않아도 자동으로 update 쿼리가 지연 저장소에 보관된다.

 

4. 예시에서 가장 상위 메서드 findOrCreateDeliveryCompany() 가 롤백되지 않고 성공적으로 끝난다면, 트랜잭션을 커밋하고 flush() 가 호출된다. 이때, 저장소에 모아둔 쿼리들이 DB 에 반영된다. 

 

이제 테스트 코드를 통해 동작을 확인해보겠다. 

 

<테스트 코드>
<쿼리 - 1>

기존에 존재하던 객체를 수정하는 테스트 코드를 실행시켰다. 날아가는 쿼리를 보면 select 조회 쿼리와  update  쿼리가 실행되는 것을 볼 수 있다. 또한 메서드가 종료되고, 즉 트랜잭션이 끝난 후에 update 쿼리가 날아가고 있다.

그럼 수정한 객체를 같은 내용으로 한번 더 수정하면 어떻게 될까? 똑같은 코드를 한 번 더 돌려봤다. 

<쿼리 - 2>

같은 값으로 수정을 하고 있기에, 데이터의 변경은 없다. 즉 변경 감지와 이를 통한 update 쿼리는 실행되지 않는다. 

 

# 작동 방식 이해하기

질문) 위에 해석한 2번 처럼 Insert 쿼리도 정말 트랜잭션이 커밋된 후에 DB 에 반영될까?

<쿼리 - 3>

 

새로운 객체를 생성할 수 있게 값을 넣어 테스트 코드를 돌려봤다. 해석과 다르게 Insert 쿼리는 save 메서드가 호출되니 바로 실행됐다. 왜 해석과 다른 결과가 나왔을까?

엔티티 클래스를 다시 봐 보자. 식별자인 id 에는 아래의 옵션이 걸려있다.

@GeneratedValue(strategy = GenerationType.IDENTITY)

이 옵션은 식별자를  auto increment(자동증가) 로 id 생성을 데이터베이스에 위임하는 것이다. 즉, 엔티티 객체가 DB 에 들어가야 pk가 부여된다. 위에 말했듯이, 영속성 컨텍스트는 식별자 값을 키, 엔티티 객체를 값으로 구분한다. 그렇기에, 방금 생성된 블로그 객체는 id가 존재하지 않기에, 아직 영속성 컨텍스트에 존재하지 않는다. 

이젠 설정해둔 식별자 생성 전략 옵션을 주석 처리하고 테스트 코드에 id를 직접 부여해서 돌려봤다.

 

<쿼리 - 4>

먼저 save() 메서드를 알아보자. JpaRepository의 save() 가 가장 먼저하는 일은 id를 조회하는 select 쿼리를 통해서 엔티티의 존재 여부를 파악한다. 그리고 엔티티가 존재하면 update 쿼리를, 존재하지 않으면 insert 쿼리를 보낸다. 앞서 <쿼리 - 3> 화면처럼 id 생성을 DB 에 위임했던 코드에서는 id 를 통해서 엔티티의 존재 여부를 조회할 수 없었다. 그렇기에 바로 insert 쿼리를 보냈다. 하지만 id 를 부여해 save 메서드를 실행하면 <쿼리 - 4> 화면처럼 먼저 id를 통해 DB를 조회한 후에, update 쿼리가 필요한지 insert 쿼리가 필요한지 판단한다. 

 

위 경우는 새로 블로그 객체를 생성하는 경우다. save 메서드는 insert 할 상황이면, persist() 메서드를 이용해 객체를 영속성 컨텍스트 안으로 넣는다. 그렇기에 쓰기 지연이 이루어지고 트랜잭션이 커밋된 후에 insert 쿼리가 실행된다.

 

마치며..

영속성 컨텍스트는 애플리케이션과 실제 DB 사이에서 가상의 DB 역할을 한다. 영속성 컨텍스트를 사용하면 캐시와 쓰기지연 등의 성능상 큰 이점을 챙길 수 있다. 또한 영속성 컨텍스트는 엔티티의 식별자를 키값으로 갖는다는 것을 주의하며 사용해야한다. 다음 글에는 DB 와 동기화를 시켜주는 flush() 에 대해 알아봐야겠다.

 


(1) 책 <자바 ORM 표준 JPA 프로그래밍> p.92
(2) 책 <자바 ORM 표준 JPA 프로그래밍> p.93

(3) 책 <자바 웹 개발 워크북> p.559