JPA(Java Persistence API) - 영속성이란? , 영속성 컨텍스트

Hailey·2025년 3월 17일

SPRING

목록 보기
12/15

영속성(Persistence)이란?

객체(entity)와 데이터베이스가 지속적으로 연결되어 있는 상태

이 상태에서는 jpa가 자동으로 변경을 감지하고,
트랜잭션이 끝날 때 변경내용을 DB에 반영한다.

영속성 컨텍스트(Persistence Context)

영속성 컨텍스트는 JPA에서 엔티티 객체를 저장하고 관리하는 가상의 데이터 저장소!

엔티티 객체를 1차 캐시(메모리)에 올려서 DB와 직접적으로 연결되지 않더라도 관리할 수 있도록 한다.

그래서 JPA에서 영속성이란, 엔티티(Entity) 객체가 영속성 컨텍스트(Persistence Context) 에 의해 관리되는 상태를 의미한다.

JPA의 핵심은 이 영속성 컨텍스트를 통해 엔티티 객체의 상태를 관리하는 것이다!

영속성 컨텍스트의 생명주기

persist() : 맡아줘!
find() : 찾아줘!

둘 다 하러 갔을 때 캐시에(?) 해당 데이터가 없는 경우
select로 찾아서 저장해준다.
있으면 select문 안 날아감!

영속성 컨텍스트가 엔티티를 관리하는 원리

  • 1차 캐시: 영속성 컨텍스트 내부에 Map(key: @Id 식별자, value: 엔티티 인스턴스)으로 관리되는 캐시

  • 동일성 보장: 반복 호출시 1차 캐시에서 같은 엔티티 인스턴스 가져올 수 있다.

  • 트랜잭션을 지원하는 쓰기 지연
    : 예를 들어 insert 할 시에, 엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 저장(flush) 대신 쓰기 지연 sql 저장소에 insert sql을 차곡차곡 쌓게 되며 커밋시 쿼리를 데이터베이스로 보낸다!

이를 트랜잭션을 지원하는 쓰기 지연이라고 한다.

flush(): 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영.

플러시 절차

  1. 영속성 컨텍스트에 보관할 때 최초 엔티티 상태를 복사해서 스냅샷으로 저장해 두고 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾아서 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 보낸다.
  2. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 저장한다.
  3. 플러시를 하는 경우: a.em.flush()를 직접 호출한다.
  4. 트랜잭션 커밋 시 플러시가 자동 호출한다.
  5. JPQL 쿼리 실행 시 플러시가 자동 호출한다

변경 감지(dirty checking)

SQL에 의존적이지 않도록 엔티티의 데이터 변경을 감지하고 데이터베이스에 자동으로 반영하는 기능을 변경 감지라고 한다.

영속성 컨텍스트에 보관할 때 최초 엔티티 상태를 복사해서 저장한 스냅샷과 이를 비교하여 감지한다.

(영속 상태의 엔티티에만 적용된다.=준영속이나 비영속은 해당되지 않는다.)

변경 감지 절차(커밋 실행 시)
1. 우선 엔티티 매니저 내부에서 먼저 플러시(flush)가 호출된다.
2. 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
3. 변경된 엔티티와 관련된 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.
4. 쓰기 지연 저장소의 SQL을 데이터베이스로 보낸다.
5. 데이터베이스에서 트랜잭션을 커밋한다.

