[Spring] Persistence Context

배창민·2025년 10월 23일
post-thumbnail

JPA Persistence Context

1. 개념 정리

1-1. EntityManager

  • 엔터티 저장·조회·수정·삭제를 담당하는 진입점
  • 스레드 세이프 아님 → 스레드 공유 금지
  • 웹 애플리케이션에서는 보통 요청 단위(request scope)로 사용

1-2. EntityManagerFactory

  • EntityManager 생성 팩토리
  • 스레드 세이프 → 애플리케이션 전역 싱글톤으로 1개 운영
  • 생성 비용이 크므로 재사용 권장

1-3. Persistence Context

  • 엔터티를 1차 캐시로 관리하는 저장소(키-값)
  • EntityManager 생성 시 함께 생성되며, EM을 통해 접근/관리
  • 변경 감지(dirty checking), 쓰기 지연(write-behind), 동일성 보장 등 제공

2. EntityManager/Factory 생성 패턴

public class EntityManagerFactoryGenerator {
    private static final EntityManagerFactory factory =
        Persistence.createEntityManagerFactory("jpatest");
    private EntityManagerFactoryGenerator() {}
    public static EntityManagerFactory getInstance() { return factory; }
}

public class EntityManagerGenerator {
    public static EntityManager getInstance() {
        return EntityManagerFactoryGenerator.getInstance().createEntityManager();
    }
}

테스트 포인트

  • 팩토리는 싱글톤 같아야 함(동일 인스턴스/해시)
  • 매 호출마다 EntityManager는 다른 인스턴스여야 함

3. CRUD 필수 어노테이션과 흐름

3-1. 엔티티 매핑 기본

@Entity(name = "Section02Menu")
@Table(name = "tbl_menu")
public class Menu {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "menu_code")  private int menuCode;

  @Column(name = "menu_name")   private String menuName;
  @Column(name = "menu_price")  private int menuPrice;
  @Column(name = "category_code") private int categoryCode;
  @Column(name = "orderable_status") private String orderableStatus;

  protected Menu() {}
  public Menu(String menuName, int menuPrice, int categoryCode, String orderableStatus) { ... }
}

3-2. 엔티티 인식 오류 해결

  • Gradle 일반 프로젝트에서 Unable to locate persister 발생 시
  • persistence.xml<persistence-unit>에 엔티티를 <class>로 명시
<persistence-unit name="jpatest">
  <class>com.ohgiraffers.section02.crud.Menu</class>
</persistence-unit>

(Spring Boot는 보통 스캔 문제 없음)

3-3. CRUD 핵심 API

  • find(Entity.class, id) 기본키 조회
  • 트랜잭션 제어: getTransaction().begin() / commit() / rollback()
  • persist(entity) 저장(커밋 시 INSERT)
  • 변경 감지: 영속 상태에서 세터 변경 → 커밋 시 UPDATE
  • remove(entity) 삭제(커밋 시 DELETE)

예시

// insert
EntityTransaction tx = em.getTransaction();
tx.begin();
em.persist(newMenu);
tx.commit();

// update (영속 엔티티 변경감지)
tx.begin();
foundMenu.setMenuName("변경");
tx.commit();

// delete
tx.begin();
em.remove(foundMenu);
tx.commit();

4. 엔티티 생명주기와 제어

상태 분류

  1. 비영속(Transient): 새로 생성, PC 미관리
  2. 영속(Managed): PC가 관리 중, 변경감지/동일성 보장
  3. 준영속(Detached): PC와 분리, 변경 감지 없음
  4. 삭제(Removed): 삭제 예약 상태

4-1. 비영속 테스트 핵심

  • 동일 필드라도 비영속 객체는 EntityManager.contains()가 false
  • 조회로 얻은 영속 객체는 contains()가 true
Menu managed = em.find(Menu.class, 1);
Menu transientMenu = new Menu(...);
assertTrue(em.contains(managed));
assertFalse(em.contains(transientMenu));

