😀
fistkim TECH BLOG
  • Intro
  • 강의
    • Reactive Programming in Modern Java using Project Reactor
      • Reactor execution model 1
      • Reactor execution model 2
      • Reactor execution model 3 - parallelism
      • Reactor execution model 4 - overview
      • Transform
      • Combine
      • Side Effect Methods
      • Exception/Error handling
      • retry, retryWhen, repeat
      • BackPressure
      • Cold & Hot Streams
    • NEXTSTEP 클린코드 with java 9기
      • 정리노트
    • NEXTSTEP DDD 세레나데 2기
      • CH01 도메인 주도 설계 이해
      • CH02 크게 소리 내어 모델링 하기
      • CH03 도메인 주도 설계 기본 요소
      • CH04 도메인 주도 설계 아키텍처
      • CH05 도메인 이벤트
    • NEXTSTEP 인프라 공방 1기
      • 망 분리하기
      • 통신 확인하기
      • 도커 컨테이너 이해하기
      • [미션 1] 서비스 구성하기 실습
      • [미션 2] 서비스 배포하기 실습
      • 서버 진단하기
      • 어플리케이션 진단하기
      • [미션 3] 서비스 운영하기
      • 웹 성능 진단하기
      • 부하 테스트
      • k6
      • [미션 4] 성능 테스트
      • 리버스 프록시 개선하기
      • 캐싱 활용하기
      • [미션 5] 화면 응답 개선하기
      • Redis Annotation 및 설정
      • 인덱스 이해하기 & DB 튜닝
      • [미션 6-1] 조회 성능 개선하기
      • [미션 6-2] DB 이중화 적용
    • NEXTSTEP 만들면서 배우는 Spring 3기
      • CH01 올바른 방향 바라보기
      • CH02 HTTP 이해 - 웹 서버 구현
        • HTTP 파싱
        • HTTP 웹 서버 구현
      • CH03 MVC - @MVC 프레임워크 구현
        • Servlet 다시 짚기
        • Cookie, Session 다시 짚기
        • MVC 프레임워크 구현
      • CH04 나만의 라이브러리 구현
      • CH05 DI - DI 프레임워크 구현
      • CH06 Aspect OP
    • 스프링 시큐리티
      • 스프링 시큐리티 아키텍처
      • WebAsyncManagerIntegrationFilter
      • SecurityContextPersistenceFilter
      • HeaderWriterFilter
      • CsrfFilter
      • (+) 스프링 시큐리티 + JWT
      • (+) 마치며
    • 더 자바, 코드를 조작하는 다양한 방법
      • CH01 JVM 이해하기
      • (+) 클래스 로더 이해하기
      • CH02 바이트 코드 분석 및 조작
      • (+) jacoco
      • CH03 리플렉션
      • CH04 다이나믹 프록시
      • CH05 애노테이션 프로세서
    • 더 자바, 애플리케이션을 테스트하는 다양한 방법
      • CH01 JUnit 5
      • CH02 Mockito
      • (+) Spy vs Mock
      • CH03 도커와 테스트
      • CH04 성능 테스트
      • (+) VisualVM
      • (+) 테스트 자동화
      • CH05 운영 이슈 테스트
      • CH06 아키텍처 테스트
    • 모든 개발자를 위한 HTTP 웹 기본 지식
      • CH01 인터넷 네트워크
      • CH02 HTTP 기본
      • CH03 HTTP 메서드 속성
      • CH04 HTTP 메서드 활용
      • CH05 HTTP 상태코드
      • CH06 HTTP 헤더1 - 일반 헤더
      • CH07 HTTP 헤더2 - 캐시와 조건부 요청
      • (+) HTTPS 원리
    • 스프링 프레임워크 핵심 기술
      • CH01 IOC 컨테이너
      • CH02 AOP
      • (+) 스프링 의존성 관리
      • (+) 생성자 주입 장점
    • 코딩으로 학습하는 GoF의 디자인 패턴
      • 객체 생성
        • 싱글톤 패턴
        • 팩토리 메소드 패턴
        • 추상 팩토리 패턴
        • 빌더 패턴
        • 프로토타입 패턴
      • 구조
        • 어댑터 패턴
        • 브릿지 패턴
        • 컴포짓 패턴
      • 행동
        • (작성중)
    • 실전 Querydsl
      • CH01 프로젝트 환경구성
      • CH02 예제 도메인 모델
      • CH03 기본문법
      • CH04 중급 문법
      • CH05 실무활용 (스프링 데이터 JPA와 Querydsl)
      • CH06 스프링데이터JPA 가 제공하는 Querydsl 기능
      • (+) 별칭(alias)
      • (+) Slice 쿼리
    • 스프링 데이터 JPA
      • CH01 핵심개념이해 1
      • CH02 핵심개념이해 2
      • CH03 핵심개념이해 3
      • CH04 Spring Data Common
      • CH05 Spring Data JPA
    • 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화
      • CH01 지연 로딩과 조회 성능 최적화
      • CH02 컬렉션 조회 최적화
      • CH03 전체 정리
    • 초보를 위한 쿠버네티스 안내서
      • CH01 쿠버네티스 시작하기
      • CH02 쿠버네티스 알아보기
      • CH03 쿠버네티스 실습 준비
      • CH04 쿠버네티스 기본 실습
    • Flutter Provider Essential
      • CH01 Introduction
      • CH02 Provider Overview
      • CH03 TODO App
      • CH04 Weather App
      • CH05 Firebase Authentication App
    • Flutter Bloc Essential
      • CH01 Introduction
      • CH02 Bloc Overview
      • CH03 TODO App
      • CH04 Weather App
      • CH05 Firebase Authentication App
    • Flutter Advanced Course - Clean Architecture With MVVM
      • CH01 Introduction
      • CH02 Clean Architecture 4 Layer
      • CH03 MVVM
      • CH04 Data Layer
      • (+) Data Layer - response to model
      • (+) Data Layer - Network
      • CH05 Domain Layer
      • CH06 Presentation Layer
      • CH07 Application Layer
      • (+) Application Layer - l10n
      • (+) Application Layer - DI
      • (+) Application Layer - environment
    • 자바 알고리즘 입문
      • CH01 문자열
      • CH02 Array(1, 2 차원 배열)
      • CH03 Two pointers, Sliding window[효율성: O(n^2)-->O(n)]
      • CH04 HashMap, TreeSet (해쉬, 정렬지원 Set)
      • CH05 Stack, Queue(자료구조)
      • CH06 Sorting and Searching(정렬, 이분검색과 결정알고리즘)
      • CH07 Recursive, Tree, Graph(DFS, BFS 기초)
      • CH08 DFS, BFS 활용
      • CH09 Greedy Algorithm
      • CH10 dynamic programming(동적계획법)
  • 도서
    • 만들면서 배우는 클린 아키텍처
      • 학습목표
      • CH01 계층형 아키텍처의 문제는 무엇일까?
      • CH02 의존성 역전하기
      • CH03 코드 구성하기
      • CH04 유스케이스 구현하기
      • CH05 웹 어댑터 구현하기
      • CH06 영속성 어댑터 구현하기
      • CH07 아키텍처 요소 테스트하기
      • CH08 경계 간 매핑하기
      • CH09 어플리케이션 조립하기
      • CH10 아키텍처 경계 강제하기
      • CH11 의식적으로 지름길 사용하기
      • CH12 아키텍처 스타일 결정하기
    • 클린 아키텍처
      • 들어가며
      • 1부 소개
        • 1장 설계와 아키텍처란?
        • 2장 두 가지 가치에 대한 이야기
      • 2부 벽돌부터 시작하기: 프로그래밍 패러다임
        • 3장 패러다임 개요
        • 4장 구조적 프로그래밍
        • 5장 객체 지향 프로그래밍
        • 6장 함수형 프로그래밍
      • 3부 설계 원칙
        • 7장 SRP: 단일 책임 원칙
        • 8장 OCP: 개방-폐쇄 원칙
        • 9장 LSP: 리스코프 치환 원칙
        • 10장 ISP: 인터페이스 분리 원칙
        • 11장 DIP: 의존성 역전 원칙
      • 4부 컴포넌트 원칙
        • 12장 컴포넌트
        • 13장 컴포넌트 응집도
        • 14장 컴포넌트 결합
      • 5부
        • 15장 아키텍처란?
    • 스프링 입문을 위한 자바 객체 지향의 원리와 이해
      • CH01 사람을 사랑한 기술
      • CH02 자바와 절차적/구조적 프로그래밍
      • CH03 자바와 객체 지향
      • (+) 자바 코드 실행에 따른 메모리 적재과정
      • CH04 자바가 확장한 객체 지향
      • CH05 객체 지향 설계 5 원칙 - SOLID
      • CH06 스프링이 사랑한 디자인 패턴
      • CH07 스프링 삼각형과 설정 정보
      • (부록) 람다(lambda)
    • 객체지향의 사실과 오해
      • CH01 협력하는 객체들의 공동체
      • CH02 이상한 나라의 객체
      • CH03 타입과 추상화
      • CH04 역할, 책임, 협력
      • CH05 책임과 메시지
      • CH06 객체 지도
      • CH07 함께 모으기
      • (+) 인터페이스 개념 바로잡기
    • 도메인 주도 개발 시작하기
      • CH01 도메인 모델 시작하기
      • CH02 아키텍처 개요
      • CH03 애그리거트
      • CH04 리포지터리와 모델 구현
      • CH05 스프링 데이터 JPA를 이용한 조회 기능
      • CH06 응용 서비스와 표현 영역
      • CH07 도메인 서비스
      • CH08 애그리거트 트랜잭션 관리
      • CH09 도메인 모델과 바운디드 컨텍스트
      • CH10 이벤트
      • CH11 CQRS
    • 자바 ORM 표준 JPA 프로그래밍
      • CH01 JPA 소개
      • CH02 JPA 시작
      • CH03 영속성 관리
      • CH04 엔티티 매핑
      • CH05 연관관계 매핑 기초
      • CH06 다양한 연관관계 매핑
      • CH07 고급 매핑
      • CH08 프록시와 연관관계 관리
      • CH09 값 타입
      • CH10 객체지향 쿼리 언어
      • CH11 웹 애플리케이션 제작
      • CH12 스프링 데이터 JPA
      • CH13 웹 애플리케이션과 영속성 관리
      • CH14 컬렉션과 부가 기능
      • CH15 고급 주제와 성능 최적화
      • CH16 트랜잭션과 락, 2차 캐시
    • 소프트웨어 세상을 여는 컴퓨터과학
      • CH01 컴퓨터 과학 소개
      • CH02 데이터 표현과 디지털 논리
    • 이펙티브 자바
      • 1 장 들어가기
      • 2장 객체 생성과 파괴
        • [01] 생성자 대신 정적 팩터리 메서드를 고려하라
        • [02] 생성자에 매개변수가 많다면 빌더를 고려하라
        • [03] private 생성자나 열거 타입으로 싱글턴임을 보증하라
        • [04] 인스턴스화를 막으려거든 private 생성자를 사용하라
        • [05] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
        • [06] 불필요한 객체 생성을 피하라
        • [07] 다 쓴 객체 참조를 해제하라
        • [08] finalizer 와 cleaner 사용을 피하라
        • [09] try-finally 보다는 try-with-resources 를 사용하라
      • 3장 모든 객체의 공통 메서드
        • [10] equals는 일반 규약을 지켜 재정의하라
        • [11] equals 를 재정의하려거든 hashCode도 재정의하라
        • [12] toString 을 항상 재정의하라
        • [13] clone 재정의는 주의해서 진행하라
        • [14] Comparable 을 구현할지 고려하라
      • 4장 클래스와 인터페이스
        • [15] 클래스와 멤버의 접근 권한을 최소화하라
  • 토픽
    • 서버 모니터링
      • CPU 사용량
      • 메모리 사용량
      • 스레드 풀
    • Spring Boot Monitoring
      • Spring actuator
      • Spring eureka
      • Prometheus
      • grafana
      • Spring actuator + Prometheus + grafana
    • JAVA 데일리 토픽
      • 메모리 누수(memory leak)
      • 객체 참조의 유형
      • 커스텀 스레드 풀
      • Mark And Compact
      • serialVersionUID 이해하기
      • 함수형 인터페이스
      • 메소드 참조
      • equals()와 hashCode()가 무엇이고 역할이 무엇인지
      • StringBuffer vs StringBuilder
      • String vs StringBuilder, StringBuffer
      • String interning
    • JAVA GC
    • 프로그래머스 문제 풀기
      • 해시
      • 스택/큐
      • 힙(Heap)
      • 정렬
      • 완전탐색
      • DFS/BFS
    • 데이터베이스 구성 및 작동 흐름
    • 데이터베이스 JOIN 원리
    • 객체지향생활체조 원칙
    • 상태(state), 상속(inheritance), 합성(composition) 의 상관관계
    • java enum은 메모리에 언제, 어떻게 할당되는가
    • Checked Exception vs UnChecked Exception
    • Reactive Streams 원리탐구 - 간단한 예제 직접 작성해보기
    • Flutter Basic
    • Flutter StatefulWidget 생명주기
    • Flutter 가 위젯을 그리는 원리
    • Flutter 클린 아키텍처
      • application layer
        • 패키지 구조 및 레이어 설명
        • environment
        • dependency injection
        • go_router
        • foreground & background
        • 다국어처리 (l10n, i18n)
        • Global 처리(시스템 점검, fore->back 등)
        • connection_manager
        • permission_manager
        • push_notification_manager
        • firebase 연동
      • data layer
        • 패키지 구조 및 레이어 설명
        • network
        • repository
      • domain layer
        • 패키지 구조 및 레이어 설명
      • presentation layer
        • 패키지 구조 및 레이어 설명
        • resources
    • 기술 관련 포스팅 읽기
  • 기타
    • 작업일지
      • 2023. 10
      • 2023. 09
      • 2023. 08
      • 2023. 07
      • 2023. 06
      • 2023. 05
      • 2023. 04
      • 2023. 03
      • 2023. 02
      • 2023. 01
      • 2022. 12
    • Business Model
      • 아이디어 불패의 법칙
      • 린 모바일 앱 개발
      • 린 스타트업
      • 제로투원
      • MIT 스타트업 바이블
      • 린치핀
    • 백로그 종합
