스프링 프레임워크에서는 다대다(Many-to-Many)관계를 사용하는 것은 데이터베이스 모델링의 중요한 부분 중 하나이다. 다대다 관계는 두 엔티티 간의 복잡한 상호작용을 관리할 때 유용하지만, 이를 올바르게 구현하고 이해하는 것이 중요하다. 다음은 스프링의 JPA에서 다대다 관계를 보다 상세하게 설명하겠다.
다대다 관계의 기본 개념
다대다 관계는 두 개의 엔티티가 서로 다수의 인스턴스를 참조할 수 있는 경우를 말한다.
EX) 학생(Student)와 강좌(Course) 간의 관계에서는 한 학생이 여러 강좌를 수강할 수 있고, 한 강좌에는 여러 학생이 등록 하루 수 있다.
데이터베이스에 모델링을 하기 위해 필요한 구조
Student 테이블
Course 테이블
다대다 관계 구현방법
기본적인 다대대 관계에서는 JPA의 @ManyToMany 어노테이션을 사용하여 중간 테이블이 자동을 생성되며 다대다 관계를 정의가 가능하다.
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
// getters and setters
}
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
// getters and setters
}
코드 설명
- @ManyToMany: Student와 Course 간의 다대다 관계를 나타냅니다.
- @JoinTable: 다대다 관계를 구현하기 위해 중간 테이블(이 경우 student_course)을 정의합니다.
- joinColumns는 이 테이블에서 현재 엔티티(Student)를 참조하는 열을 나타냅니다.
- inverseJoinColumns는 상대 엔티티(Course)를 참조하는 열을 나타냅니다.
@ManyToMany 어노테이션을 사용하여 다대다 관계를 설정할시 생기는 장단점
장점
간단한 구현:
- @ManyToMany 어노테이션을 사용하면 두 엔티티 간의 다대다 관계를 쉽게 설정할 수 있습니다. 코드가 간결하며, 중간 테이블(조인 테이블)도 자동으로 생성되기 때문에 별도의 중간 엔티티를 만들 필요가 없다.
자동 매핑:
- JPA는 @ManyToMany 어노테이션을 통해 두 엔티티 간의 조인 테이블을 자동으로 생성하고 관리한다. 개발자는 이 과정을 신경 쓰지 않아도 되며, 복잡한 SQL 쿼리를 직접 작성할 필요가 없다.
직관적인 데이터 모델링:
- 다대다 관계를 표현해야 하는 경우, @ManyToMany를 사용하면 데이터 모델이 실제 비즈니스 로직과 더 직관적으로 일치한다. 예를 들어, 학생이 여러 강의를 듣고, 하나의 강의를 여러 학생이 수강할 수 있는 시나리오를 쉽게 표현할 수 있다.
양방향 관계 지원:
- @ManyToMany 어노테이션은 양방향 관계를 쉽게 설정할 수 있다. 즉, 두 엔티티 모두 서로를 참조할 수 있어 데이터 접근이 유연해진다.
단점
성능 문제:
- @ManyToMany 관계를 사용할 때 조인 테이블이 자동으로 생성되는데, 대규모 데이터를 처리할 경우 쿼리 성능이 저하될 수 있습니다. 특히, 조인 테이블이 커지면 데이터를 조회하거나 수정하는 작업이 느려질 수 있습니다.
- 캐스케이드(cascade) 연산을 사용할 때 조인 테이블에 대한 불필요한 업데이트가 발생할 수 있습니다.
중간 테이블에 추가 속성 불가:
- @ManyToMany 어노테이션은 기본적으로 두 엔티티 간의 관계만을 다루기 때문에, 조인 테이블에 추가적인 정보를 저장할 수 없습니다. 예를 들어, 학생이 강의를 수강한 날짜와 같은 정보를 저장해야 한다면, @ManyToMany는 적합하지 않습니다. 이 경우에는 중간 엔티티를 만들어야 합니다.
복잡한 비즈니스 로직 처리 어려움:
- 복잡한 비즈니스 로직을 처리할 때, @ManyToMany는 유연성이 부족할 수 있습니다. 예를 들어, 특정 조건에 따라 다대다 관계를 동적으로 변경하거나 관리해야 하는 경우, 기본 @ManyToMany 매핑만으로는 충분하지 않을 수 있습니다.
관계 관리의 어려움:
- 양방향 @ManyToMany 관계를 사용할 때, 두 엔티티 간의 동기화 문제가 발생할 수 있습니다. 즉, 한쪽 엔티티에서 관계를 변경했을 때 다른 쪽 엔티티에서도 이를 반영해야 하는데, 이를 관리하기 위해 추가적인 코드가 필요할 수 있습니다.
중간 엔티티를 사용하는 다대다 관계
그라고 두 번째로 중간 엔티티를 사용해 다대다 관계를 설정할 수 있다.
EX) 상품(Product)와 폴더(Folder)의 1:N, N:1을 사용해 다대다 관계를 설정하고 그 사이에 중간 엔티티를 사용하여 다대다 관계를 설정하는 방식이 있다.
패키지 entity의 Product.class
package com.sparta.myselectshop.entity;
import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Getter
@Setter
@Table(name = "product") // 매핑할 테이블의 이름을 지정
@NoArgsConstructor
public class Product extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String image;
@Column(nullable = false)
private String link;
@Column(nullable = false)
private int lprice;
@Column(nullable = false)
private int myprice;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@OneToMany(mappedBy = "product")
private List<ProductFolder> productFolderList =new ArrayList<>();
public Product(ProductRequestDto requestDto, User user) {
this.title = requestDto.getTitle();
this.image = requestDto.getImage();
this.link = requestDto.getLink();
this.lprice = requestDto.getLprice();
this.user = user;
}
public void update(ProductMypriceRequestDto requestDto) {
this.myprice = requestDto.getMyprice();
}
}
패키지 entity의 Folder.class
package com.sparta.myselectshop.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "folder")
public class Folder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
public Folder(String name, User user) {
this.name = name;
this.user = user;
}
}
그리고 중간 엔티티 클래스인 ProductFolder
package com.sparta.myselectshop.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "product_folder")
public class ProductFolder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
private Product product;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "folder_id", nullable = false)
private Folder folder;
public ProductFolder(Product product, Folder folder) {
this.product = product;
this.folder = folder;
}
}
다대다 관계 구현
이 코드에서 Product와 Folder는 본질적으로 다대다 관계를 가지고 있다. 하나의 Product는 여러 Folder에 속할 수 있고, 하나의 Folder도 여러 Product를 가질 수 있다. 이를 JPA에서 다대다 관계로 표현하려면 중간 테이블이 필요하다. 이 역할을 ProductFolder 엔티티가 수행하였다.
다대다 관계를 두 개의 일대다 관계로 분해
- Product ↔ ProductFolder (일대다 관계): 하나의 Product는 여러 ProductFolder 엔티티와 연결.
- Folder ↔ ProductFolder (일대다 관계): 하나의 Folder도 여러 ProductFolder 엔티티와 연결.
이렇게 하면 다대다 관계를 효과적으로 두 개의 일대다 관계로 분해할 수 있다.
3. 왜 중간 테이블을 사용하는가?
@ManyToMany 어노테이션을 직접 사용하지 않고 중간 엔티티(ProductFolder)를 사용하는 이유
1. 추가 속성 저장 가능
@ManyToMany 어노테이션을 사용하면 두 엔티티 간의 다대다 관계를 간단하게 설정할 수 있지만, 중간 테이블에는 단순히 두 엔티티 간의 매핑 정보만 저장된다. 만약 중간 테이블에 추가적인 속성(예: 관계가 생성된 날짜, 상태, 순서 등)을 저장해야 하는 경우 @ManyToMany 어노테이션만으로는 해결할 수 없다.
중간 엔티티를 사용하면, 이러한 추가적인 속성을 중간 엔티티에 포함시킬 수 있다. 예를 들어, Product와 Folder 간의 관계를 나타내는 중간 엔티티 ProductFolder에 addedDate 같은 필드를 추가하면, 제품이 폴더에 추가된 날짜를 기록할 수 있게 된다.
2. 관계의 세밀한 관리
@ManyToMany를 사용할 경우, 두 엔티티 간의 관계는 자동으로 관리되지만, 복잡한 비즈니스 로직을 처리할 때는 한계가 있을 수 있다. 예를 들어, 특정 조건에 따라 다대다 관계를 추가하거나 제거해야 하는 경우, 또는 관계를 동적으로 관리해야 하는 경우가 있다.
중간 엔티티를 사용하면, 이러한 관계를 더 세밀하게 제어가 가능하다. 중간 엔티티를 통해 관계를 추가하거나 제거하는 로직을 명확하게 작성할 수 있으며, 트랜잭션 관리나 복잡한 쿼리 작성 시에도 더 유연하게 대처가 가능하다다.
3. 성능 최적화
@ManyToMany 어노테이션을 사용할 때는 JPA가 자동으로 생성하는 중간 테이블을 통해 조인이 이루어지기 때문에, 성능이 문제가 될 수 있다. 특히, 대규모 데이터를 처리하거나 복잡한 쿼리를 실행할 때는 성능 저하가 발생할 수 있다.
중간 엔티티를 사용하면, 중간 테이블의 구조를 직접 제어할 수 있으므로 성능 최적화를 더 세밀하게 할 수 있다. 예를 들어, 중간 엔티티에 적절한 인덱스를 설정하거나, 필요에 따라 직접 최적화된 쿼리를 작성이 가능하다.
4. 관계의 의미를 명확하게 표현
@ManyToMany 어노테이션을 사용하는 경우, 두 엔티티 간의 관계가 단순히 "다대다"로만 표현되기 때문에 비즈니스 로직에서 그 관계의 의미를 명확하게 이해하기 어려울 수 있다.
중간 엔티티를 사용하면, 관계를 더욱 명확하게 표현할 수 있다. 중간 엔티티의 이름과 속성들이 관계의 의미를 잘 나타낼 수 있기 때문에, 코드의 가독성과 유지보수성이 향상된다.
5. 양방향 관계의 복잡성 관리
@ManyToMany를 양방향으로 설정할 때는 두 엔티티 간의 동기화가 필요하다. 이 과정에서 불필요한 복잡성이 발생할 수 있으며, 관계 관리가 어려워질 수 있다.
중간 엔티티를 사용하면, 이러한 양방향 관계의 복잡성을 완화할 수 있다. 중간 엔티티가 각 엔티티와의 관계를 직접 관리하기 때문에, 양방향 관계의 동기화나 관리가 더 명확하고 직관적으로 처리된다.
@ManyToMany 어노테이션은 간단한 다대다 관계를 구현할 때 유용하지만, 관계에 대한 추가적인 정보가 필요하거나, 성능 최적화가 필요하거나, 복잡한 비즈니스 로직이 있는 경우에는 중간 엔티티를 사용하는 것이 훨씬 유리하다. 중간 엔티티를 통해 관계를 세밀하게 제어하고 확장할 수 있으며, 코드의 유연성과 유지보수성을 높일 수 있다.
(@ManyToMany쓰는것 보단 중간 엔티티 만들어서 다대다 관계 설정하는 걸 추천!)
'Spring' 카테고리의 다른 글
커스텀 어노테이션 (2) | 2024.09.05 |
---|---|
Edit Configuration을 통해 SQL DB로그인하기 (0) | 2024.09.03 |
1:N(일대다), N:1(다대일) (1) | 2024.08.28 |
RESTful API (0) | 2024.08.28 |
Entity 연관 관계 1 대 1 관계 (0) | 2024.08.23 |