스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초

스프링 부트 핵심 가이드(장정우 지음) - 연관관계 매핑 in JPA 마지막[영속성 전이(Cascade)]

개발학생 2025. 10. 20. 16:41
반응형

*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성  

 

[Java] 차근차근 Java 설치하기 (JDK17, Window 11)

자바 개발 도구 설치 방법에 대해서 알아보겠습니다. Java17은 LTS(Long Term Support : 장기 지원) 릴리즈로 1년 후까지 기술 지원 및 버그를 개선한 서비스를 제공받을 수 있습니다. 업데이트 버전을 꾸

yungenie.tistory.com

 

[Windows] Spring Tool Suite 4(STS 4) 다운로드 및 설치

STS란?Spring Tool Suite(STS)는 스프링 프로젝트를 생성하고, 개발할 수 있게 해주는 도구입니다. STS 설치 과정에 대해 설명드리겠습니다. 설치 파일 다운로드STS 공식 사이트에서 설치 파일을 다운로

priming.tistory.com

 

MySQL :: Download MySQL Community Server

Select Version: 9.3.0 Innovation 8.4.5 LTS 8.0.42 Select Operating System: Select Operating System… Microsoft Windows Ubuntu Linux Debian Linux SUSE Linux Enterprise Server Red Hat Enterprise Linux / Oracle Linux Fedora Linux - Generic Oracle Solaris mac

dev.mysql.com

  • Gradle

**STS에서 Gradle 프로젝트 생성한 과정 

*** 함께 보면 좋은 글

2025.07.10 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링 부트 핵심 가이드(장정우 지음) - 스프링 부트 개요

 

스프링 부트 핵심 가이드(장정우 지음) - 스프링 부트 개요

1. 스프링 프레임워크자바(Java) 기반 애플리케이션 프레임워크로, 엔터프라이즈급(기업 환경 대상 개발) 애플리케이션을 위한 다양한 기능 제공-> 오픈소스 경량급 애플리케이션 프레임워크로

keep-programming-study.tistory.com

2025.07.11 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링 부트 핵심 가이드(장정우 지음) - 개발에 앞서 알면 좋은 기초 지식

 

스프링 부트 핵심 가이드(장정우 지음) - 개발에 앞서 알면 좋은 기초 지식

