티스토리 뷰

이번 포스팅에서는 Spring Boot 환경에서 JPA와 Criteria API를 활용하여 여러 테이블을 Join 하는 방법을 소개하겠습니다. 특히, Criteria API를 사용하면 동적 쿼리 작성이 간편해지고, 복잡한 쿼리를 객체지향적으로 관리할 수 있습니다.

이번 예제에서는 users, orders, products 테이블을 기준으로 사용자별 주문 내역을 조회하는 과정을 살펴보겠습니다. VO를 활용하여 데이터를 매핑하고, 실무에서 많이 사용하는 Join 예제를 통해 보다 효과적으로 JPA의 활용 방법을 익히실 수 있습니다.

JPA를 처음 사용하시는 분이나 Criteria API를 활용해보고 싶은 분들에게 도움이 되길 바랍니다. 그럼 시작해보겠습니다!
전체소스는 Github를 참고해 주세요.

 

1. 테이블 생성

  • MariaDB 기준으로 작성되었습니다.
CREATE TABLE `users` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `email` varchar(255) DEFAULT NULL,
  `created_at` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
)

CREATE TABLE `products` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `description` varchar(255) DEFAULT NULL,
  `price` double NOT NULL,
  `stock` int(11) DEFAULT NULL,
  `created_at` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) 

CREATE TABLE `orders` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL,
  `product_id` bigint(20) NOT NULL,
  `total` double NOT NULL,
  `created_at` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
)

 

 

2. SpringBoot 프로젝트 생성

  • SpringBoot 3.3.4 + Java + Gradle로 작성하였습니다.

 

2.1. build.gradle

dependencies {
    // JPA + MariaDB
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mariadb.jdbc:mariadb-java-client'


    // lombok
    annotationProcessor 'org.projectlombok:lombok'
    compileOnly 'org.projectlombok:lombok'

    // undertow
    implementation 'org.springframework.boot:spring-boot-starter-undertow'
}

configurations {
    configureEach {
        // was tomcat 제외
        exclude module: 'spring-boot-starter-tomcat'
    }
}
  • Criteria API는 JPA의 표준 스펙에 포함되어 있어, 별도의 의존성을 추가하지 않아도 "spring-boot-starter-data-jpa"에 내장되어 있습니다.

 

2.2. application.properties

# 서버 포트 지정
server.port=12345

# 데이터베이스 연결 정보
spring.datasource.url=jdbc:mariadb://localhost:3306/mymall
spring.datasource.username=root
spring.datasource.password=root

# JPA 테이블 관리 옵션
# none : Hibernate가 데이터베이스 스키마를 변경하거나 검증하지 않음[운영단계]
# validate : 엔티티와 데이터베이스 테이블 구조가 일치하는지만 확인[테스트, 운영단계]
# update : 엔티티와 데이터베이스 테이블을 비교하여 필요한 경우 스키마를 업데이트[개발단계]
# create : 애플리케이션 시작 시 기존 테이블을 모두 삭제하고 엔티티 구조에 맞춰 새로 생성[개발단계]
# create-drop : create와 동일하게 애플리케이션 시작 시 테이블을 생성하지만, 애플리케이션 종료 시점에 모든 테이블 삭제[개발단계]
spring.jpa.hibernate.ddl-auto=update

# JPA 쿼리 콘솔 출력 옵션
spring.jpa.show-sql=true
  • 본 예제는 개발단계이므로 "update" 옵션으로 진행합니다.

 

2.3. 테이블 Model 생성

2.3.1. User.java

package com.criteria.criteriaexample.model;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Entity
@Getter
@Setter
@Table(name = "users")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String username;
  private String password;
  private String email;
  private String createdAt;

  @OneToMany(mappedBy = "user")
  private List<Order> orders;

}
  • users 테이블과 맵핑되는 Model입니다.
  • @OneToMany(mappedBy = "user") : User와 Order 엔티티 간의 일대다(One-to-Many) 관계를 나타냅니다.
  • mappedBy = "user"는 Order 엔티티에서 user 필드가 이 관계를 소유하고 있음을 의미합니다.
  • 즉, 한 명의 사용자는 여러 개의 주문을 가질 수 있습니다.

 

2.3.2. Product.java

package com.criteria.criteriaexample.model;

import jakarta.persistence.*;

@Entity
@Table(name = "products")
public class Product {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;
  private String description;
  private Double price;
  private Integer stock;
  private String createdAt;
}
  • products 테이블과 맵핑되는 Model입니다.

 

