티스토리 뷰

JPA가 도입된 배경

SQL을 한 땀 한 땀 작성하는 것은 효율적이지 못합니다. CRUD 하나 만드는데만 INSERT INTO, UPDATE, SELECT .. 등등의 많은 쿼리문들을 손으로 직접 작성하여 디비에 쿼리를 날리는 것은 무한 반복작업입니다. 만약 테이블을 다 짜고 SQL 구문까지 다 작성했는데 갑자기 수정사항이 발생하면 쿼리 모두를 수정해야하는 경우가 생깁니다. 즉 SQL에 의존적인 개발을 피할 수 없었습니다.

 

수십줄의 코드가 단 한 두줄로 !

-> 수십 개 이상의 복잡한 객체와 테이블을 사용합니다. 그러므로 객체 테이블을 정확하게 설계하고 매핑하는 것이 중요합니다.

-> JPA 내부 동작 방식을 반드시 이해해야합니다. JPA가 어떤 SQL을 만들고 언제 SQL을 실행하는지 !


JPA에서 가장 중요한 2가지

1. 객체와 관계형 데이터베이스 매핑

 

2. 영속성 컨텍스트

JPA를 이해하는데 가장 중요한 용어는 '영속성 컨텍스트'입니다. 엔티티를 영구 저장하는 환경이라는 뜻입니다.

영속성 컨텍스트는 논리적인 개념이라고 보시면 됩니다. 눈에 보이는 것이 아닌 개념적인 접근입니다.

엔티티의 생명주기가 몇 가지 있습니다.

  • 비영속: 영속성 컨텍스트와 전혀 관계가 없는 새로운 관계
  • 영속: 영속성 컨텍스트에 의해 관리되는 상태
  • 준영속: 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제: 삭제된 상태

영속성 컨텍스트의 이점이 몇 가지가 있습니다.

  1. 1차 캐시
  2. 동일성 보장
  3. 트랙잭션을 지원하는 쓰기 지연
  4. 변경 감지(Dirty Checking)
  5. 지연 로딩(Lazy Loading)
Member member = new Member();
member.setName("kim");
member.setId(1L);
// 여기까지 객체만 생성(비영속)

EntityManger em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(member);
// 객체를 저장한 상태(영속)

em.detach(member);
// 회원 엔티티를 영속성 컨텍스트에서 분리(준영속)

em.remove(member);

 

쓰기 지연

영속성 컨텍스트는 내부에 1차 캐시를 두고 있습니다. Map이랑 비슷한 구조로 되어있다고 생각하시면 편한데, 데이터베이스에 저장하기 전 <PK, Object>의 형태로 저장된다고 할 수 있습니다. 예를들어 아래의 코드에서 새로운 멤버를 저장하려고 하는 경우 데이터베이스에 저장되기 전 1차캐시에 <1L, member>로 저장되어 있다고 할 수 있습니다. 1차 캐시는 트랙젝션 한 싸이클 내부단위로 동작합니다.

EntityManger em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();

transaction.begin();

em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 디비에 보내지 않는다

transaction.commit();
// 커밋하는 순간 디비에 쿼리를 날린다

쓰기 지연 저장소 개념에 관한 설명 - 인프런 JPA 기본편 참고

그러면 이런 1차 캐시를 왜 두는지에 궁금점이 생길 수 있습니다 ! 결론적으로 말하면 데이터베이스에 연결되는 네트워크 오버헤드를 줄인다는 것입니다. 만약 한 트랙젝션 단위에 객체를 5개 생성하여 데이터베이스에 등록하는데 각 객체가 생성될 때 마다 디비와 연결한다고 하면 디비와의 5번의 연결이 발생합니다.

그러므로 트랙젝션이 커밋될 때 까지 쿼리를 날리지 않고 쓰기지연 저장소에 담아둬 커밋하는 순간 쓰기지연 저장소에 있는 쿼리를 날립니다.

 

 

그러나 디비에서 객체를 찾아서 가져와야 되는 경우는 커밋이 발생하기 전에 쿼리를 날려 값을 받아와야되지 않나? 라는 생각이 들 수 있습니다. find를 이용해 디비에서 객체를 찾는 경우에도 마찬가지로 1차 캐시를 이용한다고 볼 수 있습니다.

디비에서 Lee라는 멤버를 찾아 이름을 변경한다고 예를들어 보겠습니다.

1. em.find 함수를 통해 1차 캐시에 찾으려고 하는 객체가 있는지 없는지 확인합니다.

2. 1차 캐시에 객체가 없으면 디비를 조회합니다. (1차 캐시에 있으면 캐시에서 객체를 가져옵니다)

3. 가져온 객체를 1차 캐시에 저장합니다.

4. Lee라는 멤버 객체를 반환하고 이후부터는 Lee라는 멤버를 캐시에서 가져올 수 있습니다.

 

변경 감지(Dirty Checking)

디빋에서 객체를 가져온 그 순간의 객체 모습을 영속 컨텍스트에 스냅샷을 떠서 저장해둡니다. 즉 그 순간의 객체의 정보를 1차 캐시에 저장해둔다는 것입니다. 위 예제에서 find를 이용해 ID가 1인 Lee 멤버를 디비에서 가져왔습니다. 이 멤버의 이름을 Kim으로 변경한다고 하면 다시 persist를 통해 디비에 등록을 해야할까요? 결론은 아닙니다. 

객체를 가져온 이후에 해당 객체 값을 변경하고 트랙젠션이 커밋하여 flush*가 호출되면 JPA가 엔티티와 스냅샷을 비교합니다.

비교해보고 객체의 값이 바뀌었으면 쓰기 지연 저장소에 UPDATE 쿼리를 저장합니다.

transaction.begin();

Member member = em.find(Member.class, 1L);
member.setName("Kim"); // Lee -> Kim

// em.persist(member); 이걸 해야할까 ?!
transaction.commit();

// -> 쓸 필요가 없다. 왜? 생각을 해보자. 자바 컬렉션에서 객체를 가져오고 객체의 값을 수정하고나서 다시 객체를 저장하지 않는다.
  • flush: 영속성 컨텍스트의 변경내용을 데이터베이스에 반영하는 것
    1. 변경 감지
    2. 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
    3. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송
  • 영속성 컨텍스트를 flush 하는 방법
    1. em.flush()
    2. transaction.commit()
    3. JPQL 쿼리 실행시 -> flush가 먼저 자동으로 호출되고 이후에 해당 쿼리가 날라갑니다
댓글