Powered by GitBook
On this page
  • 엔티티 매니저 팩토리와 엔티티 매니저
  • 영속성 컨텍스트(Persistence Context)
  • 엔티티의 생명주기
  • 비영속
  • 영속
  • 준영속
  • 삭제
  • 영속성 컨텍스트의 특징
  • 1차 캐시
  • dirty check
  • transactional write behind
  • 동일성 보장
  • 영속성 컨텍스트의 특징 (CRUD 각 연산별 상세히 알아보기)
  • 엔티티 조회
  • 엔티티 등록
  • 엔티티 수정
  • 엔티티 삭제
  • 플러시(flush())
  • 병합(merge())
  • 정리
  1. 도서
  2. 자바 ORM 표준 JPA 프로그래밍

CH03 영속성 관리

PreviousCH02 JPA 시작NextCH04 엔티티 매핑

Last updated 1 year ago

엔티티 매니저 팩토리와 엔티티 매니저

2장에서 살펴본 바와 같이 EntityManagerFactoryBuilder와 EntityManagerFactory는 bean 으로 관리된다. 나는 EntityManagerFactory 가 bean 으로 관리된다는 것만 인지하면 된다.

2장에서 이미 살펴보았기도 했고, 아래 그림에도 나와 있듯이 EntityManagerFactory 는 스레드마다 별도로 EntityManager 를 할당해준다. 이것의 의미는 곧 정리하겠지만 EntityManager 가 관리할 영속성 컨텍스트가 각 요청별로 따로 관리된다는 것이다.

