JPA

박채은·2022년 12월 28일
0

Spring

목록 보기
17/35

JPA란?

  • Java Persistent API
    • Jakarta Persistence라고도 불림
    • Persistence: 지속성, 무엇가를 오래 지속되게 하다.
  • Java에서 사용하는 ORM 기술의 표준 사양(명세)
    • ORM: 엔티티 클래스 객체와 데이터베이스 테이블을 맵핑시켜, 객체의 정보를 테이블에 저장하는 기술

Hibernate ORM

  • JPA 표준 사양(인터페이스)을 구현한 구현체
  • JPA에서 지원하는 기능 외에, Hibernate 자체적으로 지원하는 API가 있다.

  • JPA는 데이터 액세스 계층의 상단에 위치한다.

  • 데이터 저장/조회 등은 JPA를 거쳐 JPA의 구현체인 Hibernate ORM를 통해서 일어난다. Hibernate ORM은 JDBC API를 이용해서 데이터베이스에 접근한다.


영속성 컨텍스트

  • JPA는 테이블과 맵핑되는 엔티티 객체를 영속성 컨텍스트에 보관하여, 애플리케이션 내에 오래 지속되도록 한다.
    (캐시와 비슷한 개념)

  • 영속성 컨텍스트에는 1차 캐시쓰기 지연 SQL 저장소가 있다.

JPA API

  • JPA의 영속성 컨텍스트는 EntityManager 클래스에 의해 관리된다.
  • EntityManager 객체를 통해서 JPA의 API를 사용할 수 있다.
  • Transaction 객체를 통해서 데이터베이스의 테이블에 데이터를 저장한다.

EntityManager 객체 -> em
Transaction 객체 -> tx


  • em.persist(객체)
    • 영속성 컨텍스트의 1차 캐시에 객체를 저장한다.
    • 쓰기 지연 SQL 저장소에 INSERT 쿼리가 저장된다.
  • tx.commit() : 쓰기 지연 SQL 저장소에 있던 쿼리를 실행한다.
    • 실행된 쿼리문은 쓰기 지연 SQL 저장소에서 제거된다.
    • 내부에 존재하는 em.flush()가 쿼리를 실행해주는 것!
  • em.flush() : 영속성 컨텍스트의 변경 사항을 테이블에 반영해주는 API
    • 쓰기 지연 SQL 저장소에 있던 쿼리들을 데이터베이스에 전달해준다.
    • 변경 감지: 영속성 컨텍스트의 엔티티와 스냅샷을 비교해서 UPDATE 쿼리 생성해준다.
  • em.find(클래스 타입, 식별자)
    • 먼저 1차 캐시에 해당 객체가 있는지 조회한다.
    • 1차 캐시에서 찾고자 하는 객체가 존재하지 않으면, 테이블에 직접 SELECT 쿼리를 전송해서 조회한다.
  • em.remove(객체)
    • 1차 캐시에서 객체를 제거한다.
    • 쓰기 지연 SQL 저장소에 DELETE 쿼리를 저장한다.

수정(Update)

JPA에는 따로 UPDATE API는 없고 tx.commit()를 사용한다.

setter 메서드로 값을 변경하고 tx.commit()를 실행하면 UPDATE 쿼리가 실행된다.

💡 tx.commit() 를 사용하는데 어떻게 INSERT 쿼리가 아닌 UPDATE 쿼리가 실행되는가?

영속성 컨텍스트에 엔티티 객체가 저장되는 경우에, 저장되는 시점의 상태를 그대로 가지고 있는 스냅샷을 생성한다.
해당 엔티티의 값을 setter 메서드로 변경한 후, tx.commit()을 하면 변경된 엔티티와 이 전의 스냅샷을 비교한 후, 변경된 값이 있으면 쓰기 지연 SQL 저장소에 UPDATE 쿼리를 등록하고 UPDATE 쿼리를 실행한다.


엔티티 매핑

데이터베이스 테이블과 엔티티 클래스 간에 매핑 과정은 크게 다음과 같이 나눌 수 있다.

  1. 엔티티 - 테이블 매핑
  2. 필드 - 컬럼 간의 매핑
  3. 엔티티 간의 연관 관계 매핑

1. 엔티티 - 테이블 매핑

  • 클래스 레벨에 @Entity 애너테이션을 붙여 Entity 클래스로 인식하게 만들어준다.
  • 기본키로 설정할 필드에 @Id 애너테이션을 붙여, 식별자로 만들어준다.
  • @Table 은 필수가 아니다.
  • 파라미터가 없는 기본 생성자를 필수로 추가해준다.
    • Spring Data JPA의 기술을 적용할 때, 기본 생성자가 없는 경우 에러가 발생할 수 있기 때문에

@Entity

  • 클래스 레벨에 @Entity 애너테이션을 붙이면 해당 클래스는 JPA 관리 대상 엔티티가 된다.
    ( = 테이블과 매핑된다.)
  • 속성
    • name : 엔티티 이름을 설정
    • name 속성을 작성하지 않으면, 엔티티 이름 = 클래스 이름

