DB 접근/JPA ( Java Persistence API )

[JPA] 영속성 컨텍스트

JABHACK 2025. 1. 4. 09:29

영속성 컨텍스트(PersistenceContext)

📌 Entity 객체를 영속성 상태로 관리하는 일종의 캐시 역할을 하는 공간으로 여기에 저장된 Entity는 데이터베이스와 자동으로 동기화되며 같은 트랜잭션 내에서는 동일한 객체가 유지된다.

  • 영속성 상태 = JPA(Java Persistence API)에서 **엔티티(Entity)**가 EntityManager에 의해 관리되는 상태

  • 논리적인 개념
    • 눈에 보이지 않는 공간이 생긴다.
  • Entity Manager 를 통해서 영속성 컨텍스트에 접근한다.
    • EntityManager.persist(entity);
  • Entity(객체)를 영속성 컨텍스트에 영속(저장)한다.

 

 

Entity

📌 데이터베이스의 테이블과 매핑되는 Java 클래스를 의미합니다.

  • 데이터베이스에서 Entity 저장할 수 있는 데이터의 집합을 의미한다.
  • JPA에서 Entity란 데이터베이스의 테이블을 나타내는 클래스를 의미한다.

 


1. Entity의 특징

  1. 데이터베이스 테이블과 매핑:
    • Entity는 데이터베이스 테이블과 1:1로 매핑됩니다.
    • Java 객체의 필드는 데이터베이스 테이블의 컬럼에 매핑됩니다.
  2. 필수 어노테이션:
    • @Entity: 클래스를 Entity로 지정.
    • @Id: 기본 키(Primary Key)로 사용할 필드 지정.
  3. 영속성 관리:
    • Entity는 EntityManager에 의해 영속성 상태가 관리됩니다.
  4. POJO(Plain Old Java Object):
    • Entity는 일반 Java 클래스처럼 동작하며, 특별한 상속이나 인터페이스 구현 없이 단순히 JPA의 규약을 따릅니다.

2. Entity 클래스 생성 예제

1) 기본 구조

import jakarta.persistence.*;

@Entity // 이 클래스를 Entity로 지정
@Table(name = "users") // 데이터베이스 테이블 이름 지정
public class User {

    @Id // 기본 키 지정
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 증가 값
    private Long id;

    @Column(nullable = false) // 필수 컬럼 지정
    private String name;

    @Column(unique = true) // 유니크 제약 조건
    private String email;

    // 기본 생성자
    public User() {}

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

3. 주요 어노테이션

1) @Entity

  • 클래스를 JPA Entity로 선언.
  • 필수 어노테이션.
@Entity
public class Product {
    // 필드와 메서드 정의
}

2) @Table

  • 매핑할 데이터베이스 테이블 이름을 지정.
  • 생략 시 클래스 이름이 테이블 이름으로 사용.
@Table(name = "products")

3) @Id

  • 기본 키(Primary Key)로 사용할 필드를 지정.
@Id
private Long id;

4) @GeneratedValue

  • 기본 키의 값을 자동 생성.
  • strategy 속성으로 생성 전략을 지정:
    • IDENTITY: 데이터베이스가 자동 증가 값을 제공.
    • SEQUENCE: JPA가 시퀀스를 사용해 기본 키 값 생성.
    • TABLE: 키 생성 테이블 사용.
    • AUTO: 데이터베이스에 맞는 전략을 자동 선택.
@GeneratedValue(strategy = GenerationType.IDENTITY)

5) @Column

  • 테이블의 컬럼에 매핑.
  • 속성:
    • name: 컬럼 이름 지정.
    • nullable: NULL 허용 여부.
    • unique: 유니크 제약 조건.
    • length: 문자열 길이 제한.
@Column(name = "user_name", nullable = false, length = 50)

6) @Transient

  • 특정 필드를 데이터베이스에 매핑하지 않음.
@Transient
private String temporaryData;

4. Entity의 활용

1) 데이터 저장

persist() 메서드를 사용하여 데이터를 저장.

User user = new User();
user.setName("John");
user.setEmail("john@example.com");

EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
em.persist(user); // INSERT 쿼리 실행
em.getTransaction().commit();

2) 데이터 조회

