🧑💻 JPA 연관관계 설계 트러블슈팅
@OneToMany 대신 @ManyToOne 중심으로 구조 수정하기
배달 플랫폼 프로젝트를 진행하면서 Store, Menu, Order, Review 엔티티 간의 연관관계를 설계하는 과정에서 고민이 생겼다.
처음에는 식당 기준으로 생각하면 자연스럽게 1:N 관계라고 판단하여 Store 엔티티에 @OneToMany 컬렉션을 두는 방식으로 설계했지만, 튜터님의 피드백을 통해 JPA 연관관계의 주인 개념을 다시 이해하면서 설계를 수정하게 되었다.
이번 글에서는 그 과정에서 겪었던 연관관계 설계 트러블슈팅을 정리해보려고 한다.
🚨 문제 상황
배달 플랫폼 프로젝트에서 Store, Menu, Order, Review 엔티티 간 연관관계를 설계하는 과정에서
처음에는 하나의 식당(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 구조가 훨씬 자연스럽게 정리되었고,
- 이후 주문/리뷰 설계 방향도 함께 일관되게 맞출 수 있었다.
'트러블슈팅' 카테고리의 다른 글
| 조회 성능 개선 1단계 - Pageable 적용 (0) | 2026.03.17 |
|---|---|
| 조회 API 응답 속도가 느린 이유 분석 (Postman 테스트 기반) (0) | 2026.03.17 |
| @Transactional(readOnly = true)를 조회 로직에서 사용하는 이유 (0) | 2026.03.11 |
| Spring JPA에서 findById() 예외 처리는 Service에서 해야 할까? Repository에서 해야 할까? (0) | 2026.03.11 |
| GitHub Issue Template이 표시되지 않는 문제 해결 (front-matter 설정) (0) | 2026.03.06 |