MVP 모델을 빠르게 만들 때는 Spring Data JPA를 사용하면 대부분의 조회는 JPA에서 제공하는 쿼리 메서드를 통해
충분히 처리 할 수 있습니다 간단한 CRUD 정도를 빠르게 개발하는데 목적이 있기 때문입니다
하지만 프로젝트가 점점 커지고 요구사항이 많아지면서 조회해야 할 데이터가 많아질수록 단순 조회로는 처리할 수 없는 상황이 생깁니다
예를 들어
- 여러가지 테이블을 JOIN 하여 한 번에 조회해야 하는 경우
- 테이블의 여러 데이터를 조합하여 계산해야 하는 경우
- CASE WHNE,GROUP BY,SUM,LIMIT,윈도우 함수 같은 SQL 기능이 필요한 경우
- 성능 최적화를 위해 DB레벨에서 직접 통제해야 하는 경우
이러한 상황에서 Native Query를 사용하면 SQL을 통해 제어할 수 있습니다
1. Native Query

Native Query는 데이터베이스가 실제로 실행하는 순수 SQL을 직접 작성하는 방식입니다
JPA에서 자주 사용되는 JPQL은 엔티티 객체를 기준으로 작성하게 됩니다
테이블명이 아닌 엔티티명과 필드명을 사용합니다
하지만 Native Query는 실제 테이블과 컬럼을 기준으로 작성하게 됩니다
@Query(value = """
SELECT o.order_id, m.name AS member_name, p.product_name, oi.quantity, oi.order_price
FROM orders o
JOIN member m ON o.member_id = m.member_id
JOIN order_item oi ON o.order_id = oi.order_id
JOIN product p ON oi.product_id = p.product_id
WHERE m.member_id = :memberId
ORDER BY o.created_at DESC
""", nativeQuery = true)
List<Object[]> findOrderDetailsByMemberId(@Param("memberId") Long memberId);
여기서 nativeQuery = true 옵션을 통해 JPA는 해당 쿼리를 JPQL이 아닌 실제 SQL로 해석됩니다
2. Native Query 활용
2-1. 복잡한 계산 로직을 DB에서 바로 처리하여 성능 개선을 할 때
예를 들어 쇼핑몰 서비스에서 주문별 최종 결제 금액을 구해야 한다고 가정할 때
최종 금액 계산 규칙이 다음과 같이 설계하여 사용한다고 해봅시다
1. 주문상품 금액 = 주문 수량 X 상품 가격
2. 회원 등급이 VIP면 10% 할인
3. 쿠폰이 있으면 추가 할인
4. 배송비는 총금액이 5만 원 미만일 때만 부과
5. 최종 결제 금액 기준으로 최근 주문 상위 10개 조회
이런 로직을 전부 Java를 통해 계산할 수도 있습니다
하지만 DB에서 원본 데이터를 엔티티로 가져온 뒤 애플리케이션 레벨에서 다시 계산해야 하기 때문에 비효율적입니다
이럴 때는 Native Query를 사용하여 SQL의 CASE WHEN,SUM,GROUP BY 등을 활용하여 DB에서 계산하도록 구현할 수 있습니다
@Query(value = """
SELECT
o.order_id,
m.name AS member_name,
SUM(oi.quantity * oi.order_price) AS total_amount,
CASE
WHEN m.grade = 'VIP' THEN SUM(oi.quantity * oi.order_price) * 0.9
ELSE SUM(oi.quantity * oi.order_price)
END AS discounted_amount
FROM orders o
JOIN member m ON o.member_id = m.member_id
JOIN order_item oi ON o.order_id = oi.order_id
WHERE o.member_id = :memberId
GROUP BY o.order_id, m.name, m.grade
ORDER BY discounted_amount DESC
LIMIT 10
""", nativeQuery = true)
List<Object[]> findTopOrdersWithDiscount(@Param("memberId") Long memberId);
이것처럼 계산,집계,정렬,상위 N Query 조회는 DB레벨에서 제어하는 게 훨씬 효율적인 경우가 많습니다
기존에 간단한 게시판 정도의 프로젝트를 할 때는 JPA에서 제공하는 CRUD 메서드를 통해 구현이 어느 정도는 가능했습니다
그런데 서비스가 고도화되며 여러 기능을 만들려고 할 때 JPA의 쿼리 메서드로 감당이 안 되는 상황에서는 Native Query를
통해서 서비스를 구현했던 것 같네요 성능 개선을 할 때도 많이 사용했습니다
그만큼 SQL의 중요성을 느끼면서 DB에 대해 더 공부해보고 싶어 SQLD를 준비하게 되었습니다
2-2. JPQL의 한계점과 Native Query의 장점
JPQL은 객체지향적으로 편리하지만,SQL의 모든 기능을 사용할 수 없습니다
LIMIT,WITH,서브쿼리,CASE WHEN윈도우 함수 등
Native Query를 사용하면 DB가 제공하는 성능과 기능을 그대로 활용할 수 있기 때문에 복잡한 요구사항 같은 경우 활용성이 높다고 볼 수 있습니다
3. JPQL과 Native Query의 차이점
3-1. JPQL (Java Persistence Query Language)
JPQL은 엔티티 객체를 대상으로 작성하는 객체지향 쿼리 언어입니다
SQL과 문법은 유사하지만 테이블이 아닌 엔티티,컬럼이 아닌 필드를 기준으로 동작한다는 차이점이 존재합니다
@Query(""" SELECT o FROM Order o JOIN FETCH o.member WHERE o.status = :status """)
List<Order> findByStatus(@Param("status") OrderStatus status);
Order 엔티티를 조회하면서 연관된 member 엔티티까지 함께 조회,Fetch Join을 통해 N + 1 문제 방지
엔티티 객체를 대상으로 작성하기 때문에 유지보수 측면에서는 유리합니다
3-2. Native Query
Native Query는 실제 SQL을 그대로 사용하는 방식입니다
JPA를 거치지 않고 DB에 직접 SQL을 전달하며 실제 SQL을 사용하기 때문에 테이블,컬럼명을 그대로 사용하고
복잡한 쿼리 작성이나 성능 튜닝이 필요한 상황에서 주로 사용합니다
하지만 특정 DBMS(MySQL,ORACLE,MS SQL)에 종속성이 발생하여 유지보수 측면에서 불리합니다
물론 사용하는 DBMS를 마이그레이션 할 경우에 생기는 문제이기 때문에 이 부분은 참고만 하는 게 좋을 거 같습니다
@Query(value = """
SELECT *
FROM orders
WHERE order_status = :status
ORDER BY created_at DESC
LIMIT 20
""", nativeQuery = true)
List<Order> findRecentOrdersByStatus(@Param("status") String status);
Orders 테이블에서 특정 상태 데이터를 조회 생성일 기준으로 내림차순 정렬 최신 20개의 데이터만 가져옴
복잡한 통계,요구사항을 처리해야 하거나 성능 최적화가 필요한 경우 Native Query를 사용하는 게 좋을 것 같네요
3-3. Native Query 기본 사용법
JPQL과 문법은 비슷하지만 2가지 차이점이 존재합니다
1. value에 실제 SQL을 작성한다
2. nativeQuery = true를 설정한다
@Query(value = "SELECT * FROM orders WHERE member_id = :memberId", nativeQuery = true)
List<Order> findOrders(@Param("memberId") Long memberId);
3-4. Native Query 결과를 매핑하는 방법
Native Query를 사용할 때 대표적으로 다음 3가지 방식으로 결과를 매핑합니다
- Object [ ] 또는 Object
- 엔티티(Entity)
- DTO
Object [ ]로 매핑하기
가장 단순한 방식이지만 사용할 때는 인덱스로 꺼내서 사용해야 합니다
@Query(value = """
SELECT
o.order_id,
m.name,
SUM(oi.quantity * oi.order_price) AS total_amount
FROM orders o
JOIN member m ON o.member_id = m.member_id
JOIN order_item oi ON o.order_id = oi.order_id
GROUP BY o.order_id, m.name
""", nativeQuery = true)
List<Object[]> findOrderSummary();
List<Object[]> result = orderRepository.findOrderSummary();
for (Object[] row : result) {
Long orderId = ((Number) row[0]).longValue();
String memberName = (String) row[1];
Long totalAmount = ((Number) row[2]).longValue();
}
장점
- 가장 단순하며 빠르게 작성 가능
- 임시 조회 및 테스트용으로 편리함
단점
- 인덱스 기반이라 가독성이 안 좋음
- 타입 안정성이 떨어짐
- 유지보수 측면에서 불리함
단일 컬럼일 때 Object 또는 기본 타입으로 받기
결과가 한 컬럼이면 Object 형태 및 타입에 맞게 결과를 매핑하면 됩니다
예를 들어 주문 총액만 조회를 하는 상황이라면 Long 타입으로 그대로 받아서 사용 가능합니다
@Query(value = """
SELECT SUM(oi.quantity * oi.order_price)
FROM order_item oi
WHERE oi.order_id = :orderId
""", nativeQuery = true)
Long findTotalAmountByOrderId(@Param("orderId") Long orderId);
또는 문자열을 받는 경우 다음과 같이 처리할 수 있겠죠
@Query(value = """
SELECT payment_status
FROM payment
WHERE order_id = :orderId
""", nativeQuery = true)
String findPaymentStatus(@Param("orderId") Long orderId);
엔티티(Entity)로 매핑하기
조회 결과가 엔티티의 모든 필드와 일치하는 경우라면 결과 타입을 엔티티로 받아서 사용할 수 있습니다
조회하는 엔티티가 테이블과 동일하여 결과를 엔티티로 매핑
@Query(value = """
SELECT *
FROM orders
WHERE member_id = :memberId
ORDER BY created_at DESC
""", nativeQuery = true)
List<Order> findOrdersByMemberId(@Param("memberId") Long memberId);
엔티티 매핑은 생각보다 조건이 있습니다
- 엔티티에 매핑되는 컬럼이 충분히 조회되어야 함
- PK 컬럼은 반드시 포함되어야 함
- 컬럼명이 엔티티 매핑 정보와 맞아야 함
- 조인 결과를 섞어서 가져오면 엔티티 매핑이 꼬일 수 있음
해당 테이블을 그대로 가져온다고 생각하면 좋은데 여기서 만약에 조인을 한다면 기존의 테이블 구조와 맞지 않기 때문에
엔티티로 결과를 매핑할 수 없게 됩니다 이런 경우 DTO로 받는 것이 적절합니다
DTO로 매핑하기
실제로 서비스를 개발하다 보면 주로 엔티티 하나를 조회하는 것이 아닌 여러 테이블과 조인을 통해 조회하는 경우가 많습니다
이런 경우 Object로 받아 처리하는 것보다 DTO 인터페이스를 생성하여 결과를 매핑하는 게 효율적입니다
DTO로 매핑하는 방법은 먼저 DTO 인터페이스를 생성합니다
public interface OrderSummaryDto {
Long getOrderId();
String getMemberName();
Long getTotalAmount();
String getPaymentStatus();
}
다음으로 Native Query에서 alias를 DTO의 getter 이름과 맞춰줍니다
@Query(value = """
SELECT
o.order_id AS orderId,
m.name AS memberName,
SUM(oi.quantity * oi.order_price) AS totalAmount,
p.payment_status AS paymentStatus
FROM orders o
JOIN member m ON o.member_id = m.member_id
JOIN order_item oi ON o.order_id = oi.order_id
JOIN payment p ON o.order_id = p.order_id
GROUP BY o.order_id, m.name, p.payment_status
""", nativeQuery = true)
List<OrderSummaryDto> findOrderSummaries();
예를 들어 DTO에 getMemberName()가 있다면 SQL alias도 반드시 memberName이어야 합니다
인터페이스 기반 프로젝션은 getter 이름과 alias 이름이 연결되는 방식이기 때문입니다
4. Native Query를 사용하는 기준
Native Query를 사용하면 좋은 점도 있지만 유지보수가 힘들어질 수도 있는 단점이 존재합니다
1. JPQL을 우선 고려한다
- 단순 조회, 연관관계 조회는 JPQL이 더 객체지향적이며 읽기 쉬우며 유지보수가 좋기 때문에 먼저 JPQL을 고려합니다
2. Native Query는 SQL이 가점을 가지는 상황에서 사용
- 집계,통계,복잡한 계산,DB 최적화가 필요한 경우에 사용하는 게 효율적입니다
- 또한 특정 상황에서는 JPA가 아닌 JDBC,MyBatic 등 SQL을 직접 사용하는 방법도 사용하기도 합니다
'Back-end > Spring' 카테고리의 다른 글
| [Spring] @Transactional 내부 동작 원리 (0) | 2026.03.10 |
|---|---|
| [Spring] Spring Boot 운영 환경 모니터링 시스템 구축 방법 (Prometheus + Grafana + Docker) (0) | 2026.02.23 |
| [Spring] Spring Boot JPA 비관적 락(Pessimistic Lock)을 활용한 동시성 제어 방법 (0) | 2026.02.15 |
| [Spring] Spring Boot JPA 낙관적 락(Optimistic Lock)을 활용한 동시성 제어 방법 (0) | 2026.02.13 |
| [Spring] Spring Boot(Log) Slf4j, Logback 로깅 레벨 관리하는 방법 (0) | 2026.02.12 |