상세 컨텐츠

본문 제목

[스프링 1팀] 9장.연관관계 매핑

24-25/Spring 1

by oze 2025. 1. 10. 10:00

본문

728x90

 

9장. 연관관계 매핑

연관관계 매핑 종류와 방향

✅ 종류

  • 일대일(1:1)
  • 일대다(1:N)
  • 다대일(N:1)
  • 다대다(N:N)

✅ 재고관리시스템 예시

  • 상품 엔티티에는 공급업체의 정보 엔티티가 매핑됨
  • 공급 업체 입장: 한 가게에 납품하는 상품 여러 개일 수 있으므로 일대다 관계
  • 상품 입장: 하나의 공급업체에 속하게 되므로 다대일 관계
어떤 엔티티를 중심으로 연관 엔티티를 보느냐에 따라 연관관계의 상태가 달라짐

상품과 공급업체 테이블 관계

  • JPA를 사용하는 객체지향 모델링에서는 엔티티 간 참조 방향 설정 가능
  • 데이터베이스와 관계를 일치시키기 위해 양방향으로 설정해도 무관하지만 비즈니스 로직 관점에서는 단방향 관계만 설정해도 해결되는 경우가 다수
    • 단방향: 두 엔티티의 관계에서 한쪽의 엔티티만 참조하는 형식
    • 양방향: 두 엔티티의 관계에서 각 엔티티가 서로의 엔티티를 참조하는 형식
  • 연관관계가 설정되면 한 테이블에서 다른 테이블의 기본값을 외래키로 가짐
  • 주인(Owner) 개념 사용
  • 일반적으로 외래키를 가진 테이블이 주인
  • 주인은 외래키를 사용할 수 있으나 상대 엔티티는 읽는 작업만 수행 가능 

 


 

프로젝트 생성

프로젝트 설정

  • 8장 코드 가져와 사용
  •  pom.xml: queryDSL의 의존성과 플러그인 설정 추가

 


 

일대일 매핑

📌 Product 엔티티를 대상으로 일대일 매핑될 상품 정보 테이블 생성

 

상품과 상품정보 테이블의 일대일 관계

 

✅ 일대일 단방향 매핑

 

📌 entity 패키지 안에 상품정보 엔티티 작성

@Entity
@Table(name = "product_detail")
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class ProductDetail extends BaseEntity {

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

    private String description;

    @OneToOne
    @JoinColumn(name = "product_number")
    private Product product;

}
  • @OneToOne: 다른 엔티티 객체를 필드로 정의했을 때 일대일 연관관계 매핑하기 위해 사용
  • @JoinColumn: 매핑할 외래키 설정. 기본값 설정돼 있어 자동으로 이름 매핑하지만 name 속성을 사용해 원하는 칼럼명을 지정하는 것을 추천
    • name: 매핑할 외래키의 이름을 설정
    • referencedColumnName: 외래키가 참조할 상대 테이블의 칼럼명을 지정
    • foreignKey: 외래키를 생성하면서 지정할 제약조건을 설정

 

📌 product_detail 테이블 생성

 : hibernate.ddl-auto의 값을 create로 설정 후 애플리케이션을 실행하면 하이버네이트에서 자동으로 테이블을 생성

자동 생성된 product_detail 테이블

 

📌 ProductDetailRepository 인터페이스

 : 생성된 상품정보 엔티티 객체들을 사용하기 위해 리포지토리 인터페이스 생성

public interface ProductDetailRepository extends JpaRepository<ProductDetail, Long> {

}

 

📌 ProductRepository와 ProductDetailRepository에 대한 테스트 코드

 : 연관관계를 활용한 데이터 생성 및 조회 기능을 테스트 코드로 간략하게 작성

@SpringBootTest
class ProductDetailRepositoryTest {

    @Autowired
    ProductDetailRepository productDetailRepository;

    @Autowired
    ProductRepository productRepository;

