영속성 컨텍스트(Persistence Context)

JOY🌱·2023년 4월 6일
0

🐸 JPA

목록 보기
2/8
post-thumbnail

💁‍♀️ 영속성 컨텍스트(Persistence Context)란,
엔티티를 영구 저장하는 환경. 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할. 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리.

  • 영속성 엔터티를 key value방식으로 저장하는 저장소 역할
  • 영속성 컨텍스트는 엔터티 매니저를 생성할 때 하나 만들어짐
  • 그리고 엔터티 매니저를 통해서 영속성 컨텍스트에 접근할 수 있고 영속성 컨텍스트를 관리 가능

💁‍♀️ 엔터티 매니저(EntityManager)란,
엔터티를 저장하는 메모리상의 데이터베이스와 같음. 엔터티를 저장하고 수정하고 삭제하고 조회하는 등의 엔터티와 관련된 모든 일 수행. 엔터티 매니저는 스레드세이프 하지 않아 동시성 문제가 발생할 수 있기 때문에 스레드간 공유 금지. web의 경우 일반적으로는 request scope와 일치시킴


💁‍♀️ 엔터티 매니저 팩토리(EntityManagerFactory)란,
엔터티 매니저를 생성할 수 있는 기능을 제공하는 팩토리 클래스. 스레드세이프이기 때문에 여러 스레드가 동시에 접근해도 안전하여 서로 다른 스레드간 공유해서 재사용. 하지만 스레드 세이프 한 기능을 요청 스코프마다 생성하기에는 비용(시간, 메모리)부담이 크기 때문에 application scope와 동일한 싱글톤으로 생성해서 관리하게 됨. 따라서 데이터베이스를 사용하는 애플리케이션 당 한 개의 EntityManagerFactory를 생성.


📌 Given-When-Then Pattern

💁‍♀️ Given-When-Then Pattern이란,
테스트 코드를 작성하는 표현 방식이며, 준비-실행-검증 을 의미.

👉 Given

테스트를 위해 준비하는 과정.
테스트에 사용하는 변수, 입력 값 등을 정의하거나 Mock 객체를 정의하는 구문이 포함.

👉 When

실제로 액션을 하는 테스트를 실행하는 과정.

👉 Then

테스트를 검증하는 과정.
예상한 값, 실제 실행을 통해 나온 값을 검증.


👀 엔티티 생성 방법

@Entity @Table @SequenceGenerator @Id @Column @GeneratedValue

@Entity(name="SECTION02_MENU")	// @Entity : 엔티티 객체로 만들기 위한 어노테이션. 다른 패키지에 동일 이름의 클래스가 존재하면 name 지정 필요
@Table(name="TBL_MENU")			// @Table : 데이터베이스에 매핑 될 테이블 이름 설정
@SequenceGenerator(					// @SequenceGenerator : 시퀀스 사용 시 추가해야하는 어노테이션
		name="SEQ_MENU_CODE_GENERATOR",	// name : 해당 시퀀스 설정에 대한 이름 (현재 시점에 지정)
		sequenceName="SEQ_MENU_CODE",	// sequenceName : 사용할 시퀀스 이름
		initialValue=100,				// initialValue: 초기 값 (아무 값이라도 지정은 해주어야함. DB의 설정을 따름)
		allocationSize=1				// allocationSize : 증가 값
		)
public class Menu {
	
	@Id							// @Id : PK에 해당하는 속성에 지정
	@Column(name="MENU_CODE")	// @Column : 데이터베이스에 대응되는 컬럼명 지정
	@GeneratedValue(								// @GeneratedValue : 생성되어있는 시퀀스를 PK 컬럼에 적용하기위한 어노테이션
			strategy=GenerationType.SEQUENCE,		// strategy : 값 생성 시, 시퀀스 전략을 이용하겠다는 설정
			generator="SEQ_MENU_CODE_GENERATOR"		// generator : 사용할 시퀀스 설정 이름
			)
	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;

	/* 생성자, getter&setter, toString */
    
}

👀 EntityManager CRUD tests

📌 test전 필드 및 메소드 선언

private static EntityManagerFactory entityManagerFactory;
private EntityManager entityManager;
	
@BeforeAll
public static void initFectory() {
	entityManagerFactory = Persistence.createEntityManagerFactory("jpatest"); 
        					/* xml 파일에 작성한 persistence-unit의 이름을 입력 */
}
	
@BeforeEach
public void initManager() {
	entityManager = entityManagerFactory.createEntityManager();
}
	