영속성 컨텍스트(Persistence Context)

책에서는 '엔티티를 영구 저장하는 환경' 이라고 정의하고 있다. 영속성 컨텍스트는 엔티티 매니저를 생성할 때 만들어 지고 엔티티 매니저를 통해서 영속성 컨텍스트를 관리할 수 있다. 개인적으로 ‘영속성 관리를 목적으로 사용하는 공간’이라고 이해하는 것이 좋을 것 같다.

나의 경험 내에서는 결국 실무에서는 스프링 데이터 JPA 를 쓰기 때문에 EntityManager 를 직접 다룰 일은 없다. 하지만 영속성 컨텍스트가 무엇이며, 내부적으로 어떻게 동작하는지 원리를 알아두어야 스프링 데이터 JPA 가 결국에는 어떤 일을 하는지, 내부적으로 지금 어떤 일이 일어나고 있는지 정확하게 이해할 수 있다.

책에는 지금 순서에 영속성 컨텍스트에 대해서 개념만 짚고 넘어가고 있는데, 영속성 컨텍스트는 아래와 같은 형태이다. 이를 기억하자.

엔티티의 생명주기

  • 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태

  • 영속(managed): 영속성 컨텍스트에 저장된 상태

  • 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태

  • 삭제(removed): 삭제된 상태

