Spring JPA - 고급매핑과 프록시

김재현·2022년 8월 8일
0

Programmers

목록 보기
15/28
post-custom-banner

3일차.

  • 엔티티 매핑
    • 엔티티 매핑 실습.
    • 엔티티 간의 연관관계 매핑을 실습.
    • 고급 매핑 전략에 대해 소개.
  • 프록시와 연관관계
    • 프록시에 대해 학습.
    • 즉시 로딩(Eager fetct), 지연 로딩(Lazy fetch)에 대해 학습.
    • 영속성 전이에 대해 학습(CASCADE, 고아객체)

  • 설계한 엔티티로 DB 스키마를 설계하면 설계한, ERD 형태로 테이블이 생성되기는 하지만, 실제 엔티티 객체사이에는 서로 참조하지 않고 있다.

연관관계 매핑

객체 연관관계 vs 테이블 연관관계

  • 테이블은 외래키로 연관관계를 맺음.
    반면, 객체는 참조(주소)로 연관관계를 맺는다.

핵심 키워드

방향(단방향, 양방향)

  • 회원 → 주문 또는 주문 → 회원. 둘 중 한 쪽만 참조하는 것을 단방향 관계라고 한다.
  • 회원 → 주문, 주문 → 회원 양쪽 모두 서로 참조하는 것을 양방향 관계라고 한다.
  • 테이블에서의 관계는 항상 양방향이다.
class Member {
	private long id;
	private List<Order> orders; // 회원 -> 주문
}

class Order {
	private String id;
}

Member member = new Member();
Order order = meber.getOrders().get(0); // 회원 -> 주문에 참조가 가능하다.

order.getMember() // (X)
  • 회원 → 주문 참조가 가능한 단방향 매핑
class Member {
	private long id;
	private List<Order> orders; // 회원 -> 주문
}

class Order {
	private String id;
	private Member member; // 주문 -> 회원
}

Member member = new Member();
Order order = member.getOrders().get(0); // 회원 -> 주문 참조 (O)
order.getMember(); // 주문 -> 회원 참조 (O)
  • 회원 → 주문, 주문 → 회원 모두 참조가 가능한 양방향 매핑.
SELECT * FROM member AS m JOIN orders AS o 
ON m.id = o.member_id;

SELECT * FROM orders AS o JOIN member AS m
ON o.member_id = m.id
  • 테이블은 외래키를 이용해 양방향으로 조인이 가능하다.

다중성(다대일, 일대다, 다대다)

  • 회원은 여러 주문을 할 수 있기에, 회원(1)과 주문(N)은 일대다 관계이다.
  • 주문은 여러 회원에 의해 발생할 수 있기에, 주문(N)과 회원(1)은 다대일 관계다.

연관관계 주인(mapped by)

  • 객체를 양방향 연관관계로 만들면, 연관관계의 주인을 정해야 한다.
  • 외래키를 관리할 객체를 지정. (INSERT, UPDATE, DELETE 등)
  • 연관관계 주인만이, 외래키를 등록·수정·삭제 할 수 있다. (주인이 아닌 쪽은 읽기만 가능)
  • 테이블 중 FK(Foreign Key)가 있는 쪽이 연관관계 주인이 된다. (회원(1) - 주문(N) → 주문 엔티티가 연관관계의 주인이된다.)

고급 매핑

  • JPA는 RDB의 테이블과 매핑된 객체(Entity)를 객체답게 사용할 수 있도록 여러가지 고급 매핑 전략을 제공.

상속관계 매핑

  • @Ingeritance()를 사용한다.
    JOIN 타입과 SINGLE타입으로 나뉜다.

조인테이블 전략

  • join테이블을 사용하면 DDL이 동작하며 테이블 생성할 때 Item테이블과 Food, Car, Furniture테이블을 FK기반으로 JOIN되게끔 자동으로 스키마를 형성한다.
  • @Ingeritance(strategy = IngeritanceType.JOINED)를 사용하면 추상클래스(abstract)로 바꾸어 주어야한다.

싱글테이블 전략

  • @Ingeritance(strategy = IngeritanceType.SINGLE_TABLE)
    @DiscriminatorColumnn(name= "DTYPE")추가.
  • 자식 엔티티에는 @DiscriminatorValue()을 추가해주어야함.
  • DiscriminatorColumnn이 추가되며 자식엔티티가 어디에 매핑되는지 탐색함.
  • 현업에서는 싱글테이블 전략을 더 많이 사용함. (관리해야되는 테이블이 적어진다.)

@MappedSuperclass

  • 실제 엔티티가 되는 클래스는 아니지만, 상속을 받게 되면 자식 테이블에서 컬럼값들을 추가로 테이블에 맵핑한다.
  • 생성자가 누구인지, 생성일시는 언제인지, 수정은 언제 되었는지 등을 BaseEntity로 따로 뽑아서 @MappedSuperclass를 써서 기본값들을 상속을 통해 많이 사용하고 있음.
@MappedSuperclass
public class BaseEntity {
    @Column(name = "created_by")
    private String createdBy;
    @Column(name = "created_at", columnDefinition = "TIMESTAMP")
    private LocalDateTime cratedAt;
}

식별자 클래스

  • JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자를 만들어야 한다.
