자바 ORM 표준 JPA 프로그래밍 - 기본편 : 영속성 관리

jkky98·2024년 9월 25일
0

Spring

목록 보기
47/77

JPA 내부 동작에서 가장 중요한 두 가지 이해

  • 객체와 RDB Mapping
  • 영속성 컨텍스트

이번 포스팅에서는 두번째인 영속성 컨텍스트(Persistence Context)에 대해 자세히 알아볼 예정이다.

또 JPA의 가장 강력한 이점은 sql을 다루지 않으면서 참조와 같은 자바의 문법적 특징을 그대로 사용할 수 있다는 것이다.

영속성 컨텍스트(Persistence Context)

이름 감각잡기: Persistence

영속성 컨텍스트라는 개념을 파악해도 이것이 왜 "영원히 계속되는"이라는 뜻을 가지는지 이해하지 못했다. 오히려 잠깐 존재하는 메모리적인 특징을 가지지 않는가? 라고 생각했다. 애초에 근간이 되는 라이브러리가 Persistence이고 Persistence라는 단어는 다른 프로그래밍 언어의 ORM 프레임워크에서도 공통적으로 사용하던 용어이기 때문인 것으로 파악된다. 결론적으로 DB가 영속적인 특징을 가지고 ORM 기술에서 우리가 IDE 프로그램에서 작성한 코드들은 메모리(일시적 특성을 가짐)에서 존재하는데, 이 관계에 있어 Persistence는 DB의 영구적 특징에 무게를 두어 만들어진 단어로 보인다. 즉 쓰레드에서 일시적으로 메모리상에서 존재하는 객체에 영속성을 부여하겠다는 목적성 의미를 가진다고 보면 될 것 같다.

Persistence Context에서 Context는 공간적인 느낌 + 동적인 느낌 + 결합과 연결의 느낌을 가진다. 영속성을 부여받을 것들의 묶음이라고 생각하면 될 것 같다.

나만 그런지 모르겠지만 영속성 컨텍스트라는 말이 굉장히 낯설어 어원과 느낌을 뒤져보며 정리해보았다.

엔티티의 생명주기