    @Test
    public void saveAndReadTest1() {
        Product product = new Product();
        product.setName("스프링 부트 JPA");
        product.setPrice(5000);
        product.setStock(500);

        productRepository.save(product);

        ProductDetail productDetail = new ProductDetail();
        productDetail.setProduct(product);
        productDetail.setDescription("스프링 부트와 JPA를 함께 볼 수 있는 책");

        productDetailRepository.save(productDetail);

        // 생성한 데이터 조회
        System.out.println("savedProduct : " + productDetailRepository.findById(
                productDetail.getId()).get().getProduct());

        System.out.println("savedProductDetail : " + productDetailRepository.findById(
                productDetail.getId()).get());
    }
}
  • 상품과 상품정보에 매핑된 리포지토리에 대해 의존성 주입받음
  • 조회할 엔티티 객체를 저장
  • 생성한 데이터 조회: ProductDetail 객체에서 Product 객체를 일대일 단방향 연관관계를 설정했기 때문에 ProductDetailRepository에서 ProductDetail 객체를 조회한 후 연관 매핑된 Product 객체 조회 가능
  • 생성한 데이터 조회 부분 실행 시 조회하는 쿼리에서 ProductDetail 객체와 Product 객체가 함께 조회됨
  • 엔티티를 조회할 때 연관된 엔티티도 함께 조회하는 것 = 즉시 로딩
  • 또한 쿼리를 확인하면 left outer join 수행됨. @OneToOne 어노테이션 때문

 

📌 @OneToOne 어노테이션 인터페이스

public @interface OneToOne {
	Class targetEntity() default void.class;
    
    CascadeType[] cascade() default {};
    
    FetchType fetch() default EAGER;
    
    boolean optional() default true;
    
    String mappedBy() default "";
    
    boolean orphanRemoval() default false;
    
}
  • fetch() 요소: @OneToOne 어노테이션은 기본 fetch 전략으로 EAGER(즉시 로딩) 전략 채택
  • optional() 요소: 기본값이 true(매핑되는 값이 nullable, 반드시 값이 있어야 한다면 아래와 같이 설정)

 

📌 Product 객체가 반드시 있어야 하는 ProductDetail 엔티티 클래스

//..생략..
public class ProductDetail extends BaseEntity {
    //..생략..
    @OneToOne(optional=false) // null값 허용 x
    @JoinColumn(name = "product_number")
    private Product product;
}

 

 

✅ 일대일 양방향 매핑

 : 양방향은 사실 양쪽에서 단방향으로 서로를 매핑하는 것을 의미

 

📌 일대일 양방향 매핑을 위한 Product 엔티티

@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Table(name = "product")
public class Product extends BaseEntity {

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

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Integer price;

    @Column(nullable = false)
    private Integer stock;

    @OneToOne
    private ProductDetail productDetail;

}

생성된 product 테이블

  • 앞서 작성한 테스트를 실행했을 때의 쿼리를 확인하면 양쪽에서 외래키를 가지고 left outer join을 두 번 수행
  • 이는 효율성이 떨어짐. 실제 데이터베이스에서도 테이블 간 연관관계를 맺으면 한쪽 테이블이 외래키를 갖는 구조(=주인 개념)
  • JPA에서도 한쪽 테이블에서만 외래키를 바꿀 수 있도록 하는 것이 좋음
  • 양방향을 매핑하되 한쪽에만 외래키를 주기 위해 사용하는 속성 = mappedBy(: 어떤 객체가 주인인지 표시)

 

📌 mappedBy 속성을 추가한 Product 엔티티 클래스

//..생략..
public class Product extends BaseEntity {
	//..생략..
    @OneToOne(mappedBy = "product") 
	//..생략..
}
  • mappedBy에 들어가는 값은 연관관계를 갖고 있는 상대 엔티티에 있는 연관관계 필드의 이름
  • 즉, 이 설정을 마치면 ProductDetail 엔티티가 Product 엔티티의 주인이 됨

