23.06.21
1대N 관계
@OneToMany : 1대N 관계를 맺어주는 역할
- 단방향 관계
- 외래 키를 관리하는 주인은 음식 Entity이지만 실제 외래 키는 고객 Entity가 가지고 있습니다
- 1 : N에서 N 관계의 테이블이 외래 키를 가질 수 있기 때문에 외래 키는 N 관계인 users 테이블에 외래 키 컬럼을 만들어 추가하지만 외래 키의 주인인 음식 Entity를 통해 관리합니다
- 외래 키를 음식 Entity가 직접 가질 수 있다면 INSERT 발생 시 한번에 처리할 수 있지만 실제 DB에서 외래 키를 고객 테이블이 가지고 있기 때문에 추가적인 UPDATE가 발생된다는 단점이 존재
@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
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
- 양방향 관계
- 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;
}
N대M 관계
@ManyToMany : N대M 관계를 맺어주는 역할
- 단방향 관계
- N : M 관계를 풀어내기 위해 중간 테이블(orders)을 생성하여 사용해야 한다
- 생성되는 중간 테이블을 컨트롤하기 어렵기 때문에 추후에 중간 테이블의 변경이 발생할 경우 문제가 발생할 가능성이 있다
@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<>();
}
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
N대M 관계에서 생성되는 중간테이블을 관리하기 어렵다는 단점을 해결하기 위해서 중간 테이블을 직접 만들 수 있다.
package com.sparta.jpaadvance.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@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<>();
}
package com.sparta.jpaadvance.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@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<>();
}
package com.sparta.jpaadvance.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@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;
}
여기서는 중간 테이블 Order가 외래키의 주인이 된다.
JPA는 연관관계가 설정된 Entity의 정보를 바로 가져올지, 필요할 때 가져올지 정할 수 있는데, 이때 가져오는 방법을 FetchType이라고 한다.
- LAZY : 지연로딩, 필요한 시점에 정보를 가져옴
- EAGER : 즉시로딩, 조회할 때 연관된 모든 Entity의 정보를 즉시 가져옴
- 기본적으로 @OneToMany은 Fetch Type의 default 값이 LAZY로 지정되어있고 반대로 @ManyToOne은 EAGER로 되어있음
- 애너테이션 이름에서 뒤쪽에 Many가 붙어있으면 설정된 해당 필드가 Java 컬렉션 타입이므로 효율적으로 정보를 조회하기 위해 LAZY가 default
- 반대로 이름 뒤쪽이 One일 경우 해당 Entity 정보가 한 개만 들어오기 때문에 즉시 정보를 가져와도 무리가 없어 EAGER가 default
이때 @ManyToOne의 Fetch Type을 Lazy로 바꾸고 시험해보자.
@Entity
@Getter
@Setter
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
}
@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());
}
이러면 에러가 발생하는데, 이는 영속성 컨텍스트와 연관이 있다.
영속성 컨텍스트의 기능은 1차 캐시, 쓰기 지연 저장소, 변경 감지가 있다고 배웠는데, 지연 로딩도 영속성 켄텍스트의 기능 중 하나이다. 따라서 지연 로딩된 Entity의 정보를 조회하려고 할 때는 반드시 영속성 컨텍스트가 존재해야 한다. 즉, 트랜잭션이 적용되어있어야 한다
@Test
@Transactional
@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());
}
영속성 전이
지금까지는 자료를 저장하기 위해
userRepository.save(user);
foodRepository.save(food);
foodRepository.save(food2);
이와 같이 일일히 .save를 해줘야 했으나, JPA에서는 이를 간편히 할 수 있도록 영속성 전이(CASCADE)의 PERSIST 옵션을 제공한다.
영속성 전이 : 영속 상태의 Entity에서 수행되는 작업들이 연관된 Entity까지 전파되는 상황
영속성 전이를 적용하여 해당 Entity를 저장할 때 연관된 Entity까지 자동으로 저장하기 위해서는 자동으로 저장하려고 하는 연관된 Entity에 추가한 연관관계 애너테이션에 CASCADE의 PERSIST 옵션을 설정하면 된다.
@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);// 외래 키(연관 관계) 설정
}
}
또한 이를 PERSIST외에 REMOVE 등의 기능을 통해 삭제같은 기능도 쉽게 구현이 가능하다.
@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);// 외래 키(연관 관계) 설정
}
}
@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());
}
// Cascade.REMOVE를 사용하지 않으면 이렇게 음식 데이터도 따로 지워줘야 한다.
// 주문한 음식 데이터 삭제
// foodRepository.deleteAll(user.getFoodList());
// Robbie 탈퇴
userRepository.delete(user);
}
CASCADE의 REMOVE 옵션을 적용하면 해당 Entity 객체를 삭제 했을 때 연관된 Entity 객체들을 자동으로 삭제할 수 있었는데, 하지만 REMOVE 옵션 같은 경우 연관된 Entity와 관계를 제거했다고 해서 자동으로 해당 Entity가 삭제 되지는 않는다.
JPA에서는 이를 간편하게 처리할 수 있는 방법으로 orphanRemoval 옵션을 제공
@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);// 외래 키(연관 관계) 설정
}
}
추가로 orphanRemoval 옵션도 REMOVE 옵션과 마찬가지로 해당 Entity 즉, Robbie Entity 객체를 삭제하면 연관된 음식 Entity들이 자동으로 삭제
orphanRemoval이나 REMOVE 옵션을 사용할 때 삭제하려고 하는 연관된 Entity를 다른 곳에서 참조하고 있는지 아닌지를 꼭 확인해야합니다.
A와 B에 참조되고 있던 C를 B를 삭제하면서 같이 삭제하게 되면 A는 참조하고 있던 C가 사라졌기 때문에 문제가 발생할 수 있습니다.
따라서 orphanRemoval 같은 경우 @ManyToOne 같은 애너테이션에서는 사용할 수 없습니다.
ManyToOne이 설정된 Entity는 해당 Entity 객체를 참조하는 다른 Entity 객체들이 있을 수 있기 때문에 속성으로 orphanRemoval를 가지고 있지 않습니다.
@Scheduled(corn = ~~~) : ~~~(특정 시간) 마다 정해진 작업을 수행하도록 만드는 Annotation 아래 링크 참조
CronExpression (Spring Framework 6.0.10 API)
Determine whether the given string represents a valid cron expression.
docs.spring.io
JWT 사용 흐름
- Client 가 username, password 로 로그인 성공 시
- 서버에서 "로그인 정보" → JWT 로 암호화 (Secret Key 사용)(그림 1 참조)
- JWT 를 Client 응답 Header에 전달
- 응답 Header 에 아래 형태로 JWT 전달
- Authorization: Bearer <JWT>
ex)
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzcGFydGEiLCJVU0VSTkFNRSI6IuultO2DhOydtCIsIlVTRVJfUk9MRSI6IlJPTEVfVVNFUiIsIkVYUCI6MTYxODU1Mzg5OH0.9WTrWxCWx3YvaKZG14khp21fjkU1VjZV4e9VEf05Hok
- Client 에서 JWT 저장 (쿠키)
- Client 에서 JWT 통해 인증방법
- JWT 를 API 요청 시마다 Header 에 포함
Content-Type: application/json Authorization: Bearer <JWT> ...
- 예) HTTP Headers (그림 2 참조)
- Server
- Client 가 전달한 JWT 위조 여부 검증 (Secret Key 사용)
- JWT 유효기간이 지나지 않았는지 검증
- 검증 성공시,
- JWT → 에서 사용자 정보를 가져와 확인
- ex) GET /api/products : JWT 보낸 사용자의 관심상품 목록 조회
- JWT 를 API 요청 시마다 Header 에 포함