🖤 JPA 마스터가 되고싶어!
- 해당 시리즈는 인프런 김영한님의 JPA 커리큘럼
- 그리고 JPA 도서를 기반으로 공부한 내용을 바탕으로 작성되었습니다.
🩶 Intro
- 이번에 JPA 스터디를 진행하면서, JPA를 깊게 공부할 기회가 생겼다.
- JPA는 참…멋진 기술이다. 그래서 이 기회에 정리를 해보고자 한다!
🤍 Start!
JPA란?
- JPA는 자바 진영의 ORM(Object Relational Mapping) 기술 표준이다.
- ORM이라는 기술은 객체지향 언어 진영에서 여러가지가 존재하는데, 여기서 자바 진영의 ORM을 JPA라고 하는 것이다.
- JPA는 애플리케이션과 JDBC 사이에서 동작한다.
- 즉, 애플리케이션의 “객체 지향적인 방식”으로, JDBC & DB 단의 “데이터 방식”을 변경해주는 커넥터와 같다고 보면 된다.
기본 용어 정리
- 하이버네이트 (Hibernate)
- JPA를 사용하려면 JPA를 구현한 ORM 프레임워크를 선택해야하는데, 현재 JPA를 구현한 ORM 프레임워크 중 가장 많이 사용하는 것이 하이버네이트이다.
- 우리가 사용할 spring data jpa 역시 jpa를 사용하기 위해 hibernate 구현체를 활용한다. (기본값이 하이버네이트)
- 엔티티
- 비즈니스 요구사항을 모델링한 객체
- 즉, 엔티티(Entity)는 도메인 모델을 구성하는 요소 중 하나로, 식별자(ID)로 구분되고 생명주기와 상태가 변하는 도메인 객체를 말한다.
- 쉽게 말하면 그냥 table에 매핑되는 거..라고 보면 된다.
- 엔티티 매니저
- 한 트랜잭션(또는 작업 단위) 동안 영속성 컨텍스트(1차 캐시)를 관리하는 객체를 말한다.
- 엔티티 매니저 팩토리
- 엔티티 매니저 팩토리에서 엔티티 매니저를 꺼내는 방식이다.
- 이 엔티티 매니저 팩토리는 생성 비용이 크기 때문에 하나만 생성한다.
- 🔥 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드 간에 공유해도 되지만, 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유해서는 안된다.
- 플러시
- JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영한다. 이를 플러시 라고 한다.
- 즉, 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것이다.
- 또한 JPQL은 실행하기 전에 반드시 플러시가 되어야 조회가 가능하므로 (왜냐하면..조회를 하려고 하는데 플러시가 안 되어 있어서 영속성 컨텍스트의 내용이랑 db의 내용이랑 달라버리면 조회 결과가 달라지니까), JPA는 JPQL을 실행 할 때도 플러시를 자동으로 호출한다.
- 영속성 컨텍스트
- 메모리에 엔티티를 보관·관리하는 1차 캐시를 의미한다.
- 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
- 예를 들어, em.persist()라고 하면 엔티티를 영속성 컨텍스트에 저장할 수 있다.
- 영속성 컨텍스트가 엔티티를 관리하면 지연 로딩, 변경 감지 (Dirty Checking…) 등의 장점이 있다. 사실 이거 때문에 jpa 쓴다고 해도 무방함
- 객체 그래프 탐색
- 객체에서 회원이 소속된 팀을 조회할때는 참조를 사용해서 연관된 팀을 찾으면 되는데, 이를 객체 그래프 탐색이라고 한다.
- 1차 캐시
- 영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이를 1차캐시라고 하며, 영속 상태의 엔티티는 모두 이곳에 저장된다.
엔티티 생명주기
- 비영속 (new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
- 영속 (menaged) : 영속성 컨텍스트에 저장된 상태
- 준영속 (detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제 (removed) : 삭제된 상태
🔥 여기서 중요한 것은 “준영속 상태”이다.
- 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 된다.
- 준영속 상태의 엔티티는 merge() 또는, find()해야 영속성으로 되돌릴 수 있다.
- 준영속 상태는 영속성 컨텍스트에서 분리된 상태인 것이다. 따라서 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다. (1차 캐시, 지연 로딩, 쓰기 지연, 변경 감지 등….)
엔티티 매핑 어노테이션
예시 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| package hellojpa.dto;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "MEMBER")
public class Member {
@Id
@Column(name="ID")
private String id;
@Column(name="NAME")
private String username;
private Integer age;
}
|
1. @Entity
- @Entity가 붙은 클래스는 JPA가 관리하는 것으로, 엔티티 라고 부른다.
- @Entity 사용시 기본 생성자는 필수이다.
- JPA가 관리하는 객체로, 데이터베이스의 row 하나와 일대일로 매핑된다.
2. @Table
3. @Enumerated(EnumType.STRING)
- Enum type을 매핑할 수 있다.
- EnumType.STRING을 주면 db의 string type enum과 enum 타입을 매핑해준다.
4. @Temporal(TemporalType.TIMESTAMP)
- java의 날짜 타입은 이 애노테이션을 사용해서 매핑한다.
5. @Id
참고 ) JPA 동작 방식
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| import jakarta.persistence.*;
public class JpaMain {
public static void main(String[] args) {
//엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
//엔티티 매니저 생성
EntityManager em = emf.createEntityManager();
//트랜잭션 획득
EntityTransaction tx = em.getTransaction();
try{
tx.begin(); //트랜잭션 시작
logic(em); //비즈니스 로직 실행
tx.commit(); //트랜잭션 커밋
} catch (Exception e){
tx.rollback(); //실패시 롤백
} finally {
em.close(); //엔티티 매니저 - 종료 (매니저가 파생이니까 먼저 종료)
}
emf.close(); //엔티티 매니저 팩토리 - 종료
}
public static void logic(EntityManager em){}
}
|
- 엔티티 매니저 팩토리를 생성 > 팩토리에서 엔티티 매니저 생성 > 트랜잭션 얻고 > 커밋 혹은 롤백
엔티티 등록 순서
- 트랜잭션을 커밋하면 엔티티매니저는 우선 영속성 컨텍스트를 플러시한다.
- 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화 하는 작업인데 이때 등록, 수정, 삭제한 엔티티를 데이터베이스에 반영한다.
- 쓰기 지연 SQL 저장소에 모인 쿼리를 데이터베이스에 보낸다.
- 이렇게 변경 내용을 데이터베이스에 동기화 한 후에 실제 데이터베이스 트랜잭션을 커밋한다.
데이터베이스 스키마 자동 생성
- JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원한다.
1
2
3
4
5
6
7
8
9
10
11
| spring:
profiles:
active : test
datasource:
url: jdbc:mariadb://localhost:3307/queryDSL
username: ***
password: ****
driver-class-name: org.mariadb.jdbc.Driver
jpa:
hibernate:
ddl-auto: create-drop
|
- hibernate 옵션을 통해 ddl 생성 정책을 어떻게 할지를 정할 수 있다.
hibernate.hbm2ddl.auto 속성
- create : 기존 테이블을 삭제하고 다시 생성한다. DROP + Create
- create-drop : create 속성에 추가로 애플리케이션을 종료할 때, 생성한 table을 제거한다.
- update : 데이터베이스 테이블과 엔티티 매핑 정보를 비교해서 변경사항만 수정한다.
- validate : 데이터베이스 테이블과 엔티티 매핑정보를 비교해서 차이가 있으면 경고를 남기고 애플리케이션을 실행하지 않는다. 이 설정은 DDL을 수정하지 않는다.
- none : 자동 생성 기능을 사용하지 않고 싶으면 none으로 지정하면 된다.
개발 환경에 따른 추천 전략
- 개발 초기 단계는 : create, update
- 초기화 상태로 test를 진행하는 개발자 환경과 CI 서버 : create, create-drop
- 테스트 서버 : update, validate
- 스테이징, 운영 서버 : validate, none
매핑 전략
- 단어와 단어를 구분할때, 자바 언어는 관례상 CamelCase를 이용하고, DB는 관례상 스네이크 타입을 사용한다.
- 기본적으로 Spring Boot는 Hibernate에 기본 네이밍 전략을 잡아주기 때문에, CamelCase를 role_type과 연결해준다.
연관관계 매핑 기초
- 매핑 관계는 일대일, 일대다, 다대일, 다대다 네가지로 나뉜다.
- 연관관계는 단방향 연관관계, 양방향 연관관계 두 가지가 있는데, 양방향 연관관계의 경우 “연관관계의 주인”이 존재한다.
저장
1
2
| member1.setTeam(team1);
em.persist(member1);
|
- 회원 엔티티가 팀 엔티티를 참조하고, JPA는 참조한 팀의 식별자(Team.id)를 외래키로 사용해서 적절한 쿼리를 등록하는데 이때 persist()를 이용한다.
조회 (JPQL)
1
2
3
4
5
6
7
| private static void queryLogicJoin(EntityManager em){
String jpql = "select m from Member m join m.team t where " + "t:name=:teamName";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setParameter("teamName", "팀1")
.getResultList();
//.....
}
|
수정
1
2
3
| //회원1에 새로운 팀2 설정
Member member = em.find(Member.class, "member1");
member.setTeam(team2);
|
- 연관관계 수정은 .update 같은 메소드가 존재하지 않는다.
- 트랜잭션 커밋이나 flush 호출 시, 플러시가 일어나면서 더티체킹 (변경감지) 기능을 통해 자동으로 update 쿼리가 나간다.
- 참고로 이건 영속성 컨텍스트에서 제공하는 내용이므로…반드시 영속 상태여야만 변경 감지 기능 사용이 가능하다.
제거
1
2
3
4
| private static void deleteRelation(EntityManager em){
Member member1 = em.find(Member.class, "member1");
member1.setTeam(null); //setTeam null을 통해 연관관계를 제거
}
|
1
2
3
4
| member1.setTeam(null); //@ManyToOne의 소유자는 N쪽이기 때문에, 소유자에서 null로 바꿔줘야 한다!!
member2.setTeam(null); //참고로 nullable이어야 null로 끊을 수 있다.
em.remove(team); //위에서 연관된 엔티티를 삭제한 후에, team 엔티티를 삭제
|
- 연관된 엔티티를 삭제하려면, 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.
연관관계 매핑
양방향 연관관계
1
2
3
| @ManyToOne
@JoinColumn(neam="TEAM_ID")
private Team team;
|
1
2
| @OneToMany(mappedBy="team")
private List<Member> members = new ArrayList<Member>();
|
- 양방향 연관관계에는 “연관관계의 주인”이라는 것이 존재한다.
- 연관관계의 주인은 보통 “외래키”가 존재하는 “다”쪽으로 한다.
- 연관관계의 주인에 @JoinColumn(neam=”TEAM_ID”)으로 외래키 컬럼을 지정하고, 반대편에 mappedBy를 이용해서 연관관계 주인의 필드명을 넣어주면 된다.
📌 정리하자면……
- 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
- 주인은 mappedBy 속성을 사용하지 않는다.
양방향 연관관계 주의점
- 가장 흔히 하는 실수는, 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| Member member1 = new Member("member1", "회원1");
em.persist(member1);
Member member2 = new Member("member2", "회원2");
em.persist(member2);
Team team1 = new Team("team1", "팀1");
//주인이 아닌 곳만 연관관계 설정
team1.getMembers().add(member1);
team1.getMembers().add(member2);
//즉..다음과 같이 연관관계의 주인인 곳에 설정하는 것을 생략한 것이다.
//member1.setTeam(team1);
//member2.setTeam(team1);
|
- 이렇게 하면 SELECT로 회원을 조회했을때 외래키가 null로 나온다.
- 왜냐면 실질적으로 외래키를 관리하는 연관관계의 주인에 값을 설정하지 않았기 때문이다.
연관관계 편의 메소드
- JPA가 없는 순수한 객체 환경일때를 고려하면 객체 관점에서는, 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다. (DB에 반영 하는 것만 놓고 보면, 연관관계의 주인에만 세팅해도 괜찮다.)
- 예를 들어, 테스트 코드를 작성할 때는 최대한 순수한 환경으로 작성하는게 중요하므로, 양방향으로 관계를 정의해주는 것이 중요하다.
- 이때 사용하는 것이 바로 연관관계 편의 메서드이다.
1
2
3
4
5
6
7
8
| public void setTeam(Team team){
if(this.team != null){
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this); //team list 가져와서 거기에 team 추가
}
|
- 주의할 점은, 양방향 연관관계에서 변경 시 이전의 연관관계를 삭제하지 않은채로 남겨두면 남아있는 member가 그대로 나오는 문제가 생길 수 있다는 것이다. 그러므로 변경시에는 기존에 존재하는 연관관계를 반드시 없애야 한다.. (그래서 this.team != null으로 체크하는 것이다.)
⭐ 내용을 정리하자면, 다음과 같다.
- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.
양방향 매핑은 복잡하다. 정리하자면 우선 단방향 매핑을 사용하고, 반대 방향으로 객체 그래프 탐색 기능(JPQL)이 필요할 때 양방향을 사용하도록 코드를 추가하는 것이 좋다.
연관관계 편의 메서드를 적용한 Order Entity의 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
| /*
ch5 - 연관관계 매핑 시작 실습
Order Entity
외래키를 직접 넣는 것이 아닌, 연관관계의 객체를 참조하도록 구성한다.
*/
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Entity
@Table(name = "ORDERS") // 'Order'는 Java 예약어이므로 테이블 이름을 'ORDERS'로 지정
@Getter
@Setter
public class Order {
@Id
@GeneratedValue
@Column(name = "ORDER_ID") // 컬럼 이름을 ORDER_ID로 지정
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID") // 외래키 컬럼 이름 지정
private Member member;
//Order에서 OrderItem을 조회하는 일은 왕왕 있으므로, 양방향 매핑을 해준다.
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<OrderItem>();
@Temporal(TemporalType.TIMESTAMP)
private Date orderDate; //주문 시간을 저장한다.
//Enumerated 를 이용해서 STRING 타입으로 주문 상태를 저장한다.
@Enumerated(EnumType.STRING)
private OrderStatus status; //주문 상태를 저장
/*
==연관관계 편의 메서드==
- 양방향의 경우, 연관관계 편의 메서드가 필요하다.
- 연관관계 편의 메서드는, 조회를 많이 하는 곳에 작성한다.
- 여기서는 member, delivery가 양방향 이다.
*/
public void setMember(Member member) {
this.member = member;
member.getOrders().add(this); // 양방향 연관관계 설정
//member가 가진 orders에 현재 order를 추가한다.
}
public void setDelivery(Delivery delivery) {
this.delivery = delivery;
delivery.setOrder(this); // 양방향 연관관계 설정
//delivery가 가진 order에 현재 order를 추가한다.
}
public void addOrderItem(OrderItem orderItem) {
items.add(orderItem);
orderItem.setOrder(this); // 연관관계 편의 메서드 안에 양방향 연관관계 설정
//orderItem이 가진 order에 현재 order를 추가한다.
}
}
|
- 📌 Order라는 엔티티에 연관관계 편의 메서드를 넣어둔 이유는,
- Order(애그리거트 루트)가 양방향을 다 가지고 있는게 빼먹을 염려가 적음, 그러므로 실수를 방지하기 좋다.
- 도메인 관점에서 관계 설정을 전부 Order가 가지고 있는게 설계상 깔끔하다.
- 즉, 연관관계 메서드를 정의할때는, 도메인 관점에서 주가 되는 엔티티 안에 정의하는 것이 옳다. (도메인 주도 설계)
다양한 연관관계 매핑
연관관계 매핑의 과정
- 먼저, 연관관계가 있는 두 엔티티가 일대일 관계인지 일대다 관계인지 다중성을 고려해야 한다.
- 다음으로, 두 엔티티 중 한쪽만 참조하는 단방향 관계인지 서로 참조하는 양방향 관계인지 고려해야 한다.
- 마지막으로 양방향 관계면 연관관계의 주인을 정해야 한다.
다대일 관계
- @ManyToOne을 사용해서 매핑한다.
- 추가적으로 말하자면, 양방향 연관관계는 항상 서로를 참조해야 한다.
- 참고로, 편의 메소드는 한 곳에만 작성하거나 양쪽 다 작성할 수 있는데, 양쪽에 다 작성하면 무한루프에 빠지므로 주의해야 한다.
- 참고로 연관관계의 주인이 아닌 엔티티는 조회를 위한 JPQL이나 객체 그래프를 탐색할때 사용한다.
일대다 관계
📌 일대다 단방향 매핑보다는 다대일 양방향 매핑을 이용하는 것이 좋다.
- 일대다 단방향을 사용할 경우, 단점이 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점인데, 그러므로 일(One)쪽에서 다(Many) 테이블의 FK를 갱신하려고 추가 UPDATE를 해야 하는 문제가 있다. 그래서 일대다 단방향 보다는, 다대일 양방향으로 두고 일대다 쪽에서는 읽기 위주로 사용하는 것이 좋다.
일대일 관계
- @OneToOne을 사용해서 매핑한다.
- 일대일 관계는 그 반대도 일대일 관계이다.
- 테이블 관계에서 일대다, 다대일은 항상 다(N)쪽이 외래키를 가진다.
- 반면에 일대일 관계는 주 테이블이나 대상 테이블 둘 중 어느곳이나 외래키를 가질 수 있다.
- 즉, 양방향 매핑이 필요할 경우엔 주로 수정 작업이 필요한 곳을 연관관계 주인으로 설정하면 된다.
다대다 관계
- @ManyToMany를 이용하면 다대다를 구현하는 것은 가능하나.. 다대다 관계를 이용할 경우, 중간 테이블에 추가 컬럼을 넣을 수 없다는 단점이 있다.
- 그래서 다대다 관계의 경우는 중간 엔티티를 두고 다대일 관계로 풀어내는 것이 좋다.
프록시와 연관관계 관리
프록시란?
- 지연 로딩을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데, 이것을 프록시 객체라고 한다.
프록시의 특징
- 프록시 객체는 실제 객체에 대한 참조를 보관한다.
- 그리고 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
- JPA 구현체들은 객체 그래프를 마음껏 탐색할 수 있도록 지원하는데 이때 프록시 기술을 사용하는 것이다.
즉시 로딩, 지연 로딩
즉시 로딩 (EAGER)
- 연관된 엔티티를 즉시 조회한다. 하이버네이트는 가능하면 SQL 조인을 사용해서 한 번에 조회한다.
지연 로딩 (LAZY)
🌟 추천하는 방법은, 모든 연관관계에 “지연 로딩”을 사용하는 것이다.
- 연관된 엔티티를 프록시로 조회한다. 프록시를 실제 사용할 때 초기화하면서 데이터베이스를 조회한다.
- @XXToOne 애노테이션은 전부 EAGER 로딩이 기본값으로 되어 있기 때문에, 다음과 같이 설정하는 것이 좋다.
1
| @ManyToOne(fetch = FetchType.LAZY)
|
영속성 전이 (CASCADE)
CASCADE 옵션 종류
1
2
3
4
5
6
7
8
| public enum CascadeType{
ALL, //모두 적용
PERSIST, //영속
MERGE, //병합
REMOVE, //삭제
REFRESH, //REFRESH
DETACH //DETACH
}
|
고아 객체
- JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이것을 고아 객체 (ORPHAN) 제거라고 한다.
1
2
3
4
5
6
7
8
| @Entity
public class Parent{
@Id @GemeratedValue
private Long id;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<Child>();
}
|
- orphanRemoval = true를 통해 고아 객체로 설정할 수 있다.
- 이렇게 설정하면, parent라는 부모 객체와 연관관계가 삭제 되었을 때, flush/commit 시 DB에서 삭제된다.”
- ⭐ 결국, db에서 삭제하는 것이기 때문에 부모 없이는 의미가 없는 진짜 자식일 때만 설정하는 것이 좋다.