JPA를 공부하자 01 - 동작과정, 연관관계편
JPA
김영한님의 JPA 내용중 중요한 부분을 정리한 내용입니다.
<br>
<br>
JPA를 왜 써야하는가?
관계형데이터베이스의 데이터를 Java의 컬렉션 처럼 사용 한다면 얼마나 편리할까?
Java는 객체지향관점의 언어이고,
데이터베이스는 보통 관계형데이베이스를 사용한다.
객체지향관점을, 관계형데이터베이스의 적용함에있어 많은 다름과 이질감이 발생한다.
예를 들어, 상속이라는 개념이 관계형데이터베이스에 있을까? 그렇지 않다.
하지만 많은 개발자들이,
이런 이질감을 극복하기 위해 노력해왔고, 이에 탄생한 것이 JPA이다.
그렇다고, JPA는 특별한것이 아니다.
JPA는 JDBC를 한번 더 감싸서, JAVA를 객체지향관점에서 데이터베이스를 이용 할 수 있게 하는 프레임워크일 뿐이다.
실무관점에서 JPA를 쓰는 이유중 가장 실감나는 이유는 생산성이다.
기계적으로 짜는 SQL 쿼리를 JPA가 대신해주기 때문에 생산성이 올라간다.
DB구조가 변경되어 필드 하나를 추가한다고 가정해보자
MyBatis라면, 잘짜여진 SQL을 수정하는 과정을 반복하며, 많은 시간을 소비할 것이다. 하지만 JPA라면, 객체 필드 하나만을 추가하면 될 것이다.
<br>
<br>
JPA 동작과정을 공부하자
JPA는 영속성 컨텍스트
라는 공간을 가진다.
이 영속성 컨텍스트
내부에는 크게 두 가지 공간이 있다.
1. 1차캐시
2. 쓰기 지연 SQL 저장소
1차 캐시
JPA가 현재 관여하고있는 Entity
를 @Id
를 키값으로해서 1차 캐시
라는 공간에 데이터를 가지고 있는다. Map<@Id, Entity>
여기서 관여하고 있다는 의미는 보통 다음과 같다.
한 영속성 컨텍스트 내에서, 보통 트랜잭션내에서 select
되었거나, persist()
된 entity
들이다.
<br>
이 때문에 한 영속성컨텍스트내에서,
select
한 entity
를 다시 select
한다면, 캐시내에서 entity
를 가져오게 된다.
따라서, 한 영속성컨텍스트내에서 똑같은 entity
조회 시 캐시에서 가져오게 되므로 동일성
을 보장해준다.
public void test(){
Member a = em.find(id)
list.add(a);
Member b = em.find(id)
System.out.println(list.contains(b)) // true
}
<br>
캐싱된 데이터를 버리고 싶다면, EntitiManager.clear()
메소드를 호출하자.
그리고 다시 select
한다면, DB에서 데이터를 가져오게 된다.
<br>
쓰기 지연 SQL
JPA는 commit()
시점에 한번에 SQL 쿼리를 요청한다.
한 트랜잭션간에 발생한 SQL요청을 호출 시, 즉시 데이터베이스에 요청지않는다.
SQL쿼리를 쓰기 지연 SQL 저장소
에 저장하고, commit()
직전에 한번에 요청한다.
이 말을 번역 하면, commit()
직전에 flush()
를 호출하는 것이다.
flush()
는 쓰기 지연 저장소 쌓아둔 SQL쿼리를 데이터베이스에 요청하는 행위를 한다.
JPA commit()
시점에 한번에 SQL 쿼리를 요청하지만,
예외인 경우가있다.
entity
의 @id
가 @GenerateValue(staragteg=IDENTY)
인 경우이다.
이는 @id
값이 auto_increment
이기 때문에, insert
를 하지않으면 id
를 지정할 수가없다.
따라서, save()
시점에 insert
쿼리가 전송되어, @Id
값을 갱신한다.
만약, 쌓아둔 SQL쿼리를 commit()
전에 요청하고싶다면 flush()
를 사용하자.
<br>
변경감지
JPA의 entity
의 update
는 Dirty Checking(더티체킹)
에 의해 이루어진다.
JPA는 flush()
되기 직전에,
entity
의 내용을 1차캐시에 저장된 내용과 비교하여 변동사항이 있는지 확인한다. 이 행위를 Dirtch Checking
이라 정의한다.
변동사항이 있는 경우에는, update
쿼리를 요청하게된다.
<br>
<br>
JPA 연관관계
4가지 연관관계를, 다음 어노테이션으로 정의한다
@OneToOne // 1 : 1
@OneToMany // 1 : N
@ManyToOne // N : 1
@ManyToMany // N : N
<br>
JPA가 어렵게 느껴지는 이유중 하나는
양방향 매핑에서 주인의 개념이 어렵기 때문이다.
객체지향에 양방향은 존재하지않는다.
A ->B
를 참조하고, B -> A
를 참조하는것은
양방향이 아닌 단방향 2개를 가지는 것이다.
하지만 관계형DB는 조인을 통해 양방향 매핑을 할 수 있다,
한번의 조인으로 양쪽에서 모두 참조가 가능한다.
객체지향적 관점과, 관계형데이터베이스의 이런 이질감이 JPA를 어렵게한다.
<br>
양방향의 주인의 이질감
따라서 이질감을 JPA가 어떻게 컨트롤하는지 이해한다면,
JPA를 쉽게 접근 할 수 있다.
A entity
와, B entity
가 있다고 가정하자.
둘은 1:1
관계이다.
A1-> B1
참조관계에서 B1
을 B2
로 값을 바꾼다면,
JPA는 정상적으로 외래키를 바꾼다.
반대로,
B1 -> A1
참조관계에서 A1
을 A2
로 값을 바꾼다면,
JPA는 정상적으로 외래키를 바꾼다.
그렇다면, 동시에
A1 -> B1
참조관계에서 B1
을 B2
로 값을 바꾼다면,
B1 -> A1
참조관계에서 A1
을 A2
로 값을 바꾼다면,
어떻게 해야될까?
논리적으로, 1:1
의 관계를 객체에 매핑한다면
A1 -> B1
이라면, 반대로 B1 -> A1
참조가 이루어져야한다.
하지만 위에 수정사항은 규약을 깨버린다.
따라서, 둘 중의 하나에 변동에 맞추어 외래키를 바꾸어주어야 한다.
그리고 그 맞춰어야 하는 객체를 주인
이라고 정의한다.
두 객체중 하나에 JPA는 주인
을 지정하고,
주인
의 변동사항만 데이터베이스에 반영한다.
주인이 아닌
객체는 단순히 조회만 할 수 있다.
그렇다면 두 객체중 누가 주인
이 되어야하는가?
짧게 말하면, 외래키를 필드로 가지고 있는 테이블이 주인
객체가 되야 한다.
<br>
<br>
@ManyToOne // N : 1
@ManyToOne
은 가장 많이쓰는 연관관계이다.
<br>
단방향 일 때는 그냥 쓰면된다.
<br>
양방향관계 일 때는, 관계의 주인을 지정해야한다.
간단히 코드를 짠다면 다음과 같다
public class Team {
@OneToMany(mappedby='team')
List<Memaber> members = new ArrayList<>();
}
public class Member{
@ManyToOne
@JoinColumn(name="team_id")
Team team;
}
mabbedby
가 들어가면, 주인이 아닌
객체가 된다.
앞서 말해듯이 주인
은 외래키를 가진 테이블의 객체로 지정해야한다.
1:N
관계에서 외래키는 무조건 N
이 가지고있다.
따라서 @ManyToOne
어노테이션을 가지고 있는 객체를 주인으로 지정하자.
<br>
<br>
위에 말해듯이 주인
객체의 값의 변동만 DB에 반영된다.
그렇다고 주인
객체가 가진 값만 바꿔야 할까?
그렇지 않다, 양쪽다 바꾸는 것이 관례이다.
Entity에 메소드에, 연관관계 편의 메소드
를 정의하자
어느쪽이든 상관없다, 하지만 한쪽에만 메소드만 만들자.
public class Member{
private Team team;
public void changeTeam(Team team){
this.team = team;
team.getMembers().add(this);
}
}
<br>
<br>
@OneToMany
@OneToMany
관계에서 단방향 매핑은 하지말자.
@OneToMany
의 단방향 매핑을 한 후,
public class Team {
@OneToMany
@JoinColum(name="team_id")
List<Member> members;
}
<br>
여기서 Team
을 컨트롤해서
특정 Member
와의 연관관계를 끊어보자.
그럼 Memeber
의 외래키값이 바뀔것이다.
이것이 적절할까?
JPA는 객체와 관계형테이블을 매핑해주는 역할을한다.
Team
객체는 Team
테이블에 매핑되고
Member
객체는 Member
테이블에 매핑되는것이다.
하지만, Team
객체를 조작해서 Member
테이블에 값이 바뀌는 것은 JPA관점에 적절하지 않을 것이다.
따라서 @OneToMany
를 단방향으로 사용하고 싶다면,
차라리 단방향이 아닌, 양방향매핑을 사용하고, 주인이 아닌 객체가 되자.
<br>
<br>
@OneToOne
@OneToOne
은 테이블에서 어느쪽이든 외래키를 줄 수 있을 것이다.
그리고 어느 테이블에 외래키를 줄지는 설계에 따라 다를것이다.
외래키를 필드를 가진 테이블이 주인
을 할 수 있도록 하자.
<br>
<br>
@ManyToMany
쓰지말자..
써야한다면, 별도의 JOIN 테이블을 정의하고
@ManyToOne
관계로 변경하자.
public class AEntity{
@Id
long id;
@OneToMany
List<ABRelation> abrelataions;
}
public class BEntytiy{
@Id
long Id;
@OneToMany
List<ABRelation> abrelations;
}
public class ABRelation{
@Id
long id;
@ManyToOne
AEntity a;
@ManyToOne
BEntity b;
}
<br>
<br>
JPA에서의 상속
객체의 상속을 관계형 DB로 구현할려면 세가지 방법이 있다.
<img src="https://static.podo-dev.com/blogs/images/2019/10/09/origin/c5fbd9ba-c9bc-48aa-bbd2-152e2a93558e.PNG" style="width:443px;">
- 1:1 관계를 맺는 테이블을 정의해, JOIN을 사용하던지
- 하나의 테이블에 모든 필드를 주던지
- 각자 테이블을 가지던지
각자 장단점이 있다.
<br>
<br>
1
번은 장점은 가장 객체지향적 관점에 적절하다.
1
번을 디폴트로 염두해두고 사용하자
하지만 JOIN
쿼리가 많이 발생하고, insert
쿼리도 두번씩 요청된다.
(feat. Dtype
을 꼭 정의하자.)
<br>
<br>
2
번의 장점은 성능적인 이슈에서의 장점이다
하지만 객체지향적이지 않고, 확장도 유연하지 않으면, 놀고 있는 필드가 생길 것이다.
간단하고, 앞으로 확장할 일도 없을것이다 라면 2
번을 사용하자
<br>
<br>
3
번은 쓰지말자..
<br>
<br>
코드는 다음을 참조하자
@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 상속 전략
@DiscriminatorColumn(name="type") // 구분 하는 칼럼
public abstract class Item { //추상클래스로 정의하는것을 잊지말자.
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name="no")
private Integer no;
@Column(name="name")
private String name;
@Column(name="price")
private Integer price;
}
@Entity
@DiscriminatorValue("movie") //구분하는 컬럼에 입력될 값
public class Movie extends Item{
private String actor;
}
@Entity
@DiscriminatorValue("music")
public class Music extends Item{
private String artist;
}
@Entity
@DiscriminatorValue("book")
public class Book extends Item{
private String writer;
}
<br>
<br>
@MappedSupperClass
@MappedSupperClass
는 상속과 관계 없다.
예를 들어, 여러 객체가 중복으로 꼭 사용하는 필드가 있다고 가장하자
예를들어 createBy
, createAt
같은 필드는 대부분의 entity
가 가지는 값이다.
이럴 경우 상위에 @MappedSupperClass
가진 추상 클래스를 정의하고 이를 상속받도록하자.
코드를 보면 쉽게 이해할 수 있다
@MappedSuperclass
public abstract class BaseEntity {
@CreatedBy
private String createBy;
@CreatedDate
private LocalDateTime createAt;
}
@Entity
public class BlogTag extends BaseEntity {
}
이렇게 하면 상속받은 entity
는 해당 필드를 가지고 있는다.
<br>
<br>
.