JDBC - 음식 주문 정보 조회하기 (조인하기)

Jang990·2026년 1월 23일
public class Orders {
    private Long id;
    private long userId;
    private int totalPrice;
    private LocalDateTime createdAt;

    private List<OrderItems> orderItems;
}

public class OrderItems {
    private Long id;
    private long orderId;
    private long foodId;
    private int priceAtOrder;
    private int quantity;
}

OrdersOrderItems를 조인해서 반환해야 한다.


초기 코드

	public Orders findById(long id) {
        try (Connection conn = DriverManager.getConnection(dbConfig.getUrl(), dbConfig.getUsername(), dbConfig.getPassword());
                PreparedStatement ps = conn.prepareStatement("""
                        SELECT o.id, o.user_id, o.total_price, o.created_at, oi.id, oi.food_id, oi.price_at_order, oi.quantity
                        FROM orders o
                        INNER JOIN order_items oi ON o.id = oi.order_id
                        WHERE o.id = ?
                        """)) {
            ps.setLong(1, id);

            try (ResultSet rs = ps.executeQuery()) {
                Long orderId = null;
                long userId = -1;
                int totalPrice = -1;
                LocalDateTime createdAt = null;
                List<OrderItems> orderItems = new LinkedList<>();

                boolean hasNext = rs.next();
                if(!hasNext)
                    throw new IllegalArgumentException("주문을 찾을 수 없습니다.");

                while (hasNext) {
                    if (orderId == null) {
                        orderId = rs.getLong(1);
                        userId = rs.getLong(2);
                        totalPrice = rs.getInt(3);
                        createdAt = rs.getObject(4, LocalDateTime.class);
                    }

                    long orderItemId = rs.getLong(5);
                    long foodId = rs.getLong(6);
                    int priceAtOrder = rs.getInt(7);
                    int quantity = rs.getInt(8);

                    // 기본 생성자로 생성
                    OrderItems orderItem = createOrderItemWithReflection(orderItemId, orderId, foodId, priceAtOrder, quantity);
                    orderItems.add(orderItem);
                    hasNext = rs.next();
                }

                return createOrderWithReflection(orderId, userId, totalPrice, createdAt, orderItems);
            }
        } catch (SQLException | NoSuchMethodException | InstantiationException | IllegalAccessException |
                 InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    private Orders createOrderWithReflection(Long orderId, long userId, int totalPrice, LocalDateTime createdAt, List<OrderItems> orderItems) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Constructor<Orders> defaultConstructor = Orders.class.getDeclaredConstructor();
        defaultConstructor.setAccessible(true);
        Orders order = defaultConstructor.newInstance();

        MyEntityIdInjector.injectId(order, "id", orderId);
        MyEntityIdInjector.injectId(order, "userId", userId);
        MyEntityIdInjector.injectId(order, "totalPrice", totalPrice);
        MyEntityIdInjector.injectId(order, "createdAt", createdAt);
        MyEntityIdInjector.injectId(order, "orderItems", orderItems);
        return order;
    }

    private OrderItems createOrderItemWithReflection(long orderItemId, Long orderId, long foodId, int priceAtOrder, int quantity) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<OrderItems> defaultConstructor = OrderItems.class.getDeclaredConstructor();
        defaultConstructor.setAccessible(true);
        OrderItems orderItem = defaultConstructor.newInstance();

        MyEntityIdInjector.injectId(orderItem, "id", orderItemId);
        MyEntityIdInjector.injectId(orderItem, "orderId", orderId);
        MyEntityIdInjector.injectId(orderItem, "foodId", foodId);
        MyEntityIdInjector.injectId(orderItem, "priceAtOrder", priceAtOrder);
        MyEntityIdInjector.injectId(orderItem, "quantity", quantity);
        return orderItem;
    }

JPA를 따라하기 위해서 기본 생성자로 생성하고, 리플렉션으로 값을 밀어넣었다.

Orders는 여러 도메인이 몰린다.

