스프링 핵심 원리 - 기본편 - 인프런
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다. 초급 프레임워크 및 라이브러리 웹 개발 서버 개발 Back-End Spring 객체지향 온
www.inflearn.com
강의를 들으며 생각 정리 + "자바 ORM 표준 JPA 프로그래밍" 책 참고
JPA에서 가장 중요한 두 가지는 다음과 같다.
- 객체와 관계형 데이터베이스 매핑하기
- 영속성 컨텍스트
이 장에서는 매핑한 엔티티를 엔티티 매니저를 통해 어떻게 사용하는지 영속성 컨텍스트와 관련하여 알아보자.
영속성 컨텍스트
JPA를 이해하는 데 가장 중요한 용어는 영속성 컨텍스트(persistence context)다. "엔티티를 영구 저장하는 환경"인 영속성 컨텍스트는 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다. 즉 persist는 사실 DB에 저장하는 것이 아니라 엔티티를 영속성 컨텍스트에 저장하는 것이다. 여기서 커밋을 해야 insert 쿼리가 나가 영속성 컨텍스트에서 DB로 저장된다.
엔티티 매니저를 통해서 영속성 컨텍스트에 접근할 수 있고 1대1로 매핑되어 있다.(스프링 프레임워크 같은 환경에서는 N대1도 가능하지만 일단 1대1로 이해한다.)
엔티티의 생명주기
엔티티에는 4가지 상태가 존재한다.
- 비영속(new/transient) : 영속성 컨텍스트와 전혀 관게가 없는 상태(new로 객체를 만든 상태 등)
- 영속(managed) : 영속성 컨텍스트에 저장된 상태
- 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태(em.deatch(), em.close(), em.clear())
- 삭제(removed) : 삭제된 상태(em.remove())
영속성 컨텍스트의 이점
영속성 컨텍스트가 객체, DB 중간에 위치함으로써 얻는 이점은 다음과 같다.
엔티티 조회
영속성 컨텍스트는 내부에 1차 캐시를 가지고 있다. 1차 캐시는 Map<PK, 엔티티>의 형태로 이루어져있기 때문에 객체 조회 시 PK를 기반으로 1차 캐시를 탐색한다. (영속성 컨테스트는 엔티티를 식별자 값(@Id)으로 구분하기 때문에 영속 상태는 식별자 값이 반드시 있어야 한다.) 1차 캐시에 해당 식별자가 있으면 반환하고 없으면 DB를 조회해서 1차캐시에 저장 후 반환한다. 후에 같은 식별자 조회 시 1차캐시에서 반환할 수 있다.(조회 시 쿼리가 나가지 않는다.)
그러나 트랜잭션(고객 서비스)이 끝나면 영속성 컨텍스트도 사라지기 때문에 성능 이점이 크지는 않다. 다만, 식별자가 같은 인스턴스를 조회할 때 그 동일성을 보장해주는 등 JPA의 내부 메커니즘을 유지해주는 역할을 한다.
엔티티 등록
영속성 컨텍스트는 1차 캐시 뿐만 아니라 쓰기 지연 SQL 저장소라는 것이 있다. 엔티티 매니저가 persist를 하면 insert sql이 쓰기 지연 저장소에 차곡차곡 등록이 된다. 물론 동시에 1차 캐시에도 해당 엔티티가 등록이 된다. 이제 트랜잭션을 커밋하면 엔티티 매니저는 우선 영속성 컨텍스트를 플러시한다. 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업인데 이때 등록, 수정, 삭제한 엔티티를 데이터베이스에 반영한다. 구체적으로 이야기하면 쓰기 지연 SQL 저장소에 모인 쿼리를 DB에 보낸 후 DB를 커밋한다.
엔티티 수정
JPA로 엔티티를 수정할 때는 단순히 엔티티를 조회해서 데이터만 변경하면 된다. 엔티티의 데이터만 변경했는데 어떻게 데이터베이스에 반영이 되는 걸까? 이렇게 엔티티의 변경사항을 데이터베이스에 자동으로 반영하는 기능을 변경 감지(dirty checking)라 한다.
JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는데 이것을 스냅샷이라 한다. 그리고 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다. 순서는 다음과 같다.
- 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 플러시가 호출된다.
- 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
- 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.
- 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
- 데이터베이스 트랜잭션을 커밋한다.
엔티티 삭제 역시 등록의 경우와 동일한 메커니즘으로 동작한다. 다만, remove를 호출하는 순간 엔티티는 영속성 컨텍스트에서 제거된다.
플러시
플러시(flush)는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다. 플러시를 실행하면 구체적으로 다음과 같은 일이 일어난다.
- 변경 감지 - 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾는다. 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다.
- 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.
영속성 컨텍스트를 플러시하는 방법은 다음과 같다.
- em.flush() - 보통 트랜잭션 커밋을 많이 사용하지만 자체 테스트용으로 flush를 사용할 때가 있다.
- 트랜잭션 커밋 - 플러시가 자동 호출된다.
- JPQL 쿼리 실행 - 플러시가 쿼리 실행 이전에 호출된다.
JPQL의 경우 DB에 직접 접근해야 하기 때문에 이전에 쓰기 지연 SQL 저장소의 쿼리를 비워준다.
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members= query.getResultList();
예를 들어 위와 같은 코드에서 memberA, memberB, memberC를 영속성 컨텍스트에 저장하고 insert 쿼리를 쓰기 지연 sQL 저장소에 저장한 상태일 때, 멤버 클래스 전체를 불러오는 JPQL을 사용한다면 현재 DB에는 멤버 객체들이 없기 때문에 아무것도 불러올 수가 없다. 따라서, JPQL 이전에 쓰기 지연 저장소의 insert 쿼리를 미리 DB에 반영해서 원하는 데이터를 얻을 수 있다.
플러시 모드 옵션
em.setFlushMode(FlushModeType.COMMIT)
- FlushModeType.AUTO - 커밋이나 쿼리를 실행할 때 플러시(default)
- FlushModeType.COMMIT - 커밋할 때만 플러시
+) 참고로 플러시를 영속성 컨텍스트의 엔티티들을 지우는 것이라 착각하면 안 된다. 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화 하는 것이다.(단, 쓰기 지연 저장소는 비워진다.) 그리고 데이터베이스와 동기화를 최대한 늦추는 것이 가능한 이유는 트랜잭션이라는 작업 단위가 있기 때문이다. 트랜잭션 커밋 직전에만 변경 내용을 데이터베이스에 동기화하면 된다.
준영속 상태
영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된(detached) 것을 준영속 상태라 한다. 따라서 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 있다. 영속상태의 엔티티를 준영속 상태로 만드는 방법은 다음과 같다.
- em.detach(entity) - 특정 엔티티를 준영속 상태로 만든다. 메서드를 호출하는 순간 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거된다.
- em.clear() - 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만든다.
- em.close() - 영속성 컨텍스트를 종료한다.
그럼 준영속 상태인 엔티티는 어떻게 되는 걸까?
- 영속성 컨텍스트가 제공하는 어떠한 기능도 사용할 수 없기 때문에 비영속 상태에 가깝다.
- 준영속 상태는 이미 한 번 영속 상태였으므로 식별자 값을 가지고 있다.
- 지연 로딩을 사용할 수 없다.(해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법)
병합 merge
준영속 상태의 엔티티를 다시 영속 상태로 변경하려면 병합을 사용하면 된다. merge() 메서드는 준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환한다. merge의 동작 방식은 다음과 같다.
Member mergeMember = em.merge(member);
- merge()를 실행한다.
- 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다. 만약 1차 캐시에 엔티티가 없으면 DB에서 엔티티를 조회하고 1차 캐시에 저장한다.
- 조회한 영속 엔티티에(mergeMember) 준영속 엔티티(member)의 값을 채워 넣는다.
- 해당 영속 엔티티를 반환한다.
이제 새로운 영속 상태인 mergeMember가 반환되었기 때문에 준영속 상태인 member는 사용할 필요가 없다. 따라서 다음과 같이 준영속 엔티티를 참조하던 변수를 영속 엔티티를 참조하도록 변경하는 것이 안전하다.
member = em.merge(member);
+) merge는 비영속 엔티티도 영속 상태로 만들 수 있다. 엔티티의 식별자 값이 영속성 컨텍스트의 1차 캐시, DB에서 모두 발견하지 못하면 새로운 엔티티를 생성해서 병합한다. 따라서, 병합은 준영속, 비영속을 신경 쓰지 않기 때문에 save or update 기능을 수행한다.
정리
- 엔티티 매니저 팩토리는 엔티티 매니저를 생성하고 그 내부에 영속성 컨텍스트도 함께 만들어진다. 영속성 컨텍스트는 엔티티 매니저를 통해서 접근할 수 있다.
- 영속성 컨텍스트는 1차 캐시, 동일성 보장, 쓰기 지연, 변경 감지, 지연 로딩 기능을 사용할 수 있다.
- 영속성 컨텍스트에 저장된 엔티티는 플러시 시점에 DB에 반영되는데 일반적으로 트랜잭션 커밋 시에 플러시된다.
- 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능들을 사용할 수 없다.
'java > jpa' 카테고리의 다른 글
[JPA] 다양한 연관관계 매핑 (0) | 2021.01.27 |
---|---|
[JPA] 연관관계 매핑 기초 (0) | 2021.01.26 |
[JPA] 엔티티 매핑 (0) | 2021.01.22 |
[JPA] JPA 시작하기 (0) | 2021.01.21 |
[JPA] JPA 소개 (0) | 2021.01.21 |