@AfterAll
public static void closeFactory() {
	entityManagerFactory.close();
}
	
@AfterEach
public void closeManager() {
	entityManager.close();
}

👉 삽입 (Create)

getTransaction() begin() persist() commit() rollback() contains()

@Test
public void 새로운_메뉴_추가_테스트() {
		
	// given
	Menu menu = new Menu();
	menu.setMenuName("JPA산 짬뽕맛 막걸리");
	menu.setMenuPrice(15000);
	menu.setCategoryCode(7);
	menu.setOrderableStatus("Y");
		
	// when
	EntityTransaction entityTransaction = entityManager.getTransaction(); // entityManager로 부터 EntityTransaction를 가져와서,
	entityTransaction.begin(); // 트랜잭션 시작
	try {
		entityManager.persist(menu); // persist() : 등록을 하기 위한 메소드
		entityTransaction.commit();
	} catch (Exception e) {
		entityTransaction.rollback();
		e.printStackTrace();
	}
		
	// then
	assertTrue(entityManager.contains(menu));
	}

👉 조회 (Read)

find()

@Test
public void 메뉴코드로_메뉴_조회_테스트() {
		
	// given
	int menuCode = 181;
		
	// when
	Menu foundMenu = entityManager.find(Menu.class, menuCode);
		
	// then
	assertNotNull(foundMenu);
	assertEquals(menuCode, foundMenu.getMenuCode());
	System.out.println("foundMenu = " + foundMenu);
}

👉 수정 (Update)

😈 Warning
JPA를 활용하여 update & delete 작업을 수행 시 주의
DB가 아닌 자바 내의 객체를 setter를 이용하여 변경하거나, 자바 내에서 remove()를 이용하여 삭제하기 때문에 반드시 find()로 해당되는 엔티티를 가져와야함

@Test
public void 메뉴_이름_수정_테스트() {
		
	// given
	Menu menu = entityManager.find(Menu.class, 2); // update 전, select 요청을 먼저 해야함
	System.out.println("menu = " + menu);
		
	// 가공처리
	String menuNameToChange = "우럭뽈살젤리";
		
	// when
	EntityTransaction entityTransaction = entityManager.getTransaction();
	entityTransaction.begin();
		
	try {
		menu.setMenuName(menuNameToChange); // DB가 아닌 객체를 다룬다고 생각하고 바꿀 이름으로 setter를 이용하여 필드의 값을 변경
		entityTransaction.commit();
	} catch(Exception e) {
		entityTransaction.rollback();
		e.printStackTrace();
	}
		
	// then
	assertEquals(menuNameToChange, entityManager.find(Menu.class, 2).getMenuName());
}

👉 삭제 (Delete)

remove()

@Test
public void 메뉴_삭제하기_테스트() {
		
	// given
	Menu menuToRemove = entityManager.find(Menu.class, 0); // delete 전, select 요청을 먼저 해야함
	System.out.println("menuToRemove = " + menuToRemove);
		
	// when
	EntityTransaction entityTransaction = entityManager.getTransaction();
	entityTransaction.begin();
				
	try {
		entityManager.remove(menuToRemove); // 조회해온 menuToRemove를 remove()를 이용하여 삭제
		entityTransaction.commit();
	} catch(Exception e) {
		entityTransaction.rollback();
		e.printStackTrace();
	}
		
	// then
	Menu removedMenu = entityManager.find(Menu.class, 0);
	assertEquals(null, removedMenu); // 삭제되고나서의 값이 0인지 확인
}

👀 Entity life cycle tests

📌 test전 필드 및 메소드 선언

/* 위와 동일 */

👉 비영속

@Test
public void 비영속성_테스트() {
		
	// given
	Menu foundMenu = entityManager.find(Menu.class, 11);
		
	/* 객체만 생성하면 영속성 컨텍스트나 데이터 베이스와 관련 없는 ★비영속 상태★ */
	Menu newMenu = new Menu();
	newMenu.setMenuCode(foundMenu.getMenuCode());
	newMenu.setMenuName(foundMenu.getMenuName());
	newMenu.setMenuPrice(foundMenu.getMenuPrice());
	newMenu.setCategoryCode(foundMenu.getCategoryCode());
	newMenu.setOrderableStatus(foundMenu.getOrderableStatus());
		
	// when
	boolean isTrue = (foundMenu == newMenu);
		
	// then
	assertFalse(isTrue); // 영속 상태와 비영속 상태는 같은 객체 X => false
}

