플레이데이터 - 40일차 Spring Boot Framework(5)

Kim Hyen Su·2023년 8월 28일

🌟QueryDSL

  • 대부분의 JPQL의 한계는 대부분 해소 가능하지만 직접 문자열을 입력하기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있다.

  • 위 같은 문제를 해결하기 위해서 사용하는 것이 QeryDSL 이다.

✅QueryDSL이란?

  • QueryDSL은 하이버네이트 쿼리 언어(HQL: Hibernate Query Language)의 쿼리를 타입에 안전하게 생성 및 관리해주는 프레임워크이다.

  • 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원.

  • 문자열이나 XML 파일을 통해 쿼리를 작성하는 대신에 QueryDSL에서 제공하는 플루언트(fluent) API를 통해 쿼리 생성이 가능하다.

✅QueryDSL의 장점

  • IDE가 제공하는 코드 자동 완성 기능 사용.
  • 문법적으로 잘못된 쿼리를 허용하지 않음 따라서 정상적으로 활용된 QueryDSL은 문법 오류가 발생하지 않는다.
  • 고정된 SQL 쿼리를 작성하지 않기 때문에 동적으로 쿼리 생성이 가능.
  • 코드로 작성하여 가독성 및 생산성 향상.
  • 도메인 타입과 프로퍼티를 안전하게 참조 가능.

✅QueryDSL 사용하기

프로젝트 설정

  • QueryDSL 관련 의존 추가
/*pom.xml*/

<dependency>
	<groupId>com.querydsl</groupId>
	<artifactId>querydsl-apt</artifactId>
	<scope>provided</scope>
</dependency>
<dependency>
	<groupId>com.querydsl</groupId>
	<artifactId>querydsl-jpa</artifactId>
	<scope>provided</scope>
</dependency>
  • <plugins> 태그에 QueryDSL을 사용하기 위한 APT 플러그인 추가
			<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>
  • JPAAnnotationProcessor는 @Entity 어노테이션으로 정의된 엔티티 클래스를 찾아서 쿼리 타입을 생성함.

APT란?

APT(Annotation Processing Tool)는 어노테이션으로 정의된 코드를 기반으로 새로운 코드를 생성하는 기능이다. JDK 1.6부터 도입된 기능이며, 클래스를 컴파일 하는 기능도 제공한다.


maven compile

  • 위처럼 의존을 추가한 뒤 메이븐의 compile 단계를 클릭하여 빌드 작업을 수행한다.

  • 완료 시, target 디렉토리에 generated-resource 경로에 Q도메인 클래스가 생성된다.

  • QueryDSL에서는 Qdomain이라는 쿼리 타입의 클래스를 자체적으로 생성하여 메타데이터로 사용하는데, 이를 통해 SQL 쿼리를 생성하여 제공함.

기본적인 QueryDSL 사용

- JPAQuery를 활용한 QueryDSL 테스트 코드

@PersistenceContext
EntityManager entityManager;

@Test
void queryDslTest(){
	JPAQuery<Product> query = new JPAQuery<>(entityManager);// 쿼리 작성을 위한 객체
    QProduct qProduct = QProduct.product; // QueryDSL에서 사용할 도메인 객체

	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("---------------");
	}
}
  • JPAQuery 객체는 엔티티 매니저를 활용해 생성하며, 빌더 형식으로 쿼리를 작성한다.

  • fetch() 메서드

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

JPAQueryFactory를 활용해 쿼리 작성하는 방법.

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

        List<Product> productList = jpaQueryFactory.selectFrom(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("---------------");
        }
    }
  • select 부터 작성이 가능하며, 일부 컬럼만 조회 시 select()와 from() 메서드를 구분하여 사용.
	@Test
    void queryDslTest3(){
        JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
        QProduct qProduct = QProduct.product;

        List<String> productList = jpaQueryFactory
                .select(qProduct.name)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();
        for(String product : productList){
            System.out.println("--------------");
            System.out.println("Product Name : " + product);
            System.out.println("--------------");
        }

        // 튜플 2가지 이상 조회
        List<Tuple> tupleList = jpaQueryFactory
                .select(qProduct.name, qProduct.price)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy(qProduct.price.asc())
                .fetch();

        for(Tuple product : tupleList){
            System.out.println("--------------");
            System.out.println("Product Name : " + product.get(qProduct.name));
            System.out.println("Product price : " + product.get(qProduct.price));
            System.out.println("--------------");
        }
    }
  • 만약 조회 대상이 여러 개인 경우 쉼표로 구분하여 작성하고 리턴 타입을 List<Tuple> 타입으로 지정한다.

QueryDSL 관련 config를 통한 설정

- QueryDSL configuration 파일 생성

@Configuration
public class QueryDSLConfiguration{
	@PersistenceContext
    EntityManager entityManager;
    
