JDBC - 음식 주문 정보 저장하기 (트랜잭션 + Bulk Insert와 ID 문제)

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;
}

일단 OrderItems가 들어있는 Orders 객체 생성 로직은 완성해놨다.
두 가지를 구현하면 된다.

  1. Orders 정보 Insert
  2. OrderItems 정보 Insert

이 Insert 묶음은 트랜잭션이 적용돼야 한다.


초기 코드

public class OrderRepository {
    private final DBConfig dbConfig;
    private static final String ORDERS_INSERT_SQL = """
                INSERT INTO orders (user_id, total_price, created_at)
                VALUES
                (?, ?, ?)
                """;

    private static final String ORDER_ITEMS_INSERT_SQL = """
                INSERT INTO order_items (order_id, food_id, price_at_order, quantity)
                VALUES
                (?, ?, ?, ?)
                """;

    public OrderRepository(DBConfig dbConfig) {
        this.dbConfig = dbConfig;
    }

    public Orders save(Orders order) {
        try(Connection conn = DriverManager.getConnection(dbConfig.getUrl(), dbConfig.getUsername(), dbConfig.getPassword());
            PreparedStatement psOrder = conn.prepareStatement(ORDERS_INSERT_SQL, Statement.RETURN_GENERATED_KEYS)) { // Order 삽입 ps
            conn.setAutoCommit(false);

            psOrder.setLong(1, order.getUserId());
            psOrder.setInt(2, order.getTotalPrice());
            psOrder.setObject(3, order.getCreatedAt());

            psOrder.executeUpdate();
            try (ResultSet orderIdResultSet = psOrder.getGeneratedKeys()) { // Order 삽입으로 생성된 키 확인
                orderIdResultSet.next();
                long orderId = orderIdResultSet.getLong(1);
                MyEntityIdInjector.injectId(order, orderId);

                try(PreparedStatement psOrderItems = conn.prepareStatement(ORDER_ITEMS_INSERT_SQL, Statement.RETURN_GENERATED_KEYS)) { // OrderItems 삽입
                    for (OrderItems orderItem : order.getOrderItems()) {
                        psOrderItems.setLong(1, orderId);
                        psOrderItems.setLong(2, orderItem.getFoodId());
                        psOrderItems.setInt(3, orderItem.getPriceAtOrder());
                        psOrderItems.setInt(4, orderItem.getQuantity());

                        psOrderItems.executeUpdate();

                        try (ResultSet orderItemIdResultSet = psOrderItems.getGeneratedKeys()) { // OrderItems의 id, foodId 세팅
                            orderItemIdResultSet.next();
                            long orderItemId = orderItemIdResultSet.getLong(1);
                            MyEntityIdInjector.injectId(orderItem, orderItemId);
                            MyEntityIdInjector.injectId(orderItem, "orderId", orderId);
                        }
                    }

                    conn.commit();
                    return order;
                }
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

}

문제점

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

  1. Orders랑 OrderItems Insert 처리 부분을 분리하면 좋겠다.
  2. 롤백을 catch문에서 명시적으로 해줘야 한다.
  3. OrderItems N번 Insert 쿼리 발생.

Insert 처리 나누기

	public Orders save(Orders order) {
        try(Connection conn = DriverManager.getConnection(dbConfig.getUrl(), dbConfig.getUsername(), dbConfig.getPassword())) { // Order 삽입 ps
            conn.setAutoCommit(false);

			// 메소드로 분리함
            insertOrder(conn, order);
            insertOrderItems(conn, order.getOrderItems(), order.getId());

            conn.commit();
            return order;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    // Order 삽입
    private void insertOrder(Connection conn, Orders order) throws SQLException {
        try(PreparedStatement psOrder = conn.prepareStatement("""
                INSERT INTO orders (user_id, total_price, created_at)
                VALUES
                (?, ?, ?)
                """, Statement.RETURN_GENERATED_KEYS)) {
            ...
        }
    }

    // OrderItems 삽입
    private void insertOrderItems(Connection conn, List<OrderItems> orderItems, long orderId) throws SQLException {
        try (PreparedStatement psOrderItems = conn.prepareStatement("""
                INSERT INTO order_items (order_id, food_id, price_at_order, quantity)
                VALUES
                (?, ?, ?, ?)
                """, Statement.RETURN_GENERATED_KEYS)) {
            ...
        }
    }

이제 좀 트랜잭션에서 뭘 하는지 보인다.

롤백 처리하기

	public Orders save(Orders order) {
        try(Connection conn = DriverManager.getConnection(dbConfig.getUrl(), dbConfig.getUsername(), dbConfig.getPassword())) { // Order 삽입 ps
            conn.setAutoCommit(false);

            try {
                insertOrder(conn, order);
                insertOrderItems(conn, order.getOrderItems(), order.getId());
                conn.commit();
            } catch (SQLException e) {
                conn.rollback(); // 롤백 처리
                throw e;
            }

            return order;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

분리하고 나니까 롤백 커밋도 잘보인다.

Bulk Insert와 ID 문제 (JPA 같이...)

지금 JPA 처럼 save를 구현하고 있는데 JPA는 ID가 있어야 객체를 관리할 수 있다.
그래서 saveAll을 호출할 때 JPA는 Insert문을 하나 날리고 ID 하나 받아오고를 반복해서 객체에 ID를 넣어준다.

Insert ... values에 여러 값을 한 번에 넣는다면 ID를 모두 받아올 수 있지만 순서를 보장하지 않기 때문에 JPA에서도 어쩔 수 없는 것 같다.

내 글 - JPA IDENTITY 방식은 Batch Insert가 비활성화 된다.

OrderItems N번 Insert 쿼리 발생은 그냥 이대로 둔다. JPA처럼 만든다.


이 글에선 테스트 코드는 생략

주문의 저장을 확인하기 위해선 findById가 필요하다.
findById는 orders 테이블과 order_items 테이블의 조인이 필요하기 때문에 다음 글로 넘긴다.


느낀점

  1. ps.getGeneratedKeys()하고 next()를 호출하는것을 여러 번 누락했다.
  2. try-catch 너무 귀찮고 힘들다. 코드가 거부감이 느껴진다.
  3. 같은 코드가 너무 반복된다.
  4. 너무 반복해서 JDBC가 익숙해지고 있다.
profile
개발 기록 아카이브

0개의 댓글