mappedBy 속성을 사용한 product 테이블

  • 애플리케이션을 실행하고 HeidiSQL에서 db의 테이블을 보면 외래키가 사라짐
  • 다시 테스트 코드를 실행하면 toString을 실행하는 시점에서 StackOverflowError 발생
  • 양방향으로 연관관계가 설정되면 ToString을 사용할 때 순환참조가 발생하기 때문
  • -> 대체로 단방향 연관관계, 양방향 필요할 경우 순환참조 제거를 위해 exclude를 사용해 ToString에서 제외 설정

 

📌 @ToString 어노테이션의 대상 제외

public class Product extends BaseEntity {
	//..생략..
    @OneToOne(mappedBy = "product")
    @ToString.Exclude 
    private ProductDetail productDetail;
}

 

 


 

다대일, 일대다 매핑

상품과 공급업체 테이블 관계

 

✅ 다대일 단방향 매핑

 

📌 공급업체 언티티 클래스 생성

@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Table(name = "provider")
public class Provider extends BaseEntity {

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

    private String name;

}
  • BaseEntity를 통해 생성일자와 변경일자를 상속받음

 

📌 상품 엔티티와 공급업체 언티티의 다대일 연관관계 설정

public class Product extends BaseEntity {
	//..생략..
    @ManyToOne
    @JoinColumn(name = "provider_id")
    @ToString.Exclude
    private Provider provider;
}
  • 다대일 연관관계 설정
  • 일반적으로 외래키를 갖는 쪽이 주인의 역할을 수행 -> 상품 엔티티가 공급업체 엔티티의 주인
  • 위와 같이 설정 후 애플리케이션 가동 -> Product와 Provider 테이블 생성

공급업체와 연관 설정된 상품 테이블
자동 생성된 공급업체 테이블

 

📌 ProviderRepository 인터페이스

public interface ProviderRepository extends JpaRepository<Provider, Long> {

}

 

📌 Product, Provider 엔티티 연관관계 테스트

 : Product, Provider 엔티티에서 주인은 Product 엔티티이므로 ProductRepository를 활용해 테스트

@SpringBootTest
class ProviderRepositoryTest {

    @Autowired
    ProductRepository productRepository;

    @Autowired
    ProviderRepository providerRepository;
    
    @Test
    void relationshipTest1() {
        // 테스트 데이터 생성
        Provider provider = new Provider();
        provider.setName("ㅇㅇ물산");

        providerRepository.save(provider);

        Product product = new Product();
        product.setName("가위");
        product.setPrice(5000);
        product.setStock(500);
        product.setProvider(provider);

        productRepository.save(product);

        // 테스트
        System.out.println(
                "product : " + productRepository.findById(1L).orElseThrow(RuntimeException::new));

        System.out.println("provider : " + productRepository.findById(1L).orElseThrow(RuntimeException::new).getProvider());
    }
}
  • 두 리포지토리에 대해 의존성을 주입받아 테스트 데이터 생성
  • provider 객체를 product에 추가해서 데이터베이스에 저장 
  • 테스트: Product 엔티티에서 단방향으로 Provider 엔티티 연관관계를 맺고 있기 때문에 ProductRepository만으로도 Provider 객체도 조회 가능

 

✅ 다대일 양방향 매핑

 

📌 공급업체 엔티티와 상품 엔티티의 일대다 연관관계 설정

 : 기존 상품 엔티티와 공급업체 엔티티 사이의 다대일 단방향 연관관계와 반대인 공급업체를 통해 등록된 상품을 조회하기 위한 일대다 연관관계 설정

