Spring Framework/JPA

[JPA] 4. EntityLifeCycle

hyomee2 2024. 9. 13. 18:07

디렉토리 구조 및 공통 클래스 작성하기

1. 디렉토리 구조

2. EntityManagerGenerator 클래스 작성하기

package org.example.section03.entity;

public class EntityManagerGenerator {
    private static EntityManagerFactory factory
            = Persistence.createEntityManagerFactory("jpatest");

    private EntityManagerGenerator() {}

    public static EntityManager getInstance() { return factory.createEntityManager(); }

}

3. Menu 클래스 생성하기

package org.example.section03.entity;

@Entity(name = "Section03Menu") // 이름 생략 시 class명과 동일, 중복 entity 명칭 불가
@Table(name = "tbl_menu")   // 매핑 될 테이블 설정
public class Menu {

    @Id
    @Column(name = "menu_code")
//    @GeneratedValue(strategy = GenerationType.IDENTITY)
    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;

    public Menu() {}

    public Menu(int menuCode, String menuName, int menuPrice, int categoryCode, String orderableStatus) {
        this.menuCode = menuCode;
        this.menuName = menuName;
        this.menuPrice = menuPrice;
        this.categoryCode = categoryCode;
        this.orderableStatus = orderableStatus;
    }

    public int getMenuCode() {
        return menuCode;
    }

    public String getMenuName() {
        return menuName;
    }

    public int getMenuPrice() {
        return menuPrice;
    }

    public int getCategoryCode() {
        return categoryCode;
    }

    public String getOrderableStatus() {
        return orderableStatus;
    }

    public void setMenuCode(int menuCode) {
        this.menuCode = menuCode;
    }

    public void setMenuPrice(int menuPrice) {
        this.menuPrice = menuPrice;
    }

    public void setMenuName(String menuName) {
        this.menuName = menuName;
    }

    @Override
    public String toString() {
        return "Menu{" +
                "menuCode=" + menuCode +
                ", menuName='" + menuName + '\'' +
                ", menuPrice=" + menuPrice +
                ", categoryCode=" + categoryCode +
                ", orderableStatus='" + orderableStatus + '\'' +
                '}';
    }
}

4. EntityLifeCycle 클래스 작성하기

package org.example.section03.entity;

public class EntityLifeCycle {
    private EntityManager entityManager;

    public Menu findMenuByMenuCode(int menuCode) {
        entityManager = EntityManagerGenerator.getInstance();
        return entityManager.find(Menu.class, menuCode);
    }

    public EntityManager getManagerInstance() {
        return entityManager;
    }
}

EntityLifeCycle

1. 엔티티의 생명 주기

(1) 비영속 상태(New/Transient)

엔터티 객체가 생성되고 아직 영속성 컨텍스트에 저장되지 않은 상태\

 

* 테스트 코드

package org.example.section03.entity;

class EntityLifeCycleTest {

    private EntityLifeCycle entityLifeCycle;

    @BeforeEach
    void init() {
        this.entityLifeCycle = new EntityLifeCycle();
    }

    @DisplayName("비영속 테스트")
    @ParameterizedTest
    @ValueSource(ints = {1, 2})
    void testTransient(int menuCode) {
        // when
        Menu foundMenu = entityLifeCycle.findMenuByMenuCode(menuCode);  

        Menu newMenu = new Menu(
                foundMenu.getMenuCode(),
                foundMenu.getMenuName(),
                foundMenu.getMenuPrice(),
                foundMenu.getCategoryCode(),
                foundMenu.getOrderableStatus()
        ); 
        /*foundMenu: 영속 상태의 엔터티
        newMenu: 비영속 상태의 엔터티
        <구분 기준> 영속성 컨텍스트에서 관리가 되고 있냐 아니냐*/

        EntityManager entityManager = entityLifeCycle.getManagerInstance();

        //then
        assertTrue(entityManager.contains(foundMenu)); // foundMenu는 영속성 컨텍스트에서 관리 되는 영속 상태의 객체
        assertFalse(entityManager.contains(newMenu));   // newMenu는 영속성 컨텍스트에서 관리 되지 않는 비영속 상태의 객체
        // contains 함수는 영속성 컨테이너에서 관리되고 있는 엔터티에 대해서 TRUE
    }
}

 

(2) 영속 상태(Managed)

엔터티 객체가 영속성 컨텍스트에 저장된 상태로, 이 상태에서 엔터티를 수정하면 자동으로 DB에 반영된다.

 

* 테스트 코드

package org.example.section03.entity;

class EntityLifeCycleTest {

    private EntityLifeCycle entityLifeCycle;

