김영한 선배님의 JPA 활용 강의를 듣다가 JPA 영속성 컨텍스트에 대해 궁금증이 생겨서 공부를 하게 되었습니다. 이번 포스팅은 [자바 ORM 표준 프로그래밍 - 김영한] 책을 참고하며 포스팅하였습니다😃
JPA Entity🔆
JPA는 엔티티와 테이블을 매핑하는 설계 부분과 매핑한 엔티티를 실제 사용하는 부분으로 나뉩니다. 엔티티를 실제 사용할 때는 엔티티 매니저(em)를 통해 엔티티를 저장하고, 수정하고, 삭제하고, 조회하는 작업을 할 수 있습니다.
엔티티 생명주기⏳
엔티티에는 총 4가지의 상태가 존재합니다.
- 비영속(new / transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
// 객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
위 코드처럼 엔티티 객체를 생성만 하고 저장하지 않았으므로 순수한 객체 상태이며 영속성 컨텍스트나 데이터베이스와 전혀 관련이 없는 상태를 비영속 상태라고 합니다.
- 영속(managed) : 영속성 컨텍스트에 저장된 상태
// 객체를 저장한 상태(영속)
em.persist(member);
엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장하였습니다. 영속 상태라는 것은 영속성 컨텍스트에 의해 관리된다는 뜻이며, persist() 이외에도 em.find()나 JPQL을 사용해서 조회한 엔티티도 영속 상태입니다.
- 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
// 회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);
영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 더 이상 관리하지 않으면 준영속 상태가 됩니다. 즉, 위의 코드처럼 detach를 사용하여 준영속 상태로 만들 수 있습니다. 다만, 개발자가 직접 준영속 상태로 만드는 경우는 극히 드뭅니다.
- 삭제(removed) : 삭제된 상태
// 객체를 삭제한 상태(삭제)
em.remove(member);
영속성 컨텍스트❓
JPA를 이해하는 데 가장 중요한 용어는 영속성 컨텍스트입니다. '엔티티를 영구 저장하는 환경'이라는 뜻으로, 위의 생명주기에 맞게 엔티티를 저장하거나 조회, 보관하고 관리합니다. 하지만 여기서 엔티티를 DB에 저장하는 것이 아닙니다. 엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장하는 것을 일컫습니다.
영속성 컨텍스트는 우리 눈에 보이지 않아 DB와 헷갈릴 수 있습니다. 영속성 컨텍스트는 엔티티 매니저를 생성할 때 자동으로 하나 만들어지기에 개발자들은 엔티티 매니저를 통해서 영속성 컨텍스트에 접근하고 관리할 수 있게 됩니다.
특징👀
- 영속성 컨텍스트는 엔티티를 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분합니다. 따라서 영속 상태의 엔티티는 식별자 값이 반드시 있어야 합니다.
- 위에서 언급한 것처럼 persist()하여 영속 관계가 된 엔티티는 persist() 단계에서 DB에 저장되는 것이 아니라 영속성 컨텍스트에 저장되며, 후에 트랜잭션을 커밋하는 순간 데이터베이스에 반영합니다. 이 단계를 Flush라고 하며 하단에서 설명할 예정입니다😀
장점🌞
영속성 컨텍스트가 엔티티를 관리하면 1차 캐시·동일성 보장·트랜잭션을 지원하는 쓰기 지연·변경 감지·지연 로딩 총 5가지의 장점을 가지고 있습니다.
1차 캐시
// 비영속
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
// 영속
em.persist(member);
영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 1차 캐시라고 부릅니다. persist() 단계에서 이전에 영속성 컨텍스트에 저장된다고 했는데 정확히는 영속성 컨텍스트 내부의 1차 캐시에 저장됩니다.
회원을 저장한 후 조회하는 로직은 어떻게 될까요?
Member member = em.find(Member.class, "member1");
find()를 통해 해당 회원을 조회할 때는 총 2가지의 방법으로 나뉩니다. 만약 "member1"이 영속 상태여서 1차 캐시에 엔티티가 있다면 데이터베이스를 조회하지 않고 메모리에 있는 엔티티를 바로 반환해줍니다.
반대로 "member1"이 영속 상태가 아니라면 데이터베이스를 조회한 후 바로 반환하는 것이 아닌 조회한 엔티티를 1차 캐시에 저장하고, 1차 캐시에서 엔티티를 가져오게 됩니다.
회원을 조회할 때마다 데이터베이스를 갔다 오지 않고 1차 캐시에서 관리하기 때문에 성능상 이점을 누릴 수 있습니다😄
동일성 보장
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
Q. a == b ?
A. true
위와 같이 식별자가 같은 엔티티 인스턴스를 조회해서 비교했을 때, 영속성 컨텍스트는 1차 캐시에 있는 같은 엔티티 인스턴스를 반환해주기 때문에 a == b는 true로 동일성을 보장해줍니다.
트랜잭션을 지원하는 쓰기 지연
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다
transaction.begin();
em.persist(memberA);
em.persist(memberB);
// 여기까지는 1차 캐시에 저장
transaction.commit();
// 커밋하는 순간 DB에 INSERT 쿼리문을 보낸다
JPA는 1차 캐시의 이점처럼 커밋하기 전까지는 1차 캐시에 엔티티를 저장하고 관련 쿼리들을 쌓아 놓습니다. 그리고 트랜잭션을 커밋할 때 모아둔 쿼리들을 한 번에 데이터베이스에 보내는데 이것을 트랜잭션을 지원하는 쓰기 지연이라 합니다.
쓰기 지연 기능을 활용하면 모아둔 등록 쿼리를 한번에 전달해서 SQL Batch를 사용해 성능을 최적화할 수 있습니다.
변경 감지(Dirty Checking)
사실상 변경 감지의 장점 때문에 포스팅했다고 해도 과언이 아닙니다. 중요한 부분이니 필독❗ 해주세요.
[SQL]
JPA를 사용하기 전 SQL을 사용하면 수정 쿼리를 일일이 직접 작성해야 했습니다. 프로젝트가 커지면 커질수록 요구사항이 늘어나면서 수정 쿼리도 자연스레 점점 추가됩니다.
기존에는 10개의 필드에서 필드가 추가될 때마다 개발자는 쿼리문을 직접 추가해야 했고, 빈 값이 들어올 경우 등 예외 상황에 따라 SQL을 수정해야 했습니다. 결론적으로 비즈니스 로직이 SQL에 의존하게 되고, 개발자는 이러한 부분을 의식하지 않을 수 없었습니다.
그럼 JPA는 엔티티를 어떻게 수정할까요?
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
trasaction.begin();
// 영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
// 영속 엔티티 데이터 수정
memberA.setUsername("binco");
memberA.setAge("10");
transaction.commit();
SQL을 사용했던 관점에서 바라보면 위 코드가 부족해 보이지 않나요? 상식적으로 memberA에 이름과 나이만 변경했지 update 해주는 코드는 보이지 않습니다.
이렇게 영속 상태의 엔티티의 변경사항을 데이터베이스에 자동으로 반영하는 기능을 변경 감지(Dirty Checking)이라고 합니다.
JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는데 이것을 스냅샷이라고 합니다. 그리고 플러시 시점에 스냅샷과 현재 엔티티를 비교해서 변경된 부분을 찾습니다. 만약 변경된 데이터가 있다면 update 쿼리 문을 쓰기 지연 SQL 저장소에 저장해 두었다가 트랜잭션 커밋 시 데이터베이스에 반영합니다.
📌하단에서 설명할 Merge와는 확실히 차이가 있습니다. Merge는 새로운 엔티티를 만들어서 기존 엔티티를 덮어 씌우는 방식이기 때문에, 자칫 실수로 빈 필드도 같이 합쳐질 수 있습니다. 그렇기에 많은 개발자 선배님들께서는 Merge보단 Dirty Checking 사용을 권장하십니다.
하지만 JPA의 기본 전략은 엔티티의 모든 필드를 업데이트하기 때문에, 변경 사항만 업데이트하고 싶을 시에는 @DynamicUpdate 어노테이션을 사용하여 동적으로 변경할 수 있습니다.
Flush✨
플러시(Flush)는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영합니다. 플러시를 실행하면 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾고, 해당 사항에 맞는 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록 후 데이터베이스에 전송합니다.
영속성 컨텍스트를 플러시 하는 방법은 3가지가 있습니다.
- em.flush()를 직접 호출하는 방법
- 트랜잭션 커밋 시 플러시가 자동 호출
- JPQL 쿼리 실행 시 플러시가 자동 호출
병합(Merge)📚
사실 위에서 말한 것처럼 병합보다는 변경 감지를 사용하는 것이 좋습니다. 병합은 준영속 상태의 엔티티를 다시 영속 상태로 변경할 때 사용합니다. merge() 메서드는 준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환합니다.
이 말은 즉, 준영속 상태의 엔티티가 영속 상태로 상태가 변경되는 것이 아니라 새로운 영속 상태의 엔티티가 반환된다는 뜻입니다.
[동작 방식]
- merge()를 실행한다.
- 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.
- 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고 1차 캐시에 저장한다.
- 조회한 영속 엔티티에 새로운 엔티티 값을 채워 넣는다
- merge 된 엔티티를 반환한다.
👨👩👦👦 오픈채팅방 운영
취업을 준비하는 예비 개발자분들을 위한 질문&답변할 수 있는 공간을 만들었습니다. 취업과 이직을 하기 위해서 어떤 걸 중점적으로 준비해야 하는지부터 포트폴리오&이력서 작성법 등 다양한 질문들을 받고 답변을 드립니다. 참여하셔서 다양한 정보 얻고 가시면 좋을 것 같네요😁
참여코드 : 456456
https://open.kakao.com/o/gVHZP8dg
👨💻 전자책 출간
아울러 제가 🌟비전공자에서 2년만에 보안 전문 중견기업으로 이직 한 방법들을 정리한 전자책을 출간 하게 되었습니다. 어떤 걸 공부해야 하는지, 이직을 위해서 무엇을 준비해야 하는지, 제가 받았던 기술 면접 리스트 등 다양한 목차로 구성되어 있습니다. 또한, 구매 시 1:1 채팅을 이용하여 포트폴리오 첨삭을 도와드리고 있습니다. 🐕전자책으로 얻은 모든 수익은 유기견 센터 '팅*벨 입양센터'에 후원될 예정입니다. 관심 있으신 분들은 아래 링크를 참고해주세요😁
마치며
지금까지 JPA의 영속성에 관해 알아보았습니다. 기존에 알던 내용들보다는 새롭게 알게 된 정보들이 상당히 많았던 것 같네요😅 실무에서 JPA를 사용하지만 어떤 이점이 있어서 사용하는지, 또 왜 이렇게 작동하는지에 대한 고민은 크게 안 했었는데 이번 기회에 동작 원리를 알게 된 것 같네요😄
Reference
'JPA' 카테고리의 다른 글
SpringBoot JPA 쇼핑몰 엔티티 개발Ⅰ (0) | 2022.09.13 |
---|---|
SpringBoot JPA 쇼핑몰 도메인&테이블 설계 (0) | 2022.09.07 |
JPA Eager Loading VS Lazy Loading (0) | 2022.09.05 |
SpringBoot JPA 게시판 CRUD 구현(예외 처리) (0) | 2022.07.27 |
SpringBoot JPA 게시판 CRUD 구현(Delete-TDD) (0) | 2022.07.25 |