public class Provider extends BaseEntity {
	//..생략..
    @OneToMany(mappedBy = "provider", fetch = FetchType.EAGER)
    @ToString.Exclude
    private List<Product> productList = new ArrayList<>();
}
  • 일대다 연관관계의 경우 여러 상품 엔티티가 포함될 수 있어 컬렉션(Collection, List, Map) 형식으로 생성
  • @OneToMany가 붙은 쪽에서 @JoinColumn 어노테이션을 사용하면 상대 엔티티에 외래키가 설정됨
  • 롬복의 ToString에 의해 순환참조가 발생할 수 있어 제외 처리 필요
  • @OneToMany의 기본 fetch 전략이 Lazy이기 때문에 즉시 로딩(EAGER)으로 조정

 

  • Provider 엔티티 클래스를 수정해도 애플리케이션 가동 후 컬럼 변경 안됨
  • mappedBy로 설정된 필드는 칼럼에 적용되지 않기 때문
  • 즉, 양쪽에서 연관관계를 설정하고 있을 때 RDBMS의 형식처럼 사용하기 위해 mappedBy를 통해 한쪽으로 외래키 관리를 위임한 것

 

📌 Prvider 엔티티 기반의 테스트 코드

@SpringBootTest
class ProviderRepositoryTest {

    @Autowired
    ProductRepository productRepository;

    @Autowired
    ProviderRepository providerRepository;

    @Test
    void relationshipTest() {
        // 테스트 데이터 생성
        Provider provider = new Provider();
        provider.setName("ㅇㅇ상사");

        providerRepository.save(provider);

        Product product1 = new Product();
        product1.setName("펜");
        product1.setPrice(2000);
        product1.setStock(100);
        product1.setProvider(provider);

        Product product2 = new Product();
        product2.setName("가방");
        product2.setPrice(20000);
        product2.setStock(200);
        product2.setProvider(provider);

        Product product3 = new Product();
        product3.setName("노트");
        product3.setPrice(3000);
        product3.setStock(1000);
        product3.setProvider(provider);

        productRepository.save(product1);
        productRepository.save(product2);
        productRepository.save(product3);

        System.out.println("check 1");

        List<Product> products = providerRepository.findById(provider.getId()).get()
                .getProductList();

        for (Product product : products) {
            System.out.println(product);
        }

    }
}
  • Provider 엔티티 클래스는 Product 엔티티와의 연관관계에서 주인이 아니므로 외래키 관리 불가
  • -> Provider를 등록한 후 각 Product에 객체를 설정하는 작업을 통해 db에 저장

 

  • 만약 테스트 데이터를 생성하는 방식이 아니라 Provider 엔티티에 정의한 productList 필드에 아래와 같이 Product 엔티티를 추가하는 방식으로 db에 레코드를 저장하게 되면 Provider 엔티티 클래스는 연관관계의 주인이 아니기 때문에 해당 데이터는 데이터 베이스에 미반영 
provider.getProductList().add(product1); // 무시
provider.getProductList().add(product2); // 무시
provider.getProductList().add(product3); // 무시

 

  • ProviderRepository를 통해 연관관계가 매핑된 Product 리스트를 가져와 출력

 

✅ 일대다 단방향 매핑

일대다 관계인 상품 분류 테이블과 상품 테이블

 

 

📌 상품분류 엔티티 클래스 생성

@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString
@EqualsAndHashCode
@Table(name = "category")
public class Category {

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

    @Column(unique = true)
    private String code;

    private String name;

    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "category_id")
    private List<Product> products = new ArrayList<>();

}

생성된 상품 분류 테이블

 

외래키가 추가된 상품 테이블

  • 상품 분류 엔티티에서 @OneToMany와 @JoinColumn을 사용하면 상품 엔티티에서 별도의 설정 없이 일대다 단방향 연관관계가 매핑됨
  • @JoinColumn은 필수는 아니지만 사용하지 않으면 중간 테이블로 Join 테이블이 생성됨

 

  • 지금 같은 일대다 단방향 관계의 단점의 매핑의 주체가 아닌 반대 테이블에 외래키가 추가된다는 점
  • 다대일 구조와 다르게 외래키를 설정하기 위해 다른 테이블에 대한 update 쿼리를 발생시킴

 

📌 CategoryRepository 인터페이스

