[JPA] 연관관계 매핑, 편의 메소드 그리고 주의사항

jiaLEE·2022년 6월 15일
2

개념

  • 연관관계에는 다대일, 일대다, 일대일, 다대다 이렇게 다중성이 존재한다.
    (다대다는 되도록 쓰지않습니다.)

  • 관계의 방향이 존재하는데 한 쪽이 반대쪽을 참조하는 관계를 단방향, 서로가 참조하는 관계를 양방향이라 한다.

  • 연관관계의 주인:
    DB에서는 외래키(이하 FK)를 통해 두 테이블이 연관관계를 맺는다. -> 연관관계는 FK로 관리한다.
    양방향관계일 경우, 양쪽에서 매핑을 하기 때문에 관리포인트가 2곳이다. -> JPA는 두 객체 중 하나를 정해 FK를 관리하게 한다. 여기서 FK를 관리하는 객체를 '연관관계의 주인'이라고 하며 보통 FK를 갖고 있는 엔티티가 FK를 관리하며 연관관계의 주인이 된다

이때 FK를 관리하는 연관관계의 주인(객체)만이 외래 키를 변경할 수 있으며, 주인이 아닌 객체는 읽는 것만 가능하다.(=거울)

주로 연관관계에서 FK를 갖고있는 엔티티가 주인이 되는데(주로 n쪽) 이건 단순히 누가 FK를 관리하느냐이지 우위는 아니다.
예) 자동차-바퀴, 자동차가 우위이기에 주인일 것 같지만 실제는 바퀴가 N이기에 바퀴가 주인임 -> 항상 염두!!

연관관계 매핑

1. 다대일 매핑 (N:1) - @ManyToOne

단방향의 경우, 한쪽에만 매핑을 하는 것이고 양방향의 경우 양쪽으로 매핑을 하면 된다.
기본적으로 @JoinColumn으로 어떤 컬럼과 매핑되는 지 적어준다.

@Entity
@Getter @Setter
public class OrderItem {

    @Id @GeneratedValue
    @Column(name="order_item_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name="item_id")
    private Item item;

    @ManyToOne //order:orderItem = 1:n
    @JoinColumn(name="order_id")
    private Order order;

    private int orderPrice; //주문 당시 가격
    private int count; //수량
}

2. 일대다 매핑 (1:N) - @OneToMany

일대다 이기때문에 보통 set, list와 같은 Collection에 매핑한다.
여기서 양방향까지 하게 되면 위에서 언급했다시피 연관관계의 주인을 만들어줘야 하는데 주인이 아닌쪽에 mappedBy 속성을 넣어서 매핑되는 컬럼명을 기입해 양방향 관계를 설정한다.

@Entity
@Table(name="orders")
@Getter @Setter
public class Order {

    @Id @GeneratedValue
    @Column(name="order_id")
    private Long id;

    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne
    @JoinColumn(name="delivery_id")
    private Delivery delivery;  

 }

위의 코드를 보게 되면 Order:OrderItem = 1:N의 관계이기때문에 @OneToMany 어노테이션을 쓰고 양방향으로 매핑하기때문에 mappedBy속성을 넣어서 주인이 아님을 명시한다.

3. 일대일 매핑(1:1) - @OneToOne

일대일의 매핑의 경우 @OneToOne을 통해 선언하며
여기서 양방향으로 설정하기 위해 연관관계의 주인을 정하려면 1:1 관계기에 더 자주 쓰는 엔티티를 주인이라 설정하고 매핑한다.

@Entity
@Table(name="orders")
@Getter @Setter
public class Order {

    @Id @GeneratedValue
    @Column(name="order_id")
    private Long id;
    
    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne
    @JoinColumn(name="delivery_id")
    private Delivery delivery;  //1:1이니까 둘 중 많이 쓰는 거에 fk 넣는다. order가 주인

    private LocalDateTime orderDate;    //java 제공 기본, 주문시간
}
@Entity
@Setter @Getter
public class Delivery {

    @Id
    @GeneratedValue
    @Column(name="delivery_id")
    private Long id;

    @OneToOne(mappedBy = "delivery")
    private Order order;    //1:1일 경우 fk를 어디에 둬도 상관없다. 그렇기떄문에 더 믾이 쓰는 테이블에 fk를 넣는다.

