본문 바로가기
Programming/Spring

[Spring] 테이블 매핑, 영속성 전이 (cascade)

by jenlve 2024. 3. 13.

자주 쓰이는 다대일 매핑에 대해 알아보고 영속성 전이에 대해서도 알아보기

가장 일반적인 다대일 관계인 상품 테이블과 공급업체 테이블을 가지고 설명해보도록 하겠다. 

🏷️ 다대일 단방향 매핑

위의 예시 테이블처럼, 여러 개(다)의 상품이 한 개(일)의 공급업체로 매핑되는 관계를 의미한다. 

다대일 단방향 매핑을 하기 위해서는 한쪽에(매핑의 주인, 외래키를 갖는 엔티티), 아래와 같은 코드가 추가 되어야 한다. 

public class Product {
    @ManyToOne
    @JoinColumn(name = "provider_id")
    @ToString.Exclude
    private Provider provider;
}

 

위의 코드에 대해 설명하자면, Product(상품) 엔티티에서 Provider(공급업체)에 대해 다대일(ManyToOne) 매핑을 하는 코드이다. 일반적으로 외래키를 갖는 쪽이 주인의 역할을 수행하기 때문에 provider_id라는 이름의 provider 기본키를 외래키로 갖는 Product가 Provider(공급업체)의 주인이 된다. 

아래의 테이블 생성 결과를 통해 Product가 Provider의 주인임을 알 수 있다. 

 

 

🏷️ 다대일 양방향 매핑

다대일 양방향 매핑은 앞선 다대일 단방향 매핑에, 공급업체를 통해 상품을 조회하는 일대다 매핑을 추가하면된다. 

따라서 Provider 클래스에 아래와 같은 코드를 넣어주면 된다.

public class Provider{
    @OneToMany(mappedBy = "provider", fetch = FetchType.EAGER)
    @To.String.Exclude
    private List<Product> productList = new ArrayList<>();
}

 

일대 다!의 연관관계의 경우, 여러 개의 상품 엔티티가 포함될 수 있기 때문에 컬렉션(Collection, List, Map) 형식으로 필드를 생성한다. 또한 "mappedBy" 설정을 통해 provider쪽이 아닌, 상대쪽(Product)에 외래키를 위임한다는 것을 설정해줄 수 있다. "mappedBy"는 양방향 관계를 정의할 때 사용하는 것으로, 대개 일대다에서 다쪽이 주로 외래키를 관리하게 되는데, 이러한 관계를 설정해줄 수 있다. 

위의 코드를 통해, mappedBy 속성을 사용하여 관계의 주인이 Product 쪽에 있다는 것을 알 수 있다. 

 


 

🤔 프로젝트 진행에 있어 사용자 테이블과 리뷰 테이블의 매핑 관계를 정의하는 과정이 필요했다.

최종 구현은 아니지만 우선 한 명의 사용자가 (여러 매장에 대해) 여러개의 리뷰를 작성할 수 있도록 관계를 설정하고 싶었다. 그래서 사용자에 @OneToMany, 리뷰에 @ManyToOne 매핑을 해주었는데,

여기서 사용자 삭제 시 아래와 같은 에러가 뜨게 되어,  사용자 정보가 없을 경우 리뷰 테이블을 어떻게 할지 고민하게 되었다.

 

우선 Review 테이블에서는, 사용자의 탈퇴와는 상관없이 리뷰 기록은 다른이들에게 도움이 되는 정보이기 때문에 작성자를 '알수없음'으로 표시하더라도 리뷰의 내용은 남기고 싶었다. 

그래서 Review 테이블에 User를 매핑한 곳에다 user값으로 null을 가질 수도 있다는 옵션을 추가해주었다.

@ManyToOne(optional = true)

 

 

다음으로 @ManyToOne 관계 매핑에 사용될 정보인 @JoinColumn 어노테이션을 추가해줘야 하는데,

name에은 우선 Review 테이블에 어떤 속성명으로 표현될지 이름을 작성해주고

앞서 이 User 값이 null이 될 수도 있음을 허용하는 nullable 을 true로 설정해주고

위의 에러처럼 외래키 제약으로 인한 에러가 뜨지 않도록 foriegnKey에서 NO_CONSTRAINT를 설정해주었다.

@JoinColumn(name = "user", nullable = true,
        foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT))

 

 

사용자 테이블에서는 또한 리뷰 테이블과의 일대다 관계를 매핑하기 위해 @OneToMany 어노테이션을 붙여주고 아래와 같은 설정들을 추가해줬다. 

 

mappedBy 를 써서 User 테이블이 매핑되어 있어, 현재 관계의 주인은 Review 임을 명시하고, Review 테이블이 User의 기본키를 갖고 있음을 의미한다. 

