Book/스프링부트 핵심가이드

연관관계 매핑(of. 다대다(N:N) 매핑)

블로그 주인장 2023. 11. 14.

연관관계 매핑

RDBMS를 사용할 때는 테이블 하나만 사용해서 애플리케이션의 모든 기능을 구현하는 것은 어렵다.

JPA를 사용하는 애플리케이션에서도 테이블의 연관관계를 엔티티 간의 연관관계로 표현이 가능하다.

그 중에서 대다(N:N) 매핑 방식에 대해 알아보겠습니다.


연관관계 매핑 종류와 방향

  • Many To Many : 다대다(N:N)
    • 실무에서는 거의 사용되지 않는 구성이다.
    • 예시) 상품과 생산 업체 : 한 종류의 상품이 여러 생산업체를 통해 생산될 수도 있고, 생산업체 한 곳이 여러 상품을 생산할 수도 있다.
    • 다대다 연관관계에서는 각 엔티티에서 서로를 리스트로 가지는 구조로 만들어진다.
    • '교차 엔티티' 라고 부르는 중간테이블을 생성하여 다대다 관계를 일대다 또는 다대일 관계로 해소시킨다.

 

다대다 단방향 매핑


연관관계를 가진 생산업체 엔티티를 생성

 

생산업체 엔티티

public class Producer extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String code;

    private String name;

    @ManyToMany
    @ToString.Exclude
    private List<Product> productList = new ArrayList<>();

    public void addProduct(Product product) {
        productList.add(product);
    }
}
  • 다대다 연관관계는 @ManyToMany 어노테이션으로 설정한다.
  • 리스트로 필드를 가지는 개체에서는 외래키를 가지지 않기에 별도의 @JoinColumn은 설정하지 않아도 된다.

 

리포지토리 생성

  • 생성한 생산업체 엔티티를 활용할 수 있도록 리포지토리를 생성한다.
@Repository
public interface ProducerRepository extends JpaRepository<Producer, Long> {
}

 

 

다대다 단방향 연관관계 테스트

@Test
@Transactional
void relationshipTest() {
    Product product1 = savedProduct("펜", 1000, 100);
    Product product2 = savedProduct("가방", 10000, 1000);
    Product product3 = savedProduct("가위", 5000, 500);

    Producer producer1 = savedProducer("flature");
    Producer producer2 = savedProducer("wikibooks");

    producer1.addProduct(product1);
    producer1.addProduct(product2);

    producer2.addProduct(product1);
    producer2.addProduct(product2);

    producerRepository.saveAll(Lists.newArrayList(producer1, producer2));

    System.out.println(producerRepository.findById(1L).get().getProductList());
}

private Producer savedProducer(String name) {
    Producer producer = new Producer();
    producer.setName(name);

    return producerRepository.save(producer);
}

private Product savedProduct(String name, int price, int stock) {
    return productRepository.save(Product.builder()
            .name(name)
            .price(price)
            .stock(stock)
            .build());
}
  • 가독성을 위해서 리포지토리를 통한 테스트 케이스를 별도의 메서드로 구현하였다.
  • 리포지토리를 사용하게 되면 매번 트랜잭션이 끊어져 리스트를 가져오는 작업이 불가능하다.
  • 테스트 메서드에 @Transactional 어노테이션을 지정하여 트랜잭션이 유지되도록 구성하여 테스트를 진행한다.

 

다대다 양방향 매핑


상품 엔티티에서 생산업체 엔티티 연관관계 설정

  • 필요에 따라 MappedBy 속성을 이용하여 두 엔티티 간의 연관관계 주인을 설정할 수도 있다.
  • 중간 테이블이 연관관계를 설정하고 있기에, 데이터베이스의 테이블 구조는 변경되지 않는다.
public class Product extends BaseEntity{

    //..중략
    
    @ManyToMany
    @ToString.Exclude
    private List<Producer> producers = new ArrayList<>();
    
    public void addProducer(Producer producer) {
        this.producers.add(producer);
    }
}

 

다대다 양방향 연관관계 테스트

@Test
@Transactional
void relationshipTest2() {
    Product product1 = savedProduct("펜", 1000, 100);
    Product product2 = savedProduct("가방", 10000, 1000);
    Product product3 = savedProduct("가위", 5000, 500);

    Producer producer1 = savedProducer("feature");
    Producer producer2 = savedProducer("wiki-books");

    producer1.addProduct(product1);
    producer1.addProduct(product2);
    producer2.addProduct(product2);
    producer2.addProduct(product3);

    product1.addProducer(producer1);
    product2.addProducer(producer1);
    product2.addProducer(producer2);
    product3.addProducer(producer2);

    producerRepository.saveAll(Lists.newArrayList(producer1, producer2));
    productRepository.saveAll(Lists.newArrayList(product1, product2, product3));

    System.out.println("products : " + productRepository.findById(1L)
            .get().getProducers());

    System.out.println("products : " + producerRepository.findById(1L)
            .get().getProductList());
}
  • 다대다 연관관계를 설정하면 중간 테이블을 통해 연관된 엔티티의 값을 가져올 수 있다.
  • 다대다 연관관계에서는 중간 테이블이 생성되기 때문에 예기치 못한 쿼리가 생길 수 있다.
  • 관리하기 힘든 포인트가 발생하기도 하는데, 이러한 한계를 극복하기 위해 중간 테이블을 생성하는 대신에 일대다 또는 다대일로 연관관계를 맺을 수 있는 중간 엔티티로 승격시켜 JPA에서 관리할 수 있게 생성하는 것이 좋다.
반응형

댓글