에러 발생 상황
1. 음식점 Respository에서 전체 음식이 담긴 리스트를 가져옴
2. 해당 리스트를 반환하기 위해 stream으로 순회하면서 Restaurant 객체를 DTO 객체로 매핑
3. Repository에서 가져온 음식점 리스트 안에 음식 리스트도 있어서 Food -> FoodDto로 매핑이 필요
(@OneToMany로 음식과 연관관계가 맺어져 있는 상태 -> cascade = Lazy(지연로딩))
4. Restaurant -> DTO 변환 과정 중 Food 리스트도 FoodDto 타입 리스트로 매핑하기 위해 Restaurant 객체의 .getFoods() 메서드 호출
5. LazyInitializationException 발생
Resolved [org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: cohttp://m.order.orderlink.restaurant.domain.Restaurant.foods: could not initialize proxy - no Session]
에러 원인
1. 음식점 Repository에서 전체 음식점 리스트를 가져오기 위해 repository.findAll(); 호출 👉 세션 열림
2. 해당 리스트를 가져옴 👉 세션 닫힘
3. 음식점 리스트를 DTO 객체로 변환하기 위해 stream으로 순회 및 map으로 DTO객체로 매핑
4. 매핑할 때 음식점 객체 내부에 또 음식 리스트가 있어서 해당 리스트의 타입인 음식도 DTO 객체로 매핑하기위해 Food.getFoods(); 호출 -> 음식점과 연관 관계가 맺어진 음식 엔티티
5. Food.getFoods();를 호출할 때 세션은 이미 닫힌 상태인데 db에서 foods 리스트 가져오려고 하니까 LazyInitializationException 발생 👉 처음에 repository.findAll(); 호출했을 때 가져온 음식점 객체 내부의 음식 리스트는 가짜 proxy 객체임(빈 객체)
해결 방법
해당 엔티티의 연관 관계가 맺어진 애너테이션에 cascade의 Fetch 타입을 EAGER로 변경하면 즉시 로딩되어 해당 문제는 해결되지만 음식점을 db에서 가져올 때 원하지 않아도 항상 음식(Foods) 리스트를 즉시 로딩하게 되니까 성능 저하 발생
따라서 repository에서 findAll() 메서드를 호출했을 때 세션이 닫히지 않도록 메서드 레벨에 @Transaction 애너테이션을 달아주면 해당 메서드에서 모든 매핑 과정까지 다 끝나고 종료되는 시점까지 세션이 유지됨.
음식점 Entity(Restaurant)
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "p_restaurants")
@EntityListeners(AuditingEntityListener.class)
public class Restaurant extends BaseTimeEntity {
@Id
@UuidGenerator(style = UuidGenerator.Style.AUTO)
private UUID id;
@Column(name = "name", unique = true, nullable = false)
private String name;
@Column(name = "address", unique = true, nullable = false)
private String address;
@Column(name = "phone", unique = true, nullable = false)
private String phone;
@Column(name = "description")
private String description;
@NotNull
@Column(name = "open_time", nullable = false)
private LocalTime openTime;
@NotNull
@Column(name = "close_time", nullable = false)
private LocalTime closeTime;
@Builder.Default
@Column(name = "business_status", nullable = false)
private boolean businessStatus = true;
@Column(name = "owner_name", nullable = false)
private String ownerName;
@Column(name = "business_reg_num", unique = true, nullable = false)
private String businessRegNum;
@Builder.Default
@Column(name = "avg_rating", nullable = false)
private Double avgRating = 0.0;
@Builder.Default
@Column(name = "rating_sum", nullable = false)
private Double ratingSum = 0.0;
@Builder.Default
@Column(name = "rating_count", nullable = false)
private Integer ratingCount = 0;
@Column(name = "region", nullable = false)
private String region; // 추후 String -> UUID 변경 예정
@Column(name = "categories", nullable = false)
private String categories; // 추후 String -> UUID 변경 예정
@OneToMany(mappedBy = "restaurant")
private List<Food> foods = new ArrayList<>();
}
반환할 타입 : SuccessResponse<RestaurantResponse.GetRestaurants>
RestaurantResponse
@Getter
@Builder
@AllArgsConstructor
public class RestaurantResponse {
@Getter
@Builder
@AllArgsConstructor
public static class GetRestaurants {
private final List<RestaurantDto> restaurants;
}
}
RestaurantDto
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RestaurantDto {
private UUID restaurantId;
private String name;
private String address;
private String phone;
private String description;
private LocalTime openTime;
private LocalTime closeTime;
private boolean businessStatus;
private String ownerName;
private String businessRegNum;
private Double avgRating;
private Double ratingSum;
private Integer ratingCount;
private String region;
private String categories;
private List<RestaurantFoodDto> foods;
}
RestaurantFoodDto
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class RestaurantFoodDto {
private String foodName;
private String foodDescription;
private int price;
private String imageUrl;
}
Service : getAllRestaurants() 👉 전체 음식점 조회 메서드
@Transactional
public RestaurantResponse.GetRestaurants getAllRestaurants() {
List<Restaurant> restaurants = restaurantRepository.findAll();
// Convert : List<Restaurant> -> List<RestaurantDto>
List<RestaurantDto> restaurantDtos = restaurants.stream()
.map(this::convertToRestaurantDto)
.collect(Collectors.toList());
return RestaurantResponse.GetRestaurants.builder()
.restaurants(restaurantDtos)
.build();
}
Restaurant 👉 RequestDto 매핑
// Convert : Restaurant -> RestaurantDto
private RestaurantDto convertToRestaurantDto(Restaurant restaurant) {
LocalTime now = LocalTime.now();
boolean isOpen = now.isAfter(restaurant.getOpenTime()) && now.isBefore(restaurant.getCloseTime());
return RestaurantDto.builder()
.restaurantId(restaurant.getId())
.name(restaurant.getName())
.address(restaurant.getAddress())
.phone(restaurant.getPhone())
.description(restaurant.getDescription())
.openTime(restaurant.getOpenTime())
.closeTime(restaurant.getCloseTime())
.businessStatus(isOpen)
.ownerName(restaurant.getOwnerName())
.businessRegNum(restaurant.getBusinessRegNum())
.avgRating(restaurant.getAvgRating())
.ratingSum(restaurant.getRatingSum())
.ratingCount(restaurant.getRatingCount())
.foods(restaurant.getFoods().stream()
.map(this::convertToRestaurantFoodDto)
.collect(Collectors.toList()))
.build();
}
Food 👉 RestaurantFoodDto 매핑
// Convert : Food -> RestaurantFoodDto
private RestaurantFoodDto convertToRestaurantFoodDto(Food food) {
return RestaurantFoodDto.builder()
.foodName(food.getName())
.foodDescription(food.getDescription())
.price(food.getPrice())
.imageUrl(food.getImageUrl())
.build();
}