인프런 커뮤니티 질문&답변

빛혜원님의 프로필 이미지
빛혜원

작성한 질문수

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발

지연 로딩 관련 에러 발생합니다.

작성

·

268

0

기존 강의에다가 JWT token + security 를 적용하여 프로젝트를 하고 있는데 ,

failed to lazily initialize a collection of role: study.wonyshop.user.entity.User.orders, could not initialize proxy - no Session

오류가 발생합니다.

원인을 찾아보니 이 오류는 지연로딩된 컬렉션을 사용하는 도중에 세션이 종료되어서 발생하는 문제입니다. User 엔티티에서 orders 컬렉션을 지연로딩으로 설정하였기 때문에 실제로 컬렉션에 접근할 때 데이터베이스 연결이 필요하며 세션이 닫힌 상태에서는 접근할 수 없습니다.

package study.wonyshop.user.entity;


import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import study.wonyshop.common.TimeStamped;
import study.wonyshop.delivery.Address;
import study.wonyshop.order.entity.Order;
//import study.wonyshop.order.entity.Order;

@Entity
@Table(name = "USERS") //테이블 user 예약어 있어서 사용할 수 없음)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends TimeStamped {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "user_id", nullable = false)
  private Long id;
  @Column(nullable = false, unique = true)
  private String email;

  @Column(nullable = false, unique = true)
  private String nickname;
  @Column(nullable = false)
  private String password;

  @Column(nullable = false)
  private Address address;
  @Setter
  private String profileImage;

  @Column(nullable = false)
  @Enumerated(value = EnumType.STRING)
  private UserRoleEnum role;
  @Column(nullable = false, unique = true)
  private String phoneNumber;
  @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
  private List<Order> orders = new ArrayList<>();
  //todo  즉시로딩 필요함 -> fetch 조인 생각 할것

  /**
   * 결제 수단은 포인트로 가능 ! 하다는 가정
   */
  @Column(nullable = false)
  private int point = 1000;  // 기본 가입 포인트


  private Boolean inUser; // 추후 휴면계정 관리 할때 사용 하기 위함

  @Builder
  public User(String email, String nickname, String password, Address address, String profileImage,
      UserRoleEnum role, String phoneNumber) {
    this.email = email;
    this.nickname = nickname;
    this.password = password;
    this.address = address;
    this.profileImage = profileImage;
    this.role = role;
    this.phoneNumber = phoneNumber;
  }

  // admin , seller 용
  @Builder
  public User(String email, String nickname, String password, Address address, String profileImage,
      UserRoleEnum role, String phoneNumber, int point) {
    this.email = email;
    this.nickname = nickname;
    this.password = password;
    this.address = address;
    this.profileImage = profileImage;
    this.role = role;
    this.phoneNumber = phoneNumber;
    this.point = point;
  }

  /**
   * 소비자가 -> 셀러 에게 지불
   *
   * @param totalPrice
   */
  public void payForOrder(int totalPrice) {
    int restPoint = this.point - totalPrice;
    if (restPoint < 0) {
      throw new IllegalArgumentException("포인트가 부족합니다. 포인트 충전 후 다시 이용해 주세요.");
    }
    this.point = restPoint;
  }

  /**
   * 셀러가 판매해서 받은 돈
   * @param totalPrice
   */
  public void receivePayment(int totalPrice) {
    this.point += totalPrice;
  }

  /**
   * 취소 시 환불
   * @param refundPayment
   */
  public void refundPayment(int refundPayment){
    this.point -= refundPayment;
  }

}
package study.wonyshop.order.entity;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import study.wonyshop.common.TimeStamped;
import study.wonyshop.delivery.Delivery;
import study.wonyshop.delivery.DeliveryStatus;
import study.wonyshop.orderItem.OrderItem;
import study.wonyshop.user.entity.User;

