상세 컨텐츠

본문 제목

[스프링 1팀] 8장 Spring Data JPA 활용

23-24/Spring 1

by 숨니 2023. 11. 24. 13:06

본문

728x90

 

이번 스터디 정리글에서는 Spring Data JPA에 대해서 다뤄 보려고 한다.

 

8.1 프로젝트 생성

이전 6장에서 학습한 코드를 가져와서 사용할 예정이며 JPA의 기능에 대해 살펴 보기 위해 spring initializer 등을 통해 프로젝트를 생성해 준다.


8.2 JPQL

JPQL은 JPA Query Language로 JPA에서 사용하는 쿼리이다.

JPQL는 SQL과 상당히 유사하지만 SQL과 다른 점은 테이블과 칼럼을 사용하는 대신 엔티티 객체에 대한 이름과 필드의 이름을 사용한다는 점이다.

 

<JPQL 쿼리 기본구조>

SELECT p FROM Product p WHERE p.number = ?1;

  • Product는 엔티티 타입이다.
  • p.number는 엔티티 속성이다.

8.3 쿼리 메서드 살펴보기

리포지토리는 기본적으로 JpaRepository를 상속 받음으로서 CRUD 메서드를 사용할 수 있다. (*Create, Read, Update, Delete) 하지만 기본 메서드는 식별자를 기반으로 생성되기 때문에 별도의 메서드를 정의해서 사용하는 경우가 많아진다. 이때 쿼리문 작성을 위해 사용하는 것이 쿼리 메서드이다.

 

8.3.1 쿼리 메서드의 생성

쿼리 메서드의 구성 요소로는 크게 주제와 서술어가 있다.

  • 주제(Subject): 동작을 결정 (- find By, exists By) 
  • 서술어(Predicate): By 뒤로 나오게 되며, 검색 및 정렬 조건을 지정하는 영역 ( 엔티티 속성으로의 정의 및 AND OR 사용 가능)

< 리포지토리 쿼리 메서드 생성 예시>

(리턴 타입) + { 주제 +          서술어               (속성) }
List<Person> findByLastnameAndEmail(String lastName, String email);
  • 서술어의 속성의 경우 엔티티에서 관리하고 있는 속성(필드)만 참조할 수 있다.

 

8.3.2 쿼리 메서드의 주제 키워드

<쿼리 메서드 주제의 주요 키워드>

  • find ... By
  • read ... By
  • get ... By
  • query ... By
  • search ... By
  • stream ... By

...으로 표시한 부분에는 엔티티를 사용할 수 있으나, 리포지토리에서 먼저 도메인을 선언하고 메서드를 이용하기 때문에 생략하기도 한다.

 

▷ find ... By

조회하는 기능을 수행하는 키워드.

예시) Optional<Product> findByNumber(Long number)

 

▷ exits ... By

특정 데이터가 존재하는지 확인하는 키워드. 반환 값으로는 boolean을 사용한다.

예시) boolean existsByNumber(Long number);

 

▷ count ... By

조회 쿼리를 수행한 후 쿼리 결과로 나온 레코드 개수를 리턴하는 키워드.

예시) long countByName(String name);

 

▷ delete ... By, remove ... By

삭제 쿼리를 수행하는 키워드. 리턴 타입이 없거나 삭제 횟수를 사용한다.

예시) void deleteByNumber(Long number);

 

▷ ... first<number> ..., ... Top<number> ...

쿼리를 통해 조회한 결과값의 개수를 제한하는 키워드. 주제와 By 사이에 넣어서 사용한다.

주로 한번의 동작으로 여러번의 조회할 때 사용한다. 단일 조회에 대해서는 <number>를 생략한다.

예시) List<Product> findFirst5ByName(String name); 

 

 

8.3.3 쿼리 메서드의 조건자 키워드

조건자 키워드는 쿼리 메서드의 서술어 부분에서 사용하는 키워드이다.

 

▷ Is

값의 일치를 조건으로 사용하는 조건자 키워드. Equals와 동일한 기능을 수행하며 생략 가능 하다.

예시) Product findByNumberIs(Long number);

 

▷ (Is) Not

값의 불일치를 조건으로 사용하는 조건자 키워드.

예시) Product findByNumberIsNot(Long number);

 

▷ (Is)Null, (Is)NotNull

값이 Null인지 검사하는 조건자 키워드.