Cascade ALL을 사용해서 부모 엔티티의 모든 변경을 자식 엔티티도 적용할 수 있도록 설정하였다.

 

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)

 


🏷️ 식별, 비식별 관계

erd cloud를 이용해서 테이블을 작성하고 관계를 매핑하는데, 식별, 비식별 관계를 설정하도록 되어 있는 것을 알게 되었다. 위의 두 관계는 어떤 의미이고 어떤 상황에서 쓰이는지 알아보자. 

 

❓식별 관계란

식별 관계는 자식 테이블의 기본 키가 부모 테이블의 기본 키를 포함하는 관계이다. 부모 테이블의 기본 키가 자식 테이블의 기본 키의 일부로 사용되므로, 자식 테이블의 레코드는 부모 테이블의 레코드에 의해 식별된다. 

▪️ 필요한 상황 : 자식 테이블이 꼭 부모 테이블의 기본키로 식별이 가능한 경우

 

❓비식별 관계란

비식별 관계는 자식 테이블의 기본 키가 부모 테이블의 기본 키를 포함하지 않는 관계이다. 자식 테이블의 기본키는 부모 테이블과 관련이 없으며, 부모 테이블의 기본 키를 참조하는 외래 키만을 포함한다. 

▪️ 필요한 상황 : 부모가 자식 테이블이 오로지 부모 테이블의 기본키로만 식별이 가능한 경우

 

식별 vs  비식별

예를 들어, 주문(order)과 주문 상세(order_detail) 테이블이 있을 때, 주문 상세 테이블의 기본 키가 주문 테이블의 기본 키와 함께 주문을 식별한다. 그러나 주문(order)과 상품(product) 테이블이 있을 때, 주문 테이블은 상품을 식별하는 데 사용되지만, 상품 테이블은 주문과 관련이 없습니다. 따라서 이럴때는 비식별 관계를 써야 한다. 

 

비식별 관계를 자주 쓰는 이유

  1. 데이터 무결성 문제:
    • 식별 관계로 설정하면 자식 테이블의 기본 키가 부모 테이블의 기본 키를 포함하므로, 부모 테이블에서 특정 레코드를 삭제하면 해당 레코드를 참조하는 모든 자식 레코드도 함께 삭제된다. 이는 의도치 않은 데이터 손실을 초래할 수 있다.
  2. 부모 테이블의 변경 문제:
    • 부모 테이블의 기본 키를 변경해야 하는 경우 문제가 발생할 수 있다. 부모 테이블의 기본 키를 변경하면 이를 참조하는 모든 자식 레코드도 변경되어야 한다. 이는 데이터 일관성과 무결성에 영향을 줄 수 있다.
  3. 데이터 모델의 유연성 감소:
    • 식별 관계를 설정하면 부모와 자식 테이블 간의 강력한 의존성이 생기므로, 데이터 모델의 유연성이 감소할 수 있다. 즉, 향후 데이터 모델을 수정하거나 확장하기 어려울 수 있다.

🏷️ 영속성 전이 (cascade)

특정 엔티티의 영속성 상태를 변경하고자 할 때, 그 엔티티와 관련된 다른 엔티티의 영속성에도 영향을 미쳐, 영속성 상태를 변경시키는 것

 

🔖영속성

▪️ 데이터를 생성(Create),  변경(Update), 삭제(Delete)하는 것과 같은 변경 작업이 데이터베이스에 적용되어, 영구적으로 유지되는 특성

▪️ 애플리케이션의 생명 주기와 상관없이, 데이터가 데이터베이스에 안전하게 보관되는 특성

 

 

영속성 전이 타입의 종류

 

각 타입은 엔티티의 생명주기와 연관이 있다. 

 

또한 cascade() 요소의 리턴 타입이 배열 형식인 것을 통해서, 우리가 사용하고자 하는 cascade 타입을 골라 각 상황에 적용할 수 있다. 

 

🔖 고아 객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티로 JPA에서는 orphanRemoval 속성을 통해 활성화할 수 있으며, 고아 객체를 자동으로 삭제하는 기능을 제공해준다. 따라서 예상치 못한 데이터 손실이 발생할 수 있음으로 주의해서 사용해야 한다. 

고아객체의 예를 들자면, 사용자 엔티티가 여러개의 주소 엔티티를 가지고 있고, 특정 주소가 더 이상 사용자에게 속하지 않게 되었을 때 해당 주소를 엔티티를 자동으로 삭제하고 싶은 경우, 주소 엔티티를 사용자 엔티티와 매핑 시, 사용자 엔티티 내 주소 객체 위에 orphanRemoval로 고아 객체로 설정해서 사용할 수 있다. 

 

참고 : 스프링 부트 핵심 가이드, 장정주 저