티스토리 뷰

JPA

[JPA] 다양한 연관관계 매핑

Jaime.Lee 2022. 7. 4. 10:30

소스 코드는 여기 있습니다. (commit hash: 6695e56)

> git clone https://github.com/lcalmsky/jpa
> git checkout 6695e56

Overview

다양한 연관관계에 대해 알아봅니다.

연관관계 매핑시 고려해야할 사항이 3가지가 있습니다.

  • 방향(단방향, 양방향)
  • 연관관계의 주인
  • 다중성

이중 두 가지는 이미 이전 포스팅에서 살펴보았으므로 다중성에 대해 알아보겠습니다.

다중성

사실 다중성도 이전 포스팅에서 다루긴했지만 다시 한번 개념을 정리해보겠습니다.

DB 관점에서의 다중성을 말합니다.

JPA는 객체를 테이블에 매핑하기 위해 다양한 애너테이션을 사용하는데 이 중 다중성에 해당하는 애너테이션은 다음과 같습니다.

  • @ManyToOne: 다대일
  • @OneToMany: 일대다
  • @OneToOne: 일대일
  • @ManyToMany: 다대다

다중성은 각각 자신의 속성에 대해 대칭성을 가집니다.

MemberTeamN:1 이라면 TeamMember는 당연히 1:N의 관계가 됩니다.

@ManyToOne

단방향

가장 많이 사용되는 연관관계로 한 쪽이 다른 한 쪽을 참조하는데 그 반대로는 참조를 허용하지 않습니다.

DB는 FK를 사용하기 때문에 방향과 상관없이 참조 가능합니다.

이전 포스팅에서 살펴보았던 MemberTeam의 관계로 예를 들 수 있습니다.

양방향

객체가 양쪽을 서로 참조할 필요가 있을 때 사용합니다.

@OneToMany

단방향

객체에서 일대다 단방향은 일에 해당하는 쪽이 연관관계의 주인이 됩니다. 테이블에서는 다 쪽에 FK가 존재합니다.

객체와 테이블의 차이 때문에 발생하는 헷갈리는 구조인데 아예 반대로 생각하는 게 마음이 편합니다.

@JoinColumn을 사용하지 않으면 자동으로 join하기 위한 테이블을 생성하므로 되도록이면 사용하시는 것을 권장합니다.

일대다 단방향 매핑은 관리하는 FK가 다른 테이블에 있는 것 뿐만 아니라 연관관계 관리를 위해 추가로 update를 실행해야 한다는 단점이 있습니다.

따라서 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는 것이 개발에 좀 더 용이합니다.

실제로 TeamMember가 있을 때 Team 위주로 개발을 해야할 일은 많지 않기 때문에 설명만 보면 어렵지만 실제 개발해야하는 상황에서는 직관적으로 선택을 할 수 있습니다.

양방향

이론적으로는 존재하지만 JPA 내에서 공식적으로 지원하지도 않기 때문에 간단한 설명으로 대체하겠습니다.

@JoinColumn(insertable=false, updatable=false) 이런식으로 설정하여 읽기 전용 방식으로 양방향 처럼 사용하는 방식입니다.

다대일 양방향 매핑을 사용하는 것으로 대체해야 합니다.

@OneToOne

일대일 관계는 그 반대도 동일하게 일대일이고 관계의 주인을 정할 때도 FK를 누가 가지고있느냐에따라 달라집니다.

주 테이블에 FK가 존재하는 경우 주 객체가 대상 객체의 참조를 가지는 것과 동일하게 사용할 수 있으므로 객체지향 개발자가 선호하는 방식이라고 할 수 있습니다.

JPA 매핑도 편리하고 직관적으로 할 수 있고, 주 테이블만 조회해도 대상 테이블에 데이터가 존재하는지 여부를 확인할 수 있습니다.

대신 값이 없는 경우 FKnull을 허용해줘야 하는데 이 부분이 헷갈릴 수 있습니다.

대상 테이블에 FK가 있는 경우 DBA 입장에서 조금 더 편리할 수 있습니다. 테이블의 관계가 변경되어야 하는 경우 좀 더 유연하게 대처할 수 있기 때문입니다.

반면 지연로딩으로 설정하더라도 즉시 로딩된다는 단점이 있는데 이 부분은 프록시를 다루는 부분에서 설명하도록 하겠습니다.