2.3.3. Order.java

package com.criteria.criteriaexample.model;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name = "orders")
public class Order {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @ManyToOne
  @JoinColumn(name = "user_id")
  private User user;

  @ManyToOne
  @JoinColumn(name = "product_id")
  private Product product;

  private Double total;
  private String createdAt;
}
  • orders 테이블과 맵핑되는 Model입니다.
  • @ManyToOne : 이 필드가 다대일(Many-to-One) 관계를 나타냅니다.
  • @JoinColumn : 이 필드와 데이터베이스의 열 간의 매핑을 정의합니다.

 

2.3.4. UserOrderVO.java

package com.criteria.criteriaexample.vo;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserOrderVO {
  private Long orderId; // 주문번호
  private String userName; // 사용자명
  private String productName; // 상품명
  private String productDesc; // 상품설명
  private Double total; // 총 금액
}
  • 쿼리 결과를 담을 VO을 선언합니다.

 

2.4. Service 작성

2.4.1. UserService.java

package com.criteria.criteriaexample.service;

import com.criteria.criteriaexample.model.Order;
import com.criteria.criteriaexample.model.Product;
import com.criteria.criteriaexample.model.User;
import com.criteria.criteriaexample.vo.UserOrderVO;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class UserService {

  private final EntityManager entityManager;

  public List<UserOrderVO> getUserWithOrders() {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<UserOrderVO> query = cb.createQuery(UserOrderVO.class);

    Root<User> user = query.from(User.class);
    Join<User, Order> order = user.join("orders", JoinType.LEFT);
    Join<Order, Product> product = order.join("product", JoinType.LEFT);

    query.select(cb.construct(
      UserOrderVO.class,
      order.get("id"),
      user.get("username"),
      product.get("name"),
      product.get("description"),
      order.get("total")
    ));

    query.orderBy(cb.asc(order.get("id")));

    return entityManager.createQuery(query).getResultList();
  }

}
  • 사용자의 주문 정보를 조회하여 UserOrderVO 리스트를 반환하는 메서드입니다.
  • User 엔티티의 루트(root) 객체를 생성합니다. 이 객체를 기준으로 쿼리를 작성합니다.

 

2.4.2. UserController.java

package com.criteria.criteriaexample.controller;

import com.criteria.criteriaexample.service.UserService;
import com.criteria.criteriaexample.vo.UserOrderVO;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class UserController {

  private final UserService userService;

  @GetMapping("/users/orders")
  @ResponseBody
  public List<UserOrderVO> getUserWithOrders() {
    return userService.getUserWithOrders();
  }

}
  • "/users/orders" 요청이 들어오면 userService를 호출하여 Json 형태로 반환합니다.

 

3. 실행

요청 : http://localhost:12345/users/orders

응답 : 
[
  {
    "orderId": 1,
    "userName": "일번",
    "productName": "모니터",
    "productDesc": "24인치 모니터",
    "total": 5
  },
  {
    "orderId": 2,
    "userName": "이번",
    "productName": "TV",
    "productDesc": "55인치 벽걸이 TV",
    "total": 2
  },
  {
    "orderId": 3,
    "userName": "삼번",
    "productName": "마우스",
    "productDesc": "찍찍",
    "total": 4
  },
  {
    "orderId": 4,
    "userName": "사번",
    "productName": "스피커",
    "productDesc": "사운드 빵빵",
    "total": 7
  },
  {
    "orderId": 5,
    "userName": "오번",
    "productName": "키보드",
    "productDesc": "무소음",
    "total": 10
  },
  {
    "orderId": 6,
    "userName": "육번",
    "productName": "모니터",
    "productDesc": "24인치 모니터",
    "total": 2
  },
  {
    "orderId": 7,
    "userName": "칠번",
    "productName": "TV",
    "productDesc": "55인치 벽걸이 TV",
    "total": 1
  },
  {
    "orderId": 8,
    "userName": "팔번",
    "productName": "마우스",
    "productDesc": "찍찍",
    "total": 12
  },
  {
    "orderId": 9,
    "userName": "구번",
    "productName": "스피커",
    "productDesc": "사운드 빵빵",
    "total": 3
  },
  {
    "orderId": 10,
    "userName": "십번",
    "productName": "키보드",
    "productDesc": "무소음",
    "total": 5
  }
]

 

감사합니다.

최근에 올라온 글
Total
Today
Yesterday