[스프링 부트 핵심 가이드] 08. Spring Data JPA 활용
본 게시글은 '스프링 부트 핵심 가이드' 책의 내용을 정리한 것입니다.
저자 : 장정우
출판사 : 위키북스
8.1 프로젝트 생성
책에서 실습을 위해 구성한 프로젝트 설정은 다음과 같다.
- groupId : com.springboot
- artifactId : advanced_jpa
- name : advanced_jpa
- Developer Tools : Lombok, Spring Configuration Processor
- Web : Spring Web
- SQL : Spring Data JPA, MariaDB Driver
+ 6장에서 작성한 자바 파일들
8.2 JPQL
JPA에서 사용할 수 잇는 쿼리를 의미한다. 테이블이나 칼럼의 이름을 사용하는 SQL과 달리 JPQL은 매핑된 엔티티의 이름과 필드를 사용한다는 것이 차이점이다.
SELECT p FROM Product p WHERE p.number = ?1;
8.3 쿼리 메서드 살펴보기
8.3.1 쿼리 메서드의 생성
쿼리 메서드는 동작을 결정하는 주제(Subject)와 서술어(Predicate)로 구분한다. 서술어 부분은 검색 및 정렬 조건을 지정하는 영역이다. 기본적으로 엔티티의 속성으로 정의할 수 있고, AND나 OR를 사용해 조건을 확장할 수도 있다.
//(리턴 타입) + {주제 + 서술어(속성)} 구조의 메서드
List<Person> findByLastnameAndEmail(String lastName, String email);
8.3.2 쿼리 메서드의 주제 키워드
◎ 조회 키워드
조회 기능을 수행하는 키워드이다.
- find...By
- read...By
- get...By
- query...By
- search...By
- stream...By
'...' 부분에 도메인(엔티티)을 표현할 수 있으나, 리포지토리에서 도메인을 설정한 후에 메서드를 사용하기 때문에 중복으로 판단해 생략하기도 한다. 리턴 타입으로는 Collection이나 Stream에 속한 하위 타입을 설정할 수 있다.
//find...By
Optional<Product> findByNumber(Long number);
List<Product> findAllByName(String name);
Product queryByNumber(Long Number);
◎ exists...By
특정 데이터가 존재하는지 확인하는 키워드이다. 리턴 타입으로는 boolean 타입을 사용한다,
//exists...By
boolean existsByNumber(Long number);
◎ count...By
조회 쿼리 결과로 나온 레코드의 개수를 리턴한다.
//count...By
long countByName(String name);
◎ delete...By, remove...By
삭제 쿼리를 수행한다. 리턴 타입이 없거나 삭제한 횟수를 리턴한다.
//delete...By, remove...By
void deleteByNumber(Long number);
long removeByName(String name);
◎ ...First<Number>..., ...Top<Number>...
조회된 결괏값의 개수를 제한하는 키워드이다. 주제와 By 사이에 위치한다. 일반적으로 한 번의 동작으로 여러 건을 조회할 때 사용되며, 단 건으로 조회하기 위해서는 <Number>를 생략하면 된다.
//...First<number>..., ...Top<number>...
List<Product> findFirst5ByName(String name);
List<Product> findTop10ByName(String name);
8.3.3 쿼리 메서드의 조건자 키워드
◎ Is
값의 일치를 조건으로 사용하는 키워드이다. 생략되는 경우가 많으며 Equals와 동일한 기능을 수행한다.
// findByNumber 메서드와 동일하게 동작
Product findByNumberIs(Long number);
Product findByNumberEquals(Long number);
◎ (Is)Not
값의 불일치를 조건으로 사용하는 키워드이다. Is는 생략하고 Not 키워드만 사용할 수도 있다.
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 findByisActiveTrue();
Product findByisActiveIsTrue();
Product findByisActiveFalse();
Product findByisActiveIsFalse();
◎ And, Or
여러 조건을 묶을 때 사용한다.
Product findByNumberAndName(Long number, String name);
Product findByNumberOrName(Long number, String name);
◎ (Is)GreaterThan, (Is)LessThan, (Is)Between
숫자나 datetime 칼럼을 대상으로 한 비교 연산에 사용할 수 있는 키워드이다. GreaterThan, LessThan 키워드는 비교 대상에 대한 초과/미만의 개념으로 비교 연산을 수행하고, 경곗값을 포함하려면 Equal 키워드를 추가한다.
List<Product> findByPriceIsGreaterThan(Long price);
List<Product> findByPriceGreaterThan(Long price);
List<Product> findByPriceGreaterThanEqual(Long price);
List<Product> findByPriceIsLessThan(Long price);
List<Product> findByPriceLessThan(Long price);
List<Product> findByPriceLessThanEqual(Long price);
List<Product> findByPriceIsBetween(Long lowPrice, Long highPrice);
List<Product> findByPriceBetween(Long lowPrice, Long highPrice);
◎ (Is)StartingWith(==StartsWith), (Is)EndingWith(==EndsWith), (Is)Containing(==Contains), (Is)Like
칼럼값에서 일부 일치 여부를 확인하는 키워드이다. SQL에서 '%' 키워드와 동일한 역할을 한다. 생성되는 SQL문을 보면 Containing 키워드는 문자열 양 끝, StartingWith 키워드는 문자열 앞, EndingWith 키워드는 문자열 끝에 '%'가 배치된다. Like 키워드는 코드 수준에서 메서드를 호출하면서 전달하는 값에 %를 명시적으로 입력해야 한다.
List<Product> findByNameLike(String name);
List<Product> findByNameIsLike(String name);
List<Product> findByNameContains(String name);
List<Product> findByNameContaining(String name);
List<Product> findByNameIsContaining(String name);
List<Product> findByNameStartsWith(String name);
List<Product> findByNameStartingWith(String name);
List<Product> findByNameIsStartingWith(String name);
List<Product> findByNameEndsWith(String name);
List<Product> findByNameEndingWith(String name);
List<Product> findByNameIsEndingWith(String name);
8.4 정렬과 페이징 처리
8.4.1 정렬 처리하기
OrderBy 키워드를 사용한다.
// Asc : 오름차순, Desc : 내림차순
List<Product> findByNameOrderByNumberAsc(String name);
List<Product> findByNameOrderByNumberDesc(String name);
여러 정렬 기준을 사용할 경우 우선순위를 기준으로 차례대로 작성한다.
// And를 붙이지 않음
List<Product> findByNameOrderByPriceAscStockDesc(String name);
매개변수를 활용해 정렬할 수도 있다.
List<Product> findByName(String name, Sort sort);
Sort 객체를 활용해 매개변수로 받아들인 정렬 기준을 가지고 쿼리문을 작성한다.
productRepository.findByName("펜",Sort.by(Order.asc("price")));
productRepository.findByName("펜",Sort.by(Order.asc("price"),Orderdesc("stock")));
Sort 클래스는 내부 클래스로 정의돼 있는 Order 객체를 활용해 정렬 기준을 생성한다. Order 객체에는 asc와 desc 메서드가 포함돼 있어 이 메서드를 통해 오름차순/내림차순을 지정하여, 여러 정렬 기준을 사용할 경우에는 콤마(,)를 사용해 구분한다.
매개변수를 활용한 쿼리 메서드를 사용하면 쿼리 메서드를 정의하는 단계에서 코드가 줄어드는 장점이 있으나, 호출하는 위치에서는 여전히 정렬 기준이 길어져 가독성이 떨어진다. 하지만 Sort 부분을 하나의 메서드로 분리해서 쿼리 메서드를 호출하는 코드를 작성하는 방법도 가능하다.
@SpringBootTest
class ProductRepositoryTest{
@Autowired
ProductRepository productRepository;
@Test
void sortingAndPagingTest(){
System.out.println(productRepository.findByName("펜",getSort()));
}
private Sort getSort(){
return Sort.by(
Order.asc("price"),
Order.desc("sotck")
);
}
}
8.4.2 페이징 처리
JPA에서는 페이징 처리를 위해 Page와 Pageable을 사용한다. 페이징 처리를 위한 쿼리 메서드는 다음과 같이 작성한다.
Page<Product> findByName(String name, Pageable pageable);
리턴 타입으로는 Page를 설정하고 매개변수에는 Pageable 타입의 객체를 정의한다.
이 메서드는 다음과 같이 호출한다.
Page<Product> productPage = productRespository.findByName("펜",PageRequest.of(0,2));
System.out.println(productPage);
//Page 객체를 그대로 출력하면 몇 번째 페이지에 해당하는지만 확인 가능
System.out.println(productPage.getContent());
//페이지를 구성하는 세부적인 값을 보려면 getContent()메서드를 사용하면 배열 형태로 출력된다.
Pageable 파라미터를 전달하기 위해 PageRequest 클래스를 사용한다. 일반적으로 PageRequest는 of 메서드를 통해 PageRequest 객체를 생성한다. of 메서드는 다음과 같은 매개변수 조합을 지원한다.
of 메서드 | 매개변수 설정 | 비고 |
of(int page, int size) | 페이지 번호(0부터 시작), 페이지당 데이터 개수 | 데이터를 정렬하지 않음 |
of(int page, int size, Sort) | 페이지 번호, 페이지당 데이터 개수, 정렬 | sort에 의해 정렬 |
of(int page, int size, Direction, String... properties) | 페이지 번호, 페이지당 데이터 개수, 정렬 방향, 속성(칼럼) | Sort.by(direction,properties)에 의해 정렬 |
Page 객체의 세부적인 값을 확인하려면 getContent() 메서드를 사용하면 배열 형태로 값이 출력된다.
8.5 @Query 어노테이션 사용하기
데이터베이스에서 값을 가져올 때는 메서드의 이름만으로 쿼리 메서드를 생성할 수도 있고, @Query 어노테이션을 사용해 직접 JPQL을 작성할 수도 있다.
JPQL을 사용하려면 JPA 구현체에서 자동으로 쿼리 문장을 해석하고 실행한다. 데이터베이스를 다른 데이터베이스로 변경할 일이 없다면 직접 해당 데이터베이스에 특화된 SQL을 작성할 수 있고, 주로 튜닝된 쿼리를 사용하고자 할 때 직접 SQL을 작성한다.
@Query("SELECT p FROM Product AS p WHERE p.name = ?1")
List<Product> findByName(String name);
(쿼리문에서 SQL예약어에 해당하는 단어는 대소문자 상관없다.)
(별칭 생성에 쓰이는 AS는 생략 가능)
조건문에서 사용한 '?1'은 파라미터를 전달받기 위한 인자에 해당한다. 1은 첫 번째 파라미터를 의미한다. 파라미터의 순서가 바뀌면 오류가 발생할 수 있어 @Param 어노테이션을 사용하는 것이 좋다.
@Query("SELECT p FROM Product 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이다.
8.6.1 QueryDSL이란?
QueryDSL은 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크다. 문자열이나 XML 파일 대신 QueryDSL이 제공하는 플루언트(Fluent) API를 활용해 쿼리를 생성할 수 있다.
8.6.2 QueryDSL의 장점
- IDE가 제공하는 코드 자동 완성 기능을 사용할 수 있다.
- 문법적으로 잘못된 쿼리를 허용하지 않는다.
- 고정된 SQL 쿼리를 작성하지 않기 때문에 동적으로 쿼리를 생성할 수 있다.
- 코드로 작성하므로 가독성 및 생산성이 향상된다.
- 도메인 타입과 프로퍼티를 안전하게 참조할 수 있다.
8.6.3 QueryDSL을 사용하기 위한 프로젝트 설정
pom.xml 파일에 의존성 및 플러그인 추가
<dependencies>
<!-- 생략 -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
<!-- 생략 -->
</dependencies>
<plugins>
<!-- QueryDSL을 사용하기 위한 플러그인 추가 -->
<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>
</plugins>
JPAAnnotationProcessor는 @Entity 어노테이션으로 정의된 엔티티 클래스를 찾아서 쿼리 타입을 생성한다.
※ APT란?
APT(Annotation Processing Tool)는 어노테이션으로 정의된 코드를 기반으로 새로운 코드를 생성하는 기능이다. JDK 1.6부터 도입된 기능이며, 클래스를 컴파일하는 기능도 제공한다.
작성 후 메이븐의 compile 단계 클릭해 빌드 작업 수행한다.
그럼 outpputDirectory에서 설정한 경로에 Q도메인 클래스가 생성된다.
QueryDSL은 엔티티 클래스와 Q도메인(Qdomain)이라는 쿼리 타입의 클래스를 자체적으로 생성해서 메타데이터로 사용하는데, 이를 통해 SQL과 같은 쿼리를 생성해서 제공한다.
8.6.4 기본적인 QueryDSL 사용하기
JPAQuery를 활용한 QueryDSL 코드
@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();
}
QueryDSL에 의해 생성된 Q도메인 클래스를 활용한다. 다만 Q도메인 클래스와 대응되는 테스트 클래스가 없으므로 엔티티 클래스에 대응되는 리포지토리의 테스트 클래스에 포함해도 무관하다.
QueryDSL을 사용하기 위해서는 JPAQuery 객체를 사용한다. JPAQuery는 엔티티 매니저를 활용해 생성한다. 생성된 JPAQuery는 빌더 형식으로 작성한다.(SQL 쿼리에서 사용되는 키워드로 메서드가 구성 돼 있다.)
List 타입으로 값을 리턴 받기 위해서는 fetch() 메서드를 사용해야 한다.
반환 메서드로 사용할 수 있는 메서드는 다음과 같다.
- List<T> fetch() : 조회 결과를 리스트로 반환한다.
- T fetchOne : 단 건의 조회 결과를 반환한다.
- T fetchFirst() : 여러 건의 조회 결과 중 1건을 반환한다.
- Long fetchCount() : 조회 결과의 개수를 반환한다.
- QueryResult<T> fetchResults() : 조회 결과 리스트와 개수를 포함한 QueryResults를 반환한다.
JPAQuery 대신 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();
}
@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();
List<Tuple> tupleList = jpaQueryFactory
.select(qProduct.name, qProduct.price)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
}
JPAQueryFactory에서는 select 절부터 작성 가능하다. 일부만 조회하고 싶다면 selectFrom()이 아닌 select()와 from() 메서드를 구분해서 사용할 수 있다. 조회 대상이 여러 개일 경우는 쉼표(,)로 구분해서 작성하고, 리턴 타입을 List<String> 타입이 아닌 List<Tuple> 타입으로 지정한다.
다음과 같이 Config 클래스를 생성하면 쉽게 사용할 수 있다.
@Configuration
public class QueryDSLConfiguration {
@PersistenceContext
EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
다음 코드처럼 활용한다.(JPQueryFactory 객체를 @Bean 객체로 등록해서 초기화하지 않고 스프링 컨테이너에서 쓸 수 있다.)
@Autowired
JPAQueryFactory jpaQueryFactory;
@Test
void queryDslTest4() {
QProduct qProduct = QProduct.product;
List<String> productList = jpaQueryFactory
.select(qProduct.name)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
}
8.6.5 QuerydslPredicateExecutor, QuerydslRepositorySupport 활용
QueryDSL을 편하게 사용할 수 있게 스프링 데이터 JPA에서 제공하는 클래스이다.
QuerydslPredicateExecutor 인터페이스
리포지토리에서 QueryDSL을 사용할 수 있게 인터페이스를 제공한다.
public interface QProductRepository extends JpaRepository<Product, Long>,
QuerydslPredicateExecutor<Product> {
}
QuerydslPredicateExecutor에서 제공하는 메서드는 다음과 같다.
public interface QuerydslPredicateExecutor<T> {
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);
}
대부분 Predicate 타입을 매개변수로 받는다. Predicate는 표현식을 작성할 수 있게 QueryDSL에서 제공하는 인터페이스이다.
Predicate 활용은 다음처럼 한다.
@SpringBootTest
public class QProductRepositoryTest {
@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);
}
@Test
public void queryDSLTest2() {
QProduct qProduct = QProduct.product;
Iterable<Product> productList = qProductRepository.findAll(
qProduct.name.contains("펜")
.and(qProduct.price.between(550, 1500))
);
}
}
Predicate는 표현식으로 정의하는 쿼리이다. 처음 예제처럼 명시적으로 정의하고 사용할 수도 있고, 밑의 예제처럼 서술부만 가져다 사용할 수도 있다.
QuerydslPredicateExecutor의 경우 join이나 fetch 기능은 사용할 수 없다는 단점이 있다.
QuerydslRepositorySupport 추상 클래스 사용하기
QuerydslRepositorySupport 클래스를 사용하는 보편적인 방법은 CustomRepository를 활용해 리포지토리를 구현하는 방식이다.
구현 예)
- JPARespository를 상속받는 ProductRepository를 생성한다.
- 직접 구현한 쿼리 사용을 위해 JPARepository를 상속받지 않는 ProductRepositoryCustom 인터페이스를 생성한다. 이 인터페이스에 정의하고자 하는 기능들을 메서드로 정의한다.
- ProductRepositoryCustom에서 정의한 메서드를 사용하기 위해 ProductRepository에서 ProductRepositoryCustom을 상속받는다.
- ProductRepositoryCustom에서 정의된 메서드를 기반으로 실제 쿼리 작성을 하기 위해 구현체인 ProductRepositoryCustomImpl 클래스를 생성한다.
- QueryDSL을 사용하기 위해 ProductRepositoryCustomImpl에서 QuerydslRepositorySupport를 상속받는다.
이렇게 구성하면 DAO나 서비스에서 리포지토리에 접근하기 위해 ProductRepository를 사용한다. ProductRepository를 활용함으로써 QueryDSL의 기능도 사용할 수 있다.
하나씩 살펴보면 다음과 같다.
ProductRepository는 JPARespository와 ProductRepositoryCustom을 상속받는다.
@Repository
public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom {
}
ProductRepositoryCustom에는 구현할 메서드를 정의한다.
public interface ProductRepositoryCustom {
List<Product> findByName(String name);
}
ProductRepositoryCustomImpl 클래스에서는 QueryDSL 사용하기 위해 QuerydslRepositorySupport를 상속받고 ProductRepositoryCustom 인터페이스를 구현한다. QuerydslRepositorySupport를 상속받으면 생성자를 통해 도메인 클래스를 부모 클래스에 전달해야 한다.
@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;
}
}
인터페이스 구현 과정에서 Q도메인 클래스인 Qproduct를 사용해 QuerydslRepositorySupport가 제공하는 기능을 사용한다.
코드를 사용할 때는 다음처럼 ProductRepository만 이용하면 된다.
@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());
}
}
8.7 JPA Auditing 적용
생성 주체, 생성 일자, 변경 주체, 변경 일자와 같은 값들을 자동으로 넣어주는 기능이다.
8.7.1 JPA Auditing 기능 활성화
1. main() 메서드가 있는 클래스에 @EnableJpaAuditing 어노테이션을 추가
@EnableJpaAuditing
@SpringBootApplication
public class AdvancedJpaApplication {
public static void main(String[] args) {
SpringApplication.run(AdvancedJpaApplication.class, args);
}
}
이렇게 작성하면 일부 테스트 코드에서 오류가 발생할 수 있다.
따라서 main() 메서드가 있는 클래스가 아닌 별도의 Configuration 클래스를 생성해서 애플리케이션 클래스의 기능과 분리해서 활성화할 수 있다.
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration {
}
8.7.2 BaseEntity 만들기
코드의 중복을 없애기 위해 각 엔티티에 공통으로 들어가게 되는 칼럼(필드)을 하나의 클래스로 빼는 작업을 수행한다.
@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 엔티티 리스너 클래스
- @CreateDate : 데이터 생성 날짜를 자동으로 주입하는 어노테이션
- @LastModifiedDate : 데이터 수정 날짜를 자동으로 주입하는 어노테이션
Product 엔티티 클래스에서 공통 칼럼을 제거하고 BaseEntity 클래스를 상속받게 한다.
※ JPA Auditing 기능에는 @CreatedBy, @ModifiedBy 어노테이션도 존재한다. 누가 엔티티를 생성했고 수정했는지 자동으로 값을 주입하는 기능이다. 이 기능을 사용하려면 AuditorAware를 스프링 빈으로 등록할 필요가 있다.