public interface CategoryRepository extends JpaRepository<Category, Long> {

}

 

📌 CategoryRepository를 활용한 테스트

@SpringBootTest
class CategoryRepositoryTest {

    @Autowired
    ProductRepository productRepository;

    @Autowired
    CategoryRepository categoryRepository;

    @Test
    void relationshipTest(){
        // 테스트 데이터 생성
        Product product = new Product();
        product.setName("펜");
        product.setPrice(2000);
        product.setStock(100);

        productRepository.save(product);

        Category category = new Category();
        category.setCode("S1");
        category.setName("도서");
        category.getProducts().add(product);

        categoryRepository.save(category);

        // 테스트 코드
        List<Product> products = categoryRepository.findById(1L).get().getProducts();

        for(Product foundProduct : products){
            System.out.println(foundProduct);
        }
    }
}
  • 테스트 데이터 생성을 위해 ProductRepository의 의존성을 주입받음
  • Product 객체를 Cateory에서 생성한 리스트 객체에 추가해서 연관관계를 설정

 

  • 테스트 데이터 생성 쿼리: 일대다 연관관계에서는 연관관계 설정을 위한 update 쿼리가 발생함
  • 이 같은 문제 해결을 위해선 일대다 양방향 연관관계를 사용하기보다는 다대일 연관관계 사용을 추천
  • 테스트 코드 실행 쿼리: category와 product의 조인이 발생해서 상품 데이터를 가져옴

 


 

다대다 매핑

 : 다대다 연관관계는 실무에서 거의 사용되지 않는 구성. 다대다 연관관계에서는 각 엔티티에서 서로를 리스트로 가지는 구조가 만들어짐. 이런 경우에는 교차 엔티티라고 부르는 중간 테이블을 생성해서 다대다 관계를 일대다 또는 다대일 관계로 해소함.

다대다 관계인 상품 테이블과 생산업체 테이블

 

✅ 다대다 단방향 매핑

 

📌 생산업체 엔티티 생성

@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Table(name = "producer")
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> products = new ArrayList<>();

    public void addProduct(Product product){
        products.add(product);
    }

}
  • 다대다 연관관계는 @ManyToMany 어노테이션으로 설정
  • 리스트로 필드를 가지는 객체에서는 외래키를 가지지 않기 때문에 별도의 @JoinColumn은 설정하지 않아도 됨

자동 생성된 생산업체 테이블
상품 테이블과 생산업체 테이블의 중간 테이블

  • 별도 설정 없이는 producer_products
  • 테이블의 이름을 관리하고 싶다면 @ManyToMany 아래에 @JoinTable(name = "이름")의 형식으로 정의
  • producer_products 테이블은 상품과 생산업체 테이블에서 id 값을 가져와 두 개의 외래키가 설정됨

 

📌 ProducerRepository 생성

public interface ProducerRepository extends JpaRepository<Producer, Long> {

}

 

📌 생산업체 연관관계 테스트

@SpringBootTest
class ProducerRepositoryTest {

    @Autowired
    ProducerRepository producerRepository;

    @Autowired
    ProductRepository productRepository;

    @Test
    @Transactional
    void relationshipTest() {
        Product product1 = saveProduct("동글펜", 500, 1000);
        Product product2 = saveProduct("네모 공책", 100, 2000);
        Product product3 = saveProduct("지우개", 152, 1234);

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

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

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

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

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

    private Product saveProduct(String name, Integer price, Integer stock) {
        Product product = new Product();
        product.setName(name);
        product.setPrice(price);
        product.setStock(stock);

        return productRepository.save(product);
    }

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

        return producerRepository.save(producer);
    }

}
  • 가독성을 위해 리포지토리를 통해 테스트 데이터를 생성하는 부분은 별도의 메서드로 구현
  • 이 경우 리포지토리를 사용하게 되면 매번 트랜잭션이 끊어져 생산업체 엔티티에서 상품 리스트를 가져올 수 없음
  • 이 같은 문제 해결을 위해 테스트 메서드에 @Transactional 어노테이션을 지정해 트랜잭션이 유지되도록 구성

 

  • 앞의 테스트를 통해 테스트 데이터를 생성하면 product 테이블과 producer 테이블에 레코드가 추가되지만 보여지는 내용만으로는 연관관계 설정 여부를 확인하기 어려움
  • 다대다 연관관계 설정을 통해 생성된 중간 테이블에 연관관계 매핑이 되어 있기 때문
  • -> 중간 테이블에 생성된 레코드 확인