@Table

  • 테이블의 이름을 설정해주는 애너테이션
  • 속성
    • name : 테이블 이름을 설정
    • name 속성을 작성하지 않으면, 테이블 이름 = 클래스 이름

@GeneratedValue

  • 자동으로 식별자를 생성해주는 애너테이션
  • strategy 속성
    • AUTO
      • strategy 속성에 값을 주지 않으면 AUTO로 설정된다.
      • 데이터베이스의 Dialect에 따라서 적절한 전략을 자동으로 선택한다.
    • IDENTITY
      • MySQL의 AUTO_INCREMENT을 통해 기본키를 생성
    • SEQUENCE
      • 데이터베이스에서 제공하는 시퀀스를 사용해서 기본키를 생성

IDENTITY 전략

✔️ 과정

1) em.persist() 호출

  • 객체가 1차 캐시에 저장된다.(이때는 식별자는 채워지지 않고 null로 존재)
  • tx.commit()를 하지 않아도 INSERT 문이 실행된다.

2) 테이블에 INSERT 쿼리가 실행될 때, DB에서 식별자가 AUTO_INCREMENT로 생성된다.

3) DB에서 생성된 식별자를 1차 캐시로 가져와서 채워준다.

IDENTITY 전략은 commit 호출 없이 데이터를 저장해주고, DB에 저장된 식별자를 가져와서 1차 캐시에 넣어준다.


SEQUENCE 전략

1) sequence 생성
2) sequence가 식별자를 생성
3) em.persist() 호출

  • 1차 캐시에 저장할 때, 식별자가 채워진 상태로 객체가 저장된다.

4) IDENTITY 전략과 달리, tx.commit()를 해야 INSERT 문이 실행된다.


2. 필드 - 컬럼 간의 매핑

  • @Column 애너테이션을 통해 필드와 컬럼을 매핑한다.
    • 속성
      • nullalbe - 디폴트 값이 true
      • updateable - 디폴트 값이 true
      • unique - 디폴트 값이 false
  • @Column 애너테이션이 없어도, JPA는 기본적으로 모든 필드를 컬럼으로 매핑한다.
    • 필드명이 messageId(카멜 케이스)일 때, MESSAGE_ID(snake 케이스)로 매핑
  • @Transient 애너테이션 : 테이블 컬럼과 매핑하지 않겠다는 의미
    • 임시 데이터를 메모리에서 사용하기 위한 용도
  • @Temporal 애너테이션 : java.util.Date, java.util.Calendar 타입으로 매핑할 때 사용
  • @Enumerated 애너테이션 : enum 타입과 매핑할 때 사용
    • EnumType.ORDINALEnumType.STRING 두 가지 타입이 있다.
    • EnumType.STRING을 사용하는 것을 권장함

❗️ 주의
필드가 원시 타입이라면, 기본적으로 nullable = false이다.
오히려 @Column 애너테이션만 추가한다면 nullable = true로 변경되기 때문에 (1) 애너테이션을 사용하지 않거나, (2) @Column(nullable=false)을 작성해주자.


3. 연관 관계 매핑

  • 엔티티 클래스 간의 관계를 만들어주는 것
  • DB에서는 외래키를 통해서 연관 관계가 생성되는데, JPA에서는 객체 참조를 통해 관계를 맺는다.
  • 연관 관계 매핑을 방향성 기준으로 생각했을 때,
    • 단방향 연관 관계
    • 양방향 연관 관계
  • 연관 관계 매핑을 참조할 수 있는 객체의 수를 기준으로 생각했을 때,
    • 일대다(1:N)
    • 다대일(N:1)
    • 다대다(N:N)
    • 일대일(1:1)

단방향 연관 관계

  • 한 쪽 클래스만 다른 쪽 클래스의 참조 정보를 가지고 있는 관계

양방향 연관 관계

  • 두 클래스가 서로의 객체를 참조할 수 있는 관계
  • JPA는 단방향, 양방향을 모두 지원하지만, JDBC는 단방향 연관 관계만 지원한다.

일대다 단방향

  • 일(1)에 해당하는 클래스가 다(N)에 해당하는 객체를 참조한다.
    • 일에 해당하는 클래스에 List<객체>가 포함된다.

  • 보통 테이블 간의 관계에서는 일대다 중에서 에 해당하는 테이블이 의 기본키를 외래키로 가진다.

일대다 단방향으로 객체를 참조하는 경우에, 일반적인 테이블 간의 관계를 표현하지 못한다.
=> 따라서 일다대 단방향은 잘 사용하지 않음!

다대일 단방향

  • 다(N)에 해당하는 클래스가 일(1)에 해당하는 객체를 참조한다.
  • 테이블 간의 관계와 동일하기 때문에, 보통 다대일 매핑 방식을 사용한다.
  • 에 해당하는 클래스의 외래키 필드@ManyToOne, @JoinColumn 애너테이션을 추가한다.

  • @JoinColumn(name = 외래키 칼럼명)
    • 외래키 칼럼명은 부모 테이블(에 해당하는 테이블)의 기본키의 컬럼명과 동일하다.
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member; // 외래키 필드

