[TIL] 230522 <Spring> JPA 한 걸음 더 나아가기
- -
[Entity 연관 관계]
주문 APP DB table 설계 및 연관 관계
고객 (users) 테이블
create table users ( id bigint not null auto_increment, name varchar(255), primary key (id) );
음식 (food) 테이블
create table food ( id bigint not null auto_increment, name varchar(255), price float(53) not null, primary key (id) );
주문 (orders) 테이블
create table orders ( id bigint not null auto_increment, user_id bigint, food_id bigint, order_date date, primary key (id) ); alter table orders add constraint orders_user_fk foreign key (user_id) references users (id); alter table orders add constraint orders_food_fk foreign key (food_id) references food (id);
- 고객 : 음식 = N : M 관계
- N : M 관계인 테이블들의 연관 관계를 해결하기 위해 orders 테이블처럼 중간 테이블을 사용가능.
- 고객 : 주문 = 1 : N
- 음식 : 주문 = 1 : N
Entity간의 연관 관계
양방향 관계
@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; }
@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<>(); }
- 한명의 고객은 여러번 주문이 가능한 상황입니다.
- 이를 Entity에서 여러번 가능함을 표현하기 위해 Java 컬렉션을 사용하여 List<Food> foodList = new ArrayList<>() 이처럼 표현가능
- DB 테이블에서는 고객 테이블 기준으로 음식의 정보를 조회하려고 할 때 JOIN을 사용하여 바로 조회가 가능하지만
고객 Entity 입장에서는 음식 Entity의 정보를 가지고 있지 않으면 음식의 정보를 조회할 방법이 없음- 따라서 DB 테이블에 실제 컬럼으로 존재하지는 않지만 Entity 상태에서 다른 Entity를 참조하기 위해 이러한 방법을 사용
- 현재 음식 Entity와 고객 Entity는 서로를 참조
단방향 관계
@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; }
@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; }
- 음식 Entity에서만 고객 Entity를 참조가능
- 고객 Entity에는 음식 Entity의 정보가 없기 때문에 음식 정보를 조회불가
- <정리>
- DB 테이블에서는 테이블 사이의 연관관계를 FK(외래 키)로 맺을 수 있고 방향 상관없이 조회가 가능
- Entity에서는 상대 Entity를 참조하여 Entity 사이의 연관관계를 맺을 수 있음
- 하지만 상대 Entity를 참조하지 않고 있다면 상대 Entity를 조회할 수 있는 방법이 없음
- 따라서 Entity에서는 DB 테이블에는 없는 방향의 개념이 존재
[1 대 1 관계]
@OneToOne
1 대 1 관계에서는 외래 키의 주인을 직접 지정해야함
외래 키 주인만이 외래 키 를 등록, 수정, 삭제할 수 있으며, 주인이 아닌 쪽은 오직 외래 키를 읽기만 가능!
- @JoinColumn()은 외래 키의 주인이 활용하는 애너테이션
- 컬럼명, null 여부, unique 여부 등을 지정가능
양방향 관계