단방향

먼저 주 테이블(member)이 FK를 가지고있는 경우 객체 관계는 아래 처럼 나타낼 수 있습니다.

다대일에서 단방향 매핑과 동일합니다.

반대로 대상 테이블(seat)이 FK를 가지고있는 경우는 JPA에서 지원하지 않습니다.

양방향

다대일 양방향 매핑 처럼 FK를 가진쪽이 연관관계의 주인이 되고 반대는 mappedBy를 사용합니다.

반대 테이블이 FK를 가지고 있을 때는 기존과 방법은 같고 객체에서 속성만 반대로 가지면 됩니다.

@ManyToMany

객체간은 서로를 참조할 수 있지만 이번엔 데이터베이스에서 두 개의 테이블 만으로 구현할 수 없는 방식이라고 할 수 있습니다.

그래서 FK만 매핑하는 테이블을 따로 생성해주는데 이 때 @JoinTable을 이용합니다.

객체는 컬렉션을 이용해 서로를 참조할 수 있어 두 개로 표현할 수 있습니다.

단방향, 양방향 매핑이 모두 가능합니다.

다대다 매핑이 언뜻 보기에는 좋아보일 수 있지만 중간 매핑 테이블에 컬럼을 추가해야할 경우가 생길 수 있습니다.

그럴 땐 매핑테이블을 직접 Entity로 정의하여 사용할 수 있습니다.

이 경우 @OneToMany - 매핑테이블 - @ManyToOne 을 사용하여 ManyToMany 관계를 만들 수 있습니다.

연관관계 매핑 예제

이전 포스팅에서 사용했던 스키마에 배송, 카테고리 테이블을 추가해보겠습니다.

주문과 배송은 일대일 관계를, 상품과 카테고리는 다대다 관계를 가집니다.

이것을 다시 객체로 나타내면 아래와 같습니다.

추가된 객체간의 관계는 모두 양방향 매핑으로 진행해보겠습니다.

MemberOrderItem은 변경된 부분이 없으므로 변경되거나 추가된 클래스만 작성하겠습니다.

이전 포스팅을 참고해주세요.

/src/main/java/com/tistory/jaimenote/jpa/domain/entity/Order.java

package com.tistory.jaimenote.jpa.domain.entity;

import com.tistory.jaimenote.jpa.domain.support.OrderStatus;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.ToString.Exclude;

@Entity
@Table(name = "orders")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Order {

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

  @ManyToOne
  @JoinColumn(name = "member_id")
  private Member member;

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

  private LocalDateTime orderDateTime;

  @Enumerated(EnumType.STRING)
  private OrderStatus status;

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

}

/src/main/java/com/tistory/jaimenote/jpa/domain/entity/Item.java

package com.tistory.jaimenote.jpa.domain.entity;

import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.ToString.Exclude;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Item {

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

  private String name;

  private Integer price;

  private Integer stockQuantity;

  @ManyToMany(mappedBy = "items")
  @Exclude
  private List<Category> categories = new ArrayList<>();

}

/src/main/java/com/tistory/jaimenote/jpa/domain/entity/Delivery.java

package com.tistory.jaimenote.jpa.domain.entity;

import com.tistory.jaimenote.jpa.domain.support.DeliveryStatus;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Delivery {

  @Id
  @GeneratedValue
  private Long id;

  private String city;

  private String street;

  private String zipcode;

  @Enumerated(EnumType.STRING)
  private DeliveryStatus deliveryStatus;

  @OneToOne(mappedBy = "delivery")
  private Order order;
}

/src/main/java/com/tistory/jaimenote/jpa/domain/entity/Category.java

package com.tistory.jaimenote.jpa.domain.entity;

import java.util.ArrayList;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.ToString.Exclude;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Category {

  @Id
  @GeneratedValue
  private Long id;

  private String name;

  @ManyToOne
  @JoinColumn(name = "category_parent_id")
  private Category parent;

  @OneToMany(mappedBy = "parent")
  @Exclude
  private List<Category> child = new ArrayList<>();

  @ManyToMany
  @JoinTable(
      name = "category_item",
      joinColumns = @JoinColumn(name = "category_id"),
      inverseJoinColumns = @JoinColumn(name = "item_id")
  )
  @Exclude
  private List<Item> items = new ArrayList<>();

}