그래서 파라미터를 받는 생성자를 public이 아닌 protected로 구성하고 생성해주는 도메인 서비스를 뒀다.
그래서 OrderRepository에서 직접 생성자에 접근하기 힘들었다.

문제점

문제점과 개선점은 다음과 같다.

  1. INNER JOIN을 하면 OrderItem이 없으면 결과가 나오지 않는다. LEFT OUTER JOIN이 맞다. 비즈니스적으로 OrderItem이 없을 수 없다고 해도 아우터 조인이 맞다.
  2. OrderRepository에 리플렉션 코드가 너무 많다. 외부로 빼는게 좋겠다.

LEFT OUTER JOIN

간단하게 sql문만 바꿔주면 된다.

			PreparedStatement ps = conn.prepareStatement("""
                        SELECT o.id, o.user_id, o.total_price, o.created_at, oi.id, oi.food_id, oi.price_at_order, oi.quantity
                        FROM orders o
                        LEFT OUTER JOIN order_items oi ON o.id = oi.order_id
                        WHERE o.id = ?
                        """)

리플렉션 코드를 외부 클래스로 추출

이건 그냥 그대로 두겠다.
뭔가 나중에 코드 자체가 바뀔 거 같다.
만약 계속 반복되고 코드도 안바뀐다면 나중에 따로 추출하겠다.


테스트 코드

class OrderRepositoryTest {
    MySQLConfig mySQLConfig = new MySQLConfig();

    UsersRepository usersRepository = new UsersRepository(mySQLConfig);
    FoodsRepository foodsRepository = new FoodsRepository(mySQLConfig);
    OrderRepository orderRepository = new OrderRepository(mySQLConfig);


    @Test
    void 주문정보_저장_조회_테스트() {
        Users users = new Users("김아무개", 5000);
        usersRepository.save(users);

        Foods foods1 = new Foods("떡볶이", 1000, 10);
        Foods foods2 = new Foods("짬뽕", 2000, 5);
        foodsRepository.save(foods1);
        foodsRepository.save(foods2);

        List<FoodOrders> foodOrders = List.of(
                new FoodOrders(foods1, 10),
                new FoodOrders(foods2, 20)
        );

        OrderService orderService = new OrderService();
        Orders order = orderService.order(users, foodOrders);
        orderRepository.save(order);

        Orders result = orderRepository.findById(order.getId());

        assertEquals(order.getId(), result.getId());
        assertEquals(order.getUserId(), result.getUserId());
        assertEquals(order.getTotalPrice(), result.getTotalPrice());
        assertEquals(order.getCreatedAt().truncatedTo(ChronoUnit.SECONDS), result.getCreatedAt().truncatedTo(ChronoUnit.SECONDS));

        assertEquals(order.getOrderItems().size(), result.getOrderItems().size());
        assertOrderItems(order.getOrderItems().get(0), result.getOrderItems().get(0));
        assertOrderItems(order.getOrderItems().get(1), result.getOrderItems().get(1));
    }

    private static void assertOrderItems(OrderItems order1, OrderItems order2) {
        assertEquals(order1.getId(), order2.getId());
        assertEquals(order1.getOrderId(), order2.getOrderId());
        assertEquals(order1.getFoodId(), order2.getFoodId());
        assertEquals(order1.getPriceAtOrder(), order2.getPriceAtOrder());
        assertEquals(order1.getQuantity(), order2.getQuantity());
    }

}

조회와 저장을 함께 테스트했다.

그 중에 datetime(6)으로 설정한 created_at 필드는 LocalDateTime과 정확도가 달라서 second정도까지만 비교를 했다.

다음과 같이 차이가 나서 그냥 초까지만 비교한다.

Expected :2026-01-23T21:37:45.298090700
Actual   :2026-01-23T21:37:45.298091

느낀점

이거 유지보수 어떡하나.
필드 하나 추가되면 쫘르륵 바뀔텐데.

profile
개발 기록 아카이브

0개의 댓글