    @BeforeEach
    void init() {
        this.entityLifeCycle = new EntityLifeCycle();
    }

    @DisplayName("다른 엔터티 매니저가 관리하는 엔터티의 영속성 테스트")
    @ParameterizedTest
    @ValueSource(ints = {1, 2})
    void testManagedOtherEntityManager(int menuCode) {
        //when
        Menu menu1 = entityLifeCycle.findMenuByMenuCode(menuCode);
        Menu menu2 = entityLifeCycle.findMenuByMenuCode(menuCode);

        //then
        assertNotEquals(menu1, menu2);  // menu1과 menu2은 다른 객체이다.
    }

    @DisplayName("같은 엔터티 매니저가 관리하는 엔터티의 영속성 테스트")
    @ParameterizedTest
    @ValueSource(ints = {1, 2})
    void testManagedSameEntityManager(int menuCode) {
        //when
        EntityManager entityManager = EntityManagerGenerator.getInstance();
        Menu menu1 = entityManager.find(Menu.class, menuCode);
        Menu menu2 = entityManager.find(Menu.class, menuCode);

        //then
        assertEquals(menu1, menu2);
    }
}

 

(3) 준영속 상태(Detached)

엔터티 객체가 영속성 컨텍스트와 분리된 상태로, 이 상태에서 엔티티를 수정하면 DB에는 반영되지 않는다.

분리된 엔터티를 다시 관리 상태로 만들어주기 위해서는 EntityManager 객체의 merge() 메소드를 사용한다.

1) 준영속성 detach 테스트

detach는 remove(삭제 상태)와 다르다.

package org.example.section03.entity;

class EntityLifeCycleTest {

    private EntityLifeCycle entityLifeCycle;

    @BeforeEach
    void init() {
        this.entityLifeCycle = new EntityLifeCycle();
    }

    @DisplayName("준영속화 detach 테스트")
    @ParameterizedTest
    @CsvSource({"11, 1000", "12, 1000"})
    void testDetachEntity(int menuCode, int menuPrice) {
        //given
        EntityManager entityManager = EntityManagerGenerator.getInstance();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        //when
        entityTransaction.begin();
        Menu foundMenu = entityManager.find(Menu.class, menuCode);
        
        // detach : 특정 엔터티만 준영속 상태로 만든다.
        entityManager.detach(foundMenu);
        
        foundMenu.setMenuPrice(menuPrice);
        
        // flush : 영속성 컨텍스트의 상태를 DB로 내보낸다. 단, commit 하지 않은 상태이므로 rollback 가능하다.
        entityManager.flush();

        // then
        assertNotEquals(menuPrice, entityManager.find(Menu.class, menuCode).getMenuPrice());
        entityTransaction.rollback();
    }

    @DisplayName("준영속화 detach 후 다시 영속화 테스트")
    @ParameterizedTest
    @CsvSource({"11, 1000", "12, 1000"})
    void testDetachAndMerge(int menuCode, int menuPrice) {
        //given
        EntityManager entityManager = EntityManagerGenerator.getInstance();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        //when
        entityTransaction.begin();
        Menu foundMenu = entityManager.find(Menu.class, menuCode);
        entityManager.detach(foundMenu);
        foundMenu.setMenuPrice(menuPrice);
        
        /* merge 할 때는 1차적으로 1차 캐시 보고, DB보고(DB에서 조회해서 1차 캐시에 저장), 그래도 없으면 새로 생성
        조회한 영속 엔터티 객체에 준영속 상태의 엔터티 객체의 값을 병합한 뒤 영속 엔터티 객체를 반환한다.
        혹은 조회할 수 없는 데이터라면 새로 생성해서 병합한다. key 값이 같은 것끼리 merge 된다.*/
        entityManager.merge(foundMenu);
        entityManager.flush();

        // then
        assertEquals(menuPrice, entityManager.find(Menu.class, menuCode).getMenuPrice());
        entityTransaction.rollback();
    }

    @DisplayName("detach 후 merge한 데이터 update 테스트")
    @ParameterizedTest
    @CsvSource({"11, 하양 민트초코죽", "12, 까만 딸기탕후루"})
    void testMergeUpdate(int menuCode, String menuName) {
        //given
        EntityManager entityManager = EntityManagerGenerator.getInstance();
        Menu foundMenu = entityManager.find(Menu.class, menuCode);
        entityManager.detach(foundMenu);

        // when
        foundMenu.setMenuName(menuName);
        Menu refoundMenu = entityManager.find(Menu.class, menuCode);

        entityManager.merge(foundMenu);
        // 두 객체의 이름은 모두 "하양 민트초코죽" 이 된다.

        //then
        assertEquals(menuName, refoundMenu.getMenuName());
    }

