RDBMS에서는 여러 테이블 간의 관계를 정의하고 활용해야 하며, JPA는 객체 모델에서도 이를 매핑하여 사용할 수 있습니다. 연관관계는 객체와 테이블의 성질이 다르므로 이를 조정하며 사용하는 방법을 이해해야 합니다.
두 엔티티 간의 연관관계를 매핑할 수 있는 종류는 다음과 같습니다:
예시: 상품 테이블과 공급업체 테이블의 관계
데이터베이스에서는 외래키를 통해 두 테이블 간의 관계를 연결하며, 객체 지향 모델에서는 단방향 혹은 양방향으로 매핑이 가능합니다.
JPA에서 외래키는 관계의 주인(Owner) 테이블이 설정합니다. 이 테이블만 외래키를 관리합니다.
새로운 프로젝트 설정:
하나의 상품(Product)에 하나의 상품정보(ProductDetail)가 매핑되는 구조로 일대일 관계를 구현합니다. 이를 위해 엔터티와 매핑 테이블을 설계합니다.
상품(Product) 엔터티와 상품정보(ProductDetail) 엔터티 간 단방향 매핑을 설정합니다.
@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;
}
매핑 시 주의사항
Hibernate 설정
데이터 저장 및 조회
리포지토리를 활용해 데이터 생성 및 조회 테스트를 진행합니다.
public interface ProductDetailRepository extends JpaRepository<ProductDetail, Long> {
}
@SpringBootTest
class ProductDetailRepositoryTest {
@Autowired
ProductDetailRepository productDetailRepository;
@Autowired
ProductRepository productRepository;
@Test
public void saveAndReadTest() {
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());
}
}
테스트 결과는 연관된 엔티티를 left join으로 즉시 로딩하며 조회 결과를 반환합니다.
select productdet0_.id as id1_1_,
productdet0_.description as descript3_1_,
productdet0_.product_number as product_5_1_
from product_detail productdet0_
left outer join product product1_
on productdet0_.product_number=product1_.number
where productdet0_.id=?
@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;
}
값이 반드시 필요할 때 설정 (optional=false)
@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(optional = false)
@JoinColumn(name = "product_number")
private Product product;
}
Hibernate 쿼리 (테이블 생성):
optional=false로 설정하면 데이터베이스 테이블 생성 시 NOT NULL 제약 조건이 추가됩니다.
create table product_detail (
id bigint not null auto_increment,
created_at datetime(6),
updated_at datetime(6),
description varchar(255),
product_number bigint not null,
primary key (id)
) engine=InnoDB
Hibernate 쿼리 (데이터 조회):
select
productdet1_.id as id1_1_0_,
productdet1_.product_number as product_5_1_0_
from
product_detail productdet1_
left outer join
product product1_
on
productdet1_.product_number=product1_.number
where
productdet1_.id=?;
select
productdet1_.id as id1_1_0_,
productdet1_.product_number as product_5_1_0_
from
product_detail productdet1_
inner join
product product1_
on
productdet1_.product_number=product1_.number
where
productdet1_.id=?;
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;
}
문제점
select
productdet1_.id as id1_1_0_,
productdet1_.product_number as product_5_1_0_,
product1_.product_detail_id as product_7_0_1_
from
product_detail productdet1_
left outer join
product product1_
on
productdet1_.product_number=product1_.number
left outer join
product_detail productdet2_
on
product1_.product_detail_id=productdet2_.id
where
productdet1_.id=?;
mappedBy 속성
@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;
}
순환 참조 문제
@OneToOne(mappedBy = "product")
@ToString.Exclude
private ProductDetail productDetail;
Hibernate 쿼리 (효율적인 쿼리):
select
productdet1_.id as id1_1_0_,
productdet1_.product_number as product_5_1_0_
from
product_detail productdet1_
inner join
product product1_
on
productdet1_.product_number=product1_.number
where
productdet1_.id=?;
1. 공급업체 엔티티 클래스
@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;
}
2. 상품 엔티티에 공급업체 필드 추가
@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;
}
3. Product 테이블과 Provider 테이블 생성
ProviderRepository 인터페이스 생성
public interface ProviderRepository extends JpaRepository<Provider, Long> {
}
다대일 단방향 매핑 테스트
@SpringBootTest
class ProviderRepositoryTest {
@Autowired
ProductRepository productRepository;
@Autowired
ProviderRepository providerRepository;
@Test
void relationshipTest() {
// 테스트 데이터 생성
Provider provider = new Provider();
provider.setName("OO물산");
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(() -> new RuntimeException("Product not found")));
System.out.println("provider: " + productRepository.findById(1L)
.orElseThrow(() -> new RuntimeException("Product not found"))
.getProvider());
}
}
Hibernate 실행 쿼리
insert into provider (created_at, updated_at, name) values (?, ?, ?);
insert into product (created_at, updated_at, name, price, provider_id, stock) values (?, ?, ?, ?, ?, ?);
select
product0_.number as number1_0_0_,
product0_.created_at as created2_0_0_,
product0_.updated_at as updated3_0_0_,
product0_.name as name4_0_0_,
product0_.price as price5_0_0_,
product0_.provider_id as provider7_0_0_,
product0_.stock as stock6_0_0_,
provider1_.id as id1_2_1_,
provider1_.created_at as created2_2_1_,
provider1_.updated_at as updated3_2_1_,
provider1_.name as name4_2_1_
from
product product0_
left outer join
provider provider1_
on
product0_.provider_id = provider1_.id
where
product0_.number = ?;
product: Product(super=BaseEntity(...), number=1, name=가위, price=5000, stock=500, provider=Provider(super=BaseEntity(...), id=1, name=OO물산))
provider: Provider(super=BaseEntity(...), id=1, name=OO물산)
@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 = FetchType.EAGER)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
테스트 코드
@SpringBootTest
class ProviderRepositoryTest {
@Autowired
ProductRepository productRepository;
@Autowired
ProviderRepository providerRepository;
@Test
void relationshipTest() {
// 테스트 데이터 생성
Provider provider = new Provider();
provider.setName("OO상사");
providerRepository.save(provider);
Product product1 = new Product();
product1.setName("가위");
product1.setPrice(2000);
product1.setStock(100);
product1.setProvider(provider);
productRepository.save(product1);
Product product2 = new Product();
product2.setName("가방");
product2.setPrice(20000);
product2.setStock(200);
product2.setProvider(provider);
productRepository.save(product2);
List<Product> products = providerRepository.findById(provider.getId()).get().getProductList();
for (Product product : products) {
System.out.println(product);
}
}
}
Hibernate 실행 쿼리
select
provider0_.id as id1_2_0_,
provider0_.created_at as created2_2_0_,
provider0_.updated_at as updated3_2_0_,
provider0_.name as name4_2_0_,
productlis1_.number as number1_0_1_,
productlis1_.created_at as created2_0_1_,
productlis1_.updated_at as updated3_0_1_,
productlis1_.name as name4_0_1_,
productlis1_.price as price5_0_1_,
productlis1_.provider_id as provider7_0_1_,
productlis1_.stock as stock6_0_1_
from
provider provider0_
left outer join
product productlis1_
on
provider0_.id = productlis1_.provider_id
where
provider0_.id = ?;
Product(super=BaseEntity(...), number=1, name=가위, price=2000, stock=100, ...)
Product(super=BaseEntity(...), number=2, name=가방, price=20000, stock=200, ...)
상품 분류(Category) 엔티티
@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<>();
}
테이블 생성
리포지토리 생성
public interface CategoryRepository extends JpaRepository<Category, Long> {
}
테스트 코드
@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);
}
}
}
Hibernate 실행 쿼리
insert into product (created_at, updated_at, name, price, provider_id, stock)
values (?, ?, ?, ?, ?, ?);
insert into category (code, name) values (?, ?);
update product set category_id=? where number=?;
select
category0_.id as id1_0_0_,
category0_.code as code2_0_0_,
category0_.name as name3_0_0_,
products1_.category_id as category8_1_1_,
products1_.number as number1_1_1_,
products1_.created_at as created2_1_1_,
products1_.updated_at as updated3_1_1_,
products1_.name as name4_1_1_,
products1_.price as price5_1_1_,
products1_.provider_id as provider7_1_1_,
products1_.stock as stock6_1_1_
from
category category0_
left outer join
product products1_
on
category0_.id = products1_.category_id
where
category0_.id = ?;
결과
생산업체(Producer) 엔티티
@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);
}
}
테이블 생성
리포지토리 생성
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("네이처");
Producer producer2 = saveProducer("북스");
producer1.addProduct(product1);
producer1.addProduct(product2);
producer2.addProduct(product2);
producer2.addProduct(product3);
producerRepository.saveAll(List.of(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);
}
}
Hibernate 실행 쿼리
insert into product (created_at, updated_at, name, price, provider_id, stock)
values (?, ?, ?, ?, ?, ?);
insert into producer (code, name) values (?, ?);
insert into producer_products (producer_id, products_number)
values (?, ?);
결과
상품(Product) 엔티티 - 생산업체(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);
}
}
테스트 코드
@SpringBootTest
class ProducerRepositoryTest {
@Autowired
ProducerRepository producerRepository;
@Autowired
ProductRepository productRepository;
@Test
@Transactional
void relationshipTest2() {
Product product1 = saveProduct("동글펜", 500, 1000);
Product product2 = saveProduct("네모 공책", 100, 2000);
Product product3 = saveProduct("지우개", 152, 1234);
Producer producer1 = saveProducer("네이처");
Producer producer2 = saveProducer("북스");
// 연관 관계 설정
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(List.of(producer1, producer2));
productRepository.saveAll(List.of(product1, product2, product3));
// 테스트 출력
System.out.println("products: " + producerRepository.findById(1L).get().getProducts());
System.out.println("producers: " + productRepository.findById(2L).get().getProducers());
}
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);
}
}
Hibernate 실행 쿼리
insert into product (created_at, updated_at, name, price, provider_id, stock)
values (?, ?, ?, ?, ?, ?);
insert into producer (created_at, updated_at, name, code)
values (?, ?, ?, ?);
insert into producer_products (producer_id, products_number)
values (?, ?);
select
producer0_.id as id1_6_0_,
producer0_.created_at as created2_6_0_,
producer0_.updated_at as updated3_6_0_,
producer0_.name as name4_6_0_,
producer0_.code as code5_6_0_,
products1_.producer_id as producer1_7_1_,
products1_.products_number as products2_7_1_,
product2_.number as number1_3_2_,
product2_.created_at as created2_3_2_,
product2_.updated_at as updated3_3_2_,
product2_.name as name4_3_2_,
product2_.price as price5_3_2_,
product2_.provider_id as provider7_3_2_,
product2_.stock as stock6_3_2_
from
producer producer0_
left outer join
producer_products products1_
on
producer0_.id = products1_.producer_id
left outer join
product product2_
on
products1_.products_number = product2_.number
where
producer0_.id = ?;
테스트 결과
products: [Product(super=BaseEntity(...), number=1, name=동글펜, ...), Product(super=BaseEntity(...), number=2, name=네모 공책, ...)]
producers: [Producer(super=BaseEntity(...), id=1, name=네이처, ...), Producer(super=BaseEntity(...), id=2, name=북스, ...)]
한계 및 권장 사항
공급업체(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;
@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(List.of(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;
}
Hibernate 실행 쿼리
insert into provider (created_at, updated_at, name)
values (?, ?, ?);
insert into product (created_at, updated_at, name, price, provider_id, stock)
values (?, ?, ?, ?, ?, ?);
공급업체(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;
@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(List.of(product1, product2, product3));
providerRepository.saveAndFlush(provider);
// 고아 객체 생성
Provider foundProvider = providerRepository.findById(1L).get();
foundProvider.getProductList().remove(0); // 첫 번째 Product 제거
providerRepository.findAll().forEach(System.out::println);
productRepository.findAll().forEach(System.out::println);
}
Hibernate 실행 쿼리
delete from product where number = ?;
select * from provider;
select * from product;
1. 양방향 연관관계에서 mappedBy 설정
@Entity
public class ProductDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
@JoinColumn(name = "product_id")
private Product product;
}
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(mappedBy = "__________")
private ProductDetail productDetail;
}
2. ManyToMany 중간 테이블 이름 설정
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
private List<Producer> producers = new ArrayList<>();
}
@Entity
public class Producer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
private List<Product> products = new ArrayList<>();
}
1. mappedBy
2. 즉시, 지연
3. cascade
4. @JoinTable
5. orphanRemoval = true
6. @OneToMany, Lazy
7. mappedBy, 필드
1. 양방향 연관관계에서 mappedBy 설정
@Entity
public class ProductDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
@JoinColumn(name = "product_id")
private Product product;
}
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(mappedBy = "product")
private ProductDetail productDetail;
}
2. ManyToMany 중간 테이블 이름 설정
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
@JoinTable(name = "producer_product_mapping")
private List<Producer> producers = new ArrayList<>();
}
@Entity
public class Producer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(mappedBy = "producers")
private List<Product> products = new ArrayList<>();
}
[출처] 장정우, 「스프링부트 핵심 가이드」 9장
Corner Spring 3
ⓒ Hetbahn
[스프링 3팀] 12장. 서버 간 통신 (0) | 2025.01.24 |
---|---|
[스프링 3팀] 10장. 유효성 검사와 예외처리 ~ 11장. 액추에이터 활용하기 (0) | 2025.01.10 |
[스프링 3팀] 8장. Spring Data JPA 활용 (0) | 2025.01.08 |
[스프링 3팀] 7장. 테스트 코드 작성하기 (0) | 2024.12.27 |
[스프링 3팀] 6장. 데이터베이스 연동 (1) | 2024.11.29 |