public class Member {
	@Id
	private String id1;
	@Id
	private String id2; // runtime error
}
  • JPA는 영속성 컨텍스트에 엔티티를 보관할 때, eqauls & hashCode를 이용해서 동등성 비교를 한다.

@IdClass

  • @IdClass로 명시하고 사용할 IdClass 객체를 명시할 수 있음.
@Getter
@Setter
@Entity
@IdClass(ParentId.class)
public class Parent {
    @Id
    private String id1;
    @Id
    private String id2;
}

@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class ParentId implements Serializable {
    private String id1;
    private String id2;
}
  • 이름과 같게 사용해야한다.
  • 위의 경우 ParnetId의 키값으로 사용하겠다 하는 필드와 똑같은 값을 필드명을 명시해준다.
  • Serializable인터페이스를 구현해야한다.
  • eqauls hashCode를 구현해야한다.
  • 기본생성자가 있어야한다.
  • 식별자 클래스는 public이어야한다.

@EmbeddedId(객체지향에 더 가깝다.)

  • @EmbeddedId를 명시하고 식별자 객체를 추가한다.
@Getter
@Setter
@Entity
public class Parent2 {
    @EmbeddedId
    private ParentId2 id;
}

@Getter
@Setter
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class ParentId2 implements Serializable {
    private String id1;
    private String id2;
}
  • Serializable 인터페이스를 구현해야 한다.
  • eqauls, hashCode를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public 이어야 한다.
  • @Embeddable 애노테이션이 있어야 한다.

ArgsConstructor

  • Lombok이 생성해주는 어노테이션
  • @NoArgsConstructor : 기본 생성자를 생성해준다.
    초기값 세팅이 필요한 final변수가 있을 경우 컴파일 에러가 발생함으로 주의한다.
    @NoArgsConstructor(force=true)를 사용하면 null, 0 등 기본값으로 초기화된다.
  • @AllArgsConstructor : 전체 변수를 생성하는 생성자를 만들어준다.

프록시

프록시와 연관관계

객체 그래프 탐색

  • 객체는 객체 그래프로 연관된 객체를 탐색한다.
    ex) `order.getMember() / member.getOrder() / ...
  • Entitiy는 객체가 데이터베이스(RDB)와 매핑되어 있어, 자유롭게 객체를 탐색하는데 제한이 있음.
    ex) order.getMember() → order 테이블과 member테이블의 정보를 모두 가져와야 하는 상태.
  • JPA는 '프록시객체'라는 기술을 사용하여 연관된 객체를 처음부터 데이터베이스에서 조회하지 않고, 실제 사용하는 시점에 조회할 수 있다.

프록시 객체

@Entity
@Table(name = "member")
public class Member extends BaseEntity {
		...

    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY(EAGER))
    private List<Order> orders = new ArrayList<>(); // proxy

    ...
}
  • FetchType. 뒤에 LAZY와 EAGER를 사용할 수 있다.
    LAZY의 경우 프록시 객체로 맵핑이 됨. member를 가져왔을 때 order를 바로 조회하는 것이 아닌, 프록시객체로 맵핑이 되어있고 member.getOrder()했을 때 오더를 오더테이블에서 셀렉트쿼리를 날려 가져옴.

프록시의 특징

  • 프록시 객체는 처음 사용할 때 한번만 초기화 된다.
  • 프록시 객체가 초기화되면, 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
  • 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다.
    따라서 준영속 상태의 프록시를 초기화하면 LazyInitializationException 예외가 발생한다.
  • 프록시 객체는 객체탐색을 자유롭게 하기 위해 필요한 시점에 쿼리화 해서 엔티티화를 한다.
    이를 위해서 JPA에서 제공하는 기술.
    Lezy fetch전략을 사용할 경우에만 사용된다.
  • EAGER의 경우 프록시가 아닌 엔티티가 된다.
    EAGER fetch는 바로 조인해서 사용하기 전부터 가져오는 전략.
    EAGER fetch는 프록시 객체를 사용하지 않는다.

지연로딩(LAZY) & 즉시로딩(EAGER)

지연로딩

  • 엔티티를 조회할 때, 연관된 엔티티를 함께 조회.

즉시로딩

  • 연관된 엔티티를 실제 사용할 때 조회.

영속성 전이(CASCADE)

  • 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속상태로 만들고 싶을 때 사용한다.
public enum CascadeType {
    ALL,
    PERSIST,
    MERGE,
    REMOVE,
    REFRESH,
    DETACH;

    private CascadeType() {
    }
}

CASCADE 종류

  • Entity LifeCycle이 변할 때 여기에 연관된 객체들의 영속성 전이를 어느 순간에 할 것인가.
  • ALL : 모두 적용.
  • PERSIST : 기준 엔티티가 영속화가 될 때
  • MERGE : 병합
  • REMOVE : 삭제
  • REFRESH : REFRESH(데이터 값을 다시 읽어옴.)
  • DETACH : DETACH(엔티티 제거)
  • 기준 엔티티에서 영속 상태를 얼마만큼 전이할지 결정

고아객체

  • 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제.
@Entity
@Table(name = "member")
public class Member extends BaseEntity {
    ...

    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Order> orders = new ArrayList<>();

		...
}
  • orphanRemoval를 true로 설정해주면 flush 순간 RDS에서도 삭제됨.
post-custom-banner

0개의 댓글