4-2. 영속성 테스트 핵심

  • 동일 EntityManager에서 동일 id 재조회 → 동일 인스턴스
  • 다른 EntityManager로 각각 조회 → 다른 인스턴스
Menu a1 = em.find(Menu.class, 1);
Menu a2 = em.find(Menu.class, 1);
assertEquals(a1, a2); // 같은 EM

Menu b1 = lifeCycle.findMenuByMenuCode(1); // 내부적으로 다른 EM 사용
Menu b2 = lifeCycle.findMenuByMenuCode(1);
assertNotEquals(b1, b2);

4-3. detach/merge

  • detach(entity) 특정 엔티티만 준영속화 → 변경감지 비활성
  • merge(detached) 준영속 객체 값을 영속 엔티티에 병합하여 반환

핵심 시나리오 요약

  • detach 후 값 변경 + flush → DB 반영 안 됨
  • detach 후 merge + flush/commit → DB 반영
tx.begin();
Menu m = em.find(Menu.class, id);
em.detach(m);
m.setMenuPrice(1000);
em.flush(); // 반영 안 됨
tx.rollback();

tx.begin();
Menu m2 = em.find(Menu.class, id);
em.detach(m2);
m2.setMenuPrice(1000);
em.merge(m2);
em.flush(); // 반영 됨
tx.rollback();

신규 저장 케이스(식별자 수동 세팅)

tx.begin();
detached.setMenuCode(999);
em.merge(detached);  // 식별자 기준으로 없으면 insert
tx.commit();

4-4. clear/close

  • clear() PC 초기화 → 모든 영속 엔티티가 준영속으로 전환
  • close() PC 종료 → 이후 EM 사용 시 IllegalStateException
Menu m = em.find(Menu.class, 1);
em.clear();
Menu refetched = em.find(Menu.class, 1);
assertNotEquals(m, refetched);

em.close();
assertThrows(IllegalStateException.class, () -> em.find(Menu.class, 1));

4-5. remove

  • 영속 엔티티에 remove() 호출 → 커밋/flush 시 DELETE
  • 트랜잭션 커밋 전에는 영구 반영되지 않음
tx.begin();
Menu m = em.find(Menu.class, 1);
em.remove(m);
em.flush();
assertNull(em.find(Menu.class, 1)); // 1차 캐시 관점
tx.rollback();

5. 테스트 포인트 요약

  • 팩토리 싱글톤, EM 비공유 보장
  • 영속성 컨텍스트 동일성 보장(같은 EM + 같은 id → 동일 객체)
  • 변경 감지 동작 확인(영속 상태에서 세터 변경 → 커밋 시 UPDATE)
  • detach/merge/clear/close의 효과와 타이밍 구분
  • remove는 트랜잭션 커밋 시점에 반영
  • 일반 Gradle 프로젝트에서는 persistence.xml<class> 등록 문제 확인

6. 체크리스트

  • EntityManager를 스레드 간 공유하지 않는가
  • EntityManagerFactory는 애플리케이션 싱글톤으로 1개만 생성되는가
  • persistence.xmlpersistence-unit 이름과 코드 설정이 일치하는가
  • 엔티티에 식별자 전략과 컬럼 매핑이 정확한가
  • 트랜잭션 경계를 begin/commit/rollback으로 명확히 제어하는가
  • detach/merge/clear/close의 용도와 효과를 테스트로 검증했는가

7. 자주 보는 실수와 해결

  • 엔티티 인식 실패
    persistence.xml<class> 추가, 패키지/경로 점검
  • 변경감지 미작동
    → 엔티티가 영속 상태인지 확인(contains), 트랜잭션 경계 점검
  • EM 재사용으로 인한 교차 접근
    → 요청 단위로 EM 생성/종료, 의도치 않은 공유 금지
profile
개발자 희망자

0개의 댓글