본문 바로가기

Spring/JPA

영속성 컨텍스트

객체와 관계형 데이터베이스를 매핑하는 것과 영속성 컨텍스트의 개념은 JPA에서 가장 중요한 두 가지입니다. 실제 JPA가 내부에서 어떻게 동작하는지 알기 위해서는 영속성 컨텍스트를 이해해야 합니다.

 

영속성 컨텍스트는 엔티티를 영구 저장하는 환경이라는 뜻으로 엔티티를 저장하고 관리합니다. 또한 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩 기능을 제공합니다. 영속성 컨텍스트가 제공하는 기능을 제공하기 전에 영속성 컨텍스트에 접근하도록 도와주는 엔티티 매니저 팩토리와 엔티티 매니저에 대해 살펴봅시다.

 

EntityManagerFactory & EntityManager

 

엔티티 매니저 팩토리는 DB당 하나만 생성하여 애플리케이션 전체에서 공유합니다. 위 그림처럼 엔티티 매니저 팩토리에서 각 스레드별로 엔티티 매니저를 생성해 줍니다. 엔티티 매니저가 생성되면서 영속성 컨텍스트도 함께 생성됩니다. 영속성 컨텍스트는 논리적인 개념이고, 엔티티 매니저를 통해 영속성 컨텍스트에 접근하게 됩니다. 엔티티 매니저는 절대 스레드 간 공유가 되어서는 안 되고, 내부적으로 데이터베이스 커넥션을 물고 동작하기 때문에 사용한 이후 꼭 close 해줘야 합니다.

 

아래 코드와 같이 직접 엔티티 매니저 팩토리와 엔티티 매니저를 생성해서 사용할 있지만, 실제로는 직접 생성할 일이 없습니다.

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

tx.begin();
try {
	Member findMember = em.find(Member.class, 1L);
	findMember.setName("hong");
	tx.commit();
} catch (Exception e) {
	tx.rollback();
} finally {
	em.close();
}

 

위 코드에서 tx라는 EntityTransaction 타입의 변수를 사용한 것을 볼 수 있습니다. JPA의 모든 데이터 변경은 트랜잭션 안에서 실행해야 합니다. 따라서 위에서도 tx.begin을 호출한 이후 엔티티 데이터를 조회하고 변경했습니다.

다음으로 엔티티의 생명주기를 살펴봅시다.

 

엔티티 생명주기

엔티티는 new로 생성되고 persist 함수를 통해 영속성 컨텍스트에 저장되며 영속 상태가 됩니다. 이후에 트랜잭션 커밋이 발생하여 영속성 컨텍스트가 플러쉬 될 때 DB에 영속성 컨텍스트의 엔티티를 저장하는 쿼리를 날리게 됩니다. 영속성 컨텍스트가 관리하지 않는 준영속 상태의 엔티티도 존재하고, 준영속 엔티티의 경우 영속성 컨텍스트가 제공하는 여러 기능을 사용하지 못합니다. 아래 글에 엔티티의 생명주기를 더 자세히 다뤘으니 참고하면 좋을 것 같습니다.

 

https://preyhong.tistory.com/53

 

서비스 구현 - 2. 회원 도메인 구현 (영속성 컨텍스트, Entity 생명주기, 트랜잭션)

preyhong.tistory.com

 

다음으로는 영속성 컨텍스트를 사용해 엔티티를 관리할 때 JPA가 제공하는 이점들을 살펴봅시다. 영속성 컨텍스트의 이점으로는 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 그리고 지연 로딩이 있습니다.

 

영속성 컨텍스트의 이점

1차 캐시

먼저 1 캐시를 살펴봅시다. 지금까지는 persist 통해 엔티티를 영속성 컨텍스트에 저장한다고 설명했지만, 자세히는 영속성 컨텍스트의 1 캐시에 @Id Entity 쌍으로 저장하게 됩니다. 따라서 em.find 통해 엔티티를 DB에서 조회하려 JPA 우선 1 캐시에 해당 엔티티가 있는지 찾습니다. 1 캐시에 찾으려는 엔티티가 없을 경우 DB SELECT 쿼리를 날려 엔티티를 조회하고 조회한 엔티티를 1 캐시에 저장합니다. 그러고 1 캐시에 저장된 엔티티를 반환하게 되는 방식입니다

조회하려는 엔티티가 1차 캐시에 없을 경우

 