find() 메서드를 사용하여 기본 키를 기준으로 데이터를 조회.

User user = em.find(User.class, 1L); // SELECT 쿼리 실행
System.out.println(user.getName());

3) 데이터 수정

영속 상태의 엔티티를 변경하면 변경 사항이 자동으로 데이터베이스에 반영(Dirty Checking).

em.getTransaction().begin();
user.setName("Updated Name"); // Dirty Checking으로 UPDATE 실행
em.getTransaction().commit();

4) 데이터 삭제

remove() 메서드를 사용하여 엔티티를 삭제.

em.getTransaction().begin();
em.remove(user); // DELETE 쿼리 실행
em.getTransaction().commit();

5. Entity와 데이터베이스 간 매핑

1) 필드와 컬럼 매핑

  • 엔티티 필드는 데이터베이스 컬럼에 매핑됩니다.
@Column(name = "user_email", nullable = false)
private String email;

2) 테이블 매핑

  • 엔티티 클래스는 테이블과 매핑됩니다.
@Table(name = "users")
@Entity
public class User {
    @Id
    private Long id;
    @Column(nullable = false)
    private String name;
}

3) 관계 매핑

  • JPA는 엔티티 간의 관계를 지원하며, 주요 관계 매핑 어노테이션:
    • @OneToOne
    • @OneToMany
    • @ManyToOne
    • @ManyToMany

예: @OneToMany 관계 매핑

@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "user")
    private List<Order> orders;
}

@Entity
public class Order {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

6. Entity의 장점

  1. 객체 지향 데이터 모델링:
    • 데이터베이스 테이블을 Java 객체로 매핑하여 객체 중심으로 설계 가능.
  2. SQL 자동 생성:
    • Hibernate나 JPA가 적절한 SQL을 자동으로 생성.
  3. 데이터베이스 독립성:
    • 엔티티를 작성할 때 데이터베이스에 의존하지 않음.
  4. 생산성 향상:
    • 데이터 접근 코드 작성이 단순화되고 유지보수가 쉬움.

7. 주의사항

  1. 기본 생성자 필수:
    • JPA는 리플렉션을 사용하므로 기본 생성자가 반드시 필요.
  2. Serializable 구현 권장:
    • 엔티티는 종종 네트워크나 파일로 전달되므로 Serializable을 구현하는 것이 좋음.
  3. Equals와 HashCode 재정의:
    • 엔티티를 컬렉션에서 사용할 경우, @Id 기반으로 equals()와 hashCode()를 재정의.

결론

Entity는 JPA의 핵심 요소로, 데이터베이스와의 상호작용을 객체 중심으로 단순화하고 효율적으로 설계할 수 있게 합니다. 올바른 매핑과 상태 관리를 통해 데이터베이스와 객체 간의 매끄러운 통신이 가능합니다. 추가적으로 관계 매핑이나 성능 최적화 방법에 대해 더 알고 싶다면 말씀해주세요!

 

 

Entity 생명주기

 

  1. 비영속(new/transient)
    • 영속성 컨텍스트가 모르는 새로운 상태
    • 데이터베이스와 전혀 연관이 없는 객체
  2. 영속(managed)
    • 영속성 컨텍스트에 저장되고 관리되고 있는 상태
    • 데이터베이스와 동기화되는 상태
  3. 준영속(detached)
    • 영속성 컨텍스트에 저장되었다가 분리되어 더 이상 기억하지 않는 상태
  4. 삭제(removed)
    • 영속성 컨텍스트에 의해 삭제로 표시된 상태
    • 트랜잭션이 끝나면 데이터베이스에서 제거

 

엔티티의 생명주기를 현실 세계의 사람과 행정 시스템에 비유해서 설명해 보겠습니다.


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

  • 비유:
    • 새로 태어난 아기 같은 상태입니다.
    • 아직 주민등록도 되어 있지 않고, 정부 시스템(행정 시스템)과 아무 연관이 없습니다.
    • 단순히 메모리에만 존재하는 객체이며, 데이터베이스에 저장되지 않은 상태입니다.
  • :
    User user = new User(); // 비영속 상태
    user.setName("John");
    user.setEmail("john@example.com");
    
    • User 객체는 만들어졌지만, 아직 데이터베이스와는 아무 관련이 없습니다.

2. 영속 상태 (Managed)

