-
영속성 컨텍스트, Entity Cache, Entity Lifecycle백엔드 2023. 8. 24. 17:20728x90반응형SMALL
영속성 컨텍스트 (persistent context)
프레임워크에서 컨테이너들이 관리하고 있는 내용들을 컨텍스트라고 합니다. 스프링의 빈들을 로딩하고 관리하는 일련의 작업들은 스프링 컨텍스트 위에서 활용되고 있습니다. 즉, persistent container가 관리하는 내용입니다.
영속화
- 사라지지 않고 지속적으로 접근할 수 있다는 의미입니다.
Jpa 또한, Java Persistent Api입니다. 데이터를 영속화하는데 사용하는 컨테이너가 영속성 컨텍스트입니다.
영속성 컨텍스트의 가장 주체적인 역할을 하는 클래스는 Entity Manager라는 빈입니다.
현재까지 영속성 컨텍스트 설정에 대해 신경을 쓰지 않고도 Jpa 를 활용할 수 있었던 이유는, spring-data-jpa에 의해 spring boot가 자동적으로 persistent context에 대한 설정을 해주었기 때문입니다.
영속성에 대해 좀 더 알아보기 위해 h2 db말고 mysql에 연결하여 db를 사용해보도록 하겠습니다.
의존성 설치 (build.gradle)
dependencies{ ... runtimeOnly 'mysql:mysql-connector-java' }
mysql 설정 (application.yml)
spring: jpa: generate-ddl: true hibernate: ddl-auto: create-drop datasource: url: jdbc:mysql://localhost:3306/<data_base> username: <username> password: <password>
generate-ddl을 true로 설정할 경우 자동으로 entity에서 활용하고 있는 테이블을 생성해줍니다.
ddl-auto
- none : ddl-auto를 실행하지 않습니다.
- create: 항상 새로 생성해줍니다.
- create-drop : 항상 create 하고 persistent context가 종료할 때 자동으로 drop
- update : 실제 스키마와 Entity 클래스를 비교해서 변경된 부분만 반영, drop을 하지 않습니다.
- validate : 비교 작업만 하고 Entity클래스와 스키마가 서로 다른 부분이 확인되면 오류를 반환합니다.
create, create-drop
create는 생성 전에 drop을 한 번 하고 create
create-drop은 persistent context를 띄울 때 create만 하고 종료할 때 drop해줍니다.
generate-ddl, ddl-auto
실제 상용하는 데이터베이스에 대해 자동화된 ddl구문을 사용하는 것은 위험하기 때문에 실제로는 generate-ddl: false, ddl-auto: none으로 설정하고 사용하는 경우가 많다고 합니다(간혹가다 ddl-auto: validate를 사용할 수도 있습니다).
generate-ddl은 jpa의 설정이므로 구현체와는 상관 없이 자동화된 ddl을 사용할 수 있도록 해주고 좀 더 범용적인 옵션입니다. ddl-auto는 hibernate에서 제공하는 좀 더 세밀한 옵션입니다.
ddl-auto는 generate-ddl보다 좀 더 세부적인 설정을 할 수 있기 때문에 ddl-auto가 설정될 경우 generate-ddl은 무시된다고 합니다.
(generate-ddl의 default는 false인데 h2 db에서 동작한 이유는 spring은 임베디드 db를 사용할 경우 기본이 ddl-auto가 create-drop으로 설정된다고 합니다.)
위와 같이 설정해줄 경우 mysql8dialect를 사용하게 됩니다. dialect의 경우 entity나 repository에서 사용하는 orm을 실제로 데이터베이스 쿼리로 변환해서 jdbc를 통해서 전달하도록 되어 있습니다. 즉 java에서 사용하는 getter나 setter와 같은 명령어들을 특정 데이터베이스 쿼리로 변경해주도록 정해줍니다. 여러 데이터베이스 형태에 따라 쿼리가 조금씩 다르기 때문에 dialect를 설정해줌으로써 그 쿼리를 자동으로 맞춰줄 수 있도록 합니다.
Entity Manager
컨텍스트 안에서 Entity는 생성, 조회, 수정, 제거 등이 이루어집니다. 여기서 많은 역할을 하는 것이 Entity Manager입니다.
EntityManager안에는 생성, 조회, 수정 등에 대한 쿼리를 할 수 있도록 메서드들이 정의되어 있습니다.
//EntityManager.java.persistent public interface EntityManager { /** * Make an instance managed and persistent. * @param entity entity instance * @throws EntityExistsException if the entity already exists. * (If the entity already exists, the <code>EntityExistsException</code> may * be thrown when the persist operation is invoked, or the * <code>EntityExistsException</code> or another <code>PersistenceException</code> may be * thrown at flush or commit time.) * @throws IllegalArgumentException if the instance is not an * entity * @throws TransactionRequiredException if there is no transaction when * invoked on a container-managed entity manager of that is of type * <code>PersistenceContextType.TRANSACTION</code> */ public void persist(Object entity); /** * Merge the state of the given entity into the * current persistence context. * @param entity entity instance * @return the managed instance that the state was merged to * @throws IllegalArgumentException if instance is not an * entity or is a removed entity * @throws TransactionRequiredException if there is no transaction when * invoked on a container-managed entity manager of that is of type * <code>PersistenceContextType.TRANSACTION</code> */ public <T> T merge(T entity); /** * Remove the entity instance. * @param entity entity instance * @throws IllegalArgumentException if the instance is not an * entity or is a detached entity * @throws TransactionRequiredException if invoked on a * container-managed entity manager of type * <code>PersistenceContextType.TRANSACTION</code> and there is * no transaction */ public void remove(Object entity); ...
이러한 EntityManager의 구현체를 빈으로 등록하고 있기 때문에 @Autowired를 통해서 사용할 수 있습니다.
예시)
@SpringBootTest public class EntityManagerTest { @Autowired private EntityManager entityManager; @Test public void entityManagerTest(){ System.out.println(entityManager.createQuery("select u from User u").getResultList()); } }
entityManager를 통해 쿼리를 실행 하면, User 전체 데이터를 가져오는 것을 확인할 수 있습니다.
지금까지 사용한 SimpleJpaRepository의 쿼리 메서드들은 직접적으로 entityManager를 사용하지 않아도 되도록 한 번 래핑을 해서 제공해주는 것입니다.
EntityManager의 구현체는 Hibernate에서 제공하는 SessionImpl 구현체입니다. (Hibernate에서 EntityManager를 세션이라고 부릅니다)
EntityCache
영속성 컨텍스트에서 Entity들을 관리하는 Entity Manager는 캐시를 가집니다. save() 메서드 실행 시 바로 db에 반영하지 않기 때문입니다. 우리가 사용하는 영속성 컨텍스트와 실제 db사이에 데이터 갭이 생긴다는 의미입니다.
@Transactional @Test public void test(){ System.out.println(userRepository.findByEmail("user1@gmail.com")); System.out.println(userRepository.findByEmail("user1@gmail.com")); System.out.println(userRepository.findByEmail("user1@gmail.com")); System.out.println(userRepository.findById("1L").get()); System.out.println(userRepository.findById("1L").get()); System.out.println(userRepository.findById("1L").get()); }
출력
Hibernate: select user0_.id as id1_5_, user0_.email as email2_5_, user0_.name as name3_5_ from user user0_ where user0_.email=? User(id=1, name=user1, email=user1@gmail.com, userHistories=[]) Hibernate: select user0_.id as id1_5_, user0_.email as email2_5_, user0_.name as name3_5_ from user user0_ where user0_.email=? User(id=1, name=user1, email=user1@gmail.com, userHistories=[]) Hibernate: select user0_.id as id1_5_, user0_.email as email2_5_, user0_.name as name3_5_ from user user0_ where user0_.email=? User(id=1, name=user1, email=user1@gmail.com, userHistories=[]) User(id=1, name=user1, email=user1@gmail.com, userHistories=[]) User(id=1, name=user1, email=user1@gmail.com, userHistories=[]) User(id=1, name=user1, email=user1@gmail.com, userHistories=[])
위와 같이 findByEmail과 findById를 통해 조회를 하는 쿼리를 실행하게 된다면 findByEmail에 대한 select 쿼리는 3번 실행되고 findById에 대한 select 쿼리는 실행되지 않습니다.
그 이유는 따로 캐시 설정을 해주지 않아도 영속성 컨텍스트 내에서 자동으로 Entity에 대해 캐시 처리하는 것을 Entity의 1차 캐시에서 해주기 때문입니다.
1차 캐시는 Map 형태로 만들어지게 됩니다. key는 id값 value는 해당 Entity값이 들어가게 됩니다. 그래서 id로 조회를 하게 되면 일단 영속성 컨텍스트 내에 있는 1차 캐시에 해당 id값의 Entity가 존재할 경우 db조회 없이 해당 값을 리턴해주게 됩니다. 1차 캐시에 없을 때에만 실제 쿼리로 조회를 하고 1차 캐시에 저장을 한 뒤, 값을 리턴해줍니다.
1차 캐시가 동작함에 따라 기본적인 조회 쿼리에 대한 성능이 좋아지게 됩니다. 직접 id를 통해 조회를 하는 경우 드믄 편이지만 Jpa 특성상 id를 통해 조회를 하는 경우가 많기 때문에(delete 시에도 select by id 쿼리가 한 번 실행된다), 성능 저하가 일어날 경우 1차 캐시를 사용해 성능 저하를 줄일 수 있습니다.
@Transactional
@Transactional 내부에서는 최대한 db에 반영하는 시기를 늦추게 됩니다. 영속성 컨텍스트 내에서 Entity Manager가 자체적으로 Entity 상태를 머지하고 db에 반영해야 하는 내용에 대해서만 쿼리가 실행되게 됩니다.
@Transactional 선언X
@Test void test(){ User user = new User(); user.setName("user1"); user.setEmail("user1@gmail.com"); userRepository.save(user); user.setName("ussser1"); userRepository.save(user); user.setEmail("ussser1@gmail.com"); userRepository.save(user); }
@Transactional로 묶지 않을 경우 save는 각각이 transaction이 되어 바로바로 처리되게 됩니다. 위와 같은 경우 update 쿼리는 총 2번 일어나게 됩니다.
save 메서드의 경우 @Transactional이 선언되어 있습니다. 그렇기 때문에 save 상위에 @Transactional이 선언되지 않은 경우 각각 바로 실행되게 됩니다.
@Transactional 선언
@Transactional @Test void test(){ User user = new User(); user.setName("user1"); user.setEmail("user1@gmail.com"); userRepository.save(user); user.setName("ussser1"); userRepository.save(user); user.setEmail("ussser1@gmail.com"); userRepository.save(user); // userRepository.flush(); }
@Transactional을 선언한 경우 전체 코드가 트랜잭션이 되고, 영속성 컨텍스트 내에서 각각의 변경 내역을 가지고 있다가 머지를 해서 한 번만 업데이트를 처리하게 됩니다.
위와 같은 경우 test 메서드이기 때문에 flush를 해야지만 update가 반영되기 때문에 flush가 있어야 되기는 하지만, 문맥상 필요하지 않아 주석처리했습니다.
영속성 컨텍스트 내에 쌓여있는 데이터는 EntityManager가 자체적으로 영속화를 해주지만, flush를 통해 의도하는 타이밍에 영속화를 해줄 수도 있습니다. 영속성 캐시에 쌓여 아직 반영되지 않은 Entity의 변경을 flush실행 시점에 모두 DB에 반영해줍니다.
영속성 컨텍스트와 DB 동기화 시점
1. flush 메서드 호출 시점
개발자가 의도적으로 영속성 캐시를 DB에 반영하기 위해 flush를 호출할 수 있습니다.
@Transactional @Test void test2(){ User user = new User(); user.setName("user1"); user.setEmail("user1@gmail.com"); userRepository.save(user); user.setName("ussser1"); userRepository.save(user); user.setEmail("ussser1@gmail.com"); userRepository.save(user); System.out.println(userRepository.findById(1L).get()); // 1 userRepository.flush(); System.out.println(userRepository.findById(1L).get()); // 2 }
@Transactional이 선언되어 있으므로 1번 findById 호출 직전까지 update 쿼리가 실행되지 않아 DB와 영속성 컨텍스트의 데이터 사이에 차이가 발생합니다.
하지만, 1번 findById 호출 시 수정된 데이터 값이 반환됨으로써, 마치 DB에 데이터들이 업데이트된 것처럼 실행되는 것을 알 수 있습니다.
flush를 통해 데이터를 db에 동기화 해준 이 후 2번째 findById 호출 시, cache에 저장된 id값으로 인해 select 쿼리 실행 없이도 Entity값을 가져오게 됩니다.
2. Transaction 종료될 경우
save 메서드의 경우 @Transactional이 선언되어 있으므로 각각의 save 메서드가 트랜잭션이 되므로 해당 메서드가 실행됨에 따라 바로바로 반영이되게 됩니다.
하지만, save를 호출하는 상위 메서드에서 @Transactional이 선언되어 있으면 해당 메서드가 트랜잭션이 되어, 전체 로직이 다 실행되고 트랜잭션이 끝나는 시점에 commit이 일어나며 auto flush가 발생합니다.
이처럼 하나의 Transaction이 끝나는 시점에 auto flush가 발생함으로써 DB에 반영되게 됩니다.
3. JPQL 쿼리가 실행될 경우 (복잡한 조건의 쿼리 실행 시)
@Transactional @Test void test2(){ User user = userRepository.findById(1L).get(); user.setName("ussser1"); userRepository.save(user); user.setEmail("ussser1@gmail.com"); userRepository.save(user); System.out.println(userRepository.findAll()); }
한 개의 user 데이터의 값을 수정하고 user의 데이터 값을 모두 조회하게 되면, 수정된 값이 좀 더 최신 값이기 때문에, 영속성 캐시와 db의 값을 서로 비교해서 최신 값을 사용하도록 해야합니다.
위와 같이 데이터를 비교해서 머지하는 것은 복잡한 과정이 될 것입니다. 이러한 과정을 피하기 위해 영속성 캐시에 있던 값을 db에 전부 반영을 한 뒤, db에 있는 값을 조회해서 영속성 컨텍스트에 가져오게 됩니다.
위의 Test를 실행하면 select문 전에 update 쿼리가 실행되는 것을 확인할 수 있습니다.
Hibernate: update user set email=?, name=? where id=? Hibernate: select user0_.id as id1_5_, user0_.email as email2_5_, user0_.name as name3_5_ from user user0_
Entity Lifecycle
Entity lifecycle에는 4가지가 있습니다. 비영속 상태 (New, Transient), 영속 상태(Managed), 준영속 상태(detached), 삭제 상태(removed).
1. 비영속 상태(New, Transient)
영속성 컨텍스트가 해당 Entity 객체를 관리하지 않는 상태입니다. Entity의 어노테이션 중 @Transient를 선언하게 되면 해당 필드는 영속성에서 제외되게 됩니다. 또한, 객체를 new를 통해 생성한 상태 또한 비영속 상태에 속하게 됩니다.
비영속 상태의 Entity 객체는 Entity라기 보다는 하나의 Java Object에 가깝습니다.
@Service public class UserService { @Transient public void put(){ User user = new User(); user.setName("user1"); user.setEmail("user1@gmail.com"); } } @Test void test(){ userService.put(); }
위에서 User라는 객체가 영속성 컨텍스트에 의해 관리되지 않고 있는 단순한 Java 객체로 존재하고 put()메서드가 종료하면 제거되게 됩니다.
2. 영속 상태(Managed)
해당 Entity가 영속성 컨텍스트에 의해 관리되고 있는 상태입니다.
영속 상태의 객체는 객체의 변화를 별도로 처리해주지 않아도 DB에 반영하게 됩니다.
@Transactional public void put(){ User user = new User(); user.setName("user1"); user.setEmail("user1@gmail.com"); userRepository.save(user); }
위의 put 메서드에 save메서드를 추가할 경우, save 메서드의 구현체에서 EntityManager에 의해 영속화됩니다.
@Autowired private EntityManger entityManager; @Transactional public void put(){ User user = new User(); user.setName("user1"); user.setEmail("user1@gmail.com"); entityManager.persist(user); }
실제 entityManager의 persist를 통해 영속화할 수 있습니다. 실제 insert문도 동작하게 되어 DB에도 반영됩니다.
entityManager의 persist 메서드를 통해 user 객체는 영속성 컨텍스트에서 관리하는 상태가 됩니다.
위와 같이 user객체가 영속성 컨텍스트에서 관리하는 상태가 된다면, 해당 객체의 값이 수정된다면 EntityManager에서 이를 알아서 DB에 반영하게 됩니다.
@Transactional public void put(){ User user = new User(); user.setName("user1"); user.setEmail("user1@gmail.com"); entityManager.persist(user); user.setName("useeeeeer1"); }
위와 같이 객체를 영속화 해준 뒤, 영속화된 객체의 값을 바꿀 경우 save메서드를 호출하지 않아도 update 쿼리도 실행되는 것을 알 수 있습니다.
// 출력 Hibernate: insert into user (id, email, name) values (null, ?, ?) Hibernate: update user set email=?, name=? where id=?
위와 같이 영속성 컨텍스트에 의해 관리되는 Entity의 경우 Entity 객체의 값이 수정된다면 Transition이 완료되는 시점에 별도로 DB데이터와의 정합성을 맞춰줍니다.
이러한 기능은 영속성 컨텍스트에서 제공해주는 dirty check 기능입니다. dirty check는 영속성 컨텍스트에서 가지고 있는 객체는 처음 객체를 로드할 경우 해당 정보를 스냅샷으로 가지고 있습니다. 이 후 변경 내용을 DB에 적용해야하는 시점에 스냅샷과 현재 Entity값을 일일이 비교해서 변경된 내용은 추가적으로 DB에 반영하게 됩니다.
3. 준영속 상태 (Detached)
영속화 되었던 객체를 분리해서 영속성 컨텍스트에서 꺼낸 상태입니다.
(EntityManager를 제외하고 JpaRepository에는 detach와 관련된 메서드가 없는데, 이는 영속성 컨텍스트에서 굳이 Entity를 분리할 필요가 없다는 것을 간접적으로 나타내는 것이기도 합니다.)
@Transactional public void put(){ User user = new User(); user.setName("user1"); user.setEmail("user1@gmail.com"); entityManager.persist(user); entityManager.detach(user); user.setName("useeeeeer1"); }
위와 같이 영속화된 객체를 다시 영속성 컨텍스트에서 분리하게 될 경우 맨 아래의 객체에 대한 수정이 DB에는 반영되지 않는 것을 확인할 수 있습니다.
detach에 의해 준영속 상태가 된 Entity라도 entityManager.merge() 메서드를 통해 다시 영속화된 상태가 될 수도 있습니다.
@Transactional public void put(){ User user = new User(); user.setName("user1"); user.setEmail("user1@gmail.com"); entityManager.persist(user); entityManager.detach(user); user.setName("useeeeeer1"); entityManager.merge(user); }
detach말고 clear, close 또한 영속성 컨텍스트에 들어가 있는 Entity를 영속성 컨텍스트 바깥으로 꺼낼 수 있습니다. (clear, close는 detach보다 파괴적인 메서드이긴 합니다.)
4. 삭제(removed)
더 이상 사용하지 못하는 상태의 객체를 말합니다.
@Transactional public void put(){ User user = new User(); user.setName("user1"); user.setEmail("user1@gmail.com"); entityManager.persist(user); user.setName("useeeeeer1"); User user1 = userRepository.findById(1L).get(); entityManager.remove(user1); }
remove 메서드에 의해 1번 id인 Entity는 DB상에서 제거됩니다. 제거된 Entity는 다시 사용할 수 없습니다.
오늘은 영속성 컨텍스트와 이에 따른 Entity Cache와 Entity LifeCycle에 대해 알아보았습니다. 좀 더 자세한 내용이 나오는만큼 실제 동작하는 부분에 대해 신경 쓰고 공부를 해보아야 될 것 같습니다. 다음 포스팅에서는 Transaction에서 더 자세히 다뤄보도록 하겠습니다.
반응형LIST'백엔드' 카테고리의 다른 글
Spring IOC, DI, AOP (0) 2023.08.26 JPA Transaction (0) 2023.08.25 Jpa 연관관계 살펴보기 (N:N) (0) 2023.08.23 JPA 연관관계 살펴보기 (1:1, 1:N, N:1) (4) 2023.08.21 Entity annotation, Listener (1) 2023.08.19