트러블슈팅

JPA 연관관계 설계 트러블슈팅

sudaruuu 2026. 3. 6. 20:38

🧑‍💻 JPA 연관관계 설계 트러블슈팅

@OneToMany 대신 @ManyToOne 중심으로 구조 수정하기

배달 플랫폼 프로젝트를 진행하면서 Store, Menu, Order, Review 엔티티 간의 연관관계를 설계하는 과정에서 고민이 생겼다.

 

처음에는 식당 기준으로 생각하면 자연스럽게 1:N 관계라고 판단하여 Store 엔티티에 @OneToMany 컬렉션을 두는 방식으로 설계했지만, 튜터님의 피드백을 통해 JPA 연관관계의 주인 개념을 다시 이해하면서 설계를 수정하게 되었다.

 

이번 글에서는 그 과정에서 겪었던 연관관계 설계 트러블슈팅을 정리해보려고 한다.


🚨 문제 상황

배달 플랫폼 프로젝트에서 StoreMenuOrderReview 엔티티 간 연관관계를 설계하는 과정에서


처음에는 하나의 식당(Store)에 여러 개의 메뉴(Menu), 주문(Order), 리뷰(Review)가 속한다고 판단했다.

그래서 Store 엔티티에 @OneToMany를 사용해 컬렉션을 두는 방식으로 설계했다.

@OneToMany(mappedBy = "store")
private List<Menu> menus = new ArrayList<>();

@OneToMany(mappedBy = "store")
private List<Order> orders = new ArrayList<>();

@OneToMany(mappedBy = "store")
private List<Review> reviews = new ArrayList<>();

 

식당을 기준으로 보면 다음 관계가 자연스럽기 때문이다.

  • 하나의 식당은 여러 개의 메뉴를 가진다.
  • 하나의 식당은 여러 개의 주문을 가진다.
  • 하나의 식당은 여러 개의 리뷰를 가진다.

그래서 처음에는 Store 중심의 1:N 구조가 자연스럽다고 생각했다.

 

하지만 튜터님의 피드백과 함께 JPA 연관관계의 주인 개념을 다시 검토하면서 현재 설계 방식에 대한 고민이 생겼다.

특히 Menu의 경우, 얼핏 보면 여러 식당에서 같은 이름의 메뉴를 판매할 수 있기 때문에 공통 메뉴처럼 보일 수 있다.

 

하지만 실제로는 다음과 같은 차이가 존재한다.

  • 가격이 다를 수 있고
  • 재고가 다를 수 있고
  • 판매 상태가 다를 수 있고
  • 설명이나 옵션 구성이 다를 수 있기 때문

예를 들어

  • 1번 식당의 라면 (4,500원 / 재고 10)
  • 2번 식당의 라면 (6,000원 / 재고 3)

이처럼 같은 이름이라도 서로 다른 메뉴 객체로 봐야 한다.

즉, 하나의 메뉴는 반드시 하나의 식당에 속해야 한다.

 

이 관계는 Menu 테이블이 store_id 외래키를 가지는 구조로 표현하는 것이 더 적절했다.


🔍 원인 분석

문제의 핵심은 연관관계를 조회 관점으로만 생각하고, 실제 외래키를 누가 관리하는지까지 고려하지 못한 것이었다.

 

1️⃣ 1:N 관계만 보고 Store 중심으로 설계

 

도메인적으로 보면 다음 관계는 분명 맞다.

  • 하나의 식당은 여러 개의 메뉴를 가진다.
  • 하나의 식당은 여러 개의 주문을 가진다.
  • 하나의 식당은 여러 개의 리뷰를 가진다.

그래서 자연스럽게 Store 엔티티에 @OneToMany를 두었다.

하지만 JPA에서는 관계를 조회하는 방향과 외래키를 관리하는 방향이 다를 수 있다.

 

2️⃣ 외래키를 관리하는 연관관계의 주인은 ManyToOne

 

예를 들어 Menu와 Store 관계를 보면 실제 DB 테이블 구조는 다음과 같다.