👉 영속

@Test
public void 영속성_연속_조회_테스트() {
		
	/* 엔티티 매니저가 영속성 컨텍스트에 엔티티 객체를 저장(persist)하면 영속성 컨텍스트가 엔티티 객체를 관리하게 되고 이를 '영속 상태'라고 함
	 * find(), JPQL을 사용한 조회도 '영속 상태' */
		
	// given
	Menu foundMenu1 = entityManager.find(Menu.class, 11);
	Menu foundMenu2 = entityManager.find(Menu.class, 11); 
	/* 같은 메뉴를 다시 조회 요청을 했을 때, 영속성 컨텍스트 안에 있는지 먼저 확인 (있으면 그 안에 있는 객체를 반환, 없으면 db에서 select) => 동일성 보장 */
		
	// when
	boolean isTrue = (foundMenu1 == foundMenu2);
		
	// then
	assertTrue(isTrue); // true
	/* .find()로 같은 메뉴를 조회하는 것을 두 번 요청했으나, select 구문은 한번 날아감
	 * => 이미 첫 번째 .find() 요청에서 DB에서 조회하고 영속성 컨텍스트 안에 담겨 '영속 상태'가 되었기 때문에 
	 * 두 번째 .find() 요청에서는 DB까지 가지 않고 영속성 컨텍스트 안에 담겨있는 것을 조회해옴 */
}
@Test
public void 영속성_객체_추가_테스트() {
		
	/* Menu Entity에서 잠시 시퀀스 설정 주석 처리 후 테스트 진행 */
		
	// given
	Menu menuToRegist = new Menu();
	menuToRegist.setMenuCode(500); 
	menuToRegist.setMenuName("고당도수박스프");
	menuToRegist.setMenuPrice(22000);
	menuToRegist.setCategoryCode(7);
	menuToRegist.setOrderableStatus("Y");
		
	// when
	entityManager.persist(menuToRegist); // 영속성 컨텍스트 안에 저장
	Menu foundMenu = entityManager.find(Menu.class, 500);
	boolean isTrue = (menuToRegist == foundMenu);
		
	// then
	assertTrue(isTrue); // 영속성 객체로 추가되었는지 확인	
}

👉 준영속

detach() clear() close()

@Test
public void 준영속성_detach_테스트() {
		
	// given
	Menu foundMenu1 = entityManager.find(Menu.class, 11);
	Menu foundMenu2 = entityManager.find(Menu.class, 12);
	// ======> 영속성 컨텍스트에서 관리되고 있는 상태
		
	/* 1. 영속성 확인 */
//	// when
//	foundMenu1.setMenuPrice(54321);
//	foundMenu2.setMenuPrice(54321);
//	
//	// then
//	assertEquals(54321, entityManager.find(Menu.class, 11).getMenuPrice());
//	assertEquals(54321, entityManager.find(Menu.class, 12).getMenuPrice());
		
	/* 2. 준영속성 확인 */
	/* 영속성 컨텍스트가 관리하던 엔티티 객체를 관리하지 않는 상태가 된다면 준영속 상태라고 함
	 * 그 중 detach는 특정 엔티티만 준영속 상태로 만듬 */
	// when
	entityManager.detach(foundMenu2); // detach() : 영속성 컨텍스트 안의 관리 목록에서 제거
		
	foundMenu1.setMenuPrice(5000);
	foundMenu2.setMenuPrice(5000);
		
	assertEquals(5000, entityManager.find(Menu.class, 11).getMenuPrice());
	assertEquals(5000, entityManager.find(Menu.class, 12).getMenuPrice()); 
	// 관리 목록에서 제거되었기 때문에 DB에서 조회해와서 새로운 객체를 만듬 
	// => 제거되기 전, 영속성 컨텍스트 안의 12번 메뉴는 5000원이었겠지만 제거되고나서 새로 DB에서 조회한 12번 메뉴는 2000원이므로 테스트 실패
}
@Test
public void 준영속성_clear_테스트() {
		
	// given
	Menu foundMenu1 = entityManager.find(Menu.class, 11);
	Menu foundMenu2 = entityManager.find(Menu.class, 12);
		
	// when
	/* clear()는 영속성 컨텍스트를 초기화 함 */
	entityManager.clear(); // 모두 관리가 되지 않는 상태가 됨 (초기화)
		
	foundMenu1.setMenuPrice(5000);
	foundMenu2.setMenuPrice(5000);
		
	// then
	assertEquals(5000, entityManager.find(Menu.class, 11).getMenuPrice());
	assertEquals(5000, entityManager.find(Menu.class, 12).getMenuPrice()); 
	/* 위와 같은 이유로 테스트 실패 */	
}
@Test
public void close_테스트() {
		
	// given
	Menu foundMenu1 = entityManager.find(Menu.class, 11);
	Menu foundMenu2 = entityManager.find(Menu.class, 12);
				
	// when
	/* close()는 영속성 컨텍스트를 종료시킴 */
	entityManager.close();
				
	foundMenu1.setMenuPrice(5000);
	foundMenu2.setMenuPrice(5000);
		
	// then
	/* 영속성 컨텍스트를 닫았기 때문에 다시 만들기 전에는 사용 불가
	 * java.lang.IllegalStateException : Session/EntityManager is closed */
	assertEquals(5000, entityManager.find(Menu.class, 11).getMenuPrice());
	assertEquals(5000, entityManager.find(Menu.class, 12).getMenuPrice()); 
}

