[멋사 클라우드 5기] Day 24 - Spring Data JPA (2)

2026. 3. 4. 23:34·Learning Log

1. 연관관계 매핑이란?

현실 세계에서 고객(Customer) 과 상품(Product) 은 서로 관계가 있다.

  • 한 고객은 여러 상품을 주문할 수 있다.
  • 한 상품은 여러 고객에게 주문될 수 있다.

이 관계를 Java 객체로 표현하면 참조(Reference)가 되고, DB로 표현하면 외래 키(Foreign Key)가 된다.

연관관계 매핑은 이 두 세계를 JPA 어노테이션으로 연결하는 작업이다.

[Java 세계]                    [DB 세계]
Customer → Product    ↔    customer_id FK
                            product_id FK

2. 연관관계의 종류

연관관계는 4가지로 나뉜다.

종류 어노테이션 예시
다대일 (N:1) @ManyToOne 여러 주문 → 한 고객
일대다 (1:N) @OneToMany 한 고객 → 여러 주문
일대일 (1:1) @OneToOne 고객 → 고객 상세정보
다대다 (N:M) @ManyToMany 여러 고객 ↔ 여러 상품

Customer와 Product의 관계는 다대다(N:M) 다.

한 고객이 여러 상품을 살 수 있고, 한 상품이 여러 고객에게 팔릴 수 있으니까.


3. 핵심 개념 먼저 잡기

방향 (Direction)

연관관계에는 단방향(Unidirectional)과 양방향(Bidirectional)이 있다.

// 단방향: Customer가 Product를 알지만, Product는 Customer를 모른다
class Customer {
    List<Product> products; // Customer → Product (일방통행)
}

class Product {
    // Customer에 대한 참조 없음
}

// 양방향: 서로가 서로를 안다
class Customer {
    List<Product> products; // Customer → Product
}

class Product {
    List<Customer> customers; // Product → Customer
}

DB 테이블은 외래 키 하나로 양쪽 조인이 가능하다.
하지만 Java 객체는 참조가 없으면 반대 방향으로 탐색이 불가능하다.
양방향은 사실 단방향 2개를 연결한 것이라고 이해하면 된다.


연관관계의 주인 (Owner)

양방향 관계에서는 반드시 "누가 외래 키를 관리할 것인가"를 정해야 한다.
이것이 연관관계의 주인(Owner) 이다.

 

규칙:

  • 주인 쪽: 외래 키를 직접 관리한다. mappedBy 속성을 쓰지 않는다.
  • 비주인 쪽: 읽기 전용이다. mappedBy = "상대 클래스의 필드명" 을 반드시 붙여야 한다.
// 주인: Order (orders 테이블에 customer_id FK가 있음)
@ManyToOne
@JoinColumn(name = "customer_id")  // 주인 → @JoinColumn 사용
private Customer customer;

// 비주인: Customer (mappedBy로 주인이 누구인지 알려줌)
@OneToMany(mappedBy = "customer")  // 비주인 → mappedBy 사용
private List<Order> orders;

🚨 흔한 실수: 비주인 쪽에만 값을 세팅하면 DB에 반영이 안 된다. 주인 쪽에 값을 세팅해야 한다.

 


@JoinColumn

@JoinColumn 은 외래 키 컬럼의 이름을 지정하는 어노테이션이다.

@JoinColumn(name = "customer_id")
// → DB에서 외래 키 컬럼 이름이 "customer_id"가 됨

4. 일대다 / 다대일

다대다로 넘어가기 전에, 이해를 돕기 위해 1:N 관계를 먼저 살펴보자.

"한 Customer가 여러 Order(주문)를 가진다" 는 1:N 관계다.

Customer (1) ──────── Order (N)
            1개의 고객이 여러 주문을 가짐
@Entity
public class Customer {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    // 1:N 관계 — Customer가 비주인 (mappedBy 사용)
    @OneToMany(mappedBy = "customer")
    private List<Order> orders = new ArrayList<>();
}

@Entity
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // N:1 관계 — Order가 주인 (@JoinColumn으로 FK 관리)
    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;
}

이 관계에서 DB 테이블은 아래처럼 된다.

[customer 테이블]          [orders 테이블]
┌────┬────────┐           ┌────┬─────────────┐
│ id │ name   │           │ id │ customer_id │  ← FK
├────┼────────┤           ├────┼─────────────┤
│  1 │ 홍길동   │           │  1 │      1      │
│  2 │ 김철수   │           │  2 │      1      │
└────┴────────┘           │  3 │      2      │
                          └────┴─────────────┘

외래 키(customer_id)는 N 쪽(orders 테이블)에 존재한다.

🔑 원칙: 외래 키는 항상 "N 쪽" 테이블에 존재한다.
따라서 연관관계의 주인도 보통 N 쪽(더 구체적인 쪽)이 된다.


5. 다대다 (N:M) — @ManyToMany의 문제

이제 본론이다. Customer와 Product의 관계는 다대다(N:M)다.

Customer (N) ──────── Product (M)

DB에서 N:M은 직접 표현이 불가능하다

관계형 DB는 N:M을 직접 표현할 수 없다.
반드시 중간 테이블(Junction Table / 연결 테이블) 이 필요하다.