    @DisplayName("detach 후 merge한 데이터 save 테스트")
    @Test
    void testMergeSave() {
        // given
        EntityManager entityManager = EntityManagerGenerator.getInstance();
        EntityTransaction entityTransaction = entityManager.getTransaction();
        Menu foundMenu = entityManager.find(Menu.class, 20);
        entityManager.detach(foundMenu);

        //when
        entityTransaction.begin();
        foundMenu.setMenuName("치약맛 초코 아이스크림");
        foundMenu.setMenuCode(999);

        entityManager.merge(foundMenu);
        entityTransaction.commit();

        //then
        assertEquals("치약맛 초코 아이스크림", entityManager.find(Menu.class, 999).getMenuName());
    }
}

2) 준영속성 clear 테스트

package org.example.section03.entity;

class EntityLifeCycleTest {

    private EntityLifeCycle entityLifeCycle;

    @BeforeEach
    void init() {
        this.entityLifeCycle = new EntityLifeCycle();
    }

    @DisplayName("준영속화 clear 테스트")
    @ParameterizedTest
    @ValueSource(ints = {1, 3})
    void testClearPersistenceContext(int menuCode) {
        //given
        EntityManager entityManager = EntityManagerGenerator.getInstance();
        Menu foundMenu = entityManager.find(Menu.class, menuCode);

        //when
        // clear : 영속성 컨텍스트를 초기화 한다 -> 영속성 컨텍스트 내의 모든 엔터티는 준영속화 된다
        entityManager.clear();

        //then
        Menu expectedMenu = entityManager.find(Menu.class, menuCode);
        assertNotEquals(expectedMenu, foundMenu);
    }
}

 

3) 준영속성 close 테스트

package org.example.section03.entity;

class EntityLifeCycleTest {

    private EntityLifeCycle entityLifeCycle;

    @BeforeEach
    void init() {
        this.entityLifeCycle = new EntityLifeCycle();
    }

    @DisplayName("준영속화 close 테스트")
    @ParameterizedTest
    @ValueSource(ints = {1, 3})
    void testClosePersistenceContext(int menuCode) {
        //given
        EntityManager entityManager = EntityManagerGenerator.getInstance();
        Menu foundMenu = entityManager.find(Menu.class, menuCode);

        //when
        // close : 영속성 컨텍스트 종료된다 -> 영속성 컨텍스트 내의 모든 엔터티는 준영속화 된다
        entityManager.close();

        // 해당 영속성 컨텍스트는 종료되었으므로 find을 할 시 에러가 나므로 에러 캐치
        //then
        assertThrows(
                IllegalStateException.class,
                () -> entityManager.find(Menu.class, menuCode)
        );
    }
}

 

(4) 삭제 상태(Removed)

엔터티 객체가 영속성 컨텍스트에서 제거된 상태로, 이 상태에서는 엔터티를 수정해도 DB에는 반영되지 않는다.

* 테스트 코드

 

package org.example.section03.entity;

class EntityLifeCycleTest {

    private EntityLifeCycle entityLifeCycle;

    @BeforeEach
    void init() {
        this.entityLifeCycle = new EntityLifeCycle();
    }

    @DisplayName("영속성 엔터티 삭제 remove 테스트")
    @ParameterizedTest
    @ValueSource(ints = {2})
    void testRemoveEntity(int menuCode) {
        //given
        EntityManager entityManager = EntityManagerGenerator.getInstance();
        EntityTransaction entityTransaction = entityManager.getTransaction();
        Menu foundMenu = entityManager.find(Menu.class, menuCode);

        //when
        entityTransaction.begin();
        // remove : 엔터티를 영속성 컨텍스트 및 데이터 베이스에서 삭제한다
        // 단, 트랙잭션을 제어하지 않으면 데이터 베이스에 영구 반영 되지는 않는다.
        entityManager.remove(foundMenu);
        entityManager.flush();

        //then
        Menu refoundMenu = entityManager.find(Menu.class, menuCode);
        assertNull(refoundMenu);
        entityTransaction.rollback();
    }

}

 

'Spring Framework > JPA' 카테고리의 다른 글

[JPA] 6. Association Mapping  (6) 2024.09.17
[JPA] 5. Mapping  (0) 2024.09.16
[JPA] 3. CRUD 실습  (0) 2024.09.13
[JPA] 2. Persistence Context - EntityManager 생성하기  (1) 2024.09.13
[JPA] 1. 개요 및 개발환경 구축  (0) 2024.09.13