상품 테이블과 생산업체 테이블의 중간 테이블 레코드

 

  • 중간 테이블에 설정한 연관관계에 맞춰 양 테이블의 기본키를 매핑한 레코드가 생성된 것 확인 가능

 

✅ 다대다 양방향 매핑

 

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

public class Product extends BaseEntity {

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

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Integer price;

    @Column(nullable = false)
    private Integer stock;

    @OneToOne(mappedBy = "product")
    @ToString.Exclude 
    private ProductDetail productDetail;

    @ManyToOne
    @JoinColumn(name = "provider_id")
    @ToString.Exclude
    private Provider provider;

    @ManyToMany
    @ToString.Exclude
    private List<Producer> producers = new ArrayList<>();

    public void addProducer(Producer producer){
        this.producers.add(producer);
    }

}
  • 생산업체에 대한 다대다 연관관계 설정
  • 필요에 따라 mappedBy 속성을 사용해 두 엔티티 간 연관관계의 주인 설정 가능 
  • 애플리케이션 실행 후 db의 테이블 구조 변경은 없음
  • 중간 테이블이 연관관계를 설정하고 있기 때문

 

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

@SpringBootTest
class ProducerRepositoryTest {
	//..생략..
    @Test
    @Transactional
    void relationshipTest2() {
        Product product1 = saveProduct("동글펜", 500, 1000);
        Product product2 = saveProduct("네모 공책", 100, 2000);
        Product product3 = saveProduct("지우개", 152, 1234);

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

        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 : " + producerRepository.findById(1L).get().getProducts());

        System.out.println("producers : " + productRepository.findById(2L).get().getProducers());

    }
	//..생략..
}
  • 양방향 연관관계 설정을 위해 addProducer()를 통해 연관관계 설정 코드 추가
  • 이렇게 다대다 연관관계를 설정하면 중간 테이블을 통해 연관된 엔티티의 값을 가져올 수 있음
  • 다만 다대다 연관관계에서는 중간 테이블이 생성되기 때문에 예기치 못한 쿼리가 생길 수 있음
  • 즉, 관리가 힘든 포인트 발생 가능
  • 이러한 한계를 극복하기 위해 중간 테이블을 생성하는 대신 일대다 다대일로 연관관계를 맺을 수 있는 중간 엔티티로 승격시켜 JPA에서 관리할 수 있게 생성하는 것을 추천 

 


 

영속성 전이(cascade)

 : 특정 엔티티의 영속성 상태 변경 시 그 엔티티와 연관된 엔티티의 영속성에도 영향을 미쳐 영속성 상태를 변경하는 것

 

📌 ex) @OneToMany 어노테이션의 인터페이스

  • 연관관계와 관련된 어노테이션을 보면 cascade()라는 요소를 볼 수 있음
  • 이 어노테이션은 영속성 전이를 설정하는 데 활용됨

@OneToMany 어노테이션에 있는 cascade 요소

 

 

📌 영속성 전이 타입의 종류

