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>

이 때문에 한 영속성컨텍스트내에서,
selectentity를 다시 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의 entityupdateDirty 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 참조관계에서 B1B2로 값을 바꾼다면,
JPA는 정상적으로 외래키를 바꾼다.

반대로,
B1 -> A1 참조관계에서 A1A2로 값을 바꾼다면,
JPA는 정상적으로 외래키를 바꾼다.

그렇다면, 동시에
A1 -> B1 참조관계에서 B1B2로 값을 바꾼다면,
B1 -> A1 참조관계에서 A1A2로 값을 바꾼다면,
어떻게 해야될까?

논리적으로, 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://file.podo-dev.com/blogs/images/2019/10/09/origin/c5fbd9ba-c9bc-48aa-bbd2-152e2a93558e.PNG" style="width:443px;">

  1. 1:1 관계를 맺는 테이블을 정의해, JOIN을 사용하던지
  2. 하나의 테이블에 모든 필드를 주던지
  3. 각자 테이블을 가지던지

각자 장단점이 있다.

<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>

.

0
이전 댓글 보기
등록
TOP