각 상태 별로 자세히 알아보자.

비영속

엔티티를 만들고 어떤한 행위도 하지 않은 상태이다. 영속성 컨텍스트 및 데이터 베이스와 아무런 상관이 없는 객체 그대로의 상태다.

Member member = new Member();
member.setName("홍길동");

영속

Member member = new Member();
member.setName("홍길동"); // 여기까지 아직 비영속 상태

entityManager.persist(member); // 이 순간 영속 상태가 된다

준영속

위에서 알아본 바로 entityManager 가 객체를 관리하고 있는 중의 상태가 영속이고, 아예 영속된 적이 없는 순수한 엔티티 객체 상태가 비영속이었다. 준영속은 영속이었던 객체가 영속 상태에서 벗어난 것을 의미한다.

entityManager를 살려둔 채로 초기화를 시키거나, entityManager를 아예 닫아버리면 영속 상태이던 엔티티들은 모두 준영속 상태가 된다.

또는 entityManager에서 해당 엔티티만 준영속 상태로 처리할 수도 있다.

// 1. entityManager 초기화
entityManager.clear();

// 2. entityManager 종료
entityManager.close();

// 3.해당 엔티티만 준영속 상태로 만든다.
entityManager.detach(member);

삭제

정확히는 삭제가 될 예정인 상태다. 준영속은 해당 엔티티에 대해서 entityManager 를 통해서 영속성 컨텍스트 내에서 더 이상 관리를 하지 않겠다는 의미이고, 삭제는 데이터베이스에서 지우겠다는 의미이다.