종류 설명
ALL 모든 영속 상태 변경에 대해 영속성 전이를 적용
PERSIST 엔티티가 영속화할 때 연관된 엔티티도 함께 영속화
MERGE 엔티티를 영속성 컨텍스트에 병합할 때 연관된 엔티티도 병합
REMOVE 엔티티를 제거할 때 연관된 엔티티도 제거
REFRESH 엔티티를 새로고침할 때 연관된 엔티티도 새로고침
DETACH 엔티티를 영속성 컨텍스트에서 제외하면 연관된 엔티티도 제외 
  • 영속성 전이에 사용되는 타입은 엔티티 생명주기와 연관이 있음을 알 수 있음
  • 한 엔티티가 cascade 요소의 값으로 주어진 영속 상태의 변경이 일어나면 매핑으로 연관된 엔티티에도 동일한 동작이 일어나도록 전이를 발생시키는 것
  • cascade() 요소의 리턴타입은 배열 형식
  • 즉, 개발자가 사용하고자 하는 cascade 타입을 골라 각 상황에 적용 가능 

 

✅ 영속성 전이 적용

  •  상품과 공급업체 엔티티를 사용
  • 예를 들어 한 가게가 새로운 공급업체와 계약하며 몇 가지 새 상품을 입고시키는 상황에 어떻게 영속성 전이가 적용되는지 살펴볼 것

📌 공급업체 엔티티에 영속성 전이 설정

 : 엔티티를 DB에 추가하는 경우로 영속성 전이 타입으로 PERSIST를 지정

public class Provider extends BaseEntity {
	//..생략..
    @OneToMany(mappedBy = "provider", cascade = CascadeType.PERSIST)
    @ToString.Exclude
    private List<Product> productList = new ArrayList<>();
}

 

📌 영속성 전이 테스트

    @Test
    void cascadeTest() {
        Provider provider = savedProvider("새로운 공급업체");

        Product product1 = savedProduct("상품1", 1000, 1000);
        Product product2 = savedProduct("상품2", 500, 1500);
        Product product3 = savedProduct("상품3", 750, 500);

        // 연관관계 설정
        product1.setProvider(provider);
        product2.setProvider(provider);
        product3.setProvider(provider);

        provider.getProductList().addAll(Lists.newArrayList(product1, product2, product3));

        providerRepository.save(provider);
    }
    
    private Provider savedProvider(String name) {
        Provider provider = new Provider();
        provider.setName(name);

        return provider;
    }

    private Product savedProduct(String name, Integer price, Integer stock) {
        Product product = new Product();
        product.setName(name);
        product.setPrice(price);
        product.setStock(stock);

        return product;
    }
  • 공급업체 하나와 상품 객체 3개 생성
  • 영속성 전이 테스트를 위해 객체에는 영속화 작업을 수행하지 않고 연관관계만 설정
  • 영속성 전이를 사용하면 부모 엔티티가 되는 Provider 엔티티만 저장하면 코드에 작성돼 있는 Cascade.PERSIST에 맞춰 상품 엔티티도 함께 저장 가능
  • 이처럼 특정 상황에 맞춰 영속성 전이 타입을 설정하면 영속 상태의 변화에 따라 연관된 엔티티들의 동작도 함께 수행할 수 있어 개발 생산성이 높아짐
  • 다만 자동 설정으로 동작하는 코드들이 정확히 어떤 영향을 미치는지 파악할 필요가 있음

 

✅ 고아 객체

 : JPA에서 고아란 부모 엔티티와 연관관계가 끊어진 엔티티를 의미. JPA에는 고아 객체를 자동으로 제거하는 기능 제공. 자식 엔티티가 다른 엔티티와 연관관계를 가지고 있다면 제거 기능은 비추천.

 

📌 공급업체 엔티티에 고아 객체를 제거하는 기능을 추가

public class Provider extends BaseEntity {
	//..생략..
    @OneToMany(mappedBy = "provider", cascade = CascadeType.PERSIST, orphanRemoval = true)
    @ToString.Exclude
    private List<Product> productList = new ArrayList<>();
}

 