/* 설명. JDBC API를 이용해 직접 SQL을 다룰 때 발생할 수 있는 문제점
    *  1. 데이터 변환, SQL 작성, JDBC API 코드 등의 중복 작성(개발 시간 증가, 유지보수성 저하)
    *  2. SQL에 의존하여 개발
    *  3. 패러다임 불일치(상속, 연관관계, 객체 그래프 탐색, 방향성)
    *  4. 동일성 보장 문제
   * */

   /* 목차. 1 */
   @DisplayName("직접 SQL을 작성하여 메뉴를 조회할 때 발생하는 문제 확인")
   @Test
   void testDirectSelectSql() throws SQLException {

       // given
       String query = "SELECT MENU_CODE, MENU_NAME, MENU_PRICE, CATEGORY_CODE, ORDERABLE_STATUS "
               + "FROM TBL_MENU";

       // when
       Statement stmt = con.createStatement();
       ResultSet rset = stmt.executeQuery(query);

       List<Menu> menuList = new ArrayList<>();
       while(rset.next()) {
           Menu menu = new Menu();
           menu.setMenuCode(rset.getInt("MENU_CODE"));
           menu.setMenuName(rset.getString("MENU_NAME"));
           menu.setMenuPrice(rset.getInt("MENU_PRICE"));
           menu.setCategoryCode(rset.getInt("CATEGORY_CODE"));
           menu.setOrderableStatus(rset.getString("ORDERABLE_STATUS"));

           menuList.add(menu);
       }

       // then
       Assertions.assertTrue(!menuList.isEmpty());
       if(!menuList.isEmpty()) menuList.forEach(System.out::println);

       rset.close();
       stmt.close();
   }

   /* 목차. 2 */
   @DisplayName("연관된 객체 문제 확인")
   @Test
   void testAssociationObject() throws SQLException {

       // given
       String query = "SELECT A.MENU_CODE, A.MENU_NAME, A.MENU_PRICE, B.CATEGORY_CODE, "
               + "B.CATEGORY_NAME, A.ORDERABLE_STATUS "
               + "FROM TBL_MENU A "
               + "JOIN TBL_CATEGORY B ON (A.CATEGORY_CODE = B.CATEGORY_CODE)";
       // when
       Statement stmt = con.createStatement();
       ResultSet rset = stmt.executeQuery(query);

       List<MenuAndCategory> menuAndCategories = new ArrayList<>();
       while(rset.next()) {
           MenuAndCategory menuAndCategory = new MenuAndCategory();
           menuAndCategory.setMenuCode(rset.getInt("MENU_CODE"));
           menuAndCategory.setMenuName(rset.getString("MENU_NAME"));
           menuAndCategory.setMenuPrice(rset.getInt("MENU_PRICE"));
           menuAndCategory.setCategory(new Category(rset.getInt("CATEGORY_CODE"),
                   rset.getString("CATEGORY_NAME")));
           menuAndCategory.setOrderableStatus(rset.getString("ORDERABLE_STATUS"));

           menuAndCategories.add(menuAndCategory);
       }

       // then
       Assertions.assertTrue(!menuAndCategories.isEmpty());
       menuAndCategories.forEach(System.out::println);

       rset.close();
       stmt.close();
   }

   /* 설명. 3. 패러다임 불일치(상속, 연관관계, 객체 그래프 탐색, 방향성) */
   /* 설명. 3-1. 상속 문제
    *  객체 지향 언어의 상속 개념과 유사한 것이 데이터베이스의 서브타입엔티티이다.(서브타입을 별도의 클래스로 나뉘었을 때)
    *  슈퍼타입의 모든 속성을 서브타입이 공유하지 못하여 물리적으로 다른 테이블로 분리가 된 형태이다.
    *  (설계에 따라서는 하나의 테이블로 속성이 추가되기도 한다.)
    *  하지만 객체지향의 상속은 슈퍼타입의 속성을 공유해서 사용하므로 여기에서 패러다임의 불일치가 발생한다.
    * */

   /* 설명. 3-2. 연관관계 문제, 객체 그래프 탐색 문제, 방향성 문제
    *  객체지향에서 말하는 가지고 있는(ASSOCIATION 연관관계 혹은 COLLECTION 연관관계) 경우 데이터베이스 저장 구조와
    *  다른 형태이다.
    *
    * 설명.
    *  - 데이터베이스 테이블에 맞춘 객체 모델
    *  public class Menu {
    *    private int menuCode;
    *    private String menuName;
    *    private int menuPrice;
    *    private int categoryCode;
    *    private String orderableStatus;
    *  }
    *  - 객체 지향 언어에 맞춘 객체 모델
    *  public class Menu {
    *    private int menuCode;
    *    private String menuName;
    *    private int menuPrice;
    *    private Category category;
    *    private String orderableStatus;
    *  }
    *
   * */

   /* 설명. 4. 동일성 보장 문제 */
   @DisplayName("조회한 두 개의 행을 담은 객체의 동일성 비교 테스트")
   @Test
   void testEquals() throws SQLException {

       // given
       String query = "SELECT MENU_CODE, MENU_NAME FROM TBL_MENU WHERE MENU_CODE = 12";

       // when
       Statement stmt1 = con.createStatement();
       ResultSet rset1 = stmt1.executeQuery(query);

       Menu menu1 = null;
       while(rset1.next()) {
           menu1 = new Menu();
           menu1.setMenuCode(rset1.getInt("MENU_CODE"));
           menu1.setMenuName(rset1.getString("MENU_NAME"));
       }

       Statement stmt2 = con.createStatement();
       ResultSet rset2 = stmt2.executeQuery(query);

       Menu menu2 = null;
       while(rset2.next()) {
           menu2 = new Menu();
           menu2.setMenuCode(rset2.getInt("MENU_CODE"));
           menu2.setMenuName(rset2.getString("MENU_NAME"));
       }

       // then
       Assertions.assertNotEquals(menu1, menu2);
   }

   /* 설명.
    *  JPA를 활용하면 동일 비교가 가능하다.
    *  Menu menu1 = entityManager.find(Menu.class, 1);
    *  Menu menu2 = entityManager.find(Menu.class, 1);
    *  System.out.println(menu1==menu2);
   * */
profile
럭키헤일리

2개의 댓글

comment-user-thumbnail
2025년 3월 26일

심청이님... 발표도 잘 하고 정말 대단하세요... 자극 받고 갑니다!

1개의 답글