예시) List<Product> findUpdatedAtNull();

 

▷ True, (Is)False

boolean 타입으로 지정된 칼럼의 값을 확인하는 조건자 키워드.

예시) Product findByisActiveTrue();

 

▷ And, Or

여러 조건을 묶을 때 사용하는 조건자 키워드.

예시) Product findByNumberAndName(Long number, String name);

 

▷ GreaterThan, (Is)LessThan, (Is)Between

숫자, datetime 칼럼에 대한 비교 연산이 가능한 조건자 키워드.

예시) List<Product> findByPriceIsGreaterThan(Long price);

 

▷ (Is)StartingWith(==StartsWith), (Is)EndingWith(==EndsWith), (Is)Containing(==Contains), (Is)Like

예시) List<Product> findByNameLike(String name);

 


8.4 정렬과 페이징 처리

 

8.4.1 정렬 처리하기

일반적인 쿼리문에서 정렬을 처리할 때는 Order By 구문을 이용한다.

▶ 정렬 조건이 1개
예시) List<Product> findByNameOrderByNumberAsc(String name);
  • Asc: 오름차순
  • Desc: 내림차순

위의 메서드를 통해서는 들어온 name을 바탕으로 데이터베이스에서 조회하고 조회된 칼럼에 대해 number의 값으로 다시 정렬하여 전달한다.

만약 정렬 조건을 여러개로 하고 싶다면 별다른 조건 키워드 없이 정렬 조건을 나열하면 된다.

▶ 정렬 조건이 2개 이상
예시) List<Product> findByNameOrderByPriceAscStockDesc(String name);

위의 메서드는 표기한 순서대로 price에 대해서 먼저 정렬을 하고 그 다음 Stock에 대해서 정렬을 수행한다.

 

메서드의 이름에 정렬 키워드를 넣지 않고 정렬을 하는 방법도 있다. 가독성을 위해 쿼리 메서드의 이름에 넣는 것 대신 매개변수를 이용하여 정렬을 수행한다.

▶ 정렬 키워드 분리
예시) List<Product> findByName(Stirng name, Sort sort);
호출 예시) product.Repository.findByName("펜", Sort.by(Order.asc("price")));

 

메서드에서 정렬을 넣는 것 대신 매개변수를 활용해도 역시 호출에서의 가독성이 좋지 않다. 그럴 때는 sort 부분을 하나의 메서드로 분리한뒤 쿼리 메서드를 호출하는 방법도 사용할 수 있다.

 

8.4.2 페이징 처리

페이징은 데이터베이스 레코드를 개수로 나누어서 페이지를 구분하는 것을 의미한다.

JPA는 Page와 Pageable을 통해서  페이징 처리를 한다.

예시) Page<Product> findByName(Sring name, Pageable pageable);
호출 예시) Page<Product> productPage = productRepository.findByName("펜", "PageRequest.of(0, 2));

 

PageRequst는 Pageable의 구현체이다. PageRequest는 of 메서드를 통해 PageRequest 객체를 생성한다.

 

< of 메서드>

  • of ( int page, int size ): 페이지 번호, 페이지당 데이터 개수
  • of ( int page, int size, sort ): 페이지 번호, 페이지당 데이터 개수, 정렬
  • of  ( int page, int size, Direction, String properties ): 페이지 번호, 페이지당 개수, 정렬 방향, 속성 

page 객체를 출력하면 객체의 값이 아니라 객체의 페이지 위치 정보만 전달 하기 때문에 값을 보기를 원한다면 아래의 예시처럼 작성한다.

예시) Page<Product> productPage = productRepository.findByName("펜", pageRequest.of(0, 2);
System.out.println(productPage.getContent());

.getContent() 메서드를 통해 배열 형태로 값을 출력할 수 있다.

 


8.5 @Query 어노테이션 사용하기

데이터베이스에서 값을 조회할때 직접 쿼리 메서드를 작성해도 되지만, @Query 어노테이션으로도 직접 JPQL을 작성할 수도 있다. 직접 JPQL을 사용하면 데이터베이스에 특화된 SQL의 작성이 가능하다. 튜닝된 쿼리를 이용하고자 할때 SQL을 직접 작성한다.

 

@Query("SELECT p FROM Product AS p WHERE p.name = ?1")
List<Product> findByName(String name);

 

@Query ( ) 안에 JPQL 형식의 쿼리문을 작성한다.

- FROM 문에는 엔티티 타입을 명시하고 별칭을 생성한다.

- WHERE 문에는 조건을 명시한다.

- ?1의 경우 파라미터를 전달 받기 위한 인자이다. 1은 첫 번째 파라미터를 의미한다.

 

?1 대신 파라미터를 정확하게 전달하기 위해서는 @Param 어노테이션을 사용하는 것이 좋다.

예시) 
@Query("SELECT p FROM Product AS p WHERE p.name = :name")
List<Product> findByNameParam(@Param("name") String name);

 

