*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성
- Windows11(윈도우 11) 환경
- 자바 JDK 17 버전 설치 https://yungenie.tistory.com/11
[Java] 차근차근 Java 설치하기 (JDK17, Window 11)
자바 개발 도구 설치 방법에 대해서 알아보겠습니다. Java17은 LTS(Long Term Support : 장기 지원) 릴리즈로 1년 후까지 기술 지원 및 버그를 개선한 서비스를 제공받을 수 있습니다. 업데이트 버전을 꾸
yungenie.tistory.com
- 스프링 부트 4.31.0 사용 - STS(Spring Tool Suite) 설치(Spring Tools for Eclipse - https://spring.io/tools)
=> https://priming.tistory.com/147 참고
[Windows] Spring Tool Suite 4(STS 4) 다운로드 및 설치
STS란?Spring Tool Suite(STS)는 스프링 프로젝트를 생성하고, 개발할 수 있게 해주는 도구입니다. STS 설치 과정에 대해 설명드리겠습니다. 설치 파일 다운로드STS 공식 사이트에서 설치 파일을 다운로
priming.tistory.com
- MySQL Community Server 8.0.42 설치 https://dev.mysql.com/downloads/mysql/
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 프로젝트 생성한 과정
*** 함께 보면 좋은 글
스프링 부트 핵심 가이드(장정우 지음) - 스프링 부트 개요
1. 스프링 프레임워크자바(Java) 기반 애플리케이션 프레임워크로, 엔터프라이즈급(기업 환경 대상 개발) 애플리케이션을 위한 다양한 기능 제공-> 오픈소스 경량급 애플리케이션 프레임워크로
keep-programming-study.tistory.com
스프링 부트 핵심 가이드(장정우 지음) - 개발에 앞서 알면 좋은 기초 지식
1. 서버 간 통신마이크로서비스 아키텍처에서 한 서버가 다른 서버에 통신을 요청하는 것을 의미-> 한 대는 서버/다른 한 대는 클라이언트가 됨 가장 많이 사용되는 방식은 HTTP/HTTPS 방식(TCP/IP, SOA
keep-programming-study.tistory.com
스프링 부트 핵심 가이드(장정우 지음) - 연관관계 매핑 in JPA[연관관계 매핑 개요, 연관관계 매핑
*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성 Windows11(윈도우 11) 환경자바 JDK 17 버전 설치 https://yungenie.tistory.com/11 [Java] 차근차근 Java 설치하기 (JDK17, Window 11)자바 개발 도구 설치 방법
keep-programming-study.tistory.com
스프링 부트 핵심 가이드(장정우 지음) - 연관관계 매핑 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)