양방향

  • 다대일 매핑에 일대다 매핑을 추가해서 양방향 관계를 만들어준다.
  • @ManyToOne + @JoinColumn(name = 외래키 칼럼명) : 다(N)쪽에 작성
    • 외래키 칼럼명 : N 쪽 테이블에 들어갈 외래키의 칼럼명
  • @OneToMany(mappedBy = 외래키 필드명) : 일(1) 쪽에 작성
    • 외래키 필드명 : N 쪽 엔티티 클래스에 작성된 외래키의 필드명
// 다(N)에 해당하는 클래스 - Order
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member; // 외래키 필드

// 일(1)에 해당하는 클래스 - Member
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
private void mappingManyToOneBiDirection() {
        tx.begin();
        Member member = new Member("hgd@gmail.com", "Hong Gil Dong", "010-1111-1111");
        Order order = new Order();

        member.addOrder(order); // (1)
        order.addMember(member); // (2)

        em.persist(member);
        em.persist(order);
        tx.commit();
	
        Member findMember = em.find(Member.class, 1L); // (5)

        findMember
                .getOrders()
                .stream()
                .forEach(findOrder -> {
                   System.out.println("findOrder: " +
                   findOrder.getOrderId() + ", "+ findOrder.getOrderStatus());
                });
}

(1) member 객체에 order를 추가해주지 않아도, 연관 관계가 맺어져 있기 때문에 테이블에는 정상적으로 저장된다.
-> 하지만 order를 추가해주지 않으면, 1차 캐시에는 저장되지 않기 때문에 조회를 위해서는 추가해야 한다.

(2) order에 member 객체를 필수로 추가해야 한다.
-> member는 외래키 역할을 하기 때문이다.


다대다

  • @ManyToMany 애너테이션을 사용할 수 있다.
    • 하지만 @ManyToMany는 외래키들로만 테이블을 구성하므로 추가적인 속성을 저장할 수 없다는 단점이 있다.
  • 대신에 중간에 테이블을 하나 추가해서, 두 개의 다대일 매핑을 사용한다.

일대일

  • @OneToOne 애너테이션 사용

  • 일대일 단방향의 경우, @OneToOne + @JoinColumn

  • 일대일 양방향인 경우, @OneToOne + @JoinColumn@OneToMany(mappedBy)

  • 외래키를 어느 엔티티에 두든 상관없다.
    -> 아래 예시에서 설명!

다음 그림과 같이, 회원 애그리거트에 회원 정보(Member)와 Stamp가 있다.
회원 정보가 회원 애그리거트의 애그리거트 루트가 된다.

이때 어떤 관점으로 해당 애그리거트를 바라보느냐에 따라 연관 관계 매핑이 달라진다.

  1. DDD 관점

    • Stamp는 애그리거트 루트인 회원 정보가 없다면 존재할 수 없다.
      -> 외래키는 MEMBER_ID이고 Stamp에서 Member 객체를 참조한다.
    // Stamp 클래스
    @OneToOne
     @JoinColumn(name = "MEMBER_ID")
     private Member member;
    • 이때, 회원 정보가 애그리거트 루트이기 때문에 회원 정보를 통해서 Stamp에 접근해야 한다.

    • 회원 정보가 Stamp에 접근할 수 있도록 객체 참조가 있어야 한다.
      => 따라서 회원 정보와 Stamp는 서로를 참조하는 양방향 연관 관계를 맺어야 한다.


  1. DDD를 고려하지 않은 관점
    • 회원 정보가 애그리거트 루트이기 때문에, 해당 애그리거트의 모든 엔티티들은 회원 정보를 통해 접근해야 한다.
    • 회원 정보에서 Stamp 객체를 참조한다.
      => 단방향 연관 관계

정리

외래키를 중심으로 생각하자!

1:N 관계에서는 N이 외래키를 가지고 있다.
그러므로 N이 1에 접근할 수 있지만, 1은 N에 접근할 수 없다.
하지만 1으로 N을 접근해야 한다면, 1에서 접근할 수 있도록 양방향으로 만들어줘야 한다.

  • 일대다 단방향 매핑을 사용하지 않는다.
  • 먼저 다대일 단방향 매핑을 적용한다.
  • 필요에 따라 다대일 단방향 매핑에 일대다 매핑을 추가하여 양방향으로 만든다.

💡 JPA랑 Spring Data JPA는 다른 개념 인가요?

다른 개념이다!
Spring Data JPA는 순수 JPA를 더 편리하게 사용할 수 있도록 Spring이 제공하는 기능이다.

오늘 배운 것들은 Spring Data JPA에 대한 것이 아니라 순수 JPA에 대한 것이다!

위에서 em.persist()tx.commit 과 같은 기능을 매번 직접 호출해서 사용했는데, Spring이 이를 쉽게 사용하도록 도와주는 것이 Spring data JPA이다.

0개의 댓글