안녕하세요!
오늘은 스프링부트에서 쿼리를 실행하는 여러 가지 방법,
그리고 각 방식의 장단점과 적절한 사용 상황에 대해 정리해보려 합니당.
최근 프로젝트를 진행하며, JPA의 기본 Repository 함수로는 필요한 쿼리를 작성할 수 없어 JPQL을 사용해 쿼리를 작성해왔습니다. 하지만 쿼리가 복잡해질수록 가독성이 떨어지고 작성하기 복잡한 어려움이 있었습니다.
또한 다른 분들의 소스 코드를 참고하다가 Querydsl을 처음 보게 되었고,
현재 제가 진행 중인 프로젝트에는 어떤 방식이 가장 적합할지 고민하게 되면서 스프링부트에서 쿼리를 작성할 수 있는 다양한 옵션들을 정리해보게 되었습니다.
우선 스프링부트에서 쿼리를 날리는 방법은 크게 10가지 정도로 정리해볼 수 있습니다.
1. 기본 JPA 메서드
2. JPQL
3. Native SQL
4. Querydsl
5. JDBC Template
6. MyBatis
7. Entitymanager
8. Stored Procedure
9. R2DBC
10. Criteria (거의 안씀)
위 10가지 방법에 대해 개념, 장점, 단점, 적절한 상황, 예제코드에 대해 알아보겠습니다.
1. 기본 JPA 메서드
개념
스프링 데이터 JPA는, Repository 인터페이스에 메서드 이름만 정의하면 자동으로 쿼리를 생성해줍니다.
// User 엔티티
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String email;
private String nickname;
private LocalDateTime deletedAt;
// getter/setter
}
// UserRepository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByNicknameContaining(String nickname);
List<User> findByDeletedAtIsNull();
}
장점
- 코드 작성이 간단하고, 간단한 CRUD는 대부분 커버 가능하다.
- 타입 안정성을 보장한다. -> 엔터티를 기준으로 체크해서 런타임 전에 오류 체크 가능하기 때문이다.
단점
- 복잡한 조건이나, 조인, 서브쿼리는 구현할 수 없다.
- 동적 검색에는 적합하지 않다.
적절한 상황
- 단순 CRUD나 조건이 단순할 때
2. JPQL (@Query)
개념
JPQL(Java Persistence Query Language)은 JPA 엔터티를 대상으로 하는 객체 지향 쿼리 언어입니다.
SQL과 문법은 비슷하지만, DB 테이블이 아닌 엔터티를 대상으로 쿼리를 작성합니다.
public interface UserRepository extends JpaRepository<User, Long> {
@Query("""
SELECT u FROM User u
WHERE u.deletedAt IS NULL
AND LOWER(u.nickname) LIKE CONCAT('%', :keyword, '%')
""")
List<User> searchByNickname(@Param("keyword") String keyword);
}
장점
- 복잡한 SELECT, JOIN이 가능하다.
- 객체 중심이라 코드와 자연스럽게 연결되고, 타입 안정성을 보장한다.(엔터티 기준이라)단점
- SQL이 아니라 함수나 DB 특화 기능 사용이 어렵다.
- 동적 쿼리에는 불편하다.적절한 상황
- 조인, 서브쿼리가 필요한 복잡한 쿼리
- 단순 검색 + 조건 조합
3. Native SQL (@Query nativeQuery = true)
개념
SQL 문법 그대로 쿼리를 작성할 수 있는 방법입니다.
DB 고유 기능, JSONB, 윈도우 함수 등 JPA가 지원하지 않는 기능도 사용 가능합니다.
@Query(value = "SELECT * FROM users WHERE email = :email", nativeQuery = true)
User findByEmailNative(@Param("email") String email);
장점
- SQL 100% 사용 가능, 성능 최적화에 유리하다.단점
- DB에 종속적이라, DB 변경 시 수정이 필요하다.적절한 상황
- 고성능 쿼리가 필요할 때
- DB 함수 사용 필요할 때
- SQL 최적화가 필요할 때
4. Querydsl
개념
타입 안정성과 동적 쿼리 작성에 최적화된 JPA 기반 쿼리 빌더입니다.
복잡한 검색 필터와 선택적 조건(if)이 붙는 경우에 유리합니다.
CustomRepository인터페이스를 만들고,- 이를 구현한 구현체 클래스를 선언하고,
JpaRepository가 이 커스텀 인터페이스와 JpaRepository를 상속받아 사용합니다.
동적 쿼리란?
실행 시점에 쿼리 조건이 동적으로 달라지는 쿼리입니다.
WHERE, ORDER BY, JOIN 등 조건이 바뀔 수 있는 쿼리입니다. (컴파일 후에 결정)
<-> 정적 쿼리는 컴파일 전에 결정
// 1) Repository 인터페이스
public interface UserRepositoryCustom {
List search(String keyword, Boolean active, LocalDateTime startDate, LocalDateTime endDate);
}
// 2) 커스텀 구현체
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
private final JPAQueryFactory queryFactory;
public UserRepositoryCustomImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
@Override
public List<User> search(String keyword, Boolean active, LocalDateTime startDate, LocalDateTime endDate) {
QUser u = QUser.user;
BooleanBuilder builder = new BooleanBuilder();
builder.and(u.deletedAt.isNull());
if (keyword != null && !keyword.isEmpty()) builder.and(u.nickname.containsIgnoreCase(keyword));
if (active != null) builder.and(u.active.eq(active));
if (startDate != null) builder.and(u.createdAt.goe(startDate));
if (endDate != null) builder.and(u.createdAt.loe(endDate));
return queryFactory.selectFrom(u).where(builder).fetch();
}
}
// 3) 기존 Repository에 커스텀 인터페이스 상속
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {}
장점
- 동적 쿼리 작성이 용이하다.
- 타입이 안전하고, 높은 가독성을 갖는다.단점
- 초기 세팅을 위한 Q클래스 생성이 필요하다.
적절한 상황
- 검색 필터가 많고, 조건이 선택적으로 붙는 동적 쿼리에 유리하다.
5. JDBC Template
개념
SQL을 직접 작성해서 반복적인 JDBC 코드 (try-cath, close)를 줄여주는 스프링 유틸리티 입니다.
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
public class UserRowMapper implements RowMapper<User> {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
return new User(
rs.getLong("id"),
rs.getString("email"),
rs.getString("name")
);
}
}
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
@RequiredArgsConstructor
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public User findByEmail(String email) {
String sql = "SELECT * FROM users WHERE email = ?";
return jdbcTemplate.queryForObject(
sql,
new UserRowMapper(),
email
);
}
}
장점
- 빠르고 가벼우며 SQL을 직접 제어할 수 있다.
- 성능이 우수하다.단점
- 객체 매핑을 직접 구현해야하며, 영속성 컨텍스트 기능이 없다.
적절한 상황
- SQL 중심 서비스
- 배치 처리 (대량 데이터 처리를 위한
batchUpdate메서드 제공) - 마이크로서비스 경량 DB 접근
6. MyBatis
개념
SQL을 XML 또는 어노테이션으로 작성하는 매핑 프레임워크로, DB 중심 설계에 유리합니다.
장점
- SQL 완전 제어가 가능하다.
- 성능이 좋고 복잡한 쿼리에 유리하다.단점
- XML이 많아지면 관리가 힘들고, 객체 매핑 코드가 필요하다.적절한 상황
- SQL 중심 프로젝트나, 금용/공공 서비스에서 사용한다.
7. EntityManger 직접 사용
개념
JPA의 순수 API를 사용하여 JPQL이나 Native SQL을 직접 실행하는 방식입니다.
@Repository
public class UserRepository {
@PersistenceContext
private EntityManager em;
// INSERT (영속성 컨텍스트 등록)
public void save(User user) {
em.persist(user);
}
// SELECT (1차 캐시 + 영속성 컨텍스트)
public User find(Long id) {
return em.find(User.class, id);
}
// JPQL 조회 (엔티티 기반 쿼리)
public User findByEmail(String email) {
return em.createQuery(
"select u from User u where u.email = :email", User.class)
.setParameter("email", email)
.getSingleResult();
}
// Dirty Checking (변경 감지) 테스트용
public void updateName(Long id, String newName) {
User user = em.find(User.class, id); // 영속 상태
user.changeName(newName); // setter만 변경하면 자동 update
}
// DELETE
public void delete(Long id) {
User user = em.find(User.class, id);
em.remove(user);
}
}
장점
- 모든 JPA 기능을 직접 제어할 수 있다.
- 복잡한 트랜잭션과 쿼리를 처리할 수 있다.단점
- 코드가 길어지고 repository 추상화 기능이 없다.적절한 상황
- 기존 Repository로 처리 불가능한 특수 케이스에서 사용한다.
8. Stored Procedure
개념
DB에 미리 정의된 프로시저나 함수를 호출하는 방식입니다.
@Procedure(name = "calculate_score")
Integer calculateScore(@Param("userId") Long userId);
9. R2DBC
개념
Reactive Spring에서 논블로킹 비동기 SQL 처리를 위한 API입니다.
Mono<User> user = databaseClient
.sql("SELECT * FROM users WHERE email = :email")
.bind("email", email)
.map(row -> new User(row.get("id", Long.class), row.get("email", String.class)))
.one();
10. Criteria
개념
JPA에서 제공하는 동적 쿼리 빌더이지만, Querydsl이 나오며 거의 쓰이지 않게 되었습니다.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> user = cq.from(User.class);
cq.select(user)
.where(cb.equal(user.get("email"), email));
List<User> result = em.createQuery(cq).getResultList();
보통은,
단순 CRUD → JPA 기본 메서드
동적 조건 검색 → Querydsl
SQL 튜닝 필요 → Native SQL, JDBC Template
SQL 중심 프로젝트 → MyBatis
순서로 많이 사용한다고 합니다.
저는 기본적인 쿼리는 JPQL을 사용하고, 꼭 필요한 부분들에 Querydsl을 도입해보려고 합니다!