엔티티는 크게 세 가지 생명주기(비영속,준영속,영속)상태를 가진다.

  1. 비영속(transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
  2. 영속(managed) : 영속성 컨텍스트에 관리되어지고 있는 상태
  3. 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태

영속성 컨텍스트 존재로 인한 강력한 장점

영속성 컨텍스트는 DB와 자바 사이의 중간 전초기지(캐시)같은 것이라고 생각하자. 이 전초기지는 자바로 구현된 것이기에 sql문법이 지배하는 DB와 다르게 자바 문법을 활용할 수 있다.

조회때마다 DB에 쿼리를 날리는 것이 아니고 만약 이전에 DB에서 이미 가져온 엔티티가 있다면 이를 전초기지(캐시)에 넣어놓고 사용할 수 있다.

List<Member> list1 = new ArrayList<>();
list1.add(member1);

list1.get(member1).setXXX(~)

위의 list와 관련된 코드를 보여준 이유는 JPA또한 DB를 활용하지만 DB에 들어있는 엔티티를 위 코드 처럼 코딩할 수 있다는 것이다. 참조형을 통해 리스트에서 조회한 것에 대해 곧바로 업데이트를 칠 수 있는 이러한 자바 문법적 특징을 DB에 Update를 치는 로직에도 그대로 적용할 수 있는 것이다. 이렇게 객체수정만으로 DB에 업데이트를 가능하게 하는 것을 JPA의 변경감지 특성이라고 한다.

그것이 아닌 sql 쿼리를 사용한다면, 자바 객체는 객체대로 수정해야하고 그 수정한 객체를 활용해서 sql문을 작성까지 해야한다.

엔티티 In 영속성 컨텍스트

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

JPA를 이용하기 위해 꼭 필요한 것이 EntityManager이다. 일단 이 EntityManager내에 영속성 컨텍스트라는 일시적 저장공간(1차 캐시)이 있다고 생각하자.

엔티티가 영속성 컨텍스트에 들어오기 위해서는 네 가지 방법이 존재한다.

  1. em.persist()
  2. em.find()
  3. em.merge() -> 나중에 설명
  4. JPQL 조회

persist()는 엔티티 객체를 "신규" 등록하는 것이다. 이때 DB나 컨텍스트에 이미 이 엔티티(동일 ID)가 존재해서는 안된다. find()의 경우는 DB나 컨텍스트에 존재하는 엔티티를 조회하는 것이다. merge()는 준영속 엔티티를 다시 컨텍스트에 올려놓는 것이다. JPQL 조회는 DB에 쿼리를 날려 조회를 시도하면서 컨텍스트에 해당 조회 엔티티를 올려놓는다.

결국 영속성 컨텍스트에 존재하는 엔티티들의 변화를 적용한다던지 신규 등록된 엔티티를 올린다던지의 과정을 최종적으로는 sql문으로 처리해야할 것이다.

이에 대해 persist()는 호출되는 곧바로 쓰기 지연 SQL 저장소에 Insert 쿼리문이 남게 된다. 반면 조회후 수정에 대해서 곧바로 적용하기보단 커밋시점에 최종적 결과로 Update를 하는 것이 필요하기에 객체 수정(변경 감지)의 경우에는 쿼리문이 나가지 않게 된다.

조회

EntityManager는 find() 조회시 1차 캐시에 해당하는 영속성 컨텍스트에서 먼저 엔티티를 찾는다 그리고 보이지 않는다면 DB까지 조회해서 엔티티를 가져온다. 1차 캐시에 없는 경우인 DB에서의 조회는 조회된 엔티티를 컨텍스트에 올려놓는다.

즉 캐시를 활용하는 것 처럼 접근성이 좋은 전초기지인 캐시를 먼저 조회하고 캐시에 없다면 DB까지 가는 것이다.

merge -> 쓰면 안될 기능

https://velog.io/@aal2525/%EC%8B%A4%EC%A0%84-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%99%80-JPA-%ED%99%9C%EC%9A%A91-%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC-%EA%B3%84%EC%B8%B5-%ED%85%9C%ED%94%8C%EB%A6%BF 에서 내용 확인.

JPQL

JPQL은 SQL 쿼리문을 쉽게, 엔티티 관점에서 쓸 수 있는 쿼리문이다. 우선 JPQL을 우리가 활용하는 경우는 99% 조회이다. update나 insert와 같은 변경을 가해지는 작업은 JPA의 persist나 변경감지를 이용해야 한다. JPQL은 결국 sql 쿼리문을 객체 관점에서 쓸 수 있도록 편리화해둔 하나의 sql문법이기 때문이다. 그렇기에 JPQL은 항상 DB를 먼저 조회한다. 그리고 영속성 컨텍스트를 본다. 즉 em.find()와 다르게 1차로 DB를 확인하고 2차로 영속성 컨텍스트를 서치한다. 2차로 서치할 때는 만약 DB에서 조회했던 엔티티가 이미 있다면 DB것을 버리고 영속성 컨텍스트의 엔티티를 취하는 방식이다.

예시를 생각해보자.

  1. persist(member) -> 영속화(신규)
    1. Insert 쿼리문 쓰기지연 sql 저장소에 저장됨.
    2. 영속성 컨텍스트에 member 객체 올라감
  2. member.setName("Minsu") -> 수정
    1. 영속성 컨텍스트의 member에 수정이 들어감.(이대로 적용을 위해서는 트랜잭션이 끝나거나 flush()가 있어야 한다.

2.과정 뒤에 JPQL로 조회를 시도해본다고 가정해보자. 이때 엔티티의 name필드가 "Minsu"로 변화했는데 이 변화는 영속성 컨텍스트에서만 유지되고 있다. 그렇기에 JPQL은 flush()를 통해 우선적으로 영속성 컨텍스트를 DB에 반영한다.

그리고 조회하여 엔티티를 가져온다. 이때 영속성 컨텍스트에 조회한 결과를 안착시키는데, 만약 이미 영속성 컨텍스트에 해당 엔티티가 이미 존재하는 경우 신규 조회(DB에서 가져온) 엔티티를 버리게 된다.

flush

기본적으로 JPA 동작은 트랜잭션이 끝나는 시점에 커밋이 되고 커밋때 flush를 통해 영속성 컨텍스트 내의 엔티티에 대해 쓰기 지연 sql 저장소의 sql문을 적용한다.

이러한 flush는 직접 EntityManager를 통해 강제 적용할 수도 있지만 이러한 동작을 직접 사용할 일은 거의 없다.

준영속 상태

가장 개념적으로 애매한 것이 준영속이다. 왜 굳이 "한번 영속성 컨텍스트에서 관리되었다가 나온 엔티티"의 상태를 정의하는 것일까

준영속 상태를 영속 상태로 만드는 방법은 merge()이다. 반면 비영속상태를 영속상태로 만드는 것은 persist()이다.

정확히 표현하면 준영속상태는 DB에 동일 키 엔티티가 존재하는 것이다. DB에 엔티티가 존재하기 위해서는 JPA아래에서 영속성 컨텍스트를 한번은 거쳐야 하기 때문이다. 영속성 컨텍스트에서 이전에 존재한 것과 더불어 한번은 flush된 엔티티를 말하는 것이다. 이렇게 한번 flush된 엔티티는 persist()시에 insert 쿼리문에 대해 예외를 뱉기 때문에 구분할 목적이 충분히 생기는 것이다.

트랜잭션도 고려해서 생각하면...

영속성 컨텍스트에 들어간 엔티티가 트랜잭션내에서 혹은 트랜잭션이 끝나서 DB에 제대로 안착하는 상황이 아닌 바로 나올 수 있는 경우는 거의 없다. 애초에 Entity에 GenerativeValue(IDENTITY, SEQUENCE)와 같은 조건을 자주 쓰게되는데 이 조건하에서 persist()시 즉각 insert쿼리가 나가기 때문이다. 준영속 상태일 경우는 여러 로직에 걸쳐 발생하는 경우가 많은데, 엔티티가 GenerativeValue 없이 persist()되고 persist로 발생한 insert쿼리가 작동하지 않은 채 영속성컨텍스트에서 엔티티가 나오게되는 매우매우 인위적인 경우는 사실 없다.

그렇다면 영속상태인 객체가 트랜잭션이 끝나 다같이 사라지는 경우가 아니고 detach로 빠져나오는 경우가 있을까? 실제로는 detach()로 빠져나오는 것이 아닌 개념적으로 키를 가진 엔티티가 영속성 컨텍스트에서 관리되지 않으며 존재할 시점이 생길 수 있다.

여러 계층의 로직들을 묶어 생각하면 가능하다. 만약 웹 계층을 포함한 전체 클라이언트-서버 동작에서 데이터를 수정하는 과정을 생각해보자.

  1. 웹에서 이미 있는 엔티티들(DB에 저장된 사항들)이 보여지고 있음.
  2. 특정 엔티티에 대해 수정사항을 입력하고 수정 요청을 수행. 웹에서 form으로 하여금 수정사항을 받아옴.
  3. 수정사항에 해당하는 엔티티를 new Member()와 같이 새로 제작 setter로 필드들 설정
  4. persist()
  5. Error!!

new Member()로 하여금 만들어진 엔티티 객체는 비영속객체 같아 보이지만 준영속객체이다.(키를 가지고 있기 때문에) 이유는 DB에 이미 동일키의 엔티티가 존재하기 때문이다. 이때문에 persist()는 에러가 발생한다. 영속적 컨텍스트에 들어있지 않기 때문에 객체를 수정한다 하더라도 반영이 불가능하다. 준영속객체를 반영하는 유일한 방법은 em.merge()이지만 이는 위험한 선택이다.(위험한 선택인 이유는 위의 merge부분의 링크를 확인하자)

결국 위의 0~4까지의 방법에서 2번 사항을 수정해야한다.

  1. 웹에서 이미 있는 엔티티들(DB에 저장된 사항들)이 보여지고 있음.
  2. 특정 엔티티에 대해 수정사항을 입력하고 수정 요청을 수행. 웹에서 form으로 하여금 수정사항을 받아옴.
  3. form에서 id로 하여금 DB에 em.find()로 조회시도 -> 영속성 컨텍스트에 수정전 엔티티 안착
  4. 수정전 엔티티를 수정 -> 변경감지
  5. 커밋때 수정쿼리 날라가면서 수정성공

준영속 상태는 이렇게 개발시에 여러 과정에 걸쳐 발견가능한 경우이며 이때 merge를 쓰지 않고 조회후 객체 수정과 같이 변경감지를 꼭 활용하도록 하자.

정리

JPA의 기능은 편리하지만 내부동작은 간단하지 않다. 각각의 기능들에 대해 flush시점, 동적인 영속 상태, 동적으로 반영되는 쓰기지연 쿼리 저장소 등 자신이 코딩하는 JPA 기능들에대해 각각의 움직임을 상상할 수 있어야 한다. 그렇지 않고 쓰게된다면 가장 단순한 기능들만 사용해야하거나 복잡한 기능 사용시 이유모를 에러에 mybatis와 같은 sql매퍼를 쓰는 것보다 더 생산성이 떨어질 수 있다.

준영속, 비영속을 구분하는 핵심은 @ID인 "키"이다. 영속성 컨텍스트에 DB에 존재하는 키를 가진 엔티티를 영속화 하기 위해서는 merge가 필요하지만, 실제로 merge가 필요한 상황을 만들지 않고 변경감지를 활용할 수 있도록 선조회 후변경을 진행해서 처리하는 방식이 모범적 절차이다.

profile
자바집사의 거북이 수련법

0개의 댓글