Order와 OrderMenu의 양방향을 맺어 줄 matchOrder() 메서드

김현정·2025년 4월 25일
0

문제 배경

Order에서 createOrder를 생성하면서 서비스 로직에서
request에는 원하는 메뉴Id와 수량을 적는 OrderMenu를 json형식으로 받아온다.
그 이후에 거기서 Order 객체를 생성하여 Repository를 통해서 저장하니 로직은 돌아갔는데
여기서 생기는 의문점은 그럼 OrderMenu는 저장을 안하나? 그럼 나중에 조회해올 때 내가 주문한 메뉴, 가격 등을 알 수 없나였다. 역시나 Order 주문 내역을 조회하는 로직을 구현할 때 OrderMenu는 가져오지못하고 빈 리스트 []를 가져오게 되어 createOrder부분에서 request로 들어오는 OrderMenu 리스트를 저장해서 가져오자가 문제 배경이다.

문제 요약

  • OrderMenu리스트는 request로 JSON으로 받아왔지만,
  • 서비스 로직에서는 Order만 생성하고 저장(orderRepository.save(order))
  • 그래서 OrderMenu는 저장 안 되는 것처럼 보임 → 실제로도 order.getOrderMenus()는 [] 빈 리스트가 나옴

1차 위기

List<OrderMenu> orderMenuList = requestDto.getMenus().stream().map(menuDto ->{
                Menu menu = menuRepository.findMenuByIdOrElseThrow(menuDto.getMenuId());
            // OrderMenu 객체를 생성하고, 수량 설정
            OrderMenu orderMenu = new OrderMenu();
            orderMenu.setMenu(menu); // Menu 객체 설정
            orderMenu.setQuantity(menuDto.getQuantity()); // 수량 설정

            return orderMenu;
        }).collect(Collectors.toList());

OrderService에서 requestDto에 들어오는 OrderMenu를 리스트로 저장하기 위해 orderMenuList를 생성한다. requestDto.getMenus()로 메뉴 정보 리스트를 가져온다. stream을 이용하여 menuId로 Menu를 조회한다. 그 뒤에 OrderMenu 객체에 조회한 Menu 객체를 설정한다.
그리고 요청받은 수량 정보를 setQuantity()를 통해 설정해준다.
생성된 OrderMenu객체(메뉴랑 수량)들을 collect(Collectors.toList())를 통해 리스트 형태로 저장한다.

  • 클라이언트 요청 → 메뉴 ID & 수량 → DB에서 Menu 조회 → OrderMenu 생성 → 리스트로 저장
Order order = new Order(totalPrice, generateOrderNumber(), loginUser, orderMenuList)

Order createdOrder = orderRepository.save(order);
    public Order (Long totalPrice,String orderNumber,User user, List<OrderMenu> orderMenus) {
        this.orderMenus = orderMenus;
        this.totalPrice = totalPrice;
        this.user = user;
        this.orderNumber = orderNumber;
        this.orderStatus = OrderStatus.REQUESTED;
    }

그 뒤로 Order 객체를 새로 생성하여 총 가격, 주문번호, 로그인한 유저 정보, 오더메뉴 리스트를 주입해주었다. 주입한 것을 저장해주고 반환해주었다.

그 뒤에 PostMan으로 실행했으나 orderId가 생성되지 않아서 오류가 생성.

그래서 열심히하다가 튜터님께 찾아뵙다.

1차 해결과정

	// Order.java
    public Order (Long totalPrice,String orderNumber,User user, List<OrderMenu> orderMenus) {
        for(OrderMenu orderMenu : orderMenus){
            orderMenu.matchOrder(this);
        }
        this.orderMenus.addAll(orderMenus);
        this.totalPrice = totalPrice;
        this.user = user;
        this.orderNumber = orderNumber;
        this.orderStatus = OrderStatus.REQUESTED;
    }
	// OrderMenu.java
    public void matchOrder(Order order) {
        this.order = order;
    }

Order 생성자에 OrderMenu 객체를 리스트로 받아오긴 했지만 각 OrderMenu의 order 필드는 null 상태로 남아있다. 즉, 양방향 연관관계 중 OrderMenu -> Order 방향이 끊긴 상태