- 양방향 관계에서 외래 키의 주인을 지정해 줄 때 mappedBy 옵션을 사용
- mappedBy의 속성값은 외래 키의 주인인 상대 Entity의 필드명을 의미
- 단방향이라면 외래 키의 주인만! 상대 Entity 타입의 필드를 가지면서 @JoinColumn()을 활용하여 외래 키의 속성을 설정
- 양방향이라면 외래 키의 주인은 상대 Entity 타입의 필드를 가지면서 @JoinColumn()을 활용하여 외래 키의 속성을 설정해주고
상대 Entity는 외래 키의 주인 Entity 타입의 필드를 가지면서 mappedBy 옵션을 사용하여 속성 값으로 외래 키의 주인 Entity에 선언된 @JoinColumn()으로 설정되고 있는 필드명을 넣어주면됨
※ 주의할 점
- 외래 키의 주인 Entity에서 @JoinColumn() 애너테이션을 사용하지 않아도 default 옵션이 적용되기 때문에 생략이 가능하지만
1 대 N 관계에서 외래 키의 주인 Entity가 @JoinColumn() 애너테이션을 생략한다면 JPA가 외래 키를 저장할 컬럼을 파악할 수가 없어서 의도하지 않은 중간 테이블이 생성되기 때문에 생략 안하는게 좋음 - 양방향 관계에서 mappedBy 옵션을 생략할 경우 JPA가 외래 키의 주인 Entity를 파악할 수가 없어 의도하지 않은 중간 테이블이 생성되기 때문에 반드시 설정
연습하기
외래 키의 주인이 음식 Entity인 경우
entity>Food
@Entity @Getter @Setter @Table(name = "food") public class Food { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private double price; @OneToOne @JoinColumn(name = "user_id") private User user; }
entity>User
@Entity @Getter @Setter @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // 양방향 @OneToOne(mappedBy = "user") private Food food; // 외래 키의 주인이 아닌 User에서 Food를 저장하기 위함 public void addFood(Food food) { this.food = food; food.setUser(this); // 외래 키(연관 관계) 설정 } }
repository>FoodRepository
public interface FoodRepository extends JpaRepository<Food,Long> { }
repository>UserRepository
public interface UserRepository extends JpaRepository<User,Long> { }
OneToOneTest
@Transactional @SpringBootTest public class OneToOneTest { @Autowired UserRepository userRepository; @Autowired FoodRepository foodRepository; @Test @Rollback(value = false) // 테스트에서는 @Transactional 에 의해 자동 rollback 됨으로 false 설정해준다. @DisplayName("1대1 단방향 테스트") void test1() { User user = new User(); user.setName("Robbie"); // 외래 키의 주인인 Food Entity user 필드에 user 객체를 추가해 줍니다. Food food = new Food(); food.setName("후라이드 치킨"); food.setPrice(15000); food.setUser(user); // 외래 키(연관 관계) 설정 userRepository.save(user); foodRepository.save(food); } @Test @Rollback(value = false) @DisplayName("1대1 양방향 테스트 : 외래 키 저장 실패") void test2() { Food food = new Food(); food.setName("고구마 피자"); food.setPrice(30000); // 외래 키의 주인이 아닌 User 에서 Food 를 저장해보겠습니다. User user = new User(); user.setName("Robbie"); user.setFood(food); userRepository.save(user); foodRepository.save(food); // 확인해 보시면 user_id 값이 들어가 있지 않은 것을 확인가능! } @Test @Rollback(value = false) @DisplayName("1대1 양방향 테스트 : 외래 키 저장 실패 -> 성공") void test3() { Food food = new Food(); food.setName("고구마 피자"); food.setPrice(30000); // 외래 키의 주인이 아닌 User 에서 Food 를 저장하기 위해 addFood() 메서드 추가 // 외래 키(연관 관계) 설정 food.setUser(this); 추가 User user = new User(); user.setName("Robbie"); user.addFood(food); userRepository.save(user); foodRepository.save(food); } @Test @Rollback(value = false) @DisplayName("1대1 양방향 테스트") void test4() { User user = new User(); user.setName("Robbert"); Food food = new Food(); food.setName("고구마 피자"); food.setPrice(30000); food.setUser(user); // 외래 키(연관 관계) 설정 userRepository.save(user); foodRepository.save(food); } @Test @DisplayName("1대1 조회 : Food 기준 user 정보 조회") void test5() { Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new); // 음식 정보 조회 System.out.println("food.getName() = " + food.getName()); // 음식을 주문한 고객 정보 조회 System.out.println("food.getUser().getName() = " + food.getUser().getName()); } @Test @DisplayName("1대1 조회 : User 기준 food 정보 조회") void test6() { User user = userRepository.findById(1L).orElseThrow(NullPointerException::new); // 고객 정보 조회 System.out.println("user.getName() = " + user.getName()); // 해당 고객이 주문한 음식 정보 조회 Food food = user.getFood(); System.out.println("food.getName() = " + food.getName()); System.out.println("food.getPrice() = " + food.getPrice()); } }
[N 대 1 관계]
@ManyToOne
양방향 관계

연습하기
외래 키의 주인이 음식 Entity인 경우
entity>Food
@Entity @Getter @Setter @Table(name = "food") public class Food { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private double price; @ManyToOne //N대1 관계 @JoinColumn(name = "user_id") private User user; }
entity>User
@Entity @Getter @Setter @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<>(); // 외래 키의 주인이 아닌 User 에서 Food 를 쉽게 저장하기 위함 public void addFoodList(Food food) { this.foodList.add(food); food.setUser(this); // 외래 키(연관 관계) 설정 } }
ManyToOneTest
@Transactional @SpringBootTest public class ManyToOneTest { @Autowired UserRepository userRepository; @Autowired FoodRepository foodRepository; @Test @Rollback(value = false) @DisplayName("N대1 단방향 테스트") void test1() { User user = new User(); user.setName("Robbie"); Food food = new Food(); food.setName("후라이드 치킨"); food.setPrice(15000); food.setUser(user); // 외래 키(연관 관계) 설정 Food food2 = new Food(); food2.setName("양념 치킨"); food2.setPrice(20000); food2.setUser(user); // 외래 키(연관 관계) 설정 userRepository.save(user); foodRepository.save(food); foodRepository.save(food2); } @Test @Rollback(value = false) @DisplayName("N대1 양방향 테스트 : 외래 키 저장 실패") void test2() { Food food = new Food(); food.setName("후라이드 치킨"); food.setPrice(15000); Food food2 = new Food(); food2.setName("양념 치킨"); food2.setPrice(20000); // 외래 키의 주인이 아닌 User 에서 Food 를 저장해보겠습니다. User user = new User(); user.setName("Robbie"); user.getFoodList().add(food); user.getFoodList().add(food2); userRepository.save(user); foodRepository.save(food); foodRepository.save(food2); // 확인해 보시면 user_id 값이 들어가 있지 않은 것을 확인하실 수 있습니다. } @Test @Rollback(value = false) @DisplayName("N대1 양방향 테스트 : 외래 키 저장 실패 -> 성공") void test3() { Food food = new Food(); food.setName("후라이드 치킨"); food.setPrice(15000); Food food2 = new Food(); food2.setName("양념 치킨"); food2.setPrice(20000); // 외래 키의 주인이 아닌 User 에서 Food 를 쉽게 저장하기 위해 addFoodList() 메서드 생성하고 // 해당 메서드에 외래 키(연관 관계) 설정 food.setUser(this); 추가 User user = new User(); user.setName("Robbie"); user.addFoodList(food); user.addFoodList(food2); userRepository.save(user); foodRepository.save(food); foodRepository.save(food2); } @Test @Rollback(value = false) @DisplayName("N대1 양방향 테스트") void test4() { User user = new User(); user.setName("Robbert"); Food food = new Food(); food.setName("고구마 피자"); food.setPrice(30000); food.setUser(user); // 외래 키(연관 관계) 설정 Food food2 = new Food(); food2.setName("아보카도 피자"); food2.setPrice(50000); food2.setUser(user); // 외래 키(연관 관계) 설정 userRepository.save(user); foodRepository.save(food); foodRepository.save(food2); } @Test @DisplayName("N대1 조회 : Food 기준 user 정보 조회") void test5() { Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new); // 음식 정보 조회 System.out.println("food.getName() = " + food.getName()); // 음식을 주문한 고객 정보 조회 System.out.println("food.getUser().getName() = " + food.getUser().getName()); } @Test @DisplayName("N대1 조회 : User 기준 food 정보 조회") void test6() { User user = userRepository.findById(1L).orElseThrow(NullPointerException::new); // 고객 정보 조회 System.out.println("user.getName() = " + user.getName()); // 해당 고객이 주문한 음식 정보 조회 List<Food> foodList = user.getFoodList(); for (Food food : foodList) { System.out.println("food.getName() = " + food.getName()); System.out.println("food.getPrice() = " + food.getPrice()); } } }
[1 대 N 관계]
@OneToMany
단방향 관계

- 외래 키를 관리하는 주인은 음식 Entity이지만 실제 외래 키는 고객 Entity가 가지고 있음
- 1 : N에서 N 관계의 테이블이 외래 키를 가질 수 있기 때문에 외래 키는 N 관계인 users 테이블에 외래 키 컬럼을 만들어 추가하지만 외래 키의 주인인 음식 Entity를 통해 관리
- 외래 키를 음식 Entity가 직접 가질 수 있다면 INSERT 발생 시 한번에 처리할 수 있지만 실제 DB에서 외래 키를 고객 테이블이 가지고 있기 때문에 추가적인 UPDATE가 발생된다는 단점
- 1 대 N 관계에서는 일반적으로 양방향 관계가 존재하지 않음!!!
- 1 대 N 관계에서 양방향 관계를 맺으려면 음식 Entity를 외래 키의 주인으로 정해주기 위해 고객 Entity에서 mappedBy 옵션을 사용해야 하지만 @ManyToOne 애너테이션은 mappedBy 속성을 제공하지 않음
- N 관계의 Entity인 고객 Entity에서 @JoinColum의 insertable 과 updatable 옵션을 false로 설정하여 양쪽으로 JOIN 설정을 하면 양방향처럼 설정할 수는 있긴 함
@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; }
연습하기
entity>Food
@Entity @Getter @Setter @Table(name = "food") public class Food { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private double price; @OneToMany //1대N 관계 @JoinColumn(name = "food_id") // users 테이블에 food_id 컬럼 private List<User> userList = new ArrayList<>(); }
entity>User
@Entity @Getter @Setter @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; }
OneToManyTest
@Transactional @SpringBootTest public class OneToManyTest { @Autowired UserRepository userRepository; @Autowired FoodRepository foodRepository; @Test @Rollback(value = false) @DisplayName("1대N 단방향 테스트") void test1() { User user = new User(); user.setName("Robbie"); User user2 = new User(); user2.setName("Robbert"); Food food = new Food(); food.setName("후라이드 치킨"); food.setPrice(15000); food.getUserList().add(user); // 외래 키(연관 관계) 설정 food.getUserList().add(user2); // 외래 키(연관 관계) 설정 userRepository.save(user); userRepository.save(user2); foodRepository.save(food); // 추가적인 UPDATE 쿼리 발생을 확인할 수 있습니다. } @Test @DisplayName("1대N 조회 테스트") void test2() { Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new); System.out.println("food.getName() = " + food.getName()); // 해당 음식을 주문한 고객 정보 조회 List<User> userList = food.getUserList(); for (User user : userList) { System.out.println("user.getName() = " + user.getName()); } } }
[N 대 M 관계]
@ManyToMany
양방향 관계

연습하기
entity>Food
@Entity @Getter @Setter @Table(name = "food") public class Food { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private double price; @ManyToMany //N대M 관계 @JoinTable(name = "orders", // 중간 테이블 생성 joinColumns = @JoinColumn(name = "food_id"), // 현재 위치인 Food Entity 에서 중간 테이블로 조인할 컬럼 설정 inverseJoinColumns = @JoinColumn(name = "user_id")) // 반대 위치인 User Entity 에서 중간 테이블로 조인할 컬럼 설정 private List<User> userList = new ArrayList<>(); // 양방향 // addUserList() 메서드를 생성해 user 정보를 추가 public void addUserList(User user) { this.userList.add(user); // 외래 키(연관 관계) 설정 user.getFoodList().add(this); //user 객체에 food 정보를 추가 } }
entity>User
@Entity @Getter @Setter @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<>(); // 외래 키의 주인이 아닌 User 에서 Food 를 쉽게 저장하기 위함 public void addFoodList(Food food) { this.foodList.add(food); food.getUserList().add(this); // 외래 키(연관 관계) 설정 } }
ManyToManyTest
@Transactional @SpringBootTest public class ManyToManyTest { @Autowired UserRepository userRepository; @Autowired FoodRepository foodRepository; @Test @Rollback(value = false) @DisplayName("N대M 단방향 테스트") void test1() { User user = new User(); user.setName("Robbie"); User user2 = new User(); user2.setName("Robbert"); Food food = new Food(); food.setName("후라이드 치킨"); food.setPrice(15000); food.getUserList().add(user); food.getUserList().add(user2); userRepository.save(user); userRepository.save(user2); foodRepository.save(food); // 자동으로 중간 테이블 orders 가 create 되고 insert 됨을 확인할 수 있습니다. } @Test @Rollback(value = false) @DisplayName("N대M 양방향 테스트 : 외래 키 저장 실패") void test2() { Food food = new Food(); food.setName("후라이드 치킨"); food.setPrice(15000); Food food2 = new Food(); food2.setName("양념 치킨"); food2.setPrice(20000); // 외래 키의 주인이 아닌 User 에서 Food 를 저장해보겠습니다. User user = new User(); user.setName("Robbie"); user.getFoodList().add(food); user.getFoodList().add(food2); userRepository.save(user); foodRepository.save(food); foodRepository.save(food2); // 확인해 보시면 orders 테이블에 food_id, user_id 값이 들어가 있지 않은 것을 확인하실 수 있습니다. } @Test @Rollback(value = false) @DisplayName("N대M 양방향 테스트 : 외래 키 저장 실패 -> 성공") void test3() { Food food = new Food(); food.setName("후라이드 치킨"); food.setPrice(15000); Food food2 = new Food(); food2.setName("양념 치킨"); food2.setPrice(20000); // 외래 키의 주인이 아닌 User 에서 Food 를 쉽게 저장하기 위해 addFoodList() 메서드를 생성해서 사용합니다. // 외래 키(연관 관계) 설정을 위해 Food 에서 userList 를 호출해 user 객체 자신을 add 합니다. User user = new User(); user.setName("Robbie"); user.addFoodList(food); user.addFoodList(food2); userRepository.save(user); foodRepository.save(food); foodRepository.save(food2); } @Test @Rollback(value = false) @DisplayName("N대M 양방향 테스트") void test4() { User user = new User(); user.setName("Robbie"); User user2 = new User(); user2.setName("Robbert"); Food food = new Food(); food.setName("아보카도 피자"); food.setPrice(50000); food.getUserList().add(user); // 외래 키(연관 관계) 설정 food.getUserList().add(user2); // 외래 키(연관 관계) 설정 Food food2 = new Food(); food2.setName("고구마 피자"); food2.setPrice(30000); food2.getUserList().add(user); // 외래 키(연관 관계) 설정 userRepository.save(user); userRepository.save(user2); foodRepository.save(food); foodRepository.save(food2); // User 를 통해 food 의 정보 조회 System.out.println("user.getName() = " + user.getName()); List<Food> foodList = user.getFoodList(); for (Food f : foodList) { System.out.println("f.getName() = " + f.getName()); System.out.println("f.getPrice() = " + f.getPrice()); } // 외래 키의 주인이 아닌 User 객체에 Food 의 정보를 넣어주지 않아도 DB 저장에는 문제가 없지만 // 이처럼 User 를 사용하여 food 의 정보를 조회할 수는 없습니다. } @Test @Rollback(value = false) @DisplayName("N대M 양방향 테스트 : 객체와 양방향의 장점 활용") void test5() { User user = new User(); user.setName("Robbie"); User user2 = new User(); user2.setName("Robbert"); // addUserList() 메서드를 생성해 user 정보를 추가하고 // 해당 메서드에 객체 활용을 위해 user 객체에 food 정보를 추가하는 코드를 추가합니다. user.getFoodList().add(this); Food food = new Food(); food.setName("아보카도 피자"); food.setPrice(50000); food.addUserList(user); food.addUserList(user2); Food food2 = new Food(); food2.setName("고구마 피자"); food2.setPrice(30000); food2.addUserList(user); userRepository.save(user); userRepository.save(user2); foodRepository.save(food); foodRepository.save(food2); // User 를 통해 food 의 정보 조회 System.out.println("user.getName() = " + user.getName()); List<Food> foodList = user.getFoodList(); for (Food f : foodList) { System.out.println("f.getName() = " + f.getName()); System.out.println("f.getPrice() = " + f.getPrice()); } } @Test @DisplayName("N대M 조회 : Food 기준 user 정보 조회") void test6() { Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new); // 음식 정보 조회 System.out.println("food.getName() = " + food.getName()); // 음식을 주문한 고객 정보 조회 List<User> userList = food.getUserList(); for (User user : userList) { System.out.println("user.getName() = " + user.getName()); } } @Test @DisplayName("N대M 조회 : User 기준 food 정보 조회") void test7() { User user = userRepository.findById(1L).orElseThrow(NullPointerException::new); // 고객 정보 조회 System.out.println("user.getName() = " + user.getName()); // 해당 고객이 주문한 음식 정보 조회 List<Food> foodList = user.getFoodList(); for (Food food : foodList) { System.out.println("food.getName() = " + food.getName()); System.out.println("food.getPrice() = " + food.getPrice()); } } }
중간 테이블
중간 테이블 orders를 직접 생성하여 관리하면 변경 발생 시 컨트롤하기 쉽기 때문에 확장성에 좋음

entity>Food
@Entity @Getter @Setter @Table(name = "food") public class Food { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private double price; @OneToMany(mappedBy = "food") private List<Order> orderList = new ArrayList<>(); }
entity>User
@Entity @Getter @Setter @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToMany(mappedBy = "user") private List<Order> orderList = new ArrayList<>(); }
entity>Order
@Entity @Getter @Setter @Table(name = "orders") public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "food_id") private Food food; @ManyToOne @JoinColumn(name = "user_id") private User user; }
OrderTest
@Transactional @SpringBootTest public class OrderTest { @Autowired UserRepository userRepository; @Autowired FoodRepository foodRepository; @Autowired OrderRepository orderRepository; @Test @Rollback(value = false) @DisplayName("중간 테이블 Order Entity 테스트") void test1() { User user = new User(); user.setName("Robbie"); Food food = new Food(); food.setName("후라이드 치킨"); food.setPrice(15000); // 주문 저장 Order order = new Order(); order.setUser(user); // 외래 키(연관 관계) 설정 order.setFood(food); // 외래 키(연관 관계) 설정 userRepository.save(user); foodRepository.save(food); orderRepository.save(order); } @Test @DisplayName("중간 테이블 Order Entity 조회") void test2() { // 1번 주문 조회 Order order = orderRepository.findById(1L).orElseThrow(NullPointerException::new); // order 객체를 사용하여 고객 정보 조회 User user = order.getUser(); System.out.println("user.getName() = " + user.getName()); // order 객체를 사용하여 음식 정보 조회 Food food = order.getFood(); System.out.println("food.getName() = " + food.getName()); System.out.println("food.getPrice() = " + food.getPrice()); } }
중간 테이블 Order에 주문일 컬럼 추가
@Entity @Getter @Setter @Table(name = "orders") @EntityListeners(AuditingEntityListener.class) public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "food_id") private Food food; @ManyToOne @JoinColumn(name = "user_id") private User user; @CreatedDate @Temporal(TemporalType.TIMESTAMP) private LocalDateTime orderDate; }
JpaAdvanceApplication

[지연 로딩]
지연 로딩과 즉시 로딩
- JPA는 연관관계가 설정된 Entity의 정보를 바로 가져올지, 필요할 때 가져올지 방법을 정하게되는데 Fetch Type이라 부름
- 종류에는 2가지가 있는데 하나는 LAZY, 다른 하나는 EAGER
- LAZY는 지연 로딩으로 필요한 시점에 정보를 가져옴
- EAGER는 즉시 로딩으로 이름의 뜻처럼 조회할 때 연관된 모든 Entity의 정보를 즉시 가져옴
- 기본적으로 @OneToMany 애너테이션은 Fetch Type의 default 값이 LAZY,
반대로 @ManyToOne 애너테이션은 EAGER로 지정되어 있음 - 애너테이션 이름에서 뒤쪽에 Many가 붙어있으면 설정된 해당 필드가 Java 컬렉션 타입이니 해당 Entity의 정보가 여러 개 들어있을 수 있다는 것을 의미하므로
-
- 따라서 효율적으로 정보를 조회하기 위해 지연 로딩이 default로 설정되어있음
-
- 반대로 이름 뒤쪽이 One일 경우 해당 Entity 정보가 한 개만 들어오기 때문에 즉시 정보를 가져와도 무리가 없어 즉시 로딩이 default로 설정되어있음
음식 테이블과 고객 테이블이 N : 1 양방향 관계라 가정
FetchTypeTest
@SpringBootTest public class FetchTypeTest { @Autowired UserRepository userRepository; @Autowired FoodRepository foodRepository; @Test @Transactional @Rollback(value = false) void init() { List<User> userList = new ArrayList<>(); User user1 = new User(); user1.setName("Robbie"); userList.add(user1); User user2 = new User(); user2.setName("Robbert"); userList.add(user2); userRepository.saveAll(userList); List<Food> foodList = new ArrayList<>(); Food food1 = new Food(); food1.setName("고구마 피자"); food1.setPrice(30000); food1.setUser(user1); // 외래 키(연관 관계) 설정 foodList.add(food1); Food food2 = new Food(); food2.setName("아보카도 피자"); food2.setPrice(50000); food2.setUser(user1); // 외래 키(연관 관계) 설정 foodList.add(food2); Food food3 = new Food(); food3.setName("후라이드 치킨"); food3.setPrice(15000); food3.setUser(user1); // 외래 키(연관 관계) 설정 foodList.add(food3); Food food4 = new Food(); food4.setName("후라이드 치킨"); food4.setPrice(15000); food4.setUser(user2); // 외래 키(연관 관계) 설정 foodList.add(food4); Food food5 = new Food(); food5.setName("고구마 피자"); food5.setPrice(30000); food5.setUser(user2); // 외래 키(연관 관계) 설정 foodList.add(food5); foodRepository.saveAll(foodList); } @Test @DisplayName("아보카도 피자 조회") void test1() { Food food = foodRepository.findById(2L).orElseThrow(NullPointerException::new); System.out.println("food.getName() = " + food.getName()); System.out.println("food.getPrice() = " + food.getPrice()); System.out.println("아보카도 피자를 주문한 회원 정보 조회"); System.out.println("food.getUser().getName() = " + food.getUser().getName()); } @Test @Transactional @DisplayName("Robbie 고객 조회") void test2() { User user = userRepository.findByName("Robbie"); System.out.println("user.getName() = " + user.getName()); System.out.println("Robbie가 주문한 음식 이름 조회"); for (Food food : user.getFoodList()) { System.out.println(food.getName()); } } @Test @DisplayName("Robbie 고객 조회 실패") void test3() { User user = userRepository.findByName("Robbie"); System.out.println("user.getName() = " + user.getName()); System.out.println("Robbie가 주문한 음식 이름 조회"); for (Food food : user.getFoodList()) { System.out.println(food.getName()); } } }
UserRepository
public interface UserRepository extends JpaRepository<User,Long> { User findByName(String name); }
▶ test1() 실행 시

ㄴ “아보카도 피자”의 가격을 조회하려고 했을 뿐인데 자동으로 JOIN 문을 사용하여 연관관계가 설정되어있는 고객 테이블의 정보도 가져오고 있음
▶ test2() 실행 시

- 이번에는 Robbie 고객을 조회한 후 Robbie 고객이 주문한 음식들의 이름을 조회했음
- @OneToMany 즉, default가 지연 로딩으로 설정되어있기 때문에 우선 고객을 조회한 후
user.getFoodList() 호출 즉, 주문한 음식의 정보가 필요한 시점에 음식 테이블에 해당 고객 Entity의 식별자 값을 사용하여 Select SQL이 수행되었음
영속성 컨텍스트와 지연 로딩
- 영속성 컨텍스트의 기능
- 1차 캐시
- 쓰기 지연 저장소
- 변경 감지
- 지연 로딩
- 따라서 지연 로딩된 Entity의 정보를 조회하려고 할 때는 반드시 영속성 컨텍스트가 존재해야함!
- ‘영속성 컨텍스트가 존재해야한다’라는 의미는 결국 ‘트랜잭션이 적용되어있어야 한다’라는 의미와 동일!
▶ test3() 실행 시

- ‘Robbie 고객 조회 실패’ 테스트 코드를 확인해보시면 @Transactional이 test3() 메서드에 설정되어있지 않음
- 즉, 트랜잭션이 적용되지 않았기 때문에 지연 로딩된 음식 Entity 정보들을 user.getFoodList() 즉, 필요한 시점에 조회하려고 하자 오류가 발생한 것임!
- 따라서 지연 로딩된 정보를 조회하려고 할 때는 반드시 트랜잭션이 적용되어 영속성 컨텍스트가 존재하는지를 확인해야함!
[영속성 전이]
CASCADE : PERSIST
음식 테이블과 고객 테이블이 N : 1 양방향 관계라 가정
entity>User
@Entity @Getter @Setter @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<>(); public void addFoodList(Food food) { this.foodList.add(food); food.setUser(this);// 외래 키(연관 관계) 설정 } }
CascadeTest
@SpringBootTest public class CascadeTest { @Autowired UserRepository userRepository; @Autowired FoodRepository foodRepository; @Test @DisplayName("Robbie 음식 주문") void test1() { // 고객 Robbie 가 후라이드 치킨과 양념 치킨을 주문합니다. User user = new User(); user.setName("Robbie"); // 후라이드 치킨 주문 Food food = new Food(); food.setName("후라이드 치킨"); food.setPrice(15000); user.addFoodList(food); Food food2 = new Food(); food2.setName("양념 치킨"); food2.setPrice(20000); user.addFoodList(food2); userRepository.save(user); foodRepository.save(food); foodRepository.save(food2); } }
- Robbie가 음식을 주문하기 위해서는 위처럼 user, food, food2 모두 직접 save() 메서드를 호출하면서 영속화해야하는데
JPA에서는 이를 간편하게 처리할 수 있는 방법으로 영속성 전이(CASCADE)의 PERSIST 옵션을 제공!!!- 영속성 전이 : 영속 상태의 Entity에서 수행되는 작업들이 연관된 Entity까지 전파되는 상황
- 영속성 전이를 적용하여 해당 Entity를 저장할 때 연관된 Entity까지 자동으로 저장하기 위해서는 자동으로 저장하려고 하는 연관된 Entity에 추가한 연관관계 애너테이션에 CASCADE의 PERSIST 옵션을 설정!
▼ ▼ ▼ ▼ ▼
entity>User
@Entity @Getter @Setter @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST) private List<Food> foodList = new ArrayList<>(); public void addFoodList(Food food) { this.foodList.add(food); food.setUser(this);// 외래 키(연관 관계) 설정 } }
ㄴ 고객 Entity의 @OneToMany 애너테이션에 영속성 전이를 적용해서 음식 Entity도 자동으로 저장될 수 있도록 만들었음
CascadeTest
@Test @DisplayName("영속성 전이 저장") void test2() { // 고객 Robbie 가 후라이드 치킨과 양념 치킨을 주문합니다. User user = new User(); user.setName("Robbie"); // 후라이드 치킨 주문 Food food = new Food(); food.setName("후라이드 치킨"); food.setPrice(15000); user.addFoodList(food); Food food2 = new Food(); food2.setName("양념 치킨"); food2.setPrice(20000); user.addFoodList(food2); userRepository.save(user); }
ㄴ CASCADE 설정을 적용했기 때문에 직접 음식 Entity 객체 food, food2를 영속 상태로 만들지 않아도 자동으로 저장
CASCADE : REMOVE
사용자가 주문 APP을 탈퇴하려고 하는데 주문한 음식 정보들까지 모두 삭제하도록 하려면?
entity>User
@Entity @Getter @Setter @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<>(); public void addFoodList(Food food) { this.foodList.add(food); food.setUser(this);// 외래 키(연관 관계) 설정 } }
CascadeTest
@Test @Transactional @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()); // Robbie 탈퇴 userRepository.delete(user); }
ㄴ 주문한 음식 데이터를 삭제하기 위해서 지연 로딩된 음식 Entity들을 가져와 직접 삭제해준 후 Robbie 고객의 Entity를 삭제
- JPA에서는 이를 간편하게 처리할 수 있는 방법으로 영속성 전이(CASCADE)의 REMOVE 옵션을 제공
▼ ▼ ▼ ▼ ▼
entity>User
@Entity @Getter @Setter @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToMany(mappedBy = "user", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) private List<Food> foodList = new ArrayList<>(); public void addFoodList(Food food) { this.foodList.add(food); food.setUser(this);// 외래 키(연관 관계) 설정 } }
- cascade = {CascadeType.PERSIST, CascadeType.REMOVE} 이렇게 중복으로 옵션 설정 가능
ㄴ 고객 Entity의 @OneToMany 애너테이션에 연관된 음식 Entity도 자동으로 삭제될 수 있도록 REMOVE 옵션을 추가
CascadeTest
@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); }
ㄴ Robbie 고객 Entity 객체를 조회한 후 해당 객체를 delete 하자 자동으로 연관된 음식 데이터들이 삭제됨!
[고아 Entity 삭제]
- CASCADE의 REMOVE 옵션을 적용하면 해당 Entity 객체를 삭제 했을 때 연관된 Entity 객체들을 자동으로 삭제할 수 있었음
- 하지만 REMOVE 옵션 같은 경우, 연관된 Entity와 관계를 제거했다고 해서 자동으로 해당 Entity가 삭제 되지는 않음!!!
OrphanTest
@Test @Transactional @Rollback(value = false) @DisplayName("연관관계 제거") void test1() { // 고객 Robbie 를 조회합니다. User user = userRepository.findByName("Robbie"); System.out.println("user.getName() = " + user.getName()); // 연관된 음식 Entity 제거 : 후라이드 치킨 Food chicken = null; for (Food food : user.getFoodList()) { if(food.getName().equals("후라이드 치킨")) { chicken = food; } } if(chicken != null) { user.getFoodList().remove(chicken); } // 연관관계 제거 확인 for (Food food : user.getFoodList()) { System.out.println("food.getName() = " + food.getName()); } }
ㄴ 후라이드 치킨 Entity 객체와 연관관계를 제거했지만 Delete SQL이 수행되지 않음 (테이블 보니 후라이드 치킨 안지워짐)
- JPA에서는 이를 간편하게 처리할 수 있는 방법으로 orphanRemoval 옵션을 제공
▼ ▼ ▼ ▼ ▼
orphanRemoval
entity>User
@Entity @Getter @Setter @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true) private List<Food> foodList = new ArrayList<>(); public void addFoodList(Food food) { this.foodList.add(food); food.setUser(this);// 외래 키(연관 관계) 설정 } }
- Delete SQL이 수행되어 후라이드 치킨 데이터가 삭제된 것을 확인할 수 있음!
- orphanRemoval 옵션도 REMOVE 옵션과 마찬가지로 해당 Entity 즉, Robbie Entity 객체를 삭제하면 연관된 음식 Entity들이 자동으로 삭제됨!
※ 주의할 점
ㄴ orphanRemoval이나 REMOVE 옵션을 사용할 때 삭제하려고 하는 연관된 Entity를 다른 곳에서 참조하고 있는지 아닌지를 꼭 확인해야함
- A와 B에 참조되고 있던 C를, B를 삭제하면서 같이 삭제하게 되면, A는 참조하고 있던 C가 사라졌기 때문에 문제가 발생
- 따라서 orphanRemoval 같은 경우 @ManyToOne 같은 애너테이션에서는 사용불가!!!
- ManyToOne이 설정된 Entity는 해당 Entity 객체를 참조하는 다른 Entity 객체들이 있을 수 있기 때문에 속성으로 orphanRemoval를 가지고 있지 않음
소중한 공감 감사합니다