DeliveryStatusOrderStatusenum 타입으로 껍데기만 생성했습니다.

마지막으로 애플리케이션을 실행해서 로그를 확인해보겠습니다.

2022-07-01 01:46:44.812 DEBUG 45574 --- [           main] org.hibernate.SQL                        : 

    create table category (
       id bigint not null,
        name varchar(255),
        category_parent_id bigint,
        primary key (id)
    )
2022-07-01 01:46:44.816 DEBUG 45574 --- [           main] org.hibernate.SQL                        : 

    create table category_item (
       category_id bigint not null,
        item_id bigint not null
    )
2022-07-01 01:46:44.817 DEBUG 45574 --- [           main] org.hibernate.SQL                        : 

    create table delivery (
       id bigint not null,
        city varchar(255),
        delivery_status varchar(255),
        street varchar(255),
        zipcode varchar(255),
        primary key (id)
    )
2022-07-01 01:46:44.819 DEBUG 45574 --- [           main] org.hibernate.SQL                        : 

    create table item (
       item_id bigint not null,
        name varchar(255),
        price integer,
        stock_quantity integer,
        primary key (item_id)
    )
2022-07-01 01:46:44.820 DEBUG 45574 --- [           main] org.hibernate.SQL                        : 

    create table member (
       member_id bigint not null,
        city varchar(255),
        name varchar(255),
        street varchar(255),
        zipcode varchar(255),
        primary key (member_id)
    )
2022-07-01 01:46:44.821 DEBUG 45574 --- [           main] org.hibernate.SQL                        : 

    create table order_item (
       order_item_id bigint not null,
        count integer,
        order_price integer,
        item_id bigint,
        order_id bigint,
        primary key (order_item_id)
    )
2022-07-01 01:46:44.822 DEBUG 45574 --- [           main] org.hibernate.SQL                        : 

    create table orders (
       order_id bigint not null,
        order_date_time timestamp,
        status varchar(255),
        delivery_id bigint,
        member_id bigint,
        primary key (order_id)
    )
2022-07-01 01:46:44.823 DEBUG 45574 --- [           main] org.hibernate.SQL                        : 

    alter table category 
       add constraint FKgbowg38afm73793kwnokn0203 
       foreign key (category_parent_id) 
       references category
2022-07-01 01:46:44.833 DEBUG 45574 --- [           main] org.hibernate.SQL                        : 

    alter table category_item 
       add constraint FKu8b4lwqutcdq3363gf6mlujq 
       foreign key (item_id) 
       references item
2022-07-01 01:46:44.834 DEBUG 45574 --- [           main] org.hibernate.SQL                        : 

    alter table category_item 
       add constraint FKcq2n0opf5shyh84ex1fhukcbh 
       foreign key (category_id) 
       references category
2022-07-01 01:46:44.837 DEBUG 45574 --- [           main] org.hibernate.SQL                        : 

    alter table order_item 
       add constraint FKija6hjjiit8dprnmvtvgdp6ru 
       foreign key (item_id) 
       references item
2022-07-01 01:46:44.838 DEBUG 45574 --- [           main] org.hibernate.SQL                        : 

    alter table order_item 
       add constraint FKt4dc2r9nbvbujrljv3e23iibt 
       foreign key (order_id) 
       references orders
2022-07-01 01:46:44.839 DEBUG 45574 --- [           main] org.hibernate.SQL                        : 

    alter table orders 
       add constraint FKtkrur7wg4d8ax0pwgo0vmy20c 
       foreign key (delivery_id) 
       references delivery
2022-07-01 01:46:44.842 DEBUG 45574 --- [           main] org.hibernate.SQL                        : 

    alter table orders 
       add constraint FKpktxwhj3x9m4gth5ff6bkqgeb 
       foreign key (member_id) 
       references member

의도한대로 category_item 테이블이 Entity 구현 없이 자동으로 생성되었고 FK도 정확하게 추가된 것을 확인할 수 있습니다.

'JPA' 카테고리의 다른 글

[JPA] 상속관계 매핑(2): @MappedSuperclass  (0) 2022.07.06
[JPA] 상속관계 매핑(1): 상속 전략  (1) 2022.07.05
[JPA] 연관관계 매핑 예제  (0) 2022.07.03
[JPA] 연관관계의 주인  (0) 2022.06.30
[JPA] 양방향 연관관계  (0) 2022.06.29
댓글