반응형

다대다(Many-to-Many)를 접하는 사람이라면 한 번쯤 접해보셨을 문제가 있으실 겁니다. 하나의 엔티티에 `@Id`를 2개 이상 어노테이션 적용을 못하는 문제를요.. 저 또한 프로젝트 진행 중에 다대다 클래스에서 Id에 대한 문제가 발생한 경우가 있어 이슈 트러블슈팅을 한 경험을 공유하고자 포스팅을 남깁니다.
다대다(Many to Many)
다대다는 이 글을 읽고 읽는 여러분들도 알고계시다시피 테이블과 테이블 간에 여러 개의 관계가 서로 맺어질 때 사용하는 관계입니다. (예: 상품-쿠폰, 학생-수업 등) 이 다대다 관계를 맺기 위해 스프링 프레임워크에서는 `@Entity`라는 어노테이션을 사용하여 테이블을 생성하고, 그 테이블 안에서 `@ManyToMany` 어노테이션을 사용하여 다른 테이블과 외래키를 설정하여 관계를 맺게 됩니다. (또는 `@OneToMany`를 응용해서 가운데에 관계 테이블을 생성하기도 하죠.)

(위 그림처럼 상품 엔티티와 쿠폰 엔티티는 상품_쿠폰 엔티티라는 관계 엔티티로 다대다 관계가 맺어집니다.)
위와 같은 다대다 상황의 스프링 엔티티를 다음과 같이 작성할 수 있습니다.
// Product.java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String productName;
@Column(nullable = false)
private Long productPrice;
private Product(String productName, Long productPrice) {
this.productName = productName;
this.productPrice = productPrice;
}
public static Product create(String productName, Long productPrice) {
return new Product(productName, productPrice);
}
}
// Coupon.java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Coupon {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String couponName;
@Column(nullable = false)
private String couponContent;
private Coupon(String couponName, String couponContent) {
this.couponName = couponName;
this.couponContent = couponContent;
}
public static Coupon create(String couponName, String couponContent) {
return new Coupon(couponName, couponContent);
}
}
// ProductCoupon.java
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ProductCoupon implements Persistable<ProductCouponId> {
@EmbeddedId
private ProductCouponId productCouponId;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinColumn(nullable = false)
@MapsId("productId")
private Product product;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinColumn(nullable = false)
@MapsId("couponId")
private Coupon coupon;
@CreatedDate
private LocalDateTime createdAt;
private ProductCoupon(Product product, Coupon coupon) {
this.product = product;
this.coupon = coupon;
}
@Override
public ProductCouponId getId() {
return this.productCouponId;
}
@Override
public boolean isNew() {
return createdAt == null;
}
public static ProductCoupon create(Product product, Coupon coupon) {
return new ProductCoupon(product, coupon);
}
}
(설명 작성중)
다대다 복합키 ID 클래스
위의 `ProductCoupon.java` 클래스를 보면, Id를 `@EmbeddedId`로 선언해놓은 것을 보실 수 있습니다. 아래는 EmbeddedId로 선언된 복합키 ID 클래스입니다.
// ProductCouponId.java
@Getter
@Embeddable
@EqualsAndHashCode
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ProductCouponId implements Serializable {
@Column(nullable = false)
private Long productId;
@Column(nullable = false)
private Long couponId;
private ProductCouponId(Long productId, Long couponId) {
this.productId = productId;
this.couponId = couponId;
}
public static ProductCouponId create(Long productId, Long couponId) {
return new ProductCouponId(productId, couponId);
}
}
다대다 복합키에서 지켜야할 사항들이 몇가지 있습니다.
- `@Embaddable`로 작성할 것
- `Serializable`을 상속받을 것
- `equals` 메서드와 `hashCode` 메서드를 작성할 것
- 복합키 받는 클래스(ProductCoupon)에서는 `@MapsId`로 id 필드들을 맵핑해 줄 것
'Develop > Spring' 카테고리의 다른 글
| (2) restdocs + openapi3 + swagger-ui 설정을 해보자 - 실전편 (0) | 2024.01.18 |
|---|---|
| (1) restdocs + openapi3 + swagger-ui 설정을 해보자 - 설명편 (0) | 2024.01.18 |
| docker-compose 기반 mysql, redis 데이터베이스 세팅 (1) | 2023.12.18 |
| Spring REST API 사용 시 Web + Security CORS 설정 (0) | 2023.12.04 |
| 스프링 서버를 실행할 때 자동으로 웹 브라우저 열기 (0) | 2023.08.07 |