  • 비유:
    • 주민등록이 완료된 상태입니다.
    • 행정 시스템(영속성 컨텍스트)에 등록되어 정부가 그 사람을 기억하고 관리합니다.
    • 만약 사람이 이사(데이터 변경)를 하면, 시스템에도 자동으로 반영됩니다. (자동 동기화)
  • 특징:
    • 영속성 컨텍스트가 이 객체를 관리하며, 변경 사항이 자동으로 데이터베이스에 반영됩니다.
  • :
    • em.persist(user)를 호출하면 영속성 컨텍스트가 user를 관리하기 시작합니다.
    • 이후 user.setName("Updated John")처럼 객체가 변경되면, 데이터베이스에도 자동으로 반영됩니다.
public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {
        // 비영속
        Tutor tutor = new Tutor(1L, "wonuk", 100);

        System.out.println("persist 전");
        // 영속
        em.persist(tutor);
        System.out.println("persist 후");

        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
    
    emf.close();

}

em.persist() 가 호출되며 영속 상태가 된다.

 

  • persist 후 출력 이후에 SQL이 실행된다.
  • 트랜잭션 Commit 시점에 SQL이 실행된다.

 


3. 준영속 상태 (Detached)

  • 비유:
    • 주민등록이 말소된 상태입니다.
    • 한때 행정 시스템(영속성 컨텍스트)에 등록되었지만, 더 이상 관리되지 않습니다.
    • 이 사람의 정보가 행정 시스템과는 연관이 없으므로, 정보를 업데이트해도 정부 시스템에는 반영되지 않습니다.
  • 특징:
    • 객체는 메모리에 있지만, 영속성 컨텍스트와의 연결이 끊어져 데이터베이스와 동기화되지 않습니다.
  • :
    • detach() 메서드는 객체를 영속성 컨텍스트에서 분리하여 준영속 상태로 만듭니다.
    • 객체를 변경해도 데이터베이스에 영향을 주지 않습니다.
    • 영속성 컨텍스트가 제공하는 기능을 사용하지 못한다.
    1. em.detach()
      • 특정 Entity만 준영속 상태로 변경한다.
    2. em.clear()
      • 영속성 컨텍스트를 초기화 한다.
    3. em.close()
      • 영속성 컨텍스트를 종료한다.

4. 삭제 상태 (Removed)

  • 비유:
    • 사망신고를 한 상태입니다.
    • 행정 시스템(영속성 컨텍스트)에 의해 삭제로 표시되었으며, 트랜잭션이 완료되면 데이터베이스(정부 기록)에서도 완전히 삭제됩니다.
  • 특징:
    • 삭제 상태로 표시되면, 트랜잭션이 완료되는 시점에 데이터베이스에서 삭제됩니다.
  • :
    • remove() 메서드를 호출하면, 해당 객체는 삭제 상태로 표시되고, 트랜잭션이 끝나면 데이터베이스에서 제거됩니다.
  • em.remove(user); // 삭제 상태로 전환

5. 전체 흐름을 비유로 설명

  • 비영속 상태: "아직 주민등록이 안 된 상태의 아기"
    • 시스템(영속성 컨텍스트)과 전혀 연관이 없음.
  • 영속 상태: "주민등록이 완료된 상태"
    • 정부가 해당 사람을 등록하고 관리.
    • 변경 사항(주소, 이름 등)이 자동으로 기록됨.
  • 준영속 상태: "주민등록 말소 상태"
    • 과거에 등록되었지만 더 이상 정부 시스템에서 관리하지 않음.
    • 개인이 어디에 이사 가든 정부는 알 수 없음.
  • 삭제 상태: "사망신고 상태"
    • 정부 시스템에 더 이상 존재하지 않으며, 기록에서 삭제될 예정.

6. 실제 코드 흐름 예제

// 비영속 상태
User user = new User();
user.setName("John");

// 영속 상태로 전환
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
em.persist(user); // 영속 상태
user.setName("Updated John"); // 변경 감지(Dirty Checking)

// 준영속 상태로 전환
em.detach(user);
user.setName("Detached John"); // 데이터베이스에 반영되지 않음

// 삭제 상태로 전환
em.getTransaction().begin();
em.remove(user); // 삭제 상태
em.getTransaction().commit(); // 데이터베이스에서 제거

7. 요약

상태  비유  특징 
비영속 주민등록이 안 된 상태 단순 객체 생성, 데이터베이스와 연관 없음.
영속 주민등록 완료된 상태 영속성 컨텍스트가 관리하며 데이터베이스와 동기화됨.
준영속 주민등록 말소된 상태 더 이상 영속성 컨텍스트가 관리하지 않으며 데이터베이스와 동기화되지 않음.
삭제 사망신고된 상태 데이터베이스에서 삭제로 표시되며, 트랜잭션 완료 시 실제로 제거됨.

 

 

1차 캐시

📌 엔티티를 영속성 컨텍스트에 저장할 때 생성되는 메모리 내 캐시이다. 엔티티는 먼저 1차 캐시에 저장되고 이후 같은 엔티티를 요청하면 DB를 조회하지 않고 1차 캐시에서 데이터를 반환하여 성능을 높일 수 있다.

 