entityManager.remove(member);

영속성 컨텍스트의 특징

CRUD 연산과 함께 영속성 컨텍스트에서 어떤 일들이 발생하는지 세세하게 알아보는 내용인데, 각 내용 속에 영속성 컨텍스트의 특징들이 녹아있다.

하지만 난 이미 책을 한번 다 봤기 때문에 먼저 영속성 컨텍스트의 특징에 대해서 자세히 정리를 하고 각 CRUD 연산을 보는게 더 좋을 것 같다.

아래 내용은 백기선님 강의에서 자세히 다뤘던 부분이기도 해서 해당 강의의 필기노트도 같이 정리한다.

그리고 영속성 컨텍스트의 특징을 알아보기 위해서 다시 영속성 컨텍스트의 구조에 대해서 인지하고 넘어간다.

1차 캐시

위 구조도에도 나와 있듯이 영속성 컨텍스트 내에는 1차 캐시로 사용할 수 있는 일종의 Map 이 있다. 여기서 Map 이라고 비유한 이유는 1차 캐시를 사용할 때 엔티티의 PK 값을 key 값으로 사용하기 때문이다. 그래서 특정 엔티티에 대한 조회 로직이 발생하면 데이터 베이스에 조회하지 않고 먼저 영속성 컨텍스트의 1차 캐시를 PK를 기준으로 조회한다.

1차 캐시를 굳이 1차 캐시라고 다룬 이유는 2차 캐시가 존재하기 때문이다. 나중에 다시 다루겠지만 1차 캐시와 2차 캐시의 차이는 캐시의 커버 범위이다. 1차 캐시는 해당 영속성 컨텍스트 내에서 공유되는 캐시이고 2차 캐시는 어플리케이션 전체에서 공유된다.

조회 발생시 1차 캐시에 해당 엔티티가 존재하지 않으면 비로소 데이터베이스에 접근하여 해당 엔티티를 조회하고 가져온 엔티티를 1차 캐시에 저장해둔다. 그리고 트랜잭션이 끝나기 전에 다시 해당 엔티티를 조회하게 되면 이제는 1차 캐시에 해당 엔티티가 있는 상태이므로 데이터 베이스에 접근하지 않고 1차 캐시의 엔티티를 반환한다.

dirty check

위에서 1차 캐시를 저장하는 과정에서 해당 엔티티의 스냅샷 역시 저장한다. 그리고 해당 트랜잭션 내에서 해당 엔티티에 대한 변경이 발생하면 1차 캐시 내에서 변경이 발생하고 트랜잭션이 종료되는 시점에 스냅샷과 엔티티의 차이를 비교하여 변경된 것을 우선해서 이에 대한 update 를 알아서 수행해준다.

정리를 하자면 entity의 조회 시점에 스냅샷을 만들어 두고 이를 트랜잭션이 끝나는 시점에 비교하여 변경된 것(dirty)이 있으면 이를 영속화 해준다.

상태가 변경된 것(dirty)을 트랜잭션이 끝나는 시점에 알아서 확인(check) 해서 반영해주는 것이 곧 dirty check 인 것이다.

    @Override
    @Transactional
    public void run(ApplicationArguments args) throws Exception {
        Account account = new Account();
        account.setName("fistkim");
        entityManager.persist(account);
        account.setName("fistkim1");
    }

위 코드에서 update 로직을 따로 처리한 것이 없는데 영속화 이후 이름을 바꿔준 것만으로 아래 로그처럼 update 가 발생했다. 스냅샷의 name 의 값은 최초 영속화 시점의 fistkim인데, fistkim1 로 엔티티를 바꿔줬고 트랜잭션 종료시점에 이를 비교(dirty check) 해서 변경 건에 대해서 update 를 실행해준 것이다.

Hibernate: 
    insert 
    into
        account
        (name) 
    values
        (?)
Hibernate: 
    update
        account 
    set
        name=? 
    where
        id=?

