[JPA] JPA 동작원리

LDB·2024년 11월 8일
1

JPA 기본

목록 보기
2/10
post-thumbnail

JPA 동작원리

앞으로 JPA를 설명하기 위해서는 "영속성 컨텍스트"에 대해 알 필요가 있다고 판단해 글을 작성하려고한다.

영속성 컨텍스트는 간단히 이야기 하면 엔티티를 영구 저장하는 환경 이라고 보면된다. 만약 Spring Data JPA를 사용했다면 EntityManager를 직접 생성할 필요는 없다. 하지만 영속성 컨텍스트와 EntityManager는 직접적인 연관이 있기 때문에 EntityManager에 대한 설명이 필요하다.

EntityManager안에는 영속성 컨텍스트라는 것이 있는데 정확히는 EntityManager를 생성하면 영속성 컨텍스트가 1:1로 생성된다. 그리고 EntityManager를 통해 영속성 컨텍스트에 접근할수 있다.

  • Persistence : 엔티티를 영구적으로 저장하는 환경
  • persistence.xml : JPA 설정파일
  • EntityManagerFactory : EntityManager를 생성하는 클래스
  • EntityManager : Entity객체를 관리하는 클래그

엔티티 생명주기

  1. 비영속 (new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태 ( JPA와 관계가 없는 상태 )
  2. 영속 (managed) : 영속성 컨텍스트에 관리되는 상태 ( em.persist(entity) 로 객체를 저장한 상태 )
  3. 준영속 (detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태 ( em.detach(entity) )
  4. 삭제 (removed) : 삭제된 상태 ( em.remove(entity) )

(준영속 상태로 만들게 되면 영속성 컨텍스트가 제공하는 기능을 사용하지 못한다.)

영속성 컨텍스트 이점

이렇게 복잡한 방식으로 영속성 컨텍스트를 사용하는데에는 이유가 있다.

1차 캐시

  • EntityManager는 1차 캐시를 가지고 있다.
  • EntityManager를 통해 Entity를 영속성 컨텍스트에 등록하면 1차 캐시에 등록이 된다.
  • Entity Manager를 통해 Entity를 조회하는 경우, DB를 조회하기 전에 1차 캐시를 우선 조회한다.
  • 1차 캐시를 우선으로 조회하는 경우, 응답속도가 훨씬 빠르다는 장점을 가지고 있다.
  • 1차 캐시에 존재하지 않는 Entity를 조회하는 경우 DB에 접근하여 조회한다.

1차 캐시로 데이터를 얻어오는 과정을 그림으로 표현하자면 이렇게 정리할 수 있겠다.

  1. id 고유값으로 엔티티 조회
  2. 조회하려는 엔티티가 1차 캐시에 존재하지 않을 경우 데이터베이스에 접근한다.
  3. 조건과 일치하는 값을 1차 캐시로 반환한다.
  4. 1차 캐시에 존재하는 엔티티를 반환한다.

1차 캐기 예시 코드 및 결과

	@Test
	@DisplayName("JPA 1차 캐시 테스트")
	@Transactional
	public void oneCache(){

		System.out.println("1차 캐시에 존재하지 않을경우");
		springjpaRepository.findById(1).get();
		System.out.println("1차 캐시에 존재 할 경우");
        springjpaRepository.findById(1).get();

	}


(실제로 테스트 코드상으로 하나의 트랜잭션으로 묶어 조회를 시도하면 처음에 조회한 엔티티 값이 1차 캐시에 저장되어있기 때문에 두 번째에는 DB로 가지 않고 1차 캐시안에 있는 값을 리턴한다.)


주의점

  • 1차 캐시는 하나의 트랜잭션이 시작하면서 생성되고 종료되면서 삭제된다. 
  • 애플리케이션 전체가 공유하는 캐시를 2차 캐시라고 한다.

영속 엔티티의 동일성 보장

Member findMember1 = em.find(Member.class, 1L);
Member findMember2 = em.find(Member.class, 1L);
 
System.out.printf("결과 = " + (findMember1 == findMember2)); // true
  • 자바의 관점에서 살펴보면 findMember1와 findMember2는 서로 다른 주소 값을 갖는 객체다.
  • 하지만 JPA는 하나의 트랜잭션 안에서 조회되는 동일한 객체에 대해서 같은 객체로 처리하기 때문에 동일성이 보장된다.

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

영속성 컨텍스트에는 1차 캐시 뿐만 아니라 쓰기 지연 SQL 저장소가 존재한다, commit()을 수행하기 전까지는 EntityManager가 SQL을 작성하지도, DB에게 전달하지도 않는다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
EntityManager em = emf.createEntityManager();
EntityTransaction tr = em.getTransaction(); // 트랜잭션 생성
      
tr.begin(); // 트랜잭션 시작 

// insert 쿼리가 생성되고 영속성 컨텍스트 내부의 쓰기 지연 SQL 저장소에 쌓인다.
em.persist(userA); 

// insert 쿼리가 생성되고 영속성 컨텍스트 내부의 쓰기 지연 SQL 저장소에 쌓인다.
em.persist(userB); 
      
tr.commit(); // SQL이 DB에게 명령이 전달되고 결과를 저장한다.
  • 실제로 한 번의 커밋을 트랜잭션을 의미하는데 즉 DB의 값이 변경되는 작업의 처리단위를 의미한다.
  • 만약 쿼리 하나당 한번의 작업이 수행된다고 했을 때 10개의 쿼리를 수행하기 위해서는 10번의 커밋이 필요하다.
  • 만약 10000번의 쿼리를 10000번 수행한다면 DB와 10000번 통신이 필요하다.
  • 하지만 쿼리를 10개씩 묶어서 처리한다면 1000번만 수행하면 되기 때문에 네트워킹 횟수가 현저히 줄어들고 서비스의 부하가 감소하게 된다.

변경 감지(더티 체킹)

Spring Data JPA를 사용하면 save() 메서드는 있지만 update() 메서드는 없다. 또한 데이터를 변경할 때 save 하지 않아도 값이 저장되는 것을 볼 수 있다. 이게 가능한 이유는 바로 JPA의 더티 체킹 때문이다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
EntityManager em = emf.createEntityManager();
EntityTransaction tr = em.getTransaction(); // 트랜잭션 생성
      
tr.begin(); // 트랜잭션 시작 

User user1 = em.find(User.class, "userA"); // DB에 저장된 userA를 찾는다.
      
user1.setName("HELLO");
      
tr.commit(); // user1의 name이 변화되었기 때문에 자동으로 UPDATE를 한다.

위의 코드에서 persist()을 하지 않았음에도 동작이 되는 이유는 1차 캐시를 사용했기 때문이다. 하단에 처리순서를 작성해 두겠다.

  1. userA를 조회하고 user1에 저장한 시점에 user1의 객체 값이 1차 캐시에 저장이된다. 그리고 조회한 값을 1차 캐시의 snapshot 속성에 저장한다.

  2. user1의 name에 새로 값을 추가한 후 트랜잭션에서 commit을 하는데 이시점에 flush를 호출하고 user1의 값과 1차 캐시의 snapshut을 비교하여 데이터의 변경을 감지한다.

  3. 데이터의 수정이 감지되면 자동으로 update쿼리가 생성되고 쓰기 지연SQL 저장소에 전달한다.

  4. 쓰기 지연SQL 저장소에 쌓인 쿼리를 DB에 전달한다.

(하지만 준영속, 비영속상태의 엔티티는 더티체킹의 대상이 아니다.)

JPA사용하는 이유 정리

길게 설명했지만 간단히 JPA를 사용하는 이유를 설명하면 이렇게 말할 수 있겠다.

  • 1차 캐시 사용으로 응답속도가 상승한다.
  • 동일성이 보장된다.
  • 서비스 부하가 감소된다.

플러시(flush)

플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다. 플러시를 한다고 해서 영속성 컨텍스트의 내용을 지우고 DB에 보내는 것이 아니다. 즉, 영속성 컨텍스트의 변경 내용을 데이터베이스 동기화하는 것이라 할 수 있다.

플러시 사용법

  • em.flush()로 직접 호출한다.
  • 트랜잭션 commit 시 플러시는 자동 호출 된다.
  • JPQL 쿼리 실행 시 플러시는 자동 호출 된다.

플러시 발생 시점

  • 더티 체킹으로 변경이 감지된 경우
  • 수정된 Entity를 쓰기 지연SQL 저장소에 등록한 경우
  • 쓰기 지연SQL 저장소의 쿼리를 데이터베이스에 전송한 경우

영속성 컨텍스트의 내용 DB반영 순서

1. 트랜잭션 시작
스프링 트랜잭션 AOP가 트랜잭션을 시작한다, 이때 @Transactional 어노테이션이 붙은 메소드가 호출되면 스프링 AOP는 트랜잭션을 시작하는 프록시 로직을 수행한다.

2. 메서드 실행
비즈니스 로직이 수행되며, 이 과정에서 엔티티의 상태가 변경될 수 있다, 변경된 엔티티는 영속성 컨텍스트 내에서 관리.

3. 메서드 종료
비즈니스 로직이 종료되고 메서드가 리턴, 이 시점에서 트랜잭션이 아직 commit되지 않았기 때문에 변경 내용은 데이터베이스에 반영되지 않는다.

4. 트랜잭션 커밋
스프링 AOP 트랜잭션 인터셉터가 트랜잭션을 커밋한다, 이 때 JPA는 영속성 컨텍스트를 flush한다.

5. JPA 플러시
JPA가 영속성 컨텍스트를 플러시하며, 변경된 엔티티에 대한 SQL 문이 데이터베이스에 전송된다, 이 과정에서 실제 데이터베이스에 쓰기 작업이 발생한다.

6. DB 트랜잭션 커밋
JPA 플러시 이후, 데이터베이스 트랜잭션이 커밋되며 변경 내용이 영구적으로 저장된다.


참고 사이트

https://hstory0208.tistory.com/entry/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EB%9E%80

https://devraphy.tistory.com/513

https://velog.io/@fishphobiagg/JPA-Spring-Boot-동시성-제어하기-feat.상품-주문-서비스-영속성-컨텍스트-이해하기

profile
가끔은 정신줄 놓고 멍 때리는 것도 필요하다.

0개의 댓글

관련 채용 정보