  • 영속성 컨텍스트의 1차 캐시에 저장된다.
// 비영속
Tutor tutor = new Tutor(1L, "wonuk", 100);

// 영속, 1차 캐시에 저장
em.persist(tutor);
  • 영속된 Entity 조회

  • Database가 아닌 1차 캐시에 저장된 Entity를 먼저 조회한다.
// 1차 캐시에서 조회
Tutor findTutor = em.find(Tutor.class, 1L);

 

public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {
        // 비영속
        Tutor tutor = new Tutor(1L, "wonuk", 100);

        // 영속
        System.out.println("persist 전");
        em.persist(tutor);
        System.out.println("persist 후");

        Tutor findTutor = em.find(Tutor.class, 1L);

        System.out.println("findTutor.getId() = " + findTutor.getId());
        System.out.println("findTutor.getName() = " + findTutor.getName());
        System.out.println("findTutor.getAge() = " + findTutor.getAge());

        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
    emf.close();
}

실행결과

  • 1차 캐시의 Entity를 조회한다.
    • 조회 SQL이 실행되지 않는다.
  • 트랜잭션 Commit 시점에 INSERT SQL이 실행된다.
**트랜잭션(Transaction)**은 데이터베이스 작업에서 데이터의 일관성, 무결성을 보장하기 위해 논리적으로 묶여있는 작업 단위를 의미합니다.

 

 

 

데이터베이스에 저장된 데이터 조회

  • 1차 캐시는 동일한 트랜잭션 안에서만 사용이 가능하다.
  • 요청이 들어오고 트랜잭션이 종료되면 영속성 컨텍스트는 삭제된다.
  • 코드예시
    • xml <property name="hibernate.hbm2ddl.auto" value="none" /> 설정
      • DDL을 자동으로 생성하지 않는다.
public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {

        // 데이터베이스에서 조회 후 1차 캐시에 저장
        Tutor findTutor = em.find(Tutor.class, 1L);
        // 1차 캐시에서 조회
        Tutor findCacheTutor = em.find(Tutor.class, 1L);

        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
}

실행결과 : 조회 SQL이 한번만 실행된다.

 

동일성 보장

📌 동일한 트랜잭션 안에서 특정 엔티티를 여러 번 조회해도 항상 같은 객체 인스턴스를 반환한다. 영속성 컨텍스트는 1차 캐시를 사용하여 같은 엔티티를 중복 조회해도 동일한 객체를 참조하게 하여 일관성을 유지한다.

 

  • 동일한 트랜잭션 내에서 조회된 Entity는 같은 인스턴스를 반환한다.
  • DB에 저장된 데이터를 조회하여 1차 캐시에 저장한다.
  • 1차 캐시에 저장된 데이터를 조회한다.
  • 코드예시

  • Database에 저장된 데이터가 있는 상태
public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {

        // DB 조회, 1차 캐시에 저장
        Tutor findTutor1 = em.find(Tutor.class, 1L);
        // 1차 캐시 조회
        Tutor findTutor2 = em.find(Tutor.class, 1L);

        System.out.println("findTutor1 == findTutor2 : " + findTutor1.equals(findTutor2));

        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
    emf.close();
}

 