Order 생성자에서 OrderMenu 리스트를 받아올 때, 각 OrderMenu 객체에 order를 설정해주어야 한다. 이를 위해 matchOrder()메소드를 OrderMenu 클래스에 추가하여 Order를 create할 때 새로 생성하는 Order 생성자를 넣어준다.

Order 생성자에서 orderMenus 리스트를 반복하면서 matchOrder(this)를 호출해, 각 OrderMenu 객체의 order 필드를 현재 Order로 설정

2차 위기

// Json
{
    "orderNumber": "DWS0425125527-978107",
    "orderMenus": [
        {
            "id": 1,
            "order": {
                "createdAt": "2025-04-25T12:55:27.628749",
                "updatedAt": "2025-04-25T12:55:27.628749",
                "id": 1,
                "orderNumber": "DWS0425125527-978107",
                "totalPrice": 82000,
                "orderStatus": "REQUESTED",
                "orderMenus": [
                    {
                        "id": 1,
                        "order": {
                            "createdAt": "2025-04-25T12:55:27.628749",
                            "updatedAt": "2025-04-25T12:55:27.628749",
                            "id": 1,
                            "orderNumber": "DWS0425125527-978107",
                            "totalPrice": 82000,
                            "orderStatus": "REQUESTED",
                            "orderMenus": [

OrderMenu가 잘 출력은 되지만 중복되며 반복되는 문제가 발생.
이 문제는 양방향 연관관계 때문에 발생한다고 한다.
Order 객체를 직렬화 할 때 Order가 OrderMenu 객체들을 포함하고, 또 그 OrderMenu 객체들이 다시 Order를 참조하는 구조로 인해 순환참조가 일어나게 되어서 발생함
서로서로 양방향이라 서로 조회하다보니 발생하는 문제라 이걸 DTO를 추가하여 순환을막고 필요한 정보만 직렬화함.

2차 해결과정 - DTO 사용

@Getter
public class OrderMenuResponseDto {
    private final Long id;
    private final String menuName;
    private final int quantity;

    public OrderMenuResponseDto(OrderMenu orderMenu) {
        this.id = orderMenu.getId();
        this.menuName = orderMenu.getMenu().getName();
        this.quantity = orderMenu.getQuantity();
    }
}

응답 전용 DTO를 생성.

@Getter
public class CreatedOrderResponseDto {
    private final String orderNumber;
    private final List<OrderMenuResponseDto> orderMenus;
    private final Long totalPrice;
    private final LocalDateTime createdAt;

    public CreatedOrderResponseDto(Order order) {
        this.orderNumber = order.getOrderNumber();
        this.totalPrice = order.getTotalPrice();
        this.createdAt = order.getCreatedAt();
        this.orderMenus = order.getOrderMenus().stream()
            .map(OrderMenuResponseDto::new)
            .collect(Collectors.toList());
    }
}

CreateOrderResposeDto를 수정

this.orderMenus = order.getOrderMenus().stream().map(OrderMenuResponseDto::new).collect(Collectors.toList());

여기서 OrderMenu 객체들을 그대로 클라이언트로 반환하는 것이 아니라, OrderMenuResponseDto를 만든것으로 반환.
Order와 OrderMenu는 양방향 연관관계를 갖고 있기 때문에, OrderMenu 객체가 Order를 참조하고, Order 객체가 또 OrderMenu를 참조하는 구조이기에 무한 순환이 일어났으니 이걸 방지하기위해서 OrderMenuResponseDto라는 중간다리가 양방향 관계를 끊어주면서 직렬화를 만들어준다.

return new CreatedOrderResponseDto(createdOrder);

그 뒤에 반환을 createdOrder로 해주면 순환하지않고 반환해준다.

기존 방식 vs DTO 변환 방식

  • 기존 방식: Order -> OrderMenu -> Order -> OrderMenu … 순환 참조 발생 가능

  • DTO 변환 방식: Order -> OrderMenuResponseDto -> 필요한 정보만 포함된 OrderMenu 객체들 반환

결말

완성!!! 정말 오래 헤맸지만 연관관계가 이렇게 중요한지 몰랐다.
연관관계에 따라서 양방향 관리를 잘 해줘야겠다고 생각했다.

0개의 댓글