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

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

개발학생 2025. 10. 17. 17:35
반응형

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

 

[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

 

1. 다대일 매핑 예시

다대일 매핑 예시 - 상품 테이블과 공급업체 테이블의 관계

1) 다대일 단방향 매핑

(1) 공급업체 테이블에 매핑되는 엔티티 클래스 생성(BaseEntity.java 사용)

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

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
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;
    
    // 업체 생성 일시와 업체 정보 변경 일시 자동 설정
}

(2) 기존 Product.java에, Provider.java와의 다대일 연관관계 설정

// Product.java
package com.example.demo.jpa.data.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

//클래스의 모든 필드에 대해 Getter 메서드를 자동 생성
@Getter
//클래스의 모든 필드에 대해 Setter 메서드를 자동 생성
@Setter
//파라미터가 없는 기본 생성자를 자동 생성
@NoArgsConstructor
//모든 필드를 파라미터로 받는 생성자를 자동 생성
@AllArgsConstructor
//이 클래스는 JPA 엔티티로 데이터베이스 테이블과 매핑
@Entity
// 객체 생성 시 빌더 패턴을 사용할 수 있게 해주는 어노테이션
@Builder
// toString() 메서드를 자동 생성
@ToString
//엔티티가 매핑될 테이블 이름을 명시적으로 지정 (기본값은 클래스 이름)
@Table(name="product")
// BaseEntity를 상속하여, LocalDateTime.now() 메서드로 시간을 주입하지 않아도 자동으로 생성 일시/수정 일시의 값 생성
public class Product 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 number;

    // 엔티티 클래스의 필드는 자동으로 테이블 칼럼으로 매핑되므로, 별다른 설정을 하지 않을 경우 @Column을 생략해도 됨
    // 상품 이름 (Null 허용하지 않음)
    @Column(nullable = false)
    private String name;
    
    // 가격 (Null 허용하지 않음)
    @Column(nullable = false)
    private int price;
    
    // 재고 수량 (Null 허용하지 않음)
    @Column(nullable = false)
    private int stock;

    // 상품 생성 일시와 상품 수정 일시 자동 설정
    
    // ProductDetail과 일대일 양방향 매핑
    @OneToOne
    // 엔티티 간 양방향 관계가 있을 때 toString()을 자동 생성하면 무한 루프가 발생하는 것 방지
    @ToString.Exclude
    private ProductDetail productDetail;
    
    // Provider과 다대일 매핑
    @ManyToOne
    @JoinColumn(name="provider_id")
    // 엔티티 간 양방향 관계가 있을 때 toString()을 자동 생성하면 무한 루프가 발생하는 것 방지
    @ToString.Exclude
    private Provider provider;
}

(3) 공급업체 엔티티를 객체로 사용하기 위한 리포지토리 인터페이스 생성

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

import org.springframework.data.jpa.repository.JpaRepository;

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

//이 인터페이스는 Provider 엔티티에 대한 리포지토리 역할을 수행
public interface ProviderRepository extends JpaRepository<Provider, Long> {

}

(4) 리포지토리 인터페이스에 대한 테스트 코드 작성

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

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 relationshipTest1() {
		// 테스트 데이터 생성
		Provider provider = new Provider();
		provider.setName("ㅇㅇ물산");
		
		providerRepository.save(provider);
		
		Product product = new Product();
		product.setName("가위");
		product.setPrice(4000);
		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());
	}
}

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

  • Hibernate가 SQL을 생성할 때는 엔티티 클래스의 필드 선언 순서기준으로 하되,
    연관 관계 필드(@ManyToOne, @OneToOne 등)별도로 처리하기 때문에 순서가 뒤로 밀릴 있음
select
    product0_.number as number1_0_0_,
    product0_.created_at as created_2_0_0_,
    product0_.updated_at as updated_3_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 created_2_2_1_,
    provider1_.updated_at as updated_3_2_1_,
    provider1_.name as name4_2_1_,
    productdet2_.id as id1_1_2_,
    productdet2_.created_at as created_2_1_2_,
    productdet2_.updated_at as updated_3_1_2_,
    productdet2_.description as descript4_1_2_,
    productdet2_.product_number as product_5_1_2_
from
    product product0_
left outer join
    prodvider provider1_
        on product0_.provider_id=provider1_.id
left outer join
    product_detail productdet2_
        on product0_.number=productdet2_.product_number
where
    product0_.number=?

2) 다대일 양방향 매핑

