Spring Boot/JPA

JPA & Hibernate 설명

최-코드 2024. 7. 5. 11:17

JPA는 ORM(Object Relation Mapper)로 객체와 테이블을 매핑해주는 프레임 워크이다.

Hibernate는 이러한 JPA를 구현한 구현체이다.

Hibernate/JPA는 데이터베이스와 통신을 할 때 내부적으로 JDBC를 사용한다. 즉, JDBC 위의 추상화 계층이다.

 

ORM은 EntityManager가 하는 일이다.

EntifyManager를 생성하려면 EntitiyManagerFactory를 거쳐야 하는데, EntitiyManagerFactory는 Thread Safe하므로 서버에서 단 하나만 존재한다. 이와 반대로 EntityManager는 Thread Safe하지 않으므로, Thread마다 하나의 EntityManager를 가진다. 하지만 이 때 의존성 주입을 할 때는 싱글톤이다. 이는 실제 EntityManager에 대한 프록시로, 가짜 EntityManager이다. 프록시 EntityManager를 사용할 때 실제 EntityManager가 사용된다. 결국, 요청(thread)마다 새로운 EntityManager가 생성된다.

 

트랜잭션이 같으면 영속성 컨텍스트도 같다. 반대로 트랜잭션이 다르면 영속성 컨텍스트도 다르다.

 

JPA를 사용해야 하는 이유

  • 생산성 증진
    • SQL(쿼리)에 의존적인 개발에서 탈피하여, 객체 중심으로 생산적인 개발이 가능하다.
  • 객체와 관계형 테이블의 패러다임 불일치 해결
    • 객체지향 프로그래밍은 추상화, 캡슐화, 상속, 다형성 등을 제공한다.
    • JPA외의 것은 관계형 데이터베이스 데이터 중심으로 구조화 되어있으며, OOP의 특징을 지원하지 않는다.

영속성 컨텍스트는 엔티티를 영구 저장하는 환경이라는 뜻이다. 보통 영속성 컨텍스트는 트랜잭션이 시작할 때 열리고 트랜잭션이 끝날 때 종료된다. 즉, 영속성 컨텍스트가 열렸다가 닫히면 엔티티가 데이터베이스에 영구적으로 저장된다.

엔티티매니저는 엔티티를 영속성 컨텍스트에 보관하고 관리한다. 영속성 컨텍스트에 보관하는 시점은 엔티티를 조작(CRUD)할 때이다.

jpa의 save(), delete(), find() 등의 함수는 내부적으로 엔티티매니저의 함수를 이용한다.

 

또한 엔티티 클래스엔 @Table(name="테이블명")을, 필드에 대해선 @Column(name="컬럼명")을 명시적으로 하도록 하자. 자바 코드를 리팩토링할 때 클래스명이나 필드 이름이 변경되면 기존 데이터베이스와 일치하지 않는 문제가 발생할 수 있다.

 

상속관계매핑 

  • 조인테이블 전략
    • 부모가 될 엔티티에 @Inheritance(strategy = InheritanceType.JOINED)와 abstract를 붙인다.
      • 부모 엔티티와 자식 엔티티 각각 생성된다. 부모 엔티티에는 부모가 가진 컬럼에 대해서만 가지고 있고 자식 엔티티에는 자식이 가진 컬럼에 대해서만 값을 가진다. 이 때 자식 엔티티에는 부모 엔티티의 기본키를 기본키겸 외래키로 가지고 있어 join 연산을 할 수 있다.
@Entity
@Builder
@AllArgsConstructor
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Item {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private int price;
    private int stockQuantity;
}

@Entity
@Builder
@AllArgsConstructor
public class Food extends Item{
    String chef;
}

void test(){
	Food food = Food.builder().price(1).stockQuantity(1).chef("hi").build();
        save(food); //repository 생략
}

 

  • 싱글테이블 전략
    • 부모가 될 엔티티에 @Inheritance(strategy = InheritanceType.SINGLE_TABLE)와 @DiscriminatorColumn(name="DTYPE")를 붙인다. 자식 엔티티에는 @DiscriminatorValue("VALUE")를 붙인다.
    • 자식 엔티티와 부모엔티티가 함께 있는 엔티티가 생성되고 부모 엔티티의 테이블 명을 따른다. 이 때 DTYPE이라는 컬럼이 생기는데 아래의 예제의 경우 FOOD라는 값이 들어간다. 즉 @ DiscriminatorColumn은 어떤 자식 엔티티인지 구분해주는 역할을 해준다.
    • 조인테이블 전략보다 실무에서 많이 쓴다. 조인테이블 전략은 자식 엔티티 생성 시 부모와 자식 엔티티를 따로 생성해야하고, join연산도 이뤄져야 하기 때문이다.
@Entity
@Builder
@AllArgsConstructor
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="DTYPE")
public class Item {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private int price;
    private int stockQuantity;
}

@Entity
@Builder
@AllArgsConstructor
@DiscriminatorValue("FOOD")
public class Food extends Item{
    String chef;
}

void test(){
	Food food = Food.builder().price(1).stockQuantity(1).chef("hi").build();
        save(food); //repository 생략
}

 

@MappedSuperclass

@MappedSuperclass
public class BaseEntity {
    private String createdBy;
    private LocalDateTime createdAt;
}

@Entity
public class Item extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String description;
}

BaseEntity는 엔티티는 아니고, 그저 중복 방지를 위한 컬럼을 모아놓는 클래스라고 보면 된다. 꼭 @MappedSuperclass 어노테이션을 붙여줘야 한다.