    @Embedded
    private Address address;

}

위의 코드에서 Order와 Delivery는 1:1의 연관관계이다.
이때 양방향으로 설정한다면 둘 중 하나에 주인을 주어야 하는데 보통 Order를 더 많이 사용하기 때문에 Delivery에 mappedBy설정을 넣어서 주인이 아님을 명시했다.

4. 다대다 매핑(N:M) - @ManyToMany

방식은 동일하다. 하지만 다대다 매핑을 할 경우, 추가적으로 무언가를 추가하거나 변경할 수 없기때문에 사용하지 않는다.

연관관계 편의 메소드

양방향일 경우 양 쪽 객체를 모두 신경써야 한다. -> 하나의 메소드에서 양측의 관계를 설정하게 해주는 게 안전
그래서 한쪽에서 양방향관계를 설정(set)하는 메소드를 적어주는데 이때 빈번하게 사용하는 쪽에 양쪽에 대한 메소드를 정의하는 게 좋다.
(연관관계의 주인과 상관없음)

@Entity
@Table(name="orders")
@Getter @Setter
public class Order {

    @Id @GeneratedValue
    @Column(name="order_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="member_id")   //fk 가 member_id가 된다.
    private Member member;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name="delivery_id")
    private Delivery delivery;  //1:1이니까 둘 중 많이 쓰는 거에 fk 넣는다. order가 주인

	...

    //연관관계 메소드
    public void setMember(Member member) {
        this.member = member;
        member.getOrders().add(this);
    }

    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);

    }

    public void setDelivery(Delivery delivery) {
        this.delivery = delivery;
        delivery.setOrder(this);
    }

}

주의사항

- 일대다 보다는 다대일 양방향 매핑을 이용하자!

- 엔티티 만들때 getter만 만들고 setter는 필요할때만 만드는 것을 추천한다.

: set을 다 열어두면 값이 변경될 가능성이 커서 혼란스러울 수 있다. 변경지점을 확실하게 만들기 위해 setter는 일정 포인트에서만 열어둔다.

- 모든 연관관계는 지연로딩(LAZY)으로 설정한다.

EAGER: 연관된 데이터들을 한꺼번에 로딩 -> 예측이 어렵고 어떤 게 실행된건지 확인하기 힘들다.
LAZY(지연):필요할 때 데이터들을 로딩한다.

@OneToOne, @manyToOne 의 경우 fetch가 EAGER로 기본값이기때문에 꼭 바꿔줘야한다.
(fetch = FetchType.LAZY)

연관된 엔티티를 조회하려면 fetch조인 혹은 엔티티 그래프 기능을 사용한다.
(fetchJoin에 Pagination쿼리에 Fetch JOIN을 적용하면 모든 레코드를 가져오는 쿼리가 실행되니 유의)

- 컬렉션은 필드에서 바로 초기화하는게 안전하다.

엔티티가 persist해서 하이버네이트에 저장되는 순간, 하이버네이트는 이 값을 감싸서 이후 값에 변동이 생길때 추적한다.
그렇기때문에 영속화하면 기존의 컬렉션에서 하이버네이트가 제공하는 내장 컬렉션으로 변경된다.
-> 하이버네이터가 기껏 바꿨는데 누군가가 다시 set하면 기존에 감싸진것이 꼬인다.
-> 생성할때만 사용하고 그 뒤에는 생성하는거 건들지 않는다.

이때, List타입은 하이버네트의 Bag타입으로 매핑된다.
Bag은 중복을 허용하는 비순차 컬렉션이기에 둘 이상의 컬렉션(Bag)을 Fetch Join할 경우, 중복을 포하게 되고
MulitpleBagFetchExeption이 발생
-> List를 Set으로 변경한다. / @OrderColumn

- cascade=CascadeType.ALL

엔티티의 영속성 상태가 변화할때 연관된 엔티티도 함께 변화한다.
여러 타입 중에서 선택할 수 있다.(ALL, PERSIST, REMOVE, DETACH...)

기본 생성자

기본적으로 생성자가 있어야 함 -> JPA 구현 라이브러리가 객체를 생성할때 리플랙션 같은 기술을 사용할 수 있도록 지원해야하기 때문
생성자를 만들때 스펙 상 구현이기에 protected를 통해 표시한다.

[출처] 인프런:실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발, 김영한
[출처][2019]Spring JPA의 사실과 오해, 신동민

0개의 댓글