menu
- id
- name
- price
- stock
- store_id -- FK
 

외래키 store_id는 menu 테이블에 존재한다.

 

따라서 JPA에서도 Menu 엔티티가 연관관계의 주인이 되는 것이 자연스럽다.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false, name = "store_id")
private Store store;
 

반면 Store의 @OneToMany(mappedBy = "store")는

외래키를 직접 관리하는 쪽이 아니라 양방향 매핑 시 반대편을 조회하기 위한 필드에 가깝다.

 

외래키를 생성, 관리하는 주체로 사용하려는 관점은 적절하지 않았다.

 

3️⃣ 불필요한 양방향 매핑은 복잡도를 높일 수 있음

 

실제로 다음과 같은 문제가 발생할 수 있다.

  • 조회 시 필요 없는 컬렉션까지 의식하게 됨
  • 추후 N+1 문제를 유발할 가능성이 높아짐

현재 프로젝트에서는 우선 ManyToOne 단방향으로 설계하는 편이 더 안정적이다.


🛠 해결 과정

Step 1. 연관관계의 주인을 다시 정의

실제 외래키를 가지는 Menu, Order, Review 쪽에서
@ManyToOne으로 Store를 참조하도록 수정했다.

public class Menu {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(nullable = false, name = "store_id")
    private Store store;
public class Order {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(nullable = false, name = "store_id")
    private Store store;
}
public class Review {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(nullable = false, name = "store_id",)
    private Store store;
}

 

이렇게 수정함으로써 각 엔티티가 어떤 식당에 속하는지 명확하게 표현할 수 있게 되었다.

 

Step 2. Store의 @OneToMany 컬렉션 제거

 

초기에는 Store 엔티티가 다음과 같은 컬렉션을 가지고 있었다.

@OneToMany(mappedBy = "store")
private List<Menu> menus = new ArrayList<>();

@OneToMany(mappedBy = "store")
private List<Order> orders = new ArrayList<>();

@OneToMany(mappedBy = "store")
private List<Review> reviews = new ArrayList<>();

하지만 현재 요구사항에서는
각 도메인에서 store_id를 기준으로 조회하는 방식이 더 적절하다고 판단하여 해당 컬렉션을 제거했다.

 

양방향 매핑 대신 단방향 ManyToOne 중심 구조로 단순화했다.


 📊 결과

항목 수정 전 수정 후
연관관계 설계 기준 Store 중심의 @OneToMany 컬렉션 설계 FK를 가지는 도메인 중심의
@ManyToOne 설계
연관관계 주인 이해 조회 관점 위주로 판단 외래키 관리 주체 기준으로 판단
메뉴 해석 방식 같은 이름이면 같은 메뉴처럼 인식 가능 식당별로 별개의 메뉴 객체로 분리
구조 복잡도 양방향 컬렉션 관리 필요 단방향 구조로 단순화
유지보수성 연관관계 관리 코드 필요 책임이 명확하고 확장에 유리

💡 회고 및 교훈

1️⃣ 도메인 관계와 JPA 연관관계 주인은 다르다.

  • JPA에서는 실제 외래키를 누가 가지는지를 기준으로 연관관계의 주인을 설정해야 한다.
  • “식당 1개 : 메뉴 여러 개”라는 사실만 보고 OneToMany부터 쓰기보다
  • FK가 어느 테이블에 생기는지 먼저 생각하는 습관이 중요하다는 것을 배웠다.

 

2️⃣ 메뉴는 이름이 아니라 “소속 식당까지 포함해서” 식별해야 한다.

  • 같은 이름의 메뉴라도 식당이 다르면 가격, 재고, 상태가 다르기 때문에
  • 동일한 메뉴가 아니라 다른 메뉴 객체로 봐야 한다.
  • 이 기준을 명확히 잡고 나니
  • Menu → Store 구조가 훨씬 자연스럽게 정리되었고,
  • 이후 주문/리뷰 설계 방향도 함께 일관되게 맞출 수 있었다.