본문 바로가기
TIL/Spring

23.06.21

by J1-H00N 2023. 6. 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  아래 링크 참조

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/support/CronExpression.html

 

CronExpression (Spring Framework 6.0.10 API)

Determine whether the given string represents a valid cron expression.

docs.spring.io

 

JWT 사용 흐름

  1. Client 가 username, password 로 로그인 성공 시
    1. 서버에서 "로그인 정보" → JWT 로 암호화 (Secret Key 사용)(그림 1 참조)
    2. JWT 를 Client 응답 Header에 전달
      1. 응답 Header 에 아래 형태로 JWT 전달
      2. Authorization: Bearer <JWT>

        ex)
        Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzcGFydGEiLCJVU0VSTkFNRSI6IuultO2DhOydtCIsIlVTRVJfUk9MRSI6IlJPTEVfVVNFUiIsIkVYUCI6MTYxODU1Mzg5OH0.9WTrWxCWx3YvaKZG14khp21fjkU1VjZV4e9VEf05Hok
    3. Client 에서 JWT 저장 (쿠키)
  2. Client 에서 JWT 통해 인증방법
    1. JWT 를 API 요청 시마다 Header 에 포함
      Content-Type: application/json
      Authorization: Bearer <JWT>
      ...
    2. 예) HTTP Headers (그림 2 참조)
    3. Server
      1. Client 가 전달한 JWT 위조 여부 검증 (Secret Key 사용)
      2. JWT 유효기간이 지나지 않았는지 검증
      3. 검증 성공시,
        1. JWT → 에서 사용자 정보를 가져와 확인
        2. ex) GET /api/products : JWT 보낸 사용자의 관심상품 목록 조회

그림 1
그림 2

'TIL > Spring' 카테고리의 다른 글

23.07.11  (0) 2023.07.11
23.07.10  (0) 2023.07.10
23.06.20  (0) 2023.06.20
23.06.19  (0) 2023.06.19
23.06.15  (0) 2023.06.15