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

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

개발학생 2025. 9. 27. 17:13
반응형

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

 

[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

 

1. 연관관계 매핑 개요

  • 연관관계 매핑: JPA(Java Persistence API)를 통해 객체와 관계형 데이터베이스 간의 관계를 설정하는 핵심 개념
    -> 이를 통해 엔티티 간의 관계를 선언적으로 표현하고, 복잡한 SQL 없이 객체 간 연동을 쉽게 처리
  • RDBMS를 사용할 때는 테이블 하나만 사용해서 애플리케이션의 모든 기능을 구현하는 것은 불가능하며,
    각 도메인에 맞는 테이블을 설계하고 연관관계를 설정해서 조인(Join) 등의 기능을 활용
  • JPA를 사용하는 애플리케이션의 경우 테이블 간 연관관계를 엔티티 간 연관관계로 표현할 수는 있지만,
    객체와 테이블의 성질이 달라 정확히 표현하기는 어려움
    => 이 제약을 보완하면서 연관관계를 매핑하고 사용해야 함 

연관관계 매핑 예시: 상품 테이블과 공급업체 테이블 (공급업체 번호가 겹침)

2. 연관관계 매핑 종류와 방향

1) 연관관계를 맺는 두 엔티티 간 생성 가능한 연관관계의 종류(4가지)

  • One To One: 일대일(1:1)
  • One To Many: 일대다(1:N)
  • Many To One: 다대일(N:1)
  • Many To Many: 다대다(N:M)

2) 연관관계 매핑의 방향

  • 단방향: 두 엔티티의 관계에서 한 쪽의 엔티티만 참조
  • 양방향: 두 엔티티의 관계에서 각 엔티티가 서로의 엔티티를 참조

3) 연관관계 설정 시 생기는 키

  • 한 테이블에서 다른 테이블의 기본값을 외래키로 갖게 되며 이런 관계에서 주인(Owner)이라는 개념 사용
  • 외래키를 가진 테이블이 그 관계의 주인이 되는데, 주인은 외래키를 사용할 수 있으나 상대 엔티티는 읽는 작업만 수행 가능

3. 일대일 매핑 예시

일대일 매핑 예시 - 상품 테이블과 상품 정보 테이블의 일대일 관계

1) 일대일 단방향 매핑(entity 패키지 안에 상품정보 엔티티 작성)

(1) 기존 상품 엔티티와 BaseEntity.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.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() 메서드를 자동 생성하되, name 필드는 출력에서 제외
@ToString(exclude="name")
//엔티티가 매핑될 테이블 이름을 명시적으로 지정 (기본값은 클래스 이름)
@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;

    // 상품 생성 일시와 상품 수정 일시 자동 설정
}
package com.example.demo.jpa.data.entity;

import java.time.LocalDateTime;

import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter // 모든 필드에 대한 getter 메서드 자동 생성
@Setter // 모든 필드에 대한 setter 메서드 자동 생성
@ToString // toString 메서드 자동 생성
@MappedSuperclass // 이 클래스를 상속받는 엔티티 클래스에 필드가 매핑되도록 지정
@EntityListeners(AuditingEntityListener.class) // 생성일-수정일 자동 설정을 위해 Auditing 기능 활성화
public class BaseEntity {

    @CreatedDate // 엔티티가 처음 저장될 때 자동으로 현재 시간이 설정됨
    @Column(updatable = false) // 생성일은 수정되지 않도록 설정
    private LocalDateTime createdAt; // 생성일시

    @LastModifiedDate // 엔티티가 수정될 때 자동으로 현재 시간이 설정됨
    private LocalDateTime updatedAt; // 최종 수정일시
}

(2) 상품정보 엔티티

// 상품 정보 엔티티 작성
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.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Entity
@Table(name="product_detail")
@Getter
@Setter
@NoArgsConstructor
// 부모 클래스의 필드까지 포함해서 toString() 메서드를 생성
@ToString(callSuper=true)
// 부모 클래스의 필드까지 포함해서 equals()와 hashCode()
@EqualsAndHashCode(callSuper=true)
public class ProductDetail extends BaseEntity {
    // 기본 키(PK)로 자동 생성되는 ID
    // GenerationType.IDENTITY 전략을 사용하여 DB에서 자동 증가
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 제품에 대한 설명을 담는 필드
    private String description;

    // Product 엔티티와의 일대일 관계를 나타냄
    // -> 외래키 컬럼 이름은 'product_number'이며, ProductDetail이 관계의 주인
    @OneToOne(optional = false)  // product가 null인 값을 허용하지 않음
    @JoinColumn(name = "product_number")
    private Product product;
}

(3) 상품정보 엔티티 객체 사용을 위한 리포지토리 인터페이스 생성

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

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

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

//이 인터페이스는 ProductDetail 엔티티에 대한 리포지토리 역할을 수행
public interface ProductDetailRepository extends JpaRepository<ProductDetail, Long> {
	 // 이 인터페이스를 통해 다음 기능들을 사용할 수 있음:
	 // - save(), findById(), delete() 등 기본적인 JPA 메서드
	 // - findAll(Predicate predicate) 형태의 QueryDSL 조건 검색
	 // - findByName(String name) 등 커스텀 구현한 메서드 호출 가능
}

(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.ProductDetail;

@SpringBootTest // Spring Boot 테스트 환경을 설정하여 실제 애플리케이션 컨텍스트에서 테스트를 실행
public class ProductDetailRepositoryTest {

	// ProductDetail 엔티티를 위한 JPA 리포지토리 주입
    @Autowired
    ProductDetailRepository productDetailRepository; 
    
    // Product 엔티티를 위한 JPA 리포지토리 주입
    @Autowired
    ProductRepository productRepository; 
    
    @Test
    public void saveAndReadTest1() {
        // Product 객체 생성 및 값 설정
        Product product = new Product();
        product.setName("스프링 부트 JPA"); // 상품 이름 설정
        product.setPrice(5000); // 상품 가격 설정
        product.setStock(500); // 상품 재고 설정
        
        // Product 객체 저장
        productRepository.save(product);
        
        // ProductDetail 객체 생성 및 Product와 연관 설정
        ProductDetail productDetail = new ProductDetail();
        productDetail.setProduct(product); // 연관된 Product 설정
        productDetail.setDescription("스프링 부트와 JPA를 함께 볼 수 있는 책"); // 상세 설명 설정
        
        // ProductDetail 객체 저장
        productDetailRepository.save(productDetail);
        
        // 저장된 ProductDetail의 ID를 기반으로 Product 조회 및 출력
        System.out.println("savedProduct: " + productDetailRepository.findById(
                productDetail.getId()).get().getProduct());
        
        // 저장된 ProductDetail 전체 정보 출력
        System.out.println("savedProductDetail: " + productDetailRepository.findById(
                productDetail.getId()).get());
    }
}

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

Hibernate:
	select
        productdetai0_.id as id1_2_0_,
        productdetai0_.created_at as created_2_2_0_,
        productdetai0_.updated_at as updated_3_2_0_,
        productdetai0_.description as descript4_2_0_,
        productdetai0_.product_number as product_5_2_0_,
        product1_.id as id1_1_1_,
        product1_.created_at as created_2_1_1_,
        product1_.updated_at as updated_3_1_1_,
        product1_.name as name4_1_1_,
        product1_.price as price5_1_1_,
        product1_.stock as stock6_1_1_
    from
        product_detail productdetai0_
    left outer join
        product product1_
    on
        productdetai0_.product_number=product1_.id
    where
        productdetai0_.id=?
반응형