1. 서버 간 통신마이크로서비스 아키텍처에서 한 서버가 다른 서버에 통신을 요청하는 것을 의미-> 한 대는 서버/다른 한 대는 클라이언트가 됨 가장 많이 사용되는 방식은 HTTP/HTTPS 방식(TCP/IP, SOA

keep-programming-study.tistory.com

2025.09.27 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링 부트 핵심 가이드(장정우 지음) - 연관관계 매핑 in JPA[연관관계 매핑 개요, 연관관계 매핑 종류와 방향, 일대일 매핑 예시]

 

스프링 부트 핵심 가이드(장정우 지음) - 연관관계 매핑 in JPA[연관관계 매핑 개요, 연관관계 매핑

*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성 Windows11(윈도우 11) 환경자바 JDK 17 버전 설치 https://yungenie.tistory.com/11 [Java] 차근차근 Java 설치하기 (JDK17, Window 11)자바 개발 도구 설치 방법

keep-programming-study.tistory.com

 

2025.10.17 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링 부트 핵심 가이드(장정우 지음) - 연관관계 매핑 in JPA 2[다대일 매핑 예시, 일대다 매핑 예시, 다대다 매핑 예시]

 

스프링 부트 핵심 가이드(장정우 지음) - 연관관계 매핑 in JPA 2[다대일 매핑 예시, 일대다 매핑 예

*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성 Windows11(윈도우 11) 환경자바 JDK 17 버전 설치 https://yungenie.tistory.com/11 [Java] 차근차근 Java 설치하기 (JDK17, Window 11)자바 개발 도구 설치 방법

keep-programming-study.tistory.com

 

 

1. 영속성 전이(Cascade) 개념

  • 영속성: 엔티티 객체가 영속성 컨텍스트에 저장되어 데이터베이스와의 상태 변화가 관리되는 것
  • 영속성 전이: 특정 엔티티의 영속성 상태를 변경할 때, 그 엔티티와 연관된 엔티티의 영속성에도 영향을 미쳐 영속성 상태 변경
  • 연관관계 관련 어노테이션의 인터페이스를 살펴보면 'cascade()' 요소를 볼 수 있으며,
    이 어노테이션은 영속성 전이를 설정하는 데 활용됨

2. 영속성 전이 타입의 종류

  • 영속성 전이에 사용되는 타입은 엔티티 생명주기와 연관이 있음
종류 설명
ALL 모든 영속 상태 변경에 대해 영속성 전이 적용
PERSIST 엔티티가 영속화할 때 연관된 엔티티도 함께 영속화
MERGE 엔티티를 영속성 컨텍스트에 병합할 때 연관된 엔티티도 병합
REMOVE 엔티티를 제거할 때 연관된 엔티티도 제거
REFRESH 엔티티를 새로고침할 때 연관된 엔티티도 새로고침
DETACH 엔티티를 영속성 컨텍스트에서 제외하면 연관된 엔티티도 제외

3. 영속성 전이 적용

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

package com.example.demo.jpa.data.entity;

import java.util.ArrayList;
import java.util.List;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

//클래스의 모든 필드에 대해 Getter 메서드를 자동 생성
@Getter
//클래스의 모든 필드에 대해 Setter 메서드를 자동 생성
@Setter
//파라미터가 없는 기본 생성자를 자동 생성
@NoArgsConstructor
//모든 필드를 파라미터로 받는 생성자를 자동 생성
@AllArgsConstructor
//이 클래스는 JPA 엔티티로 데이터베이스 테이블과 매핑
@Entity
//toString() 메서드를 자동 생성(부모 클래스의 필드까지 포함)
@ToString(callSuper = true)
//equals()와 hashCode() 메서드를 생성(부모 클래스의 필드까지 포함)
@EqualsAndHashCode(callSuper = true)
//엔티티가 매핑될 테이블 이름을 명시적으로 지정 (기본값은 클래스 이름)
@Table(name="provider")
//BaseEntity를 상속하여, LocalDateTime.now() 메서드로 시간을 주입하지 않아도 자동으로 생성 일시/수정 일시의 값 생성
public class Provider extends BaseEntity {
	// 기본 키(PK)로 지정된 필드. 데이터베이스의 고유 식별자 역할
    @Id
    // ID 값을 자동 생성함. GenerationType.IDENTITY는 DB가 직접 AUTO_INCREMENT 방식으로 생성하도록 함(insert 후에 pk 자동생성)
    // -> IDENTITY외에도 AUTO(기본값, 사용하는 데이터베이스에 맞게 기본값 자동 생성), 
    //    SEQUENCE(Oracle, PostgreSQL 등 시퀀스를 지원하는 DB에서 별도 시퀀스 객체를 생성해 PK값 증가), 
    //    TABLE(키 값을 관리하는 별도 테이블을 생성해 PK값 증가-성능 낮음)
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long id;
    
    // 엔티티 클래스의 필드는 자동으로 테이블 칼럼으로 매핑되므로, 별다른 설정을 하지 않을 경우 @Column을 생략해도 됨
    // 업체 이름 필드
    private String name;
    
    // 업체 생성 일시와 업체 정보 변경 일시 자동 설정
    
    // 상품 엔티티와 일대다 양방향 매핑
    // -> 연관된 엔티티의 '조회 방식'은 기본으로 변경하고, '영속성 전이'룰 활성화하여 부모 엔티티의 작업이 자식 엔티티에 자동으로 전파
    @OneToMany(mappedBy = "provider", cascade=CascadeType.PERSIST) // Product 엔티티의 'provider' 필드를 통해 매핑 즉, 외래 키는 Product 쪽에서 관리
    @ToString.Exclude // Lombok의 toString() 메서드에서 이 필드를 제외하여 무한 루프 방지
    private List<Product> productList = new ArrayList<>(); // 제공자가 가진 상품 목록을 저장하는 리스트. Null 방지를 위해 초기화
}

 

2) 영속성 전이 테스트 코드 작성

package com.example.demo.jpa.data.repository;

import java.util.List;

import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.example.demo.jpa.data.entity.Product;
import com.example.demo.jpa.data.entity.Provider;

@SpringBootTest // Spring Boot 테스트 환경을 설정하여 실제 애플리케이션 컨텍스트에서 테스트를 실행
public class ProviderRepositoryTest {
	// Product 엔티티를 위한 JPA 리포지토리 주입
	@Autowired
	ProductRepository productRepository;
	
	// Provider 엔티티를 위한 JPA 리포지토리 주입
	@Autowired
	ProviderRepository providerRepository;
	
	... 생략 ...
	
	// 영속성 전이 테스트
	@Test
	void cascadeTest() {
		// 공급업체 엔티티 생성 (영속 상태 아님)
	    Provider provider = savedProvider("새로운 공급업체");
	    
	    // 상품 엔티티 3개 생성 (영속 상태 아님)
	    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));
	    
	    // 공급업체 저장: CascadeType.PERSIST 설정되어 있으므로 상품들도 함께 저장됨
	    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) 테스트 코드 실행 시 Hibernate SQL 쿼리 로그

insert
into
    provider
    (created_at, updated_at, name)
values
    (?, ?, ?)

insert
into
    product
    (created_at, updated_at, name, prive, provider_id, stock)
values
    (?, ?, ?, ?, ?, ?)

insert
into
    product
    (created_at, updated_at, name, prive, provider_id, stock)
values
    (?, ?, ?, ?, ?, ?)
    
insert
into
    product
    (created_at, updated_at, name, prive, provider_id, stock)
values
    (?, ?, ?, ?, ?, ?)
    
insert
into
    product
    (created_at, updated_at, name, prive, provider_id, stock)
values
    (?, ?, ?, ?, ?, ?)

4. 고아(orphan) 객체 

  • 부모 엔티티와 연관관계가 끊어진 엔티티를 '고아'라고 하며, JPA에는 이 고아 객체를 자동으로 제거하는 기능이 있음 

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

package com.example.demo.jpa.data.entity;

import java.util.ArrayList;
import java.util.List;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

//클래스의 모든 필드에 대해 Getter 메서드를 자동 생성
@Getter
//클래스의 모든 필드에 대해 Setter 메서드를 자동 생성
@Setter
//파라미터가 없는 기본 생성자를 자동 생성
@NoArgsConstructor
//모든 필드를 파라미터로 받는 생성자를 자동 생성
@AllArgsConstructor
//이 클래스는 JPA 엔티티로 데이터베이스 테이블과 매핑
@Entity
//toString() 메서드를 자동 생성(부모 클래스의 필드까지 포함)
@ToString(callSuper = true)
//equals()와 hashCode() 메서드를 생성(부모 클래스의 필드까지 포함)
@EqualsAndHashCode(callSuper = true)
//엔티티가 매핑될 테이블 이름을 명시적으로 지정 (기본값은 클래스 이름)
@Table(name="provider")
//BaseEntity를 상속하여, LocalDateTime.now() 메서드로 시간을 주입하지 않아도 자동으로 생성 일시/수정 일시의 값 생성
public class Provider extends BaseEntity {
	// 기본 키(PK)로 지정된 필드. 데이터베이스의 고유 식별자 역할
    @Id
    // ID 값을 자동 생성함. GenerationType.IDENTITY는 DB가 직접 AUTO_INCREMENT 방식으로 생성하도록 함(insert 후에 pk 자동생성)
    // -> IDENTITY외에도 AUTO(기본값, 사용하는 데이터베이스에 맞게 기본값 자동 생성), 
    //    SEQUENCE(Oracle, PostgreSQL 등 시퀀스를 지원하는 DB에서 별도 시퀀스 객체를 생성해 PK값 증가), 
    //    TABLE(키 값을 관리하는 별도 테이블을 생성해 PK값 증가-성능 낮음)
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long id;
    
    // 엔티티 클래스의 필드는 자동으로 테이블 칼럼으로 매핑되므로, 별다른 설정을 하지 않을 경우 @Column을 생략해도 됨
    // 업체 이름 필드
    private String name;
    
    // 업체 생성 일시와 업체 정보 변경 일시 자동 설정
    
    // 상품 엔티티와 일대다 양방향 매핑
    // -> 연관된 엔티티의 '조회 방식'은 기본으로 변경하고, '영속성 전이'룰 활성화하여 부모 엔티티의 작업이 자식 엔티티에 자동으로 전파
    // -> -> 고아 객체를 제거하는 orphanRemoval = true를 추가
    @OneToMany(mappedBy = "provider", cascade=CascadeType.PERSIST, orphanRemoval = true) // Product 엔티티의 'provider' 필드를 통해 매핑 즉, 외래 키는 Product 쪽에서 관리
    @ToString.Exclude // Lombok의 toString() 메서드에서 이 필드를 제외하여 무한 루프 방지
    private List<Product> productList = new ArrayList<>(); // 제공자가 가진 상품 목록을 저장하는 리스트. Null 방지를 위해 초기화
}

2) 고아 객체의 제거 기능을 테스트하는 코드 작성

