스프링

 

 

중급자를 위해 준비한
[웹 개발, 백엔드] 강의입니다.

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

 

 

 

  1. 강좌 소개

 

Application -> 스프링 데이터 JPA (-> JPA -> JDBC) -> Database

 

 

 

  1. 강사 소개

 

백기선

 

마이크로소프트(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

 

 

참조 :

 

실전! * Querydsl - 7. 스프링 데이터 JPA와 Querydsl,스프링 데이터 JPA 리포지토리로 변경,사용자 정의 리포지토리, Querydsl 페이징 연동,CountQuery 최적화 ,컨트롤러 개발

 

https://macaronics.net/index.php/m01/spring/view/2104

 

 

쿼리 메소드(쿼리 생성과 쿼리 찾아쓰기)로 해결이 되지 않는 경우 직접 코딩으로 구현 가능.

  • 스프링 데이터 리포지토리 인터페이스에 기능 추가.

  • 스프링 데이터 리포지토리 기본 기능 덮어쓰기 가능.

  • 구현 방법

    1. 커스텀 리포지토리 인터페이스 정의 

    2. 인터페이스 구현 클래스 만들기 (기본 접미어는 Impl)

    3. 엔티티 리포지토리에 커스텀 리포지토리 인터페이스 추가

 

기능 추가하기

 

기본 기능 덮어쓰기

 

접미어 설정하기

 

 

 

 

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

 

 

 

모든 리포지토리에 공통적으로 추가하고 싶은 기능이 있거나 덮어쓰고 싶은 기본 기능이 있다면 

 

  1. JpaRepository를 상속 받는 인터페이스 정의

    • @NoRepositoryBean

  2. 기본 구현체를 상속 받는 커스텀 구현체 만들기

  3. @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

 

 

 

 

다음 참조 :

실전! *  Querydsl -8.스프링 데이터 JPA가 제공하는 Querydsl 기능, ★4.3 스프링 부트 2.6 이상, Querydsl 5.0 지원 방법 , Querydsl4RepositorySupport 사용 코드

 

 

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

착한 사람과 함께 있으면 마치 지란(芝蘭)의 방에 들어간 것 같아서 오래 되면 그 향기를 느끼지 못하니 더불어 그에게 동화된 것이다. 착하지 않은 사람과 함께 있으면 마치 절인 생선가게에 들어간 듯하여 오래 되면 그 냄새를 느끼지 못하니 또한 더불어 동화된 것이다. 단(丹)을 지니면 붉어지고, 칠을 지니면 검어지니 군자는 반드시 자기와 함께 있는 자를 삼가

댓글 ( 4)

댓글 남기기

작성