💁♀️ 영속성 컨텍스트(Persistence Context)란,
엔티티를 영구 저장하는 환경. 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할. 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리.
- 영속성 엔터티를 key value방식으로 저장하는 저장소 역할
- 영속성 컨텍스트는 엔터티 매니저를 생성할 때 하나 만들어짐
- 그리고 엔터티 매니저를 통해서 영속성 컨텍스트에 접근할 수 있고 영속성 컨텍스트를 관리 가능
💁♀️ 엔터티 매니저(EntityManager)란,
엔터티를 저장하는 메모리상의 데이터베이스와 같음. 엔터티를 저장하고 수정하고 삭제하고 조회하는 등의 엔터티와 관련된 모든 일 수행. 엔터티 매니저는 스레드세이프 하지 않아 동시성 문제가 발생할 수 있기 때문에 스레드간 공유 금지. web의 경우 일반적으로는 request scope와 일치시킴
💁♀️ 엔터티 매니저 팩토리(EntityManagerFactory)란,
엔터티 매니저를 생성할 수 있는 기능을 제공하는 팩토리 클래스. 스레드세이프이기 때문에 여러 스레드가 동시에 접근해도 안전하여 서로 다른 스레드간 공유해서 재사용. 하지만 스레드 세이프 한 기능을 요청 스코프마다 생성하기에는 비용(시간, 메모리)부담이 크기 때문에 application scope와 동일한 싱글톤으로 생성해서 관리하게 됨. 따라서 데이터베이스를 사용하는 애플리케이션 당 한 개의 EntityManagerFactory를 생성.
💁♀️ Given-When-Then Pattern이란,
테스트 코드를 작성하는 표현 방식이며,준비-실행-검증
을 의미.
테스트를 위해 준비하는 과정.
테스트에 사용하는 변수, 입력 값 등을 정의하거나 Mock 객체를 정의하는 구문이 포함.
실제로 액션을 하는 테스트를 실행하는 과정.
테스트를 검증하는 과정.
예상한 값, 실제 실행을 통해 나온 값을 검증.
@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 */
}
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();
}
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));
}
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);
}
😈 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());
}
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인지 확인
}
/* 위와 동일 */
@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());
}