What we have to do is to be forever curiously
testing new opinions and courting new impressions

우리가 해야 할 일은 끊임없이 호기심을 갖고
새로운 생각을 시험해보고 새로운 인상을 받는 것

JPA에 대하여

  • ORM이란
  • JPA란
  • 동작 과정
  • 사용 이유
  • 지연 로딩 vs 즉시 로딩


ORM이란

ORM(Object-relational mapping, 객체 관계 매핑)

  • 객체는 객체대로 설계
  • 관계형 데이터베이스는 관계형 데이터베이스대로 설계
  • ORM 프레임워크가 중간에서 매핑


JPA란

hibernate

EJB

  • 과거의 자바 표준 ORM(Entity Bean)
  • API의 복잡성이 높고 지저분한 코드 → 느린 속도 JPA(Java Persistence API, 자바 영속성 API)
  • 현재 자바 진영의 ORM 기술 표준
  • 인터페이스의 모음
  • JPA 2.1 표준 명세를 구현한 3가지 구현체(Hibernate, EclipseLink, DataNucleus) Hibernate
  • ORM 프레임워크, Open Source SW
  • EJB스타일의 Entity Beans 이용을 대체할 목적으로 개발 Spring Framework
  • Application 프레임워크, Open Source SW
  • EJB의 여러 문제를 해결하고, 쉽게 엔터프라이즈 애플리케이션을 만들기 위한 목적으로 개발


동작 과정

jpa-basic-structure

jpa-insert-structure

jpa-select-structure

JPA는 애플리케이션과 JDBC 사이에서 동작

  • 개발자가 JPA를 사용하면, JPA 내부에서 JDBC API를 사용하여 SQL을 호출하여 DB와 통신
  • 직접 JDBC API를 쓰는 것이 아님
  • 쿼리를 JPA가 만들어 주기 때문에 Object와 RDB 패러다임 불일치 해결

코드 구조

EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabegin");
EntityManager entityManager = emf.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
try {
   transaction.begin();

   User user = new User("user@user.com", "user", LocalDateTime.now());
   entityManager.persist(user);

   transaction.commit();
} catch (Exception ex) {
   ex.printStackTrace();
   transaction.rollback();
} finally {
   entityManager.close();
}
emf.close();

영속 단위를 구분하는 식별자 이름을 사용해서 엔티티매니저팩토리 생성

커넥션 풀, 디비 연동에 필요한 자원을 생성하는 작업

  1. 팩토리로 엔티티매니저 생성(식별자별 영속 컨텍스트 매핑)
  2. 엔티티트랜잭션 구함
  3. 트랜잭션 시작
  4. 트랜잭션 커밋
  5. 트랜잭션 롤백
  6. 엔티티매니저 닫기(자원 반환)

저장과 쿼리 실행 시점

transaction.begin();
User user = new User("user@user.com", "user", LocalDateTime.now());
entityManager.persist(user);
logger.info("EntityManager.persist 호출함");
transaction.commit(); // insert query 실행
logger.info("EntityTransaction.commit 호출함"); 

수정과 쿼리 실행 시점

transaction.begin();
User user = entityManager.find(User.class, "user@user.com"); // select query 실행
if (user == null) {
   System.out.println("User 없음");
} else {
   String newName = "이름" + (System.currentTimeMillis() % 100);
   user.changeName(newName);
   logger.info("User.changeName 호출함");
}
transaction.commit(); // update query 실행
logger.info("EntityTransaction.commit 호출함");

엔티티매니저 단위로 영속 컨텍스트를 관리

DB에서 읽어온 데이터는 바로 반영되는 것이 아니라 영속 컨텍스트(메모리)에 저장되어 있다가, 커밋 시점에 영속 컨텍스트에 변화가 발생되어 있다면 추적하여 그 변경 내역에 해당하는 내용을 반영(insert, update)을 실행하는 쿼리를 전송

예시

예를 들어 findById()로 JPA를 통해서 DB 정보를 조회하는 경우 다음과 같은 작업이 실행

JPA는 EntityManager이란 클래스를 사용해서 엔티티를 관리

EntityManager은 영속성 컨텍스트를 내부에 두어 DB에 가기전에 이 값이 조회된 값인지 영속성 컨텍스트를 확인

만약 값이 있으면 DB까지 가지 않고 객체를 바로 반환

없으면 SELECT SQL문을 생성 → JDBC API를 거쳐 DB에 전달 → 결과를 result 객체에 매핑(영속성 컨텍스트에 저장)


사용 이유

  1. SQL 중심적인 개발에서 객체 중심으로 개발
  2. 생산성 향상(간단한 CRUD)
  3. 유지보수
    1. 기존 : 필드 변경 시 모든 SQL을 수정
    2. JPA : 필드만 추가
  4. Object와 RDB 간의 패러다임 불일치 해결
  5. JPA의 성능 최적화 기능 : 모아서 쓰는 버퍼링, 읽을 때 쓰는 캐싱
    • 1차 캐시와 동일성(identity) 보장(캐싱 기능)
      • 같은 트랜잭션 안에서는 같은 엔티티를 반환
         String memberId = "100"; 
         Member m1 = jpa.find(Member.class, memberId); // SQL 
         Member m2 = jpa.find(Member.class, memberId); // 캐시 (SQL 1번만 실행, m1을 가져옴)
         println(m1 == m2) // true
        
    • 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)(버퍼링 기능)
      • INSERT SQL 쌓기
         /** 1. 트랜잭션을 커밋할 때까지 INSERT SQL을 모음 */
         transaction.begin(); // [트랜잭션] 시작
         em.persist(memberA);
         em.persist(memberB);
         em.persist(memberC); 
         // -- 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
         // 커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다.
         /** 2. JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송 */
         transaction.commit(); // [트랜잭션] 커밋
        
      • UPDATE
         /** 1. UPDATE, DELETE로 인한 로우(ROW)락 시간 최소화 */
         transaction.begin();  // [트랜잭션] 시작
         changeMember(memberA);
         deleteMember(memberB);
         비즈니스_로직_수행();     // 비즈니스 로직 수행 동안 DB 로우 락이 걸리지 않는다.
         // 커밋하는 순간 데이터베이스에 UPDATE, DELETE SQL을 보낸다.
         /** 2. 트랜잭션 커밋 시 UPDATE, DELETE SQL 실행하고, 바로 커밋 */
         transaction.commit(); // [트랜잭션] 커밋
        


지연 로딩 vs 즉시 로딩

  1. 지연 로딩(Lazy Loading)
    • 객체가 실제로 사용될 때 로딩하는 전략
    • memberDAO.find(memberId)에서는 Member 객체에 대한 SELECT 쿼리만 날린다.
    • Team team = member.getTeam()로 Team 객체를 가져온 후에 team.getName()처럼 실제로 team 객체를 건드릴 때!
      • 즉, 값이 실제로 필요한 시점에 JPA가 Team에 대한 SELECT 쿼리를 날린다.
    • Member와 Team 객체 각각 따로 조회하기 때문에 네트워크를 2번 타게 된다.
      • Member를 사용하는 경우에 대부분 Team도 같이 필요하다면 즉시 로딩을 사용한다.
  2. 즉시 로딩(Eager Loading)
    • JOIN SQL로 한 번에 연관된 객체까지 미리 조회하는 전략
    • Join을 통해 항상 연관된 모든 객체를 같이 가져온다.
    • 애플리케이션 개발할 때는 모두 지연 로딩으로 설정한 후에, 성능 최적화가 필요할 때에 옵션을 변경하는 것을 추천


댓글남기기