package com.example.demo.jpa.data.repository;

import java.util.List;

import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.example.demo.jpa.data.entity.Product;
import com.example.demo.jpa.data.entity.Provider;

import jakarta.transaction.Transactional;

@SpringBootTest // Spring Boot 테스트 환경을 설정하여 실제 애플리케이션 컨텍스트에서 테스트를 실행
public class ProviderRepositoryTest {
	// Product 엔티티를 위한 JPA 리포지토리 주입
	@Autowired
	ProductRepository productRepository;
	
	// Provider 엔티티를 위한 JPA 리포지토리 주입
	@Autowired
	ProviderRepository providerRepository;
	
	... 생략 ...
	
	// 고아 객체의 제거 기능 테스트
	@Test
	@Transactional
	void orphanRemovalTest() {
		// 공급업체 엔티티 생성 (영속 상태 아님)
	    Provider provider = savedProvider("새로운 공급업체");
	    
	    // 상품 엔티티 3개 생성 (영속 상태 아님)
	    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));
	    
	    // 공급업체 저장: CascadeType.PERSIST 설정되어 있으므로 상품들도 함께 저장됨
	    // -> save 메서드와 다르게, 엔티티를 저장하고 즉시 flush
	    providerRepository.saveAndFlush(provider);
	    
	    // 저장된 공급업체 및 상품 목록 출력 (DB에 실제 저장된 상태 확인)
	    providerRepository.findAll().forEach(System.out::println);
	    productRepository.findAll().forEach(System.out::println);

	    // 공급업체 조회 후 상품 리스트에서 첫 번째 상품 제거
	    // -> 이때 orphanRemoval = true 설정되어 있으면 해당 상품도 DB에서 삭제됨
	    Provider foundProvider = providerRepository.findById(1L).get();
	    foundProvider.getProductList().remove(0);

	    // 변경 후 공급업체 및 상품 목록 출력
	    // -> orphanRemoval 설정 여부에 따라 상품이 삭제되었는지 확인 가능
	    providerRepository.findAll().forEach(System.out::println);
	    providerRepository.findAll().forEach(System.out::println);
	    productRepository.findAll().forEach(System.out::println);
	}
}