(1) Provider.java 수정

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

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

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
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", fetch = FetchType.EAGER) // Product 엔티티의 'provider' 필드를 통해 매핑됨. 즉, 외래 키는 Product 쪽에서 관리함. EAGER는 즉시 로딩 방식
    @ToString.Exclude // Lombok의 toString() 메서드에서 이 필드를 제외하여 무한 루프 방지
    private List<Product> productList = new ArrayList<>(); // 제공자가 가진 상품 목록을 저장하는 리스트. Null 방지를 위해 초기화
}

(2) ProviderRepositoryTest.java 메서드 수정

  • Provider 엔티티 클래스는 Product 엔티티와의 연관관계에서 주인이 아니기 때문에 외래키를 관리할 수 없음!
    • 예를 들어, provider.getProductList().add(product1)과 같이 코드를 작성하면 데이터베이스에 반영되지 않음 
package com.example.demo.jpa.data.repository;

import java.util.List;

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 relationshipTest1() {
		// 테스트 데이터 생성
		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(2000);
		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);
		
		// 특정 공급업체(provider)의 상품 목록을 리스트로 가져오기
		List<Product> products = providerRepository.findById(provider.getId()).get()
				.getProductList();
		
		// 위의 상품 목록이 제대로 가져와지는지 확인하기 위해 출력
		for(Product product : products) {			
			System.out.println(product);
		}
	}
}

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

select
    provider0_.id as id1_2_0_,
    provider0_.created_at as created_2_2_0_,
    provider0_.updated_at as updated_3_2_0_,
    provider0_.name as name4_2_0_,
    productlis1_.provider_id as provider7_0_1_,
    productlis1_.number as number1_0_1_,
    productlis1_.number as number1_0_2_,
    productlis1_.created_at as created_2_0_2_,
    productlis1_.updated_at as updated_3_0_2_,
    productlis1_.name as name4_0_2_,
    productlis1_.price as price5_0_2_,
    productlis1_.stock as stock6_0_2_,
    productdet2_.id as id1_1_3_,
    productdet2_.created_at as created_2_1_3_,
    productdet2_.updated_at as updated_3_1_3_,
    productdet2_.description as descript4_1_3_,
    productdet2_.product_number as product_5_1_3_,
from
    provider provider0_
left outer join
    product productlis1_
        on provider0_.id=productlis1_.provider_id
left outer join
    product_detail productdet2_
        on productlis1_.number=productdet2_.product_number
where
    provider0_.id=?

2. 일대다 매핑 예시

일대다 매핑 예시 - 상품 테이블과 상품 분류 테이블의 관계

1) 일대다 단방향 매핑 - category와 product의 조인이 발생해서 상품 데이터를 정상적으로 가져옴

(1) 상품 분류 테이블에 매핑되는 엔티티 클래스 생성

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

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

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

//클래스의 모든 필드에 대해 Getter 메서드를 자동 생성
@Getter
//클래스의 모든 필드에 대해 Setter 메서드를 자동 생성
@Setter
//파라미터가 없는 기본 생성자를 자동 생성
@NoArgsConstructor
//모든 필드를 파라미터로 받는 생성자를 자동 생성
@AllArgsConstructor
//이 클래스는 JPA 엔티티로 데이터베이스 테이블과 매핑
@Entity
//toString() 메서드를 자동 생성
@ToString
//엔티티가 매핑될 테이블 이름을 명시적으로 지정 (기본값은 클래스 이름)
@Table(name="category")
public class Category {
	// 기본 키(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을 생략해도 됨
    // 상품 분류 코드를 unique로 설정하여, 중복되지 않게 함
    @Column(unique=true)
    private String code;
    
    // 상품 분류 이름
    private String name;
    
    // 상품 리스트와 일대다 단방향 매핑
    @OneToMany(fetch = FetchType.EAGER) // 하나의 Category가 여러 개의 Product를 가짐. 즉시 로딩 방식으로 연관된 Product들을 함께 조회함
    @JoinColumn(name="category_id") // Product 테이블에 외래 키 컬럼(category_id)을 생성하여 Category와 연결함
    private List<Product> products = new ArrayList<>(); // Category가 가진 Product 목록을 저장하는 리스트. Null 방지를 위해 초기화함
}

(2) 상품 분류 엔티티를 객체로 사용하기 위한 리포지토리 인터페이스 생성

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

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.demo.jpa.data.entity.Category;

