이번 포스팅에서는 JPA에서 Entity 간 연관 관계를 어떻게 표현하는지, DB 테이블과 어떻게 다른지를 정리합니다.
예제는 고객(User) - 음식(Food) - 주문(Order) 앱을 기반으로 설명합니다.
JPA를 제대로 이해하려면, DB 테이블과 Java Entity의 연관 관계 차이를 먼저 알아야 합니다.
| 구분 | DB 테이블 | Java Entity |
|---|---|---|
| 연관 맺기 | 외래 키(FK)로 연결 | 상대 Entity를 필드로 참조 |
| 방향성 | 없음 (양쪽에서 자유롭게 JOIN 가능) | 있음 (참조하지 않으면 조회 불가) |
| 관계 종류 | FK 컬럼 하나로 표현 | 애너테이션으로 명시 |
핵심 포인트
DB는 JOIN만 잘 쓰면 어느 테이블 기준으로든 조회가 가능합니다. (방향 개념 없음)
반면 Entity는 상대 Entity를 필드로 들고 있지 않으면 그쪽 방향으로 조회할 방법이 없습니다.
그래서 Entity에는 단방향 / 양방향이라는 방향의 개념이 존재합니다.
양방향 관계에서는 두 Entity가 서로를 참조합니다. 그런데 실제 FK를 누가 관리할지 지정해야 합니다.
@JoinColumn 사용mappedBy 옵션으로 주인을 가리킵니다일반적으로 N(다) 쪽 Entity가 외래 키의 주인이 됩니다.
@OneToOne한 고객이 하나의 음식만 선택할 수 있는(1:1) 상황을 예로 들어 보겠습니다.
@Entity
@Table(name = "food")
public class Food {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
// food 테이블에 user_id FK 컬럼이 생성됩니다.
@OneToOne
@JoinColumn(name = "user_id")
private User user;
}
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// User는 Food를 참조하지 않으므로 Food를 통해서만 조회 가능
}
@Test
@Rollback(value = false)
@DisplayName("1대1 단방향 테스트")
void test1() {
User user = new User();
user.setName("Grace");
Food food = new Food();
food.setName("후라이드 치킨");
food.setPrice(15000);
food.setUser(user); // 외래 키의 주인인 Food에서 연관 관계 설정
userRepository.save(user);
foodRepository.save(food);
}
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// mappedBy = "user" → Food Entity의 'user' 필드가 FK 주인임을 선언
@OneToOne(mappedBy = "user")
private Food food;
// 편의 메서드: User 쪽에서도 쉽게 연관 관계를 설정할 수 있도록 제공
public void addFood(Food food) {
this.food = food;
food.setUser(this); // 실제 FK 설정은 주인인 Food에서 해야 저장됨
}
}
⚠️ 주의: 외래 키의 주인이 아닌 쪽(User)에서
setFood()만 하면 FK가 저장되지 않습니다.
반드시 FK 주인(Food)의 필드에도 값을 세팅해야 합니다.
이 번거로움을 해결하기 위해 위처럼 편의 메서드를 만들어 사용합니다.
@ManyToOne실제 개발에서 가장 자주 사용하는 관계입니다. 한 고객이 여러 음식을 주문할 수 있고, 반대로 하나의 음식은 하나의 고객에게 속하는(N:1) 상황입니다.
@Entity
@Table(name = "food")
public class Food {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
// food 테이블에 user_id FK 컬럼 생성. N 쪽이 FK를 가집니다.
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 단방향이므로 User는 Food를 모름
}
@Test
@Rollback(value = false)
@DisplayName("N대1 단방향 테스트")
void test1() {
User user = new User();
user.setName("Grace");
Food food = new Food();
food.setName("후라이드 치킨");
food.setPrice(15000);
food.setUser(user); // FK 주인인 Food에서 연관 관계 설정
Food food2 = new Food();
food2.setName("양념 치킨");
food2.setPrice(20000);
food2.setUser(user);
userRepository.save(user);
foodRepository.save(food);
foodRepository.save(food2);
}
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// mappedBy = "user" → FK 주인이 Food의 'user' 필드임을 선언
// 실제 DB 컬럼은 생성되지 않으며, 읽기 전용입니다.
@OneToMany(mappedBy = "user")
private List foodList = new ArrayList<>();
// 편의 메서드
public void addFoodList(Food food) {
this.foodList.add(food);
food.setUser(this); // FK 주인인 Food에도 연관 관계 세팅
}
}
💡 N:1 vs 1:N 선택 기준
@ManyToOne(N:1) 단방향이 가장 기본적이고 권장되는 방식입니다.
User에서도 Food 목록이 필요하다면@OneToMany를 추가해 양방향으로 만들면 됩니다.
@OneToMany1에 해당하는 Entity가 N에 해당하는 Entity의 목록을 관리하는 방식입니다.
구조적으로 약간 어색한 부분이 있어서 N:1 양방향을 더 많이 씁니다.
@Entity
@Table(name = "food")
public class Food {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
// users 테이블에 food_id FK 컬럼을 만들고, Food가 이를 관리
// 단, 실제 FK는 users 테이블에 있어서 INSERT 후 UPDATE 쿼리가 추가 발생함
@OneToMany
@JoinColumn(name = "food_id")
private List userList = new ArrayList<>();
}
⚠️ 1:N 단방향의 단점
FK가 상대(users) 테이블에 존재하는데, 주인은 Food입니다.
따라서 Food를 저장(INSERT)할 때 users 테이블에 대한 UPDATE 쿼리가 추가로 발생합니다.
이 비효율 때문에@ManyToOne양방향을 더 권장합니다.
@ManyToOne에는 mappedBy 속성이 없기 때문에, 1:N 관계의 양방향은 공식 지원되지 않습니다.
억지로 구성하려면 아래처럼 insertable = false, updatable = false를 사용할 수 있지만 권장하지 않습니다.
// User 쪽에서 food_id를 읽기 전용으로만 참조하는 방식 (비권장)
@ManyToOne
@JoinColumn(name = "food_id", insertable = false, updatable = false)
private Food food;
@ManyToMany고객 한 명이 여러 음식을 주문하고, 하나의 음식이 여러 고객에게 주문될 수 있는 관계입니다.
N:M 관계는 중간 테이블이 필요합니다.
@ManyToMany + @JoinTable)@Entity
@Table(name = "food")
public class Food {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@ManyToMany
@JoinTable(
name = "orders", // 중간 테이블 이름
joinColumns = @JoinColumn(name = "food_id"), // Food → 중간 테이블 FK
inverseJoinColumns = @JoinColumn(name = "user_id") // User → 중간 테이블 FK
)
private List userList = new ArrayList<>();
}
⚠️
@ManyToMany의 문제점
JPA가 자동으로 중간 테이블을 생성해주지만, 컬럼을 추가하거나 커스터마이징하기 어렵습니다.
예를 들어 주문 날짜 컬럼을 추가하고 싶어도@ManyToMany로는 불가합니다.
@ManyToMany 대신 중간 테이블을 직접 Entity(Order)로 만들고, Food/User와 각각 N:1 관계로 연결하는 것이 훨씬 유연합니다.
// 중간 테이블을 별도 Entity로 관리
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 음식 : 주문 = 1 : N → Order가 FK 주인
@ManyToOne
@JoinColumn(name = "food_id")
private Food food;
// 고객 : 주문 = 1 : N → Order가 FK 주인
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
// 필요한 컬럼을 자유롭게 추가 가능!
@CreatedDate
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime orderDate;
}
// Food Entity - 주문 목록을 역참조
@OneToMany(mappedBy = "food")
private List orderList = new ArrayList<>();
// User Entity - 주문 목록을 역참조
@OneToMany(mappedBy = "user")
private List orderList = new ArrayList<>();
중간 테이블 저장 테스트
@Test
@Rollback(value = false)
@DisplayName("중간 테이블 Order Entity 테스트")
void test1() {
User user = new User();
user.setName("Grace");
Food food = new Food();
food.setName("후라이드 치킨");
food.setPrice(15000);
// FK 주인인 Order에서 user, food 연관 관계 설정
Order order = new Order();
order.setUser(user);
order.setFood(food);
userRepository.save(user);
foodRepository.save(food);
orderRepository.save(order);
}
| 애너테이션 | 관계 | FK 주인 | 특징 |
|---|---|---|---|
@OneToOne | 1:1 | 직접 지정 필요 | FK 주인에 @JoinColumn, 반대쪽에 mappedBy |
@ManyToOne | N:1 | N 쪽 (현재 Entity) | 실무에서 가장 많이 사용, FK를 자신이 가짐 |
@OneToMany | 1:N | 1 쪽이 N의 FK 관리 | INSERT 후 UPDATE 추가 발생, 단방향만 존재 |
@ManyToMany | N:M | @JoinTable로 설정 | 중간 테이블 자동 생성, 실무에서는 직접 Entity 분리 권장 |
@ManyToMany는 가급적 지양: 중간 테이블을 직접 Entity로 만들어 N:1로 풀어내는 것이 훨씬 유연하고 확장성이 좋습니다.