@Entity
@Table(name = "ORDERS")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order extends TimeStamped {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "order_id", nullable = false)
  private Long id;

  @ManyToOne(fetch = FetchType.LAZY) //연관관계 주인
  @JoinColumn(name = "user_id")
  private User user; // 주문자

  @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
  private List<OrderItem> orderItems = new ArrayList<>();

  @OneToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "delivery_id")
  private Delivery delivery; //배송정보

  @Enumerated(EnumType.STRING)
  private OrderStatus status; //ORDER, CANCEL


  private LocalDateTime orderDate;// 주문 날짜


  @Builder
  public Order(User user, Delivery delivery, OrderStatus status, LocalDateTime orderDate) {
    this.user = user;
    this.delivery = delivery;
    this.status = status;
    this.orderDate = orderDate;
  }

  //--- 양방향 연관관계 편의 메서드 ------// 편의메서드는 컨트롤 하는 쪽에 만들어주면 됨
  // 다 쪽이  연관관계 주인으로 값 의 변경사항은 주인쪽에서 함
  // 다 쪽 : 일의 값은 set 으로 저장
  // 일 쪽 : 리스트(다) 를 조회하여 add


  // order : user  = m :1
  public void setUser(User user) {
    this.user = user;
    user.getOrders().add(this);
  }  //이때  이것을 호출 하기전에 세션이 종료되어 에러 발생

  //order : orderItem  = 1: m
  public void addOrderItem(OrderItem orderItem) {
    this.orderItems.add(orderItem);
    orderItem.setOrder(this);
  }

  // order : delivery  = 1: 1
  public void setDelivery(Delivery delivery) {
    this.delivery = delivery;
    delivery.setOrder(this);
  }

  public void setOrderDate(LocalDateTime orderDate) {
    this.orderDate = orderDate;
  }

  public void setStatus(OrderStatus status) {
    this.status = status;

  }

  //===== 생성 메서드 =====//
  //OrderItem... orderItems에서 ...은 가변 인자를 선언하는 부분입니다. 이는 OrderItem 타입의 인자를 0개 이상 받을 수 있다는 의미입니다.
  //Order order = createOrder(user, delivery, item1, item2);
  public static Order createOrder(User user, Delivery delivery, OrderItem... orderItems) {
    Order order = new Order();
    order.setUser(user);
    order.setDelivery(delivery);
    for (OrderItem orderItem : orderItems) {
      order.addOrderItem(orderItem);
    }
    order.setStatus(OrderStatus.ORDER);
    order.setOrderDate(LocalDateTime.now());
    return order;
  }
 //---  비지니스 로직 -------//
  /**
   * 주문취소
   */

  public void cancel(){
    if(delivery.getDeliveryStatus() == DeliveryStatus.COMP ) {
      throw new IllegalStateException(" 이미 배송이 완료된 상품은 취소가 불가능 합니다. "); //Non-cancellable product
    }
    this.setStatus(OrderStatus.CANCEL);
    // 주문 상품을 다 취소 시켜야함
    for(OrderItem orderItem : orderItems){
      orderItem.cancel();
    }
  }
  //=== 조회 로직 =====//
  /**
   * 전체 주문 가격 조회
   */
  public int getTotalPrice(){
    int totalPrice = 0;
    for(OrderItem orderItem :orderItems){
      totalPrice += orderItem.getTotalPrice();
    }
    return totalPrice;
  }

}

 

  @PostMapping("")
  public OrderResponse orderItem(@AuthenticationPrincipal UserDetailsImpl userDetails
      , @RequestBody OrderRequest orderRequest) {
    Long itemId = orderRequest.getItemId();
    int quantity = orderRequest.getQuantity();

    return orderService.orderItem(userDetails.getUser(), itemId, quantity);
  }
@Service
@RequiredArgsConstructor
public class OrderService {

  private final OrderRepository orderRepository;
  private final ItemRepository itemRepository;
  private final UserRepository userRepository;
  private final DeliveryRepository deliveryRepository;

  @Transactional
  public OrderResponse orderItem(User user, Long itemId, int quantity) {
    Item findItem = itemRepository.findById(itemId).orElseThrow(
        () -> new CustomException(ExceptionStatus.NOT_EXIST)
    );
    //userRepository.findByIdWithOrders(user.getId());
    //배송 생성
    Delivery delivery = new Delivery(user.getAddress(), DeliveryStatus.READY);
    // 주문상품 생성
    OrderItem orderItem = OrderItem.createOrderItem(findItem, findItem.getPrice(), quantity);
    // 주문생성

    Order order = Order.createOrder(user, delivery, orderItem);

    deliveryRepository.save(delivery);
    userRepository.save(user);
    orderRepository.save(order);
    return new OrderResponse(order);

  }}

이 상태에서

//userRepository.findByIdWithOrders(user.getId());

이 주석 된 부분에 이처럼 user 객체를 가져올때 오더와 함께 가져오는 페치조인을 사용해봤지만 이번 엔 user가 null 값이라는 에러가 납니다. ㅠㅠㅠ 아무리 머리를 굴려도 ㅠㅠㅠㅠㅠ 해결이 안되네용 ㅠㅠㅠ

페치조인 코드 는 아래와 같이 작성했습니다.

public interface OrderRepository extends JpaRepository<Order,Long> {

@Query("select u from User u join fetch u.orders WHERE u.id = :userId")
  User findByIdWithOrders(@Param("userId")Long id);
}

 

해결방법은

FetchType.EAGER

이걸 적용하면 되는데 eager 적용하지 않고 하는 방법 없을까요 ㅠㅠ

  @OneToMany(mappedBy = "user", cascade = CascadeType.ALL,Fetch = FetchType.EAGER orphanRemoval = true)
  private List<Order> orders = new ArrayList<>();

 

답변 1

0

안녕하세요. 빛혜원님, 공식 서포터즈 y2gcoder입니다.

문제해결하고자 하는 요구사항을 정확하게 모르는 상태로 조심스럽게 답변합니다 :)

orderItem() 메서드에서 user 객체를 파라미터로 받고 계신데 파라미터로 받은 user 객체가 null이라면 말씀하신 문제가 발생할 수 있을 것 같습니다.

//userRepository.findByIdWithOrders(user.getId());

위의 메서드로 불러온 user를 사용하고 싶으시다면

user = userRepository.findByIdWithOrders(user.getId());

이러한 과정이 필요해보입니다!

 

감사합니다.

빛혜원님의 프로필 이미지
빛혜원

작성한 질문수

질문하기