하지만 사실 큰 성능적 이점을 기대하기는 어렵습니다. DB 트랜잭션이 커밋되는 순간에 영속성 컨텍스트가 지워지면서 1차 캐시도 함께 지워지기 때문입니다. 엔티티 매니저는 DB 트랜잭션의 생명주기와 함께 생성되고 종료됩니다. 클라이언트의 요청이 들어올 때 영속성 컨텍스트의 1차 캐시가 생성되고, 비즈니스 로직이 끝나면 영속성 컨텍스트의 1차 캐시도 함께 지워집니다. 따라서 여러 클라이언트에는 각각 1차 캐시를 가진 영속성 컨텍스트를 할당받습니다. 다시 말해 1차 캐시가 하나의 DB 트랜잭션 안에서만 공유하기 때문에 1차 캐시에서 제공하는 캐싱의 성능은 크지 않습니다.

 

애플리케이션 전체에서 공유하는 캐시는 2차 캐시라고 불립니다. 성능적 이점을 누리기 위해서는 2차 캐시를 활용해야 합니다. 하지만 1차 캐시의 동작 메커니즘을 이해하여 엔티티가 저장되고 조회되는 과정을 이해할 수 있기 때문에 영속성 컨텍스트의 중요한 특징 중 하나입니다.

 

영속 엔티티의 동일성 보장

1 캐시의 동작 방식 덕분에 같은 트랜잭션 내에서 영속 엔티티의 동일성을 보장할 있습니다. 1 캐시로 반복 가능한 읽기 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공합니다. 아래 코드로 살펴봅시다.

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");

System.out.println(a == b);	//동일성 비교 true

 

a 변수를 통해 member1 Id인 엔티티를 DB에 조회하여 1차 캐시에 저장한 이후 반환하게 됩니다. 두 번째로 b 변수에서 member1 Id인 엔티티를 조회할 때는 DB에서 조회하지 않고 1차 캐시에 이미 저장되어 있어 DB까지 조회하지 않고 1차 캐시의 엔티티를 반환하게 됩니다. 따라서 1차 캐시를 통해 두 번 엔티티를 조회했지만, 한 번의 SELECT 쿼리만 DB에 날릴 수 있습니다.

 

트랜잭션을 지원하는 쓰기 지연

JPA 클라이언트와 DB 사이에서 동작합니다. 따라서 버퍼와 같은 기능도 제공합니다. 엔티티를 영속성 컨텍스트에 persist 하더라도 즉시 DB INSERT 쿼리가 나가는 아닙니다. 영속성 컨텍스트에 엔티티의 저장 또는 수정 사항을 모아뒀다가 트랜잭션이 커밋되는 시점에 영속성 컨텍스트가 플러쉬 되며 DB 번의 네트워크 통신으로 쿼리를 날립니다. 아래 코드로 살펴봅시다.

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 합니다.
transaction.begin(); // [트랜잭션] 시작

em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않습니다.

//커밋하는 순간 데이터베이스에 INSERT SQL을 보냅니다.
transaction.commit(); // [트랜잭션] 커밋

 

트랜잭션이 커밋되기 전까지 영속성 컨텍스트의 쓰기 지연 SQL 저장소에 엔티티 정보들을 SQL 형태로 쌓아두다가 트랜잭션이 커밋되면 번에 SQL DB 날립니다. 아래 그림으로 엔티티를 영속성 컨텍스트에 persist 했을 동작 과정을 살펴봅시다.

em.persist(memberA)

 

memberA 엔티티를 persist 하면 영속성 컨텍스트의 1 캐시에 저장됨과 동시에 SQL 형태로 쓰기 지연 SQL 저장소에 저장됩니다. 다음으로 memberB 엔티티를 persist 해보겠습니다.

em.persist(memberB)

 

memberB 엔티티를 persist 때도 역시 1 캐시 저장되고 SQL 형태로 쓰기 지연 SQL 저장소에 저장됩니다. 이제 트랜잭션을 커밋해 보겠습니다.

transaction.commit()

 

hibernate.jdbc.batch_size 설정하면 해당 사이즈만큼 쿼리를 모아서 DB 번의 네트워크 통신으로 날립니다. 이와 같이 영속성 컨텍스트는 엔티티들이 DB 저장되기 전에 버퍼 역할을 합니다.

 

변경 감지(Dirty Checking)

