@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
@Table(name = "orders")
public class Order extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
@ToString.Exclude
private User user;
@Column(nullable = true)
private String tid;
@Column(nullable = false)
private int totalPrice; // 총 결제 금액
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private OrderStatus status = OrderStatus.READY;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
@ToString.Exclude
private List<OrderItem> orderItems;
public void setOrderItems(List<OrderItem> orderItems) {
this.orderItems = orderItems;
}
public void setUser(User user) {
this.user = user;
}
@Builder
public Order(User user, int totalPrice) {
this.user = user;
this.totalPrice = totalPrice;
}
// 주문 아이템들 추가 메서드
public void addOrderItems(List<OrderItem> items) {
this.orderItems.addAll(items);
}
// 결제 완료
public void completeOrder() {
this.tid = tid;
this.status = OrderStatus.COMPLETED;
}
// 결제 실패
public void failed() {
this.status = OrderStatus.FAILED;
}
// TID(결제 고유번호) 저장
public void addTid(String tid) {
this.tid = tid;
}
public void cancel() {
this.status = OrderStatus.CANCELED;
}
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "order_item")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "game_id", nullable = false)
private Game game;
@Column(nullable = false)
private int price;
@Builder
public OrderItem(Order order, Game game, int price) {
this.order = order;
this.game = game;
this.price = price;
}
}
@Entity
@Getter
@NoArgsConstructor
@ToString
@Table(name = "game")
public class Game extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 100)
private String name;
@Column(nullable = false, length = 50)
private String developer;
@Column(nullable = false, length = 50)
private String publisher;
@Column(nullable = false, columnDefinition = "TEXT")
private String content;
@Column(nullable = false)
private int price;
@Column(nullable = false, name = "total_price")
private int totalPrice; // 가격 % 할인률
private String pictureUrl;
private int sales = 0;
private int discount;
private boolean onSale; // 판매 중
@Column(nullable = false)
private String releaseDate;
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<GameGenre> genres = new ArrayList<>();
@Builder
public Game(String name, String developer, String publisher, String content, int price, int totalPrice, String pictureUrl, boolean onSale, String releaseDate) {
this.name = name;
this.developer = developer;
this.publisher = publisher;
this.content = content;
this.price = price;
this.totalPrice = totalPrice;
this.pictureUrl = pictureUrl;
this.sales = 0;
this.discount = 0;
this.onSale = onSale;
this.releaseDate = releaseDate;
}
public void discount(int discount) {
this.discount = discount;
this.totalPrice = (int)Math.ceil(price * (1 - discount / 100.0));
}
public void publish() {
this.onSale = true;
}
public void update(String name, String developer, String publisher,
String content, int price, String pictureUrl) {
this.name = name;
this.developer = developer;
this.publisher = publisher;
this.content = content;
this.price = price;
this.totalPrice = price; // 가격 변경 시 같이 변경
this.pictureUrl = pictureUrl;
}
}
게임을 구매하는 스팀과 비슷한 사이트를 만들고 있었다.
게임 - 주문아이템들 - 주문 (1:N) - (N:1) - (N:1) 이런 관계이다.
컨트롤러 단
// 결제 내역 조회
@LoginUser
@GetMapping("/history")
public Response<List<OrderHistoryResponse>> getOrderHistory(@AuthenticationPrincipal User user) {
List<OrderHistoryResponse> history = orderService.getOrderHistory(user);
return Response.success(history);
}
서비스 단
// 결제 내역 조회
@Override
@Transactional(readOnly = true)
// FIXME : N+1 예상됨
public List<OrderHistoryResponse> getOrderHistory(User user) {
List<Order> orders = orderRepository.findByUserOrderByCreatedAtDesc(user);
// List<Order> orders = orderRepository.findOrdersWithItemsByUser(user);
List<OrderHistoryResponse> historyList = new ArrayList<>();
for (Order order : orders) {
historyList.add(OrderHistoryResponse.fromEntity(order));
}
return historyList;
}
여기서 서비스단의 findByUserOrderByCreatedAtDesc() 메서드가 문제였다.
{
"resultCode": "SUCCESS",
"result": [
{
"orderId": 56,
"totalPrice": 106400,
"status": "COMPLETED",
"createdAt": "2025-03-13T14:55:18.457720",
"orderItems": [
{
"gameId": 23,
"gameName": "Hollow Knight",
"price": 16800
},
{
"gameId": 24,
"gameName": "Resident Evil Village",
"price": 59800
},
{
"gameId": 25,
"gameName": "Outlast 2",
"price": 29800
}
]
},
{
"orderId": 54,
"totalPrice": 59000,
"status": "COMPLETED",
"createdAt": "2025-03-13T01:45:01.049520",
"orderItems": [
{
"gameId": 3,
"gameName": "Elden Ring",
"price": 59000
}
]
}
]
}
이와 같은 결과를 도출하려면 Order, Order.id 와 같은 OrderItem을 찾아야하고 OrderItem안에 있는 Game 정보를 가져와야 해서
데이터가 많을 수록 sql도 많아 진다. 왜냐하면 Fetch 전략을 Lazy 전략을 택했기에
Order 쿼리 후 프록시에서 Order안의 OrderItem에 대한 정보 요구가 감지되면 OrderItem을 가져오는 쿼리가 출력 되고 OrderItem에서 Lazy로 걸려있는 Game에 대한 쿼리도 가져와야 하기에 데이터가 많을 수록 추가되는 SQL이 많아진다.
요약) 페치전략 Lazy는 필요할때만 연관 엔티티를 조회하는 방식,
기본적으로 프록시 객체를 반환하고, 실제 데이터가 필요할 때 쿼리를 실행한다.
order가 10개라면 ordertitem이 10번 조회 game 10번 조회 총 21번 쿼리가 돈다
10 + 1(order조회) + 10 + 10 = 21
@Repository
@RequiredArgsConstructor
public class OrderRepositoryCustomImpl implements OrderRepositoryCustom {
private final JPAQueryFactory queryFactory;
QOrder order = QOrder.order;
QOrderItem orderItem = QOrderItem.orderItem;
QGame game = QGame.game;
@Override
public List<Order> findOrdersWithItemsByUser(User user) {
return queryFactory
.selectFrom(order)
.leftJoin(order.orderItems, orderItem).fetchJoin()
.leftJoin(orderItem.game, game).fetchJoin()
.where(order.user.eq(user))
.orderBy(order.createdAt.desc())
.fetch();
}
}
.fetch join()은 연관된 데이터를 한 번의 쿼리로 조인해서 가져오기때문에 추가적인 쿼리가 실행 되지 않는다.
아쉽게도 데이터가 아직 많지는 않아 쿼리성능은 비슷한 것 같다.. 나중엔 인덱스까지 해서 속도 측정을 해보겠다.
ps. @EntityGrapth로 하는 방법도 찾아보았다. 그러나 적용 시 Outer Join을 한다는 점과, .fetchJoin()은 페이징 처리시엔 사용이 불가 하다고 들어, 나중에 페이징 api에서 한번 다뤄보겠다.
'Spring > 프로젝트' 카테고리의 다른 글
연차 신청, 관리 시스템 만들기 #14 완성...? (0) | 2024.10.14 |
---|---|
연차 신청, 관리 시스템 만들기 #13 logback.xml 설정, 로그 cloudwatch로 전달, 디스코드로 알람 설정 (0) | 2024.09.20 |
연차 신청, 관리 시스템 만들기 #12 EC2 스웨거 http -> https (0) | 2024.09.10 |
연차 신청, 관리 시스템 만들기 #11 인프라 구축 (0) | 2024.09.06 |
연차 신청, 관리 시스템 만들기 #10 로그인에 캐시를 써보자 (0) | 2024.07.30 |