    @Bean
    public JPAQueryFactory jpaQueryFactory(){
    	return new JPAQueryFactory(entityManager);
    }
}

@PersistenceContext 란?

  1. EntityManager를 빈으로 주입할 때 사용하는 어노테이션.

    • 스프링에서는 영속성 관리를 위해 Entitymanager가 존재한다.
    • Spring Container에서 EntityManger를 만들어서 빈으로 등록해둔다.
    • 이 때 만들어둔 EntityManager를 의존 주입받을 때 사용한다.
  2. @PersistenceContext로 지정된 프로퍼티에 아래 두 가지중 한가지로 EntitiyManager를 주입해준다.

    • EntityManagerFactory에서 새로운 EntityManager를 생성하는 경우.
    • Transaction에 의해 기존에 생성된 EntityManager를 반환하는 겨우.
	@Test
    @DisplayName("쿼리 생성 관련 설정을 Config 파일에 별도로 정의한 뒤 의존 주입만 하여 해당 객체를 사용한다.")
    void queryDslTest4(){
        QProduct qProduct = QProduct.product;

        List<String> productList = jpaQueryFactory // 의존 주입한 빈 객체
                .select(qProduct.name)
                .from(qProduct)
                .where(qProduct.name.eq("펜"))
                .orderBy((qProduct.price.asc()))
                .fetch();

        for(String product : productList){
            System.out.println("--------------");
            System.out.println("Product Name : " + product);
            System.out.println("--------------");
        }

    }

QuerydslPredicateExecutor, QuerydslRepositorySupport 활용

QuerydslPredicateExecutor 인터페이스

  • 레포지토리에 QuerydslPredicateExecutor 를 구현.
public interface QProductRepository extends JpaRepository<Product, Long>
, QuerydslPredicateExecutor<Product>{

}
  • QuerydslPredicateExecutor 인터페이스 내부 메서드를 보면, Predicate 타입을 매개변수로 받는다. Predicate는 표현식을 작성할 수 있도록 QueryDSL에서 제공하는 인터페이스이다.
	@Autowired
    QProductRepository qProductRepository;

    @Test
    public void queryDSLTest1(){
        Predicate predicate = QProduct.product.name.containsIgnoreCase("펜")
                .and(QProduct.product.price.between(1000,2500));

        Optional<Product> foundProduct = qProductRepository.findOne(predicate);

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

    @Test
    public void queryDSLTest2(){
        QProduct qProduct = QProduct.product;

        Iterable<Product> productList = qProductRepository.findAll(
                qProduct.name.contains("펜")
                        .and(qProduct.price.between(1000,2500))
        );

        for(Product product : productList){
            System.out.println(product.getNumber());
            System.out.println(product.getName());
            System.out.println(product.getPrice());
            System.out.println(product.getStock());
        }
    }
  • QuerydslPrediacteExecutor를 활용하면 편하게 QueryDSL을 사용할 수 있지만 join이나 fetch와 같은 기능은 사용할 수 없다는 단점이 있다.

QuerydslRepositorySupport 추상 클래스 사용

  • 가장 보편적인 방법은 CustomRepository를 활용해 레포지토리를 구현하는 방식이 있다.

  • QuerydslRepositorySupport를 사용하기 위한 상속 구조.

  • 위 구조에 대한 설명.
    • JpaRepository를 상속받는 ProductRepository를 생성.
    • 이때 직접 구현한 쿼리를 사용하기 위해서는 JpaRepository를 상속받지 않는 레포지토리 인터페이스인 ProductRepositoryCustom을 생성. 이 인터페이스에 정의하고자 하는 기능들을 메서드로 정의함.
    • ProductRepositoryCustom에서 정의한 메서드를 사용하기 위해 ProductRepository에서 ProductRepositoryCustom을 상속받는다.
    • ProductRepositoryCustom에서 정의된 메서드를 기반으로 실제 쿼리 작성을 하기 위해 구현체인 ProductRepositoryCustomImpl 클래스를 생성함.
    • ProductRepositoryCustomImpl 클래스에서는 다양한 방법으로 쿼리를 구현할 수 있지만 QueryDSL을 사용하기 위해 QueryDslRepositorySupport를 상속받는다.

QuerydslRepositorySupport 사용 예제

  • 인터페이스 생성 후 쿼리로 구현할 메서드를 정의함.
- ProductRepositoryCustom 인터페이스

public interface ProductRepository{
	List<Product> findByName(String name);
}
  • 인터페이스 구현체 클래스에 메서드 구현.
  • QuerydslRepositorySupport 상속 및 ProductRepositoryCustom 인터페이스 구현.
@Component
public class ProductRepositoryCustomImpl extends QuerydslRepositorySupport 
implements ProductRepositoryCustom{

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

    @Override
    public List<Product> findByName(String name) {
        QProduct qProduct = QProduct.product;

        List<Product> productList = from(qProduct)
                .where(qProduct.name.eq(name))
                .select(qProduct)
                .fetch();

        return productList;
    }
}
  • QuerydslRepositorySupport를 상속받기 위해 생성자를 통해 도메인 클래스를 부모 클래스에 전달해야 한다.

  • Q도메인 클래스를 사용하여 QuerydslRepositorySupport에서 제공하는 기능인 from() 메서드를 사용한다. 이는 어떤 도메인에 접근할 것인지 지정하는 기능을 한다.

  • from() 메서드 수행 후 JPAQuery 객체를 반환함.

  • 기존에 Product 엔티티 클래스와 매핑하여 사용하던 ProductRepository가 존재하는 경우, ProductRepositoryCustom을 상속받아 사용 가능하다.

public interface ProductRepository extends JpaRepository<Prouct,Long>, ProductRepositoryCustom{

}
  • findByName() 메서드 테스트를 워한 테스트코드 작성.
@SpringBootTest
public class ProductRepositoryTest {
    @Autowired
    ProductRepository productRepository;

    @Test
    void findByNameTest(){
        List<Product> productList = productRepository.findByName("펜");

        for(Product product : productList){
            System.out.println(product.getNumber());
            System.out.println(product.getName());
            System.out.println(product.getPrice());
            System.out.println(product.getStock());
        }
    }
}

🌟JPA Auditing 적용

JPA에서 Audit 이란 '감시하다'라는 뜻으로 누가 언제 데이터를 생성했고 변경했는지 감시한다는 의미로 사용된다. 클래스에는 공통적으로 들어가는 필드가 있다. 일반적으로 '생성일자'와 '수정 일자'가 있다.

  • 생성 주체
  • 생성 일자
  • 수정 주체
  • 수정 일자

이러한 필드들은 매번 엔티티를 생성하거나 변경할 대마다 값을 주입해야 한다는 번거로움이 있다. 이를 해결하기 위해 Spring Data JPA에서는 이러한 값을 자동으로 넣어주는 기능을 제공한다.

✅JPA Auditing 기능 활성화

  • 별도의 Configuration 클래스를 생성하여 애플리케이션 클래스의 기능과 분리하여 활성화해준다.
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration{

}

✅Base Entitiy 만들기

  • 코드의 중복을 없애기 위해 엔티티에 공통으로 들어가게 되는 컬럼을 하나의 클래스로 분리하는 작업을 수행해야 한다.
@Getter
@Setter
@ToString
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(insertable = false)
    private LocalDateTime updatedAt;
}
  • @MappedSuperclass : JPA의 엔티티 클래스가 상속받을 경우 자식 클래스에게 매핑 정보를 전달.

  • @Entitylisteners : 엔티티를 데이터베이스에 적용하기 전후로 콜백을 요청할 수 있게 하는 어노테이션.

  • AuditingEntityListener : 엔티티의 Auditing 정보를 주입하는 JPA 엔티티 리스너 클래스.

  • @CreatedDate : 데이터 생성 날짜를 자동으로 주입하는 어노테이션.

  • @LastModifiedDate : 데이터 수정 날짜를 자동으로 주입하는 어노테이션.

