데이터베이스에서는 두 테이블의 연관관계를 설정하면 "외래키"를 통해 서로 조인해서 참조하는 구조로 생성
JPA를 사용하는 객체지향 모델에서는 엔티티 간 참조 방향을 설정
데이터베이스와 관계를 일치시키기 위해 양방향으로 설정해도 무관하지만 비즈니스 로직의 관점에서 봤을 때 는 단방향 관계만 설정해도 해결되는 경우가 많음
@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;
}
// data/repository/ProductDetailRepository.java
public interface ProductDetailRepository extends JpaRepository<ProductDetail, Long> {
}
// test/com.springboot.relationship/data/repository/ProductDetailRepositoryTest.java
package com.springboot.relationship.data.repository;
import com.springboot.relationship.data.entity.Product;
import com.springboot.relationship.data.entity.ProductDetail;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public 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());
}
}
public @interface OneToOne {
Class targetEntity() default void.class;
CascadeType[] cascade() default {};
FetchType fetch() default FetchType.EAGER;
boolean optional() default true;
String mappedBy() default "";
boolean orphanRemoval() default false;
}
@ OneToOne 어노테이션은 기본 fetch 전략으로 EAGER(즉시 로딩) 전략이 채택
optional() 메서드는 기본값으로 true가 설정
기본값이 true인 상태는 매핑되는 값이 nullable이라는 것을 의미
@OneToOne 어노테이션에 'optional=false' 설정을 하면 product가 null인 값을 허용하지 않음
@Entity
// 생략..
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;
}
@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(mappedBy = "product")
private ProductDetail productDetail;
}
테스트 코드를 실행하면 toString 실행 시점에서 StackOverflowError 발생 -> 양방향으로 연관관계가 설정되면 ToString을 사용할 때 순환 참조가 발생하기 때문
// data/entity/Product.java
@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;
}
package com.springboot.relationship.data.entity;
// 생략..
public class Product extends BaseEntity {
// 생략..
@ManyToOne
@JoinColumn(name = "provider_id")
@ToString.Exclude
private Provider provider;
}
public interface ProviderRepository extends JpaRepository<Provider, Long> {
}
package com.springboot.relationship.data.repository;
import com.springboot.relationship.data.entity.Product;
import com.springboot.relationship.data.entity.Provider;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class ProviderRepositoryTest {
// 의존성 주입
@Autowired
ProviderRepository providerRepository;
@Autowired
ProductRepository productRepository;
@Test
void relationshipTest1(){
// 테스트 데이터 생산
Provider provider = new Provider();
provider.setName("Corner");
providerRepository.save(provider);
Product product = new Product();
product.setName("가위");
product.setPrice(5000);
product.setStock(500);
productRepository.save(product);
// 테스트
System.out.println("product: " + productRepository.findById(1L)
.orElseThrow(RuntimeException::new));
System.out.println("provider: " + productRepository.findById(1L)
.orElseThrow(RuntimeException::new).getProvider());
}
}
@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;
// @OneToMany의 기본 fetch 전략이 Lazy이기 때문에 즉시 로딩으로 조정
@OneToMany(mappedBy = "provider", fetch = FetchType.EAGER)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
@SpringBootTest
public class ProviderRepositoryTest {
@Autowired
ProviderRepository providerRepository;
@Autowired
ProductRepository productRepository;
@Test
void relationshipTest(){
// 테스트 데이터 생성
Provider provider = new Provider();
provider.setName("Corner");
providerRepository.save(provider);
Product product1 = new Product();
product1.setName("펜");
product1.setPrice(1000);
product1.setStock(1000);
product1.setProvider(provider);
Product product2 = new Product();
product2.setName("가방");
product2.setPrice(5000);
product2.setStock(300);
product2.setProvider(provider);
Product product3 = new Product();
product3.setName("모자");
product3.setPrice(500);
product3.setStock(50);
product3.setProvider(provider);
productRepository.save(product1);
productRepository.save(product2);
productRepository.save(product3);
List<Product> productList = providerRepository.findById(provider.getId()).get().getProductList();
for(Product product: productList){
System.out.println(product);
}
}
}
상품 분류 엔티티와 레포지토리 생성
// data/entity/Category.java
@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<>();
}
// data/repository/CategoryRepository.java
public interface CategoryRepository extends JpaRepository<Category, Long> {
}
// test/com.springboot.relationship/data/repository/CategoryRepositoryTest.java
@SpringBootTest
public 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> productList = categoryRepository.findById(1L).get().getProducts();
for(Product foundProduct:productList){
System.out.println(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> productList = new ArrayList<>();
public void addProduct(Product product){
productList.add(product);
}
}
public interface ProducerRepository extends JpaRepository<Producer, Long> {
}
public class ProducerRepositoryTest {
@Autowired
ProducerRepository producerRepository;
@Autowired
ProductRepository productRepository;
@Test
@Transactional
void relationshipTest(){
Product product1 = saveProduct("동글펜", 400, 1000);
Product product2 = saveProduct("네모 공책", 200, 200);
Product product3 = saveProduct("지우개", 300, 1400);
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().getProductList());
}
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 productRepository.save(producer);
}
}
@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(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);
}
}
Public class ProducerRepositoryTest {
// 생략..
@Test
@Transactional
void relationshipTest2(){
Product product1 = saveProduct("동글펜", 400, 1000);
Product product2 = saveProduct("네모 공책", 200, 200);
Product product3 = saveProduct("지우개", 300, 1400);
// 생략..
// 양방향 연관관계 설정
product1.addProducer(producer1);
product2.addProducer(producer2);
product2.addProducer(producer1);
product3.addProducer(producer2);
producerRepository.saveAll(Lists.newArrayList(producer1,producer2));
productRepository.saveAll(Lists.newArrayList(product1, product2, product3));
System.out.println(producerRepository.findById(1L).get().getProductList());
System.out.println(productRepository.findById(2L).get().getProducers());
}
// 생략..
}
cascade() 요소와 함께 사용하는 영속성 전이 타입
ALL | 모든 영속 상태 변경에 대해 영속성 전이를 적용 |
PERSIST | 엔티티가 영속화할 때 연관된 엔티티도 함께 영속화 |
MERGE | 엔티티를 영속성 컨텍스트에 병합할 때 연관된 엔티티도 병합 |
REMOVE | 엔티티를 제거할 때 연관된 엔티티도 제거 |
REFRESH | 엔티티를 새로고침할 때 연관된 엔티티도 새로고침 |
DETACH | 엔티티를 영속성 컨텍스트에서 제외하면 연관된 엔티티도 제외 |
@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;
// 영속성 전이를 위한 @OneToMany 어노테이션
@OneToMany(mappedBy = "provider", cascade = CascadeType.PERSIST)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
@SpringBootTest
public class ProviderRepositoryTest {
@Autowired
ProviderRepository providerRepository;
@Autowired
ProducerRepositoryTest productRepository;
@Test
void cascadeTest(){
Provider provider = saveProvider("new provider");
Product product1 = saveProduct("동글펜", 400, 1000);
Product product2 = saveProduct("네모 공책", 200, 200);
Product product3 = saveProduct("지우개", 300, 1400);
// 연관관계 설정
product1.setProvider(provider);
product2.setProvider(provider);
product3.setProvider(provider);
provider.getProductList().addAll(Lists.newArrayList(product1, product2, product3));
// 영속성 전이 수행
providerRepository.save(provider);
}
private Product saveProduct(String name, Integer price, Integer stock){
Product product = new Product();
product.setName(name);
product.setPrice(price);
product.setStock(stock);
return product;
}
private Provider saveProvider(String name){
Provider provider = new Provider();
provider.setName(name);
return provider;
}
}
providerRepository.save(provider);
@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;
// 'orphanRemoval = true'은 고아 객체를 제거하는 기능
@OneToMany(mappedBy = "provider", cascade = CascadeType.PERSIST, orphanRemoval = true)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
@SpringBootTest
public class ProviderRepositoryTest {
// 생략...
product1.setProvider(provider);
product2.setProvider(provider);
product3.setProvider(provider);
provider.getProductList().addAll(Lists.newArrayList(product1, product2, product3));
providerRepository.saveAndFlush(provider);
// 엔티티 출력
providerRepository.findAll().forEach(System.out::println);
productRepository.findAll().forEach(System.out::println);
// 생략...
}
Q1. 문제 (드래그하여 답 확인해주세요.)
1. 데이터베이스에서는 두 테이블의 연관관계를 설정하면 ( 외래키 )를 통해 서로 조인해서 참조하는 구조로 생성된다.
※ @JoinColum 어노테이션에서 사용할 수 있는 속성들을 작성하시오.(2~4번)
2. 매핑할 외래키의 이름을 설정 => ( name )
3. 외래키가 참조할 상대 테이블의 칼럼명을 지정 => ( referencedColumnName )
4. 외래키를 생성하면서 지정할 제약조건을 설정( unique. nullable, insertable. updatable 등). => ( foreignkey )
5. 엔티티를 조회할 때 연관된 엔티티도 함께 조회하는 것을 ( 즉시 로딩 )이라 한다.
6. ( 영속성 전이 )란 특정 엔티티의 영속성 상태를 변경할 때 그 엔티티와 연관된 엔티티의 영속성에도 영향을 미쳐 영속성 상태를 변경하는 것이다.
7. JPA에서 ( 고아객체 ) 란 부모 엔티티와 연관관계가 끊어진 엔티티고, 이러한 객체를 자동으로 제거하는 기능이 있다.
Q2. 코드 문제
다대일 양방향 매핑 코드의 빈 부분의 코드를 작성.
@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;
@OneToMany(mappedBy = "provider", fetch = ***1번***)
@ToString.***2번***
private List<Product> productList = new ArrayList<>();
}
1번:
FetchType.EAGER
2번:
Exclude
출처: 스프링부트 핵심 가이드
SPRING #2
Editor : dalpaeng4
[스프링 2팀] 10~11장. 유효성 검사와 예외처리 & 액추에이터 활용하기 (0) | 2025.01.17 |
---|---|
[스프링 2팀] 8장. Spring Data JPA 활용 (0) | 2025.01.03 |
[스프링 2] 7장. 테스트코드 작성하기 (0) | 2024.12.27 |
[스프링 2] 6장. 데이터베이스 연동 (0) | 2024.11.29 |
[스프링 2팀]5~6장. API를 작성하는 다양한 방법 & 데이터베이스 연동 (0) | 2024.11.22 |