  • 조회 SQL이 한번만 실행된다.
  • 마치 Java Collection에서 객체를 조회하듯이 사용할 수 있다.

 

쓰기 지연

📌 엔티티 객체의 변경 사항을 DB에 바로 반영하지 않고 트랜잭션이 커밋될 때 한 번에 반영하는 방식으로 이를 통해 성능을 최적화하고 트랜잭션 내에서의 불필요한 DB 쓰기 작업을 최소화한다.

 

  • 예시 코드
    • xml <property name="hibernate.hbm2ddl.auto" value="create" /> 설정
      • Entity와 매핑된 테이블을 삭제 후 새로 생성한다.
    • 코드예시
public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {
        System.out.println("트랜잭션 시작");
        Tutor tutor1 = new Tutor(1L, "wonuk1", 100);
        Tutor tutor2 = new Tutor(2L, "wonuk2", 200);

        em.persist(tutor1);
        em.persist(tutor2);

        System.out.println("트랜잭션 Commit 전");
        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
        System.out.println("트랜잭션 Commit 후");
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
    emf.close();
}
  • 트랜잭션이 Commit된 이후에 SQL이 실행된다.
  • 실행결과

  • 여러개의 SQL을 하나씩 나누어 보낸다.
  • hibernate.jdbc.batch_size
    • 여러개의 SQL을 여러번 보내는 것이 아니라 합쳐서 하나로 보낸다.
    • 통신 비용을 줄여서 성능을 개선할 수 있다.

 

변경 감지(Dirty Checking)

📌 영속성 컨텍스트가 엔티티의 초기 상태를 저장하고 트랜잭션 커밋 시점에 현재 상태와 비교해 변경 사항이 있는지 확인하는 기능이다.

 

  • Database에 저장된 데이터가 있는 상태
  • xml <property name="hibernate.hbm2ddl.auto" value="none" /> 설정
public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {
            
        Tutor tutor = em.find(Tutor.class, 1L);
        tutor.setName("수정된 이름");

        // Java Collection을 사용하면 값을 수정하고 다시 저장하지 않는다.
        // em.persist(tutor);

        System.out.println("트랜잭션 Commit 전");
        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
        System.out.println("트랜잭션 Commit 후");
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
    emf.close();
}

  • em.persist(tutor); 로 저장하지 않아도 update SQL이 실행된다.
  • Entity를 변경하고자 할 때 em.persist() 를 사용하지 않아야 실수를 방지한다.

em.remove() 를 통해 Entity를 삭제할 때도 위와 같은 방식으로 동작한다. DELETE SQL이 트랜잭션 Commit 시점에 실행된다.

 

 

flush

📌 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 기능으로, 변경된 엔티티 정보를 SQL로 변환해 데이터베이스에 동기화한다. 트랜잭션 커밋 시 자동으로 실행되지만 특정 시점에 데이터베이스 반영이 필요할 때 수동으로 호출할 수도 있다.

 

flush 사용 방법

  1. 자동 호출
    • 트랜잭션이 Commit 되는 시점에 자동으로 호출된다.
  2. 수동 호출
    • em.flush() 를 통해 수동으로 호출할 수 있다.
  • 코드예시
    • xml <property name="hibernate.hbm2ddl.auto" value="create" /> 설정
      • 기존 테이블을 삭제(DROP) 후 다시 생성(CREATE)한다.
public static void main(String[] args) {
    // EntityManagerFactory 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
    // EntityManager 생성
    EntityManager em = emf.createEntityManager();
    // Transaction 생성
    EntityTransaction transaction = em.getTransaction();

    // 트랜잭션 시작
    transaction.begin();

    try {

        Tutor tutor = new Tutor(1L, "wonuk", 100);
        em.persist(tutor);

        // flush 수동 호출
        em.flush();

        System.out.println("트랜잭션 Commit 전");
        // transaction이 commit되며 실제 SQL이 실행된다.
        transaction.commit();
    } catch (Exception e) {
        // 실패 -> 롤백
        e.printStackTrace();
        transaction.rollback();
    } finally {
        // 엔티티 매니저 연결 종료
        em.close();
    }
    emf.close();
}

 

트랜잭션이 Commit되기 전에 SQL이 실행된다.