본문 바로가기
JPA

[JPA] JPA 동작 이해하기 - 동작 원리와 영속성 컨텍스트

by 2nyong 2023. 4. 27.

목차


    JPA 동작 이해하기

    https://docs.jboss.org/hibernate/entitymanager/3.6/reference/en/html_single/

     

    - EntityManagerFactory

    • EntityManager : 인스턴스를 관리합니다.
    • 일반적으로 하나의 Database에 하나의 EntityManagerFactory가 매핑됩니다.
      예) 게시판 프로젝트를 위해 게시판 DB를 만들면 테이블과 무관하게 하나의 EntityManagerFactory가 매핑됩니다.
    • EntityManagerFactory에 DB 접근 정보, 옵션 등을 전달하기 위해 persistence.xml을 사용하는데, 스프링 부트를 사용하게 되면 application.properties를 활용해 해당 파일이 자동으로 생성됩니다.
    • Persistence 객체를 통해서 EntityManagerFactory를 만들 수 있습니다.
      EntityManagerFactory emf = Persistence.createEntityManagerFactory("DB이름")

    - EntityManager

    • EntityManager는 EntityManagerFactory를 통해 사용자 요청 1개당 1개씩 생성되며, DB 커넥션풀을 통해 DB에 CRUD를 요청합니다.
    • 이 때 하나의 트랜잭션에는 한개의 EntityManager만 존재할 수 있습니다. (멀티 쓰레드에서 EntityManager공유 불가능)
    트랜잭션이란?
    데이터베이스 개론에 포함되어 있는 개념입니다. DB의 데이터를 조작할 때, 실수가 발생할 경우 보안장치가 없어 실수를 인지하지 못하고 CRUD를 계속 수행하다보면 안정적인 데이터 관리가 불가능해집니다. 이러한 경우에도 데이터 관리의 안정성(ACID)을 보장하기 위해 보안장치로써 만들어진 개념입니다.

    @Transactional (스프링에서 사용하는 어노테이션)
    스프링과 자바 환경에는 원래 트랜잭션이라는 개념이 없으나, DB와 연결해 작업을 수행하면서 스프링이나 자바 환경에서도 트랜잭션과 같은 개념이 필요해졌습니다. DB의 트랜잭션은 데이터가 DB에 들어갔을 때만 수행되는 것이므로, 이와 같은 개념을 @Transactional 어노테이션을 통해 스프링 환경에 도입하였습니다. JPA도 트랜잭션 환경 내에서 동작하게 됩니다. 또, AOP로 동작하기 때문에 스프링 빈으로 등록된 객체에만 적용할 수 있습니다.

    영속성 이해하기

    - 영속성 컨텍스트

    영속성 컨텍스트란 엔티티를 영구 저장하는 환경이라는 뜻입니다. 저는 개인적으로 데이터베이스에서 꺼내온 데이터 객체를 보관하고 관리하는 역할을 한다는 점에서, 영속성 컨텍스트가 스프링 빈을 관리하는 스프링 컨테이너와 비슷하다고 느꼈습니다. 영속성 컨텍스트는 엔티티 매니저를 통해 엔티티를 조회하거나 저장할 수 있습니다. 사실 데이터가 필요하다면 매번 데이터베이스에서 조회해 쓰면 되긴 하지만, 이 방법은 데이터에 대한 지속성이 없기 때문에 효율적이지 못합니다. 이 때문에 같은 데이터를 재사용할 경우 다시 DB를 조회하는 일이 없도록 효율성을 위해 등장한 개념입니다.

     

    EntityManager를 통해 영속성 컨텍스트에 접근할 수 있는데, 프로젝트 환경에 따라 약간의 차이가 있습니다.

     

    J2SE 환경(기본적인 Java 실행 환경)의 경우,

    1개의 요청이 들어오면 영속성 컨텍스트에 접근하기 위해 EntityManagerFactory를 통해 EntityManager를 만듭니다. 영속성 컨텍스트는 이렇게 만들어진 EntityManager일대일로 매칭되도록 하나만 생성됩니다. 만약 다른 EntityManager를 생성한다면 그에 따라 다른 영속성 컨텍스트가 생성되어 각각의 EntityManager는 서로 다른 영속성 컨텍스트를 참조하게 됩니다.

     

    J2EE 환경(Spring과 같은 컨테이너 환경)의 경우,

    EntityManager 여러개하나의 영속성 컨텍스트에 모두 접근할 수 있습니다. 스프링 환경에서는 트랜잭션이 유지될 수 있는 환경을 제공해주는데, 하나의 트랜잭션에서는 반드시 하나의 영속성 컨텍스트만 접근하게 됩니다. 만약 사용자가 여러 개의 EntityManager를 생성해 여러 개의 영속성 컨텍스트에 접근하려고 하더라도, 스프링이 제공하는 하나의 트랜잭션 환경에서는 하나의 영속성 컨텍스트만 있으므로 여러 개의 EntityManager가 하나의 영속성 컨텍스트에 접근하게 됩니다.


    - 1차 캐시

    1차 캐시

     

    1차 캐시는 영속성 컨텍스트 내부에 존재합니다. 영속 상태의 엔티티는 해당 캐시에 Map 형태로 저장됩니다. 영속 상태의 엔티티를 저장한다는 점에서 1차 캐시가 영속성 컨텍스트를 만든 근본적인 이유이기도 합니다. 캐시의 키(key)는 @ID로 매핑한 식별자이며 값(Value)는 엔티티 인스턴스가 들어가 있습니다. 따라서 영속성 컨텍스트에서 데이터를 저장하고 조회하는 모든 기준은 기본 키(key) 값입니다.


    - 쓰기 지연

    EntityManager는 데이터의 안정성을 위해 트랜잭션을 커밋하기 전까지 DB에 엔티티를 저장하지 않습니다. 대신 엔티티 저장과 관련된 SQL을 EntityManager의 '쓰기 지연 SQL 저장소'에 보관하고 있다가 트랜잭션이 커밋되면 저장소에 보관하고 있던 모든 INSERT SQL을 DB에 요청합니다.


    - Flush()

    쓰기 지연 SQL 저장소에 모인 모든 쿼리를 DB에 요청하는 역할을 합니다. 동작 순서는 아래와 같습니다.

    1. 트랜잭션 커밋 요청 들어옴
    2. EntityManager가 Flush 실행
    3. 쓰기 지연 SQL 저장소에 모인 쿼리를 DB에 요청
    4. DB에 커밋(트랜잭션 커밋)

    - 변경 감지

    엔티티의 변경을 감지해 DB에 자동으로 업데이트를 요청합니다. 동작 순서는 아래와 같습니다.

    1. 트랜잭선 커밋 요청 들어옴
    2. 1차 캐시에 보관중인 원본 Entity와 비교하여 변경된 Entity를 찾음
    3. 변경된 Entity가 발견되면 Update 쿼리를 생성하여 쓰기 지연 SQL 저장소에 저장
    4. EntityManager가 Flush를 실행
    5. 쓰기 지연 SQL 저장소에 모인 쿼리를 DB에 요청
    6. DB에 커밋(트랜잭션 커밋)
    Hibernate의 업데이트 기본 전략
    Entity의 모든 Field를 수정하면 수정 쿼리가 항상 같기 때문에 효율성을 위해 모든 Entity의 Field를 업데이트합니다. 또, 동일한 쿼리를 내보내야할 경우 이전에 사용한 쿼리를 재사용합니다. (효율성을 매우 중요하게 생각함)

    변경 감지 조건
    1. 변경하려는 Entity 객체가 영속 상태여야 합니다. (findById로 가지고 오면 영속 상태이므로 변경 추적 가능)
    2. 트랜잭션 안에 묶여 있어야 합니다.
    3. flush 호출을 위해 트랜잭션이 커밋되어야 합니다.

    Spring Container의 영속성 컨텍스트 전략

    • Spring Container는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용합니다.
      → 말 그대로 트랜잭션 범위와 영속성 컨텍스트의 생존 범위가 같다는 뜻입니다.
    • 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 종료될 때 영속성 컨텍스트도 종료됩니다.
      → 같은 트랜잭션 안에서 항상 같은 영속성 컨텍스트에 접근하게 되는 이유
    • @Transactional 어노테이션을 사용해서 트랜잭션을 시작하고 종료합니다.
    • 일반적으로 @Transactional 어노테이션은 @Service에서 사용하게 되는데, @Transactional도 AOP 이기 때문에 코드 수행 중에 이 어노테이션을 발견하면 AOP가 가로채서 트랜잭션을 시작합니다. 따라서 @Service의 특정 메소드에 트랜잭션 어노테이션을 달아주면 메소드의 시작-종료와 함께 트랜잭션도 시작-종료하기 때문에, 서비스 메소드의 수행과정에서 사용하는 @Repository에서도 영속성 컨텍스트가 유지됩니다.
      트랜잭션 범위와 영속성 컨텍스트의 생존 범위가 같은 이유.
    • 이 범위를 벗어나게 되면 트랜잭션과 영속성 컨텍스트는 모두 사라지기 때문에, 간혹 @Controller에서 @Repository에 직접 접근해 데이터를 조회하려는 경우 에러가 발생하는 원인이 됩니다.

    Spring Data JPA와 JpaRepository

    - Spring Data JPA 구조

     

    - JpaRepository 원리

    1. JpaRepository가 상속된 인터페이스를 스캔한다.
    2. 스캔되면 스프링이 JpaRepository 인터페이스의 모든 메소드가 구현되어 있는 SimpleJpaRepository 라는 클래스를 만든다.
    3. 이 클래스를 스프링 빈으로 등록한다.
    4. 사용자 정의 메서드는 이미 정의되어 있는 규칙에 따라 메서드를 선언하면 Hibernate가 이 이름을 분석하여 SimpleJpaRepository 클래스에 구현한다.

    연습하기

    • Course Entity
    package com.example.entity;
    
    import jakarta.persistence.*;
    
    @Entity
    @Getter @Setter
    @Table(name = "course")
    public class Course { // course 테이블
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String title;
        private String instructor;
        private double cost;
    
        public Course(String title, String instructor, double cost) {
            this.title = title;
            this.instructor = instructor;
            this.cost = cost;
        }
    
        public Course(){}
    }

     

    • 테스트 코드
    package com.example.entity;
    
    import static org.assertj.core.api.Assertions.*;
    
    public class CourseTest {
    
        @Test
        @DisplayName("Course 생성")
        void createCourse() {
            EntityManagerFactory emf = Persistence.createEntityManagerFactory("course");
            EntityManager em = emf.createEntityManager();
            EntityTransaction et = em.getTransaction(); // Transaction 시작을 위한 객체 생성
    
            et.begin(); // 트랜잭션 환경 시작 -> @Transactional
    
            try {
    //            Course course = new Course("JPA", "Robbie", 1111.0);
                Course course = new Course("JTA", "Robbie", 22222.1); // 비영속(New), 순수 자바 객체
                em.persist(course); // 영속, em을 통해 Entity가 영속성컨텍스츠에 저장되어 관리되고 있는 상태
                // 쓰기 지연 저장소에 쿼리 저장 : INSERT INTO course (cost, instructor, title) values (?, ?, ?)
    
                et.commit(); // flush 메서드가 자동으로 호출, 쓰기 지연 저장소에 있던 INSERT 쿼리 날림
            } catch (Exception exception) {
                exception.printStackTrace();
                et.rollback(); // try에서 오류 발생시 트랜잭션 롤백 (DB 반영x)
            } finally { // 무조건 실행되는 부분
                em.clear(); // 엔티티 매니저 반납
            }
            emf.close();
        }
    
        @Test
        @DisplayName("Course 조회")
        void readCourse() {
            EntityManagerFactory emf = Persistence.createEntityManagerFactory("course");
            EntityManager em = emf.createEntityManager();
            EntityTransaction et = em.getTransaction(); // Transaction 시작을 위한 객체 생성
    
            et.begin(); // 트랜잭션 환경 시작 -> @Transactional
    
            try {
                Course course = em.find(Course.class, 1);
                System.out.println("course.getId() = " + course.getId());
                System.out.println("course.getTitle() = " + course.getTitle());
                System.out.println("course.getInstructor() = " + course.getInstructor());
    
                // 같은 내용을 조회할 경우 쿼리가 생성되지 않음.
                // 우선 1차 캐시 조회 후, 데이터가 없을 경우에만 DB에 쿼리를 날리기 때문.
                Course course1 = em.find(Course.class, 1);
                System.out.println("course1.getId() = " + course1.getId());
                System.out.println("course1.getTitle() = " + course1.getTitle());
                System.out.println("course1.getInstructor() = " + course1.getInstructor());
    
                System.out.println("(course == course1) = " + (course == course1));
                // 원래라면 두 객체는 서로 다른 객체이기 때문에 false가 출력되어야 하지만 true가 출력됨.
                // 처음 course를 조회하면 조회한 내용을 1차 캐시에 담아두고, course1을 조회하면 1차 캐시에서 가져오기 때문에
                // 결국 같은 객체를 조회한 경우이므로 동일성을 보장하는 의미에서 true를 반환한다.
    
                // 1차 캐시에 없는걸 조회하기 때문에 쿼리를 날린다.
                Course course3 = em.find(Course.class, 3);
                System.out.println("course3.getId() = " + course3.getId());
                System.out.println("course3.getTitle() = " + course3.getTitle());
                System.out.println("course3.getInstructor() = " + course3.getInstructor());
    
                et.commit(); // flush 메서드가 자동으로 호출, 쓰기 지연 저장소에 있던 쿼리 날림
            } catch (Exception exception) {
                exception.printStackTrace();
                et.rollback(); // try에서 오류 발생시 트랜잭션 롤백 (DB 반영x)
            } finally { // 무조건 실행되는 부분
                em.clear(); // 엔티티 매니저 반납
            }
            emf.close();
        }
    
        @Test
        @DisplayName("Course 수정")
        void updateCourse() {
            EntityManagerFactory emf = Persistence.createEntityManagerFactory("course");
            EntityManager em = emf.createEntityManager();
            EntityTransaction et = em.getTransaction(); // Transaction 시작을 위한 객체 생성
    
            et.begin(); // 트랜잭션 환경 시작 -> @Transactional
    
            try {
                Course course = em.find(Course.class, 1);
                course.setTitle("Spring"); // JPA -> Spring
                // course 수정 후, em.persist(course)를 해주지 않아도 수정이 됨 -> 변경감지에 의해서
                // @Service 에서 update 이후 courseRepository.save(); 와 같은 과정을 수행하지 않는 이유이기도 하다.
                assertThat(em.contains(course)).isTrue(); // course 영속화 확인
    
                // save() : update or insert 에 사용됨, 변경감지가 있다고 하더라도 save()는 오류가 나지 않는다.
                //          ㄴ insert의 경우, persist가 호출된다.
                //              -> 영속 상태로 만들어서 추후에 트랜잭션을 마치고 commit이 될때 flush가 호출되면서 insert 쿼리 날림
                //          ㄴ update(변경감지가 사용되는)의 경우, merge가 호출된다.
    
                // 준영속 detach()
                em.detach(course); // course가 준영속 상태다.
                assertThat(em.contains(course)).isFalse();
    
                // merge() : 조회해서 영속화를 하고, 그 다음에 병합을 하고, 반환한다.
                // 변경할 데이터 객체
                Course mergedCourse = new Course("Spring AOP", "Sparta", 11.0);
                mergedCourse.setId(course.getId());
                em.merge(mergedCourse); // 영속화 -> 병합(Update) -> 반환
                assertThat(em.contains(mergedCourse)).isFalse();
    
    
                et.commit(); // flush 메서드가 자동으로 호출, 쓰기 지연 저장소에 있던 쿼리 날림
            } catch (Exception exception) {
                exception.printStackTrace();
                et.rollback();
            } finally {
                em.clear();
            }
            emf.close();
        }
    
        @Test
        @DisplayName("Course 삭제")
        void deleteCourse() {
            EntityManagerFactory emf = Persistence.createEntityManagerFactory("course");
            EntityManager em = emf.createEntityManager();
            EntityTransaction et = em.getTransaction(); // Transaction 시작을 위한 객체 생성
    
            et.begin(); // 트랜잭션 환경 시작 -> @Transactional
    
            try {
                Course course = em.find(Course.class, 1); // 지워야할 데이터
                assertThat(em.contains(course)).isTrue();
    
                em.remove(course);
                assertThat(em.contains(course)).isFalse();
    
                et.commit(); // flush 메서드가 자동으로 호출, 쓰기 지연 저장소에 있던 쿼리 날림
            } catch (Exception exception) {
                exception.printStackTrace();
                et.rollback();
            } finally {
                em.clear();
            }
            emf.close();
        }
    }

    'JPA' 카테고리의 다른 글

    [JPA] 다대일 연관관계 (양방향)  (0) 2023.04.29
    [JPA] 일대일 연관관계 (양방향)  (0) 2023.04.29
    [JPA] 다대일 연관관계 (단방향)  (0) 2023.04.29
    [JPA] 일대일 연관관계 (단방향)  (0) 2023.04.28
    [JPA] Entity 연관 관계  (0) 2023.04.28

    댓글