@Entity
@Getter
@Setter
@ToString(callSuper = true)
@NoArgsConstructor
@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를 상속받은 Product 엔티티 클래스

  • @ToString, @EqualsAndHashCode 어노테이션에 적용한 callSuper 속성은 부모 클래스의 필드를 포함하는 역할을 수행하는 것을 의미한다.

  • Auditing 적용한 테스트 코드

    @Test
    public void auditingTest(){
        Product product = new Product();
        product.setName("펜");
        product.setPrice(1000);
        product.setStock(100);

        Product savedProduct = productRepository.save(product);

        System.out.println("productName : " + savedProduct.getName());
        System.out.println("createdAt : " + savedProduct.getCreatedAt());
    }

🌟연관관계 매핑

  • JPA를 사용하는 애플리케이션에도 테이블의 연관관계를 에닡티 간의 연관관계로 표현할 수 있다.

✅연관관계 매핑 종류와 방향

엔티티 종류

  • One To One

  • One To Many

  • Many To One

  • Many To Many

  • 관계는 어떤 엔티티를 중심으로 연관 엔티티를 보느냐에 따라 연관관계의 상태가 달라진다.

  • JPA를 사용하는 객체지향 모델링에서는 엔티티 간 참조 방향을 설정할 수 있다.

    • 단방향 : 두 엔티티의 관계에서 한쪽의 엔티티만 참조하는 형식.
    • 양방향 : 두 엔티티의 관계에서 각 엔티티가 서로의 엔티티를 참조하는 형식.
  • 일반적으로 외래키를 가진 테이블이 그 관계의 주인이 되며, 주인은 외래키를 사용할 수 있으나 상대 엔티티는 읽는 작업만 수행할 수 있다.

profile
백엔드 서버 엔지니어

0개의 댓글