👉 삭제

@Test
public void 삭제_remove_테스트() {
		
	/* remove : 엔티티를 영속성 컨텍스트 및 데이터 베이스에서 삭제
 	 * 단, 트랜잭션을 제어하지 않으면 영구 반영되지는 않음
	 * 트랜잭션을 커밋하는 순간 영속성 컨텍스트에서 관리하는 엔티티 객체가 데이터 베이스에 반영되게 됨 (=> flush)
	 * flush : 영속성 컨텍스트의 변경 내용을 데이터 베이스에 동기화 하는 작업(등록, 수정, 삭제한 엔티티를 데이터베이스에 반영) */
		
	// given
	Menu foundMenu = entityManager.find(Menu.class, 2);		// find한 2번 메뉴를
		
	// when
	entityManager.remove(foundMenu);						// 제거
	Menu refoundMenu = entityManager.find(Menu.class, 2);	// 다시 찾기 => 이미 삭제되었으므로 다시 찾아오기 불가
		
	// then
	assertEquals(2, foundMenu.getMenuCode());
	assertEquals(null, refoundMenu); // remove()한 2번 메뉴는 null
}

👉 병합

merge() hashCode()

@Test
public void 병합_merge_수정_테스트() {
		
	/* 병합(merge) : 파라미터로 넘어온 준영속 엔티티 객체의 식별자 값으로 1차 캐시에서 엔티티 객체를 조회
	 * 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고 1차 캐시에 저장
	 * 조회한 영속 엔티티 객체에 준영속 상태의 엔티티 객체의 값을 병합한 뒤 영속 엔티티 객체를 반환
	 * 혹은 조회할 수 없는 데이터의 경우 새로 생성해서 병합 (없으면 save, 있으면 update) */
		
	// given
	Menu menuToDetach = entityManager.find(Menu.class, 2);
	entityManager.detach(menuToDetach);
		
	// when
	menuToDetach.setMenuName("무등산수박스프"); // detach된 2번 메뉴의 메뉴명을 변경
	Menu refoundMenu = entityManager.find(Menu.class, 2);
		
	/* 준영속 엔티티와 영속 엔티티의 해쉬코드는 다른 상태임을 확인(같은 2번 메뉴이더라도) */
	System.out.println(menuToDetach.hashCode());
	System.out.println(refoundMenu.hashCode());
		
	entityManager.merge(menuToDetach); // 변경된 메뉴명과 함께 병합하여 다시 '영속 상태'
		
	// then
	Menu mergedMenu = entityManager.find(Menu.class, 2);
	assertEquals("무등산수박스프", mergedMenu.getMenuName());
}
@Test
public void 병합_merge_삽입_테스트() {
		
	// given
	Menu menuToDetach = entityManager.find(Menu.class, 2);
	entityManager.detach(menuToDetach);
		
	// when
	menuToDetach.setMenuCode(999);	// DB에서 조회할 수 없는 키 값으로 변경
	menuToDetach.setMenuName("상한수박스프");
		
	entityManager.merge(menuToDetach); // 영속 상태의 엔티티와 병합 (현재는 존재하지 않기 때문에 삽입(save))
		
	// then
	Menu mergedMenu = entityManager.find(Menu.class, 999);
	assertEquals("상한수박스프", mergedMenu.getMenuName());
}
profile
Tiny little habits make me

0개의 댓글