Database & ORM/JPA 프로젝트

6. Join, Fetch, Cascade, 연관 관계 주인

MJ.Lee 2022. 3. 11. 09:07

도메인 모델과 테이블

도메인 모델

  1. 주문 정보(Order)에는 주문한 고객(Member), 배송 정보(Delivery), 주문한 상품 목록(OrderItem List)가 있다. 또한 주문 상태 (OrderStatus)와 주문 날짜(Date)도 있다.
  2. Order과 Item은 다대다 관계이다. Order은 여러 Item을 주문할 수 있고, Item은 여러 Order에 쓰일 수 있다. 다대다 관계를 일대다, 다대일 관계로 바꿔주기 위해 Order_Item이라는 연결 테이블 을 사용한다. OrderItem이라는 도메인을 만들어 연결 엔터티로 사용한다.
  3. OrderItem은 주문한 상품으로 Order와 Item을 갖고 있다. 또한 주문한 가격 (orderPrice)와 주문한 상품 개수(count)도 정보로 갖고 있다.
  4. Category는 Item과 다대다 관계이다. 다대다 관계를 일대다, 다대일 관계로 바꿔주기 위한 CATEGORY_ITEM이란 연결 테이블을 생성했지만, 따로 연결 엔터티를 사용하지 않았다.

테이블 구조

도메인에서는 Orders에서 객체인 Member와 Delivery를 갖지만, 테이블에서는 식별자인 MEMBER_ID와 DELIVERY_ID를 갖는다.

  • Orders와 Member는 다대일 관계다.
  • Orders와 Order_Item은 일대다 관계다.
  • Orders와 Delivery는 일대일 관계다.
  • Category와 Item은 다대다 관계다. CATEGORY_ITEM이 연결 테이블이다.

Domain

JOIN

@ManyToOne 다대일

Orders와 Member는 테이블 구조에서 보다시피 다대일 관계다.
도메인 모델이 보면 Member 도메인을 Order의 객체로 가지고 있다.
private String member_id가 아니라, private Member member 필드를 가지고 있다.

  • @ManyToOne: 다대일 관계에서 Join을 설정할 때 사용하는 Annotation.
  • @JoinColumn: Join할 때 사용하는 테이블의 컬럼명. 여기서는 ORDERS 테이블의 member_id 컬럼이다. @JoinColumn을 가지고 있는 도메인이 연관 관계에서 주인이다.

@OneToOne 일대일

Order와 Delivery는 테이블 구조에서 보다시피 일대일 관계다.

  • @OneToOne: 일대일 관계에서 Join을 설정할 때 사용하는 Annnotation.
  • @JoinColumn이 있으므로 Order와 Delivery에서 Order가 연관 관계의 주인이다.
    Orders 테이블의 delivery_id 컬럼을 가지고 Delivery와 조인하므로, delivery_id가 name에 들어간다.

@OneToMany 일대다

Order와 OrderItem은 일대다 관계다. 하나의 주문(Order)에 여러 주문 상품(OrderItem)이 있다.

  • @OneToMany: 일대다 관계에서 Join을 설정할 때 사용하는 Annotation.
  • 일대다 관계이므로 OrderItem이 여러 건이라 List로 받는다.

  • 식별관계의 주인이 아니기 때문에, mappedBy로 연관 관계 주인인 OrderItem 내 Join 대상인 필드를 정해줘야 한다. OrderItem의 order 필드와 조인하므로, mappedBy에 order를 넣어준다.

@ManyToMany 다대다

Category와 Item은 다대다 관계다.

  • @ManyToMany @JoinTable을 사용해서 연결 테이블(CATEGORY_ITEM)을 바로 매핑한다.
  • 따라서 CategoryItem이란 엔터티 없이 매핑을 완료할 수 있다.
  • 하지만, ORDER_ITEM처럼 연결 테이블에 외래키 이외 컬럼이 추가되면 (Ex., ORDER_ITEM_ID, ORDERPRICE, COUNT) @ManyToMany를 사용할 수 없다.
  • 컬럼을 추가하지 못하는 것은 실무에서 활용하기에는 무리가 있다.
  • 따라서 실무에서는 OrderItem처럼 CategoryItem이라는 연결 엔터티를 만들어서 일대다, 다대일 관계로 매핑하는 것을 권장한다.

 

@JoinTable 속성

  • name: 연결 테이블을 지정한다.
  • joinColumns : 현재 방향인 Category와 매핑할 연결 테이블의 조인 컬럼 정보
  • inverseJoinColumns : 반대 방향인 Item과 매핑할 연결 테이블의 조인 컬럼 정보

연관 관계 주인

객체는 참조(주소)를 이용해 관계를 맺고, 테이블은 외래 키(FK)를 이용해 관계를 맺는다.
@OneToMany, @ManyToOne, @OneToOne 모두 객체의 참조와 테이블의 외래 키를 매핑하기 위함이다.

객체와 테이블의 차이점은, 객체는 Order 안에서 Member 객체를 참조하는 Order -> Member 단방향 관계이다. 반면, Table은 항상 양방향 관계이다.

