티스토리 뷰
0. JPA 추상화 과정
계층 | 기술 | 특징 |
QueryDSL | 타입 안전한 방식으로 쿼리를 작성할 수 있는 라이브러리 | |
고수준 | Spring Data JPA | - JPA 위에 있는 스프링의 고수준 추상화 - Repository만 정의하면 자동 쿼리 생성 - @Query, 페이징, 정렬, Query Method 지원 - CRUD 자동 구현 |
~ | Hibernate (JPA 구현체) | - JPA 구현체 - JPA보다 더 많은 기능 제공 (2차 캐시, Dirty Checking, Lazy Loading 등) |
~ | JPA (Java Persistence API) | - 자바 ORM 표준 인터페이스 (Hibernate, EclipseLink 등 구현체 존재) - Entity, EntityManager, @Query 등 객체와 DB 자동 매핑 |
~ | JDBC Template(Spring JDBC) | - Spring이 제공하는 JDBC 추상화 (JDBC 코드 단순화) - 반복 코드 제거, 예외 처리 일괄 처리 |
저수준 | JDBC(Java Database Connectivity) | - DB 접근을 위한 가장 기본적인 API (연결, 쿼리, ResultSet 수동 처리) |
0.1 JPA 를 추상화한 방법 (Spring Data JPA & QueryDSL)
:: JPA 중 Hibernate 를 기반으로 추상화
1. Spring Data JPA
:: JPA(Java Persistence API)를 더 쉽게 사용하도록 도와주는 Spring 기반의 데이터 접근 추상화 프레임 워크
:: 반복적인 CRUD로직을 줄이고 비즈니스로직에 집중할 수 있게 도와주는 것이 목적
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
- JpaRepository<t, id="">를 상속한 인터페이스(Repository 인터페이스)만 정의하면,
:: 구현클래스 없이 인터페이스를 통한 Query Method 작성만으로도 메서드명 기반의(JPQL 실행) 쿼리 자동 생성
:: Spring이 자동으로 구현체를 만들어 Bean으로 등록해줌
1.1 Repository 상세 구현 - SimpleJpaRepository
:: JpaRepository는 인터페이스이기에 실제 동작은 SimpleJpaRepository라는 구현 클래스에서 처리
:: 쿼리가 자동 생성 되는 것은 SimpleJpaRepository가 백그라운드에서 구현체를 만들어 주입해주기 때문
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
private final EntityManager em;
public Optional<T> findById(ID id) {
Assert.notNull(id, ID_MUST_NOT_BE_NULL);
Class<T> domainType = getDomainClass();
if (metadata == null) {
return Optional.ofNullable(em.find(domainType, id));
}
LockModeType type = metadata.getLockModeType();
Map<String, Object> hints = getQueryHints().withFetchGraphs(em).asMap();
return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}
}
- SimpleJpaRepository는
:: 내부적으로 JPA의 핵심객체인 EntityManager를 가지고 있음
::JPA 표준 API를 직접 사용해서 동작함을 의미
즉, Spring Data JPA는 단순한 추상화가 아니라, 내부에서 JPA를 실제로 사용하는 강력한 구현체를 자동으로 연결해주는 역할임
1.2 Query Method 기능
:: 메소드 이름 규칙에 따라 JPA가 JPQL을 자동 생성
메서드 명 | 생성 쿼리 |
findByUsername | WHERE username = ? |
findByAgeGreaterThan | WHERE age > ? |
findByTitleContaining | WHERE title LIKE %?% |
:: 문자열 검색용 키워드의 경우에는
메서드에서 키워드 | JPQL 변환 결과 | 설명 |
Like | LIKE :value | 직접 사용 시 % 포함 여부는 개발자가 지정 |
Containing, Contains | LIKE %:value% | 부분 문자열 포함 여부 (가장 자주 사용됨) |
StartingWith, StartsWith | LIKE :value% | 시작하는 문자열 검색 |
EndingWith, EndsWith | LIKE %:value | 끝나는 문자열 검색 |
:: 이때, 대소문자 구분 없이 처리할 경우에는
// "어벤져스"라는 단어를 포함한 영화 제목 검색
List<Movie> findByTitleContaining(String keyword);
// 대소문자 구분 없이 검색
List<Movie> findByTitleContainingIgnoreCase(String keyword);
// 특정 문자열로 시작하는 제목 검색
List<Movie> findByTitleStartingWith(String prefix);
// 특정 문자열로 끝나는 제목 검색
List<Movie> findByTitleEndingWith(String suffix);
2. Repository 기본 제공 Query Method와 커스텀 Query Method (JPQL, SQL)
:: Spring Data JPA 메소드 이름만으로 쿼리를 생성하는 기능 == JPQL생성 및 실행
:: 파라미터 바인딩, 유연한 반환 타입, 페이징 및 정렬 지원 모두 JPQL에서 비롯된 것
:: SQL 쿼리를 사용하고싶다면/해야한다면, @Query 혹은 QueryDSL
- 문자열 기반의 JPQL(HQL)
:: @Query, @NamedQuery
:: @Query 내 nativeQuery 옵션을 추가하면 JPQL이 아닌 SQL 도 직접 작성 가능 - 객체 기반의 JPQL(HQL)
:: QueryDSL
2.1 CrudRepository로 살펴보기
// 계층 구조
CrudRepository → PagingAndSortingRepository → JpaRepository
// 기본 CRUD 기능 제공
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
long count();
void deleteById(ID id);
void delete(T entity);
}
- 여기서 Query Method가 나오는데 위에서 설명한 것과 같이 메서드 명 기반의 쿼리를 의미
// 지원 키워드 :: And, Or, Like, Containing, StartsWith, EndsWith, Between, In, Not
2.2 커스텀 쿼리 작성 방법
1) @NamedQuery
:: Entity 클래스에 직접 정의
:: Entity 클래스에 NamedQuery 정의하고 Query Method Name 과 연결
@Entity
@NamedQuery( // 직접 정의
name = "Member.findByUsername",
query = "SELECT m FROM Member m WHERE m.username = :username"
)
public class Member { ... }
2) @Query
:: Repository 인터페이스에서 직접 정의
:: 이에 맞는 (JPQL or Native) Query 주입
(A) JPQL 사용
@Query("SELECT m FROM Member m WHERE m.username = ?1") // 위치 기반
Member findByUserName(String username);
@Query("SELECT m FROM Member m WHERE m.username = :username") // 이름 기반
Member findByUserName(@Param("username") String username);
- 파라미터 바인딩 방식
1) 위치(순서) 기반 :: "select m from Member m where m.username = ?1"
2) 이름 기반 :: "select m from Member m where m.username = :name"
(B) Native SQL 사용
@Query(value = "SELECT * FROM member WHERE username = :username", nativeQuery = true)
Member findByUserName(@Param("username") String username);
3) @Where (Hibernate-specific)
:: Soft Delete 혹은 조건 필터링용
@Entity
@Where(clause = "deleted = false")
public class MemberInfo {
private boolean deleted;
}
2.3 반환 타입 / 페이징 / 비동기 호출
1) 반환 타입의 유연성
:: 반환값에 따라 단일 객체로 반환하거나 Collection 객체로 반환되는 유연성을 가짐
Member findByName(Stirng name);
List<Member> findByName(Stirng name);
2) 페이징 및 정렬을 지원
:: 페이지를 위한 파라미터를 지원함
:: Pageable과 Sort
Page<Member> findByName(Stirng name, Pageable pageable);
List<Member> findByName(Stirng name, Pageable pageable);
List<Member> findByName(Stirng name, Sort sort);
3) 비동기 호출 가능
:: @Async 와 Future 사용가능
public interface CommentRepository extends JpaRepository<Comment, Long> {
@Async
Future<Comment> findCommentsByContent(String content);
@Async
@Query("select c from Comment c where c.content=:content")
Future<Comment> findCommentsByC(@Param("content") String content);
}
2. 4커스텀 SQL쿼리 사용 시, 주의점
- @Query 정의 시 (1) JPQL 통한 Aggregate 혹은 (2) NativeQuery 사용 시 주의점으로는
:: 반환받고자 하는 필드에 대한 Getter가 정의되어있는 인터페이스를 기반으로 반환해야 함(or 생성자를 따로 정의해줘야 함)
:: 왜냐면, 다음과 같이 객체의 List로 반환되기 때문에 인터페이스 or 생성자의 정의가 필요
@Query("SELECT c.year, COUNT(c.year) FROM Comment AS c GROUP BY c.year ORDER BY c.year DESC")
List<Object[]> countTotalCommentsByYear();
3. QueryDSL
:: Spring Data JPA 처럼 JPA(EntityManager 에 JQPL 직접 주입)를 추상화하여 쉽게 쓰기위한 기술
:: JPQL을 타입 안전하게, 동적으로, 깔끔하게 생성할 수 있도록 도와주는 프레임워크
:: 기존 JPQL은 문자열 기반 → 런타임 에러 가능성 있음
:: Java 코드로 쿼리를 작성해서 컴파일 타임에 오류를 잡을 수 있음
3.1 QueryDSL과 Spring Data JPA 비교
:: Spring Data JPA 의 JpaRepository 와 같은 계층이고, 둘을 합쳐 사용할 수 있다.
- Type Safety
:: QueryDSL의 경우엔 Query 검증을 Compile Time 에 진행가능 (Java 객체 문법)
:: JPQL은 Runtime Error (Compile Error X) 발생 = String 으로 구성된 Query 이기에 - Consistency 일관성
:: JPA, MongoDB, Scala 어떤것이든 QueryDSL 사용법은 모두 동일 - JPQL이 제공하는 모든 검색 조건을 제공
// 1) JPQL
String username = "java";
String jpql = "select m from Member m where m.username = :username";
List<Member> result = em.createQuery(query, Member.class).getResultList();
// 2) QueryDSL
String username = "java";
List<Member> result = queryFactory
.select(member)
.from(member)
.where(usernameEq(username))
.fetch();
- QueryDSL 의 3가지 구현 방식 및 예시
1) 상속/구현 없이 간단하게 만드는 Repository 클래스 방법
@Repository
@RequiredArgsConstructor
public class UserRepositorySupport {
private final JPAQueryFactory jpaQueryFactory;
public List<User> findByName(String name) {
return jpaQueryFactory.selectFrom(QUser.user)
.where(QUser.user.name.trim().eq(name))
.fetch();
}
}
2) Spring Data JPA 의 JpaRepository 와 같이 사용(커스텀 인터페이스 repository) - 실무에서 자주?
public interface UserRepositoryCustom {
List<User> findByName(String name);
}
@RequiredArgsConstructor
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
private final JPAQueryFactory jpaQueryFactory;
@Override
public List<User> findByName(String name) {
return jpaQueryFactory.selectFrom(QUser.user)
.where(QUser.user.name.trim().eq(name))
.fetch();
}
}
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustomImpl {
}
3) QuerydslRepositorySupport 추상클래스 상속 방법
@Repository
public class UserRepositorySupport extends QuerydslRepositorySupport {
private final JPAQueryFactory jpaQueryFactory;
public UserRepositorySupport(JPAQueryFactory jpaQueryFactory) {
super(User.class);
this.jpaQueryFactory = jpaQueryFactory;
}
public List<User> findByName(String name) {
return jpaQueryFactory.selectFrom(QUser.user)
.where(QUser.user.name.trim().eq(name))
.fetch();
}
}
차이로는
// - JPA 의 EntityManager 내 JPQL
return em.createQuery(
"select a, count(b) " +
" from author a " +
" join a.books b " +
" where a.name = :authorName " +
" group by a",
Author.class
).getResultList();
// - QueryDSL 설계 시 JPQL 로 자동 변환
return queryFactory
.select(author, author.books.size())
.from(author)
.leftJoin(author.books).fetchJoin()
.where(author.name.eq(authorName))
.groupBy(author)
.fetch();
3.2 Query DSL 사용을 위한 선결 조건 : Query Type
:: Entity 클래스 앞에 Q 가 붙은 이름을 갖는다. (Ex :: User Entity 객체는 QUser)
:: QueryDSL의 3가지 구현 방식의 JPAQueryFactory에서 QueryDSL 쿼리 작성을 위해 꼭 필요로 하는 정적 Type 변수
:: QueryDSL 사용 위한 라이브러리 설치 후 Query Type (QEntity) 생성, QueryDSL 시작
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
=> 정의한 @Entity 에 대응되는 Q 로 시작하는 Query Type 클래스가 생성됨
=> Spring Boot 프로젝트 내 QueryDSL 구현을 위한 @Configuration 설정
@Configuration
@EnableJpaAuditing
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager entityManager){
return new JPAQueryFactory(entityManager);
}
}
Q) JPA의 EntityManager는 요청마다 생성되는데, 왜 QueryDslConfig에서는 JPAQueryFactory에 싱글턴 EntityManager를 주입해도 문제가 없나요?
A) Spring의 Spring EntityManager는 사실 EntityManagerProxy로 실제 객체가 아닌 프록시임(=> 동시성 문제를 해결)
1) @PersistenceContext 또는 생성자 주입으로 EntityManager를 주입하면, EntityManager의 프록시 객체가 주입됨
2) 이 프록시 객체는 싱글턴 스코프이고, 매 요청 시점마다 실제 트랜잭션 바인딩된 EntityManager를 찾아서 위임
즉, em은 실제 EntityManager가 아니라 스프링이 관리하는 프록시
3) 따라서 JPAQueryFactory는 내부적으로 매 요청마다 실제 EntityManager에 위임
4) 결론적으로 요청마다 올바른 트랜잭션 바인딩 EntityManager가 사용됨 → 안전하게 동작
==> 한문장으로 정리하면 프록시객체를 통해 em을 생성 및 할당해서 괜찮다는 뜻
3.3 Query DSL 사용 예시
// SELECT FROM
public List<User> findByName(String name) {
return jpaQueryFactory.selectFrom(QUser.user)
.where(QUser.user.name.trim().eq(name))
.fetch();
}
// JOIN + ORDER BY + PAGINATION(OFFSET, LIMIT)
QItem item = QItem.item;
QItemDetail itemDetail = QItemDetail.itemDetail;
JPAQuery<ItemDto> query = jpaQueryFactory
.select(new QItemDto(
item.id,
item.registerdDate,
itemDetail.promotionImageUrl
))
.from(item)
.leftJoin(item.itemDetail, itemDetail)
.where(item.status.eq(status));
query.orderBy(item.registerdDate.desc());
List<ItemDto> items = query
.offset((page - 1) * size)
.limit(size)
.fetch();
참조
ASAC 수업자료
[QueryDSL] QueryDSL 동작원리(1) - 빌더패턴
JPA에서 개발자가 원하는 엔티티를 얻으려면, JPQL을 작성하고 EntityManager에 전달하여 실행하면 된다. 여기서 한 가지가 문제가 있는데, JPQL이 문자열이라는 점이다. JPQL이 문자열이기에 타입안정
lordofkangs.tistory.com
'정리용 > DB' 카테고리의 다른 글
[DB 기초] 15. Spring Cache (0) | 2025.04.26 |
---|---|
[DB 기초] 13. MyBatis (1) | 2025.04.26 |
[DB 기초] 12. JPA의 영속성 전이 옵션(CascadeType) (0) | 2025.04.22 |
[DB 기초] 11. 연관관계 객체 조회 시점 EAGER / LAZY (0) | 2025.04.20 |
[DB 기초] 10. JPA 연관관계 어노테이션 (0) | 2025.04.19 |
- Total
- Today
- Yesterday
- asac7
- ASAC
- Nginx
- useReducer
- git
- asac#asac7기
- asac7#asac
- acac
- acas#acas7기
- react
- useState
- useContext
- ssh
- useCallback
- useLayoutEffect
- useMemo
- useRef
- useEffect
- memo
- asac7기
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |