Loading the catalog…
Loading the catalog…
김영한님의 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 정리한 글입니다. 실습 환경: PostgreSQL + Hibernate 6(jakarta) 1. JPA에서 가장 중요한 2가지 객체와 관계형 데이터베이스 매핑하기 (ORM, Object Relational Mapping) → 설계와 관련된 부분. "엔티티를 테이블에 어떻게 매핑할까?" 영속성 컨텍스트 → JPA가 내부에서 실제로 어떻게 동작하는지 에 대한 부분 이번 글은 2번, 영속성 컨텍스트 를 다룹니다. 이 부분을 이해하면 " persist() 를 했는데 왜 INSERT가 바로 안 나가지?", "수정할 때 왜 update() 가 없지?" 같은 궁금증이 전부 풀립니다. 2. 엔티티 매니저 팩토리와 엔티티 매니저 [요청1] → EntityManager1 ─┐ ├─ (EntityManagerFactory가 생성) [요청2] → EntityManager2 ─┘ │ └─ 커넥션 풀의 conn 사용 → DB EntityManagerFactory : 애플리케이션에 하나 만 존재합니다. EntityManager : 고객의 요청이 올 때마다 팩토리가 새로 만들어 줍니다. EntityManager는 DB 작업이 필요할 때 커넥션 풀에서 커넥션을 하나 빌려서 사용합니다. 3. 영속성 컨텍스트란? "엔티티를 영구 저장하는 환경" 이라는 뜻입니다. em.persist(member); 이 코드를 "DB에 저장한다"로 이해하기 쉽지만, 정확히는 "member 엔티티를 영속성 컨텍스트에 저장한다" 는 뜻입니다. DB에 실제로 저장되는 시점은 그 뒤입니다. 아래에서 자세히 다룹니다. 눈에 보이지 않는 논리적 개념 영속성 컨텍스트는 논리적인 개념 이라 코드에서 직접 볼 수 없습니다. 대신 EntityManager를 통해서 접근 합니다. 쉽게 비유하면 이렇습니다. EntityManager = 창구 직원 , 영속성 컨텍스트 = 창구 뒤에 있는 작업 공간(메모리) 우리는 창구 직원( em )에게 요청만 하고, 직원은 뒤쪽 작업 공간에서 엔티티를 관리합니다. 환경에 따른 관계 환경 EntityManager : 영속성 컨텍스트 J2SE (지금 실습처럼 main 메서드) 1 : 1 J2EE, 스프링 같은 컨테이너 환경 N : 1 (같은 트랜잭션이면 같은 영속성 컨텍스트 공유) 지금은 "EntityManager를 하나 만들면 영속성 컨텍스트도 하나 생긴다" 정도로 이해하면 충분합니다. 4. 엔티티의 생명주기 엔티티는 영속성 컨텍스트와의 관계에 따라 4가지 상태 를 가집니다. 상태 의미 어떻게 되나 비영속 (new/transient) 영속성 컨텍스트와 전혀 관계없는 새 객체 new Member() 영속 (managed) 영속성 컨텍스트가 관리하는 상태 persist() , find() , JPQL 조회 준영속 (detached) 관리받다가 분리된 상태 detach() , clear() , close() 삭제 (removed) 삭제하기로 예약된 상태 remove() detach/clear/close ┌────────────────────────────→ [준영속] │ ←──── merge() ────────┘ [비영속] ── persist() ──→ [영속] ── flush() ──→ DB │ ↑ remove() persist() ↓ │ [삭제] ── flush() ──→ DB 비영속 // 객체를 생성만 한 상태 → JPA와 아무 관계 없음 Member member = new Member(); member.setId(1L); member.setName("회원1"); 그냥 평범한 자바 객체입니다. 영속 EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); // 객체를 저장한 상태 → 영속성 컨텍스트가 관리 시작 em.persist(member); ⚠️ 영속 상태가 됐다고 DB에 저장된 것은 아닙니다! persist() 시점에는 INSERT 쿼리가 나가지 않습니다. 트랜잭션을 커밋하는 시점 에 나갑니다. 직접 확인하고 싶으면 persist() 전후와 commit() 전후에 System.out.println 을 찍어 보세요. 준영속, 삭제 // 영속성 컨텍스트에서 분리 → 준영속 em.detach(member); // 삭제 예약 → 커밋 시 DELETE 쿼리 실행 em.remove(member); 5. 영속성 컨텍스트를 쓰면 좋은 점 애플리케이션과 DB 사이에 중간 계층 이 하나 생기는 셈입니다. 중간 계층이 있으면 버퍼링 과 캐싱 을 할 수 있고, 그 덕분에 아래 기능들이 생깁니다. 1차 캐시 동일성(identity) 보장 트랜잭션을 지원하는 쓰기 지연 변경 감지 (Dirty Checking) 지연 로딩 (Lazy Loading) → 뒤에서 다룸 5-1. 1차 캐시 영속성 컨텍스트 내부에는 1차 캐시 라는 Map이 있습니다. @Id (Key) Entity (Value) 1L member 객체 persist() 를 하면 1차 캐시에 먼저 저장 됩니다. 1차 캐시에서 조회 Member member = new Member(); member.setId(1L); member.setName("회원1"); em.persist(member); // 1차 캐시에 저장 Member findMember = em.find(Member.class, 1L); // 1차 캐시에서 조회 → SELECT 안 나감! find() 를 하면 DB보다 1차 캐시를 먼저 확인 합니다. 있으면 DB에 가지 않고 캐시에서 바로 꺼내 줍니다. DB에서 조회 Member findMember2 = em.find(Member.class, 2L); 1차 캐시에서 2L을 찾음 → 없음 DB에서 조회 (SELECT 쿼리 실행) 조회한 결과를 1차 캐시에 저장 엔티티 반환 그래서 같은 엔티티를 두 번 조회하면 SELECT는 한 번만 나갑니다. Member m1 = em.find(Member.class, 2L); // SELECT 쿼리 실행 Member m2 = em.find(Member.class, 2L); // 1차 캐시에서 조회 → 쿼리 없음 💡 1차 캐시는 성능상 큰 이점은 아닙니다. EntityManager는 보통 트랜잭션 단위 로 만들고 트랜잭션이 끝나면 사라집니다. 그래서 1차 캐시도 한 트랜잭션 안에서만 살아 있습니다. 여러 사용자가 공유하는 캐시는 2차 캐시 라고 부르며, 별개의 개념입니다. 1차 캐시는 성능보다 아래의 동일성 보장, 변경 감지를 가능하게 해 주는 기반 이라는 점이 더 중요합니다. 5-2. 영속 엔티티의 동일성 보장 Member a = em.find(Member.class, 1L); Member b = em.find(Member.class, 1L); System.out.println(a == b); // true 같은 1차 캐시에서 꺼내기 때문에 완전히 같은 인스턴스 입니다. 마치 자바 컬렉션에서 같은 key로 두 번 꺼낸 것 처럼 동작합니다. 💡 어려운 말로 하면 1차 캐시 덕분에 반복 가능한 읽기(REPEATABLE READ) 수준의 트랜잭션 격리를 DB가 아니라 애플리케이션 차원에서 제공합니다. 즉 같은 트랜잭션 안에서 같은 엔티티를 몇 번 조회하든 항상 같은 결과를 보장합니다. 5-3. 트랜잭션을 지원하는 쓰기 지연 (엔티티 등록) EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); // 트랜잭션 시작 em.persist(memberA); em.persist(memberB); // 여기까지 INSERT SQL을 DB에 보내지 않음! tx.commit(); // 커밋하는 순간 INSERT SQL을 한꺼번에 보냄 내부 동작 영속성 컨텍스트 안에는 1차 캐시 말고도 쓰기 지연 SQL 저장소 가 있습니다. 1 em.persist(memberA) memberA를 1차 캐시에 저장 INSERT A 쿼리를 만들어서 쓰기 지연 SQL 저장소에 쌓아 둠 2 em.persist(memberB) memberB를 1차 캐시에 저장 INSERT B 쿼리를 쓰기 지연 SQL 저장소에 추가 3 tx.commit() flush : 쌓아 둔 SQL(INSERT A, INSERT B)을 DB로 전송 commit : 실제 DB 트랜잭션 커밋 왜 모아서 보낼까? 비유: 마트에서 물건 하나 살 때마다 계산대에 가지 않고, 장바구니에 담았다가 한 번에 계산 하는 것과 같습니다. 어차피 커밋 전에는 DB에 반영돼도 확정된 게 아닙니다. 그러니 커밋 직전에만 보내면 됩니다. 모아서 보내면 DB와의 통신 횟수를 줄일 수 있습니다. <!-- persistence.xml: 쿼리를 최대 10개씩 모아서 한 번에 전송 --> <property name="hibernate.jdbc.batch_size" value="10"/> 💡 예외 : 기본 키 생성 전략이 IDENTITY (DB의 auto increment, PostgreSQL의 serial / identity )라면 DB에 INSERT를 해야 ID를 알 수 있습니다. 그래서 persist() 시점에 바로 INSERT가 나갑니다. 기본 키 매핑 파트에서 자세히 다룹니다. 5-4. 변경 감지 (Dirty Checking) - 엔티티 수정 tx.begin(); // 영속 엔티티 조회 Member memberA = em.find(Member.class, 1L); // 영속 엔티티 데이터 수정 memberA.setName("hi"); // em.update(memberA); ← 이런 코드가 있어야 할 것 같지만... 필요 없음! tx.commit(); // UPDATE 쿼리가 자동으로 나감 JPA의 목표는 "자바 컬렉션처럼 다루기" 입니다. 컬렉션에서 꺼낸 객체의 값을 바꾸고 나서 list.set() 을 다시 호출하지 않는 것과 같은 원리입니다. 내부 동작: 스냅샷 1차 캐시에는 사실 스냅샷 칸이 하나 더 있습니다. @Id Entity 스냅샷 1L memberA (현재 값) memberA (처음 조회했을 때 값) commit() → 내부적으로 flush() 호출 엔티티와 스냅샷을 하나하나 비교 바뀐 게 있으면 UPDATE SQL을 만들어 쓰기 지연 SQL 저장소에 등록 SQL을 DB로 전송 (flush) DB 커밋 ⚠️ 변경 감지는 영속 상태 인 엔티티에만 동작합니다. 준영속이나 비영속 엔티티는 값을 바꿔도 UPDATE가 나가지 않습니다. 💡 Hibernate는 기본적으로 모든 컬럼을 UPDATE 합니다. (바뀐 컬럼만 보내지 않음) 쿼리 모양이 항상 같아서 재사용하기 좋기 때문입니다. 바뀐 컬럼만 보내고 싶다면 엔티티에 @DynamicUpdate 를 붙이면 됩니다. 5-5. 엔티티 삭제 Member memberA = em.find(Member.class, 1L); em.remove(memberA); // 커밋 시점에 DELETE 쿼리 실행 등록과 마찬가지로 DELETE 쿼리도 쓰기 지연 SQL 저장소에 쌓였다가 커밋 시점에 나갑니다. 6. 플러시 (flush) 영속성 컨텍스트의 변경 내용을 DB에 반영(동기화)하는 것 플러시가 일어나면 생기는 일 변경 감지 동작 수정된 엔티티의 UPDATE 쿼리를 쓰기 지연 SQL 저장소에 등록 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송 (등록, 수정, 삭제 쿼리) ⚠️ 플러시 ≠ 커밋 플러시는 쿼리를 DB에 보내기만 하는 것이고, 확정은 커밋이 합니다. 플러시 후에 롤백하면 DB에 반영된 내용도 취소됩니다. 플러시하는 방법 3가지 방법 설명 em.flush() 직접 호출 (거의 안 씀. 테스트할 때 가끔 사용) 트랜잭션 커밋 플러시 자동 호출 JPQL 쿼리 실행 플러시 자동 호출 Member member = new Member(200L, "member200"); em.persist(member); em.flush(); // 이 시점에 INSERT 쿼리가 바로 나감 System.out.println("=========="); tx.commit(); JPQL을 실행하면 왜 자동으로 플러시될까? em.persist(memberA); em.persist(memberB); em.persist(memberC); // 아직 INSERT는 쓰기 지연 저장소에만 있고 DB에는 없음 // 중간에 JPQL 실행 List<Member> members = em.createQuery("select m from Member m", Member.class) .getResultList(); JPQL은 1차 캐시를 거치지 않고 SQL로 번역돼서 바로 DB에 날아갑니다. 그런데 이 시점의 DB에는 memberA, B, C가 아직 없습니다. 플러시를 하지 않으면 방금 저장한 데이터가 조회되지 않는 문제가 생깁니다. 그래서 JPA는 이런 실수를 막기 위해 JPQL을 실행하기 전에 자동으로 플러시 합니다. 플러시 모드 옵션 em.setFlushMode(FlushModeType.COMMIT); 모드 동작 FlushModeType.AUTO 커밋이나 쿼리 실행 시 플러시 ( 기본값, 이걸 그대로 쓰면 됨 ) FlushModeType.COMMIT 커밋할 때만 플러시 플러시 핵심 정리 플러시는 영속성 컨텍스트를 비우지 않습니다. (1차 캐시는 그대로 남아 있음) 영속성 컨텍스트의 변경 내용을 DB에 동기화 하는 것입니다. 트랜잭션이라는 작업 단위가 중요합니다 → 커밋 직전에만 동기화하면 됩니다. 7. 준영속 상태 영속 상태였던 엔티티가 영속성 컨텍스트에서 분리(detached)된 상태 분리됐기 때문에 영속성 컨텍스트가 제공하는 기능(1차 캐시, 변경 감지 등)을 사용할 수 없습니다. 준영속 상태로 만드는 방법 메서드 동작 em.detach(entity) 특정 엔티티만 준영속으로 전환 em.clear() 영속성 컨텍스트를 완전히 초기화 (1차 캐시 전부 비움) em.close() 영속성 컨텍스트를 종료 예제 1: detach하면 변경 감지가 안 된다 Member member = em.find(Member.class, 1L); // 영속 member.setName("AAAAA"); em.detach(member); // 준영속으로 분리 tx.commit(); // UPDATE 쿼리 안 나감! (SELECT만 한 번 나감) JPA가 더 이상 member를 관리하지 않기 때문에 값을 바꿔도 모릅니다. 예제 2: clear 후 다시 조회하면 SELECT가 또 나간다 Member member1 = em.find(Member.class, 1L); // SELECT 쿼리 em.clear(); // 1차 캐시 초기화 Member member2 = em.find(Member.class, 1L); // 캐시가 비었으므로 SELECT 쿼리 또 나감 System.out.println(member1 == member2); // false! (다른 인스턴스) 💡 em.clear() 는 테스트 코드에서 자주 씁니다. 1차 캐시 때문에 DB에 가지 않고 캐시 값이 나오는 걸 막고, DB에서 실제로 어떻게 조회되는지 확인하고 싶을 때 사용합니다. 💡 준영속 엔티티를 다시 영속으로 만들 때는 em.merge() 를 씁니다. 웹 애플리케이션 파트에서 자세히 다룹니다. 정리 개념 한 줄 요약 영속성 컨텍스트 엔티티를 관리하는 논리적인 공간. EntityManager로 접근 생명주기 비영속 → 영속 → 준영속/삭제 1차 캐시 영속성 컨텍스트 안의 Map. 조회 시 DB보다 먼저 확인 동일성 보장 같은 트랜잭션에서 같은 ID로 조회하면 == 비교가 true 쓰기 지연 SQL을 모아 뒀다가 커밋 시점에 한 번에 전송 변경 감지 스냅샷과 비교해서 바뀐 엔티티는 자동으로 UPDATE 플러시 변경 내용을 DB에 동기화 (비우는 게 아님, 커밋도 아님) 준영속 영속성 컨텍스트에서 분리 → 변경 감지 등 기능 사용 불가
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
영속성 관리 - 영속성 컨텍스트. 김영한님의 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 정리한 글입니다. 실습 환경: PostgreSQL + Hibernate 6(jakarta) 1. JPA에서 가장 중요한 2가지 객체와 관계형 데이터베이스 매핑하기 (ORM, Object Relational Mapping) → 설계와 관련된 부분. "엔티티를 테이블에 어떻게 매핑할까?" 영속성 컨텍스트 → JPA가 내부에서 실제로 어떻게 동작하는지 에 대한 부분 이번 글은 2번, 영속성 컨텍스트 를 다룹니다. 이 부분을 이해하면 " persist() 를 했는데 왜 INSERT가 바로 안 나가지?", "수정할 때 왜 update() 가 없지?" 같은 궁금증이 전부 풀립니다. 2. 엔티티 매니저 팩토리와 엔티티 매니저…
Open source