티스토리 뷰



다대다[N:M]

  • 실무에선 사용하지 않는 것을 추천한다. 사용하면 안되는 이유를 학습하자.

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.

  • 연결 테이블(조인 테이블)을 추가해서 일대다, 다대일 관계로 풀어내야한다.

  • 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.

    • ORM 입장에서는 테이블은 안되고, 객체는 안되는 것을 지원해줘야 한다.

    • 따라서, 아래의 그림에서와 같이 객체의 다대다 관계(멤버와 프로덕트가 서로 리스트를 가짐)와

    • 테이블에서 다대다 관계를 일대다 다대일 관계로 풀어낸 것 두개의 차이를 연결해준다.

  • JPA @ManyToMany 어노테이션을 사용하고

  • @JoinTable로 연결 테이블을 지정해줄 수 있다.

다대다 단방향

@Entity
public class Member {
  ...
   
   @ManyToMany
   @JoinTable(name = "member_product")
   private List<Product> products = new ArrayList<>();
   
  ...
}
  • 실행된 SQL

    • 조인 테이블이 하나 생성되고, 외래 키 제약조건도 두가지가 설정 된다.

        ...
     
    Hibernate:
       
       create table member_product (
          Member_id bigint not null,
          products_id bigint not null
      )
       
      ...
       
    Hibernate:
       
       alter table member_product
          add constraint FK17rh8i9jrsy7yqy2j6e9yijuw
          foreign key (products_id)
          references Product
    Hibernate:
       
       alter table member_product
          add constraint FK3cjijenmv5sgu1w04p4ofy6ik
          foreign key (Member_id)
          references Member

다대다 양방향

  • 똑같이 @ManyToMany 설정을 해주고, mappedBy 설정을 해줘야 한다.

@Entity
public class Product {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

   private String name;

   @ManyToMany(mappedBy = "products")
   private List<Member> members = new ArrayList<>();
}

다대다 매핑의 한계

  • 편리해 보이지만 실무에서 사용하면 안된다.

  • 개발하다 보면, 연결 테이블이 단순히 연결만 하고 끝나지 않는다. 조인 테이블 자체에 주문시간, 수량 같은 추가 데이터가 많이 들어갈 수 있다.

  • 하지만, 매핑 정보만 넣는 것이 가능하고, 추가 정보를 넣는 것 자체가 불가능하다.

  • 그리고 중간 테이블이 숨겨져 있기 때문에 예상하지 못하는 쿼리들이 나간다.

  • 이런 문제점들 때문에 실무에서는 안쓰는게 맞다고 본다.

다대다 한계 극복

  • 어떻게 한계를 극복할까.

  • 연결 테이블용 엔티티를 추가한다. 사실상 연결 테이블을 엔티티로 승격시킨다.

  • 그리고 @ManyToMany를 각각 일대다, 다대일로 관계를 맺어준다.

    • 사실 개인적으로 이부분에 대해서는 @ManyToOne, 다대일 관계 두개로 풀어낸다는 표현이 맞는 것 같다.

    • 앞서 일대다 관계를 학습할 때, 결론적으로 다대일 양방향 매핑을 사용하자는 결론을 내기도 했고,

    • 실제로 아래의 코드상으로 봐도 FK 2개를 중간테이블에서 관리하고, @ManyToOne 양방향 매핑 2개로 이어져있다.

    • 테이블의 배치 순서상(Member -> MemberProduct(Order) -> Product) 표현을 @OneToMany, @ManyToOne으로 표현했던 것이라면 이해가 간다.

  • JPA가 만들어주는 숨겨진 매핑테이블의 존재를 바깥으로 꺼내는 것이다.

  • 위에 다대다 매핑의 한계 첨부 그림에서는 MemberProduct의 MEMBER_ID, PRODUCT_ID를 묶어서 PK로 썻지만, 실제로는 아래 처럼 독립적으로 generated되는 id를 사용하는 것을 권장한다. ID가 두개의 테이블에 종속되지 않고 더 유연하게 개발 할 수 있다. 시스템을 운영하면서 점점 커지는데 만약 비즈니스적인 제약 조건이 커지면 PK를 운영중에 업데이트 하는 상황이 발생할 수도 있다.

  • 코드로 이해하기

    • Member

      • 멤버 엔티티에서 @OneToMany 관계로 변경한다.

        @Entity
        public class Member {
          ...
               
           @OneToMany(mappedBy = "member")
           private List<MemberProduct> memberProducts = new ArrayList<>();

          ...
        }
    • Product

      • 마찬가지로 @OneToMany 관계로 변경한다.

        @Entity
        public class Product {

          ...

           @OneToMany(mappedBy = "product")
           private List<MemberProduct> members = new ArrayList<>();
           
          ...
        }
    • MemberProduct

      • 연결 테이블을 엔티티로 승격시킨다. 그리고 @ManyToOne 매핑을 두개 한다.(연관관계의 주인)

      • 여기서 추가 데이터가 들어간다면 아예 의미있는 엔티티 이름(Order)으로 변경 될 것이다.

        @Entity
        @Getter
        @Setter
        public class MemberProduct {

           @Id
           @GeneratedValue(strategy = GenerationType.IDENTITY)
           private Long id;

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

           @ManyToOne
           @JoinColumn(name = "product_id")
           private Product product;
        }







댓글