Today
-
Yesterday
-
Total
-
  • 스프링 부트 핵심 가이드 4주차 : 8장
    책 공부/스프링 부트 핵심 가이드 2023. 12. 5. 00:01

    📖 책 정보

    나를 위한 모바일 가이드

    8장. Spring Data JPA 활용

    1. JPQL
    2. 쿼리 메서드
    3. 정렬과 페이징 처리
    4. @Query
    5. QueryDSL
    6. JPA Auditing

    8장

    1. JPQL
    2. 쿼리 메서드
    3. 정렬과 페이징 처리
    4. @Query
    5. QueryDSL
    6. JPA Auditing


    Spring Data JPA에서 제공하는 기능들에 대해 더 알아보고 다양한 활용법에 대해 살펴본다.

    그 과정에서 리포지토리 예제를 작성하고 , 리포지토리의 활용법을 테스트 코드를 통해 학습

    실습 프로젝트는 새로 생성하지 않고 지난번에 했던 프로젝트에서 진행!

    spring.io - Spring Data JPA Document




    1. JPQL

    JPA Query Language


    JPA에서 사용할 수 있는 쿼리를 의미하며,

    SQL과 문법이 매우 비슷하다.


    SQL과 JPQL의 차이점

    • SQL 테이블이나 컬럼의 이름을 사용.

    • JPQL 매핑된 엔티티의 이름과 필드의 이름을 사용. 엔티티 객체를 대상으로 수행하는 쿼리이기 때문



        🐢🎈


    2. 쿼리 메서드

    JPA에서 제공하는 기본 메서드들은 식별자 기반으로 생성되기 때문에

    별도의 메서드를 정의해서 사용하는 경우가 많다.

    이때, 간단한 쿼리문을 작성하기 위해 사용되는 것이 쿼리 메서드!


    - 생성

    쿼리 메서드는 큰 범주로 분류해 보면

    동작을 결정하는 주제 Subject서술어 Predicate 로 나눠볼 수 있다.

    메서드 구조

    // 리턴타입 주제와서술어(속성) 
    List<Person> findByLastnameAndEmail(String lastName, String email);



    - 주제 키워드

    주제 부분에 사용할 수 있는 주요 키워드는 다음과 같다.

    조회

    • find...By

    가장 일반적으로 사용 조회 결과가 없으면 null 반환

    • get...By

    단일 엔티티만을 반환. 조회 결과가 없으면 예외 던짐

    • read...By

    find...By 와 동일하게 동작

    • query...By

    find...By 와 동일하게 동작

    • search...By

    find...By 와 동일하게 동작

    • stream...By

    find...By 와 동일하게 동작 반환 타입Stream으로 지정하여 스트림 방식으로 결과를 처리할 수 있다.

    특정 데이터 존재 유무 확인

    • exists...By

    특정 데이터 존재 확인 boolean 반환

    boolean existsByNumber(Long number);

    #### 쿼리 결과 레코드 개수 반환 - #### count...By ```java long countByName(String name); ```
    #### 삭제 쿼리 수행 - #### delete...By 리턴 타입 void ```java void deleteByNumber(Long number); ``` - #### remove...By 리턴 타입 long : 삭제 횟수 ```java long removeByName(String name); ```
    #### 쿼리 결과의 개수를 제한 한번의 동작으로 여러건을 조회할 때 사용. 단건 조회시 <number> 생략 - #### ...First<number>By ```java List findFirst5ByName(String name); ``` - #### ...Top<number>By ```java List findTop10ByName(String name); ```



    - 서술부 : 조건자 키워드

    Is

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

    생략되는 경우가 많으며, Equals 와 동일한 기능을 수행한다.

    // findByNumber 메서드와 동일한 동작
    Product findByNumberIs(Long number); 
    Product findByNumberEquals(Long number); 

    (Is)Not

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

    Is는 생략 가능

    Product findByNumberIsNot(Long number);
    Product findByNumberNot(Long number);

    (Is)Null , (Is)NotNull

    값이 null인지 검사하는 키워드

    List<Product> findByUpdatedAtNull();
    List<Product> findByUpdatedAtIsNull();
    List<Product> findByUpdatedAtNotNull();
    List<Product> findByUpdatedAtIsNotNull();

    (Is)True , (Is)False

    데이터 타입이 boolean인 컬럼의 값을 확인하는 키워드

    Product findByidActiveTrue();
    Product findByidActiveIsTrue();
    Product findByidActiveFalse();
    Product findByidActiveIsFalse();

    And , Or

    여러 조건을 묶을 때 사용

    Product findByNumberAndName(Long number, String name);
    Product findByNumberOrName(Long number, String name);

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

    숫자datetime 컬럼을 대상으로 한 비교 연산에 사용할 수 있는 키워드

    GreaterThan과 LessThan은 기본적으로 경곗값은 포함하지 않으며 경곗값포함하려면 Equal 키워드를 추가하면 된다.

    (Is)GreaterThan : 비교 대상에 대한 초과의 개념으로 비교연산 수행 (Is)LessThan : 비교 대상에 대한 미만의 개념으로 비교연산 수행 (Is)Between : 비교 대상에 대해 범위를 적용하여 연산 수행

    // price보다 큰 값을 갖는 Price컬럼 조회
    List<Product> findByPriceIsGreaterThan(Long price); 
    List<Product> findByPriceGreaterThan(Long price);
     
     // price 값 이상인 Price 컬럼 조회
    List<Product> findByPriceGreaterThanEqual(Long price);
     
     // price보다 작은 값을 갖는 Price 컬럼 조회
    List<Product> findByPriceIsLessThan(Long price);
    List<Product> findByPriceIsLessThan(Long price);
     
     // price 값 이하인 Price 컬럼 조회
    List<Product> findByPriceIsLessThanEqual(Long price);
    
    // lowPrice 이상 highPrice 이하인 Price 컬럼 조회
    List<Product> findByPriceIsBetween(Long lowPrice, Long highPrice);
    List<Product> findByPriceBetween(Long lowPrice, Long highPrice);

    StartsWith , EndsWith , Contains , (Is)Like

    컬럼 값에서 일부 일치 여부를 확인하는 키워드

    SQL 쿼리에서 % 키워드와 동일한 역할을 한다.

    • (Is)StartingWith == StartsWith 문자열의 검색

    • (Is)EndingWith == EndsWith 문자열의 검색

    • (Is)Containing == Contains 문자열의 양 끝 검색

    • (Is)Like
      SQL의 Like 절 조건문에서와 마찬가지로 메서드에서 전달하는 인자%명시적으로 입력해주여야 한다.

    // name컬럼 값이 '김'으로 시작하는 데이터 조회
    List<Product> findByNameLike(String name); // String name = "김%"
    List<Product> findByNameIsLike(String name); // String name = "김%"
    
    // name 값이 name 컬럼에 포함돼있는 데이터 조회
    List<Product> findByNameContains(String name); 
    List<Product> findByNameContaining(String name);
    List<Product> findByNameIsContaining(String name);
    
    // name 값으로 시작하는 name 컬럼 데이터 조회
    List<Product> findByNameStartsWith(String name);
    List<Product> findByNameStartingWith(String name);
    List<Product> findByNameIsStartingWith(String name);
    
    // name 값으로 끝나는 name 컬럼 데이터 조회
    List<Product> findByNameEndsWith(String name);
    List<Product> findByNameEndingWith(String name);
    List<Product> findByNameIsEndingWith(String name);



        🐢🎈


    3. 정렬과 페이징 처리

    기본적인 정렬과 페이징 처리 방법에 대해 학습

    1) 정렬 처리하기

    - 쿼리 메서드 사용

    단일 조건 정렬

    일반적인 쿼리문에서 정렬을 사용할 때는 Order by 구문을 사용한다. 쿼리 메서드에서도 정렬 기능에 동일한 키워드가 사용된다.

    // Asc : 오름차순 ㄱㄴㄷ , Desc : 내림차순 ㄷㄴㄱ
    List<Product> findByNameOrderByNumberAsc(String name);
    List<Product> findByNameOrderByNumberDesc(String name);

    다중 조건 정렬

    정렬을 위한 메서드를 작성할 때, 정렬 조건을 여러개로 나타내야 할 경우 And 나 Or 키워드를 사용하지 않고 우선순위를 기준으로 차례대로 작성해주면 된다.

    List<Product> findByNameOrderByPriceAscStockDesc(String name);

    다만, 이렇게 여러개의 조건을 메서드 이름으로 나열할 경우 가독성이 떨어지는 문제가 생긴다.


    - Sort : 정렬 조건을 매개변수로 받기

    Sort 클래스를 매개변수로 전달하여 정렬을 처리할 수도 있다.

    List<Product> findByName(String name, Sort sort);

    사용 예시1

    productRepository
          .findByName(
              "펜",
              Sort.by(
                 Sort.Order.asc("price"),
                 Sort.Order.desc("stock")
              )
          );

    사용 예시2

    @Test
    void sortingAndPagingTest() {
    	...
    	productRepository.findByName("펜", getSort())
    }
    
    private Sort getSort() {
    	return Sort.by(
        		Order.asc("price"),
        		Order.asc("stock")
        );
    }



    2) 페이징 처리

    페이징은

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

    JPA에서는 페이징 처리를 위해 PagePageable을 사용한다.

    페이징 처리를 위한 쿼리 메서드 예시

    Page<Product> findByName(String name, Pageable pageable);

    리턴 타입으로 Page를 설정하고, 매개변수에 Pageable타입의 객체를 정의한다.

    위의 예제를 사용하기 위해서는 아래와 같이 호출한다.

    Page<Product> productPage 
            = productRepository
            		.findByName(
                        "펜",
                        PageRequest.of(0, 2)         
    		  		);

    Pageable을 매개변수로 받으면, 해당 메서드의 반환 타입은 Page 객체가 된다.

    PageRequest는 Pageable의 구현체이다.

    🐢 pageable 정리했던 포스팅



        _____________________🎈____________________


    4. @Query

    데이터베이스에서 값을 가져올 때는

    앞 절에서 소개한 것처럼

    메서드의 이름만으로 쿼리 메서드를 생성할 수도 있고

    이번 절에서 살펴볼 @Query 어노테이션을 사용해

    직접 JPQL을 작성할 수도 있다.


    데이터베이스를 다른 데이터베이스로 변경할 일이 없다면

    직접 특화된 SQL을 작성할 수 있으며,

    주로 튜닝된 쿼리를 사용하고자 할때 직접 SQL을 작성한다.

    - JPQL을 사용해 상품정보 조회 실습

    Repository에 JPQL 이용 상품정보 조회 메서드 추가

    WHERE 절의 파라미터에 매개변수 전달하는 방법 1

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

    WHERE절에서 사용된 ?1은 첫 번째 파라미터에 매개변수를 전달받기 위해 사용된 것이다.

    이 방법은 순서가 꼬이기 쉽고, 그만큼 오류가 나기 쉽기 때문에 추천하지 않고


    WHERE 절의 파라미터에 매개변수 전달하는 방법 2

    @Param 어노테이션을 사용한 다음의 방법을 추천한다.

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

    엔티티 타입이 아닌 원하는 컬럼 값만 추출해보기

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



        🐢🎈


    5. QueryDSL

    메서드의 이름을 기반으로 생성하는 JPQL의 한계를

    @Query 어노테이션을 통해 대부분 해소할 수 있지만

    직접 문자열을 입력하기 때문에,

    컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있다.


    이러한 이유로

    개발환경에서는 문제가 없는 것처럼 보이다가

    실제 운영 환경에 어플리케이션을 배포하고 나서

    오류가 발견되는 리스크를 유발한다.


    이와 같은 문제를 해결하기 위해 사용되는 것이 QueryDSL이다.


    QueryDSL은 문자열이 아니라 코드로 쿼리를 작성할 수 있도록 도와준다.



    - QueryDSL 이란?

    정적 타입을 이용해

    SQL과 같은 쿼리생성할 수 있도록 지원하는

    프레임워크이다.


    문자열이나 XML 파일을 통해 쿼리를 작성하는 대신

    QueryDSL이 제공하는 플루언트 API를 활용해

    쿼리를 생성할 수 있다.


    - QueryDSL 장점

    1. IDE가 제공하는 코드 자동 완성 기능을 사용할 수 있다.

    2. 문법적으로 잘못된 쿼리를 허용하지 않는다.

    3. 고정된 SQL 쿼리를 작성하지 않기 때문에 동적으로 쿼리를 생성할 수 있다.

    4. 코드로 작성하므로 가독성과 생산성이 향상된다.

    5. 도메인 타입과 프로퍼티를 안전하게 참조할 수 있다.


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

    gradle 설정

    참고한 블로그

    spring boot 2.7.12 기준

    1. 의존성 추가

    Spring Boot 2.6 이상 버전에서는 Querydsl 5.0을 사용한다. querydsl-jpa querydsl-apt


    2. plugin 추가

    plugins {
      ...
      id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
    }

    3. 관련 설정 추가

    // -------- queryDSL 설정 추가 -------------------
    // querydsl에서 사용할 경로 설정
    def querydslDir = "$buildDir/generated/querydsl"
    // JPA 사용 여부와 사용할 경로를 설정
    querydsl {
      jpa = true
      querydslSourcesDir = querydslDir
    }
    // build 시 사용할 sourceSet 추가
    sourceSets {
      main.java.srcDir querydslDir
    }
    // querydsl 컴파일시 사용할 옵션 설정
    compileQuerydsl{
      options.annotationProcessorPath = configurations.querydsl
    }
    // querydsl 이 compileClassPath 를 상속하도록 설정
    configurations {
      compileOnly {
          extendsFrom annotationProcessor
      }
      querydsl.extendsFrom compileClasspath
    }

    4. gradle build 후 어플리케이션 실행

    gradle build 후 어플리케이션을 실행하면 Q도메인 파일이 생성된다.

    나는 이 프로젝트에서 사용한 도메인 이름이 Product라서 파일 이름이 QProduct 라고 생겼다.


    Q도메인 파일은 아래 위치에 있다. jpaStudy   - build       - generated             - querydsl                   - proj.package.path.data.entity                        - QProduct.java


    - 기본적인 QueryDSL 사용하기

    1. 테스트 코드를 통해 QueryDSL의 동작을 확인

    test 전체 코드

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

    1-2. JPAQueryFactory 사용으로 전체 컬럼에 대해 조건 처리 후 가져오기

    @PersistenceContext
    EntityManager entityManager;
    
    @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("==========================================");
        }
    }

    1-3. JPAQueryFactory 사용으로 일부 컬럼에 대해 조건 처리 후 가져오기

    @PersistenceContext
    EntityManager entityManager;
    
    @Test
    void queryDslTest3() {
        JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
        QProduct qProduct = QProduct.product;
    
        // Select 대상이 1개인 경우
        List<String> productList = jpaQueryFactory
                                    .select(qProduct.name)
                                    .from(qProduct)
                                    .where(qProduct.name.eq("펜"))
                                    .orderBy(qProduct.price.asc())
                                    .fetch();
    
        productList.forEach(productName -> {
            System.out.println("=============================");
            System.out.println("productName : " + productName);
            System.out.println("=============================");
        });
    
        // Select 대상이 여러개인 경우
        List<Tuple> tupleList = jpaQueryFactory
                                    .select(qProduct.name, qProduct.price)
                                    .from(qProduct)
                                    .where(qProduct.name.eq("펜"))
                                    .orderBy(qProduct.price.asc())
                                    .fetch();
        
        tupleList.forEach(product -> {
            System.out.println("=============================");
            System.out.println("productName  : " + product.get(qProduct.name));
            System.out.println("productPrice : " + product.get(qProduct.price));
            System.out.println("=============================");
        });
    }



    2. 실제 비즈니스 로직에서 활용

    JPAQueryFactory 객체를 Bean으로 등록하여

    필요시 객체를 Spring 에서 주입하도록 설정

    2-1. config 클래스 생성

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

    2-2. TEST

    JPAQueryFactory를 Spring에서 주입해줌으로써 EntityManager 와 JPAQueryFactory 초기화 과정이 필요없어졌다.

    @Autowired
    JPAQueryFactory jpaQueryFactory;
    
    @Test
    void queryDslTest3() {
        QProduct qProduct = QProduct.product;
    
        List<String> productList = jpaQueryFactory
                                    .select(qProduct.name)
                                    .from(qProduct)
                                    .where(qProduct.name.eq("펜"))
                                    .orderBy(qProduct.price.asc())
                                    .fetch();
    
        productList.forEach(productName -> {
            System.out.println("=============================");
            System.out.println("productName : " + productName);
            System.out.println("=============================");
        });
    }



    - QuerydslPredicateExecutor ,  QuerydslRepositorySupport  활용

    Spring Data JPA 에서는

    QueryDSL을 더욱 편하게 사용할 수 있게

    QuerydslPredicateExcutor 인터페이스와

     QuerydslRepositorySupport  클래스를 제공한다.



    1. QuerydslPredicateExcutor 인터페이스

    QuerydslPredicateExcutor는

    JpaRepository와 함께

    리포지토리에서 QueryDSL을 사용할 수 있게 인터페이스를 제공한다.


    QuerydslPredicateExcutor 인터페이스를 활용하면

    더욱 편하게 QueryDSL을 사용할 수 있지만

    join , fetch 사용 불가


    실습!

    1-1. QProductRepository 생성

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

    1-2. Test 코드 실습

    @SpringBootTest
    class QProductRepositoryTest {
    
        @Autowired
        QProductRepository qProductRepository;
    
        @Test
        void queryDSLTest2() {
            QProduct qProduct = QProduct.product;
    
            Iterable<Product> productList = qProductRepository.findAll(
                    qProduct.name.contains("펜")
                            .and(qProduct.price.between(550, 1500))
            );
    
            productList.forEach(product -> {
                System.out.println(product.getNumber());
                System.out.println(product.getStock());
            });
        }
    
        @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());
            }
        }
    }



    2.  QuerydslRepositorySupport  추상 클래스 사용하기

    QuerydslRepositorySupport 클래스 역시

    QueryDSL 라이브러리를 사용하는데 유용한 기능을 제공한다.


    가장 보편적인 사용방식은

    CustomRepository를 활용해 리포지토리를 구현하는 방식이다.


    간단하게 구조를 설명하자면 다음과 같다.

    1. 먼저 앞에서 사용했던 방식처럼 JpaRepository를 상속받는 ProductRepository생성한다.

    2. 이때 직접 구현한 쿼리를 사용하기 위해서는 JpaRepository를 상속받지 않는 리포지토리 인터페이스ProductRepositoryCustom생성한다. 이 인터페이스에 정의하고자 하는 기능들을 정의!

    3. ProductRepositoryCustom에서 정의한 메서드를 사용하기 위해 ProductRepository에서 ProductRepositoryCustom을 상속받는다.

    4. ProductRepositoryCustom에서 정의된 메서드를 기반으로 실제 쿼리 작성을 하기 위해 구현체인 ProductRepositoryCustomImpl 클래스를 생성한다.

    5. ProductRepositoryCustomImple 클래스에서는 다양한 방법으로 쿼리를 구현할 수 있지만 QueryDSL을 사용하기 위해 QueryDslRepositorySupport를 상속받는다.


    위와 같이 구성하면

    DAO나 서비스에서

    리포지토리에 접근하기 위해 ProductRepository를 사용한다.

    ProductRepository를 활용함으로써 QueryDSL의 기능도 사용할 수 있게 된다.


    실습!

    2-1. ProductRepository , ProductRepositoryCustom , ProductRepositoryCustomImpl 생성

    이전에 만들어둔 인터페이스 이름과 겹치지 않게

    support 패키지 생성 후 이 안에 구현.


    2-1-1. ProductRepositoryCustom

    public interface ProductRepositoryCustom {
        List<Product> findByName(String name);
    }

    2-1-2. ProductRepositoryCustomImpl

    @Component
    public class ProductRepositoryCustomImpl extends QuerydslRepositorySupport implements ProductRepositoryCustom {
    
        // QuerydslRepositorySupport 상속시 필수 구현 부분 
        // 생성자를 통해 도메인 클래스를 부모 클래스에 전달해야 한다.
        public ProductRepositoryCustomImpl() {
            super(Product.class);
        }
    
        @Override
        public List<Product> findByName(String name) {
            QProduct product = QProduct.product;
    
            return from(product) // QuerydslRepositorySupport 제공 메서드. JPAQuery를 반환한다.
                    .where(product.name.eq(name)) // 이하 QueryDSL(JPAQuery) 메서드
                    .select(product)
                    .fetch();
        }
    }

    2-1-3. ProductRepository

    @Repository("productRepositorySupport") // 다른 파일과 같은 이름으로, bean 충돌 방지를 위해 bean 이름 지정
    public interface ProductRepository extends JpaRepository<Product, Long> , 
            ProductRepositoryCustom {
    }

    2-2. Test 코드 실습

    리포지토리를 생성하면서

    모든 로직을 구현했기 때문에

    findByName() 메서드를 사용할 때는

    간단히 구현해서 사용할 수 있다.

    @SpringBootTest
    class ProductRepositoryTest {
    
        @Autowired
        ProductRepository productRepository;
    
        @Test
        void findByNameTest() {
            List<Product> productList = productRepository.findByName("펜");
    
            productList.forEach(product -> {
                System.out.println(product.getNumber());
                System.out.println(product.getName());
                System.out.println(product.getPrice());
                System.out.println(product.getStock());
            });
    
        }
    }



        🐢🎈



    6. JPA Auditing

    JPA에서 Auditing이란 감시하다라는 뜻으로

    각 데이터마다 누가, 언제 데이터를 생성했고 변경했는지

    감시한다는 의미로 사용됨


    엔티티 클래스에는 공통적으로 들어가는 필드가 있다.

    예를 들면, 생성 일자변경 일자가 있다.


    이런 필드들은

    매번 생성하가나 변경할 때 마다 값을 주입해야 하는 번거로움이 있다.

    이런 번거로움을 해결하기 위해 JPA Auditing을 사용한다.



    - JPA Auditing 기능 활성화

    스프링 부트 애플리케이션에 Auditing 기능을 활성화해야 한다.

    Config 파일을 새로 만든 후 @EnableJpaAuditing 어노테이션을 추가해주면 된다.

    @Configuration
    @EnableJpaAuditing
    public class JpaAuditingConfiguration {
        
    }

    - BaseEntity 만들기

    각 엔티티에 공통으로 들어가게 되는 컬럼을 하나의 클래스로 분리하고

    @MappedSuperclass : JPA 엔티티 클래스가 상속받을 경우 자식 클래스에게 매핑 정보를 전달한다.

    @EntityListeners(AuditingEntityListener.class) : 엔티티를 데이터베이스에 적용하기 전후로 콜백을 요청할 수 있게 한다.

    위 2개의 어노테이션은 꼭 달아주어야 한다.

    @Getter
    @Setter
    @ToString
    @SuperBuilder
    @NoArgsConstructor
    @AllArgsConstructor
    @MappedSuperclass // JPA 엔티티 클래스가 상속받을 경우 자식 클래스에게 매핑 정보를 전달한다.
    @EntityListeners(AuditingEntityListener.class) // 엔티티를 데이터베이스에 적용하기 전후로 콜백을 요청할 수 있게 한다.
    public class BaseEntity {
    
        @CreatedDate // 데이터 생성 날짜 자동 주입
        @Column(updatable = false)
        private LocalDateTime createdAt;
    
        @LastModifiedDate // 데이터 수정 날짜 자동 주입
        private LocalDateTime updatedAt;
    
    }

    엔티티 파일에 BaseEntity 상속 및 코드 정리

    @EqualsAndHashCode @ToString

    위의 두 어노테이션은 callSuper 옵션을 true로 설정해주고,

    @Builder 어노테이션은 @SuperBuilder 어노테이션으로 변경해준다.

    그리고 BaseEntity를 상속받는다.

    @EqualsAndHashCode(callSuper = true) // callSuper = true : 부모 클래스 필드 포함
    @ToString(callSuper = true) // callSuper = true : 부모 클래스 필드 포함
    @Getter
    @Setter
    @SuperBuilder
    @NoArgsConstructor
    @AllArgsConstructor
    @Entity
    @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;
    
    }

    - Test

    @Autowired
    ProductRepository productRepository;
    
    @Test
    void auditingTest() {
        Product product = Product.builder()
                                .name("펜")
                                .price(1000)
                                .stock(100)
                                .build();
    
        Product save = productRepository.save(product);
    
        System.out.println("product name : " + save.getName());
        System.out.println("createdAt : " + save.getCreatedAt());
    }

    테스트 통과 확인



        🐢🎈

Designed by Tistory / Custom by 얼거스