중급자를 위해 준비한
[웹 개발, 백엔드] 강의입니다.
JPA(Java Persistence API)를 보다 쉽게 사용할 수 있도록 여러 기능을 제공하는 스프링 데이터 JPA에 대해 학습합니다.
✍️
이런 걸
배워요!
ORM에 대한 이해
JPA 프로그래밍
Bean 생성 방법
스프링 JPA가 어렵게 느껴졌다면?
개념과 원리, 실제까지 확실하게 학습해 보세요.
제대로 배우는
백기선의 스프링 데이터 JPA
JPA(Java Persistence API)를 보다 쉽게 사용할 수 있도록 여러 기능을 제공하는 스프링 데이터 JPA에 대해 학습합니다.
강의 :
https://www.inflearn.com/course/스프링-데이터-jpa#reviews
강의자료 :
https://docs.google.com/document/d/1IjSKwMEsLdNXhRLvFk576VTR03AKTED_3jMsk0bHANg/edit
소스 코드
https://github.com/braverokmc79/springdatajpa
https://github.com/braverokmc79/demojpa3
강좌 소개
Application -> 스프링 데이터 JPA (-> JPA -> JDBC) -> Database
강사 소개
백기선
마이크로소프트(2+) <- 아마존(1) <- 네이버(4.5) <- SLT(2.5) ...
강좌
스프링 프레임워크 입문 (Udemy)
백기선의 스프링 부트 (인프런)
특징
스프링 프레임워크 중독자
JPA 하이버네이트 애호가
유튜브 / 백기선
[2부: 스프링 데이터 JPA 활용]
15.스프링 데이터 JPA 활용 파트 소개
강의 :
https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13758&tab=curriculum
스프링 데이터
SQL & NoSQL 저장소 지원 프로젝트의 묶음.
스프링 데이터 Common
여러 저장소 지원 프로젝트의 공통 기능 제공.
스프링 데이터 REST
저장소의 데이터를 하이퍼미디어 기반 HTTP 리소스로(REST API로) 제공하는 프로젝트.
스프링 데이터 JPA
스프링 데이터 Common이 제공하는 기능에 JPA 관련 기능 추가.
http://projects.spring.io/spring-data/
16.스프링 데이터 Common: Repository
강의 :https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13759&tab=curriculum
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
application.properties
#console color spring.output.ansi.enabled=always #Springboot auto build spring.devtools.livereload.enabled=true spring.devtools.restart.enabled=true #Datasource Configuration #spring.datasource.hikari.maximum-pool-size=4 spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:tcp://localhost/~/querydsl spring.datasource.username=sa spring.datasource.password= spring.jpa.hibernate.ddl-auto=create #spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true #logging.level.org.hibernate.SQL=debug #logging.level.org.hibernate.type.descriptor.sql=trace
PostRepository
package com.jpa.spring; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface PostRepository extends JpaRepository<Post, Long> { Page<Post> findByTitleContains(String title, Pageable pageable); long countByTitleContains(String title) ; }
PostRepositoryTest
package com.jpa.spring; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import java.util.List; @SpringBootTest @Transactional @Rollback(value = false) class PostRepositoryTest { @Autowired PostRepository postRepository; @Test public void crudRepository(){ //Given Post post =new Post(); post.setTitle("hello spring boot common"); Assertions.assertThat(post.getId()).isNull(); //when Post newPost=postRepository.save(post); //Then Assertions.assertThat(newPost.getId()).isNotNull(); //When List<Post> posts =postRepository.findAll(); Assertions.assertThat(posts.size()).isEqualTo(1); Assertions.assertThat(posts).contains(newPost); //when Page<Post> page = postRepository.findAll(PageRequest.of(0, 10)); Assertions.assertThat(page.getTotalElements()).isEqualTo(1); Assertions.assertThat(page.getNumber()).isEqualTo(0); Assertions.assertThat(page.getSize()).isEqualTo(10); Assertions.assertThat(page.getNumberOfElements()).isEqualTo(1); //when page= postRepository.findByTitleContains("spring", PageRequest.of(0, 10)); Assertions.assertThat(page.getTotalElements()).isEqualTo(1); Assertions.assertThat(page.getNumber()).isEqualTo(0); Assertions.assertThat(page.getSize()).isEqualTo(10); Assertions.assertThat(page.getNumberOfElements()).isEqualTo(1); //when long spring=postRepository.countByTitleContains("spring"); //Then } }
17.스프링 데이터 Common 2. 인터페이스 정의하기
(중요 x )
강의 : https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13760&tab=curriculum
Repository 인터페이스로 공개할 메소드를 직접 일일히 정의하고 싶다면
package com.jpa.spring; import org.springframework.data.repository.RepositoryDefinition; import java.util.List; @RepositoryDefinition(domainClass = Comment.class, idClass = Long.class) public interface CommentRepository { Comment save(Comment comment); List<Comment> findAll(); }
package com.jpa.spring; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @SpringBootTest class CommentRepositoryTest { @Autowired CommentRepository commentRepository; @Test public void crud(){ Comment comment=new Comment(); comment.setComment("Hello Comment"); commentRepository.save(comment); List<Comment> all=commentRepository.findAll(); Assertions.assertThat(all.size()).isEqualTo(1); } }
특정 리포지토리 당
@RepositoryDefinition
@RepositoryDefinition(domainClass = Comment.class, idClass = Long.class) public interface CommentRepository { Comment save(Comment comment); List<Comment> findAll(); }
공통 인터페이스 정의
@NoRepositoryBean
@NoRepositoryBean public interface MyRepository<T, ID extends Serializable> extends Repository<T, ID> { <E extends T> E save(E entity); List<T> findAll(); }
18.스프링 데이터 Common 3. Null 처리
강의 : https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13761&tab=curriculum
스프링 데이터 2.0 부터 자바 8의 Optional 지원.
Optional<Post> findById(Long id);
콜렉션은 Null을 리턴하지 않고, 비어있는 콜렉션을 리턴합니다.
스프링 프레임워크 5.0부터 지원하는 Null 애노테이션 지원.
@NonNullApi, @NonNull, @Nullable.
런타임 체크 지원 함.
JSR 305 애노테이션을 메타 애노테이션으로 가지고 있음. (IDE 및 빌드 툴 지원)
인텔리J 설정
Build, Execution, Deployment
Compiler
Add runtime assertion for notnull-annotated methods and parameters
19.스프링 데이터 Common: 쿼리 만들기 개요
강의 : https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13761&tab=curriculum
스프링 데이터 저장소의 메소드 이름으로 쿼리 만드는 방법
메소드 이름을 분석해서 쿼리 만들기 (CREATE)
미리 정의해 둔 쿼리 찾아 사용하기 (USE_DECLARED_QUERY)
미리 정의한 쿼리 찾아보고 없으면 만들기 (CREATE_IF_NOT_FOUND)
쿼리 만드는 방법
리턴타입 {접두어}{도입부}By{프로퍼티 표현식}(조건식)[(And|Or){프로퍼티 표현식}(조건식)]{정렬 조건} (매개변수)
쿼리 찾는 방법
메소드 이름으로 쿼리를 표현하기 힘든 경우에 사용.
저장소 기술에 따라 다름.
JPA: @Query @NamedQuery
package com.jpa.spring; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import java.util.List; //@RepositoryDefinition(domainClass = Comment.class, idClass = Long.class) public interface CommentRepository extends JpaRepository<Comment, Long> { Comment save(Comment comment); List<Comment> findAll(); @Query("select c.comment from Comment as c ") List<String> findByCommentContains(String comment); Page<Comment> findByCommentContains(String comment, Pageable pageable); Page<Comment> findByLikeCountGreaterThanAndPost(int likeCount, Post post, Pageable pageable); }
CommentRepositoryTest
package com.jpa.spring; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import java.util.List; import java.util.Optional; @SpringBootTest class CommentRepositoryTest { @Autowired CommentRepository commentRepository; @Test public void crud(){ Comment comment=new Comment(); comment.setComment("Hello Comment"); commentRepository.save(comment); List<Comment> all=commentRepository.findAll(); Assertions.assertThat(all.size()).isEqualTo(1); Optional<Comment> byId=commentRepository.findById(100l); Assertions.assertThat(byId).isEmpty(); Comment comment1= byId.orElseThrow((IllegalArgumentException::new)); } @Test public void findByTitleContainsTest(){ Comment comment=new Comment(); comment.setComment("aaa"); commentRepository.save(comment); Comment comment2=new Comment(); comment2.setComment("bbb"); commentRepository.save(comment2); List<String> aaa = commentRepository.findByCommentContains("aaa"); aaa.forEach(a-> System.out.println("a = " + a)); Page<Comment> commentPage = commentRepository.findByCommentContains("aaa", PageRequest.of(0, 10)); System.out.println("commentPage.getTotalElements() = " + commentPage.getTotalElements()); } }
20.스프링 데이터 Common: 쿼리 만들기 실습
강의 : https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13763&tab=curriculum
기본 예제
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname); // distinct List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname); // ignoring case List<Person> findByLastnameIgnoreCase(String lastname); // ignoring case List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
정렬
List<Person> findByLastnameOrderByFirstnameAsc(String lastname); List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
페이징
Page<User> findByLastname(String lastname, Pageable pageable); Slice<User> findByLastname(String lastname, Pageable pageable); List<User> findByLastname(String lastname, Sort sort); List<User> findByLastname(String lastname, Pageable pageable);
스트리밍
Stream<User> readAllByFirstnameNotNull();
try-with-resource 사용할 것. (Stream을 다 쓴다음에 close() 해야 함)
21.스프링 데이터 Common: 비동기 쿼리
강의 : https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13764&tab=curriculum
비동기 쿼리
@Async Future<User> findByFirstname(String firstname);
@Async CompletableFuture<User> findOneByFirstname(String firstname);
@Async ListenableFuture<User> findOneByLastname(String lastname);
해당 메소드를 스프링 TaskExecutor에 전달해서 별도의 쓰레드에서 실행함.
Reactive랑은 다른 것임
권장하지 않는 이유
테스트 코드 작성이 어려움.
코드 복잡도 증가.
성능상 이득이 없음.
DB 부하는 결국 같고.
메인 쓰레드 대신 백드라운드 쓰레드가 일하는 정도의 차이.
단, 백그라운드로 실행하고 결과를 받을 필요가 없는 작업이라면 @Async를 사용해서 응답 속도를 향상 시킬 수는 있다.
22.스프링 데이터 Common: 커스텀 리포지토리
강의 : https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13765&tab=curriculum
링크 : https://github.com/braverokmc79/demojpa3
참조 :
https://macaronics.net/index.php/m01/spring/view/2104
쿼리 메소드(쿼리 생성과 쿼리 찾아쓰기)로 해결이 되지 않는 경우 직접 코딩으로 구현 가능.
스프링 데이터 리포지토리 인터페이스에 기능 추가.
스프링 데이터 리포지토리 기본 기능 덮어쓰기 가능.
구현 방법
커스텀 리포지토리 인터페이스 정의
인터페이스 구현 클래스 만들기 (기본 접미어는 Impl)
엔티티 리포지토리에 커스텀 리포지토리 인터페이스 추가
기능 추가하기
기본 기능 덮어쓰기
접미어 설정하기
1) AppConfig
package com.example.demojap3; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class AppConfig { private final EntityManager em; @Bean public JPAQueryFactory queryFactory(){ return new JPAQueryFactory(em); } }
2 )Post
package com.example.demojap3.post; import jakarta.persistence.*; import lombok.Data; import lombok.ToString; import java.util.Date; @Entity @Data @ToString(of={"title", "content"}) public class Post { @Id @GeneratedValue private Long id; private String title; @Lob private String content; @Temporal(TemporalType.TIMESTAMP) private Date created; }
3)PostDto
package com.example.demojap3.post; import com.querydsl.core.annotations.QueryProjection; import lombok.Data; import lombok.ToString; import java.util.Date; @Data @ToString public class PostDto { private Long id; private String title; private String content; private Date created; @QueryProjection public PostDto(Long id, String title, String content, Date created) { this.id = id; this.title = title; this.content = content; this.created = created; } }
4) PostSearchCondition
package com.example.demojap3.post; import lombok.Data; @Data public class PostSearchCondition { private String title; private String content; }
5) PostCustomRepository
package com.example.demojap3.post; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.util.List; public interface PostCustomRepository { public Page<PostDto> searchPostList(PostSearchCondition condition, Pageable pageable) ; public List<Post> findMyPost(); public void deletePost(Post entity); }
6) PostCustomRepositoryImpl
package com.example.demojap3.post; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.support.PageableExecutionUtils; import java.util.List; import static com.example.demojap3.post.QPost.post; import static org.springframework.util.StringUtils.hasText; //@Repository , @Transactional 생략 가능 //@Repository //@Transactional @RequiredArgsConstructor public class PostCustomRepositoryImpl implements PostCustomRepository{ private final JPAQueryFactory queryFactory; private final EntityManager entityManager; @Override public Page<PostDto> searchPostList(PostSearchCondition condition, Pageable pageable) { List<PostDto> postDtoList = queryFactory.select( new QPostDto(post.id, post.title, post.content, post.created) ).where(titleEq(condition.getTitle()), contentEq(condition.getContent()) ) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .from(post).fetch(); JPAQuery<Long> countQuery = queryFactory .select(post.count()) .from(post) .where(titleEq(condition.getTitle()), contentEq(condition.getContent()) ); return PageableExecutionUtils.getPage(postDtoList, pageable, countQuery::fetchOne); } @Override public List<Post> findMyPost() { return entityManager.createQuery("SELECT p FROM Post AS p " , Post.class).getResultList(); } @Override public void deletePost(Post entity) { System.out.println("custom delete"); entityManager.remove(entity); } private BooleanExpression titleEq(String title) { return hasText(title) ? post.title.eq(title) :null; } private BooleanExpression contentEq(String content) { return hasText(content) ? post.content.eq(content) :null; } }
7)PostRespository
package com.example.demojap3.post; import org.springframework.data.jpa.repository.JpaRepository; public interface PostRespository extends JpaRepository<Post, Long> , PostCustomRepository { }
8) 테스트
package com.example.demojap3.post; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import java.util.Date; @SpringBootTest @Transactional @Rollback(value = false) class PostRepositoryTest { @Autowired PostRespository postRespository; @Test public void crud(){ Post post=new Post(); post.setTitle("hello1"); post.setContent("content1"); post.setCreated(new Date()); postRespository.save(post); Post post2=new Post(); post2.setTitle("hello1"); post2.setContent("content2"); post2.setCreated(new Date()); postRespository.save(post2); Post post3=new Post(); post3.setTitle("hello3"); post3.setContent("content3"); post3.setCreated(new Date()); postRespository.save(post3); PostSearchCondition condition =new PostSearchCondition(); condition.setTitle("hello1"); Page<PostDto> postList =postRespository.searchPostList(condition, PageRequest.of(0, 10)); System.out.println("postList.getTotalElements() = " + postList.getTotalElements()); postList.forEach(postDto -> System.out.println("postDto.toString() = " + postDto.toString())); Assertions.assertThat(postList.getNumberOfElements()).isEqualTo(2); } @Test public void findMyPostTest(){ System.out.println("custom findMyPostTest ===>"); postRespository.findMyPost(); } @Test public void crud2(){ Post post=new Post(); post.setTitle("hello1"); post.setContent("content1"); post.setCreated(new Date()); postRespository.save(post); postRespository.deletePost(post); postRespository.flush(); } }
★23.스프링 데이터 Common: 기본 리포지토리 커스터마이징
공통적으로 구현 클해스 MyRepository
강의 : https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13765&tab=curriculum
모든 리포지토리에 공통적으로 추가하고 싶은 기능이 있거나 덮어쓰고 싶은 기본 기능이 있다면
JpaRepository를 상속 받는 인터페이스 정의
@NoRepositoryBean
기본 구현체를 상속 받는 커스텀 구현체 만들기
@EnableJpaRepositories에 설정
repositoryBaseClass
@NoRepositoryBean public interface MyRepository<T, ID extends Serializable> extends JpaRepository<T, ID> { boolean contains(T entity); }
public class SimpleMyRepository<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements MyRepository<T, ID> { private EntityManager entityManager; public SimpleMyRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) { super(entityInformation, entityManager); this.entityManager = entityManager; } @Override public boolean contains(T entity) { return entityManager.contains(entity); } }
@EnableJpaRepositories(repositoryBaseClass = SimpleMyRepository.class)
public interface PostRepository extends MyRepository<Post, Long> { }
24.스프링 데이터 Common: 도메인 이벤트
강의 : https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13767&tab=curriculum
강의 내용 이해 안가면, 다음 내용을 보세요.
출처:
https://parkadd.tistory.com/141
모든 리포지토리에 공통적으로 추가하고 싶은 기능이 있거나 덮어쓰고 싶은 기본 기능이 있다면
JpaRepository를 상속 받는 인터페이스 정의
@NoRepositoryBean
기본 구현체를 상속 받는 커스텀 구현체 만들기
@EnableJpaRepositories에 설정
repositoryBaseClass
@NoRepositoryBean public interface MyRepository<T, ID extends Serializable> extends JpaRepository<T, ID> { boolean contains(T entity); }
public class SimpleMyRepository<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements MyRepository<T, ID> { private EntityManager entityManager; public SimpleMyRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) { super(entityInformation, entityManager); this.entityManager = entityManager; } @Override public boolean contains(T entity) { return entityManager.contains(entity); } }
@EnableJpaRepositories(repositoryBaseClass = SimpleMyRepository.class)
public interface PostRepository extends MyRepository<Post, Long> { }
Spring 에서 지원하는 이벤트 관련 기능
Spring은 이벤트 관련 기능을 지원해줍니다.
ApplicationEventPublisher - 이벤트 발행자
ApplicationEvent - 이벤트 객체
ApplicationListener - 이벤트 리스너
@EventListener
위의 기능을 활용하면 이벤트를 발행하고, 이벤트 리스너가 이벤트에 대한 처리를 담당할 수 있습니다.
먼저 간단하게 순서를 설명하면 아래와 같습니다.
1. ApplicationEvent(이벤트) 객체를 생성
2. 1에서 생성한 ApplicationEvent 객체를 ApplicationEventPublisher(이벤트 발행자)에게 발행을 요청.
3. 이벤트가 발행되면 ApplicationListener 가 이벤트를 처리
그럼 간단한 예시로 먼저 알아보겠습니다.
먼저 예시용 엔티티인 Post 클래스입니다. Post는 하나의 글을 의미합니다.
PostRepository도 함께 사용하겠습니다. (JpaRepository 사용)
@Entity public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Lob private String content; protected Post() { } public Post(String content) { this.content = content; } ... Getter } public interface PostRepository extends JpaRepository<Post, Long> { }
여기까지 예시에서 사용할 도메인 객체를 만들었습니다.
다음으로 Post의 이벤트 객체를 생성합니다.
Post의 발행(저장)할 때의 이벤트를 만듭니다.
public class PostPublishedEvent extends ApplicationEvent { private final Post post; public PostPublishedEvent(Object source) { super(source); this.post = (Post) source; } public Post getPost() { return post; } }
ApplicationEvent를 상속받으며 필드로 발행된 Post를 가집니다.
다음으로 이벤트를 받아서 처리하는 이벤트 리스너를 만들겠습니다.
이벤트 리스너는 두 가지 스타일로 만들 수 있습니다. (스프링 버전에 따라 애너테이션 방식은 안될 수 있습니다.)
- 생성한 Listener 클래스가 ApplicationListener 를 implements(구현) 한다.
- 이벤트 처리 메서드에 @EventListener 애너테이션을 추가한다.
아래 예시에서 두 가지 방법을 모두 보시고 마음에 드시는 방법을 사용하시면 됩니다!
단, 두 가지 방법 모두 빈으로 등록되어야 합니다. @Component 또는 설정 클래스(@Configuration)에서 빈으로 등록해주셔야 합니다.
@Component public class PostListener { @EventListener public void onApplicationEvent(PostPublishedEvent event) { System.out.println("======================"); System.out.println("Post 이벤트 발행, Post Id = " + event.getPost().getId() + ", Content = '" + event.getPost().getContent() + "'"); System.out.println("======================"); } }
Post의 이벤트 객체와 이벤트 리스너를 모두 만들었으니 이벤트를 발행하고 처리하는 테스트를 작성해보겠습니다.
@SpringBootTest class PostEventTest { @Autowired ApplicationEventPublisher applicationEventPublisher; @Test void eventListener() { Post post = new Post("hello world"); PostPublishedEvent event = new PostPublishedEvent(post); applicationEventPublisher.publishEvent(event); } } // 실행결과 // ====================== // Post 이벤트 발행, Post Id = null, Content = 'hello world' // ======================
여기까지 스프링에서 지원하는 이벤트 관련 기능 예시였습니다.
Spring Data 에서 지원하는 이벤트 자동 발행 기능
Spring Data에서는 Repository에서 save할 때 이벤트 자동 발행 기능을 제공합니다.
AbstractAggregateRoot를 사용해서 구현할 수 있습니다.
AbstractAggregateRoot를 사용하면 다음과 같은 것들이 가능합니다.
- Post 객체를 저장 시(save 메서드 호출 시) 이벤트를 발행, 처리할 수 있다.
- Post 객체에 이벤트 객체를 여러개 저장해두고 저장 시(save 메서드 호출 시) 여러개의 이벤트를 발행, 처리할 수 있다.
- 이벤트 발행 후 모아놨던 모든 이벤트를 제거한다. (메모리 누수 차단)
AbstractAggregateRoot의 내부에 대해서 좀 더 자세히 알아보겠습니다.
- List<Object> domainEvents - 이벤트 객체를 모아놓는 필드입니다.
- <T> T registerEvent(T event) - domainEevents 에 이벤트를 추가하고 추가한 이벤트를 반환합니다.
- A andEvent(Object event) - domainEevents 에 이벤트를 추가하고 현재 엔티티 객체(Aggregate)를 반환합니다.
- clearDomainEvents() - 이벤트 발행 후 모아놨던 모든 이벤트를 제거합니다.
- domainEvents() - 현재 쌓여있는 모든 이벤트(domainEvents)를 반환합니다.
이해가 가지 않는다면 예시와 함께 봐주세요!
엔티티 클래스가 AbstractAggregateRoot를 상속하도록 합니다.
@Entity public class Post extends AbstractAggregateRoot<Post> { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Lob private String content; protected Post() { } public Post(String content) { this.content = content; } // 이벤트 추가 후 자기 자신을 반환 public Post publish() { return this.andEvent(new PostPublishedEvent(this)); } ... Getter }
publish() 메서드를 보면 이벤트를 추가합니다.
이제 해당 Post 객체를 save() 메서드로 영속화할 때 이벤트가 발행됩니다.
@SpringBootTest class PostEventTest { @Autowired PostRepository postRepository; @Test void domainEvent1() { Post newPost = new Post("hello world"); postRepository.save(newPost.publish()); System.out.println("newPost 저장!!!"); Post newPost2 = new Post("hello charlie!!"); postRepository.save(newPost2.publish()); System.out.println("newPost2 저장!!!"); } } // 실행 결과 // ... insert 쿼리 생략 // ====================== // Post 이벤트 발행, Post Id = 1, Content = 'hello world' // ====================== // newPost 저장!!! // ... insert 쿼리 생략 // ====================== // Post 이벤트 발행, Post Id = 2, Content = 'hello charlie!!' // ====================== // newPost2 저장!!!
다음의 순서로 이벤트가 발행, 처리됩니다.
1. 이벤트 객체(PostPublishedEvent)를 생성, 엔티티 객체의 domainEvents 에 추가합니다. (Post의 publis() 메서드에서 일어남)
2. Repository에서 해당 엔티티 객체로 save 메서드를 실행할 때, domainEvents의 모든 이벤트 발행(정확히는 데이터가 영속화 된 후)
3. EventListener(PostListener) 에서 이벤트를 처리.
그럼 여러개의 이벤트를 추가해놓으면 Repository의 save 메서드 호출시 한번에 이벤트가 발행될까요?
@Test void domainEvent2() { Post newPost = new Post("hello world"); newPost.addPublishEvent(); newPost.addPublishEvent(); System.out.println("newPost save 메서드 호출!!!"); postRepository.save(newPost); System.out.println("newPost 저장!!!"); } // 실행결과 // ... insert 쿼리 생략 // ====================== // Post 이벤트 발행, Post Id = 1, Content = 'hello world' // ====================== // ====================== // Post 이벤트 발행, Post Id = 1, Content = 'hello world' // ====================== // newPost 저장!!!
위 예시처럼 여러개의 이벤트를 넣어 놓고 save를 호출하면 여러개의 이벤트가 발행됩니다.
25.스프링 데이터 Common 10.QueryDSL 연동
강의 : https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=13768&tab=curriculum
다음 참조 :
22.번 볼것
findByFirstNameIngoreCaseAndLastNameStartsWithIgnoreCase(String firstName, String lastName)
이게 이게 뭐냐... @_@ 어지러우시죠?? 이 정도 되면 그냥 한글로 주석을 달아 두시는게...
여러 쿼리 메소드는 대부분 두 가지 중 하나.
Optional<T> findOne(Predicate): 이런 저런 조건으로 무언가 하나를 찾는다.
List<T>|Page<T>|.. findAll(Predicate): 이런 저런 조건으로 무언가 여러개를 찾는다.
QuerydslPredicateExecutor 인터페이스
QueryDSL
http://www.querydsl.com/
타입 세이프한 쿼리 만들 수 있게 도와주는 라이브러리
JPA, SQL, MongoDB, JDO, Lucene, Collection 지원
QueryDSL JPA 연동 가이드
스프링 데이터 JPA + QueryDSL
인터페이스: QuerydslPredicateExecutor<T>
구현체: QuerydslPredicateExecutor<T>
연동 방법
기본 리포지토리 커스터마이징 안 했을 때. (쉬움)
기본 리포지토리 커스타마이징 했을 때. (해맬 수 있으나... 제가 있잖습니까)
의존성 추가
SimpleMyRepository
package com.example.demojap3; import jakarta.persistence.EntityManager; import org.springframework.data.jpa.repository.support.JpaEntityInformation; import org.springframework.data.jpa.repository.support.QuerydslJpaRepository; import java.io.Serializable; public class SimpleMyRepository<T, ID extends Serializable> extends QuerydslJpaRepository<T, ID> implements MyRepository<T, ID> { private EntityManager entityManager; public SimpleMyRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) { super((JpaEntityInformation<T, ID>) entityInformation, entityManager); this.entityManager=entityManager; } @Override public boolean contains(T entity){ return entityManager.contains(entity); } }
package com.example.demojap3.post; import com.example.demojap3.MyRepository; import org.springframework.data.querydsl.QuerydslPredicateExecutor; public interface PostRepository extends MyRepository<Post, Long>, PostCustomRepository, QuerydslPredicateExecutor<Post>{ }
26.QueryDSL 연동 보강
강의 : https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&unitId=38371&tab=curriculum
다음 강의 를 볼것
https://macaronics.net/index.php/user/search/lists/s/%EC%8B%A4%EC%A0%84%21%20Querydsl/page/1
댓글 ( 4)
댓글 남기기