@Transactional의 경우 2개의 save 수행 시 한 개만 오류가 발생해도 모두 Rollback한다. 필요에 따라 달거나 달지 않아도 된다.
@ManyToOne
양방향 참조를 위해 고객 Entity에서 Java 컬렉션을 사용하여 음식 Entity 참조
실제 DB에는 List 형식으로 반영되는 것은 아니다!

// 외래 키의 주인
@Entity
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@ManyToOne
@JoinColumn(name = "user_id")
private User user; // 이 필드 명이 User의 mappedBy에 사용됨
}
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 양방향에서만 아래 추가
@OneToMany(mappedBy = "user")
private List<Food> foodList = new ArrayList<>();
}
mappedBy : 외래키의 주인을 설정. 외래키 주인이 나를 참조할 때 사용하는 필드 명을 적는다!
@OneToMany
외래키를 관리하는 주인 (여기서 음식) / 실제 외래키 (여기서 고객)를 서로 다른 Entity가 가지고 있다.

@Entity
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@OneToMany
@JoinColumn(name = "food_id") // users 테이블에 food_id 컬럼
private List<User> userList = new ArrayList<>();
}
음식 Entity가 외래키를 가지고 있다면 setUser를 통해 쉽게 세팅할 수 있지만, Food 테이블 자체에는 user 칼럼이 없다! 그래서 userList를 통해서 외래키를 관리해야 한다.
외래 키를 음식 Entity가 직접 가질 수 있다면 INSERT 발생 시 한번에 처리할 수 있지만 실제 DB에서 외래 키를 고객 테이블이 가지고 있기 때문에 추가적인 UPDATE가 발생된다는 단점이 존재합니다.
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "food_id", insertable = false, updatable = false)
private Food food;
}
주인이 아닌 쪽에서는 읽기만 할 수 있다고 표현하는 방법이 있다!
ManyToMany를 사용하고, 중간 테이블 생성 시 @joinTable 사용

양쪽의 id를 가지고 있는 중간 테이블을 생성하여 사용한다.
@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 Entity 에서 중간 테이블로 조인할 컬럼 설정
inverseJoinColumns = @JoinColumn(name = "user_id"))
// 반대 위치인 User Entity 에서 중간 테이블로 조인할 컬럼 설정
private List<User> userList = new ArrayList<>();
}
joinColumns : 현재 Entity
inverseJoinColumns : 상대 Entity
중간 테이블의 경우 직접 만든 게 아니기 때문에 PK가 없어서 직접 다루기 힘들다.
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 양방향일 때만 아래 추가
@ManyToMany(mappedBy = "userList") // 외래키 주인의 필드
private List<Food> foodList = new ArrayList<>();
}
JPA 에서는 연관 관계에 있는 Entity의 정보를 바로 가져올지, 필요할 때 가져올지 정할 수 있다.
FetchType.LAZY
OneToMany, ManyToMany의 default
- 애너테이션 이름에서 뒤쪽에 Many가 붙어있으면 설정된 해당 필드가 Java 컬렉션 타입
@ManyToMany(mappedBy = "userList") // 외래키 주인의 필드
private List<Food> foodList = new ArrayList<>();
- 즉, 해당 Entity의 정보가 여러 개 들어있을 수 있다는 것을 의미합니다.
- 따라서 효율적으로 정보를 조회하기 위해
지연 로딩이 default로 설정되어있습니다.
FetchType.EAGER
ManyToOne의 defaultOne의 경우 객체 타입을 가져오므로 즉시 가져와도 무방 > 즉시 로딩
@ManyToOne(fetch = FetchType.LAZY) 등으로 변경이 가능하다.
@Transactional한 Entity 저장할 때 연관된 Entity까지 모두 저장할 수 없을까?
@OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST)
private List<Food> foodList = new ArrayList<>();
user.addFoodList(food2);
// save를 일일히 해주다니 너무 귀찮다..!
// 연관된 Entity까지 모두 저장할 수 없을까? >> User 수정하기
userRepository.save(user);
foodRepository.save(food);
foodRepository.save(food2);
user.addFoodList(food2);
userRepository.save(user);
@OneToMany(mappedBy = "user", cascade = {CascadeType.PERSIST,CascadeType.REMOVE})
private List<Food> foodList = new ArrayList<>();
@Test
@Transactional // 지연 로딩 사용하기 위해서 - OneToMany
@Rollback(value = false)
@DisplayName("Robbie 탈퇴")
void test3() {
// 고객 Robbie 를 조회합니다.
User user = userRepository.findByName("Robbie");
System.out.println("user.getName() = " + user.getName());
// Robbie 가 주문한 음식 조회
for (Food food : user.getFoodList()) {
System.out.println("food.getName() = " + food.getName());
}
// 주문한 음식 데이터 삭제
foodRepository.deleteAll(user.getFoodList()); // 여러개의 List 먼저 모두 지워야
// Robbie 탈퇴
userRepository.delete(user); // 탈퇴 가능
}
@Test
@Transactional
@Rollback(value = false)
@DisplayName("영속성 전이 삭제")
void test4() {
// 고객 Robbie 를 조회합니다.
User user = userRepository.findByName("Robbie");
System.out.println("user.getName() = " + user.getName());
// Robbie 가 주문한 음식 조회
for (Food food : user.getFoodList()) {
System.out.println("food.getName() = " + food.getName());
}
// Robbie 탈퇴
userRepository.delete(user);
}
연관된 데이터까지 모두 삭제 가능하다
💁♂️ 그렇다면 Cascade.REMOVE 와 **orphanRemoval 차이점은 무엇인가?**
Cascade.REMOVE의 경우 일에 해당하는 부모 엔티티를 em.remove를 통해 직접 삭제할 때,그 아래에 있는 다에 해당하는 자식 엔티티들이 삭제되는 것입니다.
orphanRemoval=true는 위 케이스도 포함하며,일에 해당하는 부모 엔티티의 리스트에서 요소를 삭제하기만 해도 해당 다에 해당하는 자식 엔티티가 delete되는 기능까지 포함하고 있다고 이해하시면 됩니다.
즉, orphanRemoval=true 는 리스트 요소로써의 영속성 전이도 해준다는 뜻
@OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true)
private List<Food> foodList = new ArrayList<>();
Cascade = CascadeType.REMOVE의 기능도 가지고 있다.
⚠️ 주의!
- orphanRemoval이나 REMOVE 옵션을 사용할 때 삭제하려고 하는 연관된 Entity를 다른 곳에서 참조하고 있는지 아닌지를 꼭 확인해야합니다.
- A와 B에 참조되고 있던 C를 B를 삭제하면서 같이 삭제하게 되면 A는 참조하고 있던 C가 사라졌기 때문에 문제가 발생할 수 있습니다.
- 따라서 orphanRemoval 같은 경우
@ManyToOne같은 애너테이션에서는 사용할 수 없습니다.
ManyToOne이 설정된 Entity는 해당 Entity 객체를 참조하는 다른 Entity 객체들이 있을 수 있기 때문에 속성으로 orphanRemoval를 가지고 있지 않습니다.