@Query를 통해서 엔티티가 아닌 원하는 칼럼의 값만 추출하는 것도 가능하다.

이럴 때는 반환 값의 타입에 Object 형태의 리스트 타입으로 해야 한다.

예시)
@Query("SELECT p.name, p.price, p.stock FROM Product p WHERE p.name = :name")
List<Object[]> findByNameParam2(@Param("name") String name);

 


8.6 QueryDSL 적용하기

@Query 어노테이션을 이용하여 직접 JPQL을 작성할 수 있지만, 이 경우 문자열을 입력하기 때문에 컴파일 과정에서 에러를 잡지 못하고 런타임시 에러가 발생할 수 있다. 이런 문제를 해결하기 위한 것이 QueryDSL이다.

QueryDSL을 통해 문자열 대신 코드로 쿼리를 작성할 수 있다.

 

8.6.1 QueryDSL이란?

QueryDSL은 정적 타입을 이용해 SQL 같은 쿼리를 생성할 수 있도록 지원하는 프레임 워크이다.

문자열이나 XML 파일을 이용한 쿼리 생성 대신 QueryDSL이 제공하는 플루언트 API를 사용하여 쿼리를 생성한다.

 

http://querydsl.com/static/querydsl/4.0.1/reference/ko-KR/html_single/

 

Querydsl - 레퍼런스 문서

Querydsl은 JPA, JDO, Mongodb 모듈에서 코드 생성을 위해 자바6의 APT 어노테이션 처리 기능을 사용한다. 이 절에서는 코드 생성을 위한 다양한 설정 옵션과 APT에 대한 대안을 설명한다. 기본적으로 Query

querydsl.com

 

8.6.2 QueryDSL의 장점

  • IDE가 제공하는 코드 완성 기능 사용 가능
  • 문법적으로 잘못된 쿼리를 허용하지 않기 때문에 문법적 오류를 발생시키지 않음
  • 고정된 SQL 쿼리를 작성하지 않기 때문에 동적인 쿼리 생성 가능
  • 코드로 작성하기 때문에 가독성이 좋음
  • 도메인 타입과 프로퍼티를 안전하게 참조 가능

8.6.3 QueryDSL을 사용하기 위한 프로젝트 설정

QueryDSL 사용을 위해서는 설정이 필요하다. pom.xml에 다음과 같은 내용을 추가한다.

설정을 마친 다음에는 compile 버튼을 통해 빌드 작업을 수행한다.

 

		<dependency>
			<groupId>com.querydsl</groupId>
			<artifactId>querydsl-apt</artifactId>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>com.querydsl</groupId>
			<artifactId>querydsl-jpa</artifactId>
		</dependency>

(pom.xml)

 

			<plugin>
				<groupId>com.mysema.maven</groupId>
				<artifactId>apt-maven-plugin</artifactId>
				<version>1.1.3</version>
				<executions>
					<execution>
						<goals>
							<goal>process</goal>
						</goals>
						<configuration>
							<outputDirectory>target/generated-sources/java</outputDirectory>
							<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
							<options>
								<querydsl.entityAccessors>true</querydsl.entityAccessors>
							</options>
						</configuration>
					</execution>
				</executions>
			</plugin>

(pom.xml)

 

8.6.4 기본적인 QueryDSL 사용하기

▶ JPAQuery 객체 사용

@PersistenceContext
    EntityManager entityManager;

    @Test
    void queryDslTest() {
        JPAQuery<Product> query = new JPAQuery(entityManager);
        QProduct qProduct = QProduct.product;

		// 쿼리문 작성
        List<Product> productList = query
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();

        for (Product product : productList) {
            System.out.println("----------------");
            System.out.println();
            System.out.println("Product Number : " + product.getNumber());
            System.out.println("Product Name : " + product.getName());
            System.out.println("Product Price : " + product.getPrice());
            System.out.println("Product Stock : " + product.getStock());
            System.out.println();
            System.out.println("----------------");
        }
    }

 