그렇다면 변경을 많이 발생시키고 최종적으로는 결국 초기 상태 그대로로 만들면 어떻게 될까?

    @Override
    @Transactional
    public void run(ApplicationArguments args) throws Exception {
        Account account = new Account();
        account.setName("fistkim");
        entityManager.persist(account);
        account.setName("fistkim1");
        account.setName("fistkim2");
        account.setName("fistkim3");
        account.setName("fistkim");
    }
Hibernate: 
    insert 
    into
        account
        (name) 
    values
        (?)

fistkim -> fistkim1 -> fistkim2 -> fistkim3 -> fistkim 으로 결국 트랜젝션이 종료되는 시점의 상태가 최초에 persist 된 상태 그대로 이므로 dirty check 에서 걸리는 것이 없어서 update 문이 발생하지 않은 것을 알 수 있다.

transactional write behind

...

The persistence context, also known as the first level cache, acts as a buffer between the current entity state transitions and the database. In caching theory, the write-behind synchronization requires that all changes happen against the cache, whose responsibility is to eventually synchronize with the backing store.

위 글에도 나와있다시피 단순히 ‘당장 쿼리 수행을 요청하지 않고 commit이 될때 한번에 한다’의 개념이 아니라, 쿼리 수행 요청을 지연함으로써 최적으로 꼭 필요한 쿼리만을 최종 판단하여 수행 요청한다는 개념으로 이해하는 것이 좋을 것 같다.

transactional write behind 의 핵심 장점은 테이블 row 에 lock 이 걸리는 시간을 최소화 하는 것에 있다.

update(memberA);
비즈니스로직A();
비즈니스로직B();
commit();

SQL을 직접 다룬 위와 같은 로직에서 update 가 발생된 순간부터 commit 될 때까지 해당 row 에 대해 lock 이 걸리게 된다. 그래서 만약 다른 트랜잭션이 해당 row 에 접근해야할 일이 생기면 격리 수준에 따라 다르겠지만 Read Committed 이상의 격리 수준에서는 해당 row 의 lock 이 풀릴 때까지 기다리게 된다.

JPA 에서는 트랜잭션이 끝나는 그 순간에 플러시를 통해 모든 쿼리를 데이터베이스에 보내고 트랜잭션을 커밋한다. 트랜잭션 수행의 시간을 최소화 하는 것이다.

동일성 보장

이미 1장에서 다룬 부분인데 동일한 영속성 컨택스트에서 조회된 엔티티에 대해서 동일한 주소값을 보장한다. 즉, 데이터베이스에서 같은 row(같은 엔티티)에 대해서 다른 변수에 할당해줘도 결국 같은 주소값을 가진다. 영속성 컨텍스트가 이를 보장해주는 것이다.(패러다임 불일치를 해결해주는 측면이다.)

영속성 컨텍스트의 특징 (CRUD 각 연산별 상세히 알아보기)

엔티티 조회

Member member = new Member();
member.setId("member1");
entityManager.persist(member);

위와 같이 코드가 실행되었다고 하면 아래와 같은 그림이 된다.

위에서 알아본 바와 같이 이를 조회하면 PK 값을 기준으로 1차 캐시에서 먼저 조회를 해서 제공한다.

만약 여기서 기존에 이미 데이터베이스에 저장되어 있었던 다른 엔티티이자 아직 1차 캐시에는 없는 엔티티를 조회하면 위에서 알아본 바와 같이 1차 캐시 탐색 후 데이터 베이스에 직접 질의하여 엔티티를 가져와 1차 캐시에 저장 후 반환해준다.

Member member2 = entityManager.find(Member.class, "member2");

엔티티 등록

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

transaction.begin();

em.persist(memberA);
em.persist(memberB);

transaction.commit();

아래 그림들은 위 코드를 실행시 영속성 컨텍스트 내부에서 어떤 일들이 일어나는지에 대한 설명이다. 핵심은 커밋이 되기 전까지 쓰기 지연 SQL 저장소에 쿼리가 쌓이기만 할 뿐 실제로 데이터베이스에 전달이 되지 않는 다는 것이다.

flush 는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화 하는 작업인데, entityManager의 커밋(entityManager의 커밋과 실제 데이터베이스의 커밋의 개념은 다르다)이 발생하면 flush 가 발생하고 비로소 데이터베이스의 커밋이 발생한다.