영속성 컨텍스트가 관리하는 영속 엔티티에 변경이 발생하면 이를 감지하고 변경 사항에 해당하는 SQL 문을 생성해 쓰기 지연 SQL 저장소에 저장합니다. 영속 엔티티의 변경 사실을 감지하기 위해 영속성 컨텍스트의 1 캐시로 최초로 들어온 시점의 상태를 스냅샷으로 저장합니다. 트랜잭션을 커밋하는 시점에 영속성 컨텍스트가 플러쉬 되며 엔티티와 스냅샷을 비교하여 UPDATE SQL 생성하여 쓰기 지연 SQL 저장소에 저장합니다. 이후에 쓰기 지연 SQL 저장소에 쌓인 SQL DB 날려 데이터를 업데이트합니다. 아래 코드로 살펴봅시다.

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // [트랜잭션] 시작

// 영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");

// 영속 엔티티 데이터 수정
memberA.setUsername("hi");
memberA.setAge(10);

//em.update(member) 이런 코드가 있어야 하지 않을까?

transaction.commit(); // [트랜잭션] 커밋

 

em.update(member) 통해 직접 업데이트하지 않아도, 영속성 컨텍스트의 변경 감지 기능을 통해 변경 사항이 자동으로 DB 적용됩니다. 아래 그림으로 변경 감지의 동작 과정을 살펴봅시다.

 

memberA.setUsername("hi")와 memberA.setAge(10)에 의해 변경된 엔티티를 1차 캐시의 Entity 영역에 저장하고, 스냅샷 영역에는 처음 memberA 엔티티가 영속성 컨텍스트의 1차 캐시에 들어온 상태를 유지합니다. 이를 통해 Entity와 스냅샷의 엔티티를 비교하여 UPDATE SQL을 생성할 수 있게 됩니다.

 

JPA 활용할 DB 데이터를 수정하는 방식은 영속 엔티티를 통해 변경 감지를 사용하거나 직접 merge 함수를 호출하는 방식으로 나뉩니다. 결론적으로 영속 엔티티를 통해 변경 감지를 사용하는 방식을 추천합니다. 아래 글에 자세히 설명했으니 참고하면 좋을 같습니다.

 

https://preyhong.tistory.com/57

 

서비스 개발 - 5. 웹 계층 Controller 개발 (변경 감지와 병합)

preyhong.tistory.com

 

마지막으로 엔티티를 삭제하는 경우를 짧게 살펴봅시다.

 

엔티티 삭제

엔티티를 삭제할 때도 영속 엔티티를 삭제한다면 DELETE 쿼리가 쓰기 지연 SQL 저장소에 쌓이며 트랜잭션이 커밋되는 시점에 DB 쿼리가 나갑니다. 아래 코드와 같이 엔티티를 삭제할 있습니다.

//삭제 대상 엔티티 조회 
Member memberA = em.find(Member.class, “memberA");
em.remove(memberA); //엔티티 삭제

 

변경 감지와 엔티티 삭제 모두 엔티티 매니저(em)에서 find 함수를 통해 영속성 컨텍스트의 엔티티를 반환받고 있습니다. 때문에 반환받는 엔티티의 상태가 모두 영속 상태가 됩니다. persist로 영속성 컨텍스트에 넣어도 영속 상태의 엔티티이지만, find로 DB의 엔티티를 조회해서 1차 캐시에 엔티티를 넣는 것도 영속 상태의 엔티티입니다.

 

영속 상태의 엔티티에서는 영속성 컨텍스트가 제공하는 기능이 사용되지만 그렇지 않은 준영속 상태 엔티티의 경우는 영속성 컨텍스트가 제공하는 기능이 사용되지 않습니다. 준영속 상태는 영속 상태의 엔티티가 영속성 컨텍스트에 분리(detached) 상태입니다. detach 함수를 사용해 엔티티를 준영속 상태로 만들 있고 clear 함수와 close 함수를 사용해도 엔티티를 준영속 상태로 만들 있습니다. 아래 코드로 살펴봅시다.

em.detach(entity); // 특정 엔티티만 준영속 상태로 전환합니다.
em.clear(); // 영속성 컨텍스트를 완전히 초기화합니다.
em.close(); //영속성 컨텍스트를 종료합니다.

'Spring > JPA' 카테고리의 다른 글

일대일 연관관계 매핑  (1) 2024.10.17
연관관계 매핑 기초  (1) 2024.10.17
기본키 매핑  (0) 2024.10.17
영속성 컨텍스트 플러시  (1) 2024.10.16
JPA가 등장한 배경  (0) 2024.10.16