만약, 객체가 양방향 관계를 가지고 싶다면 Order와 OrderItem 처럼 서로 객체를 참조하면 된다. Order는 List로 OrderItem을 참조하고, OrderItem은 Order를 참조하고 있다. 이렇게 객체 간 양방향 관계를 만들면 연관관계의 주인을 정해야 한다.

연관 관계 주인만이 데이터베이스 연관관계와 매핑되고, 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.

어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하고, 주인이 mappedBy를 사용하지 않는다.
즉, Order는 OrderItem 간의 관계에서 주인이 아니고, OrderItem이 주인이다.

연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다. 주인이 아닌 반대편은 읽기만 가능하고 외래키를 변경하지는 못한다.
따라서, 외래 키를 테이블 컬럼에서 관리하는 ORDER_ITEM을 연관 관계의 주인으로 정한다.

참고로, 일대다, 다대일 관계에서는 항상 다 쪽(ORDER_ITEM)이 외래 키를 가진다.
다 쪽인 @ManyToOne은 항상 연관 관계의 주인이 되므로 mappedBy를 설정할 수 없다.
즉, @ManyToOne은 mappedBy 속성이 없다.

Cascade

원래라면 Order 내 OrderItem List에 OrderItem을 추가해도, 추가된 OrderItem이 DB에 반영되지 않는다. Order는 연관 관계의 주인이 아니여서 읽기 기능(Select)만 가능하기 때문이다.

하지만 Cascade 속성을 사용함으로써, OrderItem List에 OrderItem을 추가해도 DB에 반영된다.

Cascade는 영속성 전이로 연관된 엔터티도 함께 영속 상태로 만든다.
JPA에서 엔터티를 저장할 때 연관된 모든 엔터티는 영속 상태여야 한다.
영속성 전이를 사용하면 부모만 영속 상태로 만들면 연관된 자식까지 한 번에 영속 상태로 만들 수 있다.

즉, 부모인 Order 엔터티가 영속 상태가 될 때, 연관된 자식인 OrderItem들까지 한 번에 영속상태가 된다는 뜻이다. CasecadeType.All 이므로 Order가 저장되거나 삭제될 때, OrderItem까지 같이 저장되거나 삭제된다.

참고로, Cascade Type에는 All, Persist(영속), Merge(병합), Remove(삭제), Refresh, Detach 가 있다.


Order의 Delivery에도 Casecade 옵션이 있으므로, Order 삭제 시, 연관된 Delivery도 같이 삭제된다.

연관관계 편의 메소드

양방향 연관 관계를 설정하고 가장 흔히 하는 실수 연관 관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다. Cascade가 없으면 연관 관계 주인만이 값을 저장할 수 있다. 주인이 아닌 곳에 값을 입력하면 DB에 저장되지 않는다.

객체 관점에서도 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다. 안그러면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.

즉, 객체의 양방향 연관 관계는 양쪽 모두 관계를 맺어줘야 한다.

Order와 OrderItem은 양방향 관계이다.
Order에서 OrderItem을 OrderItem List에 추가할 때, OrderItem에도 Order를 세팅해준다.

이렇게, addOrderItem처럼 한 번에 양방향 관계를 설정하는 메소드 연관 관계 편의 메소드라 한다.

연관 관계 편의 메소드 작성 시 주의사항

객체 간의 관계가 변경되면 반드시 양쪽에서 참조를 끊어줘야 한다.

Fetch Type

JPA의 Fetch 전략에는 2가지가 있다.

  • 지연 로딩(LAZY): 연관된 엔터티를 프록시로 조회한다. 프록시를 실제 사용할 때 초기화하면서 데이터베이스를 조회한다.
  • 즉시 로딩(EAGER): 연관된 엔터티를 즉시 조회한다. 하이버네이트는 가능하면 SQL 조인을 사용해서 한 번에 조회한다.

JPA의 기본 페치(fetch) 전략은 연관된 엔터티가 하나면 즉시 로딩을, 컬렉션이면 지연 로딩을 사용한다. 추천 하는 방법은 모든 연관관계에 지연 로딩을 사용하는 것이다. 실제로 사용하는 상황을 보고 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화하면 된다.

객체 그래프 탐색

JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다.
이로써 연관된 객체를 계속 탐색할 수 있게 해주고, 이를 객체 그래프 탐색이라 한다.

Order에서 주문한 사람의 이름을 알기 위해, Order.member.getName()을 사용할 때, JPA가 Select SQL을 자동으로 실행해서 member의 정보를 DB로 부터 가지고 온다.

만약 FetchType.Lazy를 사용하지 않았다면, 즉시 로딩(EAGER)로 Order 조회 시 member도 같이 DB에서 조회되었을 것이다. 그러면 member.getName()으로 주문한 사람의 이름을 가져올 때, DB로 부터 가지고 오지 않고 이미 조회해서 영속 컨텍스트에 있는 member에서 가지고 온다.