하지만 위와 같은 방식으로 동작하는 것은 위 그림처럼 엔티티의 PK 값을 이미 정해준 경우에만 해당한다. 책에서는 혼동을 주지 않기 위해서 아직 설명하지 않고 있는데 아래의 경우를 보자.

    @Override
    @Transactional
    public void run(ApplicationArguments args) throws Exception {
        Member member = new Member();
        member.setId(1L);
        this.entityManager.persist(member);
        System.out.println("before commit !!!");
    }
@Entity
public class Member {

    @Id
    private Long id;
    
    ...
}
before commit !!!
Hibernate: 
    insert 
    into
        member
        (name, team_id, id) 
    values
        (?, ?, ?)

PK를 직접 설정해주니 위에서 살펴본바와 똑같이 커밋이 되고서야 insert 가 실행 되는 것을 알 수 있다. 왜냐하면 persist 보다 뒤에 있는 System.out.println("before commit !!!") 가 먼저 실행 된 것이 로그상에서 확인이 되기 때문이다.

하지만 PK 생성 전략을 다르게 하면 1차 캐시에 저장할 PK 값을 채번하기 위해서 어쩔 수 없이 데이터베이스에 먼저 insert 를 실행시킨다. insert 후 1차 캐시에 저장을 하는 것이다. 실제로 그러한지 아래 코드를 보자.

    @Override
    @Transactional
    public void run(ApplicationArguments args) throws Exception {
        Member member = new Member();
        this.entityManager.persist(member);
        System.out.println("before commit !!!");
    }
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
   
    ...
}
Hibernate: 
    insert 
    into
        member
        (name, team_id) 
    values
        (?, ?)
before commit !!!

persist() 발생하자마자 바로 insert 가 실행 된 것을 알 수 있다.

GeneratedValue 전략이 IDENTITY 인 경우에는 영속성 컨텍스트 저장을 위한 ID 값 채번이 필요하므로 persist 발생과 동시에 insert 가 실행된다. persist 처리로 인해 1차 캐시에 저장해야하고 1차 캐시에 저장하려면 PK 가 필요한데 PK 채번 주체가 데이터베이스이기 때문이다.

정리를 하자면 기본적으로 transactional write behind 가 동작을 하는 것이 맞지만 PK 생성 전략에 따라서 데이터베이스에 insert 를 해야 PK 를 알 수 있는 경우 영속화와 동시에 insert 가 실행된다고 알고 있어야 한다.

엔티티 수정

이미 위에서 dirty check 에 대해서 살펴보았듯이 JPA 에서는 엔티티의 변경건에 대해서 스냅샷과의 비교를 통해서 알아서 update 를 실행시킨다. (나의 경험 안에서 봤을때) 그렇다고 해서 update 할 일이 있을때 dirty check 을 하기 보다는 명시적으로 변경된 entity 를 save() 처리를 해준다.

아래 그림은 변경된 entity 를 명시적으로 save() 하지 않고 dirty check 에 의해서 update 가 되는 과정이다.

실행되는 update 문의 경우 변경된 특정 필드에 대해서만 update 가 실행되는 것이 아니고 변경된 entity 전체 필드를 가지고 update 문이 실행된다. 즉 실행되는 update 문은 바인딩 되는 데이터를 빼고는 항상 동일하다는 것이다. 덕분에 쿼리문을 어플리케이션 로딩 시점에 미리 생성해두고 데이터만 바꿔주는 형태로 재사용 할 수 있다.

@DynamicUpdate 를 통해서 동적으로 수정된 데이터만을 가지고 update 문을 생성해서 실행시킬 수는 있으나 책에 따르면 컬럼이 30개 이상 정도는 되어야 성능상 이점이 있다고 한다.

엔티티 삭제

삭제 역시 즉시 데이터베이스에 delete 를 실행시키는 것이 아니고 remove() 와 동시에 엔티티는 준영속 상태가 되고 commit() 이 되면서 flush 가 실행됨과 동시에 delete 문이 데이터베이스에 실행된다.

    @Override
    @Transactional
    public void run(ApplicationArguments args) throws Exception {
        Member member = new Member();
        this.entityManager.persist(member);
        this.entityManager.remove(member);
        System.out.println("before commit !!!");
    }
Hibernate: 
    insert 
    into
        member
        (name, team_id) 
    values
        (?, ?)
before commit !!!
Hibernate: 
    delete 
    from
        member 
    where
        id=?

로그에서도 확인할 수 있듯이 코드상 remove()를 먼저 실행해줬음에도 "before commit !!!" 이 먼저 실행 되고 delete 문이 실행 되었다. remove() 에서도 쓰기 지연이 사용되고 있다는 이야기이다.

플러시(flush())

