티스토리 뷰

정리용/DB

[DB 기초] 14. JPA 추상화

hee-ya07 2025. 4. 26. 23:29

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 를 기반으로 추상화

출처 :: ASAC 수업자료


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

차이로는

출처 : https://lordofkangs.tistory.com/464

// - 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

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
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
글 보관함