위에서는 QueryDSL의 Q도에인 클래스를 사용하고 있다.

QueryDSL을 사용하기 위해서는 JPAQuery 객체를 사용해야 한다. JPAQuery 객체는 entityManager를 통해서 생성한다.

그리고는 builder형식으로 쿼리를 작성한다. 빌더 메서드는 SQL에서 사용하는 키워드로 구성한다. 

 

List 타입으로 값을 받기 위해서는 fetch() 메서드를 사용해야 한다.

<반환 메서드>

  • List<T> fetch(): 조회 결과를 리스트로 반환
  • T fetchOne(): 단 건의 조회 결과 반환
  • T fetchFirst(): 여러 건의 조회 결과 중 하나를 반환
  • Long fetchCount(): 조회 결과 개수를 반환
  • QueryResult<T> fetchResults(): 조회 결과 리스트와 개수를 포함한 QueryResult를 반환

 

▶ JPAQueryFactory 사용

    @Test
    void queryDslTest2() {
        //JPAQueryFactory 초기화
        JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
        QProduct qProduct = QProduct.product;

        List<Product> productList = jpaQueryFactory.selectFrom(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();
                
        // 원하는 칼럼만        
        List<String> productList = jpaQueryFactory
                .select(qProduct.name)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();
        
        // 조회 대상이 여러 개인 경우
        List<Tuple> tupleList = jpaQueryFactory
                .select(qProduct.name, qProduct.price)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();

        for (Product product : productList) {
            System.out.println("----------------");
            System.out.println();
            System.out.println("Product Number : " + product.getNumber());
            System.out.println("Product Name : " + product.getName());
            System.out.println("Product Price : " + product.getPrice());
            System.out.println("Product Stock : " + product.getStock());
            System.out.println();
            System.out.println("----------------");
        }
    }

JPAQuery와의 차이점은 select부터 작성이 가능하다.

전체 칼럼이 아니라 원하는 칼럼만을 조회하고 싶다면 selectFrom() 대신 select()와 from()을 구분하여 사용한다.

조회 대상이 여러 개일 경우에는 리턴 타입을 List<Tuple>로 지정해준다.

 

config로 JPAQueryFactory를 @Bean에 등록하면 JPAQueryFactory를 초기화 하는 과정 없이 사용이 가능하다.

@Configuration
public class QueryDSLConfiguration {
    @PersistenceContext
    EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

 

 

8.6.4 QuerydslPredicateExecutor, QuerydslRepositorySupport 활용

 

▶ QuerydslPredicateExecutor 인터페이스

JpaRepository와 함께 리포지토리에서 QueryDSL 사용할 수 있는 인터페이스를 제공한다.

public interface QProductRepository extends JpaRepository<Product, Long>, 
	QuerydslPredicateExecutor<Product> {
    
}

위처럼 두가지를 동시에 상속 받아서 사용한다.

 

<QuerydslPredicateExecutor의 제공 메서드>

  • Optional<T> findOne(Predicate predicate)
  • Iterable<T> findAll(Predicate predicate)
  • Iterable<T> findAll(Predicate predicate, Sort sort)
  • Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders)
  • Iterable<T> findAll(OrderSpecifier<?>... orders)
  • Page<T> findAll(Predicate predicate, Pageable pageable)
  • long count(Predicate predicate)
  • boolean exists(Predicate predicate)

QuerydslPredicateExecutor 인터페이스의 메서드는 predicate 타입을 매개변수로 받는다.

predicate는 표현식을 작성할수 있도록 QueryDSL에서 제공하는 인터페이스이다.

 

@Test 
    public void queryDSLTest() {
        //predicate 사용
        Predicate predicate = QProduct.product.name.containsIgnoreCase("펜")
                .and(QProduct.product.price.between(1000, 2500));
        
        Optional<Product> foundProduct = qProductRepository.findOne(predicate);
        
        // predicate 서술부만 사용
        QProduct qProduct = QProduct.product;
        Iterable<Product> productList = qProductRepository.findAll(
                qProduct.name.contains("팬")
                        .and(qProduct.price.between(550, 1500))
        );
        
        if(foundProduct.isPresent()){
            Product product = foundProduct.get();
            System.out.println(product.getNumber());
            System.out.println(product.getName());
            System.out.println(product.getPrice());
            System.out.println(product.getStock());
        }
    }

위처럼 predicate를 사용해서 표현식을 정의해도 되고 서술부만 가져다 사용해도 된다.

 

 

 QuerydslRepositorySupport

추상 클래스 사용cutomerRepository를 사용해서 리포지토리를 구현하는 방식

  1. JpaRepository를 상속받는 ProductRepository를 생성.
  2. JpaRepository를 상속받지 않는 ProductRepoitoryCustom을 생성하여 직접 구현한 쿼리 사용한다. 인터페이스에 정의하고자 하는 기능들을 메서드로 정의.
  3. ProductRepoitoryCustom에서 정의한 메서드 사용을 위해 ProductRepoitory에서 ProductRepoitoryCustom를 상속 받는다.
  4. ProductRepoitoryCustom 정의된 메서드를 실제 쿼리 작성을 하기 위해 구현체인 ProductRepoitoryCustomImpl 클래사 생성한다.
  5. ProductRepoitoryCustomImpl클래스에서 QueryDSL을 사용하기 위해 QuerydslRepositorySupport을 상속받는다.

 

package com.springboot.advanced_jpa.data.repository.support;

import com.springboot.advanced_jpa.data.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository("ProductRepositorySupport")
public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom{

}

( ProductRepoitory.java)

위와 같이 상속을 받는다.

 

public interface ProductRepositoryCustom {

    List<Product> findByName(String name);
}

( ProductRepoitoryCustom.java)

구현할 메서드를 정의해 놓는다.

 

@Component
public class ProductRepositoryCustomImpl extends QuerydslRepositorySupport implements
        ProductRepositoryCustom {

    public ProductRepositoryCustomImpl() {
        super(Product.class);
    }

    @Override
    public List<Product> findByName(String name) {
        QProduct product = QProduct.product;
        List<Product> productList = from(product)
                .where(product.name.eq(name))
                .select(product)
                .fetch();

        return productList;
    }
}

( ProductRepoitoryCustomImpl.java)

정의해 놓은 메서드를 구현한다.

 


8.7 JPA  Auditing 사용

JPA Auditing은 데이터 생성 및 변경 날짜를 관리해준다.

 

8.7.1 JPA Auditing 기능 활성화

@EnableJpaAuditing 어노테이션을 사용해서 spring boot 애플리케이션의 Auditing 기능을 활성화 한다.

@EnableJpaAuditing
@SpringBootApplication
public class AdvancedJpaApplication {

	public static void main(String[] args) {
		SpringApplication.run(AdvancedJpaApplication.class, args);
	}

}

 

위의 어노테이션 설정 대신 @WebMvcTest 어노테이션의 호출 예외 발생 등을 처리하기 위해 별도의 configuration 파일을 아래와 같이 만들어서 Auditing 기능을 활성화 해주어도 된다.

@Configuration
@EnableJpaAuditing
public class JoaAuditingConfiguration {

}

 

8.7.2 Base Entity 만들기

코드의 중복을 없애기 위해 각 엔티티에 공통으로 들어가는 칼럼은 하나의 클래스로 빼두어야 한다.

생성일자와 변경일자만 아래와 같이 빼둔다.

@Getter
@Setter
@ToString
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
    
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime updatedAt;
}

 

  • @MappedSuperclass: JPA의 엔티티 클래스가 상속받을 경우 자식 클래스에게 매핑 정보를 전달한다.
  • @EntityListeners: 엔티티를 데이터베이스에 적용하기 전후로 콜백을 요청할 수 있게 하는 어노테이션
  • AuditingEntityListener: 엔티티의 Auditing정보를 주입하는 JPA 엔티티 리스터 클래스
  • @CreatedDate: 데이터 생성날짜를 자동으로 주입하는 어노테이션
  • @LsatModifiedDate: 데이터 수정 날짜를 자동으로 주입하는 어노테이션
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Table(name = "product")
public class Product extends BaseEntity{

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

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Integer price;

    @Column(nullable = false)
    private Integer stock;

}

기존 엔티티에 부모 엔티티로 baseEntity를 상속해주고 @EqualsAndHashCode(callSuper = true) 어노테이션을 통해서 부모 클래스 필드를 포함하도록 한다.

 

작성자: 니나노

728x90

관련글 더보기