[customer 테이블]    [customer_product 테이블]    [product 테이블]
┌────┬────────┐      ┌─────────────┬────────────┐  ┌────┬──────────┐
│ id │ name   │      │ customer_id │ product_id │  │ id │ name     │
├────┼────────┤      ├─────────────┼────────────┤  ├────┼──────────┤
│  1 │ 홍길동   │      │      1      │     1      │  │  1 │ 노트북    │
│  2 │ 김철수   │      │      1      │     2      │  │  2 │ 마우스    │
└────┴────────┘      │      2      │     1      │  └────┴──────────┘
                     └─────────────┴────────────┘

@ManyToMany로 간단히 표현하면?

JPA는 @ManyToMany 어노테이션으로 N:M 관계를 쉽게 표현할 수 있게 해준다.

@Entity
public class Customer {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToMany
    @JoinTable(
        name = "customer_product",             // 중간 테이블 이름
        joinColumns = @JoinColumn(name = "customer_id"),         // 이 엔티티의 FK
        inverseJoinColumns = @JoinColumn(name = "product_id")   // 반대 엔티티의 FK
    )
    private List<Product> products = new ArrayList<>();
}

@Entity
public class Product {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private int price;

    @ManyToMany(mappedBy = "products") // 비주인
    private List<Customer> customers = new ArrayList<>();
}

코드가 간결하고 깔끔해 보인다.
하지만 실무에서는 @ManyToMany를 사용하지 않는다. 왜일까?


@ManyToMany의 치명적인 문제점

문제 1: 중간 테이블에 컬럼을 추가할 수 없다

실제 서비스에서 "주문 내역"에는 주문 날짜, 수량, 가격 등 추가 정보가 필요하다.
하지만 @ManyToMany 가 자동 생성한 중간 테이블에는 외래 키 두 개 외에 컬럼을 추가할 방법이 없다.

[실제로 필요한 테이블]
┌─────────────┬────────────┬──────────────┬──────┬────────┐
│ customer_id │ product_id │ ordered_at   │ qty  │ price  │  ← 이런 컬럼들이 필요!
├─────────────┼────────────┼──────────────┼──────┼────────┤
│      1      │     1      │  2024-01-15  │  2   │ 1200000│
└─────────────┴────────────┴──────────────┴──────┴────────┘

[@ManyToMany가 만드는 테이블]
┌─────────────┬────────────┐
│ customer_id │ product_id │  ← FK 두 개만 존재, 추가 컬럼 불가
└─────────────┴────────────┘

문제 2: 예상치 못한 쿼리가 발생한다

@ManyToMany 는 내부적으로 복잡한 조인 쿼리와 중간 테이블 관련 SQL을 발생시키는데, 개발자가 이를 예측하고 제어하기 어렵다.
이는 성능 문제로 이어지기 쉽다.


6. 올바른 다대다 해결책 — 중간 테이블을 Entity로 승격

실무에서는 @ManyToMany 대신, 중간 테이블을 직접 Entity 클래스로 만드는 방법 을 사용한다.

Customer ↔ Product 사이에 Order (주문) 엔티티를 만드는 것이다.

Customer (1) ──── Order (N:M의 중간) ──── Product (1)
              N                       N

이렇게 하면 Customer와 Order 는 1:N, Product와 Order 도 1:N 관계가 된다.
N:M이 두 개의 1:N으로 분해된다.

전체 구조 설계

[customer]        [orders]                                              [product]
┌────┬──────┐  ┌────┬─────────────┬────────────┬────────────┬──────┐  ┌────┬────────┬────────┐
│ id │ name │  │ id │ customer_id │ product_id │ ordered_at │ qty  │  │ id │ name   │ price  │
└────┴──────┘  └────┴─────────────┴────────────┴────────────┴──────┘  └────┴────────┴────────┘
     1 ──────────────── N                                N ──────────────────── 1

'Learning Log' 카테고리의 다른 글

[멋사 클라우드 5기] Day 26, 27 - Spring Security & JWT  (0) 2026.03.09
[멋사 클라우드 5기] Day 25 - Spring Data JPA (3)  (0) 2026.03.05
[멋사 클라우드 5기] Day 23 - Spring Data JPA (1)  (1) 2026.03.03
[멋사 클라우드 5기] Day 22 - SpringBoot 그리고 웹 서버  (0) 2026.02.27
[멋사 클라우드 5기] Day 21 - 제어의 역전(IoC)과 의존성 주입(DI)  (0) 2026.02.26
'Learning Log' 카테고리의 다른 글
  • [멋사 클라우드 5기] Day 26, 27 - Spring Security & JWT
  • [멋사 클라우드 5기] Day 25 - Spring Data JPA (3)
  • [멋사 클라우드 5기] Day 23 - Spring Data JPA (1)
  • [멋사 클라우드 5기] Day 22 - SpringBoot 그리고 웹 서버
allluck777
allluck777
allluck777
    • 분류 전체보기 (41)
      • AWS (0)
      • Network (0)
      • Linux (0)
      • Docker (0)
      • Project (4)
        • CloudNote (4)
      • Learning Log (34)
      • Lecture (3)
        • 스프링 입문 - 코드로 배우는 스프링 부트, 웹 .. (3)
  • 전체
    오늘
    어제
  • hELLO· Designed By정상우.v4.10.6
allluck777
[멋사 클라우드 5기] Day 24 - Spring Data JPA (2)
상단으로

티스토리툴바