[JPA] 연관관계 매핑 - M:N

Bam·2025년 5월 19일
0

Spring

목록 보기
59/73
post-thumbnail

다대다(M:N) ManyToMany

쇼핑몰에서 하나의 고객은 여러 상품을 구매할 수 있고 하나의 상품은 여러 고객들에게 구매될 수 있습니다. 이와 같은 관계를 다대다(M:N) 연관관계라고 합니다.

일대일, 일대다를 표현할 때는 테이블을 서로 연관관계로 이어서 표시하기만 하면 됐습니다. 하지만 이 방법으로는 다대다 관계를 표현할 수 없습니다.위 이미지와 같이 다대다를 표현하기 위해서는 연결 테이블이라는 개념을 이용해서 나타내게 됩니다. 연결 테이블을 통해 고객과 일대다, 상품과 다대일 관계를 표현하게 되면서 다대다 연관관계를 나타내게 됩니다.

테이블은 위와 같이 연결 테이블을 이용해서 다대다 관계를 표현해야 했으나 자바 객체에서는 상품에선 고객 리스트, 고객에서는 상품 리스트와 같이 컬렉션을 이용해서 두 개의 객체만을 사용해도 표현할 수 있습니다.

단방향 다대다

주인 테이블을 Customer라고 가정했을 때의 단방향 다대다 매핑입니다.

@Entity
@Table(name = "customers_table")
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "c_id", nullable = false)
    private Long id;

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
        name               = "customer_product",
        joinColumns        = @JoinColumn(name = "c_id"),
        inverseJoinColumns = @JoinColumn(name = "p_id")
    )
    private Set<Product> products = new HashSet<>();

    // 생성자, getter
}
@Entity
@Table(name = "products_table")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "p_id", nullable = false)
    private Long id;

    // 생성자, getter
}

@ManyToMany, @JoinTable을 사용해서 고객과 상품 엔티티를 다대다 매핑했습니다.

@JoinTable을 사용해서 연결 테이블인 customer_product를 직접 매핑했기 때문에 연결 테이블 엔티티 객체 생성 없이 바로 단방향 매핑을 할 수 있었습니다.

@JoinTable의 속성은 다음과 같습니다.

속성설명
name연결 테이블 지정
joinColumns현재 방향 엔티티와 매핑할 @JoinColumn 정보 지정
inverseJoinColumns반대 방향 엔티티와 매핑할 @JoinColumn 정보 지정

위 예시에서 연관관계 방향은 고객 -> 연결 테이블, 연결 테이블 <- 상품이므로 현재 방향은 고객이 되고, 반대 방향은 상품이 됩니다.

위 결과로 c_id가 담은 상품을 조회하는 경우 연결 테이블과 상품 테이블이 JOIN되어 탐색하게 됩니다.

양방향 다대다

양방향은 기존 단방향에서 반대되는 엔티티에 mappedBy 속성을 추가해주면 됩니다.

@Entity
@Table(name = "products_table")
public class Product {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "p_id", nullable = false)
    private Long id;

    @ManyToMany(mappedBy = "products", fetch = FetchType.LAZY)
    private Set<Customer> customers = new HashSet<>();

    // 생성자, getter
}

조인 엔티티

위 예시에서는 연결 테이블이 단순히 다대다 관계를 표현하기 위한 구조라서 객체 생성 등의 문제 없이 매핑을 수행했습니다. 그러나 다음처럼 연결 테이블에 데이터가 추가되어 유의미한 정보를 가진다면 어떻게 해야할까요?위와 같이 컬럼이 추가되면 @ManyToMany를 사용할 수 없습니다. 왜냐하면 orderedAt 컬럼을 회원이나 상품 측에서 매핑할 수 없기 때문입니다.

이런 경우에는 연결 엔티티를 별개의 엔티티로 구현해야하는데 이런 엔티티를 조인 엔티티 Association Entity라고 합니다. 그리고 조인 엔티티 입장에서 고객과 상품과의 관계를 다대일, 일대다로 풀어서 표현하게 됩니다.

식별자 클래스

위 테이블 구조에서 고객-상품 테이블은 c_id, p_id로 이루어진 복합 키를 기본 키로 사용하게 됩니다. JPA에서는 이러한 복합 키를 사용하기 위해서 따로 식별자 클래스를 정의해야합니다.

@Embeddable
public class CustomerProductId implements Serializable {
    @Column(name = "c_id")
    private Long customerId;

    @Column(name = "p_id")
    private Long productId;

    protected CustomerProductId() {}

    public CustomerProductId(Long customerId, Long productId) {
        this.customerId = customerId;
        this.productId  = productId;
    }
    
    // equals, hashCode Override 생략
}

@Embeddable은 직접 정의한 값 타입을 표시합니다.

조인 엔티티 양방향 매핑

식별자 클래스를 생성했으면 다음과 같이 조인 엔티티를 매핑합니다.

@Entity
@Table(name = "customer_product")
public class CustomerProduct {

    @EmbeddedId
    private CustomerProductId id;

    @MapsId("customerId")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "c_id", nullable = false)
    private Customer customer;

    @MapsId("productId")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "p_id", nullable = false)
    private Product product;

    @Column(name = "ordered_at", nullable = false)
    private LocalDateTime orderedAt;

    protected CustomerProduct() {}

    public CustomerProduct(Customer customer, Product product, LocalDateTime orderedAt) {
        this.id        = new CustomerProductId(customer.getId(), product.getId());
        this.customer  = customer;
        this.product   = product;
        this.orderedAt = orderedAt;
    }

    //getter 생략
}
  • @EmbeddedId: 복합 키 식별자 클래스 매핑을 위함
  • @MapsId: @EmbeddedId를 사용할 때 함께 사용. 연관관계의 외래 키를 이 엔티티의 기본 키로 사용하겠다 선언. 여기서는 식별자 클래스의 각 키 필드 명을 사용.

이렇게 식별자 클래스를 통해 조인 엔티티 정의가 완료되었으면 이제 고객과 상품에 대하여 1:N 매핑을 수행합니다.

@Entity
@Table(name = "customers_table")
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "c_id", nullable = false)
    private Long id;

    @OneToMany(
      mappedBy = "customer",
      cascade   = CascadeType.ALL,
      orphanRemoval = true
    )
    private List<CustomerProduct> orders = new ArrayList<>();

    //생성자 및 getter
}
@Entity
@Table(name = "products_table")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "p_id", nullable = false)
    private Long id;

    @OneToMany(
      mappedBy = "product",
      fetch    = FetchType.LAZY
    )
    private List<CustomerProduct> customers = new ArrayList<>();

    //생성자 및 getter
}

위와 같이 복합 키를 위한 식별자 클래스를 정의하는 등의 과정이 어렵다면 조인 엔티티를 하나의 엔티티(테이블)로 만들고 기본 키를 생성하여 사용하는 방식도 존재합니다.

0개의 댓글