📌 고아 객체의 제거 기능 테스트

    @Test
    @Transactional
    void orphanRemovalTest() {
        Provider provider = savedProvider("새로운 공급업체");

        Product product1 = savedProduct("상품1", 1000, 1000);
        Product product2 = savedProduct("상품2", 500, 1500);
        Product product3 = savedProduct("상품3", 750, 500);

        // 연관관계 설정
        product1.setProvider(provider);
        product2.setProvider(provider);
        product3.setProvider(provider);

        provider.getProductList().addAll(Lists.newArrayList(product1, product2, product3));

        providerRepository.saveAndFlush(provider);

        System.out.println("## Before Removal ##");
        System.out.println("## provider list ##");
        providerRepository.findAll().forEach(System.out::println);

        System.out.println("## product list ##");
        productRepository.findAll().forEach(System.out::println);

        // 연관관계 제거
        Provider foundProvider = providerRepository.findById(1L).get();
        foundProvider.getProductList().remove(0);

        System.out.println("## After Removal ##");
        System.out.println("## provider list ##");
        providerRepository.findAll().forEach(System.out::println);

        System.out.println("## product list ##");
        productRepository.findAll().forEach(System.out::println);
    }
  • 테스트 데이터 생성 후 연관관계 매핑 수행
  • 연관관계가 매핑된 각 엔티티들을 저장한 후 각 엔티티를 출력
  • -> 생성한 공급업체 엔티티 1개, 상품 엔티티 3개 출력
  • 고아 객체를 생성하기 위해 생성한 공급업체 엔티티를 가져온 후 첫 번째로 매핑돼 있는 상품 엔티티의 연관관계를 제거 -> 수행 결과 연관관계가 끊긴 상품의 엔티티가 제거된 것 확인 가능

 

 


QUIZ

  1. 연관관계 설정 시 다른 테이블의 기본값을 외래키로 갖는 테이블을 ( 주인(Owner) )이라는 개념을 사용하여 표현한다.
  2. 매핑할 외래키를 설정하기 위해 ( @JoinColumn ) 어노테이션을 사용한다.
  3. ( mappedBy )는 어떤 객체가 주인인지 표시하는 속성으로 양방향을 매핑하되 한쪽에만 외래키를 주기 위해 사용한다.
  4. 양방향으로 연관관계가 설정되면 ToString을 사용할 때 ( 순환참조 )가 발생하므로 이를 제거하기 위해 ( exclude )를 사용해 ToString에서 제외 설정을 한다.
  5. ( 영속성 전이(cascade) )는 특정 엔티티의 영속성 상태 변경 시 그 엔티티와 연관된 엔티티의 영속성에도 영향을 미쳐 영속성 상태를 변경하는 것을 의미한다.

PROGRAMMING QUIZ

1. 일대다 연관관계이며 주인 객체가 provider이고 fetch 전략이 즉시 로딩이 되도록 빈칸을 채우시오.

public class Provider extends BaseEntity {
	//..생략..
    /*빈칸*/
    @ToString.Exclude
    private List<Product> productList = new ArrayList<>();
}

 

2. 다음 코드의 빈칸에 엔티티가 영속화할 때 연관된 엔티티도 함께 영속화하고 고아 객체를 제거하도록 작성하시오.

public class Provider extends BaseEntity {
	//..생략..
    @OneToMany(mappedBy = "provider", /*빈칸*/)
    @ToString.Exclude
    private List<Product> productList = new ArrayList<>();
}

1번 답

public class Provider extends BaseEntity {
	//..생략..
    @OneToMany(mappedBy = "provider", fetch = FetchType.EAGER) //빈칸 답
    @ToString.Exclude
    private List<Product> productList = new ArrayList<>();
}

 

2번 답

public class Provider extends BaseEntity {
	//..생략..
    @OneToMany(mappedBy = "provider", cascade = CascadeType.PERSIST, orphanRemoval = true)
    @ToString.Exclude
    private List<Product> productList = new ArrayList<>();
}

 


 

출처: 장정우, 『스프링 부트 핵심 가이드』, 위키북스(2022), p247-292.

Corner Spring 1
Editor:  Minyong

728x90

관련글 더보기