entityManager 의 flush() 는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하게 한다. flush() 를 실행하면 아래와 같은 일들이 발생한다.

  1. 변경 감지가 동작해서 1차 캐시 내에 있는 entity 와 스냅샷을 비교해서 update 문을 쓰기 지연 SQL 저장소에 등록한다.

  2. 쓰기 지연 SQL 저장소에 쌓인 쿼리를 데이터 베이스에 전송한다.

주의해야 할 점이 entityManager의 flush 만으로는 데이터베이스에 commit() 이 발생하지는 않는다는 것이다. 그리고 flush() 를 실행한다고 해서 1차 캐시가 비워지는 것도 아니다. 단지 영속성 컨텍스트의 내용을 데이터베이스에 동기화 시키는 행위이다.

    /**
     * Synchronize the persistence context to the
     * underlying database.
     * @throws TransactionRequiredException if there is
     *         no transaction or if the entity manager has not been
     *         joined to the current transaction
     * @throws PersistenceException if the flush fails
     */
    public void flush();

영속성 컨텍스트를 플러시하는 방법은 아래 세 가지이다.

  1. 직접 호출 entityManager.flush(); 로 직접 호출을 한다. 실무에서는 스프링 데이터 JPA 를 사용하여 내부적으로 자동으로 해주므로 이렇게 직접 호출 할 일은 없다.

  2. 트랜잭션 커밋시 플러시 자동 호출 JPA 에서 트랜잭션이 완료되면 커밋 직전에 플러시를 자동으로 호출해준다. 스프링 데이터 JPA 를 사용하면 이 방식으로 플러시를 계속 호출하는 것이다.

  3. JPQL 쿼리 실행시 플러시 자동 호출 JPQL 쿼리를 실행할 경우 자동으로 플러시가 실행된다. 아래 코드 마지막 줄에서 플러시로 인해서 insert 문들이 실행된다.

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);

//중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class); 
List<Member> members= query.getResultList();

병합(merge())

책에서는 '준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환한다' 고 나와있는데, 그렇다고 해서 파라미터로 반드시 준영속 상태의 엔티티만 받는 것은 아니다. 영속 상태의 엔티티도 파라미터로 받을 수 있다.

    /**
     * Merge the state of the given entity into the
     * current persistence context.
     * @param entity  entity instance
     * @return the managed instance that the state was merged to
     * @throws IllegalArgumentException if instance is not an
     *         entity or is a removed entity
     * @throws TransactionRequiredException if there is no transaction when
     *         invoked on a container-managed entity manager of that is of type 
     *         <code>PersistenceContextType.TRANSACTION</code>
     */    
    public <T> T merge(T entity);

위 설명에도 나와 있듯이 파라미터로 받은 엔티티를 영속성 컨텍스트에 병합시킬 뿐이다.

여기서 기억해야할 것은 결국 병합의 결과로 영속성 컨텍스트 내에서 관리되고 있는 객체를 반환한다는 것이다. 따라서 후속 처리가 있다면 파라미터로 넘긴 엔티티가 아니라 반환된 것을 사용해야한다.

이걸 굳이 짚고 넘어가는 의미는 실무에서 다룰 스프링 데이터 JPA 의 save() 의 원형이 아래와 같기 때문이다.

@Transactional
@Override
public <S extends T> S save(S entity) {

    Assert.notNull(entity, "Entity must not be null");

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

정리

사실 실무에서는 결국 스프링 데이터 JPA 를 사용하기 때문에 EntityManager 를 직접 다룰 일은 적어도 나의 경험 안에서는 없어서 이번 장이 어떻게 보면 큰 의미가 없어 보일 수도 있다. 하지만 경험상 결국 스프링 데이터 JPA 를 잘 다루려면 하이버네이트가 내부적으로 어떻게 동작하고 있는지에 대해서 깊게 이해하고 있어야 한다.

그런 의미에서 이번 장에서 학습한 영속성 컨텍스트의 구조와 원리, CRUD 연산마다 영속성 컨텍스트가 내부적으로 어떻게 동작하는지 등에 관한 내용들이 JPA 학습을 성공적으로 잘 하기 위해 매우 필수적인 것들이라고 생각한다.

위에서 1차 캐시와 dirty check 에서 확인한 바와 같이, JPA 는 트랜젝션이 끝나는 순간(= commit 이 수행되는 순간)에 쿼리 요청을 수행한다. 이를 라고 하는데 위에서 다룬 1차 캐시와 밀접한 연관이 있다.

Hibernate tries to defer the Persistence Context flushing up until the last possible moment. This strategy has been traditionally known as .

Write behind
transactional write-behind