//이 인터페이스는 Category 엔티티에 대한 리포지토리 역할을 수행
public interface CategoryRepository extends JpaRepository<Category, Long>{

}

 

(3) 리포지토리 인터페이스에 대한 테스트 코드 작성

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

import java.util.List;

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.Category;
import com.example.demo.jpa.data.entity.Product;

@SpringBootTest // Spring Boot 테스트 환경을 설정하여 실제 애플리케이션 컨텍스트에서 테스트를 실행
public class CategoryRepositoryTest {
	// Product 엔티티를 위한 JPA 리포지토리 주입
	@Autowired
	ProductRepository productRepository;
	
	// Category 엔티티를 위한 JPA 리포지토리 주입
	@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 객체에 Product를 추가
		category.getProducts().add(product);
		
		categoryRepository.save(category);
		
		// 테스트 코드 
		//Category 엔티티의 ID가 1인 객체를 조회하는 코드
		// :반환 타입은 Optional<Category>이므로 .get()을 사용해 실제 객체를 꺼냄
		//   .get()은 해당 ID가 없을 경우 NoSuchElementException이 발생할 수 있으므로, 
		//   실무에서는 orElseThrow()나 ifPresent() 등을 사용하는 게 안전
		List<Product> products = categoryRepository.findById(1L).get().getProducts();
		
		for(Product foundProduct : products) {
			System.out.println(foundProduct);
		}
	}
}

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

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_.number as number1_1_2_,
    products1_.created_at as created_2_1_2_,
    products1_.updated_at as updated_3_1_2_,
    products1_.name as name4_1_2_,
    products1_.price as price5_1_2_,
    products1_.stock as stock6_1_2_,
    provider2_.id as id1_3_3_,
    provider2_.created_at as created_2_3_3_,
    provider2_.updated_at as updated_3_3_3_,
    provider2_.name as name4_3_3_,
    productdet3_.id as id1_2_4_,
    productdet3_.created_at as created_2_2_4_,
    productdet3_.updated_at as updated_3_2_4_,
    productdet3_.description as descript4_2_4_,
    productdet3_.product_number as product_5_2_4_
from
    category category0_
left outer join
    product products1_
        on category0_.id=products1_.category_id
left outer join
    provider provider2_
        on products1_.provider_id=provider2_.id
left outer join
    product_detail productdet3_
        on products1_.number=productdet3_.product_number
where
    category0_.id=?

2) 일대다 양방향 매핑

  • 해당 교재에서 다루지 않음
    • @OneToMany를 사용하는 입장에서는 어느 엔티티 클래스도 연관관계의 주인이 될 수 없기 때문

3. 다대다 매핑: 실무에서 거의 사용되지 않는 구성

1) 중간 테이블에 추가 컬럼을 넣기 어려움

  • JPA의 기본 다대다 매핑은 단순히 두 테이블을 연결하는 조인 테이블만 자동 생성
  • 하지만 실무에서는 중간 테이블에 추가 정보(예: 등록일, 상태, 권한 등)을 넣어야 하는 경우가 많음
    • 학생(Student)과 강의(Course) 사이의 관계를 나타내는 수강(StudentCourse) 테이블에 수강일, 성적 같은 컬럼이 필요할 수 있는데,  @ManyToMany 매핑으로는 이런 추가 컬럼을 처리할 수 없습니다.

2) 중간 엔티티를 만들지 않으면 제어가 어려움

  • 다대다 매핑은 내부적으로 자동으로 조인 테이블을 관리하지만, 이 테이블을 직접 제어하거나 조회하기 어려움
  • 실무에서는 중간 테이블을 명시적인 엔티티로 분리해서 관리하거나, 다대다를 두 개의 일대다/다대일로 분해
  • 회원과 모임 사이의 관계를 나타내는 참여 테이블에 역할, 참여 상태, 초대 여부 같은 비즈니스 로직이 들어가는 등
    비즈니스 로직이 중간 테이블에 들어가는 경우, 반드시 중간 엔티티를 만들어서 @ManyToOne 관계로 분해

3) 성능 및 유지보수 측면에서 불리

  • 자동 생성된 조인 테이블은 쿼리 최적화나 인덱스 관리가 어려우며, 복잡한 조건으로 조회하기 어려움
  • 또한, 양방향 관계가 많아질수록 무한 루프, 순환 참조, 복잡한 fetch 전략 등으로 인해 유지보수가 어려움

 

 

 

반응형