3) 테스트 코드 실행 시 Hibernate SQL 쿼리 로그

  • 연관관계가 끊긴 상품의 엔티티가 제거된 것을 확인할 수 있음 
select
    provider0_.id as id1_6_,
    provider0_.created_at as created_2_6_,
    provider0_.updated_at as updated_3_6_,
    provider0_.name as name4_6
from
    provider provider0_

Proider(super=BaseEntity(createdAt=2025-10-20T11:40:59.142135500, updatedAt=2025-10-
20T11:40:59.142135500), id=1, name=새로운 공급업체)

delete
from
    product
where
    number=?

select
    product0_.number as number1_3_,
    product0_.created_at as created_2_3_,
    product0_.updated_at as updated_3_3_,
    product0_.name as name4_3_,
    product0_.price as price5_3_,
    product0_.provider_id as provider7_3_,
    product0_.stock as stock6_3_
from
    product product0_

Product(super=BaseEntity(createdAt=2025-10-20T11:40:59.221837349, updatedAt-2025-10-
20T11:40:59.221837349), number=2, name=상품2, price=500, stock=1500)

Product(super=BaseEntity(createdAt=2025-10-20T11:40:59.221937355, updatedAt-2025-10-
20T11:40:59.221937355), number=3, name=상품3, price=750, stock=500)
반응형